diff --git a/CHANGES.rst b/CHANGES.rst index 9e4d1b7d8b..ccfead5fdf 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,7 @@ Changelog 2.5.0 (unreleased) ------------------ +- #2452 Fix reference definition range validation - #2450 Fix search bar from worksheet listing does not work - #2449 Fix Mine button from worksheets listing does not filter by current user - #2448 Fix open filter is not visible to analysts in worksheets listing diff --git a/src/bika/lims/browser/widgets/referenceresultswidget.py b/src/bika/lims/browser/widgets/referenceresultswidget.py index edb610802a..ae39d8bef7 100644 --- a/src/bika/lims/browser/widgets/referenceresultswidget.py +++ b/src/bika/lims/browser/widgets/referenceresultswidget.py @@ -220,9 +220,17 @@ def process_form(self, instance, field, form, s_min = self._get_spec_value(form, uid, "min", result) s_max = self._get_spec_value(form, uid, "max", result) + # shift min/max values according to the result + if s_max < result: + s_max = result + if s_min > result: + s_min = result + # If an error percentage was given, calculate the min/max from the # error percentage if s_err: + # Negative percentage not permitted to prevent min above max + s_err = abs(float(s_err)) s_min = float(result) * (1 - float(s_err)/100) s_max = float(result) * (1 + float(s_err)/100) @@ -233,7 +241,7 @@ def process_form(self, instance, field, form, "result": result, "min": s_min, "max": s_max, - "error": s_err + "error": str(s_err), } return values.values(), {} diff --git a/src/bika/lims/content/referencedefinition.py b/src/bika/lims/content/referencedefinition.py index 869b8afc75..35fb3bc4b0 100644 --- a/src/bika/lims/content/referencedefinition.py +++ b/src/bika/lims/content/referencedefinition.py @@ -18,29 +18,36 @@ # Copyright 2018-2021 by it's authors. # Some rights reserved, see README and LICENSE. -""" Reference Definitions represent standard specifications for - reference samples used in quality control -""" from AccessControl import ClassSecurityInfo -from Products.Archetypes.public import * -from bika.lims.content.bikaschema import BikaSchema +from bika.lims import bikaMessageFactory as _ from bika.lims.browser.fields import ReferenceResultsField from bika.lims.browser.widgets import ReferenceResultsWidget from bika.lims.config import PROJECTNAME -from bika.lims import bikaMessageFactory as _ +from bika.lims.content.bikaschema import BikaSchema from bika.lims.interfaces import IDeactivable +from Products.Archetypes.public import BaseContent +from Products.Archetypes.public import BooleanField +from Products.Archetypes.public import BooleanWidget +from Products.Archetypes.public import Schema +from Products.Archetypes.public import registerType from zope.interface import implements schema = BikaSchema.copy() + Schema(( - ReferenceResultsField('ReferenceResults', - schemata = 'Reference Values', - required = 1, - subfield_validators = { - 'result':'referencevalues_validator',}, - widget = ReferenceResultsWidget( - label=_("Reference Values"), - description =_( - "Click on Analysis Categories (against shaded background" + ReferenceResultsField( + "ReferenceResults", + schemata="Reference Values", + required=1, + subfield_validators={ + "result": "analysisspecs_validator", + "min": "analysisspecs_validator", + "max": "analysisspecs_validator", + }, + widget=ReferenceResultsWidget( + label=_("label_referencedefinition_referencevalues", + default=u"Reference Values"), + description=_( + "description_referencedefinition_referencevalues", + default=u"Click on Analysis Categories " "to see Analysis Services in each category. Enter minimum " "and maximum values to indicate a valid results range. " "Any result outside this range will raise an alert. " @@ -48,41 +55,55 @@ "considered when evaluating results against minimum and " "maximum values. A result out of range but still in range " "if the % error is taken into consideration, will raise a " - "less severe alert."), + "less severe alert." + ), ), ), - BooleanField('Blank', - schemata = 'Description', - default = False, - widget = BooleanWidget( - label=_("Blank"), - description=_("Reference sample values are zero or 'blank'"), + + BooleanField( + "Blank", + schemata="Description", + default=False, + widget=BooleanWidget( + label=_("label_referencedefinition_blank", + default=u"Blank"), + description=_( + "description_referencedefinition_blank", + default=u"Reference sample values are zero or 'blank'" + ), ), ), - BooleanField('Hazardous', - schemata = 'Description', - default = False, - widget = BooleanWidget( - label=_("Hazardous"), - description=_("Samples of this type should be treated as hazardous"), + + BooleanField( + "Hazardous", + schemata="Description", + default=False, + widget=BooleanWidget( + label=_("label_referencedefinition_hazardous", + default=u"Hazardous"), + description=_( + "description_referencedefinition_hazardous", + default=u"Samples of this type should be treated as hazardous" + ), ), ), )) -schema['title'].schemata = 'Description' -schema['title'].widget.visible = True -schema['description'].schemata = 'Description' -schema['description'].widget.visible = True +schema["title"].schemata = "Description" +schema["title"].widget.visible = True +schema["description"].schemata = "Description" +schema["description"].widget.visible = True + class ReferenceDefinition(BaseContent): implements(IDeactivable) security = ClassSecurityInfo() - displayContentsTab = False schema = schema - _at_rename_after_creation = True + def _renameAfterCreation(self, check_auto_id=False): from senaite.core.idserver import renameAfterCreation renameAfterCreation(self) + registerType(ReferenceDefinition, PROJECTNAME) diff --git a/src/bika/lims/content/referencesample.py b/src/bika/lims/content/referencesample.py index 41be8f6805..8716302cb4 100644 --- a/src/bika/lims/content/referencesample.py +++ b/src/bika/lims/content/referencesample.py @@ -170,7 +170,7 @@ schemata = 'Reference Values', required = 1, subfield_validators = { - 'result':'referencevalues_validator',}, + 'result':'analysisspecs_validator',}, widget = ReferenceResultsWidget( label=_("Expected Values"), ), diff --git a/src/bika/lims/validators.py b/src/bika/lims/validators.py index 66fa0ec09b..56fd054b7a 100644 --- a/src/bika/lims/validators.py +++ b/src/bika/lims/validators.py @@ -1004,64 +1004,6 @@ def __call__(self, value, *args, **kwargs): validation.register(DurationValidator()) -class ReferenceValuesValidator: - """Min value must be below max value - Percentage value must be between 0 and 100 - Values must be numbers - Expected values must be between min and max values - """ - - implements(IValidator) - name = "referencevalues_validator" - - def __call__(self, value, *args, **kwargs): - request = kwargs.get('REQUEST', {}) - # Retrieve all AS uids - services = request.get('service', [{}])[0] - for uid, service_name in services.items(): - err_msg = self.validate_service(request, uid) - if not err_msg: - continue - - # Validation failed - err_msg = "{}: {}".format(_("Validation for '{}' failed"), - _(err_msg)) - err_msg = err_msg.format(service_name) - translate = api.get_tool('translation_service').translate - return to_utf8(translate(safe_unicode(err_msg))) - - return True - - def validate_service(self, request, uid): - """Validates the specs values from request for the service uid. Returns - a non-translated message if the validation failed.""" - - result = get_record_value(request, uid, 'result') - if not result: - # No result set for this service, dismiss - return None - - if not api.is_floatable(result): - return "Expected result value must be numeric" - - spec_min = get_record_value(request, uid, "min", result) - spec_max = get_record_value(request, uid, "max", result) - error = get_record_value(request, uid, "error", "0") - if not api.is_floatable(spec_min): - return "'Min' value must be numeric" - if not api.is_floatable(spec_max): - return "'Max' value must be numeric" - if api.to_float(spec_min) > api.to_float(result): - return "'Min' value must be below the expected result" - if api.to_float(spec_max) < api.to_float(result): - return "'Max' value must be above the expected result" - if not api.is_floatable(error) or 0.0 < api.to_float(error) > 100: - return "% Error must be between 0 and 100" - return None - -validation.register(ReferenceValuesValidator()) - - class PercentValidator: """ Floatable, >=0, <=100. """ diff --git a/src/senaite/core/profiles/default/metadata.xml b/src/senaite/core/profiles/default/metadata.xml index 05fb03a002..191ba0c7f7 100644 --- a/src/senaite/core/profiles/default/metadata.xml +++ b/src/senaite/core/profiles/default/metadata.xml @@ -1,6 +1,6 @@ - 2522 + 2523 profile-Products.ATContentTypes:base profile-Products.CMFEditions:CMFEditions diff --git a/src/senaite/core/upgrade/v02_05_000.py b/src/senaite/core/upgrade/v02_05_000.py index 2b1eab6027..2b5407f31d 100644 --- a/src/senaite/core/upgrade/v02_05_000.py +++ b/src/senaite/core/upgrade/v02_05_000.py @@ -576,3 +576,62 @@ def fix_searches_worksheets(tool): obj._p_deactivate() logger.info("Reindexing listing_searchable_text from Worksheets [DONE]") + + +def fix_range_values(tool): + """Fix possible min > max in reference definition/sample ranges + """ + logger.info("Fix min/max for reference definitions and samples ...") + fix_range_values_for(api.search({"portal_type": "ReferenceDefinition"})) + # XXX: Reference Samples live in SENAITE CATALOG + fix_range_values_for(api.search({"portal_type": "ReferenceSample"})) + logger.info("Fix min/max for reference definitions and samples [DONE]") + + +def fix_range_values_for(brains): + """Fix range values for the given brains + """ + total = len(brains) + for num, brain in enumerate(brains): + obj = api.get_object(brain) + reindex = False + logger.info("Checking range values %d/%d: `%s`" % ( + num+1, total, api.get_path(obj))) + rr = obj.getReferenceResults() + for r in rr: + r_key = r.get("keyword") + r_min = api.to_float(r.get("min"), 0) + r_max = api.to_float(r.get("max"), 0) + + # check if max > min + if r_min > r_max: + # set min value to the same as max value + r["min"] = r["max"] + logger.info( + "Fixing range values for service '{r_key}': " + "[{r_min},{r_max}] -> [{new_min},{new_max}]" + .format( + r_key=r_key, + r_min=r_min, + r_max=r_max, + new_min=r["min"], + new_max=r["max"], + )) + reindex = True + + # check if error < 0 + r_err = api.to_float(r.get("error"), 0) + if r_err < 0: + r_err = abs(r_err) + r["error"] = str(r_err) + logger.info( + "Fixing negative error % for service '{r_key}: {r_err}" + .format( + r_key=r_key, + r_err=r["error"], + )) + reindex = True + + if reindex: + obj.reindexObject() + obj._p_deactivate() diff --git a/src/senaite/core/upgrade/v02_05_000.zcml b/src/senaite/core/upgrade/v02_05_000.zcml index 06ed6a410a..4b0bfa0eb8 100644 --- a/src/senaite/core/upgrade/v02_05_000.zcml +++ b/src/senaite/core/upgrade/v02_05_000.zcml @@ -3,6 +3,14 @@ xmlns:genericsetup="http://namespaces.zope.org/genericsetup" i18n_domain="senaite.core"> + +