def getFormattedResult(self, specs=None, decimalmark='.', sciformat=1): """Formatted result: 1. If the result is not floatable, return it without being formatted 2. If the analysis specs has hidemin or hidemax enabled and the result is out of range, render result as '<min' or '>max' 3. If the result is floatable, render it to the correct precision specs param is optional. A dictionary as follows: {'min': <min_val>, 'max': <max_val>, 'error': <error>, 'hidemin': <hidemin_val>, 'hidemax': <hidemax_val>} :param sciformat: 1. The sci notation has to be formatted as aE^+b 2. The sci notation has to be formatted as a·10^b 3. As 2, but with super html entity for exp 4. The sci notation has to be formatted as a·10^b 5. As 4, but with super html entity for exp By default 1 """ result = self.getResult() service = self.getService() # 1. If the result is not floatable, return it without being formatted try: result = float(result) except: return result # 2. If the analysis specs has enabled hidemin or hidemax and the # result is out of range, render result as '<min' or '>max' belowmin = False abovemax = False if not specs: uid = self.getServiceUID() specs = self.aq_parent.getResultsRangeDict().get(uid, {}) hidemin = specs.get('hidemin', '') hidemax = specs.get('hidemax', '') try: belowmin = hidemin and result < float(hidemin) or False except: belowmin = False pass try: abovemax = hidemax and result > float(hidemax) or False except: abovemax = False pass # 2.1. If result is below min and hidemin enabled, return '<min' if belowmin: return formatDecimalMark('< %s' % hidemin, decimalmark) # 2.2. If result is above max and hidemax enabled, return '>max' if abovemax: return formatDecimalMark('> %s' % hidemax, decimalmark) # 3. If the result is floatable, render it to the correct precision return formatDecimalMark( format_numeric_result(self, result, sciformat), decimalmark)
def getFormattedResult(self, specs=None, decimalmark='.', sciformat=1): """Formatted result: 1. If the result is not floatable, return it without being formatted 2. If the analysis specs has hidemin or hidemax enabled and the result is out of range, render result as '<min' or '>max' 3. If the result is floatable, render it to the correct precision specs param is optional. A dictionary as follows: {'min': <min_val>, 'max': <max_val>, 'error': <error>, 'hidemin': <hidemin_val>, 'hidemax': <hidemax_val>} :param sciformat: 1. The sci notation has to be formatted as aE^+b 2. The sci notation has to be formatted as a·10^b 3. As 2, but with super html entity for exp 4. The sci notation has to be formatted as a·10^b 5. As 4, but with super html entity for exp By default 1 """ result = self.getResult() service = self.getService() # 1. If the result is not floatable, return it without being formatted try: result = float(result) except: return result # 2. If the analysis specs has enabled hidemin or hidemax and the # result is out of range, render result as '<min' or '>max' belowmin = False abovemax = False if not specs: uid = self.getServiceUID() specs = self.aq_parent.getResultsRangeDict().get(uid, {}) hidemin = specs.get('hidemin', '') hidemax = specs.get('hidemax', '') try: belowmin = hidemin and result < float(hidemin) or False except: belowmin = False pass try: abovemax = hidemax and result > float(hidemax) or False except: abovemax = False pass # 2.1. If result is below min and hidemin enabled, return '<min' if belowmin: return formatDecimalMark('< %s' % hidemin, decimalmark) # 2.2. If result is above max and hidemax enabled, return '>max' if abovemax: return formatDecimalMark('> %s' % hidemax, decimalmark) # 3. If the result is floatable, render it to the correct precision return formatDecimalMark(format_numeric_result(self, result, sciformat), decimalmark)
def getFormattedResult(self): """Formatted result: 1. Print ResultText of matching ResultOptions 2. If the result is not floatable, return it without being formatted 3. If the analysis specs has hidemin or hidemax enabled and the result is out of range, render result as '<min' or '>max' 4. Otherwise, render numerical value """ result = self.getResult() service = self.getService() choices = service.getResultOptions() # 1. Print ResultText of matching ResulOptions match = [x['ResultText'] for x in choices if str(x['ResultValue']) == str(result)] if match: return match[0] # 2. If the result is not floatable, return it without being formatted try: result = float(result) except: return result # 3. If the analysis specs has enabled hidemin or hidemax and the # result is out of range, render result as '<min' or '>max' belowmin = False abovemax = False specs = self.getAnalysisSpecs() specs = specs.getResultsRangeDict() if specs is not None else {} specs = specs.get(self.getKeyword(), {}) hidemin = specs.get('hidemin', '') hidemax = specs.get('hidemax', '') try: belowmin = hidemin and result < float(hidemin) or False except: belowmin = False pass try: abovemax = hidemax and result > float(hidemax) or False except: abovemax = False pass # 3.1. If result is below min and hidemin enabled, return '<min' if belowmin: return '< %s' % hidemin # 3.2. If result is above max and hidemax enabled, return '>max' if abovemax: return '> %s' % hidemax # Render numerical value return format_numeric_result(self, result)
def calculate(self, uid=None): analysis = self.analyses[uid] form_result = self.current_results[uid] service = analysis.getService() precision = service.getPrecision() calculation = service.getCalculation() if analysis.portal_type == 'ReferenceAnalysis': deps = {} else: deps = {} for dep in analysis.getDependencies(): deps[dep.UID()] = dep path = '++resource++bika.lims.images' mapping = {} # values to be returned to form for this UID Result = {'uid': uid, 'result': form_result} try: Result['result'] = float(form_result) except: if form_result == "0/0": Result['result'] = "" if calculation: # add all our dependent analyses results to the mapping. # Retrieve value from database if it's not in the current_results. unsatisfied = False for dependency_uid, dependency in deps.items(): if dependency_uid in self.ignore_uids: unsatisfied = True break if dependency_uid in self.current_results: result = self.current_results[dependency_uid] else: result = dependency.getResult() if result == '': unsatisfied = True break key = dependency.getService().getKeyword() # All mappings must be float, or they are ignored. try: mapping[key] = float(self.current_results[dependency_uid]) except: pass if unsatisfied: # unsatisfied means that one or more result on which we depend # is blank or unavailable, so we set blank result and abort. self.results.append({ 'uid': uid, 'result': '', 'formatted_result': '' }) return None # Add all interims to mapping for i_uid, i_data in self.item_data.items(): for i in i_data: # if this interim belongs to current analysis and is blank, # return an empty result for this analysis. if i_uid == uid and i['value'] == '': self.results.append({ 'uid': uid, 'result': '', 'formatted_result': '' }) return None # All interims must be float, or they are ignored. try: i['value'] = float(i['value']) except: pass # all interims are ServiceKeyword.InterimKeyword if i_uid in deps: key = "%s.%s" % (deps[i_uid].getService().getKeyword(), i['keyword']) mapping[key] = i['value'] # this analysis' interims get extra reference # without service keyword prefix if uid == i_uid: mapping[i['keyword']] = i['value'] # Grab values for hidden InterimFields for only for current calculation # we can't allow non-floats through here till we change the eval's # interpolation hidden_fields = [] c_fields = calculation.getInterimFields() s_fields = service.getInterimFields() for field in c_fields: if field.get('hidden', False): hidden_fields.append(field['keyword']) try: mapping[field['keyword']] = float(field['value']) except ValueError: pass # also grab stickier defaults from AnalysisService for field in s_fields: if field['keyword'] in hidden_fields: try: mapping[field['keyword']] = float(field['value']) except ValueError: pass # convert formula to a valid python string, ready for interpolation formula = calculation.getMinifiedFormula() formula = formula.replace('[', '%(').replace(']', ')f') try: formula = eval("'%s'%%mapping" % formula, { "__builtins__": None, 'math': math, 'context': self.context }, {'mapping': mapping}) # calculate result = eval(formula) Result['result'] = result self.current_results[uid] = result except TypeError as e: # non-numeric arguments in interim mapping? alert = { 'field': 'Result', 'icon': path + '/exclamation.png', 'msg': "{0}: {1} ({2}) ".format(t(_("Type Error")), html_quote(str(e.args[0])), formula) } if uid in self.alerts: self.alerts[uid].append(alert) else: self.alerts[uid] = [ alert, ] except ZeroDivisionError as e: Result['result'] = '0/0' Result['formatted_result'] = '0/0' self.current_results[uid] = '0/0' self.results.append(Result) alert = { 'field': 'Result', 'icon': path + '/exclamation.png', 'msg': "{0}: {1} ({2}) ".format(t(_("Division by zero")), html_quote(str(e.args[0])), formula) } if uid in self.alerts: self.alerts[uid].append(alert) else: self.alerts[uid] = [ alert, ] return None except KeyError as e: alert = { 'field': 'Result', 'icon': path + '/exclamation.png', 'msg': "{0}: {1} ({2}) ".format(t(_("Key Error")), html_quote(str(e.args[0])), formula) } if uid in self.alerts: self.alerts[uid].append(alert) else: self.alerts[uid] = [ alert, ] # format result belowmin = False abovemax = False # Some analyses will not have AnalysisSpecs, eg, ReferenceAnalysis if hasattr(analysis, 'getAnalysisSpecs'): specs = analysis.getAnalysisSpecs() specs = specs.getResultsRangeDict() if specs is not None else {} specs = specs.get(analysis.getKeyword(), {}) hidemin = specs.get('hidemin', '') hidemax = specs.get('hidemax', '') if Result.get('result', ''): fresult = Result['result'] try: belowmin = hidemin and fresult < float(hidemin) or False except ValueError: belowmin = False pass try: abovemax = hidemax and fresult > float(hidemax) or False except ValueError: abovemax = False pass if belowmin is True: Result['formatted_result'] = '< %s' % hidemin elif abovemax is True: Result['formatted_result'] = '> %s' % hidemax else: try: Result['formatted_result'] = format_numeric_result( analysis, Result['result']) except ValueError: # non-float Result['formatted_result'] = Result['result'] # calculate Dry Matter result # if parent is not an AR, it's never going to be calculable dm = hasattr(analysis.aq_parent, 'getReportDryMatter') and \ analysis.aq_parent.getReportDryMatter() and \ analysis.getService().getReportDryMatter() if dm: dry_service = self.context.bika_setup.getDryMatterService() # get the UID of the DryMatter Analysis from our parent AR dry_analysis = [ a for a in analysis.aq_parent.getAnalyses(full_objects=True) if a.getService().UID() == dry_service.UID() ] if dry_analysis: dry_analysis = dry_analysis[0] dry_uid = dry_analysis.UID() # get the current DryMatter analysis result from the form if dry_uid in self.current_results: try: dry_result = float(self.current_results[dry_uid]) except: dm = False else: try: dry_result = float(dry_analysis.getResult()) except: dm = False else: dm = False Result['dry_result'] = dm and dry_result and \ '%.2f' % ((Result['result'] / dry_result) * 100) or '' self.results.append(Result) # if App.config.getConfiguration().debug_mode: # logger.info("calc.py: %s->%s %s" % (analysis.aq_parent.id, # analysis.id, # Result)) self.uncertainties.append({ 'uid': uid, 'uncertainty': analysis.getUncertainty(Result['result']) }) # maybe a service who depends on us must be recalculated. if analysis.portal_type == 'ReferenceAnalysis': dependents = [] else: dependents = analysis.getDependents() if dependents: for dependent in dependents: dependent_uid = dependent.UID() # ignore analyses that no longer exist. if dependent_uid in self.ignore_uids or \ dependent_uid not in self.analyses: continue self.calculate(dependent_uid) # These self.alerts are just for the json return. # we're placing the entire form's results in kwargs. adapters = getAdapters((analysis, ), IFieldIcons) for name, adapter in adapters: alerts = adapter(result=Result['result'], form_results=self.current_results) if alerts: if analysis.UID() in self.alerts: self.alerts[analysis.UID()].extend(alerts[analysis.UID()]) else: self.alerts[analysis.UID()] = alerts[analysis.UID()]
def getFormattedResult(self, specs=None, decimalmark='.', sciformat=1, html=True): """Formatted result: 1. If the result is a detection limit, returns '< LDL' or '> UDL' 2. Print ResultText of matching ResultOptions 3. If the result is not floatable, return it without being formatted 4. If the analysis specs has hidemin or hidemax enabled and the result is out of range, render result as '<min' or '>max' 5. If the result is below Lower Detection Limit, show '<LDL' 6. If the result is above Upper Detecion Limit, show '>UDL' 7. Otherwise, render numerical value :param specs: Optional result specifications, a dictionary as follows: {'min': <min_val>, 'max': <max_val>, 'error': <error>, 'hidemin': <hidemin_val>, 'hidemax': <hidemax_val>} :param decimalmark: The string to be used as a decimal separator. default is '.' :param sciformat: 1. The sci notation has to be formatted as aE^+b 2. The sci notation has to be formatted as a·10^b 3. As 2, but with super html entity for exp 4. The sci notation has to be formatted as a·10^b 5. As 4, but with super html entity for exp By default 1 :param html: if true, returns an string with the special characters escaped: e.g: '<' and '>' (LDL and UDL for results like < 23.4). """ result = self.getResult() # 1. The result is a detection limit, return '< LDL' or '> UDL' dl = self.getDetectionLimitOperand() if dl: try: res = float(result) # required, check if floatable res = drop_trailing_zeros_decimal(res) 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) choices = self.getResultOptions() # 2. Print ResultText of matching ResulOptions match = [x['ResultText'] for x in choices if str(x['ResultValue']) == str(result)] if match: return match[0] # 3. If the result is not floatable, return it without being formatted 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' specs = specs if specs else self.getResultsRange() hidemin = specs.get('hidemin', '') hidemax = specs.get('hidemax', '') try: belowmin = hidemin and result < float(hidemin) or False except (TypeError, ValueError): belowmin = False try: abovemax = hidemax and result > float(hidemax) or False except (TypeError, ValueError): abovemax = False # 4.1. If result is below min and hidemin enabled, return '<min' if belowmin: fdm = formatDecimalMark('< %s' % hidemin, decimalmark) return fdm.replace('< ', '< ', 1) if html else fdm # 4.2. If result is above max and hidemax enabled, return '>max' if abovemax: fdm = formatDecimalMark('> %s' % hidemax, decimalmark) return fdm.replace('> ', '> ', 1) if html else fdm # Below Lower Detection Limit (LDL)? ldl = self.getLowerDetectionLimit() if result < ldl: # LDL must not be formatted according to precision, etc. # Drop trailing zeros from decimal ldl = drop_trailing_zeros_decimal(ldl) fdm = formatDecimalMark('< %s' % ldl, decimalmark) return fdm.replace('< ', '< ', 1) if html else fdm # Above Upper Detection Limit (UDL)? udl = self.getUpperDetectionLimit() if result > udl: # UDL must not be formatted according to precision, etc. # Drop trailing zeros from decimal udl = drop_trailing_zeros_decimal(udl) fdm = formatDecimalMark('> %s' % udl, decimalmark) return fdm.replace('> ', '> ', 1) if html else fdm # Render numerical values return format_numeric_result(self, self.getResult(), decimalmark=decimalmark, sciformat=sciformat)
def calculate(self, uid=None): # TODO Move this function to api.analysis.calculate analysis = self.analyses[uid] form_result = self.current_results[uid]['result'] calculation = analysis.getCalculation() if analysis.portal_type == 'ReferenceAnalysis': deps = {} else: deps = {} for dep in analysis.getDependencies(): deps[dep.UID()] = dep path = '++resource++bika.lims.images' mapping = {} # values to be returned to form for this UID Result = {'uid': uid, 'result': form_result} try: Result['result'] = float(form_result) except: if form_result == "0/0": Result['result'] = "" if calculation: """We need first to create the map of available parameters acording to the interims, analyses and wildcards: params = { <as-1-keyword> : <analysis_result>, <as-1-keyword>.<wildcard-1> : <wildcard_1_value>, <as-1-keyword>.<wildcard-2> : <wildcard_2_value>, <interim-1> : <interim_result>, ... } """ # Get dependent analyses results and wildcard values to the # mapping. If dependent analysis without result found, # break and abort calculation unsatisfied = False for dependency_uid, dependency in deps.items(): if dependency_uid in self.ignore_uids: unsatisfied = True break # LIMS-1769. Allow to use LDL and UDL in calculations. # https://jira.bikalabs.com/browse/LIMS-1769 analysisvalues = {} if dependency_uid in self.current_results: analysisvalues = self.current_results[dependency_uid] else: # Retrieve the result and DLs from the analysis analysisvalues = { 'keyword': dependency.getKeyword(), 'result': dependency.getResult(), 'ldl': dependency.getLowerDetectionLimit(), 'udl': dependency.getUpperDetectionLimit(), 'belowldl': dependency.isBelowLowerDetectionLimit(), 'aboveudl': dependency.isAboveUpperDetectionLimit(), } if analysisvalues['result'] == '': unsatisfied = True break key = analysisvalues.get('keyword', dependency.getKeyword()) # Analysis result # All result mappings must be float, or they are ignored. try: mapping[key] = float(analysisvalues.get('result')) mapping['%s.%s' % (key, 'RESULT')] = float(analysisvalues.get('result')) mapping['%s.%s' % (key, 'LDL')] = float(analysisvalues.get('ldl')) mapping['%s.%s' % (key, 'UDL')] = float(analysisvalues.get('udl')) mapping['%s.%s' % (key, 'BELOWLDL')] = int(analysisvalues.get('belowldl')) mapping['%s.%s' % (key, 'ABOVEUDL')] = int(analysisvalues.get('aboveudl')) except: # If not floatable, then abort! unsatisfied = True break if unsatisfied: # unsatisfied means that one or more result on which we depend # is blank or unavailable, so we set blank result and abort. self.results.append({'uid': uid, 'result': '', 'formatted_result': ''}) return None # Add all interims to mapping for i_uid, i_data in self.item_data.items(): for i in i_data: # if this interim belongs to current analysis and is blank, # return an empty result for this analysis. if i_uid == uid and i['value'] == '': self.results.append({'uid': uid, 'result': '', 'formatted_result': ''}) return None # All interims must be float, or they are ignored. try: i['value'] = float(i['value']) except: pass # all interims are ServiceKeyword.InterimKeyword if i_uid in deps: key = "%s.%s" % (deps[i_uid].getKeyword(), i['keyword']) mapping[key] = i['value'] # this analysis' interims get extra reference # without service keyword prefix if uid == i_uid: mapping[i['keyword']] = i['value'] # Grab values for hidden InterimFields for only for current calculation # we can't allow non-floats through here till we change the eval's # interpolation hidden_fields = [] c_fields = calculation.getInterimFields() s_fields = analysis.getInterimFields() for field in c_fields: if field.get('hidden', False): hidden_fields.append(field['keyword']) try: mapping[field['keyword']] = float(field['value']) except ValueError: pass # also grab stickier defaults from AnalysisService for field in s_fields: if field['keyword'] in hidden_fields: try: mapping[field['keyword']] = float(field['value']) except ValueError: pass # convert formula to a valid python string, ready for interpolation formula = calculation.getMinifiedFormula() formula = formula.replace('[', '%(').replace(']', ')f') try: formula = eval("'%s'%%mapping" % formula, {"__builtins__": None, 'math': math, 'context': self.context}, {'mapping': mapping}) # calculate result = eval(formula, calculation._getGlobals()) Result['result'] = result self.current_results[uid]['result'] = result except TypeError as e: # non-numeric arguments in interim mapping? alert = {'field': 'Result', 'icon': path + '/exclamation.png', 'msg': "{0}: {1} ({2}) ".format( t(_("Type Error")), html_quote(str(e.args[0])), formula)} if uid in self.alerts: self.alerts[uid].append(alert) else: self.alerts[uid] = [alert, ] except ZeroDivisionError as e: Result['result'] = '0/0' Result['formatted_result'] = '0/0' self.current_results[uid]['result'] = '0/0' self.results.append(Result) alert = {'field': 'Result', 'icon': path + '/exclamation.png', 'msg': "{0}: {1} ({2}) ".format( t(_("Division by zero")), html_quote(str(e.args[0])), formula)} if uid in self.alerts: self.alerts[uid].append(alert) else: self.alerts[uid] = [alert, ] return None except KeyError as e: alert = {'field': 'Result', 'icon': path + '/exclamation.png', 'msg': "{0}: {1} ({2}) ".format( t(_("Key Error")), html_quote(str(e.args[0])), formula)} if uid in self.alerts: self.alerts[uid].append(alert) else: self.alerts[uid] = [alert, ] # format result try: Result['formatted_result'] = format_numeric_result(analysis, Result['result']) except ValueError: # non-float Result['formatted_result'] = Result['result'] self.results.append(Result) # if App.config.getConfiguration().debug_mode: # logger.info("calc.py: %s->%s %s" % (analysis.aq_parent.id, # analysis.id, # Result)) # LIMS-1808 Uncertainty calculation on DL # https://jira.bikalabs.com/browse/LIMS-1808 flres = Result.get('result', None) if flres and isnumber(flres): flres = float(flres) anvals = self.current_results[uid] isldl = anvals.get('isldl', False) isudl = anvals.get('isudl', False) ldl = anvals.get('ldl', 0) udl = anvals.get('udl', 0) ldl = float(ldl) if isnumber(ldl) else 0 udl = float(udl) if isnumber(udl) else 10000000 belowldl = (isldl or flres < ldl) aboveudl = (isudl or flres > udl) unc = '' if (belowldl or aboveudl) else analysis.getUncertainty(Result.get('result')) if not (belowldl or aboveudl): self.uncertainties.append({'uid': uid, 'uncertainty': unc}) # maybe a service who depends on us must be recalculated. if analysis.portal_type == 'ReferenceAnalysis': dependents = [] else: dependents = analysis.getDependents() if dependents: for dependent in dependents: dependent_uid = dependent.UID() # ignore analyses that no longer exist. if dependent_uid in self.ignore_uids or \ dependent_uid not in self.analyses: continue self.calculate(dependent_uid) # Render out of range / in shoulder alert info self._render_range_alert(analysis, Result["result"])
def calculate(self, uid=None): analysis = self.analyses[uid] form_result = self.current_results[uid]['result'] service = analysis.getService() calculation = service.getCalculation() if analysis.portal_type == 'ReferenceAnalysis': deps = {} else: deps = {} for dep in analysis.getDependencies(): deps[dep.UID()] = dep path = '++resource++bika.lims.images' mapping = {} # values to be returned to form for this UID Result = {'uid': uid, 'result': form_result} try: Result['result'] = float(form_result) except: if form_result == "0/0": Result['result'] = "" if calculation: ''' We need first to create the map of available parameters acording to the interims, analyses and wildcards: params = { <as-1-keyword> : <analysis_result>, <as-1-keyword>.<wildcard-1> : <wildcard_1_value>, <as-1-keyword>.<wildcard-2> : <wildcard_2_value>, <interim-1> : <interim_result>, ... } ''' # Get dependent analyses results and wildcard values to the # mapping. If dependent analysis without result found, # break and abort calculation unsatisfied = False for dependency_uid, dependency in deps.items(): if dependency_uid in self.ignore_uids: unsatisfied = True break # LIMS-1769. Allow to use LDL and UDL in calculations. # https://jira.bikalabs.com/browse/LIMS-1769 analysisvalues = {} if dependency_uid in self.current_results: analysisvalues = self.current_results[dependency_uid] else: # Retrieve the result and DLs from the analysis analysisvalues = { 'keyword': dependency.getKeyword(), 'result': dependency.getResult(), 'ldl': dependency.getLowerDetectionLimit(), 'udl': dependency.getUpperDetectionLimit(), 'belowldl': dependency.isBelowLowerDetectionLimit(), 'aboveudl': dependency.isAboveUpperDetectionLimit(), } if analysisvalues['result']=='': unsatisfied = True break; key = analysisvalues.get('keyword',dependency.getService().getKeyword()) # Analysis result # All result mappings must be float, or they are ignored. try: mapping[key] = float(analysisvalues.get('result')) mapping['%s.%s' % (key, 'RESULT')] = float(analysisvalues.get('result')) mapping['%s.%s' % (key, 'LDL')] = float(analysisvalues.get('ldl')) mapping['%s.%s' % (key, 'UDL')] = float(analysisvalues.get('udl')) mapping['%s.%s' % (key, 'BELOWLDL')] = int(analysisvalues.get('belowldl')) mapping['%s.%s' % (key, 'ABOVEUDL')] = int(analysisvalues.get('aboveudl')) except: # If not floatable, then abort! unsatisfied = True break if unsatisfied: # unsatisfied means that one or more result on which we depend # is blank or unavailable, so we set blank result and abort. self.results.append({'uid': uid, 'result': '', 'formatted_result': ''}) return None # Add all interims to mapping for i_uid, i_data in self.item_data.items(): for i in i_data: # if this interim belongs to current analysis and is blank, # return an empty result for this analysis. if i_uid == uid and i['value'] == '': self.results.append({'uid': uid, 'result': '', 'formatted_result': ''}) return None # All interims must be float, or they are ignored. try: i['value'] = float(i['value']) except: pass # all interims are ServiceKeyword.InterimKeyword if i_uid in deps: key = "%s.%s" % (deps[i_uid].getService().getKeyword(), i['keyword']) mapping[key] = i['value'] # this analysis' interims get extra reference # without service keyword prefix if uid == i_uid: mapping[i['keyword']] = i['value'] # Grab values for hidden InterimFields for only for current calculation # we can't allow non-floats through here till we change the eval's # interpolation hidden_fields = [] c_fields = calculation.getInterimFields() s_fields = service.getInterimFields() for field in c_fields: if field.get('hidden', False): hidden_fields.append(field['keyword']) try: mapping[field['keyword']] = float(field['value']) except ValueError: pass # also grab stickier defaults from AnalysisService for field in s_fields: if field['keyword'] in hidden_fields: try: mapping[field['keyword']] = float(field['value']) except ValueError: pass # convert formula to a valid python string, ready for interpolation formula = calculation.getMinifiedFormula() formula = formula.replace('[', '%(').replace(']', ')f') try: formula = eval("'%s'%%mapping" % formula, {"__builtins__": None, 'math': math, 'context': self.context}, {'mapping': mapping}) # calculate result = eval(formula) Result['result'] = result self.current_results[uid]['result'] = result except TypeError as e: # non-numeric arguments in interim mapping? alert = {'field': 'Result', 'icon': path + '/exclamation.png', 'msg': "{0}: {1} ({2}) ".format( t(_("Type Error")), html_quote(str(e.args[0])), formula)} if uid in self.alerts: self.alerts[uid].append(alert) else: self.alerts[uid] = [alert, ] except ZeroDivisionError as e: Result['result'] = '0/0' Result['formatted_result'] = '0/0' self.current_results[uid]['result'] = '0/0' self.results.append(Result) alert = {'field': 'Result', 'icon': path + '/exclamation.png', 'msg': "{0}: {1} ({2}) ".format( t(_("Division by zero")), html_quote(str(e.args[0])), formula)} if uid in self.alerts: self.alerts[uid].append(alert) else: self.alerts[uid] = [alert, ] return None except KeyError as e: alert = {'field': 'Result', 'icon': path + '/exclamation.png', 'msg': "{0}: {1} ({2}) ".format( t(_("Key Error")), html_quote(str(e.args[0])), formula)} if uid in self.alerts: self.alerts[uid].append(alert) else: self.alerts[uid] = [alert, ] if analysis.portal_type == 'ReferenceAnalysis': # The analysis is a Control or Blank. We might use the # reference results instead other specs _uid = analysis.getServiceUID() specs = analysis.aq_parent.getResultsRangeDict().get(_uid, {}) else: # Get the specs directly from the analysis. The getResultsRange # function already takes care about which are the specs to be used: # AR, client or lab. specs = analysis.getResultsRange() # format result try: Result['formatted_result'] = format_numeric_result(analysis, Result['result']) except ValueError: # non-float Result['formatted_result'] = Result['result'] # calculate Dry Matter result # if parent is not an AR, it's never going to be calculable dm = hasattr(analysis.aq_parent, 'getReportDryMatter') and \ analysis.aq_parent.getReportDryMatter() and \ analysis.getService().getReportDryMatter() if dm: dry_service = self.context.bika_setup.getDryMatterService() # get the UID of the DryMatter Analysis from our parent AR dry_analysis = [a for a in analysis.aq_parent.getAnalyses(full_objects=True) if a.getService().UID() == dry_service.UID()] if dry_analysis: dry_analysis = dry_analysis[0] dry_uid = dry_analysis.UID() # get the current DryMatter analysis result from the form if dry_uid in self.current_results: try: dry_result = float(self.current_results[dry_uid]) except: dm = False else: try: dry_result = float(dry_analysis.getResult()) except: dm = False else: dm = False Result['dry_result'] = dm and dry_result and \ '%.2f' % ((Result['result'] / dry_result) * 100) or '' self.results.append(Result) # if App.config.getConfiguration().debug_mode: # logger.info("calc.py: %s->%s %s" % (analysis.aq_parent.id, # analysis.id, # Result)) # LIMS-1808 Uncertainty calculation on DL # https://jira.bikalabs.com/browse/LIMS-1808 flres = Result.get('result', None) if flres and isnumber(flres): flres = float(flres) anvals = self.current_results[uid] isldl = anvals.get('isldl', False) isudl = anvals.get('isudl', False) ldl = anvals.get('ldl',0) udl = anvals.get('udl',0) ldl = float(ldl) if isnumber(ldl) else 0 udl = float(udl) if isnumber(udl) else 10000000 belowldl = (isldl or flres < ldl) aboveudl = (isudl or flres > udl) unc = '' if (belowldl or aboveudl) else analysis.getUncertainty(Result.get('result')) if not (belowldl or aboveudl): self.uncertainties.append({'uid': uid, 'uncertainty': unc}) # maybe a service who depends on us must be recalculated. if analysis.portal_type == 'ReferenceAnalysis': dependents = [] else: dependents = analysis.getDependents() if dependents: for dependent in dependents: dependent_uid = dependent.UID() # ignore analyses that no longer exist. if dependent_uid in self.ignore_uids or \ dependent_uid not in self.analyses: continue self.calculate(dependent_uid) # These self.alerts are just for the json return. # we're placing the entire form's results in kwargs. adapters = getAdapters((analysis, ), IFieldIcons) for name, adapter in adapters: alerts = adapter(result=Result['result'], form_results=self.current_results) if alerts: if analysis.UID() in self.alerts: self.alerts[analysis.UID()].extend(alerts[analysis.UID()]) else: self.alerts[analysis.UID()] = alerts[analysis.UID()]
def __call__(self): pc = self.portal_catalog self.checkPermission = self.context.portal_membership.checkPermission self.now = DateTime() self.SamplingWorkflowEnabled = self.context.bika_setup.getSamplingWorkflowEnabled() # Client details (if client is associated) self.client = None client_uid = hasattr(self.context, 'getClientUID') and self.context.getClientUID() if client_uid: proxies = pc(portal_type='Client', UID=client_uid) if proxies: self.client = proxies[0].getObject() client_address = self.client.getPostalAddress() if self.contact and not client_address: client_address = self.contact.getBillingAddress() if not client_address: client_address = self.contact.getPhysicalAddress() if client_address: _keys = ['address', 'city', 'state', 'zip', 'country'] _list = [client_address.get(v) for v in _keys if client_address.get(v)] self.client_address = "<br/>".join(_list).replace("\n", "<br/>") if self.client_address.endswith("<br/>"): self.client_address = self.client_address[:-5] else: self.client_address = None # Reporter self.member = self.context.portal_membership.getAuthenticatedMember() self.username = self.member.getUserName() self.reporter = self.user_fullname(self.username) self.reporter_email = self.user_email(self.username) self.reporter_signature = "" c = [x for x in self.bika_setup_catalog(portal_type='LabContact') if x.getObject().getUsername() == self.username] if c: sf = c[0].getObject().getSignature() if sf: self.reporter_signature = sf.absolute_url() + "/Signature" # laboratory self.laboratory = self.context.bika_setup.laboratory self.accredited = self.laboratory.getLaboratoryAccredited() lab_address = self.laboratory.getPrintAddress() if lab_address: _keys = ['address', 'city', 'state', 'zip', 'country'] _list = [lab_address.get(v) for v in _keys if lab_address.get(v)] self.lab_address = "<br/>".join(_list).replace("\n", "<br/>") if self.lab_address.endswith("<br/>"): self.lab_address = self.lab_address[:-5] else: self.lab_address = None # Analysis Request results self.ars = [] self.ar_headers = [_("Request ID"), _("Date Requested"), _("Sample Type"), _("Sample Point")] self.analysis_headers = [_("Analysis Service"), _("Method"), _("Result"), _("Analyst")] for ar in self.context.getAnalysisRequests(): datecreated = ar.created() # datereceived = ar.getDateReceived() # datepublished = ar.getDatePublished() datalines = [] for analysis in ar.getAnalyses(full_objects=True): service = analysis.getService() method = service.getMethod() sample = ar.getSample() result = analysis.getResult() formatted_result = format_numeric_result(analysis, result) datalines.append({_("Analysis Service"): analysis.getService().Title(), _("Method"): method and method.Title() or "", _("Result"): formatted_result, _("Analyst"): self.user_fullname(analysis.getAnalyst()), _("Remarks"): analysis.getRemarks()}) self.ars.append({ _("Request ID"): ar.getRequestID(), _("Date Requested"): self.ulocalized_time(datecreated), # requested->created _("Sample Type"): sample.getSampleType() and sample.getSampleType().Title() or '', _("Sample Point"): sample.getSamplePoint() and sample.getSamplePoint().Title() or '', _("datalines"): datalines, }) # Create Report fn = self.context.Title() + " " + self.ulocalized_time(self.now) report_html = self.template() debug_mode = App.config.getConfiguration().debug_mode if debug_mode: tmp_fd, tmp_fn = tempfile.mkstemp(suffix=".html") logger.debug("Writing HTML for %s to %s" % (self.context, tmp_fn)) tmp_fd.write(report_html) tmp_fd.close() pdf_fd, pdf_fn = tempfile.mkstemp(suffix="pdf") pdf_fd.close() pdf = createPdf(report_html, outfile=pdf_fn) if debug_mode: logger.debug("Wrote PDF for %s to %s" % (self.context, pdf_fn)) else: os.remove(pdf_fn) # XXX Email published batches to who? # Send PDF to browser if not pdf.err: setheader = self.request.RESPONSE.setHeader setheader('Content-Type', 'application/pdf') setheader("Content-Disposition", "attachment;filename=\"%s\"" % fn) self.request.RESPONSE.write(pdf)
def folderitems(self): rc = getToolByName(self.context, REFERENCE_CATALOG) bsc = getToolByName(self.context, 'bika_setup_catalog') analysis_categories = bsc(portal_type="AnalysisCategory", sort_on="sortable_title") analysis_categories_order = dict([(b.Title, "{:04}".format(a)) for a, b in enumerate(analysis_categories)]) workflow = getToolByName(self.context, 'portal_workflow') mtool = getToolByName(self.context, 'portal_membership') checkPermission = mtool.checkPermission if not self.allow_edit: can_edit_analyses = False else: if self.contentFilter.get('getPointOfCapture', '') == 'field': can_edit_analyses = checkPermission(EditFieldResults, self.context) else: can_edit_analyses = checkPermission(EditResults, self.context) self.allow_edit = can_edit_analyses self.show_select_column = self.allow_edit context_active = isActive(self.context) self.categories = [] items = super(AnalysesView, self).folderitems(full_objects = True) # manually skim retracted analyses from the list new_items = [] for i,item in enumerate(items): # self.contentsMethod may return brains or objects. if not ('obj' in items[i]): continue obj = hasattr(items[i]['obj'], 'getObject') and \ items[i]['obj'].getObject() or \ items[i]['obj'] if workflow.getInfoFor(obj, 'review_state') == 'retracted' \ and not checkPermission(ViewRetractedAnalyses, self.context): continue new_items.append(item) items = new_items methods = self.get_methods_vocabulary() self.interim_fields = {} self.interim_columns = {} self.specs = {} show_methodinstr_columns = False for i, item in enumerate(items): # self.contentsMethod may return brains or objects. obj = hasattr(items[i]['obj'], 'getObject') and \ items[i]['obj'].getObject() or \ items[i]['obj'] if workflow.getInfoFor(obj, 'review_state') == 'retracted' \ and not checkPermission(ViewRetractedAnalyses, self.context): continue result = obj.getResult() service = obj.getService() calculation = service.getCalculation() unit = service.getUnit() keyword = service.getKeyword() if self.show_categories: cat = obj.getService().getCategoryTitle() cat_order = analysis_categories_order.get(cat) items[i]['category'] = cat if (cat, cat_order) not in self.categories: self.categories.append((cat, cat_order)) # Check for InterimFields attribute on our object, interim_fields = hasattr(obj, 'getInterimFields') \ and obj.getInterimFields() or [] # kick some pretty display values in. for x in range(len(interim_fields)): interim_fields[x]['formatted_value'] = \ format_numeric_result(obj, interim_fields[x]['value']) self.interim_fields[obj.UID()] = interim_fields items[i]['service_uid'] = service.UID() items[i]['Service'] = service.Title() items[i]['Keyword'] = keyword items[i]['Unit'] = format_supsub(unit) if unit else '' items[i]['Result'] = '' items[i]['formatted_result'] = '' items[i]['interim_fields'] = interim_fields items[i]['Remarks'] = obj.getRemarks() items[i]['Uncertainty'] = '' items[i]['DetectionLimit'] = '' items[i]['retested'] = obj.getRetested() items[i]['class']['retested'] = 'center' items[i]['result_captured'] = self.ulocalized_time( obj.getResultCaptureDate(), long_format=0) items[i]['calculation'] = calculation and True or False try: items[i]['Partition'] = obj.getSamplePartition().getId() except AttributeError: items[i]['Partition'] = '' if obj.portal_type == "ReferenceAnalysis": items[i]['DueDate'] = self.ulocalized_time(obj.aq_parent.getExpiryDate(), long_format=0) else: items[i]['DueDate'] = self.ulocalized_time(obj.getDueDate(), long_format=1) cd = obj.getResultCaptureDate() items[i]['CaptureDate'] = cd and self.ulocalized_time(cd, long_format=1) or '' items[i]['Attachments'] = '' item['allow_edit'] = [] client_or_lab = "" tblrowclass = items[i].get('table_row_class'); if obj.portal_type == 'ReferenceAnalysis': items[i]['st_uid'] = obj.aq_parent.UID() items[i]['table_row_class'] = ' '.join([tblrowclass, 'qc-analysis']); elif obj.portal_type == 'DuplicateAnalysis' and \ obj.getAnalysis().portal_type == 'ReferenceAnalysis': items[i]['st_uid'] = obj.aq_parent.UID() items[i]['table_row_class'] = ' '.join([tblrowclass, 'qc-analysis']); else: sample = None if self.context.portal_type == 'AnalysisRequest': sample = self.context.getSample() elif self.context.portal_type == 'Worksheet': if obj.portal_type in ('DuplicateAnalysis', 'RejectAnalysis'): sample = obj.getAnalysis().getSample() else: sample = obj.aq_parent.getSample() elif self.context.portal_type == 'Sample': sample = self.context st_uid = sample.getSampleType().UID() if sample else '' items[i]['st_uid'] = st_uid if checkPermission(ManageBika, self.context): service_uid = service.UID() latest = rc.lookupObject(service_uid).version_id items[i]['Service'] = service.Title() items[i]['class']['Service'] = "service_title" # Show version number of out-of-date objects # No: This should be done in another column, if at all. # The (vX) value confuses some more fragile forms. # if hasattr(obj, 'reference_versions') and \ # service_uid in obj.reference_versions and \ # latest != obj.reference_versions[service_uid]: # items[i]['after']['Service'] = "(v%s)" % \ # (obj.reference_versions[service_uid]) # choices defined on Service apply to result fields. choices = service.getResultOptions() if choices: item['choices']['Result'] = choices # permission to view this item's results can_view_result = \ getSecurityManager().checkPermission(ViewResults, obj) # permission to edit this item's results # Editing Field Results is possible while in Sample Due. poc = self.contentFilter.get("getPointOfCapture", 'lab') can_edit_analysis = self.allow_edit and context_active and \ ( (poc == 'field' and getSecurityManager().checkPermission(EditFieldResults, obj)) or (poc != 'field' and getSecurityManager().checkPermission(EditResults, obj)) ) allowed_method_states = ['to_be_sampled', 'to_be_preserved', 'sample_received', 'sample_registered', 'sampled', 'assigned'] # Prevent from being edited if the instrument assigned # is not valid (out-of-date or uncalibrated), except if # the analysis is a QC with assigned status can_edit_analysis = can_edit_analysis \ and (obj.isInstrumentValid() \ or (obj.portal_type == 'ReferenceAnalysis' \ and item['review_state'] in allowed_method_states)) if can_edit_analysis: items[i]['allow_edit'].extend(['Analyst', 'Result', 'Remarks']) # if the Result field is editable, our interim fields are too for f in self.interim_fields[obj.UID()]: items[i]['allow_edit'].append(f['keyword']) # if there isn't a calculation then result must be re-testable, # and if there are interim fields, they too must be re-testable. if not items[i]['calculation'] or \ (items[i]['calculation'] and self.interim_fields[obj.UID()]): items[i]['allow_edit'].append('retested') # TODO: Only the labmanager must be able to change the method # can_set_method = getSecurityManager().checkPermission(SetAnalysisMethod, obj) can_set_method = can_edit_analysis \ and item['review_state'] in allowed_method_states method = obj.getMethod() \ if hasattr(obj, 'getMethod') and obj.getMethod() \ else service.getMethod() # Display the methods selector if the AS has at least one # method assigned item['Method'] = '' item['replace']['Method'] = '' if can_set_method: voc = self.get_methods_vocabulary(obj) if voc: # The service has at least one method available item['Method'] = method.UID() if method else '' item['choices']['Method'] = voc item['allow_edit'].append('Method') show_methodinstr_columns = True elif method: # This should never happen # The analysis has set a method, but its parent # service hasn't any method available O_o item['Method'] = method.Title() item['replace']['Method'] = "<a href='%s'>%s</a>" % \ (method.absolute_url(), method.Title()) show_methodinstr_columns = True elif method: # Edition not allowed, but method set item['Method'] = method.Title() item['replace']['Method'] = "<a href='%s'>%s</a>" % \ (method.absolute_url(), method.Title()) show_methodinstr_columns = True # TODO: Instrument selector dynamic behavior in worksheet Results # Only the labmanager must be able to change the instrument to be used. Also, # the instrument selection should be done in accordance with the method selected # can_set_instrument = service.getInstrumentEntryOfResults() and getSecurityManager().checkPermission(SetAnalysisInstrument, obj) can_set_instrument = service.getInstrumentEntryOfResults() \ and can_edit_analysis \ and item['review_state'] in allowed_method_states item['Instrument'] = '' item['replace']['Instrument'] = '' if service.getInstrumentEntryOfResults(): instrument = None # If the analysis has an instrument already assigned, use it if service.getInstrumentEntryOfResults() \ and hasattr(obj, 'getInstrument') \ and obj.getInstrument(): instrument = obj.getInstrument() # Otherwise, use the Service's default instrument elif service.getInstrumentEntryOfResults(): instrument = service.getInstrument() if can_set_instrument: # Edition allowed voc = self.get_instruments_vocabulary(obj) if voc: # The service has at least one instrument available item['Instrument'] = instrument.UID() if instrument else '' item['choices']['Instrument'] = voc item['allow_edit'].append('Instrument') show_methodinstr_columns = True elif instrument: # This should never happen # The analysis has an instrument set, but the # service hasn't any available instrument item['Instrument'] = instrument.Title() item['replace']['Instrument'] = "<a href='%s'>%s</a>" % \ (instrument.absolute_url(), instrument.Title()) show_methodinstr_columns = True elif instrument: # Edition not allowed, but instrument set item['Instrument'] = instrument.Title() item['replace']['Instrument'] = "<a href='%s'>%s</a>" % \ (instrument.absolute_url(), instrument.Title()) show_methodinstr_columns = True else: # Manual entry of results, instrument not allowed item['Instrument'] = _('Manual') msgtitle = t(_( "Instrument entry of results not allowed for ${service}", mapping={"service": safe_unicode(service.Title())}, )) item['replace']['Instrument'] = \ '<a href="#" title="%s">%s</a>' % (msgtitle, t(_('Manual'))) # Sets the analyst assigned to this analysis if can_edit_analysis: analyst = obj.getAnalyst() # widget default: current user if not analyst: analyst = mtool.getAuthenticatedMember().getUserName() items[i]['Analyst'] = analyst item['choices']['Analyst'] = self.getAnalysts() else: items[i]['Analyst'] = obj.getAnalystName() # If the user can attach files to analyses, show the attachment col can_add_attachment = \ getSecurityManager().checkPermission(AddAttachment, obj) if can_add_attachment or can_view_result: attachments = "" if hasattr(obj, 'getAttachment'): for attachment in obj.getAttachment(): af = attachment.getAttachmentFile() icon = af.icon attachments += "<span class='attachment' attachment_uid='%s'>" % (attachment.UID()) if icon: attachments += "<img src='%s/%s'/>" % (self.portal_url, icon) attachments += '<a href="%s/at_download/AttachmentFile"/>%s</a>' % (attachment.absolute_url(), af.filename) if can_edit_analysis: attachments += "<img class='deleteAttachmentButton' attachment_uid='%s' src='%s'/>" % (attachment.UID(), "++resource++bika.lims.images/delete.png") attachments += "</br></span>" items[i]['replace']['Attachments'] = attachments[:-12] + "</span>" # Only display data bearing fields if we have ViewResults # permission, otherwise just put an icon in Result column. if can_view_result: items[i]['Result'] = result scinot = self.context.bika_setup.getScientificNotationResults() dmk = self.context.bika_setup.getResultsDecimalMark() items[i]['formatted_result'] = obj.getFormattedResult(sciformat=int(scinot),decimalmark=dmk) # LIMS-1379 Allow manual uncertainty value input # https://jira.bikalabs.com/browse/LIMS-1379 fu = format_uncertainty(obj, result, decimalmark=dmk, sciformat=int(scinot)) fu = fu if fu else '' if can_edit_analysis and service.getAllowManualUncertainty() == True: unc = obj.getUncertainty(result) item['allow_edit'].append('Uncertainty') items[i]['Uncertainty'] = unc if unc else '' items[i]['before']['Uncertainty'] = '± '; items[i]['after']['Uncertainty'] = '<em class="discreet" style="white-space:nowrap;"> %s</em>' % items[i]['Unit']; elif fu: items[i]['Uncertainty'] = fu items[i]['before']['Uncertainty'] = '± '; items[i]['after']['Uncertainty'] = '<em class="discreet" style="white-space:nowrap;"> %s</em>' % items[i]['Unit']; # LIMS-1700. Allow manual input of Detection Limits # LIMS-1775. Allow to select LDL or UDL defaults in results with readonly mode # https://jira.bikalabs.com/browse/LIMS-1700 # https://jira.bikalabs.com/browse/LIMS-1775 if can_edit_analysis and \ hasattr(obj, 'getDetectionLimitOperand') and \ hasattr(service, 'getDetectionLimitSelector') and \ service.getDetectionLimitSelector() == True: isldl = obj.isBelowLowerDetectionLimit() isudl = obj.isAboveUpperDetectionLimit() dlval='' if isldl or isudl: dlval = '<' if isldl else '>' item['allow_edit'].append('DetectionLimit') item['DetectionLimit'] = dlval choices=[{'ResultValue': '<', 'ResultText': '<'}, {'ResultValue': '>', 'ResultText': '>'}] item['choices']['DetectionLimit'] = choices self.columns['DetectionLimit']['toggle'] = True srv = obj.getService() defdls = {'min':srv.getLowerDetectionLimit(), 'max':srv.getUpperDetectionLimit(), 'manual':srv.getAllowManualDetectionLimit()} defin = '<input type="hidden" id="DefaultDLS.%s" value=\'%s\'/>' defin = defin % (obj.UID(), json.dumps(defdls)) item['after']['DetectionLimit'] = defin # LIMS-1769. Allow to use LDL and UDL in calculations. # https://jira.bikalabs.com/browse/LIMS-1769 # Since LDL, UDL, etc. are wildcards that can be used # in calculations, these fields must be loaded always # for 'live' calculations. if can_edit_analysis: dls = {'default_ldl': 'none', 'default_udl': 'none', 'below_ldl': False, 'above_udl': False, 'is_ldl': False, 'is_udl': False, 'manual_allowed': False, 'dlselect_allowed': False} if hasattr(obj, 'getDetectionLimits'): dls['below_ldl'] = obj.isBelowLowerDetectionLimit() dls['above_udl'] = obj.isBelowLowerDetectionLimit() dls['is_ldl'] = obj.isLowerDetectionLimit() dls['is_udl'] = obj.isUpperDetectionLimit() dls['default_ldl'] = service.getLowerDetectionLimit() dls['default_udl'] = service.getUpperDetectionLimit() dls['manual_allowed'] = service.getAllowManualDetectionLimit() dls['dlselect_allowed'] = service.getDetectionLimitSelector() dlsin = '<input type="hidden" id="AnalysisDLS.%s" value=\'%s\'/>' dlsin = dlsin % (obj.UID(), json.dumps(dls)) item['after']['Result'] = dlsin else: items[i]['Specification'] = "" if 'Result' in items[i]['allow_edit']: items[i]['allow_edit'].remove('Result') items[i]['before']['Result'] = \ '<img width="16" height="16" ' + \ 'src="%s/++resource++bika.lims.images/to_follow.png"/>' % \ (self.portal_url) # Everyone can see valid-ranges spec = self.get_analysis_spec(obj) if spec: min_val = spec.get('min', '') min_str = ">{0}".format(min_val) if min_val else '' max_val = spec.get('max', '') max_str = "<{0}".format(max_val) if max_val else '' error_val = spec.get('error', '') error_str = "{0}%".format(error_val) if error_val else '' rngstr = ",".join([x for x in [min_str, max_str, error_str] if x]) else: rngstr = "" items[i]['Specification'] = rngstr # Add this analysis' interim fields to the interim_columns list for f in self.interim_fields[obj.UID()]: if f['keyword'] not in self.interim_columns and not f.get('hidden', False): self.interim_columns[f['keyword']] = f['title'] # and to the item itself items[i][f['keyword']] = f items[i]['class'][f['keyword']] = 'interim' # check if this analysis is late/overdue resultdate = obj.aq_parent.getDateSampled() \ if obj.portal_type == 'ReferenceAnalysis' \ else obj.getResultCaptureDate() duedate = obj.aq_parent.getExpiryDate() \ if obj.portal_type == 'ReferenceAnalysis' \ else obj.getDueDate() items[i]['replace']['DueDate'] = \ self.ulocalized_time(duedate, long_format=1) if items[i]['review_state'] not in ['to_be_sampled', 'to_be_preserved', 'sample_due', 'published']: if (resultdate and resultdate > duedate) \ or (not resultdate and DateTime() > duedate): items[i]['replace']['DueDate'] = '%s <img width="16" height="16" src="%s/++resource++bika.lims.images/late.png" title="%s"/>' % \ (self.ulocalized_time(duedate, long_format=1), self.portal_url, t(_("Late Analysis"))) # Submitting user may not verify results (admin can though) if items[i]['review_state'] == 'to_be_verified' and \ not checkPermission(VerifyOwnResults, obj): user_id = getSecurityManager().getUser().getId() self_submitted = False try: review_history = list(workflow.getInfoFor(obj, 'review_history')) review_history.reverse() for event in review_history: if event.get('action') == 'submit': if event.get('actor') == user_id: self_submitted = True break if self_submitted: items[i]['after']['state_title'] = \ "<img src='++resource++bika.lims.images/submitted-by-current-user.png' title='%s'/>" % \ (t(_("Cannot verify: Submitted by current user"))) except WorkflowException: pass # add icon for assigned analyses in AR views if self.context.portal_type == 'AnalysisRequest': obj = items[i]['obj'] if obj.portal_type in ['ReferenceAnalysis', 'DuplicateAnalysis'] or \ workflow.getInfoFor(obj, 'worksheetanalysis_review_state') == 'assigned': br = obj.getBackReferences('WorksheetAnalysis') if len(br) > 0: ws = br[0] items[i]['after']['state_title'] = \ "<a href='%s'><img src='++resource++bika.lims.images/worksheet.png' title='%s'/></a>" % \ (ws.absolute_url(), t(_("Assigned to: ${worksheet_id}", mapping={'worksheet_id': safe_unicode(ws.id)}))) # the TAL requires values for all interim fields on all # items, so we set blank values in unused cells for item in items: for field in self.interim_columns: if field not in item: item[field] = '' # XXX order the list of interim columns interim_keys = self.interim_columns.keys() interim_keys.reverse() # add InterimFields keys to columns for col_id in interim_keys: if col_id not in self.columns: self.columns[col_id] = {'title': self.interim_columns[col_id], 'input_width': '6', 'input_class': 'ajax_calculate numeric', 'sortable': False} if can_edit_analyses: new_states = [] for state in self.review_states: # InterimFields are displayed in review_state # They are anyway available through View.columns though. # In case of hidden fields, the calcs.py should check calcs/services # for additional InterimFields!! pos = 'Result' in state['columns'] and \ state['columns'].index('Result') or len(state['columns']) for col_id in interim_keys: if col_id not in state['columns']: state['columns'].insert(pos, col_id) # retested column is added after Result. pos = 'Result' in state['columns'] and \ state['columns'].index('Uncertainty') + 1 or len(state['columns']) state['columns'].insert(pos, 'retested') new_states.append(state) self.review_states = new_states # Allow selecting individual analyses self.show_select_column = True # Dry Matter. # The Dry Matter column is never enabled for reference sample contexts # and refers to getReportDryMatter in ARs. if items and \ (hasattr(self.context, 'getReportDryMatter') and \ self.context.getReportDryMatter()): # look through all items # if the item's Service supports ReportDryMatter, add getResultDM(). for item in items: if item['obj'].getService().getReportDryMatter(): item['ResultDM'] = item['obj'].getResultDM() else: item['ResultDM'] = '' if item['ResultDM']: item['after']['ResultDM'] = "<em class='discreet'>%</em>" # modify the review_states list to include the ResultDM column new_states = [] for state in self.review_states: pos = 'Result' in state['columns'] and \ state['columns'].index('Uncertainty') + 1 or len(state['columns']) state['columns'].insert(pos, 'ResultDM') new_states.append(state) self.review_states = new_states if self.show_categories: self.categories = map(lambda x: x[0], sorted(self.categories, key=lambda x: x[1])) else: self.categories.sort() # self.json_specs = json.dumps(self.specs) self.json_interim_fields = json.dumps(self.interim_fields) self.items = items # Method and Instrument columns must be shown or hidden at the # same time, because the value assigned to one causes # a value reassignment to the other (one method can be performed # by different instruments) self.columns['Method']['toggle'] = show_methodinstr_columns self.columns['Instrument']['toggle'] = show_methodinstr_columns return items
def getFormattedResult(self, specs=None): """Formatted result: 1. Print ResultText of matching ResultOptions 2. If the result is not floatable, return it without being formatted 3. If the analysis specs has hidemin or hidemax enabled and the result is out of range, render result as '<min' or '>max' 4. Otherwise, render numerical value specs param is optional. A dictionary as follows: {'min': <min_val>, 'max': <max_val>, 'error': <error>, 'hidemin': <hidemin_val>, 'hidemax': <hidemax_val>} """ result = self.getResult() service = self.getService() choices = service.getResultOptions() # 1. Print ResultText of matching ResulOptions match = [ x['ResultText'] for x in choices if str(x['ResultValue']) == str(result) ] if match: return match[0] # 2. If the result is not floatable, return it without being formatted try: result = float(result) except: return result # 3. If the analysis specs has enabled hidemin or hidemax and the # result is out of range, render result as '<min' or '>max' belowmin = False abovemax = False if not specs: specs = self.getAnalysisSpecs() specs = specs.getResultsRangeDict() if specs is not None else {} specs = specs.get(self.getKeyword(), {}) hidemin = specs.get('hidemin', '') hidemax = specs.get('hidemax', '') try: belowmin = hidemin and result < float(hidemin) or False except: belowmin = False pass try: abovemax = hidemax and result > float(hidemax) or False except: abovemax = False pass # 3.1. If result is below min and hidemin enabled, return '<min' if belowmin: return '< %s' % hidemin # 3.2. If result is above max and hidemax enabled, return '>max' if abovemax: return '> %s' % hidemax # Render numerical value return format_numeric_result(self, result)
def __call__(self): pc = self.portal_catalog self.checkPermission = self.context.portal_membership.checkPermission self.now = DateTime() self.SamplingWorkflowEnabled = self.context.bika_setup.getSamplingWorkflowEnabled( ) # Client details (if client is associated) self.client = None client_uid = hasattr(self.context, 'getClientUID') and self.context.getClientUID() if client_uid: proxies = pc(portal_type='Client', UID=client_uid) if proxies: self.client = proxies[0].getObject() client_address = self.client.getPostalAddress() if self.contact and not client_address: client_address = self.contact.getBillingAddress() if not client_address: client_address = self.contact.getPhysicalAddress() if client_address: _keys = ['address', 'city', 'state', 'zip', 'country'] _list = [ client_address.get(v) for v in _keys if client_address.get(v) ] self.client_address = "<br/>".join(_list).replace( "\n", "<br/>") if self.client_address.endswith("<br/>"): self.client_address = self.client_address[:-5] else: self.client_address = None # Reporter self.member = self.context.portal_membership.getAuthenticatedMember() self.username = self.member.getUserName() self.reporter = self.user_fullname(self.username) self.reporter_email = self.user_email(self.username) self.reporter_signature = "" c = [ x for x in self.bika_setup_catalog(portal_type='LabContact') if x.getObject().getUsername() == self.username ] if c: sf = c[0].getObject().getSignature() if sf: self.reporter_signature = sf.absolute_url() + "/Signature" # laboratory self.laboratory = self.context.bika_setup.laboratory self.accredited = self.laboratory.getLaboratoryAccredited() lab_address = self.laboratory.getPrintAddress() if lab_address: _keys = ['address', 'city', 'state', 'zip', 'country'] _list = [lab_address.get(v) for v in _keys if lab_address.get(v)] self.lab_address = "<br/>".join(_list).replace("\n", "<br/>") if self.lab_address.endswith("<br/>"): self.lab_address = self.lab_address[:-5] else: self.lab_address = None # Analysis Request results self.ars = [] self.ar_headers = [ _("Request ID"), _("Date Requested"), _("Sample Type"), _("Sample Point") ] self.analysis_headers = [ _("Analysis Service"), _("Method"), _("Result"), _("Analyst") ] for ar in self.context.getAnalysisRequests(): datecreated = ar.created() # datereceived = ar.getDateReceived() # datepublished = ar.getDatePublished() datalines = [] for analysis in ar.getAnalyses(full_objects=True): service = analysis.getService() method = service.getMethod() sample = ar.getSample() result = analysis.getResult() formatted_result = format_numeric_result(analysis, result) datalines.append({ _("Analysis Service"): analysis.getService().Title(), _("Method"): method and method.Title() or "", _("Result"): formatted_result, _("Analyst"): self.user_fullname(analysis.getAnalyst()), _("Remarks"): analysis.getRemarks() }) self.ars.append({ _("Request ID"): ar.getRequestID(), _("Date Requested"): self.ulocalized_time(datecreated), # requested->created _("Sample Type"): sample.getSampleType() and sample.getSampleType().Title() or '', _("Sample Point"): sample.getSamplePoint() and sample.getSamplePoint().Title() or '', _("datalines"): datalines, }) # Create Report fn = self.context.Title() + " " + self.ulocalized_time(self.now) report_html = self.template() debug_mode = App.config.getConfiguration().debug_mode if debug_mode: tmp_fd, tmp_fn = tempfile.mkstemp(suffix=".html") logger.debug("Writing HTML for %s to %s" % (self.context, tmp_fn)) tmp_fd.write(report_html) tmp_fd.close() pdf_fd, pdf_fn = tempfile.mkstemp(suffix="pdf") pdf_fd.close() pdf = createPdf(report_html, outfile=pdf_fn) if debug_mode: logger.debug("Wrote PDF for %s to %s" % (self.context, pdf_fn)) else: os.remove(pdf_fn) # XXX Email published batches to who? # Send PDF to browser if not pdf.err: setheader = self.request.RESPONSE.setHeader setheader('Content-Type', 'application/pdf') setheader("Content-Disposition", "attachment;filename=\"%s\"" % fn) self.request.RESPONSE.write(pdf)
def folderitems(self): rc = getToolByName(self.context, REFERENCE_CATALOG) bsc = getToolByName(self.context, 'bika_setup_catalog') workflow = getToolByName(self.context, 'portal_workflow') mtool = getToolByName(self.context, 'portal_membership') checkPermission = mtool.checkPermission if not self.allow_edit: can_edit_analyses = False else: if self.contentFilter.get('getPointOfCapture', '') == 'field': can_edit_analyses = checkPermission(EditFieldResults, self.context) else: can_edit_analyses = checkPermission(EditResults, self.context) self.allow_edit = can_edit_analyses self.show_select_column = self.allow_edit context_active = isActive(self.context) self.categories = [] items = super(AnalysesView, self).folderitems(full_objects=True) # manually skim retracted analyses from the list new_items = [] for i, item in enumerate(items): # self.contentsMethod may return brains or objects. if not ('obj' in items[i]): continue obj = hasattr(items[i]['obj'], 'getObject') and \ items[i]['obj'].getObject() or \ items[i]['obj'] if workflow.getInfoFor(obj, 'review_state') == 'retracted' \ and not checkPermission(ViewRetractedAnalyses, self.context): continue new_items.append(item) items = new_items methods = self.get_methods_vocabulary() self.interim_fields = {} self.interim_columns = {} self.specs = {} show_methodinstr_columns = False for i, item in enumerate(items): # self.contentsMethod may return brains or objects. obj = hasattr(items[i]['obj'], 'getObject') and \ items[i]['obj'].getObject() or \ items[i]['obj'] if workflow.getInfoFor(obj, 'review_state') == 'retracted' \ and not checkPermission(ViewRetractedAnalyses, self.context): continue result = obj.getResult() service = obj.getService() calculation = service.getCalculation() unit = service.getUnit() keyword = service.getKeyword() if self.show_categories: cat = obj.getService().getCategoryTitle() items[i]['category'] = cat if cat not in self.categories: self.categories.append(cat) # Check for InterimFields attribute on our object, interim_fields = hasattr(obj, 'getInterimFields') \ and obj.getInterimFields() or [] # kick some pretty display values in. for x in range(len(interim_fields)): interim_fields[x]['formatted_value'] = \ format_numeric_result(obj, interim_fields[x]['value']) self.interim_fields[obj.UID()] = interim_fields items[i]['service_uid'] = service.UID() items[i]['Service'] = service.Title() items[i]['Keyword'] = keyword items[i]['Unit'] = format_supsub(unit) if unit else '' items[i]['Result'] = '' items[i]['formatted_result'] = '' items[i]['interim_fields'] = interim_fields items[i]['Remarks'] = obj.getRemarks() items[i]['Uncertainty'] = '' items[i]['DetectionLimit'] = '' items[i]['retested'] = obj.getRetested() items[i]['class']['retested'] = 'center' items[i]['result_captured'] = self.ulocalized_time( obj.getResultCaptureDate(), long_format=0) items[i]['calculation'] = calculation and True or False try: items[i]['Partition'] = obj.getSamplePartition().getId() except AttributeError: items[i]['Partition'] = '' if obj.portal_type == "ReferenceAnalysis": items[i]['DueDate'] = self.ulocalized_time( obj.aq_parent.getExpiryDate(), long_format=0) else: items[i]['DueDate'] = self.ulocalized_time(obj.getDueDate(), long_format=1) cd = obj.getResultCaptureDate() items[i]['CaptureDate'] = cd and self.ulocalized_time( cd, long_format=1) or '' items[i]['Attachments'] = '' item['allow_edit'] = [] client_or_lab = "" tblrowclass = items[i].get('table_row_class') if obj.portal_type == 'ReferenceAnalysis': items[i]['st_uid'] = obj.aq_parent.UID() items[i]['table_row_class'] = ' '.join( [tblrowclass, 'qc-analysis']) elif obj.portal_type == 'DuplicateAnalysis' and \ obj.getAnalysis().portal_type == 'ReferenceAnalysis': items[i]['st_uid'] = obj.aq_parent.UID() items[i]['table_row_class'] = ' '.join( [tblrowclass, 'qc-analysis']) else: sample = None if self.context.portal_type == 'AnalysisRequest': sample = self.context.getSample() elif self.context.portal_type == 'Worksheet': if obj.portal_type in ('DuplicateAnalysis', 'RejectAnalysis'): sample = obj.getAnalysis().getSample() else: sample = obj.aq_parent.getSample() elif self.context.portal_type == 'Sample': sample = self.context st_uid = sample.getSampleType().UID() if sample else '' items[i]['st_uid'] = st_uid if checkPermission(ManageBika, self.context): service_uid = service.UID() latest = rc.lookupObject(service_uid).version_id items[i]['Service'] = service.Title() items[i]['class']['Service'] = "service_title" # Show version number of out-of-date objects # No: This should be done in another column, if at all. # The (vX) value confuses some more fragile forms. # if hasattr(obj, 'reference_versions') and \ # service_uid in obj.reference_versions and \ # latest != obj.reference_versions[service_uid]: # items[i]['after']['Service'] = "(v%s)" % \ # (obj.reference_versions[service_uid]) # choices defined on Service apply to result fields. choices = service.getResultOptions() if choices: item['choices']['Result'] = choices # permission to view this item's results can_view_result = \ getSecurityManager().checkPermission(ViewResults, obj) # permission to edit this item's results # Editing Field Results is possible while in Sample Due. poc = self.contentFilter.get("getPointOfCapture", 'lab') can_edit_analysis = self.allow_edit and context_active and \ ( (poc == 'field' and getSecurityManager().checkPermission(EditFieldResults, obj)) or (poc != 'field' and getSecurityManager().checkPermission(EditResults, obj)) ) allowed_method_states = [ 'to_be_sampled', 'to_be_preserved', 'sample_received', 'sample_registered', 'sampled', 'assigned' ] # Prevent from being edited if the instrument assigned # is not valid (out-of-date or uncalibrated), except if # the analysis is a QC with assigned status can_edit_analysis = can_edit_analysis \ and (obj.isInstrumentValid() \ or (obj.portal_type == 'ReferenceAnalysis' \ and item['review_state'] in allowed_method_states)) if can_edit_analysis: items[i]['allow_edit'].extend(['Analyst', 'Result', 'Remarks']) # if the Result field is editable, our interim fields are too for f in self.interim_fields[obj.UID()]: items[i]['allow_edit'].append(f['keyword']) # if there isn't a calculation then result must be re-testable, # and if there are interim fields, they too must be re-testable. if not items[i]['calculation'] or \ (items[i]['calculation'] and self.interim_fields[obj.UID()]): items[i]['allow_edit'].append('retested') # TODO: Only the labmanager must be able to change the method # can_set_method = getSecurityManager().checkPermission(SetAnalysisMethod, obj) can_set_method = can_edit_analysis \ and item['review_state'] in allowed_method_states method = obj.getMethod() \ if hasattr(obj, 'getMethod') and obj.getMethod() \ else service.getMethod() # Display the methods selector if the AS has at least one # method assigned item['Method'] = '' item['replace']['Method'] = '' if can_set_method: voc = self.get_methods_vocabulary(obj) if voc: # The service has at least one method available item['Method'] = method.UID() if method else '' item['choices']['Method'] = voc item['allow_edit'].append('Method') show_methodinstr_columns = True elif method: # This should never happen # The analysis has set a method, but its parent # service hasn't any method available O_o item['Method'] = method.Title() item['replace']['Method'] = "<a href='%s'>%s</a>" % \ (method.absolute_url(), method.Title()) show_methodinstr_columns = True elif method: # Edition not allowed, but method set item['Method'] = method.Title() item['replace']['Method'] = "<a href='%s'>%s</a>" % \ (method.absolute_url(), method.Title()) show_methodinstr_columns = True # TODO: Instrument selector dynamic behavior in worksheet Results # Only the labmanager must be able to change the instrument to be used. Also, # the instrument selection should be done in accordance with the method selected # can_set_instrument = service.getInstrumentEntryOfResults() and getSecurityManager().checkPermission(SetAnalysisInstrument, obj) can_set_instrument = service.getInstrumentEntryOfResults() \ and can_edit_analysis \ and item['review_state'] in allowed_method_states item['Instrument'] = '' item['replace']['Instrument'] = '' if service.getInstrumentEntryOfResults(): instrument = None # If the analysis has an instrument already assigned, use it if service.getInstrumentEntryOfResults() \ and hasattr(obj, 'getInstrument') \ and obj.getInstrument(): instrument = obj.getInstrument() # Otherwise, use the Service's default instrument elif service.getInstrumentEntryOfResults(): instrument = service.getInstrument() if can_set_instrument: # Edition allowed voc = self.get_instruments_vocabulary(obj) if voc: # The service has at least one instrument available item['Instrument'] = instrument.UID( ) if instrument else '' item['choices']['Instrument'] = voc item['allow_edit'].append('Instrument') show_methodinstr_columns = True elif instrument: # This should never happen # The analysis has an instrument set, but the # service hasn't any available instrument item['Instrument'] = instrument.Title() item['replace']['Instrument'] = "<a href='%s'>%s</a>" % \ (instrument.absolute_url(), instrument.Title()) show_methodinstr_columns = True elif instrument: # Edition not allowed, but instrument set item['Instrument'] = instrument.Title() item['replace']['Instrument'] = "<a href='%s'>%s</a>" % \ (instrument.absolute_url(), instrument.Title()) show_methodinstr_columns = True else: # Manual entry of results, instrument not allowed item['Instrument'] = _('Manual') msgtitle = t( _( "Instrument entry of results not allowed for ${service}", mapping={"service": safe_unicode(service.Title())}, )) item['replace']['Instrument'] = \ '<a href="#" title="%s">%s</a>' % (msgtitle, t(_('Manual'))) # Sets the analyst assigned to this analysis if can_edit_analysis: analyst = obj.getAnalyst() # widget default: current user if not analyst: analyst = mtool.getAuthenticatedMember().getUserName() items[i]['Analyst'] = analyst item['choices']['Analyst'] = self.getAnalysts() else: items[i]['Analyst'] = obj.getAnalystName() # If the user can attach files to analyses, show the attachment col can_add_attachment = \ getSecurityManager().checkPermission(AddAttachment, obj) if can_add_attachment or can_view_result: attachments = "" if hasattr(obj, 'getAttachment'): for attachment in obj.getAttachment(): af = attachment.getAttachmentFile() icon = af.getBestIcon() attachments += "<span class='attachment' attachment_uid='%s'>" % ( attachment.UID()) if icon: attachments += "<img src='%s/%s'/>" % ( self.portal_url, icon) attachments += '<a href="%s/at_download/AttachmentFile"/>%s</a>' % ( attachment.absolute_url(), af.filename) if can_edit_analysis: attachments += "<img class='deleteAttachmentButton' attachment_uid='%s' src='%s'/>" % ( attachment.UID(), "++resource++bika.lims.images/delete.png") attachments += "</br></span>" items[i]['replace'][ 'Attachments'] = attachments[:-12] + "</span>" # Only display data bearing fields if we have ViewResults # permission, otherwise just put an icon in Result column. if can_view_result: items[i]['Result'] = result scinot = self.context.bika_setup.getScientificNotationResults() dmk = self.context.bika_setup.getResultsDecimalMark() items[i]['formatted_result'] = obj.getFormattedResult( sciformat=int(scinot), decimalmark=dmk) # LIMS-1379 Allow manual uncertainty value input # https://jira.bikalabs.com/browse/LIMS-1379 fu = format_uncertainty(obj, result, decimalmark=dmk, sciformat=int(scinot)) fu = fu if fu else '' if can_edit_analysis and service.getAllowManualUncertainty( ) == True: unc = obj.getUncertainty(result) item['allow_edit'].append('Uncertainty') items[i]['Uncertainty'] = unc if unc else '' items[i]['before']['Uncertainty'] = '± ' items[i]['after'][ 'Uncertainty'] = '<em class="discreet" style="white-space:nowrap;"> %s</em>' % items[ i]['Unit'] elif fu: items[i]['Uncertainty'] = fu items[i]['before']['Uncertainty'] = '± ' items[i]['after'][ 'Uncertainty'] = '<em class="discreet" style="white-space:nowrap;"> %s</em>' % items[ i]['Unit'] # LIMS-1700. Allow manual input of Detection Limits # LIMS-1775. Allow to select LDL or UDL defaults in results with readonly mode # https://jira.bikalabs.com/browse/LIMS-1700 # https://jira.bikalabs.com/browse/LIMS-1775 if can_edit_analysis and \ hasattr(obj, 'getDetectionLimitOperand') and \ hasattr(service, 'getDetectionLimitSelector') and \ service.getDetectionLimitSelector() == True: isldl = obj.isBelowLowerDetectionLimit() isudl = obj.isAboveUpperDetectionLimit() dlval = '' if isldl or isudl: dlval = '<' if isldl else '>' item['allow_edit'].append('DetectionLimit') item['DetectionLimit'] = dlval choices = [{ 'ResultValue': '<', 'ResultText': '<' }, { 'ResultValue': '>', 'ResultText': '>' }] item['choices']['DetectionLimit'] = choices self.columns['DetectionLimit']['toggle'] = True srv = obj.getService() defdls = { 'min': srv.getLowerDetectionLimit(), 'max': srv.getUpperDetectionLimit(), 'manual': srv.getAllowManualDetectionLimit() } defin = '<input type="hidden" id="DefaultDLS.%s" value=\'%s\'/>' defin = defin % (obj.UID(), json.dumps(defdls)) item['after']['DetectionLimit'] = defin # LIMS-1769. Allow to use LDL and UDL in calculations. # https://jira.bikalabs.com/browse/LIMS-1769 # Since LDL, UDL, etc. are wildcards that can be used # in calculations, these fields must be loaded always # for 'live' calculations. if can_edit_analysis: dls = { 'default_ldl': 'none', 'default_udl': 'none', 'below_ldl': False, 'above_udl': False, 'is_ldl': False, 'is_udl': False, 'manual_allowed': False, 'dlselect_allowed': False } if hasattr(obj, 'getDetectionLimits'): dls['below_ldl'] = obj.isBelowLowerDetectionLimit() dls['above_udl'] = obj.isBelowLowerDetectionLimit() dls['is_ldl'] = obj.isLowerDetectionLimit() dls['is_udl'] = obj.isUpperDetectionLimit() dls['default_ldl'] = service.getLowerDetectionLimit() dls['default_udl'] = service.getUpperDetectionLimit() dls['manual_allowed'] = service.getAllowManualDetectionLimit( ) dls['dlselect_allowed'] = service.getDetectionLimitSelector( ) dlsin = '<input type="hidden" id="AnalysisDLS.%s" value=\'%s\'/>' dlsin = dlsin % (obj.UID(), json.dumps(dls)) item['after']['Result'] = dlsin else: items[i]['Specification'] = "" if 'Result' in items[i]['allow_edit']: items[i]['allow_edit'].remove('Result') items[i]['before']['Result'] = \ '<img width="16" height="16" ' + \ 'src="%s/++resource++bika.lims.images/to_follow.png"/>' % \ (self.portal_url) # Everyone can see valid-ranges spec = self.get_analysis_spec(obj) if spec: min_val = spec.get('min', '') min_str = ">{0}".format(min_val) if min_val else '' max_val = spec.get('max', '') max_str = "<{0}".format(max_val) if max_val else '' error_val = spec.get('error', '') error_str = "{0}%".format(error_val) if error_val else '' rngstr = ",".join( [x for x in [min_str, max_str, error_str] if x]) else: rngstr = "" items[i]['Specification'] = rngstr # Add this analysis' interim fields to the interim_columns list for f in self.interim_fields[obj.UID()]: if f['keyword'] not in self.interim_columns and not f.get( 'hidden', False): self.interim_columns[f['keyword']] = f['title'] # and to the item itself items[i][f['keyword']] = f items[i]['class'][f['keyword']] = 'interim' # check if this analysis is late/overdue resultdate = obj.aq_parent.getDateSampled() \ if obj.portal_type == 'ReferenceAnalysis' \ else obj.getResultCaptureDate() duedate = obj.aq_parent.getExpiryDate() \ if obj.portal_type == 'ReferenceAnalysis' \ else obj.getDueDate() items[i]['replace']['DueDate'] = \ self.ulocalized_time(duedate, long_format=1) if items[i]['review_state'] not in [ 'to_be_sampled', 'to_be_preserved', 'sample_due', 'published' ]: if (resultdate and resultdate > duedate) \ or (not resultdate and DateTime() > duedate): items[i]['replace']['DueDate'] = '%s <img width="16" height="16" src="%s/++resource++bika.lims.images/late.png" title="%s"/>' % \ (self.ulocalized_time(duedate, long_format=1), self.portal_url, t(_("Late Analysis"))) # Submitting user may not verify results (admin can though) if items[i]['review_state'] == 'to_be_verified' and \ not checkPermission(VerifyOwnResults, obj): user_id = getSecurityManager().getUser().getId() self_submitted = False try: review_history = list( workflow.getInfoFor(obj, 'review_history')) review_history.reverse() for event in review_history: if event.get('action') == 'submit': if event.get('actor') == user_id: self_submitted = True break if self_submitted: items[i]['after']['state_title'] = \ "<img src='++resource++bika.lims.images/submitted-by-current-user.png' title='%s'/>" % \ (t(_("Cannot verify: Submitted by current user"))) except WorkflowException: pass # add icon for assigned analyses in AR views if self.context.portal_type == 'AnalysisRequest': obj = items[i]['obj'] if obj.portal_type in ['ReferenceAnalysis', 'DuplicateAnalysis'] or \ workflow.getInfoFor(obj, 'worksheetanalysis_review_state') == 'assigned': br = obj.getBackReferences('WorksheetAnalysis') if len(br) > 0: ws = br[0] items[i]['after']['state_title'] = \ "<a href='%s'><img src='++resource++bika.lims.images/worksheet.png' title='%s'/></a>" % \ (ws.absolute_url(), t(_("Assigned to: ${worksheet_id}", mapping={'worksheet_id': safe_unicode(ws.id)}))) # the TAL requires values for all interim fields on all # items, so we set blank values in unused cells for item in items: for field in self.interim_columns: if field not in item: item[field] = '' # XXX order the list of interim columns interim_keys = self.interim_columns.keys() interim_keys.reverse() # add InterimFields keys to columns for col_id in interim_keys: if col_id not in self.columns: self.columns[col_id] = { 'title': self.interim_columns[col_id], 'input_width': '6', 'input_class': 'ajax_calculate numeric', 'sortable': False } if can_edit_analyses: new_states = [] for state in self.review_states: # InterimFields are displayed in review_state # They are anyway available through View.columns though. # In case of hidden fields, the calcs.py should check calcs/services # for additional InterimFields!! pos = 'Result' in state['columns'] and \ state['columns'].index('Result') or len(state['columns']) for col_id in interim_keys: if col_id not in state['columns']: state['columns'].insert(pos, col_id) # retested column is added after Result. pos = 'Result' in state['columns'] and \ state['columns'].index('Uncertainty') + 1 or len(state['columns']) state['columns'].insert(pos, 'retested') new_states.append(state) self.review_states = new_states # Allow selecting individual analyses self.show_select_column = True # Dry Matter. # The Dry Matter column is never enabled for reference sample contexts # and refers to getReportDryMatter in ARs. if items and \ (hasattr(self.context, 'getReportDryMatter') and \ self.context.getReportDryMatter()): # look through all items # if the item's Service supports ReportDryMatter, add getResultDM(). for item in items: if item['obj'].getService().getReportDryMatter(): item['ResultDM'] = item['obj'].getResultDM() else: item['ResultDM'] = '' if item['ResultDM']: item['after']['ResultDM'] = "<em class='discreet'>%</em>" # modify the review_states list to include the ResultDM column new_states = [] for state in self.review_states: pos = 'Result' in state['columns'] and \ state['columns'].index('Uncertainty') + 1 or len(state['columns']) state['columns'].insert(pos, 'ResultDM') new_states.append(state) self.review_states = new_states self.categories.sort() # self.json_specs = json.dumps(self.specs) self.json_interim_fields = json.dumps(self.interim_fields) self.items = items # Method and Instrument columns must be shown or hidden at the # same time, because the value assigned to one causes # a value reassignment to the other (one method can be performed # by different instruments) self.columns['Method']['toggle'] = show_methodinstr_columns self.columns['Instrument']['toggle'] = show_methodinstr_columns return items
def calculate(self, uid=None): analysis = self.analyses[uid] form_result = self.current_results[uid] service = analysis.getService() calculation = service.getCalculation() if analysis.portal_type == 'ReferenceAnalysis': deps = {} else: deps = {} for dep in analysis.getDependencies(): deps[dep.UID()] = dep path = '++resource++bika.lims.images' mapping = {} # values to be returned to form for this UID Result = {'uid': uid, 'result': form_result} try: Result['result'] = float(form_result) except: if form_result == "0/0": Result['result'] = "" if calculation: # add all our dependent analyses results to the mapping. # Retrieve value from database if it's not in the current_results. unsatisfied = False for dependency_uid, dependency in deps.items(): if dependency_uid in self.ignore_uids: unsatisfied = True break if dependency_uid in self.current_results: result = self.current_results[dependency_uid] else: result = dependency.getResult() if result == '': unsatisfied = True break key = dependency.getService().getKeyword() # All mappings must be float, or they are ignored. try: mapping[key] = float(self.current_results[dependency_uid]) except: pass if unsatisfied: # unsatisfied means that one or more result on which we depend # is blank or unavailable, so we set blank result and abort. self.results.append({'uid': uid, 'result': '', 'formatted_result': ''}) return None # Add all interims to mapping for i_uid, i_data in self.item_data.items(): for i in i_data: # if this interim belongs to current analysis and is blank, # return an empty result for this analysis. if i_uid == uid and i['value'] == '': self.results.append({'uid': uid, 'result': '', 'formatted_result': ''}) return None # All interims must be float, or they are ignored. try: i['value'] = float(i['value']) except: pass # all interims are ServiceKeyword.InterimKeyword if i_uid in deps: key = "%s.%s" % (deps[i_uid].getService().getKeyword(), i['keyword']) mapping[key] = i['value'] # this analysis' interims get extra reference # without service keyword prefix if uid == i_uid: mapping[i['keyword']] = i['value'] # Grab values for hidden InterimFields for only for current calculation # we can't allow non-floats through here till we change the eval's # interpolation hidden_fields = [] c_fields = calculation.getInterimFields() s_fields = service.getInterimFields() for field in c_fields: if field.get('hidden', False): hidden_fields.append(field['keyword']) try: mapping[field['keyword']] = float(field['value']) except ValueError: pass # also grab stickier defaults from AnalysisService for field in s_fields: if field['keyword'] in hidden_fields: try: mapping[field['keyword']] = float(field['value']) except ValueError: pass # convert formula to a valid python string, ready for interpolation formula = calculation.getMinifiedFormula() formula = formula.replace('[', '%(').replace(']', ')f') try: formula = eval("'%s'%%mapping" % formula, {"__builtins__": None, 'math': math, 'context': self.context}, {'mapping': mapping}) # calculate result = eval(formula) Result['result'] = result self.current_results[uid] = result except TypeError as e: # non-numeric arguments in interim mapping? alert = {'field': 'Result', 'icon': path + '/exclamation.png', 'msg': "{0}: {1} ({2}) ".format( t(_("Type Error")), html_quote(str(e.args[0])), formula)} if uid in self.alerts: self.alerts[uid].append(alert) else: self.alerts[uid] = [alert, ] except ZeroDivisionError as e: Result['result'] = '0/0' Result['formatted_result'] = '0/0' self.current_results[uid] = '0/0' self.results.append(Result) alert = {'field': 'Result', 'icon': path + '/exclamation.png', 'msg': "{0}: {1} ({2}) ".format( t(_("Division by zero")), html_quote(str(e.args[0])), formula)} if uid in self.alerts: self.alerts[uid].append(alert) else: self.alerts[uid] = [alert, ] return None except KeyError as e: alert = {'field': 'Result', 'icon': path + '/exclamation.png', 'msg': "{0}: {1} ({2}) ".format( t(_("Key Error")), html_quote(str(e.args[0])), formula)} if uid in self.alerts: self.alerts[uid].append(alert) else: self.alerts[uid] = [alert, ] # format result belowmin = False abovemax = False # Some analyses will not have AnalysisSpecs, eg, ReferenceAnalysis if hasattr(analysis, 'getAnalysisSpecs'): specs = analysis.getAnalysisSpecs() specs = specs.getResultsRangeDict() if specs is not None else {} specs = specs.get(analysis.getKeyword(), {}) hidemin = specs.get('hidemin', '') hidemax = specs.get('hidemax', '') if Result.get('result', ''): fresult = Result['result'] try: belowmin = hidemin and fresult < float(hidemin) or False except ValueError: belowmin = False pass try: abovemax = hidemax and fresult > float(hidemax) or False except ValueError: abovemax = False pass if belowmin is True: Result['formatted_result'] = '< %s' % hidemin elif abovemax is True: Result['formatted_result'] = '> %s' % hidemax else: try: Result['formatted_result'] = format_numeric_result(analysis, Result['result']) except ValueError: # non-float Result['formatted_result'] = Result['result'] # calculate Dry Matter result # if parent is not an AR, it's never going to be calculable dm = hasattr(analysis.aq_parent, 'getReportDryMatter') and \ analysis.aq_parent.getReportDryMatter() and \ analysis.getService().getReportDryMatter() if dm: dry_service = self.context.bika_setup.getDryMatterService() # get the UID of the DryMatter Analysis from our parent AR dry_analysis = [a for a in analysis.aq_parent.getAnalyses(full_objects=True) if a.getService().UID() == dry_service.UID()] if dry_analysis: dry_analysis = dry_analysis[0] dry_uid = dry_analysis.UID() # get the current DryMatter analysis result from the form if dry_uid in self.current_results: try: dry_result = float(self.current_results[dry_uid]) except: dm = False else: try: dry_result = float(dry_analysis.getResult()) except: dm = False else: dm = False Result['dry_result'] = dm and dry_result and \ '%.2f' % ((Result['result'] / dry_result) * 100) or '' self.results.append(Result) # if App.config.getConfiguration().debug_mode: # logger.info("calc.py: %s->%s %s" % (analysis.aq_parent.id, # analysis.id, # Result)) self.uncertainties.append({'uid': uid, 'uncertainty': analysis.getUncertainty( Result['result'])}) # maybe a service who depends on us must be recalculated. if analysis.portal_type == 'ReferenceAnalysis': dependents = [] else: dependents = analysis.getDependents() if dependents: for dependent in dependents: dependent_uid = dependent.UID() # ignore analyses that no longer exist. if dependent_uid in self.ignore_uids or \ dependent_uid not in self.analyses: continue self.calculate(dependent_uid) # These self.alerts are just for the json return. # we're placing the entire form's results in kwargs. adapters = getAdapters((analysis, ), IFieldIcons) for name, adapter in adapters: alerts = adapter(result=Result['result'], form_results=self.current_results) if alerts: if analysis.UID() in self.alerts: self.alerts[analysis.UID()].extend(alerts[analysis.UID()]) else: self.alerts[analysis.UID()] = alerts[analysis.UID()]
def calculate(self, uid=None): analysis = self.analyses[uid] form_result = self.current_results[uid]['result'] service = analysis.getService() calculation = service.getCalculation() if analysis.portal_type == 'ReferenceAnalysis': deps = {} else: deps = {} for dep in analysis.getDependencies(): deps[dep.UID()] = dep path = '++resource++bika.lims.images' mapping = {} # values to be returned to form for this UID Result = {'uid': uid, 'result': form_result} try: Result['result'] = float(form_result) except: if form_result == "0/0": Result['result'] = "" if calculation: ''' We need first to create the map of available parameters acording to the interims, analyses and wildcards: params = { <as-1-keyword> : <analysis_result>, <as-1-keyword>.<wildcard-1> : <wildcard_1_value>, <as-1-keyword>.<wildcard-2> : <wildcard_2_value>, <interim-1> : <interim_result>, ... } ''' # Get dependent analyses results and wildcard values to the # mapping. If dependent analysis without result found, # break and abort calculation unsatisfied = False for dependency_uid, dependency in deps.items(): if dependency_uid in self.ignore_uids: unsatisfied = True break # LIMS-1769. Allow to use LDL and UDL in calculations. # https://jira.bikalabs.com/browse/LIMS-1769 analysisvalues = {} if dependency_uid in self.current_results: analysisvalues = self.current_results[dependency_uid] else: # Retrieve the result and DLs from the analysis analysisvalues = { 'keyword': dependency.getKeyword(), 'result': dependency.getResult(), 'ldl': dependency.getLowerDetectionLimit(), 'udl': dependency.getUpperDetectionLimit(), 'belowldl': dependency.isBelowLowerDetectionLimit(), 'aboveudl': dependency.isAboveUpperDetectionLimit(), } if analysisvalues['result'] == '': unsatisfied = True break key = analysisvalues.get('keyword', dependency.getService().getKeyword()) # Analysis result # All result mappings must be float, or they are ignored. try: mapping[key] = float(analysisvalues.get('result')) mapping['%s.%s' % (key, 'RESULT')] = float( analysisvalues.get('result')) mapping['%s.%s' % (key, 'LDL')] = float( analysisvalues.get('ldl')) mapping['%s.%s' % (key, 'UDL')] = float( analysisvalues.get('udl')) mapping['%s.%s' % (key, 'BELOWLDL')] = int( analysisvalues.get('belowldl')) mapping['%s.%s' % (key, 'ABOVEUDL')] = int( analysisvalues.get('aboveudl')) except: # If not floatable, then abort! unsatisfied = True break if unsatisfied: # unsatisfied means that one or more result on which we depend # is blank or unavailable, so we set blank result and abort. self.results.append({ 'uid': uid, 'result': '', 'formatted_result': '' }) return None # Add all interims to mapping for i_uid, i_data in self.item_data.items(): for i in i_data: # if this interim belongs to current analysis and is blank, # return an empty result for this analysis. if i_uid == uid and i['value'] == '': self.results.append({ 'uid': uid, 'result': '', 'formatted_result': '' }) return None # All interims must be float, or they are ignored. try: i['value'] = float(i['value']) except: pass # all interims are ServiceKeyword.InterimKeyword if i_uid in deps: key = "%s.%s" % (deps[i_uid].getService().getKeyword(), i['keyword']) mapping[key] = i['value'] # this analysis' interims get extra reference # without service keyword prefix if uid == i_uid: mapping[i['keyword']] = i['value'] # Grab values for hidden InterimFields for only for current calculation # we can't allow non-floats through here till we change the eval's # interpolation hidden_fields = [] c_fields = calculation.getInterimFields() s_fields = service.getInterimFields() for field in c_fields: if field.get('hidden', False): hidden_fields.append(field['keyword']) try: mapping[field['keyword']] = float(field['value']) except ValueError: pass # also grab stickier defaults from AnalysisService for field in s_fields: if field['keyword'] in hidden_fields: try: mapping[field['keyword']] = float(field['value']) except ValueError: pass # convert formula to a valid python string, ready for interpolation formula = calculation.getMinifiedFormula() formula = formula.replace('[', '%(').replace(']', ')f') try: formula = eval("'%s'%%mapping" % formula, { "__builtins__": None, 'math': math, 'context': self.context }, {'mapping': mapping}) # calculate result = eval(formula) Result['result'] = result self.current_results[uid]['result'] = result except TypeError as e: # non-numeric arguments in interim mapping? alert = { 'field': 'Result', 'icon': path + '/exclamation.png', 'msg': "{0}: {1} ({2}) ".format(t(_("Type Error")), html_quote(str(e.args[0])), formula) } if uid in self.alerts: self.alerts[uid].append(alert) else: self.alerts[uid] = [ alert, ] except ZeroDivisionError as e: Result['result'] = '0/0' Result['formatted_result'] = '0/0' self.current_results[uid]['result'] = '0/0' self.results.append(Result) alert = { 'field': 'Result', 'icon': path + '/exclamation.png', 'msg': "{0}: {1} ({2}) ".format(t(_("Division by zero")), html_quote(str(e.args[0])), formula) } if uid in self.alerts: self.alerts[uid].append(alert) else: self.alerts[uid] = [ alert, ] return None except KeyError as e: alert = { 'field': 'Result', 'icon': path + '/exclamation.png', 'msg': "{0}: {1} ({2}) ".format(t(_("Key Error")), html_quote(str(e.args[0])), formula) } if uid in self.alerts: self.alerts[uid].append(alert) else: self.alerts[uid] = [ alert, ] # format result belowmin = False abovemax = False # Some analyses will not have AnalysisSpecs, eg, ReferenceAnalysis if hasattr(analysis, 'getAnalysisSpecs'): specs = analysis.getAnalysisSpecs() specs = specs.getResultsRangeDict() if specs is not None else {} specs = specs.get(analysis.getKeyword(), {}) hidemin = specs.get('hidemin', '') hidemax = specs.get('hidemax', '') if Result.get('result', ''): fresult = Result['result'] try: belowmin = hidemin and fresult < float(hidemin) or False except ValueError: belowmin = False pass try: abovemax = hidemax and fresult > float(hidemax) or False except ValueError: abovemax = False pass if belowmin is True: Result['formatted_result'] = '< %s' % hidemin elif abovemax is True: Result['formatted_result'] = '> %s' % hidemax else: try: Result['formatted_result'] = format_numeric_result( analysis, Result['result']) except ValueError: # non-float Result['formatted_result'] = Result['result'] # calculate Dry Matter result # if parent is not an AR, it's never going to be calculable dm = hasattr(analysis.aq_parent, 'getReportDryMatter') and \ analysis.aq_parent.getReportDryMatter() and \ analysis.getService().getReportDryMatter() if dm: dry_service = self.context.bika_setup.getDryMatterService() # get the UID of the DryMatter Analysis from our parent AR dry_analysis = [ a for a in analysis.aq_parent.getAnalyses(full_objects=True) if a.getService().UID() == dry_service.UID() ] if dry_analysis: dry_analysis = dry_analysis[0] dry_uid = dry_analysis.UID() # get the current DryMatter analysis result from the form if dry_uid in self.current_results: try: dry_result = float(self.current_results[dry_uid]) except: dm = False else: try: dry_result = float(dry_analysis.getResult()) except: dm = False else: dm = False Result['dry_result'] = dm and dry_result and \ '%.2f' % ((Result['result'] / dry_result) * 100) or '' self.results.append(Result) # if App.config.getConfiguration().debug_mode: # logger.info("calc.py: %s->%s %s" % (analysis.aq_parent.id, # analysis.id, # Result)) # LIMS-1808 Uncertainty calculation on DL # https://jira.bikalabs.com/browse/LIMS-1808 flres = Result.get('result', None) if flres and isnumber(flres): flres = float(flres) anvals = self.current_results[uid] isldl = anvals.get('isldl', False) isudl = anvals.get('isudl', False) ldl = anvals.get('ldl', 0) udl = anvals.get('udl', 0) ldl = float(ldl) if isnumber(ldl) else 0 udl = float(udl) if isnumber(udl) else 10000000 belowldl = (isldl or flres < ldl) aboveudl = (isudl or flres > udl) unc = '' if (belowldl or aboveudl) else analysis.getUncertainty( Result.get('result')) if not (belowldl or aboveudl): self.uncertainties.append({'uid': uid, 'uncertainty': unc}) # maybe a service who depends on us must be recalculated. if analysis.portal_type == 'ReferenceAnalysis': dependents = [] else: dependents = analysis.getDependents() if dependents: for dependent in dependents: dependent_uid = dependent.UID() # ignore analyses that no longer exist. if dependent_uid in self.ignore_uids or \ dependent_uid not in self.analyses: continue self.calculate(dependent_uid) # These self.alerts are just for the json return. # we're placing the entire form's results in kwargs. adapters = getAdapters((analysis, ), IFieldIcons) for name, adapter in adapters: alerts = adapter(result=Result['result'], form_results=self.current_results) if alerts: if analysis.UID() in self.alerts: self.alerts[analysis.UID()].extend(alerts[analysis.UID()]) else: self.alerts[analysis.UID()] = alerts[analysis.UID()]
def getFormattedResult(self, specs=None, decimalmark='.', sciformat=1): """Formatted result: 1. Print ResultText of matching ResultOptions 2. If the result is not floatable, return it without being formatted 3. If the analysis specs has hidemin or hidemax enabled and the result is out of range, render result as '<min' or '>max' 4. Otherwise, render numerical value specs param is optional. A dictionary as follows: {'min': <min_val>, 'max': <max_val>, 'error': <error>, 'hidemin': <hidemin_val>, 'hidemax': <hidemax_val>} :param sciformat: 1. The sci notation has to be formatted as aE^+b 2. The sci notation has to be formatted as a·10^b 3. As 2, but with super html entity for exp 4. The sci notation has to be formatted as a·10^b 5. As 4, but with super html entity for exp By default 1 """ result = self.getResult() service = self.getService() choices = service.getResultOptions() # 1. Print ResultText of matching ResulOptions match = [x['ResultText'] for x in choices if str(x['ResultValue']) == str(result)] if match: return match[0] # 2. If the result is not floatable, return it without being formatted try: result = float(result) except: return result # 3. If the analysis specs has enabled hidemin or hidemax and the # result is out of range, render result as '<min' or '>max' belowmin = False abovemax = False if not specs: specs = self.getAnalysisSpecs() specs = specs.getResultsRangeDict() if specs is not None else {} specs = specs.get(self.getKeyword(), {}) hidemin = specs.get('hidemin', '') hidemax = specs.get('hidemax', '') try: belowmin = hidemin and result < float(hidemin) or False except: belowmin = False pass try: abovemax = hidemax and result > float(hidemax) or False except: abovemax = False pass # 3.1. If result is below min and hidemin enabled, return '<min' if belowmin: return formatDecimalMark('< %s' % hidemin, decimalmark) # 3.2. If result is above max and hidemax enabled, return '>max' if abovemax: return formatDecimalMark('> %s' % hidemax, decimalmark) # Render numerical values return formatDecimalMark(format_numeric_result(self, result, sciformat=sciformat), decimalmark=decimalmark)