def getResultsRange(self): """Returns the valid result range for this analysis duplicate, based on both on the result and duplicate variation set in the original analysis A Duplicate will be out of range if its result does not match with the result for the parent analysis plus the duplicate variation in % as the margin error. :return: A dictionary with the keys min and max :rtype: dict """ specs = ResultsRangeDict() analysis = self.getAnalysis() if not analysis: return specs result = analysis.getResult() if not api.is_floatable(result): return specs specs.min = specs.max = result result = api.to_float(result) dup_variation = analysis.getDuplicateVariation() dup_variation = api.to_float(dup_variation) if not dup_variation: return specs margin = abs(result) * (dup_variation / 100.0) specs.min = str(result - margin) specs.max = str(result + margin) return specs
def validate_service(self, request, uid): """Validates the specs values from request for the service uid. Returns a message if the validation failed """ spec_min = get_record_value(request, uid, "min") spec_max = get_record_value(request, uid, "max") min_panic = get_record_value(request, uid, "min_panic") max_panic = get_record_value(request, uid, "max_panic") if not min_panic and not max_panic: # Neither min_panic nor max_panic values are set, dismiss return None if min_panic: if not api.is_floatable(min_panic): return _("'{}' value must be numeric or empty").format( _("Min panic")) if api.to_float(min_panic) > api.to_float(spec_min): return _("'{}' value must be below '{}' or empty").format( _("Min panic"), _("Min")) if max_panic: if not api.is_floatable(max_panic): return _("'{}' value must be numeric or empty").format( _("Max panic")) if api.to_float(max_panic) < api.to_float(spec_max): return _("'{}' value must be above '{}' or empty").format( _("Max panic"), _("Max")) return None
def validate_service(self, request, uid): """Validates the specs values from request for the service uid. Returns a non-translated message if the validation failed.""" result = get_record_value(request, uid, 'result') if not result: # No result set for this service, dismiss return None if not api.is_floatable(result): return "Expected result value must be numeric" spec_min = get_record_value(request, uid, "min", result) spec_max = get_record_value(request, uid, "max", result) error = get_record_value(request, uid, "error", "0") if not api.is_floatable(spec_min): return "'Min' value must be numeric" if not api.is_floatable(spec_max): return "'Max' value must be numeric" if api.to_float(spec_min) > api.to_float(result): return "'Min' value must be below the expected result" if api.to_float(spec_max) < api.to_float(result): return "'Max' value must be above the expected result" if not api.is_floatable(error) or 0.0 < api.to_float(error) > 100: return "% Error must be between 0 and 100" return None
def validate_service(self, request, uid): """Validates the specs values from request for the service uid. Returns a non-translated message if the validation failed. """ spec_min = get_record_value(request, uid, "min") spec_max = get_record_value(request, uid, "max") error = get_record_value(request, uid, "error", "0") warn_min = get_record_value(request, uid, "warn_min") warn_max = get_record_value(request, uid, "warn_max") if not spec_min and not spec_max: # Neither min nor max values have been set, dismiss return None if not api.is_floatable(spec_min): return "'Min' value must be numeric" if not api.is_floatable(spec_max): return "'Max' value must be numeric" if api.to_float(spec_min) > api.to_float(spec_max): return "'Max' value must be above 'Min' value" if not api.is_floatable(error) or 0.0 < api.to_float(error) > 100: return "% Error must be between 0 and 100" if warn_min: if not api.is_floatable(warn_min): return "'Warn Min' value must be numeric or empty" if api.to_float(warn_min) > api.to_float(spec_min): return "'Warn Min' value must be below 'Min' value" if warn_max: if not api.is_floatable(warn_max): return "'Warn Max' value must be numeric or empty" if api.to_float(warn_max) < api.to_float(spec_max): return "'Warn Max' value must be above 'Max' value" return None
def format_price(self, price): """Formats the price with the set decimal mark and currency """ # ensure we have a float price = api.to_float(price, default=0.0) dm = self.get_decimal_mark() cur = self.get_currency_symbol() price = "%s %.2f" % (cur, price) return price.replace(".", dm)
def getResultsRange(self): """Returns the valid result range for this analysis duplicate, based on both on the result and duplicate variation set in the original analysis A Duplicate will be out of range if its result does not match with the result for the parent analysis plus the duplicate variation in % as the margin error. If the duplicate is from an analysis with result options and/or string results enabled (with non-numeric value), returns an empty result range :return: A dictionary with the keys min and max :rtype: dict """ # Get the original analysis original_analysis = self.getAnalysis() if not original_analysis: logger.warn("Orphan duplicate: {}".format(repr(self))) return {} # Return empty if results option enabled (exact match expected) if original_analysis.getResultOptions(): return {} # Return empty if non-floatable (exact match expected) original_result = original_analysis.getResult() if not api.is_floatable(original_result): return {} # Calculate the min/max based on duplicate variation % specs = ResultsRangeDict(uid=self.getServiceUID()) dup_variation = original_analysis.getDuplicateVariation() dup_variation = api.to_float(dup_variation, default=0) if not dup_variation: # We expect an exact match specs.min = specs.max = original_result return specs original_result = api.to_float(original_result) margin = abs(original_result) * (dup_variation / 100.0) specs.min = str(original_result - margin) specs.max = str(original_result + margin) return specs
def get_listing_view_adapters(self): """Returns subscriber adapters used to modify the listing behavior, sorted from higher to lower priority """ # Allows to override this listing by multiple subscribers without the # need of inheritance. We use subscriber adapters here because we need # different add-ons to be able to modify columns, etc. without # dependencies amongst them. adapters = subscribers((self, self.context), IListingViewAdapter) return sorted(adapters, key=lambda ad: api.to_float( getattr(ad, "priority_order", 1000)))
def remove_error_subfield_from_analysis_specs(portal, ut): # Update Analysis Specifications catalog = api.get_tool('bika_setup_catalog') brains = catalog(portal_type='AnalysisSpec') for brain in brains: specs = api.get_object(brain) specs_rr = specs.getResultsRange() for spec in specs_rr: min = spec.get('min', '') max = spec.get('max', '') error = api.to_float(spec.get('error', '0'), 0) if api.is_floatable(min) and api.is_floatable(max) and error > 0: # Add min_warn and max_warn fields min = api.to_float(min) max = api.to_float(max) warn_min = min - (abs(min) * (error/100.0)) warn_max = max + (abs(max) * (error/100.0)) spec['warn_min'] = str(warn_min) spec['warn_max'] = str(warn_max) del spec['error'] specs.setResultsRange(specs_rr)
def add_analysis(self, analysis): """Adds an analysis to be consumed by the Analyses Chart machinery (js) :param analysis_object: analysis to be rendered in the chart """ analysis_object = api.get_object(analysis) result = analysis_object.getResult() results_range = analysis_object.getResultsRange() range_result = results_range.get('result', None) range_min = results_range.get('min', None) range_max = results_range.get('max', None) # All them must be floatable for value in [result, range_result, range_min, range_max]: if not api.is_floatable(value): return cap_date = analysis_object.getResultCaptureDate() cap_date = api.is_date(cap_date) and \ cap_date.strftime('%Y-%m-%d %I:%M %p') or '' if not cap_date: return # Create json ref_sample_id = analysis_object.getSample().getId() as_keyword = analysis_object.getKeyword() as_name = analysis_object.Title() as_ref = '{} ({})'.format(as_name, as_keyword) as_rows = self.analyses_dict.get(as_ref, {}) an_rows = as_rows.get(ref_sample_id, []) an_rows.append({ 'date': cap_date, 'target': api.to_float(range_result), 'upper': api.to_float(range_max), 'lower': api.to_float(range_min), 'result': api.to_float(result), 'unit': analysis_object.getUnit(), 'id': api.get_uid(analysis_object) }) as_rows[ref_sample_id] = an_rows self.analyses_dict[as_ref] = as_rows
def get_result(self, column_name, result, line): result = str(result) if result.startswith('--') or result == '' or result == 'ND': return 0.0 if api.is_floatable(result): result = api.to_float(result) return result > 0.0 and result or 0.0 self.err("No valid number ${result} in column (${column_name})", mapping={"result": result, "column_name": column_name}, numline=self._numline, line=line) return
def validate_service(self, request, uid): """Validates the specs values from request for the service uid. Returns a non-translated message if the validation failed. """ logger.info("Validating......") message = BaseValidator.validate_service(self, request, uid) if message: # Somehow, failed a validation for one or more of the default # range fields (min, max, error, warn_min, warn_max) return message spec_min = get_record_value(request, uid, "min") spec_max = get_record_value(request, uid, "max") min_panic = get_record_value(request, uid, "minpanic") max_panic = get_record_value(request, uid, "maxpanic") # minpanic must be below min and below maxpanic if not min_panic and not max_panic: # Neither min_panic nor max_panic values are set, dismiss return None if min_panic: if not api.is_floatable(min_panic): return _b("'{}' value must be numeric or empty").format( _("Min panic")) if api.to_float(min_panic) > api.to_float(spec_min): return _b("'{}' value must be below '{}").format( _("Min panic"), _("Min")) if max_panic: if not api.is_floatable(min_panic): return _b("'{}' value must be numeric or empty").format( _("Max panic")) if api.to_float(min_panic) > api.to_float(spec_max): return _b("'{}' value must be above '{}").format( _("Max panic"), _("Max")) return None
def isAboveUpperDetectionLimit(self): """Returns True if the result is above the Upper Detection Limit or if Upper Detection Limit has been manually set """ if self.isUpperDetectionLimit(): return True result = self.getResult() if result and str(result).strip().startswith(UDL): return True if api.is_floatable(result): return api.to_float(result) > self.getUpperDetectionLimit() return False
def isBelowLowerDetectionLimit(self): """Returns True if the result is below the Lower Detection Limit or if Lower Detection Limit has been manually set """ if self.isLowerDetectionLimit(): return True result = self.getResult() if result and str(result).strip().startswith(LDL): return True if api.is_floatable(result): return api.to_float(result) < self.getLowerDetectionLimit() return False
def __init__(self, name, request, context, *arg, **kw): super(QueueTask, self).__init__(*arg, **kw) if api.is_uid(context): context_uid = context context_path = kw.get("context_path") if not context_path: raise ValueError("context_path is missing") elif api.is_object(context): context_uid = api.get_uid(context) context_path = api.get_path(context) else: raise TypeError("No valid context object") # Set defaults kw = kw or {} task_uid = str(kw.get("task_uid", tmpID())) uids = map(str, kw.get("uids", [])) created = api.to_float(kw.get("created"), default=time.time()) status = kw.get("status", None) min_sec = api.to_int(kw.get("min_seconds"), default=get_min_seconds()) max_sec = api.to_int(kw.get("max_seconds"), default=get_max_seconds()) priority = api.to_int(kw.get("priority"), default=10) retries = api.to_int(kw.get("retries"), default=get_max_retries()) unique = self._is_true(kw.get("unique", False)) chunks = api.to_int(kw.get("chunk_size"), default=get_chunk_size(name)) username = kw.get("username", self._get_authenticated_user(request)) err_message = kw.get("error_message", None) self.update({ "task_uid": task_uid, "name": name, "context_uid": context_uid, "context_path": context_path, "uids": uids, "created": created, "status": status and str(status) or None, "error_message": err_message and str(err_message) or None, "min_seconds": min_sec, "max_seconds": max_sec, "priority": priority, "retries": retries, "unique": unique, "chunk_size": chunks, "username": str(username), })
def __call__(self, value, *args, **kwargs): # Result Value must be floatable if not api.is_floatable(value): return _t(_("Result Value must be a number")) # Get all records instance = kwargs['instance'] field_name = kwargs['field'].getName() request = instance.REQUEST records = request.form.get(field_name) # Result values must be unique value = api.to_float(value) values = map(lambda ro: ro.get("ResultValue"), records) values = filter(api.is_floatable, values) values = map(api.to_float, values) duplicates = filter(lambda val: val == value, values) if len(duplicates) > 1: return _t(_("Result Value must be unique")) return True
def extractCredentials(self, request): # noqa camelCase """IExtractionPlugin implementation. Extracts login name from the request's "X-Queue-Auth-Token" header. This header contains an encrypted token with it's expiration date, together with the user name. Returns a dict with {'login': <username>} if: - current layer is ISenaiteQueueLayer, - the token can be decrypted, - the decrypted token contains both expiration and username and - the the token has not expired (expiration date) Returns an empty dict otherwise :param request: the HTTPRequest object to extract credentials from :return: a dict {"login": <username>} or empty dict """ # Check if request provides ISenaiteQueueLayer if not ISenaiteQueueLayer.providedBy(request): return {} # Read the magical header that contains the encrypted info auth_token = request.getHeader("X-Queue-Auth-Token") if not auth_token: return {} # Decrypt the auth_token key = api.get_registry_record("senaite.queue.auth_key") token = Fernet(str(key)).decrypt(auth_token) # Check if token is valid tokens = token.split(":") if len(tokens) < 2 or not api.is_floatable(tokens[0]): return {} # Check if token has expired expiration = api.to_float(tokens[0]) if expiration < time.time(): return {} user_id = "".join(tokens[1:]) return {"login": user_id}
def is_out_of_range(brain_or_object, result=_marker): """Checks if the result for the analysis passed in is out of range and/or out of shoulders range. min max warn min max warn ·········|---------------|=====================|---------------|········· ----- out-of-range -----><----- in-range ------><----- out-of-range ----- <-- shoulder --><----- in-range ------><-- shoulder --> :param brain_or_object: A single catalog brain or content object :param result: Tentative result. If None, use the analysis result :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain :returns: Tuple of two elements. The first value is `True` if the result is out of range and `False` if it is in range. The second value is `True` if the result is out of shoulder range and `False` if it is in shoulder range :rtype: (bool, bool) """ analysis = api.get_object(brain_or_object) if not IAnalysis.providedBy(analysis) and \ not IReferenceAnalysis.providedBy(analysis): api.fail("{} is not supported. Needs to be IAnalysis or " "IReferenceAnalysis".format(repr(analysis))) if result is _marker: result = api.safe_getattr(analysis, "getResult", None) if result in [None, '']: # Empty result return False, False if IDuplicateAnalysis.providedBy(analysis): # Result range for duplicate analyses is calculated from the original # result, applying a variation % in shoulders. If the analysis has # result options enabled or string results enabled, system returns an # empty result range for the duplicate: result must match %100 with the # original result original = analysis.getAnalysis() original_result = original.getResult() # Does original analysis have a valid result? if original_result in [None, '']: return False, False # Does original result type matches with duplicate result type? if api.is_floatable(result) != api.is_floatable(original_result): return True, True # Does analysis has result options enabled or non-floatable? if analysis.getResultOptions() or not api.is_floatable(original_result): # Let's always assume the result is 'out from shoulders', cause we # consider the shoulders are precisely the duplicate variation % out_of_range = original_result != result return out_of_range, out_of_range elif not api.is_floatable(result): # A non-duplicate with non-floatable result. There is no chance to know # if the result is out-of-range return False, False # Convert result to a float result = api.to_float(result) # Note that routine analyses, duplicates and reference analyses all them # implement the function getResultRange: # - For routine analyses, the function returns the valid range based on the # specs assigned during the creation process. # - For duplicates, the valid range is the result of the analysis the # the duplicate was generated from +/- the duplicate variation. # - For reference analyses, getResultRange returns the valid range as # indicated in the Reference Sample from which the analysis was created. result_range = api.safe_getattr(analysis, "getResultsRange", None) if not result_range: # No result range defined or the passed in object does not suit return False, False # Maybe there is a custom adapter adapters = getAdapters((analysis,), IResultOutOfRange) for name, adapter in adapters: ret = adapter(result=result, specification=result_range) if not ret or not ret.get('out_of_range', False): continue if not ret.get('acceptable', True): # Out of range + out of shoulders return True, True # Out of range, but in shoulders return True, False result_range = ResultsRangeDict(result_range) # The assignment of result as default fallback for min and max guarantees # the result will be in range also if no min/max values are defined specs_min = api.to_float(result_range.min, result) specs_max = api.to_float(result_range.max, result) in_range = False min_operator = result_range.min_operator if min_operator == "geq": in_range = result >= specs_min else: in_range = result > specs_min max_operator = result_range.max_operator if in_range: if max_operator == "leq": in_range = result <= specs_max else: in_range = result < specs_max # If in range, no need to check shoulders if in_range: return False, False # Out of range, check shoulders. If no explicit warn_min or warn_max have # been defined, no shoulders must be considered for this analysis. Thus, use # specs' min and max as default fallback values warn_min = api.to_float(result_range.warn_min, specs_min) warn_max = api.to_float(result_range.warn_max, specs_max) in_shoulder = warn_min <= result <= warn_max return True, not in_shoulder
def to_float_or_none(value): """Returns the float if the value is floatable. Otherwise, returns None """ if api.is_floatable(value): return api.to_float(value) return None
def is_out_of_range(brain_or_object, result=_marker): """Checks if the result for the analysis passed in is out of range and/or out of shoulders range. min max warn min max warn ·········|---------------|=====================|---------------|········· ----- out-of-range -----><----- in-range ------><----- out-of-range ----- <-- shoulder --><----- in-range ------><-- shoulder --> :param brain_or_object: A single catalog brain or content object :param result: Tentative result. If None, use the analysis result :type brain_or_object: ATContentType/DexterityContentType/CatalogBrain :returns: Tuple of two elements. The first value is `True` if the result is out of range and `False` if it is in range. The second value is `True` if the result is out of shoulder range and `False` if it is in shoulder range :rtype: (bool, bool) """ analysis = api.get_object(brain_or_object) if not IAnalysis.providedBy(analysis) and \ not IReferenceAnalysis.providedBy(analysis): api.fail("{} is not supported. Needs to be IAnalysis or " "IReferenceAnalysis".format(repr(analysis))) if result is _marker: result = api.safe_getattr(analysis, "getResult", None) if not api.is_floatable(result): # Result is empty/None or not a valid number return False, False result = api.to_float(result) # Note that routine analyses, duplicates and reference analyses all them # implement the function getResultRange: # - For routine analyses, the function returns the valid range based on the # specs assigned during the creation process. # - For duplicates, the valid range is the result of the analysis the # the duplicate was generated from +/- the duplicate variation. # - For reference analyses, getResultRange returns the valid range as # indicated in the Reference Sample from which the analysis was created. result_range = api.safe_getattr(analysis, "getResultsRange", None) if not result_range: # No result range defined or the passed in object does not suit return False, False # Maybe there is a custom adapter adapters = getAdapters((analysis, ), IResultOutOfRange) for name, adapter in adapters: ret = adapter(result=result, specification=result_range) if not ret or not ret.get('out_of_range', False): continue if not ret.get('acceptable', True): # Out of range + out of shoulders return True, True # Out of range, but in shoulders return True, False # The assignment of result as default fallback for min and max guarantees # the result will be in range also if no min/max values are defined specs_min = api.to_float(result_range.get('min', result), result) specs_max = api.to_float(result_range.get('max', result), result) if specs_min <= result <= specs_max: # In range, no need to check shoulders return False, False # Out of range, check shoulders. If no explicit warn_min or warn_max have # been defined, no shoulders must be considered for this analysis. Thus, use # specs' min and max as default fallback values warn_min = api.to_float(result_range.get('warn_min', specs_min), specs_min) warn_max = api.to_float(result_range.get('warn_max', specs_max), specs_max) in_shoulder = warn_min <= result <= warn_max return True, not in_shoulder