diff --git a/CHANGES.rst b/CHANGES.rst index 50af7bd438..af6d35d834 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,7 @@ Changelog 2.4.0 (unreleased) ------------------ +- #2219 Make `UIDReferenceField` to not keep back-references by default - #2209 Migrate AnalysisRequest's ReferenceField fields to UIDReferenceField - #2218 Improve performance of legacy AT `UIDReferenceField`'s setter - #2214 Remove `DefaultContainerType` field (stale) from AnalysisRequest diff --git a/src/bika/lims/browser/fields/uidreferencefield.py b/src/bika/lims/browser/fields/uidreferencefield.py index d7376cfa0b..980689d005 100644 --- a/src/bika/lims/browser/fields/uidreferencefield.py +++ b/src/bika/lims/browser/fields/uidreferencefield.py @@ -39,9 +39,8 @@ class ReferenceException(Exception): class UIDReferenceField(StringField): """A field that stores References as UID values. This acts as a drop-in - replacement for Archetypes' ReferenceField. A relationship is required - but if one is not provided, it will be composed from a concatenation - of `portal_type` + `fieldname`. + replacement for Archetypes' ReferenceField. If no relationship is provided, + the field won't keep backreferences in referenced objects """ _properties = Field._properties.copy() _properties.update({ @@ -55,6 +54,16 @@ class UIDReferenceField(StringField): security = ClassSecurityInfo() + @property + def keep_backreferences(self): + """Returns whether this field must keep back references. Returns False + if the value for property relationship is None or empty + """ + relationship = getattr(self, "relationship", None) + if relationship and isinstance(relationship, six.string_types): + return True + return False + def get_relationship_key(self, context): """Return the configured relationship key or generate a new one """ @@ -231,7 +240,8 @@ def set(self, context, value, **kwargs): uids = filter(api.is_uid, uids) # Back-reference current object to referenced objects - self._set_backreferences(context, uids, **kwargs) + if self.keep_backreferences: + self._set_backreferences(context, uids, **kwargs) # Store the referenced objects as uids if not self.multiValued: diff --git a/src/bika/lims/content/abstractanalysis.py b/src/bika/lims/content/abstractanalysis.py index 94701f9dbf..897000ad3a 100644 --- a/src/bika/lims/content/abstractanalysis.py +++ b/src/bika/lims/content/abstractanalysis.py @@ -66,7 +66,8 @@ Attachment = UIDReferenceField( 'Attachment', multiValued=1, - allowed_types=('Attachment',) + allowed_types=('Attachment',), + relationship='AnalysisAttachment' ) # The final result of the analysis is stored here. The field contains a @@ -88,7 +89,8 @@ # Returns the retracted analysis this analysis is a retest of RetestOf = UIDReferenceField( - 'RetestOf' + 'RetestOf', + relationship="AnalysisRetestOf", ) # If the result is outside of the detection limits of the method or instrument, @@ -1127,7 +1129,7 @@ def getRetestOfUID(self): def getRawRetest(self): """Returns the UID of the retest that comes from this analysis, if any """ - relationship = "{}RetestOf".format(self.portal_type) + relationship = self.getField("RetestOf").relationship uids = get_backreferences(self, relationship) if not uids: return None diff --git a/src/bika/lims/content/analysisrequest.py b/src/bika/lims/content/analysisrequest.py index 3ede64d2a4..6e6edad4d0 100644 --- a/src/bika/lims/content/analysisrequest.py +++ b/src/bika/lims/content/analysisrequest.py @@ -230,7 +230,6 @@ 'Client', required=1, allowed_types=('Client',), - relationship='AnalysisRequestClient', mode="rw", read_permission=View, write_permission=FieldEditClient, diff --git a/src/bika/lims/content/analysisservice.py b/src/bika/lims/content/analysisservice.py index 59a7c18d1e..ff6fce967c 100644 --- a/src/bika/lims/content/analysisservice.py +++ b/src/bika/lims/content/analysisservice.py @@ -98,6 +98,7 @@ "Calculation", schemata="Method", required=0, + relationship="AnalysisServiceCalculation", vocabulary="_default_calculation_vocabulary", allowed_types=("Calculation", ), accessor="getRawCalculation", diff --git a/src/bika/lims/content/calculation.py b/src/bika/lims/content/calculation.py index 121addfc34..3ad3ec7a3e 100644 --- a/src/bika/lims/content/calculation.py +++ b/src/bika/lims/content/calculation.py @@ -71,6 +71,7 @@ 'DependentServices', required=1, multiValued=1, + relationship="CalculationDependentServices", allowed_types=('AnalysisService',), widget=ReferenceWidget( checkbox_bound=0, diff --git a/src/senaite/core/profiles/default/metadata.xml b/src/senaite/core/profiles/default/metadata.xml index fd78db8d8c..cff0994311 100644 --- a/src/senaite/core/profiles/default/metadata.xml +++ b/src/senaite/core/profiles/default/metadata.xml @@ -1,6 +1,6 @@ - 2408 + 2410 profile-Products.ATContentTypes:base profile-Products.CMFEditions:CMFEditions diff --git a/src/senaite/core/upgrade/v02_04_000.py b/src/senaite/core/upgrade/v02_04_000.py index fa4ecf6cd2..fc043f5775 100644 --- a/src/senaite/core/upgrade/v02_04_000.py +++ b/src/senaite/core/upgrade/v02_04_000.py @@ -23,6 +23,8 @@ from bika.lims import api from bika.lims import LDL from bika.lims import UDL +from bika.lims.browser.fields import UIDReferenceField +from bika.lims.browser.fields.uidreferencefield import get_storage from bika.lims.interfaces import IRejected from bika.lims.interfaces import IRetracted from Products.Archetypes.config import REFERENCE_CATALOG @@ -304,7 +306,7 @@ def migrate_reference_fields(obj, field_names): # Get the relationship id from field field = obj.getField(field_name) - ref_id = getattr(field, "relationship", False) + ref_id = field.get_relationship_key(obj) if not ref_id: logger.error("No relationship for field {}".format(field_name)) @@ -323,3 +325,115 @@ def migrate_reference_fields(obj, field_names): # Remove this relationship from reference catalog ref_tool.deleteReferences(obj, relationship=ref_id) + + +def rename_retestof_relationship(tool): + """Renames the relationship for field RetestOf from the format + 'RetestOf' to 'AnalysisRetestOf'. This field is inherited by + different analysis-like types and since we now assume that if no + relationship is explicitly set, UIDReferenceField does not keep + back-references, we need to update the relationship for those objects that + are not from 'Analysis' portal_type + """ + logger.info("Rename RetestOf relationship ...") + uc = api.get_tool("uid_catalog") + portal_types = ["DuplicateAnalysis", "ReferenceAnalysis", "RejectAnalysis"] + brains = uc(portal_type=portal_types) + total = len(brains) + for num, brain in enumerate(brains): + if num and num % 100 == 0: + logger.info("Rename RetestOf relationship {}/{}" + .format(num, total)) + + if num and num % 1000 == 0: + transaction.savepoint() + + # find out if the current analysis is a retest + obj = api.get_object(brain) + field = obj.getField("RetestOf") + retest_of = field.get(obj) + if retest_of: + # remove the back-reference with the old relationship name + portal_type = api.get_portal_type(obj) + old_relationship_key = "{}RetestOf".format(portal_type) + back_storage = get_storage(retest_of) + back_storage.pop(old_relationship_key, None) + + # re-link referenced object with the new relationship name + field.link_reference(retest_of, obj) + + # Flush the object from memory + obj._p_deactivate() + + logger.info("Rename RetestOf relationship [DONE]") + + +def purge_backreferences(tool): + """Purges back-references that are no longer required + """ + logger.info("Purge no longer required back-references ...") + portal_types = [ + "Analysis", + "AnalysisRequest", + "AnalysisService", + "AnalysisSpec", + "ARReport", + "Batch", + "Calculation", + "DuplicateAnalysis", + "Instrument", + "LabContact", + "Laboratory", + "Method", + "ReferenceAnalysis", + "RejectAnalysis" + "Worksheet", + ] + + uc = api.get_tool("uid_catalog") + brains = uc(portal_type=portal_types) + total = len(brains) + for num, obj in enumerate(brains): + if num and num % 100 == 0: + logger.info("Processed objects: {}/{}".format(num, total)) + + if num and num % 1000 == 0: + # reduce memory size of the transaction + transaction.savepoint() + + # Migrate the reference fields for current sample + obj = api.get_object(obj) + purge_backreferences_to(obj) + + # Flush the object from memory + obj._p_deactivate() + + logger.info("Purge no longer required back-references [DONE]") + + +def purge_backreferences_to(obj): + """Removes back-references that are no longer needed that point to the + given object + """ + fields = api.get_fields(obj) + portal_type = api.get_portal_type(obj) + + for field_name, field in fields.items(): + if not isinstance(field, UIDReferenceField): + continue + + # Only purge if back-references are not required + if field.keep_backreferences: + continue + + # Get the referenced objects + references = field.get(obj) + if not isinstance(references, (list, tuple)): + references = [references] + + # Remove the back-references from these referenced objects to current + relationship = "{}{}".format(portal_type, field.getName()) + references = filter(None, references) + for reference in references: + back_storage = get_storage(reference) + back_storage.pop(relationship, None) diff --git a/src/senaite/core/upgrade/v02_04_000.zcml b/src/senaite/core/upgrade/v02_04_000.zcml index 4f5c21272a..24d7be8fb8 100644 --- a/src/senaite/core/upgrade/v02_04_000.zcml +++ b/src/senaite/core/upgrade/v02_04_000.zcml @@ -83,4 +83,24 @@ handler="senaite.core.upgrade.v02_04_000.migrate_analysisrequest_referencefields" profile="senaite.core:default"/> + + + + + +