def before_transition_handler(instance, event): """Always update the modification date before a WF transition. This ensures that all cache keys are invalidated. """ parent = api.get_parent(instance) if api.get_portal_type(instance) == "AnalysisRequest": update_ar_modification_dates(instance) elif api.get_portal_type(parent) == "AnalysisRequest": update_ar_modification_dates(parent) else: update_modification_date(instance)
def metadata_to_searchable_text(self, brain, key, value): """Parse the given metadata to text :param brain: ZCatalog Brain :param key: The name of the metadata column :param value: The raw value of the metadata column :returns: Searchable and translated unicode value or None """ if not value: return u"" if value is Missing.Value: return u"" if api.is_uid(value): return u"" if isinstance(value, (bool)): return u"" if isinstance(value, (list, tuple)): for v in value: return self.metadata_to_searchable_text(brain, key, v) if isinstance(value, (dict)): for k, v in value.items(): return self.metadata_to_searchable_text(brain, k, v) if self.is_date(value): return self.to_str_date(value) if "state" in key.lower(): return self.translate_review_state(value, api.get_portal_type(brain)) if not isinstance(value, basestring): value = str(value) return safe_unicode(value)
def get_generated_number(context, config, variables, **kw): """Generate a new persistent number with the number generator for the sequence type "Generated" """ # separator where to split the ID separator = kw.get('separator', '-') # allow portal_type override portal_type = kw.get("portal_type") or api.get_portal_type(context) # The ID format for string interpolation, e.g. WS-{seq:03d} id_template = config.get("form", "") # The split length defines where the variable part of the ID template begins split_length = config.get("split_length", 1) # The prefix tempalte is the static part of the ID prefix_template = slice(id_template, separator=separator, end=split_length) # get the number generator number_generator = getUtility(INumberGenerator) # generate the key for the number generator storage prefix = prefix_template.format(**variables) # normalize out any unicode characters like Ö, É, etc. from the prefix prefix = api.normalize_filename(prefix) # The key used for the storage key = make_storage_key(portal_type, prefix) # Handle flushed storage if key not in number_generator: max_num = 0 existing = get_ids_with_prefix(portal_type, prefix) numbers = map( lambda id: get_seq_number_from_id(id, id_template, prefix), existing) # figure out the highest number in the sequence if numbers: max_num = max(numbers) # set the number generator logger.info("*** SEEDING Prefix '{}' to {}".format(prefix, max_num)) number_generator.set_number(key, max_num) if not kw.get("dry_run", False): # Generate a new number # NOTE Even when the number exceeds the given ID sequence format, # it will overflow gracefully, e.g. # >>> {sampleId}-R{seq:03d}'.format(sampleId="Water", seq=999999) # 'Water-R999999‘ number = number_generator.generate_number(key=key) else: # => This allows us to "preview" the next generated ID in the UI # TODO Show the user the next generated number somewhere in the UI number = number_generator.get(key, 1) # Return an int or Alphanumber return get_alpha_or_number(number, id_template)
def get_default_attributes(self, obj): """Returns the default attributes from the object to keep mapped against the SQL database """ portal_type = api.get_portal_type(obj) if portal_type not in self.get_multiplexed_types(): return [] def is_field_supported(field): if field.getName() in NON_SUPPORTED_FIELD_NAMES: return False if field.type in NON_SUPPORTED_FIELD_TYPES: return False return True # Grab only attributes that make sense! fields = api.get_fields(obj).values() fields = filter(is_field_supported, fields) # Get the ids of the fields to store attrs = map(lambda f: f.getName(), fields) if not attrs: return [] # Always keep the UID if "UID" not in attrs: attrs.append("UID") return attrs
def _to_service(self, thing): """Convert to Analysis Service :param thing: UID/Catalog Brain/Object/Something :returns: Analysis Service object or None """ # Convert UIDs to objects if api.is_uid(thing): thing = api.get_object_by_uid(thing, None) # Bail out if the thing is not a valid object if not api.is_object(thing): logger.warn("'{}' is not a valid object!".format(repr(thing))) return None # Ensure we have an object here and not a brain obj = api.get_object(thing) if IAnalysisService.providedBy(obj): return obj if IAnalysis.providedBy(obj): return obj.getAnalysisService() # An object, but neither an Analysis nor AnalysisService? # This should never happen. msg = "ARAnalysesField doesn't accept objects from {} type. " \ "The object will be dismissed.".format(api.get_portal_type(obj)) logger.warn(msg) return None
def _to_service(self, thing): """Convert to Analysis Service :param thing: UID/Catalog Brain/Object/Something :returns: Analysis Service object or None """ # Convert UIDs to objects if api.is_uid(thing): thing = api.get_object_by_uid(thing, None) # Bail out if the thing is not a valid object if not api.is_object(thing): logger.warn("'{}' is not a valid object!".format(repr(thing))) return None # Ensure we have an object here and not a brain obj = api.get_object(thing) if IAnalysisService.providedBy(obj): return obj if IAnalysis.providedBy(obj): return obj.getAnalysisService() # An object, but neither an Analysis nor AnalysisService? # This should never happen. portal_type = api.get_portal_type(obj) logger.error("ARAnalysesField doesn't accept objects from {} type. " "The object will be dismissed.".format(portal_type)) return None
def get_item_info(self, brain_or_object): """Return the data of this brain or object """ portal_type = api.get_portal_type(brain_or_object) state = api.get_workflow_status_of(brain_or_object) return { "obj": brain_or_object, "uid": api.get_uid(brain_or_object), "url": api.get_url(brain_or_object), "id": api.get_id(brain_or_object), "title": api.get_title(brain_or_object), "portal_type": api.get_portal_type(brain_or_object), "review_state": state, "state_title": self.translate_review_state(state, portal_type), "state_class": "state-{}".format(state), }
def listing_identifier(self): """Identifier for similar listings This identifier is used as the local storage key for custom column configuration. Also see this issue for more details: https://github.com/senaite/senaite.core.listing/issues/16 """ key = None view_name = self.__name__ portal_type = self.contentFilter.get("portal_type", None) # Handle the global Samples listing different, because the columns do # not match with the listings in Clients if api.get_portal_type(self.context) == "AnalysisRequestsFolder": key = "AnalysisRequestsListing" elif isinstance(portal_type, six.string_types): key = portal_type elif self.catalog == CATALOG_ANALYSIS_REQUEST_LISTING: key = "AnalysisRequest" elif self.catalog == CATALOG_ANALYSIS_LISTING: key = "Analysis" elif self.catalog == CATALOG_WORKSHEET_LISTING: key = "Worksheet" elif self.catalog == CATALOG_AUDITLOG: key = "Auditlog" else: return view_name return "-".join([key, view_name])
def can_export(obj): """Decides if the object can be exported or not """ if not api.is_object(obj): return False if api.get_portal_type(obj) in SKIP_TYPES: return False return True
def get_portal_type_title(self, obj): """Returns the title of the portal type of the obj passed-in """ portal = api.get_portal() portal_type = api.get_portal_type(obj) portal_type = portal.portal_types.getTypeInfo(portal_type) if portal_type: return portal_type.title return None
def get_generated_number(context, config, variables, **kw): """Generate a new persistent number with the number generator for the sequence type "Generated" """ # separator where to split the ID separator = kw.get('separator', '-') # allow portal_type override portal_type = kw.get("portal_type") or api.get_portal_type(context) # The ID format for string interpolation, e.g. WS-{seq:03d} id_template = config.get("form", "") # The split length defines where the variable part of the ID template begins split_length = config.get("split_length", 1) # The prefix tempalte is the static part of the ID prefix_template = slice(id_template, separator=separator, end=split_length) # get the number generator number_generator = getUtility(INumberGenerator) # generate the key for the number generator storage prefix = prefix_template.format(**variables) # normalize out any unicode characters like Ö, É, etc. from the prefix prefix = api.normalize_filename(prefix) # The key used for the storage key = make_storage_key(portal_type, prefix) # Handle flushed storage if key not in number_generator: max_num = 0 existing = get_ids_with_prefix(portal_type, prefix) numbers = map(lambda id: get_seq_number_from_id(id, id_template, prefix), existing) # figure out the highest number in the sequence if numbers: max_num = max(numbers) # set the number generator logger.info("*** SEEDING Prefix '{}' to {}".format(prefix, max_num)) number_generator.set_number(key, max_num) if not kw.get("dry_run", False): # Generate a new number # NOTE Even when the number exceeds the given ID sequence format, # it will overflow gracefully, e.g. # >>> {sampleId}-R{seq:03d}'.format(sampleId="Water", seq=999999) # 'Water-R999999‘ number = number_generator.generate_number(key=key) else: # => This allows us to "preview" the next generated ID in the UI # TODO Show the user the next generated number somewhere in the UI number = number_generator.get(key, 1) return number
def to_link(obj, key, value, **kw): """to link """ value = to_string(obj, key, value) if not value: return "" if api.get_portal_type(obj) in LINK_TO_PARENT_TYPES: obj = api.get_parent(obj) url = addTokenToUrl(api.get_url(obj)) return get_link(url, value)
def unindex(self, obj): if not self.supports_multiplex(obj): return # Delete the object from the SQL db uid = api.get_uid(obj) portal_type = api.get_portal_type(obj) operation = "DELETE FROM {} WHERE UID='{}'".format(portal_type, uid) self.execute(operation, raise_error=False) # Do something here logger.info("Multiplexer::Unindexing {}".format(repr(obj)))
def get_item_info(self, brain_or_object): """Return the data of this brain or object """ return { "obj": brain_or_object, "uid": api.get_uid(brain_or_object), "url": api.get_url(brain_or_object), "id": api.get_id(brain_or_object), "title": api.get_title(brain_or_object), "portal_type": api.get_portal_type(brain_or_object), "review_state": api.get_workflow_status_of(brain_or_object), }
def init_auditlog(portal): """Initialize the contents for the audit log """ # reindex the auditlog folder to display the icon right in the setup portal.bika_setup.auditlog.reindexObject() # Initialize contents for audit logging start = time.time() uid_catalog = api.get_tool("uid_catalog") brains = uid_catalog() total = len(brains) logger.info("Initializing {} objects for the audit trail...".format(total)) for num, brain in enumerate(brains): # Progress notification if num and num % 1000 == 0: transaction.commit() logger.info("{}/{} ojects initialized for audit logging".format( num, total)) # End progress notification if num + 1 == total: end = time.time() duration = float(end - start) logger.info( "{} ojects initialized for audit logging in {:.2f}s".format( total, duration)) if api.get_portal_type(brain) in SKIP_TYPES_FOR_AUDIT_LOG: continue obj = api.get_object(brain) if not supports_snapshots(obj): continue if has_snapshots(obj): continue # Take one snapshot per review history item rh = api.get_review_history(obj, rev=False) for item in rh: actor = item.get("actor") user = get_user(actor) if user: # remember the roles of the actor item["roles"] = get_roles(user) # The review history contains the variable "time" which we will set # as the "modification" time timestamp = item.pop("time", DateTime()) item["time"] = timestamp.ISO() item["modified"] = timestamp.ISO() item["remote_address"] = None take_snapshot(obj, **item)
def get_table_create(self, obj, attributes): portal_type = api.get_portal_type(obj) def to_column(attribute): # TODO We just always assume a varchar type here! return "`{}` varchar(191) NULL".format(attribute) # Build the create table operation base = "CREATE TABLE `{}` ({}, PRIMARY KEY (`UID`)) ENGINE=InnoDB " \ "DEFAULT CHARSET=utf8mb4" cols = ", ".join(map(to_column, attributes)) operation = base.format(portal_type, cols) logger.info(operation) return operation
def get_type_id(context, **kw): """Returns the type id for the context passed in """ portal_type = kw.get("portal_type", None) if portal_type: return portal_type # Override by provided marker interface if IAnalysisRequestPartition.providedBy(context): return "AnalysisRequestPartition" elif IAnalysisRequestRetest.providedBy(context): return "AnalysisRequestRetest" return api.get_portal_type(context)
def get_fields(self, portal_type=None): """Returns all schema fields of the selected query type IMPORTANT: Do not call from within `__init__` due to permissions """ obj = self._create_temporary_object(portal_type=portal_type) if obj is None: return [] fields = api.get_fields(obj) # drop ignored fields for field in IGNORE_FIELDS: fields.pop(field, None) # Inject Parent Field portal_type = api.get_portal_type(obj) parent_type = PARENT_TYPES.get(portal_type) if parent_type: field = ParentField(portal_type=parent_type) fields["Parent"] = field return fields
def to_super_model(obj): # avoid circular imports from senaite.core.supermodel import SuperModel # Object is already a SuperModel, return immediately if isinstance(obj, SuperModel): return obj # Only portal objects are supported if not api.is_object(obj): return None # Wrap the object into a specific Publication Object Adapter uid = api.get_uid(obj) portal_type = api.get_portal_type(obj) adapter = queryAdapter(uid, ISuperModel, name=portal_type) if adapter is None: return SuperModel(uid) return adapter
def get_insert_update(self, obj, attributes): """Returns a tuple of two values: SQL insert-update operation, and operation parameters values. The name of the SQL table is the portal type. Attributes represent both obj functions/attributes and SQL columns :param obj: the object to insert :param attributes: attributes/columns from the object to be inserted """ record = self.get_info(obj, attributes) portal_type = api.get_portal_type(obj) # Build the base insert/update thingy attrs = map(lambda a: "`{}`".format(a), attributes) update_values = map(lambda v: "{}=VALUES({})".format(v, v), attrs) insert = "INSERT INTO {} ({}) VALUES ({}) ON DUPLICATE KEY UPDATE {}"\ .format(portal_type, ", ".join(attrs), ", ".join(["%s"]*len(attrs)), ", ".join(update_values)) # Get the values for the columns data = map(lambda column: record.get(column) or "", attributes) return insert, data
def get_config(context, **kw): """Fetch the config dict from the Bika Setup for the given portal_type """ # get the ID formatting config config_map = api.get_bika_setup().getIDFormatting() # allow portal_type override portal_type = kw.get("portal_type") or api.get_portal_type(context) # check if we have a config for the given portal_type for config in config_map: if config['portal_type'].lower() == portal_type.lower(): return config # return a default config default_config = { 'form': '%s-{seq}' % portal_type.lower(), 'sequence_type': 'generated', 'prefix': '%s' % portal_type.lower(), } return default_config
def get_config(context, **kw): """Fetch the config dict from the Bika Setup for the given portal_type """ # get the ID formatting config config_map = api.get_bika_setup().getIDFormatting() # allow portal_type override portal_type = kw.get("portal_type") or api.get_portal_type(context) # check if we have a config for the given portal_type for config in config_map: if config['portal_type'].lower() == portal_type.lower(): return config # return a default config default_config = { 'form': '%s-{seq}' % portal_type.lower(), 'sequence_type': 'generated', 'prefix': '%s' % portal_type.lower(), } return default_config
def _get_service_uid(self, item): if api.is_uid(item): return item if not api.is_object(item): logger.warn("Not an UID: {}".format(item)) return None obj = api.get_object(item) if IAnalysisService.providedBy(obj): return api.get_uid(obj) if IAnalysis.providedBy(obj) and IRequestAnalysis.providedBy(obj): return obj.getServiceUID() # An object, but neither an Analysis nor AnalysisService? # This should never happen. msg = "ARAnalysesField doesn't accept objects from {} type. " \ "The object will be dismissed." logger.warn(msg.format(api.get_portal_type(obj))) return None
def get_icon_for(self, brain_or_object): """Get the navigation portlet icon for the brain or object The cache key ensures that the lookup is done only once per domain name """ portal_types = api.get_tool("portal_types") fti = portal_types.getTypeInfo(api.get_portal_type(brain_or_object)) icon = fti.getIcon() if not icon: return "" # Always try to get the big icon for high-res displays icon_big = icon.replace(".png", "_big.png") # fall back to a default icon if the looked up icon does not exist if self.context.restrictedTraverse(icon_big, None) is None: icon_big = None portal_url = api.get_url(api.get_portal()) title = api.get_title(brain_or_object) html_tag = "<img title='{}' src='{}/{}' width='16' />".format( title, portal_url, icon_big or icon) logger.debug("Generated Icon Tag for {}: {}".format( api.get_path(brain_or_object), html_tag)) return html_tag
def make_catalog_query(self, context, field, value): """Create a catalog query for the field """ # get the catalogs for the context catalogs = api.get_catalogs_for(context) # context not in any catalog? if not catalogs: logger.warn("UniqueFieldValidator: Context '{}' is not assigned" "to any catalog!".format(repr(context))) return None # take the first catalog catalog = catalogs[0] # Check if the field accessor is indexed field_index = field.getName() accessor = field.getAccessor(context) if accessor: field_index = accessor.__name__ # return if the field is not indexed if field_index not in catalog.indexes(): return None # build a catalog query query = { "portal_type": api.get_portal_type(context), "path": { "query": api.get_parent_path(context), "depth": 1, } } query[field_index] = value logger.info("UniqueFieldValidator:Query={}".format(query)) return query
def make_catalog_query(self, context, field, value): """Create a catalog query for the field """ # get the catalogs for the context catalogs = api.get_catalogs_for(context) # context not in any catalog? if not catalogs: logger.warn("UniqueFieldValidator: Context '{}' is not assigned" "to any catalog!".format(repr(context))) return None # take the first catalog catalog = catalogs[0] # Check if the field accessor is indexed field_index = field.getName() accessor = field.getAccessor(context) if accessor: field_index = accessor.__name__ # return if the field is not indexed if field_index not in catalog.indexes(): return None # build a catalog query query = { "portal_type": api.get_portal_type(context), "path": { "query": api.get_parent_path(context), "depth": 1, } } query[field_index] = value logger.info("UniqueFieldValidator:Query={}".format(query)) return query
def get_portal_type(brain_or_object): """Proxy to senaite.api.get_portal_type """ return api.get_portal_type(brain_or_object)
def get_portal_type(brain_or_object): """Proxy to bika.lims.api.get_portal_type """ return api.get_portal_type(brain_or_object)
def get_variables(context, **kw): """Prepares a dictionary of key->value pairs usable for ID formatting """ # allow portal_type override portal_type = kw.get("portal_type") or api.get_portal_type(context) # The variables map hold the values that might get into the construced id variables = { 'context': context, 'id': api.get_id(context), 'portal_type': portal_type, 'year': get_current_year(), 'parent': api.get_parent(context), 'seq': 0, } # Augment the variables map depending on the portal type if portal_type == "AnalysisRequest": variables.update({ 'sampleId': context.getSample().getId(), 'sample': context.getSample(), }) elif portal_type == "SamplePartition": variables.update({ 'sampleId': context.aq_parent.getId(), 'sample': context.aq_parent, }) elif portal_type == "Sample": # get the prefix of the assigned sample type sample_id = context.getId() sample_type = context.getSampleType() sampletype_prefix = sample_type.getPrefix() date_now = DateTime() sampling_date = context.getSamplingDate() date_sampled = context.getDateSampled() # Try to get the date sampled and sampling date if sampling_date: samplingDate = DT2dt(sampling_date) else: # No Sample Date? logger.error("Sample {} has no sample date set".format(sample_id)) # fall back to current date samplingDate = DT2dt(date_now) if date_sampled: dateSampled = DT2dt(date_sampled) else: # No Sample Date? logger.error("Sample {} has no sample date set".format(sample_id)) dateSampled = DT2dt(date_now) variables.update({ 'clientId': context.aq_parent.getClientID(), 'dateSampled': dateSampled, 'samplingDate': samplingDate, 'sampleType': sampletype_prefix, }) return variables
def supports_multiplex(self, obj): """Returns whether the obj supports multiplex """ return api.get_portal_type(obj) in self.get_multiplexed_types()
def get_portal_type(self, obj): """Returns the portal type of the object """ if not api.is_object(obj): return None return api.get_portal_type(obj)
def get_portal_type(brain_or_object): """Proxy to bika.lims.api.get_portal_type """ return api.get_portal_type(brain_or_object)
def get_parent_objects(self, context): """Return all objects of the same type from the parent object """ parent_object = api.get_parent(context) portal_type = api.get_portal_type(context) return parent_object.objectValues(portal_type)
def get_variables(context, **kw): """Prepares a dictionary of key->value pairs usable for ID formatting """ # allow portal_type override portal_type = kw.get("portal_type") or api.get_portal_type(context) # The variables map hold the values that might get into the constructed id variables = { 'context': context, 'id': api.get_id(context), 'portal_type': portal_type, 'year': get_current_year(), 'parent': api.get_parent(context), 'seq': 0, } # Augment the variables map depending on the portal type if portal_type == "AnalysisRequest": variables.update({ 'sampleId': context.getSample().getId(), 'sample': context.getSample(), }) elif portal_type == "SamplePartition": variables.update({ 'sampleId': context.aq_parent.getId(), 'sample': context.aq_parent, }) elif portal_type == "Sample": # get the prefix of the assigned sample type sample_id = context.getId() sample_type = context.getSampleType() sampletype_prefix = sample_type.getPrefix() date_now = DateTime() sampling_date = context.getSamplingDate() date_sampled = context.getDateSampled() # Try to get the date sampled and sampling date if sampling_date: samplingDate = DT2dt(sampling_date) else: # No Sample Date? logger.error("Sample {} has no sample date set".format(sample_id)) # fall back to current date samplingDate = DT2dt(date_now) if date_sampled: dateSampled = DT2dt(date_sampled) else: # No Sample Date? logger.error("Sample {} has no sample date set".format(sample_id)) dateSampled = DT2dt(date_now) variables.update({ 'clientId': context.aq_parent.getClientID(), 'dateSampled': dateSampled, 'samplingDate': samplingDate, 'sampleType': sampletype_prefix, }) return variables
def get_parent_objects(self, context): """Return all objects of the same type from the parent object """ parent_object = api.get_parent(context) portal_type = api.get_portal_type(context) return parent_object.objectValues(portal_type)
def get_raw_query(self): """Returns the raw query to use for current search, based on the base query + update query """ query = super(ClientAwareReferenceWidgetAdapter, self).get_raw_query() logger.info("===============================================") logger.info("Custom client-aware reference widget vocabulary") logger.info(repr(query)) # Get the portal types from the query portal_type = self.get_portal_type(query) if not portal_type: # Don't know the type we are searching for, do nothing return query if portal_type not in self.client_aware_types: # The portal type is not client aware, do nothing return query # Try to resolve the client from the query client = self.get_client_from_query(query, purge=True) # Resolve the client from the context chain client = get_client_from_chain(self.context) or client # Resolve the criteria for filtering criteria = {} if portal_type in self.widely_shared_types: # The portal type can be shared widely (e.g. Sample Type) criteria = self.resolve_query(portal_type, client, True) elif portal_type in self.internally_shared_types: # The portal type can be shared among internal clients (e.g Batch) criteria = resolve_query_for_shareable(portal_type, client) elif portal_type == "Client": # Special case, when the item to look for is a Client context_portal_type = api.get_portal_type(self.context) if context_portal_type in self.internally_shared_types: # Current context can be shared internally (e.g. Batch) if client: # Display only the current Client in searches criteria = self.resolve_query(portal_type, client, False) else: # Display all internal clients internal_clients = api.get_portal().internal_clients criteria = { "path": { "query": api.get_path(internal_clients), "depth": 1 } } else: # Display current client only (if client != None) or all them criteria = self.resolve_query(portal_type, client, False) elif client: # Portal type is not shareable in any way (e.g Contact) criteria = self.resolve_query(portal_type, client, False) query.update(criteria) logger.info(repr(query)) logger.info("===============================================") return query