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">
+
+
+