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

Allow manual selection of units on results entry #2201

Merged
merged 18 commits into from
Feb 21, 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
3 changes: 2 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changelog
2.4.0 (unreleased)
------------------

- #2201 Allow manual selection of units on results entry
- #2258 Reduce conflict errors on number generation
- #2256 Do not keep DX UID reference field back-references per default
- #2257 Fix UnicodeEncode error when viewing report objects
Expand Down Expand Up @@ -39,7 +40,7 @@ Changelog
- #2208 Remove `default_method` from AnalysisRequest's Contact field
- #2204 Fix traceback when retracting an analysis with a detection limit
- #2202 Fix detection limit set manually is not displayed on result save
- #2203 Fix empty date sampled in samples listing when sampling workflow is enabled
- #2203 Fix empty date sampled in samples listing when sampling workflow is enabled
- #2197 Use portal as relative path for sticker icons
- #2196 Order sample analyses by sortable title on get per default
- #2193 Fix analyst cannot import results from instruments
Expand Down
100 changes: 95 additions & 5 deletions src/bika/lims/browser/analyses/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,19 @@ def __init__(self, context, request, **kwargs):
"input_class": "ajax_calculate numeric",
"ajax": True,
"sortable": False}),
("Specification", {
"title": _("Specification"),
"sortable": False}),
("Uncertainty", {
"title": _("+-"),
"ajax": True,
"sortable": False}),
("Unit", {
"title": _("Unit"),
"sortable": False,
"ajax": True,
"on_change": "_on_unit_change",
"toggle": True}),
("Specification", {
"title": _("Specification"),
"sortable": False}),
("retested", {
"title": _("Retested"),
"type": "boolean",
Expand Down Expand Up @@ -454,6 +460,31 @@ def get_methods_vocabulary(self, analysis_brain):
})
return vocab

def get_unit_vocabulary(self, analysis_brain):
"""Returns a vocabulary with all the units available for the passed in
analysis.

The vocabulary is a list of dictionaries. Each dictionary has the
following structure:

{'ResultValue': <unit>,
'ResultText': <unit>}

:param analysis_brain: A single Analysis brain
:type analysis_brain: CatalogBrain
:returns: A list of dicts
"""
obj = self.get_object(analysis_brain)
# Get unit choices
unit_choices = obj.getUnitChoices()
vocab = []
for unit in unit_choices:
vocab.append({
"ResultValue": unit['value'],
"ResultText": unit['value'],
})
return vocab

def get_instruments_vocabulary(self, analysis, method=None):
"""Returns a vocabulary with the valid and active instruments available
for the analysis passed in.
Expand Down Expand Up @@ -664,6 +695,8 @@ def folderitem(self, obj, item, index):
self._folder_item_result(obj, item)
# Fill calculation and interim fields
self._folder_item_calculation(obj, item)
# Fill unit field
self._folder_item_unit(obj, item)
# Fill method
self._folder_item_method(obj, item)
# Fill instrument
Expand Down Expand Up @@ -778,14 +811,15 @@ def folderitems(self):
# analyses requires them to be displayed for selection
self.columns["Method"]["toggle"] = self.is_method_column_required()
self.columns["Instrument"]["toggle"] = self.is_instrument_column_required()
self.columns["Unit"]["toggle"] = self.is_unit_selection_column_required()

return items

def render_unit(self, unit, css_class=None):
"""Render HTML element for unit
"""
if css_class is None:
css_class = "unit d-inline-block py-2 small text-secondary"
css_class = "unit d-inline-block py-2 small text-secondary text-nowrap"
return "<span class='{css_class}'>{unit}</span>".format(
unit=unit, css_class=css_class)

Expand Down Expand Up @@ -1059,6 +1093,21 @@ def _folder_item_calculation(self, analysis_brain, item):
item["interimfields"] = interim_fields
self.interim_fields[analysis_brain.UID] = interim_fields

def _folder_item_unit(self, analysis_brain, item):
"""Fills the analysis' unit to the item passed in.

:param analysis_brain: Brain that represents an analysis
:param item: analysis' dictionary counterpart that represents a row
"""
if not self.is_analysis_edition_allowed(analysis_brain):
return

# Edition allowed
voc = self.get_unit_vocabulary(analysis_brain)
if voc:
item["choices"]["Unit"] = voc
item["allow_edit"].append("Unit")

def _folder_item_method(self, analysis_brain, item):
"""Fills the analysis' method to the item passed in.

Expand Down Expand Up @@ -1128,6 +1177,19 @@ def _folder_item_instrument(self, analysis_brain, item):
else:
item["Instrument"] = _("Manual")

def _on_unit_change(self, uid=None, value=None, item=None, **kw):
""" updates the rendered unit on selection of unit.
"""
RML-IAEA marked this conversation as resolved.
Show resolved Hide resolved
if not all([value, item]):
return None
item["after"]["Result"] = self.render_unit(value)
uncertainty = item.get("Uncertainty")
if uncertainty:
item["after"]["Uncertainty"] = self.render_unit(value)
elif "Uncertainty" in item["allow_edit"]:
item["after"]["Uncertainty"] = self.render_unit(value)
return item

def _folder_item_analyst(self, obj, item):
obj = self.get_object(obj)
analyst = obj.getAnalyst()
Expand Down Expand Up @@ -1195,15 +1257,21 @@ def _folder_item_uncertainty(self, analysis_brain, item):
allow_edit = self.is_uncertainty_edition_allowed(analysis_brain)
if allow_edit:
item["Uncertainty"] = obj.getUncertainty()
item["before"]["Uncertainty"] = "± "
item["allow_edit"].append("Uncertainty")
unit = item.get("Unit")
if unit:
item["after"]["Uncertainty"] = self.render_unit(unit)
return

formatted = format_uncertainty(
obj, decimalmark=self.dmk, sciformat=int(self.scinot))
if formatted:
item["replace"]["Uncertainty"] = formatted
item["before"]["Uncertainty"] = "± "
item["after"]["Uncertainty"] = obj.getUnit()
unit = item.get("Unit")
if unit:
item["after"]["Uncertainty"] = self.render_unit(unit)

def _folder_item_detection_limits(self, analysis_brain, item):
"""Fills the analysis' detection limits to the item passed in.
Expand Down Expand Up @@ -1573,6 +1641,17 @@ def is_instrument_required(self, analysis):
# a method is selected
return len(instruments) > 0

def is_unit_choices_required(self, analysis):
"""Returns whether the render of the unit choice selection list is
required for the analysis passed-in.
:param analysis: Brain or object that represents an analysis
"""
# Always return true if the analysis has unitchoices
analysis = self.get_object(analysis)
if analysis.getUnitChoices():
return True
xispa marked this conversation as resolved.
Show resolved Hide resolved
return False

def is_method_column_required(self):
"""Returns whether the method column has to be rendered or not.
Returns True if at least one of the analyses from the listing requires
Expand All @@ -1594,3 +1673,14 @@ def is_instrument_column_required(self):
if self.is_instrument_required(obj):
return True
return False

def is_unit_selection_column_required(self):
"""Returns whether the unit column has to be rendered or not.
Returns True if at least one of the analyses from the listing requires
the list for unit selection to be rendered
"""
for item in self.items:
obj = item.get("obj")
if self.is_unit_choices_required(obj):
return True
return False
54 changes: 44 additions & 10 deletions src/bika/lims/content/abstractbaseanalysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,51 @@
Unit = StringField(
'Unit',
schemata="Description",
write_permission=FieldEditAnalysisResult,
widget=StringWidget(
label=_("Unit"),
label=_(
u"label_analysis_unit",
default=u"Default Unit"
),
description=_(
"The measurement units for this analysis service' results, e.g. "
"mg/l, ppm, dB, mV, etc."),
u"description_analysis_unit",
default=u"The measurement units for this analysis service' "
u"results, e.g. mg/l, ppm, dB, mV, etc."
),
)
)

# A selection of units that are able to update Unit.
UnitChoices = RecordsField(
"UnitChoices",
schemata="Description",
type="UnitChoices",
subfields=(
"value",
),
subfield_labels={
"value": u"",
},
subfield_types={
"value": "string",
},
subfield_sizes={
"value": 20,
},
subfield_maxlength={
"value": 50,
},
widget=RecordsWidget(
label=_(
u"label_analysis_unitchoices",
default=u"Units for Selection"
),
description=_(
u"description_analysis_unitchoices",
default=u"Provide a list of units that are suitable for the "
u"analysis. Ensure to include the default unit in this "
u"list"
),
)
)

Expand Down Expand Up @@ -686,6 +726,7 @@
ProtocolID,
ScientificName,
Unit,
UnitChoices,
Precision,
ExponentialFormatPrecision,
LowerDetectionLimit,
Expand Down Expand Up @@ -745,13 +786,6 @@ def _getCatalogTool(self):
def Title(self):
return _c(self.title)

@security.public
def getUnit(self):
"""Returns the Unit
"""
unit = self.Schema().getField("Unit").get(self) or ""
return unit.strip()

@security.public
def getDefaultVAT(self):
"""Return default VAT from bika_setup
Expand Down