diff --git a/docs/changelog.rst b/docs/changelog.rst index 950890e..6f09bc1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog 1.4.0 (unreleased) ------------------ +- #69 Fix Patient Workflows and Permissions - #68 Allow client local patients - #66 Fix widget view mode - #65 Fix cannot create partitions from samples with Patient assigned diff --git a/src/senaite/patient/api.py b/src/senaite/patient/api.py index 474c87d..62dc683 100644 --- a/src/senaite/patient/api.py +++ b/src/senaite/patient/api.py @@ -25,6 +25,7 @@ from bika.lims.utils import tmpID from dateutil.relativedelta import relativedelta from senaite.patient.config import PATIENT_CATALOG +from senaite.patient.permissions import AddPatient from zope.component import getUtility from zope.component.interfaces import IFactory from zope.event import notify @@ -155,12 +156,15 @@ def create_temporary_patient(): return patient -def store_temporary_patient(patient): +def store_temporary_patient(container, patient): """Store temporary patient to the patients folder + + :param container: The container where the patient should be stored + :param patient: A temporary patient object """ - portal = api.get_portal() - container = portal.patients + # Notify `ObjectCreateEvent` to generate a UID first notify(ObjectCreatedEvent(patient)) + # set the patient in the container container._setObject(patient.id, patient) patient = container.get(patient.getId()) return patient @@ -333,3 +337,22 @@ def is_patient_allowed_in_client(): allowed = api.get_registry_record( "senaite.patient.allow_patients_in_clients", False) return allowed + + +def get_patient_folder(): + """Returns the global patient folder + + :returns: global patients folder + """ + portal = api.get_portal() + return portal.patients + + +def is_patient_creation_allowed(container): + """Check if the security context allows to add a new patient + + :param container: The container to check the permission + :returns: True if it is allowed to create a patient in the container, + otherwise False + """ + return api.security.check_permission(AddPatient, container) diff --git a/src/senaite/patient/browser/configure.zcml b/src/senaite/patient/browser/configure.zcml index 4b83e83..323a88e 100644 --- a/src/senaite/patient/browser/configure.zcml +++ b/src/senaite/patient/browser/configure.zcml @@ -17,6 +17,7 @@ for="senaite.patient.content.patientfolder.IPatientFolder" class=".patientfolder.PatientFolderView" permission="zope2.View" + layer="senaite.patient.interfaces.ISenaitePatientLayer" /> @@ -24,14 +25,16 @@ name="patient-controlpanel" for="Products.CMFPlone.interfaces.IPloneSiteRoot" class=".controlpanel.PatientControlPanelView" - permission="senaite.core.permissions.ManageBika" - layer="senaite.patient.interfaces.ISenaitePatientLayer" /> + permission="senaite.patient.permissions.ManagePatients" + layer="senaite.patient.interfaces.ISenaitePatientLayer" + /> + name="senaite.patient.static" + /> + layer="senaite.patient.interfaces.ISenaitePatientLayer" + /> diff --git a/src/senaite/patient/browser/patientfolder.py b/src/senaite/patient/browser/patientfolder.py index 8d68483..23a5604 100644 --- a/src/senaite/patient/browser/patientfolder.py +++ b/src/senaite/patient/browser/patientfolder.py @@ -30,6 +30,7 @@ from senaite.patient.api import to_identifier_type_name from senaite.patient.api import tuplify_identifiers from senaite.patient.catalog import PATIENT_CATALOG +from senaite.patient.permissions import AddPatient class PatientFolderView(ListingView): @@ -57,7 +58,7 @@ def __init__(self, context, request): self.context_actions = { _("Add"): { "url": "++add++Patient", - "permission": "Add portal content", + "permission": AddPatient, "icon": "++resource++bika.lims.images/add.png"} } diff --git a/src/senaite/patient/configure.zcml b/src/senaite/patient/configure.zcml index d4ef395..0958080 100644 --- a/src/senaite/patient/configure.zcml +++ b/src/senaite/patient/configure.zcml @@ -85,6 +85,7 @@ description="Run various configuration actions" handler=".setuphandlers.setup_handler"> + diff --git a/src/senaite/patient/permissions.py b/src/senaite/patient/permissions.py index 3aa66d9..ec8e40b 100644 --- a/src/senaite/patient/permissions.py +++ b/src/senaite/patient/permissions.py @@ -18,10 +18,13 @@ # Copyright 2020-2022 by it's authors. # Some rights reserved, see README and LICENSE. -ManagePatients = "senaite.patient: Manage Patients" - -AddPatientFolder = "senaite.patient: Add PatientFolder" +# Add permission for our custom content types. +# Slso see `initialize` function in `__init__.py` AddPatient = "senaite.patient: Add Patient" +AddPatientFolder = "senaite.patient: Add PatientFolder" + +# Permission that governs the control panel view +ManagePatients = "senaite.patient: Manage Patients" # Transition permissions TransitionActivate = "senaite.patient: Transition: Activate" diff --git a/src/senaite/patient/permissions.zcml b/src/senaite/patient/permissions.zcml index 8f7f1bc..8e05d71 100644 --- a/src/senaite/patient/permissions.zcml +++ b/src/senaite/patient/permissions.zcml @@ -4,9 +4,9 @@ - - + + diff --git a/src/senaite/patient/profiles/default/metadata.xml b/src/senaite/patient/profiles/default/metadata.xml index f6ab94c..e2f505a 100644 --- a/src/senaite/patient/profiles/default/metadata.xml +++ b/src/senaite/patient/profiles/default/metadata.xml @@ -1,14 +1,6 @@ - - 1407 - - + 1408 profile-senaite.lims:default diff --git a/src/senaite/patient/profiles/default/rolemap.xml b/src/senaite/patient/profiles/default/rolemap.xml index 32385dc..0723c78 100644 --- a/src/senaite/patient/profiles/default/rolemap.xml +++ b/src/senaite/patient/profiles/default/rolemap.xml @@ -17,17 +17,24 @@ + + - + + + + + + + Add portal content Access contents information Delete objects List folder contents Modify portal content View + + senaite.patient: Add Patient + - + + + + + + + + - - Analyst - ClientGuest LabClerk LabManager - Preserver - RegulatoryInspector - Sampler - SamplingCoordinator - Manager - Owner - Site Administrator - - - - Analyst - ClientGuest + + LabClerk LabManager - Preserver - RegulatoryInspector - Sampler - SamplingCoordinator - Manager - Site Administrator - - Client + + LabClerk LabManager - Manager - + + + + + LabClerk LabManager - Manager - Owner + + + Manager + + - - Analyst - Client - ClientGuest LabClerk LabManager - Preserver - RegulatoryInspector - Sampler - SamplingCoordinator - Manager - Owner - Site Administrator + 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 ecae5af..640b40d 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 @@ -8,21 +8,14 @@ manager_bypass="False" i18n:domain="senaite.patient"> - + + Add portal content + Access contents information Delete objects - + List folder contents Modify portal content - View - - - - Access contents information - - - List folder contents - - + senaite.patient: Field: Edit MRN senaite.patient: Field: Edit ID senaite.patient: Field: Edit Fullname @@ -30,38 +23,76 @@ senaite.patient: Field: Edit Gender senaite.patient: Field: Edit Date of Birth senaite.patient: Field: Edit Address - senaite.patient: Transition: Activate senaite.patient: Transition: Deactivate + - + + + - - - - - + + + + ClientGuest + LabClerk + LabManager + Manager + Owner + + + + LabClerk + LabManager + Manager + Owner + + + + + + + ClientGuest + LabClerk + LabManager + Manager + Owner + + + + LabClerk + LabManager + Manager + Owner + + - - Analyst ClientGuest LabClerk LabManager - Preserver - RegulatoryInspector - Sampler - SamplingCoordinator - Manager Owner - Site Administrator - + + @@ -69,21 +100,66 @@ - + + - + - + + - - - - - - - + + + + + + ClientGuest + LabClerk + LabManager + Manager + Owner + + + + + + + + + + ClientGuest + LabClerk + LabManager + Manager + Owner + + + + + + + ClientGuest + LabClerk + LabManager + Manager + Owner + + + @@ -91,8 +167,9 @@ - + + diff --git a/src/senaite/patient/skins/templates/senaite_patient_widgets/temporaryidentifierwidget.pt b/src/senaite/patient/skins/templates/senaite_patient_widgets/temporaryidentifierwidget.pt index 947c01b..54341ad 100644 --- a/src/senaite/patient/skins/templates/senaite_patient_widgets/temporaryidentifierwidget.pt +++ b/src/senaite/patient/skins/templates/senaite_patient_widgets/temporaryidentifierwidget.pt @@ -31,8 +31,6 @@
setting MRN to temporary".format( + api.user.get_user_id(), api.get_path(container))) + # make the MRN temporary + # XXX: Refactor logic from Widget -> Field/DataManager + mrn_field = instance.getField("MedicalRecordNumber") + mrn = dict(mrn_field.get(instance)) + mrn["temporary"] = True + mrn_field.set(instance, mrn) + message = _("You are not allowed to add a patient in {} folder. " + "Medical Record Number set to Temporary." + .format(api.get_title(container))) + instance.plone_utils.addPortalMessage(message, "error") + return None + + logger.info("Creating new Patient in '{}' with MRN: '{}'" + .format(api.get_path(container), mrn)) values = get_patient_fields(instance) try: patient = patient_api.create_temporary_patient() patient_api.update_patient(patient, **values) - patient_api.store_temporary_patient(patient) + patient_api.store_temporary_patient(container, patient) except ValueError as exc: logger.error("%s" % exc) logger.error("Failed to create patient for values: %r" % values) diff --git a/src/senaite/patient/tests/base.py b/src/senaite/patient/tests/base.py index dc56d04..4e4b589 100644 --- a/src/senaite/patient/tests/base.py +++ b/src/senaite/patient/tests/base.py @@ -22,11 +22,14 @@ import unittest2 as unittest from plone.app.testing import PLONE_FIXTURE from plone.app.testing import TEST_USER_ID +from plone.app.testing import TEST_USER_NAME +from plone.app.testing import TEST_USER_PASSWORD from plone.app.testing import FunctionalTesting from plone.app.testing import PloneSandboxLayer from plone.app.testing import applyProfile from plone.app.testing import setRoles from plone.testing import zope +from plone.testing.z2 import Browser class SimpleTestLayer(PloneSandboxLayer): @@ -38,11 +41,11 @@ def setUpZope(self, app, configurationContext): super(SimpleTestLayer, self).setUpZope(app, configurationContext) import bika.lims - import senaite.lims - import senaite.core import senaite.app.listing - import senaite.impress import senaite.app.spotlight + import senaite.core + import senaite.impress + import senaite.lims import senaite.patient # Load ZCML @@ -92,6 +95,23 @@ def setUp(self): self.request["ACTUAL_URL"] = self.portal.absolute_url() setRoles(self.portal, TEST_USER_ID, ["LabManager", "Manager"]) + def getBrowser(self, + username=TEST_USER_NAME, + password=TEST_USER_PASSWORD, + loggedIn=True): + + # Instantiate and return a testbrowser for convenience + browser = Browser(self.portal) + browser.addHeader("Accept-Language", "en-US") + browser.handleErrors = False + if loggedIn: + browser.open(self.portal.absolute_url()) + browser.getControl("Login Name").value = username + browser.getControl("Password").value = password + browser.getControl("Log in").click() + self.assertTrue("You are now logged in" in browser.contents) + return browser + class FunctionalTestCase(unittest.TestCase): layer = SIMPLE_TESTING diff --git a/src/senaite/patient/tests/doctests/PatientWorkflow.rst b/src/senaite/patient/tests/doctests/PatientWorkflow.rst index d278a50..c515fe3 100644 --- a/src/senaite/patient/tests/doctests/PatientWorkflow.rst +++ b/src/senaite/patient/tests/doctests/PatientWorkflow.rst @@ -11,17 +11,23 @@ Test Setup Needed Imports: >>> from AccessControl.PermissionRole import rolesForPermissionOn + >>> from DateTime import DateTime + >>> from Products.CMFCore.permissions import AccessContentsInformation + >>> from Products.CMFCore.permissions import AddPortalContent + >>> from Products.CMFCore.permissions import DeleteObjects + >>> from Products.CMFCore.permissions import ListFolderContents + >>> from Products.CMFCore.permissions import ModifyPortalContent + >>> from Products.CMFCore.permissions import View >>> from bika.lims import api + >>> from bika.lims.api.security import get_roles_for_permission >>> from bika.lims.utils.analysisrequest import create_analysisrequest >>> from bika.lims.utils.analysisrequest import create_partition >>> from bika.lims.workflow import doActionFor as do_action_for - >>> from bika.lims.workflow import isTransitionAllowed >>> from bika.lims.workflow import getAllowedTransitions - >>> from DateTime import DateTime - >>> from plone.app.testing import setRoles + >>> from bika.lims.workflow import isTransitionAllowed >>> from plone.app.testing import TEST_USER_ID >>> from plone.app.testing import TEST_USER_PASSWORD - >>> from bika.lims.api.security import get_roles_for_permission + >>> from plone.app.testing import setRoles Functional Helpers: @@ -67,8 +73,8 @@ We need to create some basic objects for the test: >>> MS = api.create(setup.bika_analysisservices, "AnalysisService", title="Malaria Species", Keyword="MS", Price="10", Category=category.UID(), Accredited=True) -Patient Folder Permissions -.......................... +Patient Folder Workflow +....................... Get the mapped workflow and status of the patient folder: @@ -80,20 +86,56 @@ Get the mapped workflow and status of the patient folder: >>> api.get_workflow_status_of(patients) 'active' -Global add permission: + +Patient Folder Permissions +.......................... + +The creation of a patients folder is governed with the custom `AddPatientFolder` permission. +Also see the `initialize` function in `__init__.py`. >>> from senaite.patient.permissions import AddPatientFolder >>> get_roles_for_permission(AddPatientFolder, portal) ['Manager'] - >>> from senaite.patient.permissions import AddPatient - >>> get_roles_for_permission(AddPatient, patients) +The `View` permission governs who is allowed to see the patients folder and if +it is displayed in the side navigation or not: + + >>> get_roles_for_permission(View, patients) + ['LabClerk', 'LabManager', 'Manager'] + +The `DeleteObjects` permission governs if it is allowed to delete *any kind of +objects* from this folder: + + >>> get_roles_for_permission(DeleteObjects, patients) + [] + +The `AccessContentsInformation` permission governs if the basic access to the +folder, without necessarily viewing it: + + >>> get_roles_for_permission(AccessContentsInformation, patients) ['LabClerk', 'LabManager', 'Manager'] +The `ListFolderContents` permission governs whether you can get a listing of the patients: + + >>> get_roles_for_permission(ListFolderContents, patients) + ['LabClerk', 'LabManager', 'Manager'] + +The `ModifyPortalContent` permission governs whether it is allowed to change e.g. the Title of the folder: + + >>> get_roles_for_permission(ModifyPortalContent, patients) + ['Manager'] + Patient Permissions ................... +The creation of a patients is governed with the custom `AddPatient` permission. +Also see the `initialize` function in `__init__.py`. + + >>> from senaite.patient.permissions import AddPatient + >>> get_roles_for_permission(AddPatient, patients) + ['LabClerk', 'LabManager', 'Manager'] + Create a new patient: >>> patient = api.create(patients, "Patient", mrn="1", fullname="Clark Kent") @@ -113,6 +155,52 @@ Allowed transitions: >>> getAllowedTransitions(patient) ['deactivate'] + +Default permissions in **active** state: + +The following roles can `Access contents information` of patients, e.g. to see +the results in the reference widget: + + >>> get_roles_for_permission(AccessContentsInformation, patient) + ['ClientGuest', 'LabClerk', 'LabManager', 'Manager', 'Owner'] + +The `AddPortalContent` permission governs wether it is allowed to add contents +inside a patient. + +Although it is not used currently, we use the default permissions including the +`Owner` for client-local patients: + + >>> get_roles_for_permission(AddPortalContent, patient) + ['LabClerk', 'LabManager', 'Manager', 'Owner'] + +The `DeleteObjects` permission governs wether it is allowed to removed contents +inside a patient. We (almost) never allow this: + + >>> get_roles_for_permission(DeleteObjects, patient) + [] + +The `ListFolderContents` permission governs wether it is allowed list contents +inside patients. + +Although it is not used currently, we use the default roles including the +`Owner` for client-local and `ClientGuest` for shared patients: + + >>> get_roles_for_permission(ListFolderContents, patient) + ['ClientGuest', 'LabClerk', 'LabManager', 'Manager', 'Owner'] + +The `ModifyPortalContent` permission governs wether it is allowed to edit a patient. +Note that we do not allow this for `ClientGuest` role, because we do not want that +shared patients can be edited from basically client contacts: + + >>> get_roles_for_permission(ModifyPortalContent, patient) + ['LabClerk', 'LabManager', 'Manager', 'Owner'] + +The `View` permission governs if the patient can be viewed: + + >>> get_roles_for_permission(View, patient) + ['ClientGuest', 'LabClerk', 'LabManager', 'Manager', 'Owner'] + + Field permission in **active** state: >>> from senaite.patient.permissions import FieldEditMRN @@ -145,6 +233,40 @@ Deactivating the patient >>> api.get_workflow_status_of(patient) 'inactive' + +Default permissions in **inactive** state: + +Accessing the patient is possible for the same roles: + + >>> get_roles_for_permission(AccessContentsInformation, patient) + ['ClientGuest', 'LabClerk', 'LabManager', 'Manager', 'Owner'] + +It should be no longer possible to add contents to a deactivated patient: + + >>> get_roles_for_permission(AddPortalContent, patient) + [] + +Deleting contents is not allowed: + + >>> get_roles_for_permission(DeleteObjects, patient) + [] + +Inactive clients should be still listed for the same roles: + + >>> get_roles_for_permission(ListFolderContents, patient) + ['ClientGuest', 'LabClerk', 'LabManager', 'Manager', 'Owner'] + +No modifications are allowed for inactive patients: + + >>> get_roles_for_permission(ModifyPortalContent, patient) + [] + +Viewing an inactive client is still possible for the same roles + + >>> get_roles_for_permission(View, patient) + ['ClientGuest', 'LabClerk', 'LabManager', 'Manager', 'Owner'] + + Field permission in **inactive** state: >>> from senaite.patient.permissions import FieldEditMRN diff --git a/src/senaite/patient/upgrade/v01_04_000.py b/src/senaite/patient/upgrade/v01_04_000.py index f5bf2ab..b52c6dd 100644 --- a/src/senaite/patient/upgrade/v01_04_000.py +++ b/src/senaite/patient/upgrade/v01_04_000.py @@ -19,19 +19,22 @@ # Some rights reserved, see README and LICENSE. import transaction - from bika.lims import api from senaite.core.catalog import SAMPLE_CATALOG from senaite.core.upgrade import upgradestep from senaite.core.upgrade.utils import UpgradeUtils from senaite.patient import logger from senaite.patient.api import patient_search +from senaite.patient.catalog import PATIENT_CATALOG from senaite.patient.config import PRODUCT_NAME from senaite.patient.setuphandlers import setup_catalogs version = "1.4.0" profile = "profile-{0}:default".format(PRODUCT_NAME) +PATIENT_WORKFLOW = "senaite_patient_workflow" +PATIENT_FOLDER_WORKFLOW = "senaite_patient_folder_workflow" + @upgradestep(PRODUCT_NAME, version) def upgrade(tool): @@ -214,3 +217,50 @@ def allow_patients_in_clients(tool): setup.runImportStepFromProfile(profile, "plone.app.registry") logger.info("Allow patients in clients [DONE]") + + +@upgradestep(PRODUCT_NAME, version) +def update_patient_workflows(tool): + """Update patient workflows and security settings + """ + logger.info("Update patient workflows ...") + + # import rolemap, workflow and typeinfo + portal = tool.aq_inner.aq_parent + setup = portal.portal_setup + setup.runImportStepFromProfile(profile, "rolemap") + setup.runImportStepFromProfile(profile, "workflow") + setup.runImportStepFromProfile(profile, "typeinfo") + + # get patient folder + workflow + patientsfolder = portal.patients + wf_tool = api.get_tool("portal_workflow") + patients_workflow = wf_tool.getWorkflowById(PATIENT_FOLDER_WORKFLOW) + + # update rolemappings + object security for patients folder + patients_workflow.updateRoleMappingsFor(patientsfolder) + patientsfolder.reindexObject(idxs=["allowedRolesAndUsers"]) + + # fetch patients + workflow + patients = api.search({"portal_type": "Patient"}, PATIENT_CATALOG) + total = len(patients) + patient_workflow = wf_tool.getWorkflowById(PATIENT_WORKFLOW) + + for num, patient in enumerate(patients): + obj = api.get_object(patient) + logger.info("Processing patient %s/%s: %s" + % (num+1, total, obj.Title())) + + # update rolemappings + object security for patient + patient_workflow.updateRoleMappingsFor(obj) + obj.reindexObject(idxs=["allowedRolesAndUsers"]) + + if num and num % 10 == 0: + logger.info("Commiting patient %s/%s" % (num+1, total)) + transaction.commit() + logger.info("Commit done") + + # Flush the object from memory + obj._p_deactivate() + + logger.info("Update patient workflows [DONE]") diff --git a/src/senaite/patient/upgrade/v01_04_000.zcml b/src/senaite/patient/upgrade/v01_04_000.zcml index fbc04a7..8fafff5 100644 --- a/src/senaite/patient/upgrade/v01_04_000.zcml +++ b/src/senaite/patient/upgrade/v01_04_000.zcml @@ -2,6 +2,15 @@ xmlns="http://namespaces.zope.org/zope" xmlns:genericsetup="http://namespaces.zope.org/genericsetup"> + + +