diff --git a/CHANGES.rst b/CHANGES.rst
index eb88f13818..db09a156b8 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -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
@@ -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
diff --git a/bika/lims/browser/viewlets/analysisrequest.py b/bika/lims/browser/viewlets/analysisrequest.py
index a1e32397e6..7cdb19e7b3 100644
--- a/bika/lims/browser/viewlets/analysisrequest.py
+++ b/bika/lims/browser/viewlets/analysisrequest.py
@@ -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
"""
@@ -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
"""
diff --git a/bika/lims/browser/viewlets/templates/resultsranges_out_of_date_viewlet.pt b/bika/lims/browser/viewlets/templates/resultsranges_out_of_date_viewlet.pt
index dfe1b31dcd..d2b1ee88e8 100644
--- a/bika/lims/browser/viewlets/templates/resultsranges_out_of_date_viewlet.pt
+++ b/bika/lims/browser/viewlets/templates/resultsranges_out_of_date_viewlet.pt
@@ -1,15 +1,14 @@
-
-
+ 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">
diff --git a/bika/lims/browser/viewlets/templates/specification_non_compliant_viewlet.pt b/bika/lims/browser/viewlets/templates/specification_non_compliant_viewlet.pt
index d65e48cae9..c26196905e 100644
--- a/bika/lims/browser/viewlets/templates/specification_non_compliant_viewlet.pt
+++ b/bika/lims/browser/viewlets/templates/specification_non_compliant_viewlet.pt
@@ -1,15 +1,14 @@
-
-
+ alert_class python: 'portlet-alert-item alert {} alert-dismissible'.format(alert_class)"
+ tal:condition="non_compliant">
diff --git a/bika/lims/content/abstractroutineanalysis.py b/bika/lims/content/abstractroutineanalysis.py
index d60a441f9a..1dae2a96b1 100644
--- a/bika/lims/content/abstractroutineanalysis.py
+++ b/bika/lims/content/abstractroutineanalysis.py
@@ -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
@@ -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):
diff --git a/bika/lims/content/analysisrequest.py b/bika/lims/content/analysisrequest.py
index 65f970fbc2..2de5c2bdd1 100644
--- a/bika/lims/content/analysisrequest.py
+++ b/bika/lims/content/analysisrequest.py
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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'>> 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()
@@ -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()
@@ -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)
diff --git a/bika/lims/upgrade/v01_03_004.py b/bika/lims/upgrade/v01_03_004.py
index f9abc57930..4ab65c3ea2 100644
--- a/bika/lims/upgrade/v01_03_004.py
+++ b/bika/lims/upgrade/v01_03_004.py
@@ -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
@@ -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]")