From 2fc79cc580ac5f06080d5c99682c29eb4d31f79a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Wed, 21 Dec 2022 16:23:28 +0100 Subject: [PATCH 1/3] Fix traceback when retracting an analysis with a detection limit ``` Traceback (innermost last): Module ZServer.ZPublisher.Publish, line 144, in publish Module ZPublisher.mapply, line 85, in mapply Module ZServer.ZPublisher.Publish, line 44, in call_object Module bika.lims.browser.workflow, line 154, in __call__ Module bika.lims.browser.workflow, line 165, in __call__ Module bika.lims.browser.workflow, line 184, in do_action Module bika.lims.workflow, line 123, in doActionFor Module Products.CMFCore.WorkflowTool, line 252, in doActionFor Module Products.CMFCore.WorkflowTool, line 537, in _invokeWithNotification AttributeError: 'exceptions.TypeError' object has no attribute 'with_traceback' ``` --- CHANGES.rst | 3 +- src/bika/lims/content/abstractanalysis.py | 36 ++++----- .../core/profiles/default/metadata.xml | 2 +- .../doctests/WorkflowAnalysisRetract.rst | 76 +++++++++++++++++++ src/senaite/core/upgrade/v02_04_000.py | 45 +++++++++++ src/senaite/core/upgrade/v02_04_000.zcml | 10 +++ 6 files changed, 147 insertions(+), 25 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index b603c8048a..f640249afd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,7 +4,8 @@ Changelog 2.4.0 (unreleased) ------------------ -- #2203 Fix empty date sampled in samples listing when sampling workflow is enabled +- #2204 Fix traceback when retracting an analysis with a detection limit +- #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 diff --git a/src/bika/lims/content/abstractanalysis.py b/src/bika/lims/content/abstractanalysis.py index 9e2111750a..d08c86f750 100644 --- a/src/bika/lims/content/abstractanalysis.py +++ b/src/bika/lims/content/abstractanalysis.py @@ -353,14 +353,12 @@ def getLowerDetectionLimit(self): """ if self.isLowerDetectionLimit(): result = self.getResult() - try: - # in this case, the result itself is the LDL. - return float(result) - except (TypeError, ValueError): - logger.warn("The result for the analysis %s is a lower " - "detection limit, but not floatable: '%s'. " - "Returnig AS's default LDL." % - (self.id, result)) + if api.is_floatable(result): + return result + + logger.warn("The result for the analysis %s is a lower detection " + "limit, but not floatable: '%s'. Returning AS's " + "default LDL." % (self.id, result)) return AbstractBaseAnalysis.getLowerDetectionLimit(self) # Method getUpperDetectionLimit overrides method of class BaseAnalysis @@ -373,14 +371,12 @@ def getUpperDetectionLimit(self): """ if self.isUpperDetectionLimit(): result = self.getResult() - try: - # in this case, the result itself is the LDL. - return float(result) - except (TypeError, ValueError): - logger.warn("The result for the analysis %s is a lower " - "detection limit, but not floatable: '%s'. " - "Returnig AS's default LDL." % - (self.id, result)) + if api.is_floatable(result): + return result + + logger.warn("The result for the analysis %s is an upper detection " + "limit, but not floatable: '%s'. Returning AS's " + "default UDL." % (self.id, result)) return AbstractBaseAnalysis.getUpperDetectionLimit(self) @security.public @@ -487,13 +483,7 @@ def setResult(self, value): # Result prefixed with LDL/UDL oper = val[0] # Strip off LDL/UDL from the result - val = val.replace(oper, "", 1) - # Check if the value is indeterminate / non-floatable - try: - val = float(val) - except (ValueError, TypeError): - val = value - + val = val.replace(oper, "", 1).strip() # We dismiss the operand and the selector visibility unless the user # is allowed to manually set the detection limit or the DL selector # is visible. diff --git a/src/senaite/core/profiles/default/metadata.xml b/src/senaite/core/profiles/default/metadata.xml index 4f236394cc..98cd6ee4fa 100644 --- a/src/senaite/core/profiles/default/metadata.xml +++ b/src/senaite/core/profiles/default/metadata.xml @@ -1,6 +1,6 @@ - 2403 + 2405 profile-Products.ATContentTypes:base profile-Products.CMFEditions:CMFEditions diff --git a/src/senaite/core/tests/doctests/WorkflowAnalysisRetract.rst b/src/senaite/core/tests/doctests/WorkflowAnalysisRetract.rst index 44dd28e146..454c18df45 100644 --- a/src/senaite/core/tests/doctests/WorkflowAnalysisRetract.rst +++ b/src/senaite/core/tests/doctests/WorkflowAnalysisRetract.rst @@ -329,3 +329,79 @@ But the retest does not provide `IRetracted`: >>> retest = analysis.getRetest() >>> IRetracted.providedBy(retest) False + + +Retract an analysis with a result that is a Detection Limit +........................................................... + +Allow the user to manually enter the detection limit as the result: + + >>> Cu.setAllowManualDetectionLimit(True) + +Create the sample: + + >>> sample = new_ar([Cu]) + >>> cu = sample.getAnalyses(full_objects=True)[0] + >>> cu.setResult("< 10") + >>> success = do_action_for(cu, "submit") + >>> cu.getResult() + '10' + + >>> cu.getFormattedResult(html=False) + '< 10' + + >>> cu.isLowerDetectionLimit() + True + + >>> cu.getDetectionLimitOperand() + '<' + +The Detection Limit is not kept on the retest: + + >>> success = do_action_for(analysis, "retract") + >>> retest = analysis.getRetest() + >>> retest.getResult() + '' + + >>> retest.getFormattedResult(html=False) + '' + + >>> retest.isLowerDetectionLimit() + False + + >>> retest.getDetectionLimitOperand() + '' + +Do the same with Upper Detection Limit (UDL): + + >>> sample = new_ar([Cu]) + >>> cu = sample.getAnalyses(full_objects=True)[0] + >>> cu.setResult("> 10") + >>> success = do_action_for(cu, "submit") + >>> cu.getResult() + '10' + + >>> cu.getFormattedResult(html=False) + '> 10' + + >>> cu.isUpperDetectionLimit() + True + + >>> cu.getDetectionLimitOperand() + '>' + +The Detection Limit is not kept on the retest: + + >>> success = do_action_for(analysis, "retract") + >>> retest = analysis.getRetest() + >>> retest.getResult() + '' + + >>> retest.getFormattedResult(html=False) + '' + + >>> retest.isUpperDetectionLimit() + False + + >>> retest.getDetectionLimitOperand() + '' diff --git a/src/senaite/core/upgrade/v02_04_000.py b/src/senaite/core/upgrade/v02_04_000.py index 2d2f12c36b..002b514f5a 100644 --- a/src/senaite/core/upgrade/v02_04_000.py +++ b/src/senaite/core/upgrade/v02_04_000.py @@ -19,6 +19,8 @@ # Some rights reserved, see README and LICENSE. from bika.lims import api +from bika.lims import LDL +from bika.lims import UDL from bika.lims.interfaces import IRejected from bika.lims.interfaces import IRetracted from senaite.core import logger @@ -137,3 +139,46 @@ def import_typeinfo(tool): setup = portal.portal_setup setup.runImportStepFromProfile("profile-bika.lims:default", "typeinfo") setup.runImportStepFromProfile(profile, "typeinfo") + + +def fix_traceback_retract_dl(tool): + """Migrates the values of LDL and UDL of analyses/services to string, as + well as results that are DetectionLimit and stored as floats + """ + logger.info("Migrate LDL, UDL and result fields to string ...") + cat = api.get_tool("uid_catalog") + query = {"portal_type": ["AnalysisService", "Analysis", + "DuplicateAnalysis", "ReferenceAnalysis"]} + brains = cat.search(query) + total = len(brains) + + for num, brain in enumerate(brains): + if num and num % 100 == 0: + logger.info("Migrated {0}/{1} LDL/UDL fields".format(num, total)) + + obj = api.get_object(brain) + + # Migrate UDL to string + field = obj.getField("UpperDetectionLimit") + value = field.get(obj) + if isinstance(value, (int, float)): + field.set(obj, str(value)) + + # Migrate LDL to string + field = obj.getField("LowerDetectionLimit") + value = field.get(obj) + if isinstance(value, (int, float)): + field.set(obj, str(value)) + + # Migrate the result + field = obj.getField("Result") + if field and obj.getDetectionLimitOperand() in [LDL, UDL]: + # The result is the detection limit + result = field.get(obj) + if isinstance(result, (int, float)): + field.set(obj, str(result)) + + # Flush the object from memory + obj._p_deactivate() + + logger.info("Migrate LDL, UDL and result fields to string [DONE]") diff --git a/src/senaite/core/upgrade/v02_04_000.zcml b/src/senaite/core/upgrade/v02_04_000.zcml index 20747d7f94..9107b965ed 100644 --- a/src/senaite/core/upgrade/v02_04_000.zcml +++ b/src/senaite/core/upgrade/v02_04_000.zcml @@ -43,4 +43,14 @@ handler="senaite.core.upgrade.v02_04_000.import_typeinfo" profile="senaite.core:default"/> + + + From f89cf7fb3a451c4a962b15cc13c7d49732e07707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Wed, 21 Dec 2022 16:44:37 +0100 Subject: [PATCH 2/3] Fix doctest --- src/senaite/core/tests/test_calculations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/senaite/core/tests/test_calculations.py b/src/senaite/core/tests/test_calculations.py index da173dc117..60819700ae 100644 --- a/src/senaite/core/tests/test_calculations.py +++ b/src/senaite/core/tests/test_calculations.py @@ -354,7 +354,7 @@ def test_analysis_method_calculation(self): or an.isUpperDetectionLimit(): operator = an.getDetectionLimitOperand() strres = f['analyses'][key].replace(operator, '') - self.assertEqual(an.getResult(), float(strres)) + self.assertEqual(an.getResult(), strres) else: self.assertEqual(an.getResult(), f['analyses'][key]) elif key == self.calcservice.getKeyword(): From 99a41e665b4da65d87e115960de8a1b5d0b88300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Wed, 21 Dec 2022 19:45:04 +0100 Subject: [PATCH 3/3] Remove duplicate changelog entry --- CHANGES.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index d00ed73cb7..f4058c0a67 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,6 @@ Changelog ------------------ - #2204 Fix traceback when retracting an analysis with a detection limit -- #2203 Fix empty date sampled in samples listing when sampling workflow is enabled - #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 - #2197 Use portal as relative path for sticker icons