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 wrong result when "Result options" with "String result" setting enabled #2275

Merged
merged 3 commits into from
Mar 17, 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)
------------------

- #2275 Fix wrong result when both "Result options" and "String" are enabled
- #2273 Improve performance for sample listing index
- #2272 Allow to configure the position of additional value columns

Expand Down
53 changes: 25 additions & 28 deletions src/bika/lims/content/abstractanalysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -857,28 +857,8 @@ def getFormattedResult(self, specs=None, decimalmark='.', sciformat=1,
escaped: e.g: '<' and '>' (LDL and UDL for results like < 23.4).
"""
result = self.getResult()

# 0: The result is a StringResult, return without any formatting
string_result = self.getStringResult() is True
if string_result:
# return without further result handling
return result

# 1. The result is a detection limit, return '< LDL' or '> UDL'
dl = self.getDetectionLimitOperand()
if dl:
try:
res = api.float_to_string(float(result))
fdm = formatDecimalMark(res, decimalmark)
hdl = cgi.escape(dl) if html else dl
return '%s %s' % (hdl, fdm)
except (TypeError, ValueError):
logger.warn(
"The result for the analysis %s is a detection limit, "
"but not floatable: %s" % (self.id, result))
return formatDecimalMark(result, decimalmark=decimalmark)

# 2. Print ResultText of matching ResultOptions
# If result options, return text of matching option
choices = self.getResultOptions()
if choices:
# Create a dict for easy mapping of result options
Expand All @@ -900,14 +880,31 @@ def getFormattedResult(self, specs=None, decimalmark='.', sciformat=1,
except (ValueError, TypeError):
pass

# 3. If the result is not floatable, return it without being formatted
# If string result, return without any formatting
if self.getStringResult():
return result

# If a detection limit, return '< LDL' or '> UDL'
dl = self.getDetectionLimitOperand()
if dl:
try:
res = api.float_to_string(float(result))
fdm = formatDecimalMark(res, decimalmark)
hdl = cgi.escape(dl) if html else dl
return '%s %s' % (hdl, fdm)
except (TypeError, ValueError):
logger.warn(
"The result for the analysis %s is a detection limit, "
"but not floatable: %s" % (self.id, result))
return formatDecimalMark(result, decimalmark=decimalmark)

# If not floatable, return without any formatting
try:
result = float(result)
except (TypeError, ValueError):
return formatDecimalMark(result, decimalmark=decimalmark)

# 4. If the analysis specs has enabled hidemin or hidemax and the
# result is out of range, render result as '<min' or '>max'
# If specs are set, evaluate if out of range
specs = specs if specs else self.getResultsRange()
hidemin = specs.get('hidemin', '')
hidemax = specs.get('hidemax', '')
Expand All @@ -920,17 +917,17 @@ def getFormattedResult(self, specs=None, decimalmark='.', sciformat=1,
except (TypeError, ValueError):
abovemax = False

# 4.1. If result is below min and hidemin enabled, return '<min'
# If below min and hidemin enabled, return '<min'
if belowmin:
fdm = formatDecimalMark('< %s' % hidemin, decimalmark)
return fdm.replace('< ', '&lt; ', 1) if html else fdm

# 4.2. If result is above max and hidemax enabled, return '>max'
# If above max and hidemax enabled, return '>max'
if abovemax:
fdm = formatDecimalMark('> %s' % hidemax, decimalmark)
return fdm.replace('> ', '&gt; ', 1) if html else fdm

# Below Lower Detection Limit (LDL)?
# If below LDL, return '< LDL'
ldl = self.getLowerDetectionLimit()
ldl = api.to_float(ldl, 0.0)
if result < ldl:
Expand All @@ -939,7 +936,7 @@ def getFormattedResult(self, specs=None, decimalmark='.', sciformat=1,
fdm = formatDecimalMark('< %s' % ldl, decimalmark)
return fdm.replace('< ', '&lt; ', 1) if html else fdm

# Above Upper Detection Limit (UDL)?
# If above UDL, return '< UDL'
udl = self.getUpperDetectionLimit()
udl = api.to_float(udl, 0.0)
if result > udl:
Expand Down
146 changes: 146 additions & 0 deletions src/senaite/core/tests/doctests/ResultOptions.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
Result Options
--------------

An analysis can be configured so a selection list with options are displayed
for selection rather than an input text for manual introduction of a value.

Running this test from the buildout directory::

bin/test test_textual_doctests -t ResultOptions


Test Setup
..........

Needed Imports:

>>> from bika.lims import api
>>> from bika.lims.utils.analysisrequest import create_analysisrequest
>>> from bika.lims.workflow import doActionFor as do_action_for
>>> from DateTime import DateTime
>>> from plone.app.testing import setRoles
>>> from plone.app.testing import TEST_USER_ID
>>> from plone.app.testing import TEST_USER_PASSWORD

Functional Helpers:

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

>>> def timestamp(format="%Y-%m-%d"):
... return DateTime().strftime(format)

>>> def new_sample(services):
... values = {
... 'Client': client.UID(),
... 'Contact': contact.UID(),
... 'DateSampled': date_now,
... 'SampleType': sampletype.UID()}
... service_uids = map(api.get_uid, services)
... ar = create_analysisrequest(client, request, values, service_uids)
... transitioned = do_action_for(ar, "receive")
... return ar

>>> def get_analysis(sample, service):
... service_uid = api.get_uid(service)
... for analysis in sample.getAnalyses(full_objects=True):
... if analysis.getServiceUID() == service_uid:
... return analysis
... return None

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

Variables:

>>> portal = self.portal
>>> request = self.request
>>> setup = api.get_setup()
>>> date_now = DateTime().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)
>>> 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="Zinc", Keyword="Zn", Price="20", Category=category.UID())

Apply result options to the services:

>>> options = [
... {"ResultValue": "0", "ResultText": "Zero"},
... {"ResultValue": "1", "ResultText": "One"},
... {"ResultValue": "2", "ResultText": "Two"},
... {"ResultValue": "3", "ResultText": "Three"},
... ]
>>> services = [Cu, Fe, Au, Zn]
>>> for service in services:
... service.setResultOptions(options)

And a different control type for each service

>>> Cu.setResultOptionsType("select")
>>> Fe.setResultOptionsType("multiselect")
>>> Au.setResultOptionsType("multiselect_duplicates")
>>> Zn.setResultOptionsType("multichoice")

Test formatted result
.....................

The system returns the option text as the formatted result:

>>> sample = new_sample([Cu, Fe, Au, Zn])

>>> cu = get_analysis(sample, Cu)
>>> cu.setResult('0')
>>> cu.getResult()
'0'
>>> cu.getFormattedResult()
'Zero'

>>> fe = get_analysis(sample, Fe)
>>> fe.setResult(['0', '1'])
>>> fe.getResult()
'["0", "1"]'
>>> fe.getFormattedResult()
'Zero<br/>One'

>>> au = get_analysis(sample, Au)
>>> au.setResult(['0', '1', '1'])
>>> au.getResult()
'["0", "1", "1"]'
>>> au.getFormattedResult()
'Zero<br/>One<br/>One'

>>> zn = get_analysis(sample, Zn)
>>> zn.setResult(['0', '1'])
>>> zn.getResult()
'["0", "1"]'
>>> zn.getFormattedResult()
'Zero<br/>One'

Even if the analysis has the "String result" setting enabled:

>>> analyses = [cu, fe, au, zn]
>>> for analysis in analyses:
... analysis.setStringResult(True)

>>> cu.getFormattedResult()
'Zero'
>>> fe.getFormattedResult()
'Zero<br/>One'
>>> au.getFormattedResult()
'Zero<br/>One<br/>One'
>>> zn.getFormattedResult()
'Zero<br/>One'