diff --git a/CHANGES.rst b/CHANGES.rst index 15f7305f92..80c37c4129 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,7 @@ Changelog 1.3.6 (unreleased) ------------------ +- #2324 Migrate AnalysisRequest's ReferenceField to UIDReferenceField (#2209 port) - #2323 Remove default_method from AnalysisRequest's Contact field (#2208 port) - #2322 Cleanup getUID functions to prevent unnecessary searches (#2163 port) - #2321 Improve the creation process of AT content types (#2150 port) diff --git a/bika/lims/browser/fields/uidreferencefield.py b/bika/lims/browser/fields/uidreferencefield.py index 362a68b29e..d665ce1963 100644 --- a/bika/lims/browser/fields/uidreferencefield.py +++ b/bika/lims/browser/fields/uidreferencefield.py @@ -153,18 +153,27 @@ def get(self, context, **kwargs): :return: object or list of objects for multiValued fields. :rtype: BaseContent | list[BaseContent] """ - value = StringField.get(self, context, **kwargs) - if not value: - return [] if self.multiValued else None + uids = StringField.get(self, context, **kwargs) + if not isinstance(uids, list): + uids = [uids] + + # Do a direct search for all brains at once + uc = api.get_tool("uid_catalog") + references = uc(UID=uids) + + # Keep the original order of items + references = sorted(references, key=lambda it: uids.index(it.UID)) + + # Return objects by default + full_objects = kwargs.pop("full_objects", True) + if full_objects: + references = [api.get_object(ref) for ref in references] + if self.multiValued: - # Only return objects which actually exist; this is necessary here - # because there are no HoldingReferences. This opens the - # possibility that deletions leave hanging references. - ret = filter( - lambda x: x, [self.get_object(context, uid) for uid in value]) - else: - ret = self.get_object(context, value) - return ret + return references + elif references: + return references[0] + return None @security.public def getRaw(self, context, aslist=False, **kwargs): diff --git a/bika/lims/content/analysisrequest.py b/bika/lims/content/analysisrequest.py index 1e26cd24b1..ce342c8ea3 100644 --- a/bika/lims/content/analysisrequest.py +++ b/bika/lims/content/analysisrequest.py @@ -149,8 +149,6 @@ label=_("Contact"), render_own_label=True, size=20, - helper_js=("bika_widgets/referencewidget.js", - "++resource++bika.lims.js/contact.js"), description=_("The primary contact of this sample, " "who will receive notifications and publications " "via email"), @@ -175,10 +173,9 @@ ), ), - ReferenceField( + UIDReferenceField( 'CCContact', multiValued=1, - vocabulary_display_path_bound=sys.maxsize, allowed_types=('Contact',), referenceClass=HoldingReference, relationship='AnalysisRequestCCContact', @@ -227,7 +224,7 @@ ), ), - ReferenceField( + UIDReferenceField( 'Client', required=1, allowed_types=('Client',), @@ -256,7 +253,7 @@ # Field for the creation of Secondary Analysis Requests. # This field is meant to be displayed in AR Add form only. A viewlet exists # to inform the user this Analysis Request is secondary - ReferenceField( + UIDReferenceField( "PrimaryAnalysisRequest", allowed_types=("AnalysisRequest",), referenceClass=HoldingReference, @@ -296,7 +293,7 @@ ) ), - ReferenceField( + UIDReferenceField( 'Batch', allowed_types=('Batch',), relationship='AnalysisRequestBatch', @@ -327,13 +324,13 @@ {'columnName': 'getClientTitle', 'width': '30', 'label': _('Client'), 'align': 'left'}, ], - force_all = False, + force_all=False, ui_item="getId", showOn=True, ), ), - ReferenceField( + UIDReferenceField( 'SubGroup', required=False, allowed_types=('SubGroup',), @@ -366,7 +363,7 @@ ), ), - ReferenceField( + UIDReferenceField( 'Template', allowed_types=('ARTemplate',), referenceClass=HoldingReference, @@ -376,8 +373,10 @@ write_permission=FieldEditTemplate, widget=ReferenceWidget( label=_("Sample Template"), - description=_("The predefined values of the Sample template are set " - "in the request"), + description=_( + "The predefined values of the Sample template are set in the " + "request" + ), size=20, render_own_label=True, visible={ @@ -392,35 +391,11 @@ ), ), - # TODO Remove Profile field (in singular) - ReferenceField( - 'Profile', - allowed_types=('AnalysisProfile',), - referenceClass=HoldingReference, - relationship='AnalysisRequestAnalysisProfile', - mode="rw", - read_permission=View, - write_permission=ModifyPortalContent, - widget=ReferenceWidget( - label=_("Analysis Profile"), - description=_("Analysis profiles apply a certain set of analyses"), - size=20, - render_own_label=True, - visible=False, - catalog_name='bika_setup_catalog', - base_query={"is_active": True, - "sort_on": "sortable_title", - "sort_order": "ascending"}, - showOn=False, - ), - ), - - ReferenceField( + UIDReferenceField( 'Profiles', multiValued=1, allowed_types=('AnalysisProfile',), referenceClass=HoldingReference, - vocabulary_display_path_bound=sys.maxsize, relationship='AnalysisRequestAnalysisProfiles', mode="rw", read_permission=View, @@ -584,7 +559,8 @@ ), ), - DateTimeField('DatePreserved', + DateTimeField( + "DatePreserved", mode="rw", read_permission=View, write_permission=FieldEditDatePreserved, @@ -600,7 +576,8 @@ }, ), ), - StringField('Preserver', + StringField( + "Preserver", required=0, mode="rw", read_permission=View, @@ -618,7 +595,8 @@ ), ), # TODO Sample cleanup - This comes from partition - DurationField('RetentionPeriod', + DurationField( + "RetentionPeriod", required=0, mode="r", read_permission=View, @@ -643,7 +621,7 @@ ), ), - ReferenceField( + UIDReferenceField( 'Specification', required=0, primary_bound=True, # field changes propagate to partitions @@ -695,7 +673,7 @@ widget=ComputedWidget(visible=False), ), - ReferenceField( + UIDReferenceField( 'PublicationSpecification', required=0, allowed_types='AnalysisSpec', @@ -706,7 +684,8 @@ widget=ReferenceWidget( label=_("Publication Specification"), description=_( - "Set the specification to be used before publishing a Sample."), + "Set the specification to be used before publishing a Sample." + ), size=20, render_own_label=True, visible={ @@ -898,7 +877,7 @@ ), # TODO Remove - Is this still necessary? - ReferenceField( + UIDReferenceField( 'DefaultContainerType', allowed_types=('ContainerType',), relationship='AnalysisRequestContainerType', @@ -970,7 +949,7 @@ ), ), - ReferenceField( + UIDReferenceField( 'Attachment', multiValued=1, allowed_types=('Attachment',), @@ -1005,9 +984,8 @@ ) ), - ReferenceField( + UIDReferenceField( 'Invoice', - vocabulary_display_path_bound=sys.maxsize, allowed_types=('Invoice',), referenceClass=HoldingReference, relationship='AnalysisRequestInvoice', @@ -1246,7 +1224,7 @@ ), # The Primary Sample the current sample was detached from - ReferenceField( + UIDReferenceField( "DetachedFrom", allowed_types=("AnalysisRequest",), relationship="AnalysisRequestDetachedFrom", @@ -1261,7 +1239,7 @@ # The Analysis Request the current Analysis Request comes from because of # an invalidation of the former - ReferenceField( + UIDReferenceField( 'Invalidated', allowed_types=('AnalysisRequest',), relationship='AnalysisRequestRetracted', @@ -1274,14 +1252,6 @@ ), ), - # The Analysis Request that was automatically generated due to the - # invalidation of the current Analysis Request - ComputedField( - 'Retest', - expression="here.get_retest()", - widget=ComputedWidget(visible=False) - ), - # For comments or results interpretation # Old one, to be removed because of the incorporation of # ResultsInterpretationDepts (due to LIMS-1628) @@ -1350,7 +1320,7 @@ "clients."), format="radio", render_own_label=True, - visible={'add': 'edit',} + visible={'add': 'edit'} ), ), ) @@ -1669,7 +1639,7 @@ def getBillableItems(self): # Keywords of the contained services billable_service_keys = map( lambda s: s.getKeyword(), set(billable_profile_services)) - # The billable items contain billable profiles and single selected analyses + # Billable items contain billable profiles and single selected analyses billable_items = billable_profiles # Get the analyses to be billed exclude_rs = ["retracted", "rejected"] @@ -2002,10 +1972,10 @@ def getAnalysisServiceSettings(self, uid): sets = [adv] if 'hidden' in adv else [] # Created by using an AR Profile? - if not sets and self.getProfiles(): - adv = [] - adv += [profile.getAnalysisServiceSettings(uid) for profile in - self.getProfiles()] + profiles = self.getProfiles() + if not sets and profiles: + adv = [profile.getAnalysisServiceSettings(uid) for profile in + profiles] sets = adv if 'hidden' in adv[0] else [] return sets[0] if sets else {'uid': uid} @@ -2151,15 +2121,27 @@ def set_ARAttachment(self, value): "It can not hold an own value!") return None + def getRawRetest(self): + """Returns the UID of the Analysis Request that has been generated + automatically because of the retraction of the current Analysis Request + """ + relationship = self.getField("Invalidated").relationship + uids = get_backreferences(self, relationship=relationship) + return uids[0] if uids else None + + @deprecated("Use getRetest instead") def get_retest(self): - """Returns the Analysis Request automatically generated because of the - retraction of the current analysis request + """Returns the Analysis Request that has been generated automatically + because of the retraction of the current Analysis Request + """ + return self.getRetest() + + def getRetest(self): + """Returns the Analysis Request that has been generated automatically + because of the retraction of the current Analysis Request """ - relationship = "AnalysisRequestRetracted" - retest = self.getBackReferences(relationship=relationship) - if retest and len(retest) > 1: - logger.warn("More than one retest for {0}".format(self.getId())) - return retest and retest[0] or None + uid = self.getRawRetest() + return api.get_object_by_uid(uid, default=None) def getAncestors(self, all_ancestors=True): """Returns the ancestor(s) of this Analysis Request @@ -2208,8 +2190,8 @@ def getDescendantsUIDs(self): This method is used as metadata """ - rel_id = "AnalysisRequestParentAnalysisRequest" - return get_backreferences(self, relationship=rel_id) + relationship = self.getField("ParentAnalysisRequest").relationship + return get_backreferences(self, relationship=relationship) def isPartition(self): """Returns true if this Analysis Request is a partition @@ -2246,11 +2228,19 @@ def setParentAnalysisRequest(self, value): parent = self.getParentAnalysisRequest() alsoProvides(parent, IAnalysisRequestWithPartitions) + def getRawSecondaryAnalysisRequests(self): + """Returns the UIDs of the secondary Analysis Requests from this + Analysis Request + """ + relationship = self.getField("PrimaryAnalysisRequest").relationship + return get_backreferences(self, relationship) + def getSecondaryAnalysisRequests(self): """Returns the secondary analysis requests from this analysis request """ - relationship = "AnalysisRequestPrimaryAnalysisRequest" - return self.getBackReferences(relationship=relationship) + uids = self.getRawSecondaryAnalysisRequests() + uc = api.get_tool("uid_catalog") + return [api.get_object(brain) for brain in uc(UID=uids)] def setDateReceived(self, value): """Sets the date received to this analysis request and to secondary @@ -2394,10 +2384,10 @@ def process_inline_images(self, html): # convert relative URLs to absolute URLs # N.B. This is actually a TinyMCE issue, but hardcoded in Plone: - # https://www.tiny.cloud/docs/configure/url-handling/#relative_urls + # https://www.tiny.cloud/docs/configure/url-handling/#relative_urls image_sources = re.findall(IMG_SRC_RX, html) - # we need a trailing slash so that urljoin does not remove the last segment + # add a trailing slash so that urljoin doesn't remove the last segment base_url = "{}/".format(api.get_url(self)) for src in image_sources: diff --git a/bika/lims/content/attachment.py b/bika/lims/content/attachment.py index 67c100a623..105abde637 100644 --- a/bika/lims/content/attachment.py +++ b/bika/lims/content/attachment.py @@ -143,10 +143,9 @@ def getLinkedRequests(self): :returns: sorted list of ARs, where the latest AR comes first """ - rc = api.get_tool("reference_catalog") - refs = rc.getBackReferences(self, "AnalysisRequestAttachment") + uids = get_backreferences(self, "AnalysisRequestAttachment") # fetch the objects by UID and handle nonexisting UIDs gracefully - ars = map(lambda ref: api.get_object_by_uid(ref.sourceUID, None), refs) + ars = map(lambda uid: api.get_object_by_uid(uid, None), uids) # filter out None values (nonexisting UIDs) ars = filter(None, ars) # sort by physical path, so that attachments coming from an AR with a @@ -161,9 +160,9 @@ def getLinkedAnalyses(self): :returns: sorted list of ANs, where the latest AN comes first """ # Fetch the linked Analyses UIDs - refs = get_backreferences(self, "AnalysisAttachment") + uids = get_backreferences(self, "AnalysisAttachment") # fetch the objects by UID and handle nonexisting UIDs gracefully - ans = map(lambda uid: api.get_object_by_uid(uid, None), refs) + ans = map(lambda uid: api.get_object_by_uid(uid, None), uids) # filter out None values (nonexisting UIDs) ans = filter(None, ans) # sort by physical path, so that attachments coming from an AR with a diff --git a/bika/lims/exportimport/setupdata/__init__.py b/bika/lims/exportimport/setupdata/__init__.py index 2e877711a9..c0e2036c09 100644 --- a/bika/lims/exportimport/setupdata/__init__.py +++ b/bika/lims/exportimport/setupdata/__init__.py @@ -2292,13 +2292,13 @@ def Import(self): getFullname=row['CCContact_Fullname'])[0].getObject() obj.setCCContact(contact) if row['AnalysisProfile_title']: - profile = pc(portal_type="AnalysisProfile", - title=row['AnalysisProfile_title'].getObject()) - obj.setProfile(profile) + profiles = pc(portal_type="AnalysisProfile", + title=row['AnalysisProfile_title'])[0].getObject() + obj.setProfiles([profiles]) if row['ARTemplate_title']: template = pc(portal_type="ARTemplate", - title=row['ARTemplate_title'])[0].getObject() - obj.setProfile(template) + title=row['ARTemplate_title'])[0].getObject() + obj.setTemplate(template) obj.unmarkCreationFlag() diff --git a/bika/lims/idserver.py b/bika/lims/idserver.py index 5f49edaa13..2557e7e55b 100644 --- a/bika/lims/idserver.py +++ b/bika/lims/idserver.py @@ -179,8 +179,7 @@ def get_secondary_count(context, default=0): if not primary: return default - - return len(primary.getSecondaryAnalysisRequests()) + return len(primary.getRawSecondaryAnalysisRequests()) def is_ar(context): diff --git a/bika/lims/profiles/default/metadata.xml b/bika/lims/profiles/default/metadata.xml index c421b7ef2d..0b57ccc9a8 100644 --- a/bika/lims/profiles/default/metadata.xml +++ b/bika/lims/profiles/default/metadata.xml @@ -1,6 +1,6 @@ - 1.3.6 + 13601 profile-jarn.jsi18n:default profile-Products.ATExtensions:default diff --git a/bika/lims/upgrade/configure.zcml b/bika/lims/upgrade/configure.zcml index 7ecd4d1279..23c56fb219 100644 --- a/bika/lims/upgrade/configure.zcml +++ b/bika/lims/upgrade/configure.zcml @@ -3,6 +3,9 @@ xmlns:genericsetup="http://namespaces.zope.org/genericsetup" i18n_domain="senaite.core"> + + + - - diff --git a/bika/lims/upgrade/v01_03_006.py b/bika/lims/upgrade/v01_03_006.py index 0d41b4a5f6..f9450e7321 100644 --- a/bika/lims/upgrade/v01_03_006.py +++ b/bika/lims/upgrade/v01_03_006.py @@ -174,3 +174,75 @@ def is_orphan(uid): """ obj = api.get_object_by_uid(uid, None) return obj is None + + +def migrate_analysisrequest_referencefields(tool): + """Migrates the ReferenceField from AnalysisRequest to UIDReferenceField + """ + logger.info("Migrate ReferenceFields to UIDReferenceField ...") + field_names = [ + "Attachment", + "Batch", + "CCContact", + "Client", + "DefaultContainerType", + "DetachedFrom", + "Invalidated", + "Invoice", + "PrimaryAnalysisRequest", + "Profile", + "Profiles", + "PublicationSpecification", + "Specification", + "SubGroup", + "Template", + ] + + cat = api.get_tool(CATALOG_ANALYSIS_REQUEST_LISTING) + brains = cat(portal_type="AnalysisRequest") + total = len(brains) + for num, sample in enumerate(brains): + if num and num % 100 == 0: + logger.info("Processed samples: {}/{}".format(num, total)) + + if num and num % 1000 == 0: + # reduce memory size of the transaction + transaction.savepoint() + + # Migrate the reference fields for current sample + sample = api.get_object(sample) + migrate_reference_fields(sample, field_names) + + # Flush the object from memory + sample._p_deactivate() + + logger.info("Migrate ReferenceFields to UIDReferenceField [DONE]") + + +def migrate_reference_fields(obj, field_names): + """Migrates the reference fields with the names specified from the obj + """ + ref_tool = api.get_tool(REFERENCE_CATALOG) + for field_name in field_names: + + # Get the relationship id from field + field = obj.getField(field_name) + ref_id = getattr(field, "relationship", False) + if not ref_id: + logger.error("No relationship for field {}".format(field_name)) + + # Extract the referenced objects + references = obj.getRefs(relationship=ref_id) + if not references: + # Processed already or no referenced objects + continue + + # Re-assign the object directly to the field + if field.multiValued: + value = [api.get_uid(val) for val in references] + else: + value = api.get_uid(references[0]) + field.set(obj, value) + + # Remove this relationship from reference catalog + ref_tool.deleteReferences(obj, relationship=ref_id) diff --git a/bika/lims/upgrade/v01_03_006.zcml b/bika/lims/upgrade/v01_03_006.zcml new file mode 100644 index 0000000000..e309dccbc6 --- /dev/null +++ b/bika/lims/upgrade/v01_03_006.zcml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file