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

Allow to edit Profiles in Samples for pre verified/published states #1657

Merged
merged 11 commits into from
Oct 13, 2020
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.0.0rc2 (unreleased)
---------------------

- #1657 Allow to edit Profiles in Samples for pre verified/published states
- #1655 Rename service's "Result Options" and "Additional Values"
- #1655 Move service's "Additional values" to "Result Options" tab
- #1654 Fix Text of interim choices is not displayed correctly on readonly mode
Expand Down
28 changes: 28 additions & 0 deletions src/bika/lims/content/analysisrequest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1465,6 +1465,34 @@ def setResultsRange(self, value, recursive=True):
if check_permission(permission, descendant):
descendant.setResultsRange(value)

def setProfiles(self, value):
"""Set Analysis Profiles to the Sample
"""
if not isinstance(value, (list, tuple)):
value = [value]
# filter out empties
value = filter(None, value)
# ensure we have UIDs
uids = map(api.get_uid, value)
# get the current set profiles
current_profiles = self.getRawProfiles()
# return immediately if nothing changed
if current_profiles == uids:
return
# get the profiles
profiles = map(api.get_object_by_uid, uids)
# get the current set of analyses/services
analyses = self.getAnalyses(full_objects=True)
services = map(lambda an: an.getAnalysisService(), analyses)
# determine all the services to add
services_to_add = set(services)
for profile in profiles:
services_to_add.update(profile.getService())
# set all analyses
self.setAnalyses(list(services_to_add))
# set the profiles value
self.getField("Profiles").set(self, value)

def getClient(self):
"""Returns the client this object is bound to. We override getClient
from ClientAwareMixin because the "Client" schema field is only used to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,7 @@
<permission-map name="senaite.core: Field: Edit Preservation" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Preserver" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Priority" acquired="True"/>
<!-- Make the field 'Profiles' readonly -->
<permission-map name="senaite.core: Field: Edit Profiles" acquired="False"/>
<permission-map name="senaite.core: Field: Edit Profiles" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Publication Specification" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Rejection Reasons" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Remarks" acquired="True"/>
Expand Down Expand Up @@ -340,8 +339,7 @@
<permission-map name="senaite.core: Field: Edit Preservation" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Preserver" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Priority" acquired="True"/>
<!-- Make the field 'Profiles' readonly -->
<permission-map name="senaite.core: Field: Edit Profiles" acquired="False"/>
<permission-map name="senaite.core: Field: Edit Profiles" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Publication Specification" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Rejection Reasons" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Remarks" acquired="True"/>
Expand Down Expand Up @@ -428,8 +426,7 @@
<permission-map name="senaite.core: Field: Edit Preservation" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Preserver" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Priority" acquired="True"/>
<!-- Make the field 'Profiles' readonly -->
<permission-map name="senaite.core: Field: Edit Profiles" acquired="False"/>
<permission-map name="senaite.core: Field: Edit Profiles" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Publication Specification" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Rejection Reasons" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Remarks" acquired="True"/>
Expand Down Expand Up @@ -515,8 +512,7 @@
<permission-map name="senaite.core: Field: Edit Preservation" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Preserver" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Priority" acquired="True"/>
<!-- Make the field 'Profiles' readonly -->
<permission-map name="senaite.core: Field: Edit Profiles" acquired="False"/>
<permission-map name="senaite.core: Field: Edit Profiles" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Publication Specification" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Rejection Reasons" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Remarks" acquired="True"/>
Expand Down Expand Up @@ -598,8 +594,7 @@
<permission-map name="senaite.core: Field: Edit Preservation" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Preserver" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Priority" acquired="True"/>
<!-- Make the field 'Profiles' readonly -->
<permission-map name="senaite.core: Field: Edit Profiles" acquired="False"/>
<permission-map name="senaite.core: Field: Edit Profiles" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Publication Specification" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Rejection Reasons" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Remarks" acquired="True"/>
Expand Down Expand Up @@ -691,8 +686,7 @@
<permission-map name="senaite.core: Field: Edit Preservation" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Preserver" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Priority" acquired="True"/>
<!-- Make the field 'Profiles' readonly -->
<permission-map name="senaite.core: Field: Edit Profiles" acquired="False"/>
<permission-map name="senaite.core: Field: Edit Profiles" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Publication Specification" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Rejection Reasons" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Remarks" acquired="True"/>
Expand Down Expand Up @@ -776,8 +770,7 @@
<permission-map name="senaite.core: Field: Edit Preservation" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Preserver" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Priority" acquired="True"/>
<!-- Make the field 'Profiles' readonly -->
<permission-map name="senaite.core: Field: Edit Profiles" acquired="False"/>
<permission-map name="senaite.core: Field: Edit Profiles" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Publication Specification" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Rejection Reasons" acquired="True"/>
<permission-map name="senaite.core: Field: Edit Remarks" acquired="True"/>
Expand Down
183 changes: 183 additions & 0 deletions src/senaite/core/tests/doctests/AnalysisProfile.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
Analysis Profile
================

Running this test from the buildout directory::

bin/test test_textual_doctests -t AnalysisProfile

Needed Imports:

>>> import re
>>> from AccessControl.PermissionRole import rolesForPermissionOn
>>> from bika.lims import api
>>> from bika.lims.content.analysisrequest import AnalysisRequest
>>> from bika.lims.utils.analysisrequest import create_analysisrequest
>>> from bika.lims.utils import tmpID
>>> from bika.lims.interfaces import ISubmitted
>>> from bika.lims.workflow import doActionFor as do_action_for
>>> from bika.lims.workflow import getCurrentState
>>> from bika.lims.workflow import getAllowedTransitions
>>> from DateTime import DateTime
>>> from plone.app.testing import TEST_USER_ID
>>> from plone.app.testing import TEST_USER_PASSWORD
>>> from plone.app.testing import setRoles

Functional Helpers:

>>> def start_server():
... from Testing.ZopeTestCase.utils import startZServer
... ip, port = startZServer()
... return "http://{}:{}/{}".format(ip, port, portal.id)


>>> def get_services(sample):
... analyses = sample.getAnalyses(full_objects=True)
... services = map(lambda an: an.getAnalysisService(), analyses)
... return services

>>> def receive_sample(sample):
... do_action_for(sample, "receive")

>>> def submit_analyses(sample):
... for analysis in sample.getAnalyses(full_objects=True):
... analysis.setResult(13)
... do_action_for(analysis, "submit")

>>> def verify_analyses(sample):
... for analysis in sample.getAnalyses(full_objects=True):
... if ISubmitted.providedBy(analysis):
... do_action_for(analysis, "verify")

>>> def retract_analyses(sample):
... for analysis in sample.getAnalyses(full_objects=True):
... if ISubmitted.providedBy(analysis):
... do_action_for(analysis, "retract")

Variables:

>>> portal = self.portal
>>> request = self.request
>>> setup = portal.bika_setup
>>> date_now = DateTime().strftime("%Y-%m-%d")
>>> date_future = (DateTime() + 5).strftime("%Y-%m-%d")

We need to create some basic objects for the test:

>>> setRoles(portal, TEST_USER_ID, ['LabManager',])
>>> client = api.create(portal.clients, "Client", Name="Happy Hills", ClientID="HH", MemberDiscountApplies=True)
>>> contact = api.create(client, "Contact", Firstname="Rita", Lastname="Mohale")
>>> sampletype = api.create(setup.bika_sampletypes, "SampleType", title="Water", Prefix="W")
>>> labcontact = api.create(setup.bika_labcontacts, "LabContact", Firstname="Lab", Lastname="Manager")
>>> department = api.create(setup.bika_departments, "Department", title="Chemistry", Manager=labcontact)
>>> category = api.create(setup.bika_analysiscategories, "AnalysisCategory", title="Metals", Department=department)
>>> supplier = api.create(setup.bika_suppliers, "Supplier", Name="Naralabs")
>>> Cu = api.create(setup.bika_analysisservices, "AnalysisService", title="Copper", Keyword="Cu", Price="15", Category=category.UID(), Accredited=True)
>>> Fe = api.create(setup.bika_analysisservices, "AnalysisService", title="Iron", Keyword="Fe", Price="10", Category=category.UID())
>>> Au = api.create(setup.bika_analysisservices, "AnalysisService", title="Gold", Keyword="Au", Price="20", Category=category.UID())
>>> Zn = api.create(setup.bika_analysisservices, "AnalysisService", title="Zink", Keyword="Zn", Price="20", Category=category.UID())
>>> service_uids1 = [Cu.UID(), Fe.UID(), Au.UID()]
>>> service_uids2 = [Zn.UID()]
>>> service_uids3 = [Cu.UID(), Fe.UID(), Au.UID(), Zn.UID()]
>>> profile1 = api.create(setup.bika_analysisprofiles, "AnalysisProfile", title="Profile", Service=service_uids1)
>>> profile2 = api.create(setup.bika_analysisprofiles, "AnalysisProfile", title="Profile", Service=service_uids2)
>>> profile3 = api.create(setup.bika_analysisprofiles, "AnalysisProfile", title="Profile", Service=service_uids3)


Assign Profile(s)
-----------------

Assigning Analysis Profiles adds the Analyses of the profile to the sample.

>>> setup.setSelfVerificationEnabled(True)

>>> values = {
... 'Client': client.UID(),
... 'Contact': contact.UID(),
... 'DateSampled': date_now,
... 'SampleType': sampletype.UID()}

Create some Analysis Requests:

>>> ar1 = create_analysisrequest(client, request, values, [Au.UID()])
>>> ar2 = create_analysisrequest(client, request, values, [Fe.UID()])
>>> ar3 = create_analysisrequest(client, request, values, [Cu.UID()])

Apply the profile object. Note the custom `setProfiles` (plural) setter:

>>> ar1.setProfiles(profile1)

All analyses from the profile should be added to the sample:

>>> services = get_services(ar1)
>>> set(map(api.get_uid, services)).issuperset(service_uids1)
True

The profile is applied to the sample:

>>> profile1 in ar1.getProfiles()
True

Apply the profile UID:

>>> ar2.setProfiles(profile2.UID())

All analyses from the profile should be added to the sample:

>>> services = get_services(ar2)
>>> set(map(api.get_uid, services)).issuperset(service_uids2)
True

The profile is applied to the sample:

>>> profile2 in ar2.getProfiles()
True


Apply multiple profiles:

>>> ar3.setProfiles([profile1, profile2, profile3.UID()])

All analyses from the profiles should be added to the sample:

>>> services = get_services(ar3)
>>> set(map(api.get_uid, services)).issuperset(service_uids1 + service_uids2 + service_uids3)
True


Remove Profile(s)
-----------------

Removing an analyis Sample retains the assigned analyses:

>>> analyses = ar1.getAnalyses(full_objects=True)
>>> ar1.setProfiles([])
>>> ar1.getProfiles()
[]

>>> set(ar1.getAnalyses(full_objects=True)) == set(analyses)
True


Assigning Profiles in "to_be_verified" status
---------------------------------------------

>>> ar4 = create_analysisrequest(client, request, values, [Au.UID()])

>>> receive_sample(ar4)
>>> submit_analyses(ar4)

>>> api.get_workflow_status_of(ar4)
'to_be_verified'

>>> ar4.getProfiles()
[]

Setting the profile works up to this state:

>>> ar4.setProfiles(profile1.UID())
>>> api.get_workflow_status_of(ar4)
'sample_received'

>>> services = get_services(ar3)
>>> set(map(api.get_uid, services)).issuperset(service_uids1 + [Au.UID()])
True
44 changes: 43 additions & 1 deletion src/senaite/core/upgrade/v02_00_000.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
# Some rights reserved, see README and LICENSE.

from bika.lims import api
from bika.lims.catalog import CATALOG_ANALYSIS_REQUEST_LISTING
from bika.lims.catalog import SETUP_CATALOG
from bika.lims.setuphandlers import add_dexterity_setup_items
from senaite.core import logger
from senaite.core.config import PROJECTNAME as product
from senaite.core.setuphandlers import _run_import_step
from senaite.core.upgrade import upgradestep
from senaite.core.upgrade.utils import UpgradeUtils

Expand Down Expand Up @@ -68,15 +70,22 @@ def upgrade(tool):
# Install the new SENAITE CORE package
install_senaite_core(portal)

# Add Interpretation Template(s) content type
# run import steps located in senaite.core profiles
setup.runImportStepFromProfile(profile, "typeinfo")
setup.runImportStepFromProfile(profile, "workflow")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You still need to keep setup.runImportStepFromProfile(profile, "workflow") because of #1620: workflow definitions for InterpretationTemplate and InterpretationTemplates are located in senaite.core profile.

# run import steps located in bika.lims profiles
_run_import_step(portal, "typeinfo", profile="profile-bika.lims:default")
_run_import_step(portal, "workflow", profile="profile-bika.lims:default")

add_dexterity_setup_items(portal)

# Published results tab is not displayed to client contacts
# https://github.com/senaite/senaite.core/pull/1638
fix_published_results_permission(portal)

# Update workflow mappings for samples to allow profile editing
update_workflow_mappings_samples(portal)

logger.info("{0} upgraded to version {1}".format(product, version))
return True

Expand Down Expand Up @@ -140,3 +149,36 @@ def fix_published_results_permission(portal):
if action.id == "published_results":
action.permissions = ("View", )
break


def update_workflow_mappings_samples(portal):
"""Allow to edit analysis profiles
"""
logger.info("Updating role mappings for Samples ...")
wf_id = "bika_ar_workflow"
query = {"portal_type": "AnalysisRequest",
"review_state": [
"sample_due",
"sample_registered",
"scheduled_sampling",
"to_be_sampled",
"sample_received",
"attachment_due",
"to_be_verified",
"to_be_preserved",
]}
brains = api.search(query, CATALOG_ANALYSIS_REQUEST_LISTING)
update_workflow_mappings_for(portal, wf_id, brains)
logger.info("Updating role mappings for Samples [DONE]")


def update_workflow_mappings_for(portal, wf_id, brains):
wf_tool = api.get_tool("portal_workflow")
workflow = wf_tool.getWorkflowById(wf_id)
total = len(brains)
for num, brain in enumerate(brains):
if num and num % 100 == 0:
logger.info("Updating role mappings: {0}/{1}".format(num, total))
obj = api.get_object(brain)
workflow.updateRoleMappingsFor(obj)
obj.reindexObject(idxs=["allowedRolesAndUsers"])