Passed
Push — master ( 804f45...1fa2c0 )
by Jordi
04:08
created

bika/lims/browser/fields/uidreferencefield.py (1 issue)

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-2019 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
from AccessControl import ClassSecurityInfo
22
from Products.Archetypes.Field import Field, StringField
23
from bika.lims import logger
24
from bika.lims import api
25
from bika.lims.interfaces.field import IUIDReferenceField
26
from persistent.list import PersistentList
27
from persistent.dict import PersistentDict
28
from zope.annotation.interfaces import IAnnotations
29
from zope.interface import implements
30
31
BACKREFS_STORAGE = "bika.lims.browser.fields.uidreferencefield.backreferences"
32
33
34
class ReferenceException(Exception):
35
    pass
36
37
38
class UIDReferenceField(StringField):
39
    """A field that stores References as UID values.  This acts as a drop-in
40
    replacement for Archetypes' ReferenceField.  A relationship is required
41
    but if one is not provided, it will be composed from a concatenation
42
    of `portal_type` + `fieldname`.
43
    """
44
    _properties = Field._properties.copy()
45
    _properties.update({
46
        'type': 'uidreference',
47
        'default': '',
48
        'default_content_type': 'text/plain',
49
        'relationship': '',
50
    })
51
52
    implements(IUIDReferenceField)
53
54
    security = ClassSecurityInfo()
55
56
    def get_relationship_key(self, context):
57
        """Return the configured relationship key or generate a new one
58
        """
59
        if not self.relationship:
60
            return context.portal_type + self.getName()
61
        return self.relationship
62
63
    def link_reference(self, source, target):
64
        """Link the target to the source
65
        """
66
        target_uid = api.get_uid(target)
67
        # get the annotation storage key
68
        key = self.get_relationship_key(target)
69
        # get all backreferences from the source
70
        # N.B. only like this we get the persistent mapping!
71
        backrefs = get_backreferences(source, relationship=None)
72
        if key not in backrefs:
73
            backrefs[key] = PersistentList()
74
        if target_uid not in backrefs[key]:
75
            backrefs[key].append(target_uid)
76
        return True
77
78
    def unlink_reference(self, source, target):
79
        """Unlink the target from the source
80
        """
81
        target_uid = api.get_uid(target)
82
        # get the storage key
83
        key = self.get_relationship_key(target)
84
        # get all backreferences from the source
85
        # N.B. only like this we get the persistent mapping!
86
        backrefs = get_backreferences(source, relationship=None)
87
        if key not in backrefs:
88
            logger.warn(
89
                "Referenced object {} has no backreferences for the key {}"
90
                .format(repr(source), key))
91
            return False
92
        if target_uid not in backrefs[key]:
93
            logger.warn("Target {} was not linked by {}"
94
                        .format(repr(target), repr(source)))
95
            return False
96
        backrefs[key].remove(target_uid)
97
        return True
98
99
    @security.public
100
    def get_object(self, context, value):
101
        """Resolve a UID to an object.
102
103
        :param context: context is the object containing the field's schema.
104
        :type context: BaseContent
105
        :param value: A UID.
106
        :type value: string
107
        :return: Returns a Content object.
108
        :rtype: BaseContent
109
        """
110
        if not value:
111
            return None
112
        obj = _get_object(context, value)
113
        if obj is None:
114
            logger.warning(
115
                "{}.{}: Resolving UIDReference failed for {}.  No object will "
116
                "be returned.".format(context, self.getName(), value))
117
        return obj
118
119
    @security.public
120
    def get_uid(self, context, value):
121
        """Takes a brain or object (or UID), and returns a UID.
122
123
        :param context: context is the object who's schema contains this field.
124
        :type context: BaseContent
125
        :param value: Brain, object, or UID.
126
        :type value: Any
127
        :return: resolved UID.
128
        :rtype: string
129
        """
130
        # Empty string or list with single empty string, are commonly
131
        # passed to us from form submissions
132
        if not value or value == ['']:
133
            ret = ''
134
        elif api.is_brain(value):
135
            ret = value.UID
136
        elif api.is_at_content(value) or api.is_dexterity_content(value):
137
            ret = value.UID()
138
        elif api.is_uid(value):
139
            ret = value
140
        else:
141
            raise ReferenceException("{}.{}: Cannot resolve UID for {}".format(
142
                context, self.getName(), value))
143
        return ret
144
145
    @security.public
146
    def get(self, context, **kwargs):
147
        """Grab the stored value, and resolve object(s) from UID catalog.
148
149
        :param context: context is the object who's schema contains this field.
150
        :type context: BaseContent
151
        :param kwargs: kwargs are passed directly to the underlying get.
152
        :type kwargs: dict
153
        :return: object or list of objects for multiValued fields.
154
        :rtype: BaseContent | list[BaseContent]
155
        """
156
        value = StringField.get(self, context, **kwargs)
157
        if not value:
158
            return [] if self.multiValued else None
159
        if self.multiValued:
160
            # Only return objects which actually exist; this is necessary here
161
            # because there are no HoldingReferences. This opens the
162
            # possibility that deletions leave hanging references.
163
            ret = filter(
164
                lambda x: x, [self.get_object(context, uid) for uid in value])
165
        else:
166
            ret = self.get_object(context, value)
167
        return ret
168
169
    @security.public
170
    def getRaw(self, context, aslist=False, **kwargs):
171
        """Grab the stored value, and return it directly as UIDs.
172
173
        :param context: context is the object who's schema contains this field.
174
        :type context: BaseContent
175
        :param aslist: Forces a single-valued field to return a list type.
176
        :type aslist: bool
177
        :param kwargs: kwargs are passed directly to the underlying get.
178
        :type kwargs: dict
179
        :return: UID or list of UIDs for multiValued fields.
180
        :rtype: string | list[string]
181
        """
182
        value = StringField.get(self, context, **kwargs)
183
        if not value:
184
            return [] if self.multiValued else None
185
        if self.multiValued:
186
            ret = value
187
        else:
188
            ret = self.get_uid(context, value)
189
            if aslist:
190
                ret = [ret]
191
        return ret
192
193
    def _set_backreferences(self, context, items, **kwargs):
194
        """Set the back references on the linked items
195
196
        This will set an annotation storage on the referenced items which point
197
        to the current context.
198
        """
199
200
        # Don't set any references during initialization.
201
        # This might cause a recursion error when calling `getRaw` to fetch the
202
        # current set UIDs!
203
        initializing = kwargs.get('_initializing_', False)
204
        if initializing:
205
            return
206
207
        # UID of the current object
208
        uid = api.get_uid(context)
209
        # current set UIDs
210
        raw = self.getRaw(context) or []
211
        # handle single reference fields
212
        if isinstance(raw, basestring):
213
            raw = [raw, ]
214
        cur = set(raw)
215
        # UIDs to be set
216
        new = set(map(api.get_uid, items))
217
        # removed UIDs
218
        removed = cur.difference(new)
219
220
        # Unlink removed UIDs from the source
221
        for uid in removed:
222
            source = api.get_object_by_uid(uid, None)
223
            if source is None:
224
                logger.warn("UID {} does not exist anymore".format(uid))
225
                continue
226
            self.unlink_reference(source, context)
227
228
        # Link backrefs
229
        for item in items:
230
            self.link_reference(item, context)
231
232
    @security.public
233
    def set(self, context, value, **kwargs):
234
        """Accepts a UID, brain, or an object (or a list of any of these),
235
        and stores a UID or list of UIDS.
236
237
        :param context: context is the object who's schema contains this field.
238
        :type context: BaseContent
239
        :param value: A UID, brain or object (or a sequence of these).
240
        :type value: Any
241
        :param kwargs: kwargs are passed directly to the underlying get.
242
        :type kwargs: dict
243
        :return: None
244
        """
245
        if self.multiValued:
246
            if not value:
247
                value = []
248
            if type(value) not in (list, tuple):
249
                value = [value, ]
250
            ret = [self.get_object(context, val) for val in value if val]
251
            self._set_backreferences(context, ret, **kwargs)
252
            uids = [self.get_uid(context, r) for r in ret if r]
253
            StringField.set(self, context, uids, **kwargs)
254
        else:
255
            # Sometimes we get given a list here with an empty string.
256
            # This is generated by html forms with empty values.
257
            # This is a single-valued field though, so:
258
            if isinstance(value, list) and value:
259
                if len(value) > 1:
260
                    logger.warning(
261
                        "Found values '\'{}\'' for singleValued field <{}>.{} "
262
                        "- using only the first value in the list.".format(
263
                            '\',\''.join(value), context.UID(), self.getName()))
264
                value = value[0]
265
            ret = self.get_object(context, value)
266
            if ret:
267
                self._set_backreferences(context, [ret, ], **kwargs)
268
                uid = self.get_uid(context, ret)
269
                StringField.set(self, context, uid, **kwargs)
270
            else:
271
                StringField.set(self, context, '', **kwargs)
272
273
274
def _get_object(context, value):
275
    """Resolve a UID to an object.
276
277
    :param context: context is the object containing the field's schema.
278
    :type context: BaseContent
279
    :param value: A UID.
280
    :type value: string
281
    :return: Returns a Content object or None.
282
    :rtype: BaseContent
283
    """
284
    if not value:
285
        return None
286
    if api.is_brain(value):
287
        return api.get_object(value)
288
    if api.is_object(value):
289
        return value
290
    if api.is_uid(value):
291
        uc = api.get_tool('uid_catalog', context=context)
292
        brains = uc(UID=value)
293
        if len(brains) == 0:
294
            # Broken Reference!
295
            logger.warn("Reference on {} with UID {} is broken!"
296
                        .format(repr(context), value))
297
            return None
298
        return brains[0].getObject()
299
    return None
300
301
302
def get_storage(context):
303
    annotation = IAnnotations(context)
304
    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...
305
        annotation[BACKREFS_STORAGE] = PersistentDict()
306
    return annotation[BACKREFS_STORAGE]
307
308
309
def _get_catalog_for_uid(uid):
310
    at = api.get_tool('archetype_tool')
311
    uc = api.get_tool('uid_catalog')
312
    pc = api.get_tool('portal_catalog')
313
    # get uid_catalog brain for uid
314
    ub = uc(UID=uid)[0]
315
    # get portal_type of brain
316
    pt = ub.portal_type
317
    # get the registered catalogs for portal_type
318
    cats = at.getCatalogsByType(pt)
319
    # try avoid 'portal_catalog'; XXX multiple catalogs in setuphandlers.py?
320
    cats = [cat for cat in cats if cat != pc]
321
    if cats:
322
        return cats[0]
323
    return pc
324
325
326
def get_backreferences(context, relationship=None, as_brains=None):
327
    """Return all objects which use a UIDReferenceField to reference context.
328
329
    :param context: The object which is the target of references.
330
    :param relationship: The relationship name of the UIDReferenceField.
331
    :param as_brains: Requests that this function returns only catalog brains.
332
        as_brains can only be used if a relationship has been specified.
333
334
    This function can be called with or without specifying a relationship.
335
336
    - If a relationship is provided, the return value will be a list of items
337
      which reference the context using the provided relationship.
338
339
      If relationship is provided, then you can request that the backrefs
340
      should be returned as catalog brains.  If you do not specify as_brains,
341
      the raw list of UIDs will be returned.
342
343
    - If the relationship is not provided, then the entire set of
344
      backreferences to the context object is returned (by reference) as a
345
      dictionary.  This value can then be modified in-place, to edit the stored
346
      backreferences.
347
    """
348
349
    instance = context.aq_base
350
    raw_backrefs = get_storage(instance)
351
352
    if not relationship:
353
        assert not as_brains, "You cannot use as_brains with no relationship"
354
        return raw_backrefs
355
356
    backrefs = list(raw_backrefs.get(relationship, []))
357
    if not backrefs:
358
        return []
359
360
    if not as_brains:
361
        return backrefs
362
363
    cat = _get_catalog_for_uid(backrefs[0])
364
    return cat(UID=backrefs)
365