From 1a39deb6066b4cd00681916f161c9181dcaa3184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Sun, 5 Jun 2022 16:36:20 +0200 Subject: [PATCH 1/7] Allow client users to create patients --- .../patient/profiles/default/rolemap.xml | 9 +++++++- .../definition.xml | 13 +++++++++++ src/senaite/patient/upgrade/v01_01_000.py | 22 +++++++++++++++++-- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/senaite/patient/profiles/default/rolemap.xml b/src/senaite/patient/profiles/default/rolemap.xml index 1a30327..cecdbfe 100644 --- a/src/senaite/patient/profiles/default/rolemap.xml +++ b/src/senaite/patient/profiles/default/rolemap.xml @@ -24,28 +24,35 @@ - + + + + + + diff --git a/src/senaite/patient/profiles/default/workflows/senaite_patient_folder_workflow/definition.xml b/src/senaite/patient/profiles/default/workflows/senaite_patient_folder_workflow/definition.xml index bd08a02..7951b09 100644 --- a/src/senaite/patient/profiles/default/workflows/senaite_patient_folder_workflow/definition.xml +++ b/src/senaite/patient/profiles/default/workflows/senaite_patient_folder_workflow/definition.xml @@ -9,6 +9,7 @@ i18n:domain="senaite.patient"> + Add portal content Access contents information Delete objects List folder contents @@ -29,6 +30,7 @@ SamplingCoordinator Manager + Owner Site Administrator @@ -45,13 +47,23 @@ Manager Site Administrator + + Client + LabClerk + LabManager + + Manager + LabClerk LabManager + Manager + Owner + Client Analyst LabClerk LabManager @@ -61,6 +73,7 @@ SamplingCoordinator Manager + Owner Site Administrator diff --git a/src/senaite/patient/upgrade/v01_01_000.py b/src/senaite/patient/upgrade/v01_01_000.py index e92b562..642ed40 100644 --- a/src/senaite/patient/upgrade/v01_01_000.py +++ b/src/senaite/patient/upgrade/v01_01_000.py @@ -48,6 +48,7 @@ def upgrade(tool): version)) # -------- ADD YOUR STUFF BELOW -------- + setup.runImportStepFromProfile(profile, "rolemap") setup.runImportStepFromProfile(profile, "controlpanel") setup.runImportStepFromProfile(profile, "plone.app.registry") setup.runImportStepFromProfile(profile, "workflow") @@ -65,7 +66,10 @@ def upgrade(tool): migrate_birthdates(portal) # do not allow access to patients root folder other than lab personnel - update_patient_role_mappings(portal) + update_patient_folder_role_mappings(portal) + + # allow client-specific and local roles on patient objects + update_patients_role_mappings(portal) logger.info("{0} upgraded to version {1}".format(PRODUCT_NAME, version)) return True @@ -167,7 +171,7 @@ def migrate_birthdates(portal): logger.info("Migrate patient birthdate timezones [DONE]") -def update_patient_role_mappings(portal): +def update_patient_folder_role_mappings(portal): """Updates the role mappings of patients root folder to prevent access to non-laboratory users """ @@ -179,3 +183,17 @@ def update_patient_role_mappings(portal): workflow.updateRoleMappingsFor(folder) folder.reindexObjectSecurity() logger.info("Fixing permissions for patient's root folder [DONE]") + + +def update_patients_role_mappings(portal): + """Updates the role mappings of patient objects to allow client-specific + and local roles to have access + """ + logger.info("Fixing permissions for patients ...") + wf_tool = api.get_tool("portal_workflow") + wf_id = "senaite_patient_workflow" + workflow = wf_tool.getWorkflowById(wf_id) + for patient in portal.patients.objectValues(): + workflow.updateRoleMappingsFor(patient) + patient.reindexObjectSecurity() + logger.info("Fixing permissions for patients [DONE]") From 30a6168db6ef98f70a5caf745a54bdccb8cc0af8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Sun, 5 Jun 2022 16:54:31 +0200 Subject: [PATCH 2/7] Changelog --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index fa37d90..84c3f07 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog 1.1.0 (unreleased) ------------------ +- #37 Allow client users to create samples and patients - #36 Do not display Patient root folder to users other than lab contact - #35 Integrate new DX address field for patients - #34 Fix UnicodeDecodeError when storing Patients with special characters From e0ca6753d98e573c85cb2c5aa20bdb451d06e12d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Sun, 5 Jun 2022 17:49:37 +0200 Subject: [PATCH 3/7] Add ClientShareable behavior to Patient type --- src/senaite/patient/content/patient.py | 8 ++++---- src/senaite/patient/profiles/default/types/Patient.xml | 1 + src/senaite/patient/upgrade/v01_01_000.py | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/senaite/patient/content/patient.py b/src/senaite/patient/content/patient.py index 07a5551..6f04e80 100644 --- a/src/senaite/patient/content/patient.py +++ b/src/senaite/patient/content/patient.py @@ -18,8 +18,6 @@ # Copyright 2020-2022 by it's authors. # Some rights reserved, see README and LICENSE. -from six import string_types - from AccessControl import ClassSecurityInfo from bika.lims import api from bika.lims.api.mail import is_valid_email_address @@ -28,6 +26,7 @@ from plone.supermodel import model from plone.supermodel.directives import fieldset from Products.CMFCore import permissions +from senaite.core.behaviors import IClientShareable from senaite.core.schema import AddressField from senaite.core.schema import DatetimeField from senaite.core.schema.addressfield import OTHER_ADDRESS @@ -42,10 +41,11 @@ from senaite.patient.catalog import PATIENT_CATALOG from senaite.patient.config import GENDERS from senaite.patient.interfaces import IPatient +from six import string_types from zope import schema +from zope.interface import implementer from zope.interface import Interface from zope.interface import Invalid -from zope.interface import implementer from zope.interface import invariant @@ -280,7 +280,7 @@ def validate_email(data): raise Invalid(_("Patient email is invalid")) -@implementer(IPatient, IPatientSchema) +@implementer(IPatient, IPatientSchema, IClientShareable) class Patient(Container): """Results Interpretation Template content """ diff --git a/src/senaite/patient/profiles/default/types/Patient.xml b/src/senaite/patient/profiles/default/types/Patient.xml index 3ec02be..4055f82 100644 --- a/src/senaite/patient/profiles/default/types/Patient.xml +++ b/src/senaite/patient/profiles/default/types/Patient.xml @@ -52,6 +52,7 @@ + diff --git a/src/senaite/patient/upgrade/v01_01_000.py b/src/senaite/patient/upgrade/v01_01_000.py index 642ed40..050f2af 100644 --- a/src/senaite/patient/upgrade/v01_01_000.py +++ b/src/senaite/patient/upgrade/v01_01_000.py @@ -48,6 +48,7 @@ def upgrade(tool): version)) # -------- ADD YOUR STUFF BELOW -------- + setup.runImportStepFromProfile(profile, "typeinfo") setup.runImportStepFromProfile(profile, "rolemap") setup.runImportStepFromProfile(profile, "controlpanel") setup.runImportStepFromProfile(profile, "plone.app.registry") From ec58e41625035702298bbf52287c0b939269e6bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Sun, 5 Jun 2022 18:19:14 +0200 Subject: [PATCH 4/7] Ensure Client does not have access to Patients from others --- .../definition.xml | 2 +- .../senaite_patient_workflow/definition.xml | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/senaite/patient/profiles/default/workflows/senaite_patient_folder_workflow/definition.xml b/src/senaite/patient/profiles/default/workflows/senaite_patient_folder_workflow/definition.xml index 7951b09..42d6039 100644 --- a/src/senaite/patient/profiles/default/workflows/senaite_patient_folder_workflow/definition.xml +++ b/src/senaite/patient/profiles/default/workflows/senaite_patient_folder_workflow/definition.xml @@ -62,7 +62,7 @@ Owner - + Client Analyst LabClerk diff --git a/src/senaite/patient/profiles/default/workflows/senaite_patient_workflow/definition.xml b/src/senaite/patient/profiles/default/workflows/senaite_patient_workflow/definition.xml index 79057fd..c81f44a 100644 --- a/src/senaite/patient/profiles/default/workflows/senaite_patient_workflow/definition.xml +++ b/src/senaite/patient/profiles/default/workflows/senaite_patient_workflow/definition.xml @@ -44,7 +44,21 @@ - + + + + Analyst + LabClerk + LabManager + Preserver + RegulatoryInspector + Sampler + SamplingCoordinator + + Manager + Owner + Site Administrator + From 6e976b9431ddd0fd8cb74d1023f0a3494c5edf4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Sun, 5 Jun 2022 19:26:10 +0200 Subject: [PATCH 5/7] Allow to automatically share patients on sample creation --- src/senaite/patient/browser/controlpanel.py | 7 +++++++ .../senaite_patient_folder_workflow/definition.xml | 5 ++++- .../senaite_patient_workflow/definition.xml | 1 + src/senaite/patient/subscribers/analysisrequest.py | 14 ++++++++++++++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/senaite/patient/browser/controlpanel.py b/src/senaite/patient/browser/controlpanel.py index f336706..da1af5b 100644 --- a/src/senaite/patient/browser/controlpanel.py +++ b/src/senaite/patient/browser/controlpanel.py @@ -112,6 +112,13 @@ class IPatientControlPanel(Interface): default=False, ) + share_patients = schema.Bool( + title=_(u"Share patient on sample creation"), + description=_(u"If selected, patients created or referred on sample " + u"creation will automatically be shared across users " + u"from same client the sample belongs to") + ) + @invariant def validate_identifiers(data): """Checks if the keyword is unique and valid diff --git a/src/senaite/patient/profiles/default/workflows/senaite_patient_folder_workflow/definition.xml b/src/senaite/patient/profiles/default/workflows/senaite_patient_folder_workflow/definition.xml index 42d6039..b3456ad 100644 --- a/src/senaite/patient/profiles/default/workflows/senaite_patient_folder_workflow/definition.xml +++ b/src/senaite/patient/profiles/default/workflows/senaite_patient_folder_workflow/definition.xml @@ -22,6 +22,7 @@ Analyst + ClientGuest LabClerk LabManager Preserver @@ -37,6 +38,7 @@ Analyst + ClientGuest LabClerk LabManager Preserver @@ -63,8 +65,9 @@ - Client Analyst + Client + ClientGuest LabClerk LabManager Preserver diff --git a/src/senaite/patient/profiles/default/workflows/senaite_patient_workflow/definition.xml b/src/senaite/patient/profiles/default/workflows/senaite_patient_workflow/definition.xml index c81f44a..e31de06 100644 --- a/src/senaite/patient/profiles/default/workflows/senaite_patient_workflow/definition.xml +++ b/src/senaite/patient/profiles/default/workflows/senaite_patient_workflow/definition.xml @@ -48,6 +48,7 @@ Analyst + ClientGuest LabClerk LabManager Preserver diff --git a/src/senaite/patient/subscribers/analysisrequest.py b/src/senaite/patient/subscribers/analysisrequest.py index 8f5356a..70c2be6 100644 --- a/src/senaite/patient/subscribers/analysisrequest.py +++ b/src/senaite/patient/subscribers/analysisrequest.py @@ -19,6 +19,7 @@ # Some rights reserved, see README and LICENSE. from bika.lims import api +from senaite.core.behaviors import IClientShareableBehavior from senaite.patient import api as patient_api from senaite.patient import check_installed from senaite.patient import logger @@ -39,6 +40,19 @@ def on_object_created(instance, event): email = patient.getEmail() add_cc_email(instance, email) + # share patient with sample's client users if necessary + reg_key = "senaite.patient.share_patients" + if api.get_registry_record(reg_key, default=False): + client_uid = api.get_uid(instance.getClient()) + behavior = IClientShareableBehavior(patient) + # Note we get Raw clients because if current user is a Client, she/he + # does not have enough privileges to wake-up clients other than the one + # she/he belongs to. Still, we need to keep the rest of shared clients + client_uids = behavior.getRawClients() or [] + if client_uid not in client_uids: + client_uids.append(client_uid) + behavior.setClients(client_uids) + @check_installed(None) def on_object_edited(instance, event): From 9c7f4ef9508104bef6f9ba3a6f813661c4649a23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Wed, 8 Jun 2022 10:05:40 +0200 Subject: [PATCH 6/7] Fix tests --- src/senaite/patient/browser/patientfolder.py | 14 ++++++- src/senaite/patient/tests/base.py | 2 +- .../tests/doctests/PatientWorkflow.rst | 42 +++++++++---------- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/src/senaite/patient/browser/patientfolder.py b/src/senaite/patient/browser/patientfolder.py index 0d5e136..a882c71 100644 --- a/src/senaite/patient/browser/patientfolder.py +++ b/src/senaite/patient/browser/patientfolder.py @@ -19,16 +19,17 @@ # Some rights reserved, see README and LICENSE. import collections - from bika.lims import api from bika.lims import senaiteMessageFactory as _ from bika.lims.utils import get_email_link from bika.lims.utils import get_link +from bika.lims.utils import get_link_for from senaite.app.listing.view import ListingView +from senaite.core.behaviors import IClientShareableBehavior from senaite.patient import messageFactory as _sp -from senaite.patient.catalog import PATIENT_CATALOG from senaite.patient.api import to_identifier_type_name from senaite.patient.api import tuplify_identifiers +from senaite.patient.catalog import PATIENT_CATALOG class PatientFolderView(ListingView): @@ -88,6 +89,9 @@ def __init__(self, context, request): ("birthdate", { "title": _("Birthdate"), "index": "patient_birthdate"}), + ("shared_with", { + "title": _("Shared with"), + }) )) self.review_states = [ @@ -194,4 +198,10 @@ def folderitem(self, obj, item, index): if birthdate: item["birthdate"] = self.ulocalized_time(birthdate, long_format=0) + # Shared with + behavior = IClientShareableBehavior(obj) + clients = filter(None, behavior.getClients()) + clients = [get_link_for(client) for client in clients] + item["replace"]["shared_with"] = ", ".join(clients) + return item diff --git a/src/senaite/patient/tests/base.py b/src/senaite/patient/tests/base.py index 58ac4b2..22c4653 100644 --- a/src/senaite/patient/tests/base.py +++ b/src/senaite/patient/tests/base.py @@ -76,7 +76,7 @@ def setUpPloneSite(self, portal): SIMPLE_FIXTURE = SimpleTestLayer() SIMPLE_TESTING = FunctionalTesting( bases=(SIMPLE_FIXTURE, ), - name="senaite.storage:SimpleTesting" + name="senaite.patient:SimpleTesting" ) diff --git a/src/senaite/patient/tests/doctests/PatientWorkflow.rst b/src/senaite/patient/tests/doctests/PatientWorkflow.rst index bd9f508..c48224f 100644 --- a/src/senaite/patient/tests/doctests/PatientWorkflow.rst +++ b/src/senaite/patient/tests/doctests/PatientWorkflow.rst @@ -75,7 +75,7 @@ Get the mapped workflow and status of the patient folder: >>> patients = portal.patients >>> api.get_workflows_for(patients) - ('senaite_one_state_workflow',) + ('senaite_patient_folder_workflow',) >>> api.get_workflow_status_of(patients) 'active' @@ -117,23 +117,23 @@ Field permission in **active** state: >>> from senaite.patient.permissions import FieldEditMRN >>> get_roles_for_permission(FieldEditMRN, patient) - ['LabClerk', 'LabManager', 'Manager'] + ['LabClerk', 'LabManager', 'Manager', 'Owner'] >>> from senaite.patient.permissions import FieldEditFullName >>> get_roles_for_permission(FieldEditFullName, patient) - ['LabClerk', 'LabManager', 'Manager'] + ['LabClerk', 'LabManager', 'Manager', 'Owner'] >>> from senaite.patient.permissions import FieldEditGender >>> get_roles_for_permission(FieldEditGender, patient) - ['LabClerk', 'LabManager', 'Manager'] + ['LabClerk', 'LabManager', 'Manager', 'Owner'] >>> from senaite.patient.permissions import FieldEditDateOfBirth >>> get_roles_for_permission(FieldEditDateOfBirth, patient) - ['LabClerk', 'LabManager', 'Manager'] + ['LabClerk', 'LabManager', 'Manager', 'Owner'] >>> from senaite.patient.permissions import FieldEditAddress >>> get_roles_for_permission(FieldEditAddress, patient) - ['LabClerk', 'LabManager', 'Manager'] + ['LabClerk', 'LabManager', 'Manager', 'Owner'] Deactivating the patient @@ -183,23 +183,23 @@ All patient fields are editable in `sample_due`: >>> from senaite.patient.permissions import FieldEditMRN >>> get_roles_for_permission(FieldEditMRN, sample) - ['LabClerk', 'LabManager', 'Manager'] + ['LabClerk', 'LabManager', 'Manager', 'Owner'] >>> from senaite.patient.permissions import FieldEditFullName >>> get_roles_for_permission(FieldEditFullName, sample) - ['LabClerk', 'LabManager', 'Manager'] + ['LabClerk', 'LabManager', 'Manager', 'Owner'] >>> from senaite.patient.permissions import FieldEditGender >>> get_roles_for_permission(FieldEditGender, sample) - ['LabClerk', 'LabManager', 'Manager'] + ['LabClerk', 'LabManager', 'Manager', 'Owner'] >>> from senaite.patient.permissions import FieldEditDateOfBirth >>> get_roles_for_permission(FieldEditDateOfBirth, sample) - ['LabClerk', 'LabManager', 'Manager'] + ['LabClerk', 'LabManager', 'Manager', 'Owner'] >>> from senaite.patient.permissions import FieldEditAddress >>> get_roles_for_permission(FieldEditAddress, sample) - ['LabClerk', 'LabManager', 'Manager'] + ['LabClerk', 'LabManager', 'Manager', 'Owner'] Receive the sample: @@ -211,23 +211,23 @@ All patient fields are editable in `sample_received`: >>> from senaite.patient.permissions import FieldEditMRN >>> get_roles_for_permission(FieldEditMRN, sample) - ['LabClerk', 'LabManager', 'Manager'] + ['LabClerk', 'LabManager', 'Manager', 'Owner'] >>> from senaite.patient.permissions import FieldEditFullName >>> get_roles_for_permission(FieldEditFullName, sample) - ['LabClerk', 'LabManager', 'Manager'] + ['LabClerk', 'LabManager', 'Manager', 'Owner'] >>> from senaite.patient.permissions import FieldEditGender >>> get_roles_for_permission(FieldEditGender, sample) - ['LabClerk', 'LabManager', 'Manager'] + ['LabClerk', 'LabManager', 'Manager', 'Owner'] >>> from senaite.patient.permissions import FieldEditDateOfBirth >>> get_roles_for_permission(FieldEditDateOfBirth, sample) - ['LabClerk', 'LabManager', 'Manager'] + ['LabClerk', 'LabManager', 'Manager', 'Owner'] >>> from senaite.patient.permissions import FieldEditAddress >>> get_roles_for_permission(FieldEditAddress, sample) - ['LabClerk', 'LabManager', 'Manager'] + ['LabClerk', 'LabManager', 'Manager', 'Owner'] Set results and submit: @@ -248,23 +248,23 @@ All patient fields are editable in `to_be_verified`: >>> from senaite.patient.permissions import FieldEditMRN >>> get_roles_for_permission(FieldEditMRN, sample) - ['LabClerk', 'LabManager', 'Manager'] + ['LabClerk', 'LabManager', 'Manager', 'Owner'] >>> from senaite.patient.permissions import FieldEditFullName >>> get_roles_for_permission(FieldEditFullName, sample) - ['LabClerk', 'LabManager', 'Manager'] + ['LabClerk', 'LabManager', 'Manager', 'Owner'] >>> from senaite.patient.permissions import FieldEditGender >>> get_roles_for_permission(FieldEditGender, sample) - ['LabClerk', 'LabManager', 'Manager'] + ['LabClerk', 'LabManager', 'Manager', 'Owner'] >>> from senaite.patient.permissions import FieldEditDateOfBirth >>> get_roles_for_permission(FieldEditDateOfBirth, sample) - ['LabClerk', 'LabManager', 'Manager'] + ['LabClerk', 'LabManager', 'Manager', 'Owner'] >>> from senaite.patient.permissions import FieldEditAddress >>> get_roles_for_permission(FieldEditAddress, sample) - ['LabClerk', 'LabManager', 'Manager'] + ['LabClerk', 'LabManager', 'Manager', 'Owner'] Verify the results: From 9fdfb2181e525eec3aa89f03ffd0441ab7eedaf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Wed, 8 Jun 2022 10:09:36 +0200 Subject: [PATCH 7/7] Undo changes in patient folder listing --- src/senaite/patient/browser/patientfolder.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/senaite/patient/browser/patientfolder.py b/src/senaite/patient/browser/patientfolder.py index a882c71..0d5e136 100644 --- a/src/senaite/patient/browser/patientfolder.py +++ b/src/senaite/patient/browser/patientfolder.py @@ -19,17 +19,16 @@ # Some rights reserved, see README and LICENSE. import collections + from bika.lims import api from bika.lims import senaiteMessageFactory as _ from bika.lims.utils import get_email_link from bika.lims.utils import get_link -from bika.lims.utils import get_link_for from senaite.app.listing.view import ListingView -from senaite.core.behaviors import IClientShareableBehavior from senaite.patient import messageFactory as _sp +from senaite.patient.catalog import PATIENT_CATALOG from senaite.patient.api import to_identifier_type_name from senaite.patient.api import tuplify_identifiers -from senaite.patient.catalog import PATIENT_CATALOG class PatientFolderView(ListingView): @@ -89,9 +88,6 @@ def __init__(self, context, request): ("birthdate", { "title": _("Birthdate"), "index": "patient_birthdate"}), - ("shared_with", { - "title": _("Shared with"), - }) )) self.review_states = [ @@ -198,10 +194,4 @@ def folderitem(self, obj, item, index): if birthdate: item["birthdate"] = self.ulocalized_time(birthdate, long_format=0) - # Shared with - behavior = IClientShareableBehavior(obj) - clients = filter(None, behavior.getClients()) - clients = [get_link_for(client) for client in clients] - item["replace"]["shared_with"] = ", ".join(clients) - return item