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
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 |