Passed
Push — 2.x ( 2aa223...829bc3 )
by Ramon
06:01
created

bika.lims.browser.fields.uidreferencefield   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 305
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 40
eloc 146
dl 0
loc 305
rs 9.2
c 0
b 0
f 0

3 Functions

Rating   Name   Duplication   Size   Complexity  
A _get_catalog_for_uid() 0 15 2
A get_storage() 0 5 2
A get_backreferences() 0 39 4

9 Methods

Rating   Name   Duplication   Size   Complexity  
A UIDReferenceField.set() 0 28 5
A UIDReferenceField.link_reference() 0 14 3
A UIDReferenceField.get_uid() 0 11 2
A UIDReferenceField._set_backreferences() 0 37 5
A UIDReferenceField.unlink_reference() 0 20 3
A UIDReferenceField.getRaw() 0 17 3
B UIDReferenceField.get() 0 32 6
A UIDReferenceField.keep_backreferences() 0 9 3
A UIDReferenceField.get_relationship_key() 0 6 2

How to fix   Complexity   

Complexity

Complex classes like bika.lims.browser.fields.uidreferencefield often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of SENAITE.CORE.
4
#
5
# SENAITE.CORE is free software: you can redistribute it and/or modify it under
6
# the terms of the GNU General Public License as published by the Free Software
7
# Foundation, version 2.
8
#
9
# This program is distributed in the hope that it will be useful, but WITHOUT
10
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
# details.
13
#
14
# You should have received a copy of the GNU General Public License along with
15
# this program; if not, write to the Free Software Foundation, Inc., 51
16
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
#
18
# Copyright 2018-2021 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import six
22
23
from AccessControl import ClassSecurityInfo
24
from bika.lims import APIError
25
from Products.Archetypes.Field import Field, StringField
26
from bika.lims import logger
27
from bika.lims import api
28
from bika.lims.interfaces.field import IUIDReferenceField
29
from persistent.list import PersistentList
30
from persistent.dict import PersistentDict
31
from zope.annotation.interfaces import IAnnotations
32
from zope.interface import implements
33
34
BACKREFS_STORAGE = "bika.lims.browser.fields.uidreferencefield.backreferences"
35
36
37
class ReferenceException(Exception):
38
    pass
39
40
41
class UIDReferenceField(StringField):
42
    """A field that stores References as UID values.  This acts as a drop-in
43
    replacement for Archetypes' ReferenceField. If no relationship is provided,
44
    the field won't keep backreferences in referenced objects
45
    """
46
    _properties = Field._properties.copy()
47
    _properties.update({
48
        'type': 'uidreference',
49
        'default': '',
50
        'default_content_type': 'text/plain',
51
        'relationship': '',
52
    })
53
54
    implements(IUIDReferenceField)
55
56
    security = ClassSecurityInfo()
57
58
    @property
59
    def keep_backreferences(self):
60
        """Returns whether this field must keep back references. Returns False
61
        if the value for property relationship is None or empty
62
        """
63
        relationship = getattr(self, "relationship", None)
64
        if relationship and isinstance(relationship, six.string_types):
65
            return True
66
        return False
67
68
    def get_relationship_key(self, context):
69
        """Return the configured relationship key or generate a new one
70
        """
71
        if not self.relationship:
72
            return context.portal_type + self.getName()
73
        return self.relationship
74
75
    def link_reference(self, source, target):
76
        """Link the target to the source
77
        """
78
        target_uid = api.get_uid(target)
79
        # get the annotation storage key
80
        key = self.get_relationship_key(target)
81
        # get all backreferences from the source
82
        # N.B. only like this we get the persistent mapping!
83
        backrefs = get_backreferences(source, relationship=None)
84
        if key not in backrefs:
85
            backrefs[key] = PersistentList()
86
        if target_uid not in backrefs[key]:
87
            backrefs[key].append(target_uid)
88
        return True
89
90
    def unlink_reference(self, source, target):
91
        """Unlink the target from the source
92
        """
93
        target_uid = api.get_uid(target)
94
        # get the storage key
95
        key = self.get_relationship_key(target)
96
        # get all backreferences from the source
97
        # N.B. only like this we get the persistent mapping!
98
        backrefs = get_backreferences(source, relationship=None)
99
        if key not in backrefs:
100
            logger.warn(
101
                "Referenced object {} has no backreferences for the key {}"
102
                .format(repr(source), key))
103
            return False
104
        if target_uid not in backrefs[key]:
105
            logger.warn("Target {} was not linked by {}"
106
                        .format(repr(target), repr(source)))
107
            return False
108
        backrefs[key].remove(target_uid)
109
        return True
110
111
    @security.public
112
    def get_uid(self, value):
113
        """Takes a brain or object (or UID), and returns a UID
114
        :param value: Brain, object, or UID
115
        :type value: Any
116
        :return: resolved UID
117
        """
118
        try:
119
            return api.get_uid(value)
120
        except APIError:
121
            return None
122
123
    @security.public
124
    def get(self, context, **kwargs):
125
        """Grab the stored value, and resolve object(s) from UID catalog.
126
127
        :param context: context is the object who's schema contains this field.
128
        :type context: BaseContent
129
        :param kwargs: kwargs are passed directly to the underlying get.
130
        :type kwargs: dict
131
        :return: object or list of objects for multiValued fields.
132
        :rtype: BaseContent | list[BaseContent]
133
        """
134
        uids = StringField.get(self, context, **kwargs)
135
        if not isinstance(uids, list):
136
            uids = [uids]
137
138
        # Do a direct search for all brains at once
139
        uc = api.get_tool("uid_catalog")
140
        references = uc(UID=uids)
141
142
        # Keep the original order of items
143
        references = sorted(references, key=lambda it: uids.index(it.UID))
144
145
        # Return objects by default
146
        full_objects = kwargs.pop("full_objects", True)
147
        if full_objects:
148
            references = [api.get_object(ref) for ref in references]
149
150
        if self.multiValued:
151
            return references
152
        elif references:
153
            return references[0]
154
        return None
155
156
    @security.public
157
    def getRaw(self, context, **kwargs):
158
        """Grab the stored value, and return it directly as UIDs.
159
160
        :param context: context is the object who's schema contains this field.
161
        :type context: BaseContent
162
        :param kwargs: kwargs are passed directly to the underlying get.
163
        :type kwargs: dict
164
        :return: UID or list of UIDs for multiValued fields.
165
        :rtype: string | list[string]
166
        """
167
        uids = StringField.get(self, context, **kwargs)
168
        if not isinstance(uids, list):
169
            uids = [uids]
170
        if self.multiValued:
171
            return filter(None, uids)
172
        return uids[0]
173
174
    def _set_backreferences(self, context, items, **kwargs):
175
        """Set the back references on the linked items
176
177
        This will set an annotation storage on the referenced items which point
178
        to the current context.
179
        """
180
181
        # Don't set any references during initialization.
182
        # This might cause a recursion error when calling `getRaw` to fetch the
183
        # current set UIDs!
184
        initializing = kwargs.get('_initializing_', False)
185
        if initializing:
186
            return
187
188
        # current set UIDs
189
        raw = self.getRaw(context) or []
190
        # handle single reference fields
191
        if isinstance(raw, six.string_types):
192
            raw = [raw, ]
193
        cur = set(raw)
194
        # UIDs to be set
195
        uids = set(map(api.get_uid, items))
196
        # removed UIDs
197
        removed = cur.difference(uids)
198
        # missing UIDs
199
        missing = uids.difference(cur)
200
201
        # Unlink removed UIDs from the source
202
        uc = api.get_tool("uid_catalog")
203
        for brain in uc(UID=list(removed)):
204
            source = api.get_object(brain)
205
            self.unlink_reference(source, context)
206
207
        # Link missing UIDs
208
        for brain in uc(UID=list(missing)):
209
            target = api.get_object(brain)
210
            self.link_reference(target, context)
211
212
    @security.public
213
    def set(self, context, value, **kwargs):
214
        """Accepts a UID, brain, or an object (or a list of any of these),
215
        and stores a UID or list of UIDS.
216
217
        :param context: context is the object who's schema contains this field.
218
        :type context: BaseContent
219
        :param value: A UID, brain or object (or a sequence of these).
220
        :type value: Any
221
        :param kwargs: kwargs are passed directly to the underlying get.
222
        :type kwargs: dict
223
        :return: None
224
        """
225
        if not isinstance(value, (list, tuple)):
226
            value = [value]
227
228
        # Extract uids and remove empties
229
        uids = [self.get_uid(item) for item in value]
230
        uids = filter(None, uids)
231
232
        # Back-reference current object to referenced objects
233
        if self.keep_backreferences:
234
            self._set_backreferences(context, uids, **kwargs)
235
236
        # Store the referenced objects as uids
237
        if not self.multiValued:
238
            uids = uids[0] if uids else ""
239
        StringField.set(self, context, uids, **kwargs)
240
241
242
def get_storage(context):
243
    annotation = IAnnotations(context)
244
    if annotation.get(BACKREFS_STORAGE) is None:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable BACKREFS_STORAGE does not seem to be defined.
Loading history...
245
        annotation[BACKREFS_STORAGE] = PersistentDict()
246
    return annotation[BACKREFS_STORAGE]
247
248
249
def _get_catalog_for_uid(uid):
250
    at = api.get_tool('archetype_tool')
251
    uc = api.get_tool('uid_catalog')
252
    pc = api.get_tool('portal_catalog')
253
    # get uid_catalog brain for uid
254
    ub = uc(UID=uid)[0]
255
    # get portal_type of brain
256
    pt = ub.portal_type
257
    # get the registered catalogs for portal_type
258
    cats = at.getCatalogsByType(pt)
259
    # try avoid 'portal_catalog'; XXX multiple catalogs in setuphandlers.py?
260
    cats = [cat for cat in cats if cat != pc]
261
    if cats:
262
        return cats[0]
263
    return pc
264
265
266
def get_backreferences(context, relationship=None, as_brains=None):
267
    """Return all objects which use a UIDReferenceField to reference context.
268
269
    :param context: The object which is the target of references.
270
    :param relationship: The relationship name of the UIDReferenceField.
271
    :param as_brains: Requests that this function returns only catalog brains.
272
        as_brains can only be used if a relationship has been specified.
273
274
    This function can be called with or without specifying a relationship.
275
276
    - If a relationship is provided, the return value will be a list of items
277
      which reference the context using the provided relationship.
278
279
      If relationship is provided, then you can request that the backrefs
280
      should be returned as catalog brains.  If you do not specify as_brains,
281
      the raw list of UIDs will be returned.
282
283
    - If the relationship is not provided, then the entire set of
284
      backreferences to the context object is returned (by reference) as a
285
      dictionary.  This value can then be modified in-place, to edit the stored
286
      backreferences.
287
    """
288
289
    instance = context.aq_base
290
    raw_backrefs = get_storage(instance)
291
292
    if not relationship:
293
        assert not as_brains, "You cannot use as_brains with no relationship"
294
        return raw_backrefs
295
296
    backrefs = list(raw_backrefs.get(relationship, []))
297
    if not backrefs:
298
        return []
299
300
    if not as_brains:
301
        return backrefs
302
303
    cat = _get_catalog_for_uid(backrefs[0])
304
    return cat(UID=backrefs)
305