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

Dynamic Analysis Specs: Lookup dynamic specs only when specification is set and hide spec compliance viewlets #1588

Merged
merged 7 commits into from
Jun 9, 2020
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
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Changelog

**Added**

- #1588 Dynamic Analysis Specs: Lookup dynamic spec only when the specification is set
- #1586 Allow to configure the variables for IDServer with an Adapter
- #1584 Date (yymmdd) support in IDs generation
- #1582 Allow to retest analyses without the need of retraction
Expand All @@ -14,6 +15,7 @@ Changelog

**Changed**

- #1588 Dynamic Analysis Specs: Hide compliance viewlets
- #1579 Remove classic mode in folderitems
- #1577 Do not force available workflow transitions in batches listing
- #1573 Do not display top-level "Clients" folder to non-lab users
Expand Down
14 changes: 14 additions & 0 deletions bika/lims/browser/viewlets/analysisrequest.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ class ResultsRangesOutOfDateViewlet(ViewletBase):
specification ranges will be used instead of the new ones.
"""

def available(self):
spec = self.context.getSpecification()
if spec:
dynamic_spec = spec.getDynamicAnalysisSpec()
return not dynamic_spec
return True

def is_specification_editable(self):
"""Returns whether the Specification field is editable or not
"""
Expand Down Expand Up @@ -136,6 +143,13 @@ class SpecificationNotCompliantViewlet(ViewletBase):
analyses are different from the Specification initially set.
"""

def available(self):
spec = self.context.getSpecification()
if spec:
dynamic_spec = spec.getDynamicAnalysisSpec()
return not dynamic_spec
return True

def is_specification_editable(self):
"""Returns whether the Specification field is editable or not
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
<div tal:omit-tag=""
tal:define="out_of_date python:view.is_results_ranges_out_of_date()"
tal:condition="python:out_of_date"
tal:condition="python:view.available()"
i18n:domain="senaite.core">

<div class="visualClear"></div>

<div id="portal-alert"
tal:define="sample python:view.context;
editable python:view.is_specification_editable();
alert_class python: editable and 'alert-warning' or 'alert-info';
alert_class python: 'portlet-alert-item alert {} alert-dismissible'.format(alert_class)">
alert_class python: 'portlet-alert-item alert {} alert-dismissible'.format(alert_class);
out_of_date python:view.is_results_ranges_out_of_date()"
tal:condition="out_of_date">

<div tal:attributes="class python: alert_class">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
<div tal:omit-tag=""
tal:define="non_compliant python:view.get_non_compliant_analyses()"
tal:condition="python:non_compliant"
tal:condition="python:view.available()"
i18n:domain="senaite.core">

<div class="visualClear"></div>

<div id="portal-alert"
tal:define="sample python:view.context;
non_compliant python:view.get_non_compliant_analyses()
editable python:view.is_specification_editable();
alert_class python: editable and 'alert-warning' or 'alert-info';
alert_class python: 'portlet-alert-item alert {} alert-dismissible'.format(alert_class)">
alert_class python: 'portlet-alert-item alert {} alert-dismissible'.format(alert_class)"
tal:condition="non_compliant">

<div tal:attributes="class python: alert_class">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
Expand Down
17 changes: 6 additions & 11 deletions bika/lims/content/abstractroutineanalysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
from bika.lims.content.reflexrule import doReflexRuleAction
from bika.lims.interfaces import IAnalysis
from bika.lims.interfaces import ICancellable
from bika.lims.interfaces import IDynamicResultsRange
from bika.lims.interfaces import IInternalUse
from bika.lims.interfaces import IRoutineAnalysis
from bika.lims.interfaces.analysis import IRequestAnalysis
Expand Down Expand Up @@ -325,19 +324,15 @@ def getResultsRange(self):
"""Returns the valid result range for this routine analysis

A routine analysis will be considered out of range if it result falls
out of the range defined in "min" and "max". If there are values set for
"warn_min" and "warn_max", these are used to compute the shoulders in
both ends of the range. Thus, an analysis can be out of range, but be
within shoulders still.
out of the range defined in "min" and "max". If there are values set
for "warn_min" and "warn_max", these are used to compute the shoulders
in both ends of the range. Thus, an analysis can be out of range, but
be within shoulders still.

:return: A dictionary with keys "min", "max", "warn_min" and "warn_max"
:rtype: dict
"""
results_range = self.getField("ResultsRange").get(self)
# dynamic results range adapter
adapter = IDynamicResultsRange(self, None)
if adapter:
results_range.update(adapter())
return results_range
return self.getField("ResultsRange").get(self)

@security.public
def getSiblings(self, with_retests=False):
Expand Down
64 changes: 35 additions & 29 deletions bika/lims/content/analysisrequest.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,33 +25,6 @@
from urlparse import urljoin

from AccessControl import ClassSecurityInfo
from DateTime import DateTime
from Products.ATExtensions.field import RecordsField
from Products.Archetypes.Widget import RichWidget
from Products.Archetypes.atapi import BaseFolder
from Products.Archetypes.atapi import BooleanField
from Products.Archetypes.atapi import BooleanWidget
from Products.Archetypes.atapi import ComputedField
from Products.Archetypes.atapi import ComputedWidget
from Products.Archetypes.atapi import FileField
from Products.Archetypes.atapi import FileWidget
from Products.Archetypes.atapi import FixedPointField
from Products.Archetypes.atapi import ReferenceField
from Products.Archetypes.atapi import StringField
from Products.Archetypes.atapi import StringWidget
from Products.Archetypes.atapi import TextField
from Products.Archetypes.atapi import registerType
from Products.Archetypes.public import Schema
from Products.Archetypes.references import HoldingReference
from Products.CMFCore.permissions import ModifyPortalContent
from Products.CMFCore.permissions import View
from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.utils import _createObjectByType
from Products.CMFPlone.utils import safe_unicode
from zope.interface import alsoProvides
from zope.interface import implements
from zope.interface import noLongerProvides

from bika.lims import api
from bika.lims import bikaMessageFactory as _
from bika.lims import deprecated
Expand All @@ -60,9 +33,9 @@
from bika.lims.browser.fields import ARAnalysesField
from bika.lims.browser.fields import DateTimeField
from bika.lims.browser.fields import DurationField
from bika.lims.browser.fields import EmailsField
from bika.lims.browser.fields import ResultsRangesField
from bika.lims.browser.fields import UIDReferenceField
from bika.lims.browser.fields import EmailsField
from bika.lims.browser.fields.remarksfield import RemarksField
from bika.lims.browser.widgets import DateTimeWidget
from bika.lims.browser.widgets import DecimalWidget
Expand All @@ -86,6 +59,7 @@
from bika.lims.interfaces import IBatch
from bika.lims.interfaces import ICancellable
from bika.lims.interfaces import IClient
from bika.lims.interfaces import IDynamicResultsRange
from bika.lims.interfaces import ISubmitted
from bika.lims.permissions import FieldEditBatch
from bika.lims.permissions import FieldEditClient
Expand All @@ -112,8 +86,8 @@
from bika.lims.permissions import FieldEditResultsInterpretation
from bika.lims.permissions import FieldEditSampleCondition
from bika.lims.permissions import FieldEditSamplePoint
from bika.lims.permissions import FieldEditSampleType
from bika.lims.permissions import FieldEditSampler
from bika.lims.permissions import FieldEditSampleType
from bika.lims.permissions import FieldEditSamplingDate
from bika.lims.permissions import FieldEditSamplingDeviation
from bika.lims.permissions import FieldEditScheduledSampler
Expand All @@ -127,6 +101,32 @@
from bika.lims.utils import user_fullname
from bika.lims.workflow import getTransitionDate
from bika.lims.workflow import getTransitionUsers
from DateTime import DateTime
from Products.Archetypes.atapi import BaseFolder
from Products.Archetypes.atapi import BooleanField
from Products.Archetypes.atapi import BooleanWidget
from Products.Archetypes.atapi import ComputedField
from Products.Archetypes.atapi import ComputedWidget
from Products.Archetypes.atapi import FileField
from Products.Archetypes.atapi import FileWidget
from Products.Archetypes.atapi import FixedPointField
from Products.Archetypes.atapi import ReferenceField
from Products.Archetypes.atapi import StringField
from Products.Archetypes.atapi import StringWidget
from Products.Archetypes.atapi import TextField
from Products.Archetypes.atapi import registerType
from Products.Archetypes.public import Schema
from Products.Archetypes.references import HoldingReference
from Products.Archetypes.Widget import RichWidget
from Products.ATExtensions.field import RecordsField
from Products.CMFCore.permissions import ModifyPortalContent
from Products.CMFCore.permissions import View
from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.utils import _createObjectByType
from Products.CMFPlone.utils import safe_unicode
from zope.interface import alsoProvides
from zope.interface import implements
from zope.interface import noLongerProvides

IMG_SRC_RX = re.compile(r'<img.*?src="(.*?)"')
IMG_DATA_SRC_RX = re.compile(r'<img.*?src="(data:image/.*?;base64,)(.*?)"')
Expand Down Expand Up @@ -1449,7 +1449,13 @@ def setResultsRange(self, value, recursive=True):
for analysis in self.objectValues("Analysis"):
if not ISubmitted.providedBy(analysis):
service_uid = analysis.getRawAnalysisService()
# get the default results range from the spec
result_range = field.get(self, search_by=service_uid)
# check if we have an dynamic results range adapter
adapter = IDynamicResultsRange(analysis, None)
if adapter:
# update the result range with the dynamic values
result_range.update(adapter())
analysis.setResultsRange(result_range)
analysis.reindexObject()

Expand Down
16 changes: 16 additions & 0 deletions bika/lims/tests/doctests/DynamicAnalysisSpec.rst
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ Now we hook in our Dynamic Analysis Specification to the standard Specification:

>>> specification.setDynamicAnalysisSpec(ds)


The specification need to get unset/set again, so that the dynamic values get looked up:

>>> sample.setSpecification(None)
>>> sample.setSpecification(specification)

The specification of the `Ca` Analysis with the Method `Method A`:

>>> ca_spec = ca.getResultsRange()
Expand All @@ -178,6 +184,11 @@ Now let's change the `Ca` Analysis Method to `Method B`:

>>> ca.setMethod(method_b)

Unset and set the specification again:

>>> sample.setSpecification(None)
>>> sample.setSpecification(specification)

And get the results range again:

>>> ca_spec = ca.getResultsRange()
Expand All @@ -192,6 +203,11 @@ The same now with the `Mg` Analysis in one run:

>>> mg.setMethod(method_b)

Unset and set the specification again:

>>> sample.setSpecification(None)
>>> sample.setSpecification(specification)

>>> mg_spec = mg.getResultsRange()
>>> mg_spec["min"], mg_spec["max"]
(7, 8)
41 changes: 41 additions & 0 deletions bika/lims/upgrade/v01_03_004.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ def upgrade(tool):
setup.runImportStepFromProfile(profile, "workflow")
update_workflow_mappings_for_to_be_verified(portal)

# Unset/set specifications with dynamic results ranges assigned
# https://github.com/senaite/senaite.core/pull/1588
update_dynamic_analysisspecs(portal)

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

Expand Down Expand Up @@ -115,3 +119,40 @@ def update_workflow_mappings_for_to_be_verified(portal):
obj = api.get_object(brain)
workflow.updateRoleMappingsFor(obj)
logger.info("Updating role mappings for 'to_be_verified' analyses [DONE]")


def update_dynamic_analysisspecs(portal):
"""Unset/set specifications that have dynamic result ranges assigned
"""

# Skip update when there are no dynamic specs registered in the system
setup = api.get_setup()
dynamic_specs = getattr(setup, "dynamic_analysisspecs", None)
if dynamic_specs is None:
return
if not dynamic_specs.objectIds():
return

logger.info("Updating specifications with dynamic results ranges...")
catalog = api.get_tool(CATALOG_ANALYSIS_REQUEST_LISTING)
samples = catalog({"portal_type": "AnalysisRequest"})
total = len(samples)

logger.info("Checking dynamic specifications of {} samples".format(total))
for num, sample in enumerate(samples):
if num and num % 100 == 0:
logger.info("Checked {}/{} samples".format(num, total))
obj = api.get_object(sample)
spec = obj.getSpecification()
if spec is None:
continue
if not spec.getDynamicAnalysisSpec():
continue

# Unset/set the specification
logger.info("Updating specification '{}' of smaple '{}'".format(
spec.Title(), sample.getId()))
sample.setAnalysisSpec(None)
sample.setAnalysisSpec(spec)

logger.info("Updating specifications with dynamic results ranges [DONE]")