def process_form(self, instance, field, form, empty_marker = None, emptyReturnsMarker = False): """ Return a list of dictionaries fit for AnalysisSpecsResultsField consumption. If neither hidemin nor hidemax are specified, only services which have float()able entries in result,min and max field will be included. If hidemin and/or hidemax specified, results might contain empty min and/or max fields. """ value = [] if 'service' in form: for uid, keyword in form['keyword'][0].items(): hidemin = form['hidemin'][0].get(uid, '') if 'hidemin' in form else '' hidemax = form['hidemax'][0].get(uid, '') if 'hidemax' in form else '' mins = form['min'][0].get(uid, '') if 'min' in form else '' maxs = form['max'][0].get(uid, '') if 'max' in form else '' err = form['error'][0].get(uid, '') if 'error' in form else '' rangecomment = form['rangecomment'][0].get(uid, '') if 'rangecomment' in form else '' if not isnumber(hidemin) and not isnumber(hidemax) and \ (not isnumber(mins) or not isnumber(maxs)): # If neither hidemin nor hidemax have been specified, # min and max values are mandatory. continue value.append({'keyword': keyword, 'uid': uid, 'min': mins if isnumber(mins) else '', 'max': maxs if isnumber(maxs) else '', 'hidemin': hidemin if isnumber(hidemin) else '', 'hidemax': hidemax if isnumber(hidemax) else '', 'error': err if isnumber(err) else '0', 'rangecomment': rangecomment}) return value, {}
def _check_conditions(instance, conditions): uc = getToolByName(instance, 'uid_catalog') for condition in conditions: range0 = condition.get('range0', None) range1 = condition.get('range1', None) discreteresult = condition.get('discreteresult', None) analysisservice = condition.get('analysisservice', None) and_or = condition.get('and_or', 'no') cond_row_idx = condition.get('cond_row_idx', None) as_brain = uc(UID=analysisservice) if (not discreteresult and (not range0 or not range1)) or \ (discreteresult and range0 and range1): logger.warn( _('If range values are empty, discreteresult must contain a ' 'value, and if discreteresult has a value, ranges must be ' 'empty. But ranges or discreteresult must conatin a value.' 'The given values are: ' 'discreteresult: %s, range0: %s, range1: %s' % (discreteresult, range0, range1))) return False if range1 and not (isnumber(range1)): logger.warn('The range must be a number. Now its value is: ' '%s' % (range1)) return False if range0 and not (isnumber(range0)): logger.warn('The range must be a number. Now its value is: ' '%s' % (range0)) return False if not as_brain: logger.warn('Not correct analysis service with UID. %s' % (analysisservice)) return False if and_or not in ['and', 'or', 'no']: logger.warn('Not correct and_or value') return False if cond_row_idx and not (isnumber(cond_row_idx)): logger.warn('The cond_row_idx must be a number. Now its value is: ' '%s' % (cond_row_idx)) return False return True
def process_form(self, instance, field, form, empty_marker=None, emptyReturnsMarker=False): values = BaseWidget.process_form(self, instance, field, form, empty_marker, emptyReturnsMarker) keys = ['minpanic', 'maxpanic'] for i in range(len(values)): for j in range(len(values[i])): uid = values[i][j]['uid'] for key in keys: keyval = form[key][0].get(uid, '') if key in form else '' keyval = keyval if isnumber(keyval) else '' values[i][j][key] = keyval return values
def _check_actions(instance, actions): uc = getToolByName(instance, 'uid_catalog') for action in actions: act_row_idx = action.get('act_row_idx', '0') action_name = action.get('action', '') an_result_id = action.get('an_result_id', '') analyst = action.get('analyst', '') otherWS = action.get('otherWS', 'current') setresultdiscrete = action.get('setresultdiscrete', '') setresulton = action.get('setresulton', '') setresultvalue = action.get('setresultvalue', '') worksheettemplate = action.get('worksheettemplate', '') wt_brain = uc(UID=worksheettemplate) if act_row_idx and not (isnumber(act_row_idx)): logger.warn('The act_row_idx must be a number. Now its value is: ' '%s' % (act_row_idx)) return False if action_name not in [ 'repeat', 'duplicate', 'setresult', 'setvisibility' ]: logger.warn('Not correct action_name value') return False if otherWS not in ['current', 'to_another', 'create_another', 'no_ws']: logger.warn('Not correct otherWS value') return False if setresulton not in ['original', 'bew']: logger.warn('Not correct setresulton value') return False if setresultvalue and not (isnumber(setresultvalue)): logger.warn( 'The setresultvalue must be a number. Now its value is: ' '%s' % (setresultvalue)) return False if worksheettemplate and not wt_brain: logger.warn('Not correct worksheet template with UID. %s' % (worksheettemplate)) return False return True
def process_form(self, instance, field, form, empty_marker=None, emptyReturnsMarker=False): """ Return a list of dictionaries fit for AnalysisSpecsResultsField consumption. If neither hidemin nor hidemax are specified, only services which have float()able entries in result,min and max field will be included. If hidemin and/or hidemax specified, results might contain empty min and/or max fields. """ value = [] if "service" in form: for uid, keyword in form["keyword"][0].items(): hidemin = form["hidemin"][0].get(uid, "") if "hidemin" in form else "" hidemax = form["hidemax"][0].get(uid, "") if "hidemax" in form else "" mins = form["min"][0].get(uid, "") if "min" in form else "" maxs = form["max"][0].get(uid, "") if "max" in form else "" err = form["error"][0].get(uid, "") if "error" in form else "" rangecomment = form["rangecomment"][0].get(uid, "") if "rangecomment" in form else "" if not isnumber(hidemin) and not isnumber(hidemax) and (not isnumber(mins) or not isnumber(maxs)): # If neither hidemin nor hidemax have been specified, # min and max values are mandatory. continue value.append( { "keyword": keyword, "uid": uid, "min": mins if isnumber(mins) else "", "max": maxs if isnumber(maxs) else "", "hidemin": hidemin if isnumber(hidemin) else "", "hidemax": hidemax if isnumber(hidemax) else "", "error": err if isnumber(err) else "0", "rangecomment": rangecomment, } ) return value, {}
def _analysis_data(self, analysis, decimalmark=None): keyword = analysis.getKeyword() service = analysis.getService() andict = {'obj': analysis, 'id': analysis.id, 'title': analysis.Title(), 'keyword': keyword, 'scientific_name': service.getScientificName(), 'accredited': service.getAccredited(), 'point_of_capture': to_utf8(POINTS_OF_CAPTURE.getValue(service.getPointOfCapture())), 'category': to_utf8(service.getCategoryTitle()), 'result': analysis.getResult(), 'isnumber': isnumber(analysis.getResult()), 'unit': to_utf8(service.getUnit()), 'formatted_unit': format_supsub(to_utf8(service.getUnit())), 'capture_date': analysis.getResultCaptureDate(), 'request_id': analysis.aq_parent.getId(), 'formatted_result': '', 'uncertainty': analysis.getUncertainty(), 'formatted_uncertainty': '', 'retested': analysis.getRetested(), 'remarks': to_utf8(analysis.getRemarks()), 'resultdm': to_utf8(analysis.getResultDM()), 'outofrange': False, 'type': analysis.portal_type, 'reftype': analysis.getReferenceType() \ if hasattr(analysis, 'getReferenceType') else None, 'worksheet': None, 'specs': {}, 'formatted_specs': ''} if analysis.portal_type == 'DuplicateAnalysis': andict['reftype'] = 'd' ws = analysis.getBackReferences('WorksheetAnalysis') andict['worksheet'] = ws[0].id if ws and len(ws) > 0 else None andict['worksheet_url'] = ws[0].absolute_url if ws and len(ws) > 0 else None andict['refsample'] = analysis.getSample().id \ if analysis.portal_type == 'Analysis' \ else '%s - %s' % (analysis.aq_parent.id, analysis.aq_parent.Title()) 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() andict['specs'] = specs scinot = self.context.bika_setup.getScientificNotationReport() andict['formatted_result'] = analysis.getFormattedResult(specs=specs, sciformat=int(scinot), decimalmark=decimalmark) fs = '' if specs.get('min', None) and specs.get('max', None): fs = '%s - %s' % (specs['min'], specs['max']) elif specs.get('min', None): fs = '> %s' % specs['min'] elif specs.get('max', None): fs = '< %s' % specs['max'] andict['formatted_specs'] = formatDecimalMark(fs, decimalmark) andict['formatted_uncertainty'] = format_uncertainty(analysis, analysis.getResult(), decimalmark=decimalmark, sciformat=int(scinot)) # Out of range? if specs: adapters = getAdapters((analysis, ), IResultOutOfRange) bsc = getToolByName(self.context, "bika_setup_catalog") for name, adapter in adapters: ret = adapter(specification=specs) if ret and ret['out_of_range']: andict['outofrange'] = True break return andict
def _analysis_data(self, analysis, decimalmark=None): if analysis.UID() in self._cache['_analysis_data']: return self._cache['_analysis_data'][analysis.UID()] keyword = analysis.getKeyword() service = analysis.getService() andict = {'obj': analysis, 'id': analysis.id, 'title': analysis.Title(), 'keyword': keyword, 'scientific_name': service.getScientificName(), 'accredited': service.getAccredited(), 'point_of_capture': to_utf8(POINTS_OF_CAPTURE.getValue(service.getPointOfCapture())), 'category': to_utf8(service.getCategoryTitle()), 'result': analysis.getResult(), 'isnumber': isnumber(analysis.getResult()), 'unit': to_utf8(service.getUnit()), 'formatted_unit': format_supsub(to_utf8(service.getUnit())), 'capture_date': analysis.getResultCaptureDate(), 'request_id': analysis.aq_parent.getId(), 'formatted_result': '', 'uncertainty': analysis.getUncertainty(), 'formatted_uncertainty': '', 'retested': analysis.getRetested(), 'remarks': to_utf8(analysis.getRemarks()), 'resultdm': to_utf8(analysis.getResultDM()), 'outofrange': False, 'type': analysis.portal_type, 'reftype': analysis.getReferenceType() \ if hasattr(analysis, 'getReferenceType') else None, 'worksheet': None, 'specs': {}, 'formatted_specs': ''} if analysis.portal_type == 'DuplicateAnalysis': andict['reftype'] = 'd' ws = analysis.getBackReferences('WorksheetAnalysis') andict['worksheet'] = ws[0].id if ws and len(ws) > 0 else None andict['worksheet_url'] = ws[0].absolute_url if ws and len(ws) > 0 else None andict['refsample'] = analysis.getSample().id \ if analysis.portal_type == 'Analysis' \ else '%s - %s' % (analysis.aq_parent.id, analysis.aq_parent.Title()) 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() andict['specs'] = specs scinot = self.context.bika_setup.getScientificNotationReport() fresult = analysis.getFormattedResult(specs=specs, sciformat=int(scinot), decimalmark=decimalmark) # We don't use here cgi.encode because results fields must be rendered # using the 'structure' wildcard. The reason is that the result can be # expressed in sci notation, that may include <sup></sup> html tags. # Please note the default value for the 'html' parameter from # getFormattedResult signature is set to True, so the service will # already take into account LDLs and UDLs symbols '<' and '>' and escape # them if necessary. andict['formatted_result'] = fresult; fs = '' if specs.get('min', None) and specs.get('max', None): fs = '%s - %s' % (specs['min'], specs['max']) elif specs.get('min', None): fs = '> %s' % specs['min'] elif specs.get('max', None): fs = '< %s' % specs['max'] andict['formatted_specs'] = formatDecimalMark(fs, decimalmark) andict['formatted_uncertainty'] = format_uncertainty(analysis, analysis.getResult(), decimalmark=decimalmark, sciformat=int(scinot)) # Out of range? if specs: adapters = getAdapters((analysis, ), IResultOutOfRange) bsc = getToolByName(self.context, "bika_setup_catalog") for name, adapter in adapters: ret = adapter(specification=specs) if ret and ret['out_of_range']: andict['outofrange'] = True break self._cache['_analysis_data'][analysis.UID()] = andict return andict
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 _analysis_data(self, analysis, decimalmark=None): keyword = analysis.getKeyword() service = analysis.getService() andict = {'obj': analysis, 'id': analysis.id, 'title': analysis.Title(), 'keyword': keyword, 'scientific_name': service.getScientificName(), 'accredited': service.getAccredited(), 'point_of_capture': to_utf8(POINTS_OF_CAPTURE.getValue(service.getPointOfCapture())), 'category': to_utf8(service.getCategoryTitle()), 'result': analysis.getResult(), 'isnumber': isnumber(analysis.getResult()), 'unit': to_utf8(service.getUnit()), 'formatted_unit': format_supsub(to_utf8(service.getUnit())), 'capture_date': analysis.getResultCaptureDate(), 'request_id': analysis.aq_parent.getId(), 'formatted_result': '', 'uncertainty': analysis.getUncertainty(), 'formatted_uncertainty': '', 'retested': analysis.getRetested(), 'remarks': to_utf8(analysis.getRemarks()), 'resultdm': to_utf8(analysis.getResultDM()), 'outofrange': False, 'type': analysis.portal_type, 'reftype': analysis.getReferenceType() \ if hasattr(analysis, 'getReferenceType') else None, 'worksheet': None, 'specs': {}, 'formatted_specs': ''} if analysis.portal_type == 'DuplicateAnalysis': andict['reftype'] = 'd' ws = analysis.getBackReferences('WorksheetAnalysis') andict['worksheet'] = ws[0].id if ws and len(ws) > 0 else None andict['worksheet_url'] = ws[0].absolute_url if ws and len( ws) > 0 else None andict['refsample'] = analysis.getSample().id \ if analysis.portal_type == 'Analysis' \ else '%s - %s' % (analysis.aq_parent.id, analysis.aq_parent.Title()) # Which analysis specs must be used? # Try first with those defined at AR Publish Specs level 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, {}) elif analysis.portal_type == 'DuplicateAnalysis': specs = analysis.getAnalysisSpecs() else: ar = analysis.aq_parent specs = ar.getPublicationSpecification() if not specs or keyword not in specs.getResultsRangeDict(): specs = analysis.getAnalysisSpecs() specs = specs.getResultsRangeDict().get(keyword, {}) \ if specs else {} andict['specs'] = specs scinot = self.context.bika_setup.getScientificNotationReport() andict['formatted_result'] = analysis.getFormattedResult( specs=specs, sciformat=int(scinot), decimalmark=decimalmark) fs = '' if specs.get('min', None) and specs.get('max', None): fs = '%s - %s' % (specs['min'], specs['max']) elif specs.get('min', None): fs = '> %s' % specs['min'] elif specs.get('max', None): fs = '< %s' % specs['max'] andict['formatted_specs'] = formatDecimalMark(fs, decimalmark) andict['formatted_uncertainty'] = format_uncertainty( analysis, analysis.getResult(), decimalmark=decimalmark, sciformat=int(scinot)) # Return specs of current analysis andict['specs_dict'] = analysis.getSpecification().getResultsRangeDict( ).get(analysis.id) # Out of range? if specs: adapters = getAdapters((analysis, ), IResultOutOfRange) bsc = getToolByName(self.context, "bika_setup_catalog") for name, adapter in adapters: ret = adapter(specification=specs) if ret and ret['out_of_range']: andict['outofrange'] = True break return andict
def _areConditionsMet(self, action_set, analysis, forceuid=False): """ This function returns a boolean as True if the conditions in the action_set are met, and returns False otherwise. :analysis: the analysis full object which we want to obtain the rules for. :action_set: a set of rules and actions as a dictionary. {'actions': [{'act_row_idx': 0, 'action': 'setresult', 'an_result_id': 'set-4', 'analyst': 'analyst1', 'otherWS': 'current', 'setresultdiscrete': '3', 'setresulton': 'new', 'setresultvalue': '', 'worksheettemplate': ''}], 'conditions': [{'analysisservice': 'dup-2', 'and_or': 'and', 'cond_row_idx': 0, 'discreteresult': '2', 'range0': '', 'range1': ''}, {'analysisservice': 'dup-7', 'and_or': 'no', 'cond_row_idx': 1, 'discreteresult': '2', 'range0': '', 'range1': ''}], 'mother_service_uid': 'ddaa2a7538bb4d188798498d6e675abd', 'rulenumber': '1', 'trigger': 'submit'} :forceuid: a boolean used to get the analysis service's UID from the analysis even if the analysis has been reflected and has a local_id. :returns: a Boolean. """ conditions = action_set.get('conditions', []) eval_str = '' # Getting the analysis local id or its uid instead alocalid = analysis.getReflexRuleLocalID() if \ analysis.getIsReflexAnalysis() and not forceuid else analysis.getServiceUID() # Getting the local ids (or analysis service uid) from the condition # with the same local id (or analysis service uid) as the analysis # attribute localids = [cond.get('analysisservice') for cond in conditions if cond.get('analysisservice', '') == alocalid] # Now the alocalid could be the UID of the analysis service (if # this analysis has not been crated by a previous reflex rule) or it # could be a local_id if the analysis has been created by a reflex # rule. # So, if the analysis was reflexed, and no localid has been found # inside the action_set matching the analysis localid, lets look for # the analysis service UID. # forceuid is True when this second query has been done. if not localids and not forceuid and analysis.getIsReflexAnalysis(): return self._areConditionsMet(action_set, analysis, forceuid=True) # action_set will not have any action for this analysis elif not localids: return False # Getting the action_set.rulenumber in order to check the # analysis.ReflexRuleActionsTriggered rulenumber = action_set.get('rulenumber', '') # Getting the reflex rules uid in order to fill the # analysis.ReflexRuleActionsTriggered attribute later rr_uid = self.UID() # Building the possible analysis.ReflexRuleActionsTriggered rr_actions_triggered = '.'.join([rr_uid, rulenumber]) # If the follow condition is met, it means that the action_set for this # analysis has already been done by any other analysis from the # action_set. (e.g analsys.local_id =='dup-2', but this action has been # ran by the analysis with local_id=='dup-1', so we do not have to # run it again) if rr_actions_triggered in\ analysis.getReflexRuleActionsTriggered().split('|'): return False # Check that rules are not repited: lets supose that some conditions # are met and an analysis with id analysis-1 is reflexed using a # duplicate action. Now we have analysis-1 and analysis1-dup. If the # same conditions are met while submitting/verifying analysis1-dup, the # duplicated shouldn't trigger the reflex action again. if forceuid and analysis.IsReflexAnalysis and rr_actions_triggered in\ analysis.getReflexRuleActionsTriggered().split('|'): return False # To save the analysis related in the same action_set ans_related_to_set = [] for condition in conditions: # analysisservice can be either a service uid (if it is the first # condition in the reflex rule) or it could be a local id such # as 'dup-2' if the analysis_set belongs to a derivate rule. ans_cond = condition.get('analysisservice', '') ans_uid_cond = action_set.get('mother_service_uid', '') # Be aware that we already know that the local_id for 'analysis' # has been found inside the conditions for this action_set if ans_cond != alocalid: # If the 'analysisservice' item from the condition is not the # same as the local_id from the analysis, the system # should look for the possible analysis with this localid # (e.g dup-2) and get its analysis object in order to compare # the results of the 'analysis variable' with the analysis # object obtained here curranalysis = _fetch_analysis_for_local_id( analysis, ans_cond) else: # if the local_id of the 'analysis' is the same as the # local_id in the condition, we will use it as the current # analysis curranalysis = analysis if not curranalysis: continue ans_related_to_set.append(curranalysis) # the value of the analysis' result as string result = curranalysis.getResult() if len(analysis.getResultOptions()) > 0: # Discrete result as expacted value exp_val = condition.get('discreteresult', '') else: exp_val = ( condition.get('range0', ''), condition.get('range1', '') ) and_or = condition.get('and_or', '') service_uid = curranalysis.getServiceUID() # Resolve the conditions resolution = \ ans_uid_cond == service_uid and\ ((isnumber(result) and isinstance(exp_val, str) and exp_val == result) or (isnumber(result) and len(exp_val) == 2 and isnumber(exp_val[0]) and isnumber(exp_val[1]) and float(exp_val[0]) <= float(result) and float(result) <= float(exp_val[1]))) # Build a string and then use eval() if and_or == 'no': eval_str += str(resolution) else: eval_str += str(resolution) + ' ' + and_or + ' ' if eval_str and eval(eval_str): for an in ans_related_to_set: an.addReflexRuleActionsTriggered(rr_actions_triggered) return True else: return False
def _check_set_values(instance, dic): """ This function checks if the dict values are correct. :instance: the object instance. Used for querying :dic: is a dictionary with the following format: {'actions': [{'act_row_idx': 0, 'action': 'repeat', 'an_result_id': 'rep-1', 'analyst': '', 'otherWS': 'current', 'setresultdiscrete': '', 'setresulton': 'original', 'setresultvalue': '', 'worksheettemplate': ''}], 'conditions': [{'analysisservice': '52853cf7d5114b5aa8c159afad2f3da1', 'and_or': 'no', 'cond_row_idx': 0, 'discreteresult': '', 'range0': '11', 'range1': '12'}], 'mother_service_uid': '52853cf7d5114b5aa8c159afad2f3da1', 'rulenumber': '0', 'trigger': 'submit'}, These are the checking rules: :range0/range1: string or number. They are the numeric range within the action will be carried on. It is needed to keep it as None or '' if the discreteresult is going to be used instead. :discreteresult: string If discreteresult is not Null, ranges have to be Null. :trigger: string. So far there are only two options: 'submit'/'verify'. They are defined in browser/widgets/reflexrulewidget.py/ReflexRuleWidget/getTriggerVoc. :analysisservice: it is the uid of an analysis service :actions: It is a list of dictionaries with the following format: [{'action':'<action_name>', 'act_row_idx':'X', 'otherWS':Bool, 'analyst': '<analyst_id>'}, {'action':'<action_name>', 'act_row_idx':'X', 'otherWS':Bool, 'analyst': '<analyst_id>'}, ] :'repetition_max': integer or string representing an integer. <action_name> options are found in browser/widgets/reflexrulewidget.py/ReflexRuleWidget/getActionVoc so far. """ uc = getToolByName(instance, 'uid_catalog') rulenumber = dic.get('rulenumber', '0') if rulenumber and not (isnumber(rulenumber)): logger.warn('The range must be a number. Now its value is: ' '%s' % (rulenumber)) return False trigger = dic.get('trigger', 'submit') if trigger not in ['submit', 'verify']: logger.warn('Only available triggers are "verify" or "submit". ' '%s has been introduced.' % (trigger)) return False mother_service_uid = dic.get('mother_service_uid', '') as_brain = uc(UID=mother_service_uid) if not as_brain: logger.warn('Not correct analysis service with UID. %s' % (mother_service_uid)) return False # Checking the conditions conditions = dic.get('conditions', []) if not conditions or not _check_conditions(instance, conditions): return False # Checking the actions actions = dic.get('actions', []) if not actions or not _check_actions(instance, actions): return False return True
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()]