diff --git a/CHANGES.rst b/CHANGES.rst index f22db4c6c0..d505587244 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,7 @@ Changelog **Added** +- #1462 Allow to extend the behavior of fields from AddSample view with adapters - #1455 Added support for adapters in guard handler - #1436 Setting in setup for auto-reception of samples upon creation - #1433 Added Submitter column in Sample's analyses listing @@ -35,6 +36,7 @@ Changelog **Fixed** +- #1462 Autofill Client Contact in Sample Add form when current user is a client - #1461 Allow unassign transition for cancelled/rejected/retracted analyses - #1449 sort_limit was not considered in ReferenceWidget searches - #1449 Fix Clients were unable to add batches diff --git a/bika/lims/adapters/addsample.py b/bika/lims/adapters/addsample.py new file mode 100644 index 0000000000..c9db15c9f9 --- /dev/null +++ b/bika/lims/adapters/addsample.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# +# This file is part of SENAITE.CORE. +# +# SENAITE.CORE is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation, version 2. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Copyright 2018-2019 by it's authors. +# Some rights reserved, see README and LICENSE. + +from zope.component import adapts + +from bika.lims import api +from bika.lims.interfaces import IAddSampleObjectInfo + + +class AddSampleObjectInfoAdapter(object): + """Base implementation of an adapter for reference fields used in Sample + Add form + """ + adapts(IAddSampleObjectInfo) + + def __init__(self, context): + self.context = context + + def get_base_info(self): + """Returns the basic dictionary structure for the current object + """ + return { + "id": api.get_id(self.context), + "uid": api.get_uid(self.context), + "title": api.get_title(self.context), + "field_values": {}, + "filter_queries": {}, + } + + def get_object_info(self): + """Returns the dict representation of the context object for its + correct consumption by Sample Add form. + See IAddSampleObjectInfo for further details + """ + raise NotImplementedError("get_object_info not implemented") diff --git a/bika/lims/browser/analysisrequest/add2.py b/bika/lims/browser/analysisrequest/add2.py index d5f7f8e37f..5d308d82a4 100644 --- a/bika/lims/browser/analysisrequest/add2.py +++ b/bika/lims/browser/analysisrequest/add2.py @@ -22,31 +22,35 @@ from collections import OrderedDict from datetime import datetime -from bika.lims import POINTS_OF_CAPTURE -from bika.lims import api -from bika.lims import bikaMessageFactory as _ -from bika.lims import logger -from bika.lims.api.analysisservice import get_calculation_dependencies_for -from bika.lims.api.analysisservice import get_service_dependencies_for -from bika.lims.interfaces import IGetDefaultFieldValueARAddHook -from bika.lims.utils import tmpID -from bika.lims.utils.analysisrequest import create_analysisrequest as crar -from bika.lims.workflow import ActionHandlerPool from BTrees.OOBTree import OOBTree from DateTime import DateTime -from plone import protect -from plone.memoize.volatile import DontCache -from plone.memoize.volatile import cache from Products.CMFPlone.utils import _createObjectByType from Products.CMFPlone.utils import safe_unicode from Products.Five.browser import BrowserView from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile +from plone import protect +from plone.memoize import view as viewcache +from plone.memoize.volatile import DontCache +from plone.memoize.volatile import cache from zope.annotation.interfaces import IAnnotations +from zope.component import getAdapters from zope.component import queryAdapter from zope.i18n.locales import locales from zope.interface import implements from zope.publisher.interfaces import IPublishTraverse +from bika.lims import POINTS_OF_CAPTURE +from bika.lims import api +from bika.lims import bikaMessageFactory as _ +from bika.lims import logger +from bika.lims.api.analysisservice import get_calculation_dependencies_for +from bika.lims.api.analysisservice import get_service_dependencies_for +from bika.lims.interfaces import IGetDefaultFieldValueARAddHook, \ + IAddSampleFieldsFlush, IAddSampleObjectInfo +from bika.lims.utils import tmpID +from bika.lims.utils.analysisrequest import create_analysisrequest as crar +from bika.lims.workflow import ActionHandlerPool + AR_CONFIGURATION_STORAGE = "bika.lims.browser.analysisrequest.manage.add" SKIP_FIELD_ON_COPY = ["Sample", "PrimaryAnalysisRequest", "Remarks"] @@ -107,6 +111,10 @@ def get_view_url(self): return url return "{}?{}".format(url, qs) + # N.B.: We are caching here persistent objects! + # It should be safe to do this but only on the view object, + # because it get recreated per request (transaction border). + @viewcache.memoize def get_object_by_uid(self, uid): """Get the object by UID """ @@ -433,11 +441,16 @@ def get_default_contact(self, client=None): "query": path, "depth": 1 }, - "incactive_state": "active", + "is_active": True, } contacts = catalog(query) if len(contacts) == 1: return api.get_object(contacts[0]) + elif client == api.get_current_client(): + # Current user is a Client contact. Use current contact + current_user = api.get_current_user() + return api.get_user_contact(current_user, contact_types=["Contact"]) + return None def getMemberDiscountApplies(self): @@ -782,13 +795,6 @@ def get_uids_from_record(self, record, key): value = value.split(",") return filter(lambda uid: uid, value) - def get_objs_from_record(self, record, key): - """Returns a mapping of UID -> object - """ - uids = self.get_uids_from_record(record, key) - objs = map(self.get_object_by_uid, uids) - return dict(zip(uids, objs)) - @cache(cache_key) def get_base_info(self, obj): """Returns the base info of an object @@ -797,11 +803,11 @@ def get_base_info(self, obj): return {} info = { - "id": obj.getId(), - "uid": obj.UID(), - "title": obj.Title(), - "description": obj.Description(), - "url": obj.absolute_url(), + "id": api.get_id(obj), + "uid": api.get_uid(obj), + "title": api.get_title(obj), + "field_values": {}, + "filter_queries": {}, } return info @@ -811,15 +817,11 @@ def get_client_info(self, obj): """Returns the client info of an object """ info = self.get_base_info(obj) - - default_contact_info = {} default_contact = self.get_default_contact(client=obj) if default_contact: - default_contact_info = self.get_contact_info(default_contact) - - info.update({ - "default_contact": default_contact_info - }) + info["field_values"].update({ + "Contact": self.get_contact_info(default_contact) + }) # UID of the client uid = api.get_uid(obj) @@ -845,39 +847,38 @@ def get_client_info(self, obj): # catalog queries for UI field filtering filter_queries = { - "contact": { + "Contact": { "getParentUID": [uid] }, - "cc_contact": { + "CCContact": { "getParentUID": [uid] }, - "invoice_contact": { + "InvoiceContact": { "getParentUID": [uid] }, - "samplepoint": { + "SamplePoint": { "getClientUID": [uid, bika_samplepoints_uid], }, - "artemplates": { + "Template": { "getClientUID": [uid, bika_artemplates_uid], }, - "analysisprofiles": { + "Profiles": { "getClientUID": [uid, bika_analysisprofiles_uid], }, - "analysisspecs": { + "Specification": { "getClientUID": [uid, bika_analysisspecs_uid], }, - "samplinground": { + "SamplingRound": { "getParentUID": [uid], }, - "sample": { + "Sample": { "getClientUID": [uid], }, - "batch": { + "Batch": { "getClientUID": [uid, ""], } } info["filter_queries"] = filter_queries - return info @cache(cache_key) @@ -891,20 +892,24 @@ def get_contact_info(self, obj): # Note: It might get a circular dependency when calling: # map(self.get_contact_info, obj.getCCContact()) - cccontacts = {} + cccontacts = [] for contact in obj.getCCContact(): uid = api.get_uid(contact) fullname = contact.getFullname() email = contact.getEmailAddress() - cccontacts[uid] = { + cccontacts.append({ + "uid": uid, + "title": fullname, "fullname": fullname, "email": email - } + }) info.update({ "fullname": fullname, "email": email, - "cccontacts": cccontacts, + "field_values": { + "CCContact": cccontacts + }, }) return info @@ -1024,42 +1029,21 @@ def get_sampletype_info(self, obj): client = self.get_client() client_uid = client and api.get_uid(client) or "" - # sample matrix - sample_matrix = obj.getSampleMatrix() - sample_matrix_uid = sample_matrix and sample_matrix.UID() or "" - sample_matrix_title = sample_matrix and sample_matrix.Title() or "" - - # container type - container_type = obj.getContainerType() - container_type_uid = container_type and container_type.UID() or "" - container_type_title = container_type and container_type.Title() or "" - - # sample points - sample_points = obj.getSamplePoints() - sample_point_uids = map(lambda sp: sp.UID(), sample_points) - sample_point_titles = map(lambda sp: sp.Title(), sample_points) - info.update({ "prefix": obj.getPrefix(), "minimum_volume": obj.getMinimumVolume(), "hazardous": obj.getHazardous(), "retention_period": obj.getRetentionPeriod(), - "sample_matrix_uid": sample_matrix_uid, - "sample_matrix_title": sample_matrix_title, - "container_type_uid": container_type_uid, - "container_type_title": container_type_title, - "sample_point_uids": sample_point_uids, - "sample_point_titles": sample_point_titles, }) # catalog queries for UI field filtering filter_queries = { - "samplepoint": { + "SamplePoint": { "getSampleTypeTitles": [obj.Title(), ''], "getClientUID": [client_uid, bika_samplepoints_uid], "sort_order": "descending", }, - "specification": { + "Specification": { "getSampleTypeTitle": obj.Title(), "getClientUID": [client_uid, bika_analysisspecs_uid], "sort_order": "descending", @@ -1070,75 +1054,66 @@ def get_sampletype_info(self, obj): return info @cache(cache_key) - def get_sample_info(self, obj): - """Returns the info for a Sample + def get_primaryanalysisrequest_info(self, obj): + """Returns the info for a Primary Sample """ info = self.get_base_info(obj) - # sample type + batch = obj.getBatch() + client = obj.getClient() sample_type = obj.getSampleType() - sample_type_uid = sample_type and sample_type.UID() or "" - sample_type_title = sample_type and sample_type.Title() or "" - - # sample condition sample_condition = obj.getSampleCondition() - sample_condition_uid = sample_condition \ - and sample_condition.UID() or "" - sample_condition_title = sample_condition \ - and sample_condition.Title() or "" - - # storage location storage_location = obj.getStorageLocation() - storage_location_uid = storage_location \ - and storage_location.UID() or "" - storage_location_title = storage_location \ - and storage_location.Title() or "" - - # sample point sample_point = obj.getSamplePoint() - sample_point_uid = sample_point and sample_point.UID() or "" - sample_point_title = sample_point and sample_point.Title() or "" - - # container type - container_type = sample_type and sample_type.getContainerType() or None - container_type_uid = container_type and container_type.UID() or "" - container_type_title = container_type and container_type.Title() or "" - - # Sampling deviation + container = obj.getContainer() deviation = obj.getSamplingDeviation() - deviation_uid = deviation and deviation.UID() or "" - deviation_title = deviation and deviation.Title() or "" + preservation = obj.getPreservation() + specification = obj.getSpecification() + sample_template = obj.getTemplate() + profiles = obj.getProfiles() or [] + cccontacts = obj.getCCContact() or [] + contact = obj.getContact() info.update({ - "sample_id": obj.getId(), - "batch_uid": obj.getBatchUID() or None, - "date_sampled": self.to_iso_date(obj.getDateSampled()), - "sampling_date": self.to_iso_date(obj.getSamplingDate()), - "sample_type_uid": sample_type_uid, - "sample_type_title": sample_type_title, - "container_type_uid": container_type_uid, - "container_type_title": container_type_title, - "sample_condition_uid": sample_condition_uid, - "sample_condition_title": sample_condition_title, - "storage_location_uid": storage_location_uid, - "storage_location_title": storage_location_title, - "sample_point_uid": sample_point_uid, - "sample_point_title": sample_point_title, - "environmental_conditions": obj.getEnvironmentalConditions(), "composite": obj.getComposite(), - "client_uid": obj.getClientUID(), - "client_title": obj.getClientTitle(), - "contact": self.get_contact_info(obj.getContact()), - "client_order_number": obj.getClientOrderNumber(), - "client_sample_id": obj.getClientSampleID(), - "client_reference": obj.getClientReference(), - "sampling_deviation_uid": deviation_uid, - "sampling_deviation_title": deviation_title, - "sampling_workflow_enabled": obj.getSamplingWorkflowEnabled(), - "remarks": obj.getRemarks(), }) + + # Set the fields for which we want the value to be set automatically + # when the primary sample is selected + info["field_values"].update({ + "Client": self.to_field_value(client), + "Contact": self.to_field_value(contact), + "CCContact": map(self.to_field_value, cccontacts), + "CCEmails": obj.getCCEmails() or [], + "Batch": self.to_field_value(batch), + "DateSampled": {"value": self.to_iso_date(obj.getDateSampled())}, + "SamplingDate": {"value": self.to_iso_date(obj.getSamplingDate())}, + "SampleType": self.to_field_value(sample_type), + "EnvironmentalConditions": {"value": obj.getEnvironmentalConditions()}, + "ClientSampleID": {"value": obj.getClientSampleID()}, + "ClientReference": {"value": obj.getClientReference()}, + "ClientOrderNumber": {"value": obj.getClientOrderNumber()}, + "SampleCondition": self.to_field_value(sample_condition), + "SamplePoint": self.to_field_value(sample_point), + "StorageLocation": self.to_field_value(storage_location), + "Container": self.to_field_value(container), + "SamplingDeviation": self.to_field_value(deviation), + "Preservation": self.to_field_value(preservation), + "Specification": self.to_field_value(specification), + "Template": self.to_field_value(sample_template), + "Profiles": map(self.to_field_value, profiles), + "Composite": {"value": obj.getComposite()} + }) + return info + @cache(cache_key) + def to_field_value(self, obj): + return { + "uid": obj and api.get_uid(obj) or "", + "title": obj and api.get_title(obj) or "" + } + @cache(cache_key) def get_specification_info(self, obj): """Returns the info for a Specification @@ -1180,22 +1155,6 @@ def get_service_by_keyword(keyword): info["service_uids"] = specifications.keys() return info - @cache(cache_key) - def get_container_info(self, obj): - """Returns the info for a Container - """ - info = self.get_base_info(obj) - info.update({}) - return info - - @cache(cache_key) - def get_preservation_info(self, obj): - """Returns the info for a Preservation - """ - info = self.get_base_info(obj) - info.update({}) - return info - def ajax_get_global_settings(self): """Returns the global Bika settings """ @@ -1205,6 +1164,64 @@ def ajax_get_global_settings(self): } return settings + def ajax_get_flush_settings(self): + """Returns the settings for fields flush + """ + flush_settings = { + "Client": [ + "Contact", + "CCContact", + "InvoiceContact", + "SamplePoint", + "Template", + "Profiles", + "PrimaryAnalysisRequest", + "Specification", + "Batch" + ], + "Contact": [ + "CCContact" + ], + "SampleType": [ + "SamplePoint", + "Specification" + ], + "PrimarySample": [ + "Batch" + "Client", + "Contact", + "CCContact", + "CCEmails", + "ClientOrderNumber", + "ClientReference", + "ClientSampleID", + "ContainerType", + "DateSampled", + "EnvironmentalConditions", + "InvoiceContact", + "Preservation", + "Profiles", + "SampleCondition", + "SamplePoint", + "SampleType", + "SamplingDate", + "SamplingDeviation", + "StorageLocation", + "Specification", + "Template", + ] + } + + # Maybe other add-ons have additional fields that require flushing + for name, ad in getAdapters((self.context,), IAddSampleFieldsFlush): + logger.info("Additional flush settings from {}".format(name)) + additional_settings = ad.get_flush_settings() + for key, values in additional_settings.items(): + new_values = flush_settings.get(key, []) + values + flush_settings[key] = list(set(new_values)) + + return flush_settings + def ajax_get_service(self): """Returns the services information """ @@ -1221,223 +1238,268 @@ def ajax_get_service(self): return info def ajax_recalculate_records(self): - """Recalculate all AR records and dependencies + out = {} + records = self.get_records() + for num_sample, record in enumerate(records): + # Get reference fields metadata + metadata = self.get_record_metadata(record) + + # Extract additional metadata from this record + # service_to_specs + service_to_specs = self.get_service_to_specs_info(metadata) + metadata.update(service_to_specs) + + # service_to_templates, template_to_services + templates_additional = self.get_template_additional_info(metadata) + metadata.update(templates_additional) + + # service_to_profiles, profiles_to_services + profiles_additional = self.get_profiles_additional_info(metadata) + metadata.update(profiles_additional) + + # dependencies + dependencies = self.get_unmet_dependencies_info(metadata) + metadata.update(dependencies) - - samples - - templates - - profiles - - services - - dependecies + # Set the metadata for current sample number (column) + out[num_sample] = metadata - XXX: This function has grown too much and needs refactoring! + return out + + def get_record_metadata(self, record): + """Returns the metadata for the record passed in """ - out = {} + metadata = {} + extra_fields = {} + for key, value in record.items(): + if not key.endswith("_uid"): + continue - # The sorted records from the request - records = self.get_records() + # This is a reference field (ends with _uid), so we add the + # metadata key, even if there is no way to handle objects this + # field refers to + metadata_key = key.replace("_uid", "") + metadata_key = "{}_metadata".format(metadata_key.lower()) + metadata[metadata_key] = {} - for n, record in enumerate(records): + if not value: + continue - # Mapping of client UID -> client object info - client_metadata = {} - # Mapping of contact UID -> contact object info - contact_metadata = {} - # Mapping of sample UID -> sample object info - sample_metadata = {} - # Mapping of sampletype UID -> sampletype object info - sampletype_metadata = {} - # Mapping of specification UID -> specification object info - specification_metadata = {} - # Mapping of specification UID -> list of service UIDs - specification_to_services = {} - # Mapping of service UID -> list of specification UIDs - service_to_specifications = {} - # Mapping of template UID -> template object info - template_metadata = {} - # Mapping of template UID -> list of service UIDs - template_to_services = {} - # Mapping of service UID -> list of template UIDs - service_to_templates = {} - # Mapping of profile UID -> list of service UIDs - profile_to_services = {} - # Mapping of service UID -> list of profile UIDs - service_to_profiles = {} - # Profile metadata for UI purposes - profile_metadata = {} - # Mapping of service UID -> service object info - service_metadata = {} - # mapping of service UID -> unmet service dependency UIDs - unmet_dependencies = {} - - # Mappings of UID -> object of selected items in this record - _clients = self.get_objs_from_record(record, "Client_uid") - _contacts = self.get_objs_from_record(record, "Contact_uid") - _specifications = self.get_objs_from_record( - record, "Specification_uid") - _templates = self.get_objs_from_record(record, "Template_uid") - _samples = self.get_objs_from_record(record, "PrimaryAnalysisRequest_uid") - _profiles = self.get_objs_from_record(record, "Profiles_uid") - _services = self.get_objs_from_record(record, "Analyses") - _sampletypes = self.get_objs_from_record(record, "SampleType_uid") - - # CLIENTS - for uid, obj in _clients.iteritems(): - # get the client metadata - metadata = self.get_client_info(obj) - # remember the sampletype metadata - client_metadata[uid] = metadata - - # CONTACTS - for uid, obj in _contacts.iteritems(): - # get the client metadata - metadata = self.get_contact_info(obj) - # remember the sampletype metadata - contact_metadata[uid] = metadata - - # SPECIFICATIONS - for uid, obj in _specifications.iteritems(): - # get the specification metadata - metadata = self.get_specification_info(obj) - # remember the metadata of this specification - specification_metadata[uid] = metadata - # get the spec'd service UIDs - service_uids = metadata["service_uids"] - # remember a mapping of specification uid -> spec'd services - specification_to_services[uid] = service_uids - # remember a mapping of service uid -> specifications - for service_uid in service_uids: - if service_uid in service_to_specifications: - service_to_specifications[service_uid].append(uid) - else: - service_to_specifications[service_uid] = [uid] - - # AR TEMPLATES - for uid, obj in _templates.iteritems(): - # get the template metadata - metadata = self.get_template_info(obj) - # remember the template metadata - template_metadata[uid] = metadata - - # profile from the template - profile = obj.getAnalysisProfile() - # add the profile to the other profiles - if profile is not None: - profile_uid = api.get_uid(profile) - _profiles[profile_uid] = profile - - # get the template analyses - # [{'partition': 'part-1', 'service_uid': '...'}, - # {'partition': 'part-1', 'service_uid': '...'}] - analyses = obj.getAnalyses() or [] - # get all UIDs of the template records - service_uids = map( - lambda rec: rec.get("service_uid"), analyses) - # remember a mapping of template uid -> service - template_to_services[uid] = service_uids - # remember a mapping of service uid -> templates - for service_uid in service_uids: - # append service to services mapping + # Get objects information (metadata) + objs_info = self.get_objects_info(record, key) + objs_uids = map(lambda obj: obj["uid"], objs_info) + metadata[metadata_key] = dict(zip(objs_uids, objs_info)) + + # Grab 'field_values' fields to be recalculated too + for obj_info in objs_info: + field_values = obj_info.get("field_values", {}) + for field_name, field_value in field_values.items(): + if not isinstance(field_value, dict): + # this is probably a list, e.g. "Profiles" field + continue + uids = self.get_uids_from_record(field_value, "uid") + if len(uids) == 1: + extra_fields[field_name] = uids[0] + + # Populate metadata with object info from extra fields + for field_name, uid in extra_fields.items(): + key = "{}_metadata".format(field_name.lower()) + if metadata.get(key): + # This object has been processed already, skip + continue + obj = self.get_object_by_uid(uid) + if not obj: + continue + obj_info = self.get_object_info(obj, field_name) + if not obj_info or "uid" not in obj_info: + continue + metadata[key] = {obj_info["uid"]: obj_info} + + return metadata + + def get_service_to_specs_info(self, metadata): + service_to_specs = {} + specifications = metadata.get("specification_metadata", {}) + for uid, obj_info in specifications.items(): + service_uids = obj_info["service_uids"] + for service_uid in service_uids: + if service_uid in service_to_specs: + service_to_specs[service_uid].append(uid) + else: + service_to_specs[service_uid] = [uid] + return {"service_to_specifications": service_to_specs} + + def get_template_additional_info(self, metadata): + template_to_services = {} + service_to_templates = {} + service_metadata = metadata.get("service_metadata", {}) + profiles_metadata = metadata.get("profiles_metadata", {}) + template = metadata.get("template_metadata", {}) + # We don't expect more than one template, but who knows about future? + for uid, obj_info in template.items(): + obj = self.get_object_by_uid(uid) + # profile from the template + profile = obj.getAnalysisProfile() + # add the profile to the other profiles + if profile is not None: + profile_uid = api.get_uid(profile) + if profile_uid not in profiles_metadata: + profile = self.get_object_by_uid(profile_uid) + profile_info = self.get_profile_info(profile) + profiles_metadata[profile_uid] = profile_info + + # get the template analyses + # [{'partition': 'part-1', 'service_uid': '...'}, + # {'partition': 'part-1', 'service_uid': '...'}] + analyses = obj.getAnalyses() or [] + # get all UIDs of the template records + service_uids = map(lambda rec: rec.get("service_uid"), analyses) + # remember a mapping of template uid -> service + template_to_services[uid] = service_uids + # remember a mapping of service uid -> templates + for service_uid in service_uids: + # remember the template of all services + if service_uid in service_to_templates: + service_to_templates[service_uid].append(uid) + else: + service_to_templates[service_uid] = [uid] + # remember the service metadata + if service_uid not in service_metadata: service = self.get_object_by_uid(service_uid) - # remember the template of all services - if service_uid in service_to_templates: - service_to_templates[service_uid].append(uid) - else: - service_to_templates[service_uid] = [uid] - - # remember the service metadata - if service_uid not in service_metadata: - metadata = self.get_service_info(service) - service_metadata[service_uid] = metadata - - # PROFILES - for uid, obj in _profiles.iteritems(): - # get the profile metadata - metadata = self.get_profile_info(obj) - # remember the profile metadata - profile_metadata[uid] = metadata - # get all services of this profile - services = obj.getService() - # get all UIDs of the profile services - service_uids = map(api.get_uid, services) - # remember all services of this profile - profile_to_services[uid] = service_uids - # remember a mapping of service uid -> profiles - for service in services: - # get the UID of this service - service_uid = api.get_uid(service) - # add the service to the other services - _services[service_uid] = service - # remember the profiles of this service - if service_uid in service_to_profiles: - service_to_profiles[service_uid].append(uid) + service_info = self.get_service_info(service) + service_metadata[service_uid] = service_info + + return { + "service_to_templates": service_to_templates, + "template_to_services": template_to_services, + "service_metadata": service_metadata, + "profiles_metadata": profiles_metadata, + } + + def get_profiles_additional_info(self, metadata): + profile_to_services = {} + service_to_profiles = metadata.get("service_to_profiles", {}) + service_metadata = metadata.get("service_metadata", {}) + profiles = metadata.get("profiles_metadata", {}) + for uid, obj_info in profiles.items(): + obj = self.get_object_by_uid(uid) + # get all services of this profile + services = obj.getService() + # get all UIDs of the profile services + service_uids = map(api.get_uid, services) + # remember all services of this profile + profile_to_services[uid] = service_uids + # remember a mapping of service uid -> profiles + for service in services: + # get the UID of this service + service_uid = api.get_uid(service) + # remember the profiles of this service + if service_uid in service_to_profiles: + service_to_profiles[service_uid].append(uid) + else: + service_to_profiles[service_uid] = [uid] + # remember the service metadata + if service_uid not in service_metadata: + service_info = self.get_service_info(service) + service_metadata[service_uid] = service_info + + return { + "profile_to_services": profile_to_services, + "service_to_profiles": service_to_profiles, + "service_metadata": service_metadata, + } + + def get_unmet_dependencies_info(self, metadata): + # mapping of service UID -> unmet service dependency UIDs + unmet_dependencies = {} + services = metadata.get("service_metadata", {}).copy() + for uid, obj_info in services.items(): + obj = self.get_object_by_uid(uid) + # get the dependencies of this service + deps = get_service_dependencies_for(obj) + + # check for unmet dependencies + for dep in deps["dependencies"]: + # we use the UID to test for equality + dep_uid = api.get_uid(dep) + if dep_uid not in services: + if uid in unmet_dependencies: + unmet_dependencies[uid].append(self.get_base_info(dep)) else: - service_to_profiles[service_uid] = [uid] - - # PRIMARY ANALYSIS REQUESTS - for uid, obj in _samples.iteritems(): - # get the sample metadata - metadata = self.get_sample_info(obj) - # remember the sample metadata - sample_metadata[uid] = metadata - - # SAMPLETYPES - for uid, obj in _sampletypes.iteritems(): - # get the sampletype metadata - metadata = self.get_sampletype_info(obj) - # remember the sampletype metadata - sampletype_metadata[uid] = metadata - - # SERVICES - for uid, obj in _services.iteritems(): - # get the service metadata - metadata = self.get_service_info(obj) - - # remember the services' metadata - service_metadata[uid] = metadata - - # DEPENDENCIES - for uid, obj in _services.iteritems(): - # get the dependencies of this service - deps = get_service_dependencies_for(obj) - - # check for unmet dependencies - for dep in deps["dependencies"]: - # we use the UID to test for equality - dep_uid = api.get_uid(dep) - if dep_uid not in _services.keys(): - if uid in unmet_dependencies: - unmet_dependencies[uid].append( - self.get_base_info(dep)) - else: - unmet_dependencies[uid] = [self.get_base_info(dep)] - # remember the dependencies in the service metadata - service_metadata[uid].update({ - "dependencies": map( - self.get_base_info, deps["dependencies"]), - }) - - # Each key `n` (1,2,3...) contains the form data for one AR Add - # column in the UI. - # All relevant form data will be set accoriding to this data. - out[n] = { - "client_metadata": client_metadata, - "contact_metadata": contact_metadata, - "sample_metadata": sample_metadata, - "sampletype_metadata": sampletype_metadata, - "specification_metadata": specification_metadata, - "specification_to_services": specification_to_services, - "service_to_specifications": service_to_specifications, - "template_metadata": template_metadata, - "template_to_services": template_to_services, - "service_to_templates": service_to_templates, - "profile_metadata": profile_metadata, - "profile_to_services": profile_to_services, - "service_to_profiles": service_to_profiles, - "service_metadata": service_metadata, - "unmet_dependencies": unmet_dependencies, - } + unmet_dependencies[uid] = [self.get_base_info(dep)] + # remember the dependencies in the service metadata + metadata["service_metadata"][uid].update({ + "dependencies": map( + self.get_base_info, deps["dependencies"]), + }) + return { + "unmet_dependencies": unmet_dependencies + } - return out + def get_objects_info(self, record, key): + """ + Returns a list with the metadata for the objects the field with + field_name passed in refers to. Returns empty list if the field is not + a reference field or the record for this key cannot be handled + :param record: a record for a single sample (column) + :param key: The key of the field from the record (e.g. Client_uid) + :return: list of info objects + """ + # Get the objects from this record. Returns a list because the field + # can be multivalued + uids = self.get_uids_from_record(record, key) + objects = map(self.get_object_by_uid, uids) + objects = map(lambda obj: self.get_object_info(obj, key), objects) + return filter(None, objects) + + def object_info_cache_key(method, self, obj, key): + if obj is None or not key: + raise DontCache + field_name = key.replace("_uid", "").lower() + obj_key = api.get_cache_key(obj) + return "-".join([field_name, obj_key]) + + @cache(object_info_cache_key) + def get_object_info(self, obj, key): + """Returns the object info metadata for the passed in object and key + :param obj: the object from which extract the info from + :param key: The key of the field from the record (e.g. Client_uid) + :return: dict that represents the object + """ + # Check if there is a function to handle objects for this field + field_name = key.replace("_uid", "") + func_name = "get_{}_info".format(field_name.lower()) + func = getattr(self, func_name, None) + + # Get the info for each object + info = callable(func) and func(obj) or self.get_base_info(obj) + + # Check if there is any adapter to handle objects for this field + for name, adapter in getAdapters((obj, ), IAddSampleObjectInfo): + logger.info("adapter for '{}': {}".format(field_name, name)) + ad_info = adapter.get_object_info() + self.update_object_info(info, ad_info) + + return info + + def update_object_info(self, base_info, additional_info): + """Updates the dictionaries for keys 'field_values' and 'filter_queries' + from base_info with those defined in additional_info. If base_info is + empty or None, updates the whole base_info dict with additional_info + """ + if not base_info: + base_info.update(additional_info) + return + + # Merge field_values info + field_values = base_info.get("field_values", {}) + field_values.update(additional_info.get("field_values", {})) + base_info["field_values"] = field_values + + # Merge filter_queries info + filter_queries = base_info.get("filter_queries", {}) + filter_queries.update(additional_info.get("filter_queries", {})) + base_info["filter_queries"] = filter_queries def show_recalculate_prices(self): bika_setup = api.get_bika_setup() diff --git a/bika/lims/browser/js/bika.lims.analysisrequest.add.js b/bika/lims/browser/js/bika.lims.analysisrequest.add.js index e46d9ccd7a..6599ef0f01 100644 --- a/bika/lims/browser/js/bika.lims.analysisrequest.add.js +++ b/bika/lims/browser/js/bika.lims.analysisrequest.add.js @@ -10,6 +10,8 @@ indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; window.AnalysisRequestAdd = (function() { + var typeIsArray; + function AnalysisRequestAdd() { this.init_file_fields = bind(this.init_file_fields, this); this.on_form_submit = bind(this.on_form_submit, this); @@ -23,25 +25,18 @@ this.on_analysis_profile_removed = bind(this.on_analysis_profile_removed, this); this.on_analysis_profile_selected = bind(this.on_analysis_profile_selected, this); this.on_analysis_template_changed = bind(this.on_analysis_template_changed, this); - this.on_specification_changed = bind(this.on_specification_changed, this); - this.on_sampletype_changed = bind(this.on_sampletype_changed, this); - this.on_sample_changed = bind(this.on_sample_changed, this); this.on_analysis_lock_button_click = bind(this.on_analysis_lock_button_click, this); this.on_analysis_details_click = bind(this.on_analysis_details_click, this); this.on_analysis_specification_changed = bind(this.on_analysis_specification_changed, this); - this.on_contact_changed = bind(this.on_contact_changed, this); - this.on_client_changed = bind(this.on_client_changed, this); + this.on_referencefield_value_changed = bind(this.on_referencefield_value_changed, this); this.hide_all_service_info = bind(this.hide_all_service_info, this); this.get_service = bind(this.get_service, this); this.set_service_spec = bind(this.set_service_spec, this); this.set_service = bind(this.set_service, this); this.set_template = bind(this.set_template, this); - this.set_sampletype = bind(this.set_sampletype, this); - this.set_sample = bind(this.set_sample, this); - this.set_contact = bind(this.set_contact, this); - this.set_client = bind(this.set_client, this); this.set_reference_field = bind(this.set_reference_field, this); this.set_reference_field_query = bind(this.set_reference_field_query, this); + this.reset_reference_field_query = bind(this.reset_reference_field_query, this); this.get_field_by_id = bind(this.get_field_by_id, this); this.get_fields = bind(this.get_fields, this); this.get_form = bind(this.get_form, this); @@ -51,6 +46,7 @@ this.update_form = bind(this.update_form, this); this.recalculate_prices = bind(this.recalculate_prices, this); this.recalculate_records = bind(this.recalculate_records, this); + this.get_flush_settings = bind(this.get_flush_settings, this); this.get_global_settings = bind(this.get_global_settings, this); this.render_template = bind(this.render_template, this); this.template_dialog = bind(this.template_dialog, this); @@ -65,12 +61,14 @@ this._ = window.jarn.i18n.MessageFactory("senaite.core"); $('input[type=text]').prop('autocomplete', 'off'); this.global_settings = {}; + this.flush_settings = {}; this.records_snapshot = {}; this.applied_templates = {}; $(".blurrable").removeClass("blurrable"); this.bind_eventhandler(); this.init_file_fields(); this.get_global_settings(); + this.get_flush_settings(); return this.recalculate_records(); }; @@ -93,17 +91,13 @@ $("body").on("click", "tr[fieldname=Composite] input[type='checkbox']", this.recalculate_records); $("body").on("click", "tr[fieldname=InvoiceExclude] input[type='checkbox']", this.recalculate_records); $("body").on("click", "tr[fieldname=Analyses] input[type='checkbox']", this.on_analysis_checkbox_click); - $("body").on("selected change", "tr[fieldname=Client] input[type='text']", this.on_client_changed); - $("body").on("selected change", "tr[fieldname=Contact] input[type='text']", this.on_contact_changed); + $("body").on("selected change", "input[type='text'].referencewidget", this.on_referencefield_value_changed); $("body").on("change", "input.min", this.on_analysis_specification_changed); $("body").on("change", "input.max", this.on_analysis_specification_changed); $("body").on("change", "input.warn_min", this.on_analysis_specification_changed); $("body").on("change", "input.warn_max", this.on_analysis_specification_changed); $("body").on("click", ".service-lockbtn", this.on_analysis_lock_button_click); $("body").on("click", ".service-infobtn", this.on_analysis_details_click); - $("body").on("selected change", "tr[fieldname=PrimaryAnalysisRequest] input[type='text']", this.on_sample_changed); - $("body").on("selected change", "tr[fieldname=SampleType] input[type='text']", this.on_sampletype_changed); - $("body").on("selected change", "tr[fieldname=Specification] input[type='text']", this.on_specification_changed); $("body").on("selected change", "tr[fieldname=Template] input[type='text']", this.on_analysis_template_changed); $("body").on("selected", "tr[fieldname=Profiles] input[type='text']", this.on_analysis_profile_selected); $("body").on("click", "tr[fieldname=Profiles] img.deletebtn", this.on_analysis_profile_removed); @@ -203,6 +197,18 @@ }); }; + AnalysisRequestAdd.prototype.get_flush_settings = function() { + + /* + * Retrieve the flush settings + */ + return this.ajax_post_form("get_flush_settings").done(function(settings) { + console.debug("Flush settings:", settings); + this.flush_settings = settings; + return $(this).trigger("flush_settings:updated", settings); + }); + }; + AnalysisRequestAdd.prototype.recalculate_records = function() { /* @@ -249,11 +255,15 @@ me = this; $(".service-lockbtn").hide(); return $.each(records, function(arnum, record) { - $.each(record.client_metadata, function(uid, client) { - return me.set_client(arnum, client); - }); - $.each(record.contact_metadata, function(uid, contact) { - return me.set_contact(arnum, contact); + var discard; + discard = ["service_metadata", "specification_metadata", "template_metadata"]; + $.each(record, function(name, metadata) { + if (indexOf.call(discard, name) >= 0 || !name.endsWith("_metadata")) { + return; + } + return $.each(metadata, function(uid, obj_info) { + return me.apply_field_value(arnum, obj_info); + }); }); $.each(record.service_metadata, function(uid, metadata) { var lock; @@ -271,12 +281,6 @@ return me.set_service_spec(arnum, uid, service_spec); }); }); - $.each(record.sample_metadata, function(uid, sample) { - return me.set_sample(arnum, sample); - }); - $.each(record.sampletype_metadata, function(uid, sampletype) { - return me.set_sampletype(arnum, sampletype); - }); return $.each(record.unmet_dependencies, function(uid, dependencies) { var context, dialog, service; service = record.service_metadata[uid]; @@ -372,10 +376,105 @@ return $(field_id); }; + typeIsArray = Array.isArray || function(value) { + + /* + * Returns if the given value is an array + * Taken from: https://coffeescript-cookbook.github.io/chapters/arrays/check-type-is-array + */ + return {}.toString.call(value) === '[object Array]'; + }; + + AnalysisRequestAdd.prototype.apply_field_value = function(arnum, record) { + + /* + * Applies the value for the given record, by setting values and applying + * search filters to dependents + */ + var me, title; + me = this; + title = record.title; + console.debug("apply_field_value: arnum=" + arnum + " record=" + title); + me.apply_dependent_values(arnum, record); + return me.apply_dependent_filter_queries(record, arnum); + }; + + AnalysisRequestAdd.prototype.apply_dependent_values = function(arnum, record) { + + /* + * Sets default field values to dependents + */ + var me; + me = this; + return $.each(record.field_values, function(field_name, values) { + return me.apply_dependent_value(arnum, field_name, values); + }); + }; + + AnalysisRequestAdd.prototype.apply_dependent_value = function(arnum, field_name, values) { + + /* + * Apply search filters to dependendents + */ + var field, me, values_json; + me = this; + values_json = $.toJSON(values); + field = $("#" + field_name + ("-" + arnum)); + if ((values.if_empty != null) && values.if_empty === true) { + if (!field.val()) { + return; + } + } + console.debug("apply_dependent_value: field_name=" + field_name + " field_values=" + values_json); + if ((values.uid != null) && (values.title != null)) { + return me.set_reference_field(field, values.uid, values.title); + } else if (values.value != null) { + if (typeof values.value === "boolean") { + return field.prop("checked", values.value); + } else { + return field.val(values.value); + } + } else if (typeIsArray(values)) { + return $.each(values, function(index, item) { + return me.apply_dependent_value(arnum, field_name, item); + }); + } + }; + + AnalysisRequestAdd.prototype.apply_dependent_filter_queries = function(record, arnum) { + + /* + * Apply search filters to dependents + */ + var me; + me = this; + return $.each(record.filter_queries, function(field_name, query) { + var field; + field = $("#" + field_name + ("-" + arnum)); + return me.set_reference_field_query(field, query); + }); + }; + + AnalysisRequestAdd.prototype.flush_fields_for = function(field_name, arnum) { + + /* + * Flush dependant fields + */ + var field_ids, me; + me = this; + field_ids = this.flush_settings[field_name]; + return $.each(this.flush_settings[field_name], function(index, id) { + var field; + console.debug("flushing: id=" + id); + field = $("#" + id + "-" + arnum); + return me.flush_reference_field(field); + }); + }; + AnalysisRequestAdd.prototype.flush_reference_field = function(field) { /* - * Empty the reference field + * Empty the reference field and restore the search query */ var catalog_name; catalog_name = field.attr("catalog_name"); @@ -384,7 +483,22 @@ } field.val(""); $("input[type=hidden]", field.parent()).val(""); - return $(".multiValued-listing", field.parent()).empty(); + $(".multiValued-listing", field.parent()).empty(); + return this.reset_reference_field_query(field); + }; + + AnalysisRequestAdd.prototype.reset_reference_field_query = function(field) { + + /* + * Restores the catalog search query for the given reference field + */ + var catalog_name, query; + catalog_name = field.attr("catalog_name"); + if (!catalog_name) { + return; + } + query = $.parseJSON(field.attr("base_query")); + return this.set_reference_field_query(field, query); }; AnalysisRequestAdd.prototype.set_reference_field_query = function(field, query, type) { @@ -474,157 +588,6 @@ } }; - AnalysisRequestAdd.prototype.set_client = function(arnum, client) { - - /* - * Filter Contacts - * Filter CCContacts - * Filter InvoiceContacts - * Filter SamplePoints - * Filter ARTemplates - * Filter Specification - * Filter SamplingRound - * Filter Batch - */ - var contact_title, contact_uid, field, query; - field = $("#Contact-" + arnum); - query = client.filter_queries.contact; - this.set_reference_field_query(field, query); - if (document.URL.indexOf("analysisrequests") > -1) { - contact_title = client.default_contact.title; - contact_uid = client.default_contact.uid; - if (contact_title && contact_uid) { - this.set_reference_field(field, contact_uid, contact_title); - } - } - field = $("#CCContact-" + arnum); - query = client.filter_queries.cc_contact; - this.set_reference_field_query(field, query); - field = $("#InvoiceContact-" + arnum); - query = client.filter_queries.invoice_contact; - this.set_reference_field_query(field, query); - field = $("#SamplePoint-" + arnum); - query = client.filter_queries.samplepoint; - this.set_reference_field_query(field, query); - field = $("#Template-" + arnum); - query = client.filter_queries.artemplates; - this.set_reference_field_query(field, query); - field = $("#Profiles-" + arnum); - query = client.filter_queries.analysisprofiles; - this.set_reference_field_query(field, query); - field = $("#Specification-" + arnum); - query = client.filter_queries.analysisspecs; - this.set_reference_field_query(field, query); - field = $("#SamplingRound-" + arnum); - query = client.filter_queries.samplinground; - this.set_reference_field_query(field, query); - field = $("#PrimaryAnalysisRequest-" + arnum); - query = client.filter_queries.sample; - this.set_reference_field_query(field, query); - field = $("#Batch-" + arnum); - query = client.filter_queries.batch; - return this.set_reference_field_query(field, query); - }; - - AnalysisRequestAdd.prototype.set_contact = function(arnum, contact) { - - /* - * Set CC Contacts - */ - var field, me; - me = this; - field = $("#CCContact-" + arnum); - return $.each(contact.cccontacts, function(uid, cccontact) { - var fullname; - fullname = cccontact.fullname; - return me.set_reference_field(field, uid, fullname); - }); - }; - - AnalysisRequestAdd.prototype.set_sample = function(arnum, sample) { - - /* - * Apply the sample data to all fields of arnum - */ - var contact, field, fullname, title, uid, value; - field = $("#Client-" + arnum); - uid = sample.client_uid; - title = sample.client_title; - this.set_reference_field(field, uid, title); - field = $("#Contact-" + arnum); - contact = sample.contact; - uid = contact.uid; - fullname = contact.fullname; - this.set_reference_field(field, uid, fullname); - this.set_contact(arnum, contact); - field = $("#SamplingDate-" + arnum); - value = sample.sampling_date; - field.val(value); - field = $("#DateSampled-" + arnum); - value = sample.date_sampled; - field.val(value); - field = $("#SampleType-" + arnum); - uid = sample.sample_type_uid; - title = sample.sample_type_title; - this.set_reference_field(field, uid, title); - field = $("#EnvironmentalConditions-" + arnum); - value = sample.environmental_conditions; - field.val(value); - field = $("#ClientSampleID-" + arnum); - value = sample.client_sample_id; - field.val(value); - field = $("#ClientReference-" + arnum); - value = sample.client_reference; - field.val(value); - field = $("#ClientOrderNumber-" + arnum); - value = sample.client_order_number; - field.val(value); - field = $("#Composite-" + arnum); - field.prop("checked", sample.composite); - field = $("#SampleCondition-" + arnum); - uid = sample.sample_condition_uid; - title = sample.sample_condition_title; - this.set_reference_field(field, uid, title); - field = $("#SamplePoint-" + arnum); - uid = sample.sample_point_uid; - title = sample.sample_point_title; - this.set_reference_field(field, uid, title); - field = $("#StorageLocation-" + arnum); - uid = sample.storage_location_uid; - title = sample.storage_location_title; - this.set_reference_field(field, uid, title); - field = $("#DefaultContainerType-" + arnum); - uid = sample.container_type_uid; - title = sample.container_type_title; - this.set_reference_field(field, uid, title); - field = $("#SamplingDeviation-" + arnum); - uid = sample.sampling_deviation_uid; - title = sample.sampling_deviation_title; - return this.set_reference_field(field, uid, title); - }; - - AnalysisRequestAdd.prototype.set_sampletype = function(arnum, sampletype) { - - /* - * Recalculate partitions - * Filter Sample Points - */ - var field, query, title, uid; - field = $("#SamplePoint-" + arnum); - query = sampletype.filter_queries.samplepoint; - this.set_reference_field_query(field, query); - field = $("#DefaultContainerType-" + arnum); - if (!field.val()) { - uid = sampletype.container_type_uid; - title = sampletype.container_type_title; - this.flush_reference_field(field); - this.set_reference_field(field, uid, title); - } - field = $("#Specification-" + arnum); - query = sampletype.filter_queries.specification; - return this.set_reference_field_query(field, query); - }; - AnalysisRequestAdd.prototype.set_template = function(arnum, template) { /* @@ -778,45 +741,27 @@ /* EVENT HANDLER */ - AnalysisRequestAdd.prototype.on_client_changed = function(event) { + AnalysisRequestAdd.prototype.on_referencefield_value_changed = function(event) { /* - * Eventhandler when the client changed (happens on Batches) + * Generic event handler for when a field value changes */ - var $el, arnum, el, field_ids, me, uid; + var $el, arnum, el, field_name, has_value, me, uid; me = this; el = event.currentTarget; $el = $(el); + has_value = $el.val(); uid = $el.attr("uid"); + field_name = $el.closest("tr[fieldname]").attr("fieldname"); arnum = $el.closest("[arnum]").attr("arnum"); - console.debug("°°° on_client_changed: arnum=" + arnum + " °°°"); - field_ids = ["Contact", "CCContact", "InvoiceContact", "SamplePoint", "Template", "Profiles", "PrimaryAnalysisRequest", "Specification", "Batch"]; - $.each(field_ids, function(index, id) { - var field; - field = me.get_field_by_id(id, arnum); - return me.flush_reference_field(field); - }); - return $(me).trigger("form:changed"); - }; - - AnalysisRequestAdd.prototype.on_contact_changed = function(event) { - - /* - * Eventhandler when the contact changed - */ - var $el, arnum, el, field_ids, me, uid; - me = this; - el = event.currentTarget; - $el = $(el); - uid = $el.attr("uid"); - arnum = $el.closest("[arnum]").attr("arnum"); - console.debug("°°° on_contact_changed: arnum=" + arnum + " °°°"); - field_ids = ["CCContact"]; - $.each(field_ids, function(index, id) { - var field; - field = me.get_field_by_id(id, arnum); - return me.flush_reference_field(field); - }); + if (field_name === "Template" || field_name === "Profiles") { + return; + } + console.debug("°°° on_referencefield_value_changed: field_name=" + field_name + " arnum=" + arnum + " °°°"); + me.flush_fields_for(field_name, arnum); + if (!has_value) { + $("input[type=hidden]", $el.parent()).val(""); + } return $(me).trigger("form:changed"); }; @@ -854,7 +799,7 @@ if (uid in record.service_to_profiles) { profiles = record.service_to_profiles[uid]; $.each(profiles, function(index, uid) { - return extra["profiles"].push(record.profile_metadata[uid]); + return extra["profiles"].push(record.profiles_metadata[uid]); }); } if (uid in record.service_to_templates) { @@ -905,7 +850,7 @@ context["templates"] = []; if (uid in record.service_to_profiles) { profile_uid = record.service_to_profiles[uid]; - context["profiles"].push(record.profile_metadata[profile_uid]); + context["profiles"].push(record.profiles_metadata[profile_uid]); } if (uid in record.service_to_templates) { template_uid = record.service_to_templates[uid]; @@ -919,73 +864,6 @@ return dialog = this.template_dialog("service-dependant-template", context, buttons); }; - AnalysisRequestAdd.prototype.on_sample_changed = function(event) { - - /* - * Eventhandler when the Sample was changed. - */ - var $el, arnum, el, has_sample_selected, me, uid, val; - me = this; - el = event.currentTarget; - $el = $(el); - uid = $(el).attr("uid"); - val = $el.val(); - arnum = $el.closest("[arnum]").attr("arnum"); - has_sample_selected = $el.val(); - console.debug("°°° on_sample_change::UID=" + uid + " PrimaryAnalysisRequest=" + val + "°°°"); - if (!has_sample_selected) { - $("input[type=hidden]", $el.parent()).val(""); - } - return $(me).trigger("form:changed"); - }; - - AnalysisRequestAdd.prototype.on_sampletype_changed = function(event) { - - /* - * Eventhandler when the SampleType was changed. - * Fires form:changed event - */ - var $el, arnum, el, field_ids, has_sampletype_selected, me, uid, val; - me = this; - el = event.currentTarget; - $el = $(el); - uid = $(el).attr("uid"); - val = $el.val(); - arnum = $el.closest("[arnum]").attr("arnum"); - has_sampletype_selected = $el.val(); - console.debug("°°° on_sampletype_change::UID=" + uid + " SampleType=" + val + "°°°"); - if (!has_sampletype_selected) { - $("input[type=hidden]", $el.parent()).val(""); - } - field_ids = ["SamplePoint", "Specification"]; - $.each(field_ids, function(index, id) { - var field; - field = me.get_field_by_id(id, arnum); - return me.flush_reference_field(field); - }); - return $(me).trigger("form:changed"); - }; - - AnalysisRequestAdd.prototype.on_specification_changed = function(event) { - - /* - * Eventhandler when the Specification was changed. - */ - var $el, arnum, el, has_specification_selected, me, uid, val; - me = this; - el = event.currentTarget; - $el = $(el); - uid = $(el).attr("uid"); - val = $el.val(); - arnum = $el.closest("[arnum]").attr("arnum"); - has_specification_selected = $el.val(); - console.debug("°°° on_specification_change::UID=" + uid + " Specification=" + val + "°°°"); - if (!has_specification_selected) { - $("input[type=hidden]", $el.parent()).val(""); - } - return $(me).trigger("form:changed"); - }; - AnalysisRequestAdd.prototype.on_analysis_template_changed = function(event) { /* @@ -1095,7 +973,7 @@ uid = $el.attr("uid"); arnum = $el.closest("[arnum]").attr("arnum"); record = this.records_snapshot[arnum]; - profile_metadata = record.profile_metadata[uid]; + profile_metadata = record.profiles_metadata[uid]; profile_services = []; $.each(record.profile_to_services[uid], function(index, uid) { return profile_services.push(record.service_metadata[uid]); diff --git a/bika/lims/browser/js/coffee/bika.lims.analysisrequest.add.coffee b/bika/lims/browser/js/coffee/bika.lims.analysisrequest.add.coffee index 519e22375b..48f10c1e55 100644 --- a/bika/lims/browser/js/coffee/bika.lims.analysisrequest.add.coffee +++ b/bika/lims/browser/js/coffee/bika.lims.analysisrequest.add.coffee @@ -18,6 +18,9 @@ class window.AnalysisRequestAdd # storage for global Bika settings @global_settings = {} + # storage for mapping of fields to flush on_change + @flush_settings = {} + # services data snapshot from recalculate_records # returns a mapping of arnum -> services data @records_snapshot = {} @@ -40,6 +43,9 @@ class window.AnalysisRequestAdd # get the global settings on load @get_global_settings() + # get the flush settings + @get_flush_settings() + # recalculate records on load (needed for AR copies) @recalculate_records() @@ -67,10 +73,9 @@ class window.AnalysisRequestAdd $("body").on "click", "tr[fieldname=InvoiceExclude] input[type='checkbox']", @recalculate_records # Analysis Checkbox clicked $("body").on "click", "tr[fieldname=Analyses] input[type='checkbox']", @on_analysis_checkbox_click - # Client changed - $("body").on "selected change", "tr[fieldname=Client] input[type='text']", @on_client_changed - # Contact changed - $("body").on "selected change", "tr[fieldname=Contact] input[type='text']", @on_contact_changed + # Generic onchange event handler for reference fields + $("body").on "selected change" , "input[type='text'].referencewidget", @on_referencefield_value_changed + # Analysis Specification changed $("body").on "change", "input.min", @on_analysis_specification_changed $("body").on "change", "input.max", @on_analysis_specification_changed @@ -80,12 +85,6 @@ class window.AnalysisRequestAdd $("body").on "click", ".service-lockbtn", @on_analysis_lock_button_click # Analysis info button clicked $("body").on "click", ".service-infobtn", @on_analysis_details_click - # Sample changed - $("body").on "selected change", "tr[fieldname=PrimaryAnalysisRequest] input[type='text']", @on_sample_changed - # SampleType changed - $("body").on "selected change", "tr[fieldname=SampleType] input[type='text']", @on_sampletype_changed - # Specification changed - $("body").on "selected change", "tr[fieldname=Specification] input[type='text']", @on_specification_changed # Analysis Template changed $("body").on "selected change", "tr[fieldname=Template] input[type='text']", @on_analysis_template_changed # Analysis Profile selected @@ -189,6 +188,16 @@ class window.AnalysisRequestAdd $(@).trigger "settings:updated", settings + get_flush_settings: => + ### + * Retrieve the flush settings + ### + @ajax_post_form("get_flush_settings").done (settings) -> + console.debug "Flush settings:", settings + @flush_settings = settings + $(@).trigger "flush_settings:updated", settings + + recalculate_records: => ### * Submit all form values to the server to recalculate the records @@ -235,13 +244,15 @@ class window.AnalysisRequestAdd # set all values for one record (a single column in the AR Add form) $.each records, (arnum, record) -> - # set client - $.each record.client_metadata, (uid, client) -> - me.set_client arnum, client - - # set contact - $.each record.contact_metadata, (uid, contact) -> - me.set_contact arnum, contact + # Apply the values generically, but those to be handled differently + discard = ["service_metadata", "specification_metadata", "template_metadata"] + $.each record, (name, metadata) -> + # Discard those fields that will be handled differently and those that + # do not contain explicit object metadata (e.g service_to_specification) + if name in discard or !name.endsWith("_metadata") + return + $.each metadata, (uid, obj_info) -> + me.apply_field_value arnum, obj_info # set services $.each record.service_metadata, (uid, metadata) -> @@ -266,14 +277,6 @@ class window.AnalysisRequestAdd $.each spec.specifications, (uid, service_spec) -> me.set_service_spec arnum, uid, service_spec - # set sample - $.each record.sample_metadata, (uid, sample) -> - me.set_sample arnum, sample - - # set sampletype - $.each record.sampletype_metadata, (uid, sampletype) -> - me.set_sampletype arnum, sampletype - # handle unmet dependencies, one at a time $.each record.unmet_dependencies, (uid, dependencies) -> service = record.service_metadata[uid] @@ -369,9 +372,96 @@ class window.AnalysisRequestAdd return $(field_id) + typeIsArray = Array.isArray || (value) -> + ### + * Returns if the given value is an array + * Taken from: https://coffeescript-cookbook.github.io/chapters/arrays/check-type-is-array + ### + return {}.toString.call( value ) is '[object Array]' + + + apply_field_value: (arnum, record) -> + ### + * Applies the value for the given record, by setting values and applying + * search filters to dependents + ### + me = this + title = record.title + console.debug "apply_field_value: arnum=#{arnum} record=#{title}" + + # Set default values to dependents + me.apply_dependent_values arnum, record + + # Apply search filters to other fields + me.apply_dependent_filter_queries record, arnum + + + apply_dependent_values: (arnum, record) -> + ### + * Sets default field values to dependents + ### + me = this + $.each record.field_values, (field_name, values) -> + me.apply_dependent_value arnum, field_name, values + + + apply_dependent_value: (arnum, field_name, values) -> + ### + * Apply search filters to dependendents + ### + me = this + values_json = $.toJSON values + field = $("#" + field_name + "-#{arnum}") + + if values.if_empty? and values.if_empty is true + # Set the value if the field is empty only + if not field.val() + return + + console.debug "apply_dependent_value: field_name=#{field_name} field_values=#{values_json}" + + if values.uid? and values.title? + # This is a reference field + me.set_reference_field field, values.uid, values.title + + else if values.value? + # This is a normal input field + if typeof values.value == "boolean" + field.prop "checked", values.value + else + field.val values.value + + else if typeIsArray values + # This is a multi field (e.g. CCContact) + $.each values, (index, item) -> + me.apply_dependent_value arnum, field_name, item + + + apply_dependent_filter_queries: (record, arnum) -> + ### + * Apply search filters to dependents + ### + me = this + $.each record.filter_queries, (field_name, query) -> + field = $("#" + field_name + "-#{arnum}") + me.set_reference_field_query field, query + + + flush_fields_for: (field_name, arnum) -> + ### + * Flush dependant fields + ### + me = this + field_ids = @flush_settings[field_name] + $.each @flush_settings[field_name], (index, id) -> + console.debug "flushing: id=#{id}" + field = $("##{id}-#{arnum}") + me.flush_reference_field field + + flush_reference_field: (field) -> ### - * Empty the reference field + * Empty the reference field and restore the search query ### catalog_name = field.attr "catalog_name" @@ -382,6 +472,17 @@ class window.AnalysisRequestAdd $("input[type=hidden]", field.parent()).val("") $(".multiValued-listing", field.parent()).empty() + # restore the original search query + @reset_reference_field_query field + + reset_reference_field_query: (field) => + ### + * Restores the catalog search query for the given reference field + ### + catalog_name = field.attr "catalog_name" + return unless catalog_name + query = $.parseJSON field.attr "base_query" + @set_reference_field_query field, query set_reference_field_query: (field, query, type="base_query") => ### @@ -483,207 +584,6 @@ class window.AnalysisRequestAdd $field.val("") - set_client: (arnum, client) => - ### - * Filter Contacts - * Filter CCContacts - * Filter InvoiceContacts - * Filter SamplePoints - * Filter ARTemplates - * Filter Specification - * Filter SamplingRound - * Filter Batch - ### - - # filter Contacts - field = $("#Contact-#{arnum}") - query = client.filter_queries.contact - @set_reference_field_query field, query - - # handle default contact for /analysisrequests listing - # https://github.com/senaite/senaite.core/issues/705 - if document.URL.indexOf("analysisrequests") > -1 - contact_title = client.default_contact.title - contact_uid = client.default_contact.uid - if contact_title and contact_uid - @set_reference_field field, contact_uid, contact_title - - # filter CCContacts - field = $("#CCContact-#{arnum}") - query = client.filter_queries.cc_contact - @set_reference_field_query field, query - - # filter InvoiceContact - # XXX Where is this field? - field = $("#InvoiceContact-#{arnum}") - query = client.filter_queries.invoice_contact - @set_reference_field_query field, query - - # filter Sample Points - field = $("#SamplePoint-#{arnum}") - query = client.filter_queries.samplepoint - @set_reference_field_query field, query - - # filter AR Templates - field = $("#Template-#{arnum}") - query = client.filter_queries.artemplates - @set_reference_field_query field, query - - # filter Analysis Profiles - field = $("#Profiles-#{arnum}") - query = client.filter_queries.analysisprofiles - @set_reference_field_query field, query - - # filter Analysis Specs - field = $("#Specification-#{arnum}") - query = client.filter_queries.analysisspecs - @set_reference_field_query field, query - - # filter Samplinground - field = $("#SamplingRound-#{arnum}") - query = client.filter_queries.samplinground - @set_reference_field_query field, query - - # filter Sample - field = $("#PrimaryAnalysisRequest-#{arnum}") - query = client.filter_queries.sample - @set_reference_field_query field, query - - # filter Batch - field = $("#Batch-#{arnum}") - query = client.filter_queries.batch - @set_reference_field_query field, query - - - set_contact: (arnum, contact) => - ### - * Set CC Contacts - ### - me = this - - field = $("#CCContact-#{arnum}") - - $.each contact.cccontacts, (uid, cccontact) -> - fullname = cccontact.fullname - me.set_reference_field field, uid, fullname - - - set_sample: (arnum, sample) => - ### - * Apply the sample data to all fields of arnum - ### - - # set the client - field = $("#Client-#{arnum}") - uid = sample.client_uid - title = sample.client_title - @set_reference_field field, uid, title - - # set the client contact - field = $("#Contact-#{arnum}") - contact = sample.contact - uid = contact.uid - fullname = contact.fullname - @set_reference_field field, uid, fullname - @set_contact(arnum, contact) - - # set the sampling date - field = $("#SamplingDate-#{arnum}") - value = sample.sampling_date - field.val value - - # set the date sampled - field = $("#DateSampled-#{arnum}") - value = sample.date_sampled - field.val value - - # set the sample type (required) - field = $("#SampleType-#{arnum}") - uid = sample.sample_type_uid - title = sample.sample_type_title - @set_reference_field field, uid, title - - # set environmental conditions - field = $("#EnvironmentalConditions-#{arnum}") - value = sample.environmental_conditions - field.val value - - # set client sample ID - field = $("#ClientSampleID-#{arnum}") - value = sample.client_sample_id - field.val value - - # set client reference - field = $("#ClientReference-#{arnum}") - value = sample.client_reference - field.val value - - # set the client order number - field = $("#ClientOrderNumber-#{arnum}") - value = sample.client_order_number - field.val value - - # set composite - field = $("#Composite-#{arnum}") - field.prop "checked", sample.composite - - # set the sample condition - field = $("#SampleCondition-#{arnum}") - uid = sample.sample_condition_uid - title = sample.sample_condition_title - @set_reference_field field, uid, title - - # set the sample point - field = $("#SamplePoint-#{arnum}") - uid = sample.sample_point_uid - title = sample.sample_point_title - @set_reference_field field, uid, title - - # set the storage location - field = $("#StorageLocation-#{arnum}") - uid = sample.storage_location_uid - title = sample.storage_location_title - @set_reference_field field, uid, title - - # set the default container type - field = $("#DefaultContainerType-#{arnum}") - uid = sample.container_type_uid - title = sample.container_type_title - @set_reference_field field, uid, title - - # set the sampling deviation - field = $("#SamplingDeviation-#{arnum}") - uid = sample.sampling_deviation_uid - title = sample.sampling_deviation_title - @set_reference_field field, uid, title - - - set_sampletype: (arnum, sampletype) => - ### - * Recalculate partitions - * Filter Sample Points - ### - - # restrict the sample points - field = $("#SamplePoint-#{arnum}") - query = sampletype.filter_queries.samplepoint - @set_reference_field_query field, query - - # set the default container - field = $("#DefaultContainerType-#{arnum}") - # apply default container if the field is empty - if not field.val() - uid = sampletype.container_type_uid - title = sampletype.container_type_title - @flush_reference_field field - @set_reference_field field, uid, title - - # restrict the specifications - field = $("#Specification-#{arnum}") - query = sampletype.filter_queries.specification - @set_reference_field_query field, query - - set_template: (arnum, template) => ### * Apply the template data to all fields of arnum @@ -845,59 +745,29 @@ class window.AnalysisRequestAdd ### EVENT HANDLER ### - on_client_changed: (event) => + on_referencefield_value_changed: (event) => ### - * Eventhandler when the client changed (happens on Batches) + * Generic event handler for when a field value changes ### - me = this el = event.currentTarget $el = $(el) + has_value = $el.val() uid = $el.attr "uid" + field_name = $el.closest("tr[fieldname]").attr "fieldname" arnum = $el.closest("[arnum]").attr "arnum" + if field_name in ["Template", "Profiles"] + # These fields have it's own event handler + return - console.debug "°°° on_client_changed: arnum=#{arnum} °°°" - - # Flush client depending fields - field_ids = [ - "Contact" - "CCContact" - "InvoiceContact" - "SamplePoint" - "Template" - "Profiles" - "PrimaryAnalysisRequest" - "Specification" - "Batch" - ] - $.each field_ids, (index, id) -> - field = me.get_field_by_id id, arnum - me.flush_reference_field field - - # trigger form:changed event - $(me).trigger "form:changed" - - - on_contact_changed: (event) => - ### - * Eventhandler when the contact changed - ### - - me = this - el = event.currentTarget - $el = $(el) - uid = $el.attr "uid" - arnum = $el.closest("[arnum]").attr "arnum" + console.debug "°°° on_referencefield_value_changed: field_name=#{field_name} arnum=#{arnum} °°°" - console.debug "°°° on_contact_changed: arnum=#{arnum} °°°" + # Flush depending fields + me.flush_fields_for field_name, arnum - # Flush client depending fields - field_ids = [ - "CCContact" - ] - $.each field_ids, (index, id) -> - field = me.get_field_by_id id, arnum - me.flush_reference_field field + # Manually flush UID field if the field does not have a selected value + if not has_value + $("input[type=hidden]", $el.parent()).val("") # trigger form:changed event $(me).trigger "form:changed" @@ -944,7 +814,7 @@ class window.AnalysisRequestAdd if uid of record.service_to_profiles profiles = record.service_to_profiles[uid] $.each profiles, (index, uid) -> - extra["profiles"].push record.profile_metadata[uid] + extra["profiles"].push record.profiles_metadata[uid] # inject template info if uid of record.service_to_templates @@ -994,7 +864,7 @@ class window.AnalysisRequestAdd # collect profiles if uid of record.service_to_profiles profile_uid = record.service_to_profiles[uid] - context["profiles"].push record.profile_metadata[profile_uid] + context["profiles"].push record.profiles_metadata[profile_uid] # collect templates if uid of record.service_to_templates @@ -1008,85 +878,6 @@ class window.AnalysisRequestAdd dialog = @template_dialog "service-dependant-template", context, buttons - on_sample_changed: (event) => - ### - * Eventhandler when the Sample was changed. - ### - - me = this - el = event.currentTarget - $el = $(el) - uid = $(el).attr "uid" - val = $el.val() - arnum = $el.closest("[arnum]").attr "arnum" - has_sample_selected = $el.val() - console.debug "°°° on_sample_change::UID=#{uid} PrimaryAnalysisRequest=#{val}°°°" - - # deselect the sample if the field is empty - if not has_sample_selected - # XXX manually flush UID field - $("input[type=hidden]", $el.parent()).val("") - - # trigger form:changed event - $(me).trigger "form:changed" - - - on_sampletype_changed: (event) => - ### - * Eventhandler when the SampleType was changed. - * Fires form:changed event - ### - - me = this - el = event.currentTarget - $el = $(el) - uid = $(el).attr "uid" - val = $el.val() - arnum = $el.closest("[arnum]").attr "arnum" - has_sampletype_selected = $el.val() - console.debug "°°° on_sampletype_change::UID=#{uid} SampleType=#{val}°°°" - - # deselect the sampletype if the field is empty - if not has_sampletype_selected - # XXX manually flush UID field - $("input[type=hidden]", $el.parent()).val("") - - # Flush sampletype depending fields - field_ids = [ - "SamplePoint" - "Specification" - ] - $.each field_ids, (index, id) -> - field = me.get_field_by_id id, arnum - me.flush_reference_field field - - # trigger form:changed event - $(me).trigger "form:changed" - - - on_specification_changed: (event) => - ### - * Eventhandler when the Specification was changed. - ### - - me = this - el = event.currentTarget - $el = $(el) - uid = $(el).attr "uid" - val = $el.val() - arnum = $el.closest("[arnum]").attr "arnum" - has_specification_selected = $el.val() - console.debug "°°° on_specification_change::UID=#{uid} Specification=#{val}°°°" - - # deselect the specification if the field is empty - if not has_specification_selected - # XXX manually flush UID field - $("input[type=hidden]", $el.parent()).val("") - - # trigger form:changed event - $(me).trigger "form:changed" - - on_analysis_template_changed: (event) => ### * Eventhandler when an Analysis Template was changed. @@ -1218,7 +1009,7 @@ class window.AnalysisRequestAdd arnum = $el.closest("[arnum]").attr "arnum" record = @records_snapshot[arnum] - profile_metadata = record.profile_metadata[uid] + profile_metadata = record.profiles_metadata[uid] profile_services = [] # prepare a list of services used by the profile with the given UID diff --git a/bika/lims/interfaces/__init__.py b/bika/lims/interfaces/__init__.py index 9e80020f2e..684d0afaf5 100644 --- a/bika/lims/interfaces/__init__.py +++ b/bika/lims/interfaces/__init__.py @@ -961,3 +961,45 @@ class IGuardAdapter(Interface): def guard(self, transition): """Return False if you want to block the transition """ + + +class IAddSampleFieldsFlush(Interface): + """Marker interface for field dependencies flush for Add Sample form + """ + + def get_flush_settings(self): + """Returns a dict where the key is the name of the field and the value + is an array dependencies as field names + """ + + +class IAddSampleObjectInfo(Interface): + """Marker interface for objects metadata mapping + """ + + def get_object_info(self): + """Returns the dict representation of the context object for its + correct consumption by Sample Add form: + + {'id': , + 'uid': , + 'title': , + 'filter_queries': { + : { + : + } + }, + 'field_values': { + : { + : , + : <dependent_title> + } + } + + Besides the basic keys (id, uid, title), two additional keys can be + provided: + - filter_queries: contains the filter queries for other fields to be + applied when the value of current field changes. + - field_values: contains default values for other fields to be applied + when the value of the current field changes. + """ \ No newline at end of file