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

Fix reference definition range validation #2452

Merged
merged 10 commits into from
Dec 20, 2023
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
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.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
Expand Down
10 changes: 9 additions & 1 deletion src/bika/lims/browser/widgets/referenceresultswidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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(), {}
Expand Down
89 changes: 55 additions & 34 deletions src/bika/lims/content/referencedefinition.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,71 +18,92 @@
# 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. "
"The % Error field allows for an % uncertainty to be "
"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)
2 changes: 1 addition & 1 deletion src/bika/lims/content/referencesample.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@
schemata = 'Reference Values',
required = 1,
subfield_validators = {
'result':'referencevalues_validator',},
'result':'analysisspecs_validator',},
widget = ReferenceResultsWidget(
label=_("Expected Values"),
),
Expand Down
58 changes: 0 additions & 58 deletions src/bika/lims/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, thanks for searching over the codebase once again! Fixed in b75d84e



class PercentValidator:
""" Floatable, >=0, <=100. """

Expand Down
2 changes: 1 addition & 1 deletion src/senaite/core/profiles/default/metadata.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0"?>
<metadata>
<version>2522</version>
<version>2523</version>
<dependencies>
<dependency>profile-Products.ATContentTypes:base</dependency>
<dependency>profile-Products.CMFEditions:CMFEditions</dependency>
Expand Down
59 changes: 59 additions & 0 deletions src/senaite/core/upgrade/v02_05_000.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
8 changes: 8 additions & 0 deletions src/senaite/core/upgrade/v02_05_000.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
i18n_domain="senaite.core">

<genericsetup:upgradeStep
title="SENAITE.CORE 2.5.0: Fix min/max range values"
description="Fix possible min > max in range values for reference definitions/samples"
source="2522"
destination="2523"
handler=".v02_05_000.fix_range_values"
profile="senaite.core:default"/>

<genericsetup:upgradeStep
title="SENAITE.CORE 2.5.0: Fix searches in worksheets not working"
description="Reindex the listing_searchable_text index for worksheets"
Expand Down