Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added behavior to allow sharing objects across clients users #2004

Merged
merged 17 commits into from
Jun 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changelog
2.2.0 (unreleased)
------------------

- #2004 Added behavior to allow sharing objects across clients users
- #2001 Fix Traceback when rendering UIDReferenceWidget with limited privileges
- #1999 Allow multi-choice/multiselect interim fields in calculations
- #1998 Fix analisys hidden status erases when submit through worksheet
Expand Down
8 changes: 8 additions & 0 deletions src/bika/lims/profiles/default/rolemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
<role name="SamplingCoordinator"/>
<role name="Verifier"/>

<!-- Never assign this role by default to users. This role is assigned
locally to clients users with whom the current object has been shared with
-->
<role name="ClientGuest"/>

<!-- PLONE ROLES -->
<role name="Anonymous"/>
<role name="Authenticated"/>
Expand All @@ -33,6 +38,7 @@
<permission name="View" acquire="True">
<role name="Analyst"/>
<role name="Client"/>
<role name="ClientGuest"/>
<role name="LabClerk"/>
<role name="LabManager"/>
<role name="Manager"/>
Expand All @@ -47,6 +53,7 @@
<permission name="Access contents information" acquire="True">
<role name="Analyst"/>
<role name="Client"/>
<role name="ClientGuest"/>
<role name="LabClerk"/>
<role name="LabManager"/>
<role name="Manager"/>
Expand All @@ -61,6 +68,7 @@
<permission name="List folder contents" acquire="True">
<role name="Analyst"/>
<role name="Client"/>
<role name="ClientGuest"/>
<role name="LabClerk"/>
<role name="LabManager"/>
<role name="Manager"/>
Expand Down
8 changes: 8 additions & 0 deletions src/senaite/core/adapters/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,12 @@
factory=".localroles.ClientAwareLocalRoles"
name="senaite.core.adapter.localroles.clientaware" />

<!-- Adapter for the dynamic assignment of local and client-specific roles
for contents shared across clients -->
<adapter
for="senaite.core.behaviors.IClientShareable"
provides="senaite.core.interfaces.IDynamicLocalRoles"
factory=".localroles.ClientShareableLocalRoles"
name="senaite.core.adapter.localroles.clientshareable" />

</configure>
42 changes: 42 additions & 0 deletions src/senaite/core/adapters/localroles.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from bika.lims.utils import get_client
from borg.localrole.default_adapter import DefaultLocalRoleAdapter
from plone.memoize import ram
from senaite.core.behaviors import IClientShareableBehavior
from senaite.core.interfaces import IDynamicLocalRoles
from zope.component import getAdapters
from zope.interface import implementer
Expand All @@ -32,8 +33,12 @@
def _getRolesInContext_cachekey(method, self, context, principal_id):
"""Function that generates the key for volatile caching
"""
# We need the cachekey to change when global roles of a given user change
user = api.get_user(principal_id)
roles = user and ":".join(sorted(user.getRoles())) or ""
return ".".join([
principal_id,
roles,
api.get_path(context),
api.get_modification_date(context).ISO(),
])
Expand All @@ -53,6 +58,10 @@ def getRolesInContext(self, context, principal_id):
@param principal_id: User login id
@return List of dynamically calculated local-roles for user and context
"""
if not api.get_user(principal_id):
# principal_id can be a group name, but we consider users only
return []

roles = set()
path = api.get_path(context)
adapters = getAdapters((context,), IDynamicLocalRoles)
Expand Down Expand Up @@ -126,3 +135,36 @@ def getRoles(self, principal_id):
return []

return ["Owner"]


@implementer(IDynamicLocalRoles)
class ClientShareableLocalRoles(object):
"""Adapter for the assignment of roles for content shared across clients
"""

def __init__(self, context):
self.context = context

def getRoles(self, principal_id):
"""Returns ["ClientGuest"] local role if the current context is
shareable across clients and the user for the principal_id belongs to
one of the clients for which the context can be shared
"""
# Get the clients this context is shared with
behavior = IClientShareableBehavior(self.context)
clients = filter(api.is_uid, behavior.getRawClients())
if not clients:
return []

# Check if the user belongs to at least one of the clients
# this context is shared with
query = {
"portal_type": "Contact",
"getUsername": principal_id,
"getParentUID": clients,
}
brains = api.search(query, catalog="portal_catalog")
if len(brains) == 0:
return []

return ["ClientGuest"]
24 changes: 24 additions & 0 deletions src/senaite/core/behaviors/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
#
# This file is part of SENAITE.CORE.
#
# SENAITE.CORE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, version 2.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2018-2022 by it's authors.
# Some rights reserved, see README and LICENSE.

# Convenient imports
from clientshareable import IClientShareable
from clientshareable import IClientShareableBehavior
from clientshareable import IClientShareableMarker
153 changes: 153 additions & 0 deletions src/senaite/core/behaviors/clientshareable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# -*- coding: utf-8 -*-
#
# This file is part of SENAITE.CORE.
#
# SENAITE.CORE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, version 2.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2018-2022 by it's authors.
# Some rights reserved, see README and LICENSE.

from AccessControl import ClassSecurityInfo
from bika.lims import senaiteMessageFactory as _
from plone.autoform import directives
from plone.autoform.interfaces import IFormFieldProvider
from plone.behavior.interfaces import IBehavior
from plone.supermodel import model
from plone.supermodel.directives import fieldset
from Products.CMFCore import permissions
from senaite.core.behaviors.utils import get_behavior_schema
from senaite.core.schema import UIDReferenceField
from senaite.core.z3cform.widgets.uidreference import UIDReferenceWidgetFactory
from zope.interface import implementer
from zope.interface import Interface
from zope.interface import provider


class IClientShareable(Interface):
"""Marker interface to implement by types for which ClientShareableBehavior
can be applied
"""
pass


class IClientShareableMarker(Interface):
"""Marker interface provided by objects with ClientShareableBehavior
"""
pass


@provider(IFormFieldProvider)
class IClientShareableBehavior(model.Schema):
"""Behavior with schema fields to allow to share the context with users
that belong to other clients
"""

clients = UIDReferenceField(
title=_(u"Clients"),
description=_(
u"Clients with whom this content will be shared across. This "
u"content will become available on searches to users that belong "
u"to any of the selected clients thanks to the role 'ClientGuest'"
),
allowed_types=("Client", ),
multi_valued=True,
required=False,
)

directives.widget(
"clients",
UIDReferenceWidgetFactory,
catalog="portal_catalog",
query={
"portal_type": "Client",
"is_active": True,
"sort_on": "title",
"sort_order": "ascending",
},
display_template="<a href='${url}'>${title}</a>",
columns=[
{
"name": "title",
"width": "30",
"align": "left",
"label": _(u"Title"),
}, {
"name": "description",
"width": "70",
"align": "left",
"label": _(u"Description"),
},
],
limit=15,
)

fieldset(
"clientshareable",
label=u"Client share",
fields=["clients"],
)


@implementer(IBehavior, IClientShareableBehavior)
class ClientShareableFactory(object):
"""Factory that provides IClientShareableBehavior"""

security = ClassSecurityInfo()

def __init__(self, context):
self.context = context
self._schema = None

@property
def schema(self):
"""Return the schema provided by the underlying behavior
"""
if self._schema is None:
behavior = IClientShareableBehavior
self._schema = get_behavior_schema(self.context, behavior)
return self._schema

def accessor(self, fieldname, raw=False):
"""Return the field accessor for the fieldname
"""
if fieldname in self.schema:
field = self.schema[fieldname]
if raw:
return field.get_raw
return field.get
return None

def mutator(self, fieldname):
"""Return the field mutator for the fieldname
"""
if fieldname in self.schema:
return self.schema[fieldname].set
return None

@security.protected(permissions.View)
def getClients(self):
accessor = self.accessor("clients")
return accessor(self.context)

@security.protected(permissions.View)
def getRawClients(self):
accessor = self.accessor("clients", raw=True)
return accessor(self.context)

@security.protected(permissions.ModifyPortalContent)
def setClients(self, value):
mutator = self.mutator("clients")
mutator(self.context, value)

clients = property(getClients, setClients)
25 changes: 25 additions & 0 deletions src/senaite/core/behaviors/configure.zcml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:plone="http://namespaces.plone.org/plone"
i18n_domain="senaite.core">

<include package="plone.behavior" file="meta.zcml"/>

<!-- See https://pypi.org/project/plone.behavior/#zcml-examples -->

<!--
ClientShareableBehavior
- with `behavior = IClientShareableBehavior(context)` a ClientShareableFactory is returned
- `context` provides IClientShareableMarker
- `ClientShareableFactory` provides `IClientShareableBehavior`
-->
<plone:behavior
title="ClientShareableBehavior"
description="Adds the ability to share a content across clients"
name="senaite.core.behavior.clientshareable"
provides=".IClientShareableBehavior"
marker=".IClientShareableMarker"
factory=".clientshareable.ClientShareableFactory"
for=".IClientShareable"/>

</configure>
33 changes: 33 additions & 0 deletions src/senaite/core/behaviors/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
#
# This file is part of SENAITE.CORE.
#
# SENAITE.CORE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, version 2.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2018-2022 by it's authors.
# Some rights reserved, see README and LICENSE.


from plone.dexterity.utils import getAdditionalSchemata


def get_behavior_schema(context, behavior):
"""Returns the schema of the context that is provided by the behavior
interface passed-in, if any
"""
schemata = getAdditionalSchemata(context=context)
for sch in schemata:
if sch.isOrExtends(behavior):
return sch
raise TypeError("No behavior schema found")
1 change: 1 addition & 0 deletions src/senaite/core/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

<!-- package includes -->
<include package=".adapters" />
<include package=".behaviors" />
<include package=".browser" />
<include package=".catalog" />
<include package=".datamanagers" />
Expand Down
Loading