Skip to content

Commit

Permalink
Support multi-line text on result entry (#2537)
Browse files Browse the repository at this point in the history
* Replace ResultOptionsType by ResultType

* Added upgrade step

* Fix validator for default result

* Remove noisy deprecated decorator

* Cleanup

* Fix doctests

* setResultOptionsType --> setResultType

* Simplify

* Fix doctests

* Added doctest

* Changelog updated

---------

Co-authored-by: Ramon Bartl <rb@ridingbytes.com>
  • Loading branch information
xispa and ramonski committed Apr 29, 2024
1 parent 1256dd2 commit 9080614
Show file tree
Hide file tree
Showing 12 changed files with 273 additions and 60 deletions.
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.6.0 (unreleased)
------------------

- #2537 Support multi-line text on result entry
- #2536 Fix counts from control-panel includes client-specific items
- #2535 Fix client-specific analysis profiles are displayed under setup
- #2534 Fix client-specific sample templates are displayed under setup
Expand Down
13 changes: 5 additions & 8 deletions src/bika/lims/browser/analyses/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -993,20 +993,17 @@ def _folder_item_result(self, analysis_brain, item):
item["Result"] = "{} {}".format(operand, result).strip()

# Prepare result options
result_type = obj.getResultType()
item["result_type"] = result_type

choices = self.get_result_options(obj)
if choices:
choices_type = obj.getResultOptionsType()
if choices_type == "select":
if result_type == "select":
# By default set empty as the default selected choice
choices.insert(0, dict(ResultValue="", ResultText=""))
item["choices"]["Result"] = choices
item["result_type"] = choices_type

elif obj.getStringResult():
item["result_type"] = "string"

else:
item["result_type"] = "numeric"
if result_type == "numeric":
item["help"]["Result"] = _(
"Enter the result either in decimal or scientific "
"notation, e.g. 0.00005 or 1e-5, 10000 or 1e5")
Expand Down
6 changes: 2 additions & 4 deletions src/bika/lims/content/abstractanalysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@
relationship='AnalysisAttachment'
)

# The final result of the analysis is stored here. The field contains a
# String value, but the result itself is required to be numeric. If
# a non-numeric result is needed, ResultOptions can be used.
# The final result of the analysis is stored here
Result = StringField(
'Result',
read_permission=ViewResults,
Expand Down Expand Up @@ -466,7 +464,7 @@ def setResult(self, value):
:param value: is expected to be a string.
"""
# Convert to list ff the analysis has result options set with multi
if self.getResultOptions() and "multi" in self.getResultOptionsType():
if self.getResultOptions() and "multi" in self.getResultType():
if not isinstance(value, (list, tuple)):
value = filter(None, [value])

Expand Down
73 changes: 50 additions & 23 deletions src/bika/lims/content/abstractbaseanalysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,28 @@
)
)

RESULT_TYPES = (
("numeric", _("Numeric")),
("string", _("String")),
("text", _("Text")),
("select", _("Selection list")),
("multiselect", _("Multiple selection")),
("multiselect_duplicates", _("Multiple selection (with duplicates)")),
("multichoice", _("Multiple choices")),
)

# Type of control to be rendered on results entry
ResultType = StringField(
"ResultType",
schemata="Result Options",
default="numeric",
vocabulary=DisplayList(RESULT_TYPES),
widget=SelectionWidget(
label=_("Result type"),
format="select",
)
)

# Results can be selected from a dropdown list. This prevents the analyst
# from entering arbitrary values. Each result must have a ResultValue, which
# must be a number - it is this number which is interpreted as the actual
Expand Down Expand Up @@ -611,25 +633,12 @@
)
)

RESULT_OPTIONS_TYPES = (
("select", _("Selection list")),
("multiselect", _("Multiple selection")),
("multiselect_duplicates", _("Multiple selection (with duplicates)")),
("multichoice", _("Multiple choices")),
)

# TODO Remove ResultOptionsType field. It was Replaced by ResultType
ResultOptionsType = StringField(
"ResultOptionsType",
schemata="Result Options",
default="select",
vocabulary=DisplayList(RESULT_OPTIONS_TYPES),
widget=SelectionWidget(
label=_("Control type"),
description=_(
"Type of control to be displayed on result entry when predefined "
"results are set"
),
format="select",
readonly=True,
widget=StringWidget(
visible=False,
)
)

Expand Down Expand Up @@ -663,15 +672,12 @@
)

# Allow/disallow the capture of text as the result of the analysis
# TODO Remove StringResult field. It was Replaced by ResultType
StringResult = BooleanField(
"StringResult",
schemata="Analysis",
default=False,
readonly=True,
widget=BooleanWidget(
label=_("String result"),
description=_(
"Enable this option to allow the capture of text as result"
)
visible=False,
)
)

Expand Down Expand Up @@ -794,6 +800,7 @@
Uncertainties,
PrecisionFromUncertainty,
AllowManualUncertainty,
ResultType,
ResultOptions,
ResultOptionsType,
ResultOptionsSorting,
Expand Down Expand Up @@ -1071,3 +1078,23 @@ def getMaxTimeAllowed(self):
"""
tat = self.Schema().getField("MaxTimeAllowed").get(self)
return tat or self.bika_setup.getDefaultTurnaroundTime()

# TODO Remove. ResultOptionsType field was replaced by ResulType field
def getResultOptionsType(self):
if self.getStringResult():
return "select"
return self.getResultType()

# TODO Remove. ResultOptionsType field was replaced by ResulType field
def setResultOptionsType(self, value):
self.setResultType(value)

# TODO Remove. StringResults field was replaced by ResulType field
def getStringResult(self):
result_type = self.getResultType()
return result_type in ["string", "text"]

# TODO Remove. StringResults field was replaced by ResulType field
def setStringResult(self, value):
result_type = "string" if bool(value) else "numeric"
self.setResultType(result_type)
6 changes: 3 additions & 3 deletions src/bika/lims/content/analysisservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@
# Allow/disallow the capture of text as the result of the analysis
DefaultResult = StringField(
"DefaultResult",
schemata="Analysis",
schemata="Result Options",
validators=('service_defaultresult_validator',),
widget=StringWidget(
label=_("Default result"),
Expand Down Expand Up @@ -292,8 +292,8 @@
schema.moveField("Method", after="Methods")
# Move default instrument field after available instruments field
schema.moveField("Instrument", after="Instruments")
# Move default result field after String result
schema.moveField("DefaultResult", after="StringResult")
# Move default result field after Result Options
schema.moveField("DefaultResult", after="ResultOptions")


class AnalysisService(AbstractBaseAnalysis):
Expand Down
34 changes: 17 additions & 17 deletions src/bika/lims/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -1355,27 +1355,27 @@ class DefaultResultValidator(object):
name = "service_defaultresult_validator"

def __call__(self, value, **kwargs):
instance = kwargs['instance']
request = kwargs.get('REQUEST', {})
field_name = kwargs['field'].getName()
translate = getToolByName(instance, 'translation_service').translate

default_result = request.get(field_name, None)
if default_result:
# Default result must be one of the available result options
options = request.get("ResultOptions", None)
if options:
values = map(lambda ro: ro.get("ResultValue"), options)
if default_result not in values:
msg = _("Default result must be one of the following "
"result options: {}").format(", ".join(values))
return to_utf8(translate(msg))

elif not request.get("StringResult"):
# Default result must be numeric
if not api.is_floatable(default_result):
msg = _("Default result is not numeric")
return to_utf8(translate(msg))
if not default_result:
return True

result_type = request.get("ResultType")
if result_type in ["string", "text"]:
return True

elif result_type == "numeric":
if not api.is_floatable(default_result):
return _t(_("Default result is not numeric"))

else:
options = request.get("ResultOptions", [])
values = map(lambda ro: ro.get("ResultValue"), options)
if default_result not in values:
return _t(_("Default result must be one of the following "
"result options: {}").format(", ".join(values)))

return True

Expand Down
20 changes: 20 additions & 0 deletions src/senaite/core/browser/form/adapters/analysisservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ def initialized(self, data):
self.add_readonly_field(
"Keyword", _("Keyword is used in active analyses "
"and can not be changed anymore"))
# Show/hide predefined options
result_type = form.get("ResultType")
self.toggle_result_type(result_type)

return self.data

Expand Down Expand Up @@ -115,6 +118,10 @@ def modified(self, data):
self.add_update_field("Instrument", {
"options": empty + i_opts})

# Show/hide predefined options
elif name == "ResultType":
self.toggle_result_type(value)

return self.data

def get_available_instruments_for(self, methods):
Expand Down Expand Up @@ -185,3 +192,16 @@ def validate_keyword(self, value):
if isinstance(check, string_types):
return _(check)
return None

def toggle_result_type(self, result_type):
"""Hides/Show result options depending on the resulty type
"""
if result_type and api.is_list(result_type):
return self.toggle_result_type(result_type[0])

if result_type in ["numeric", "string", "text"]:
self.add_hide_field("ResultOptions")
self.add_hide_field("ResultOptionsSorting")
else:
self.add_show_field("ResultOptions")
self.add_show_field("ResultOptionsSorting")
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>2618</version>
<version>2619</version>
<dependencies>
<dependency>profile-Products.ATContentTypes:base</dependency>
<dependency>profile-Products.CMFEditions:CMFEditions</dependency>
Expand Down
8 changes: 4 additions & 4 deletions src/senaite/core/tests/doctests/ResultOptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,10 @@ Apply result options to the services:

And a different control type for each service

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

Test formatted result
.....................
Expand Down

0 comments on commit 9080614

Please sign in to comment.