|
1 # |
|
2 # Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net> |
|
3 # All Rights Reserved. |
|
4 # |
|
5 # This software is subject to the provisions of the Zope Public License, |
|
6 # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. |
|
7 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED |
|
8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS |
|
10 # FOR A PARTICULAR PURPOSE. |
|
11 # |
|
12 |
|
13 __docformat__ = 'restructuredtext' |
|
14 |
|
15 |
|
16 # import standard library |
|
17 from datetime import datetime |
|
18 |
|
19 # import interfaces |
|
20 from pyams_thesaurus.interfaces.extension import IThesaurusTermExtension |
|
21 from pyams_thesaurus.interfaces.term import STATUS_PUBLISHED, IThesaurusTerm, IThesaurusTermsContainer, \ |
|
22 IThesaurusLoaderTerm |
|
23 from pyams_thesaurus.interfaces.thesaurus import IThesaurus, IThesaurusExtract |
|
24 from pyams_utils.interfaces.tree import INode |
|
25 from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectRemovedEvent, IObjectModifiedEvent, \ |
|
26 IObjectMovedEvent |
|
27 from zope.traversing.interfaces import ITraversable |
|
28 |
|
29 # import packages |
|
30 from persistent import Persistent |
|
31 from pyams_catalog.utils import index_object, unindex_object, reindex_object |
|
32 from pyams_utils.adapter import adapter_config, ContextAdapter |
|
33 from pyams_utils.registry import query_utility |
|
34 from pyams_utils.timezone import tztime |
|
35 from pyams_utils.traversing import get_parent |
|
36 from pyams_utils.unicode import translate_string |
|
37 from pyramid.events import subscriber |
|
38 from zope.container.contained import Contained |
|
39 from zope.interface import implementer, alsoProvides, noLongerProvides |
|
40 from zope.schema.fieldproperty import FieldProperty |
|
41 |
|
42 |
|
43 REVERSE_LINK_ATTRIBUTES = {'generic': 'specifics', |
|
44 'usage': 'used_for'} |
|
45 |
|
46 REVERSE_LIST_ATTRIBUTES = {'specifics': 'generic', |
|
47 'used_for': 'usage'} |
|
48 |
|
49 |
|
50 @adapter_config(name='terms', context=IThesaurus, provides=ITraversable) |
|
51 class ThesaurusTermsNamespace(ContextAdapter): |
|
52 """Thesaurus ++terms++ namespace""" |
|
53 |
|
54 def traverse(self, name, furtherpath=None): |
|
55 terms = IThesaurus(self.context).terms |
|
56 if name: |
|
57 return terms[name] |
|
58 else: |
|
59 return terms |
|
60 |
|
61 |
|
62 @implementer(IThesaurusTerm) |
|
63 class ThesaurusTerm(Persistent, Contained): |
|
64 """Thesaurus term""" |
|
65 |
|
66 label = FieldProperty(IThesaurusTerm['label']) |
|
67 alt = FieldProperty(IThesaurusTerm['alt']) |
|
68 definition = FieldProperty(IThesaurusTerm['definition']) |
|
69 note = FieldProperty(IThesaurusTerm['note']) |
|
70 _generic = FieldProperty(IThesaurusTerm['generic']) |
|
71 _specifics = FieldProperty(IThesaurusTerm['specifics']) |
|
72 _associations = FieldProperty(IThesaurusTerm['associations']) |
|
73 _usage = FieldProperty(IThesaurusTerm['usage']) |
|
74 _used_for = FieldProperty(IThesaurusTerm['used_for']) |
|
75 _extracts = FieldProperty(IThesaurusTerm['extracts']) |
|
76 _extensions = FieldProperty(IThesaurusTerm['extensions']) |
|
77 status = FieldProperty(IThesaurusTerm['status']) |
|
78 level = FieldProperty(IThesaurusTerm['level']) |
|
79 micro_thesaurus = FieldProperty(IThesaurusTerm['micro_thesaurus']) |
|
80 parent = FieldProperty(IThesaurusTerm['parent']) |
|
81 _created = FieldProperty(IThesaurusTerm['created']) |
|
82 _modified = FieldProperty(IThesaurusTerm['modified']) |
|
83 |
|
84 def __init__(self, label, alt=None, definition=None, note=None, generic=None, specifics=None, associations=None, |
|
85 usage=None, used_for=None, created=None, modified=None): |
|
86 self.label = label |
|
87 self.alt = alt |
|
88 self.definition = definition |
|
89 self.note = note |
|
90 self.generic = generic |
|
91 self.specifics = specifics or [] |
|
92 self.associations = associations or [] |
|
93 self.usage = usage |
|
94 self.used_for = used_for or [] |
|
95 self.created = created |
|
96 self.modified = modified |
|
97 |
|
98 def __eq__(self, other): |
|
99 if other is None: |
|
100 return False |
|
101 else: |
|
102 return isinstance(other, ThesaurusTerm) and (self.label == other.label) |
|
103 |
|
104 def __hash__(self): |
|
105 return hash(self.label) |
|
106 |
|
107 @property |
|
108 def base_label(self): |
|
109 return translate_string(self.label, escape_slashes=True, force_lower=True, spaces=' ') |
|
110 |
|
111 @property |
|
112 def title(self): |
|
113 if self._usage: |
|
114 label = self._usage.label |
|
115 terms = [term.label for term in self._usage.used_for if term.status == STATUS_PUBLISHED] |
|
116 elif self._used_for: |
|
117 label = self.label |
|
118 terms = [term.label for term in self._used_for if term.status == STATUS_PUBLISHED] |
|
119 else: |
|
120 label = self.label |
|
121 terms = [] |
|
122 return label + (' [ {0} ]'.format(', '.join(terms)) if terms else '') |
|
123 |
|
124 @property |
|
125 def generic(self): |
|
126 return self._generic |
|
127 |
|
128 @generic.setter |
|
129 def generic(self, value): |
|
130 self._generic = value |
|
131 if value is not None: |
|
132 self.extracts = self.extracts & value.extracts |
|
133 |
|
134 @property |
|
135 def specifics(self): |
|
136 return self._specifics |
|
137 |
|
138 @specifics.setter |
|
139 def specifics(self, value): |
|
140 self._specifics = [term for term in value or ()] |
|
141 |
|
142 @property |
|
143 def associations(self): |
|
144 return self._associations |
|
145 |
|
146 @associations.setter |
|
147 def associations(self, value): |
|
148 self._associations = [term for term in value or ()] |
|
149 |
|
150 @property |
|
151 def usage(self): |
|
152 return self._usage |
|
153 |
|
154 @usage.setter |
|
155 def usage(self, value): |
|
156 self._usage = value |
|
157 if value is not None: |
|
158 self.generic = None |
|
159 self.extracts = value.extracts |
|
160 |
|
161 @property |
|
162 def used_for(self): |
|
163 return self._used_for |
|
164 |
|
165 @used_for.setter |
|
166 def used_for(self, value): |
|
167 self._used_for = [term for term in value or ()] |
|
168 |
|
169 @property |
|
170 def extracts(self): |
|
171 return self._extracts or set() |
|
172 |
|
173 @extracts.setter |
|
174 def extracts(self, value): |
|
175 old_value = self._extracts or set() |
|
176 new_value = value or set() |
|
177 if self._generic is not None: |
|
178 new_value = new_value & (self._generic.extracts or set()) |
|
179 if old_value != new_value: |
|
180 removed = old_value - new_value |
|
181 if removed: |
|
182 for term in self.specifics: |
|
183 term.extracts = (term.extracts or set()) - removed |
|
184 self._extracts = new_value |
|
185 # Extracts selection also applies to term synonyms... |
|
186 for term in self.used_for or (): |
|
187 term.extracts = self.extracts |
|
188 |
|
189 def add_extract(self, extract, check=True): |
|
190 if IThesaurusExtract.providedBy(extract): |
|
191 extract = extract.name |
|
192 if check: |
|
193 self.extracts = (self.extracts or set()) | {extract} |
|
194 else: |
|
195 self._extracts = (self._extracts or set()) | {extract} |
|
196 # Extracts selection also applies to term synonyms... |
|
197 for term in self.used_for or (): |
|
198 term.extracts = self.extracts |
|
199 |
|
200 def remove_extract(self, extract, check=True): |
|
201 if IThesaurusExtract.providedBy(extract): |
|
202 extract = extract.name |
|
203 if check: |
|
204 self.extracts = (self.extracts or set()) - {extract} |
|
205 else: |
|
206 self._extracts = (self._extracts or set()) - {extract} |
|
207 # Extracts selection also applies to term synonyms... |
|
208 for term in self.used_for or (): |
|
209 term.extracts = self.extracts |
|
210 |
|
211 @property |
|
212 def extensions(self): |
|
213 return self._extensions or set() |
|
214 |
|
215 @extensions.setter |
|
216 def extensions(self, value): |
|
217 old_value = self._extensions or set() |
|
218 new_value = value or set() |
|
219 if old_value != new_value: |
|
220 added = new_value - old_value |
|
221 removed = old_value - new_value |
|
222 for ext in removed: |
|
223 extension = query_utility(IThesaurusTermExtension, ext) |
|
224 if extension is not None: |
|
225 noLongerProvides(self, extension.target_interface) |
|
226 for ext in added: |
|
227 extension = query_utility(IThesaurusTermExtension, ext) |
|
228 if extension is not None: |
|
229 alsoProvides(self, extension.target_interface) |
|
230 self._extensions = new_value |
|
231 |
|
232 def query_extensions(self): |
|
233 return [util for util in [query_utility(IThesaurusTermExtension, ext) for ext in self.extensions] |
|
234 if util is not None] |
|
235 |
|
236 @property |
|
237 def created(self): |
|
238 return self._created |
|
239 |
|
240 @created.setter |
|
241 def created(self, value): |
|
242 if isinstance(value, str): |
|
243 if ' ' in value: |
|
244 value = datetime.strptime(value, '%Y-%m-%d %H:%M:%S') |
|
245 else: |
|
246 value = datetime.strptime(value, '%Y-%m-%d') |
|
247 self._created = tztime(value) |
|
248 |
|
249 @property |
|
250 def modified(self): |
|
251 return self._modified |
|
252 |
|
253 @modified.setter |
|
254 def modified(self, value): |
|
255 if isinstance(value, str): |
|
256 if ' ' in value: |
|
257 value = datetime.strptime(value, '%Y-%m-%d %H:%M:%S') |
|
258 else: |
|
259 value = datetime.strptime(value, '%Y-%m-%d') |
|
260 self._modified = tztime(value) |
|
261 |
|
262 def get_parents(self): |
|
263 terms = [] |
|
264 parent = self.generic |
|
265 while parent is not None: |
|
266 terms.append(parent) |
|
267 parent = parent.generic |
|
268 return terms |
|
269 |
|
270 @property |
|
271 def level(self): |
|
272 return len(self.get_parents()) + 1 |
|
273 |
|
274 def get_parent_childs(self): |
|
275 terms = [] |
|
276 parent = self.generic |
|
277 if parent is not None: |
|
278 [terms.append(term) for term in parent.specifics if term is not self] |
|
279 return terms |
|
280 |
|
281 def get_all_childs(self, terms=None, with_synonyms=False): |
|
282 if terms is None: |
|
283 terms = [] |
|
284 if with_synonyms: |
|
285 terms.extend(self.used_for) |
|
286 terms.extend(self.specifics) |
|
287 for term in self.specifics: |
|
288 term.get_all_childs(terms, with_synonyms) |
|
289 return terms |
|
290 |
|
291 def merge(self, term, configuration): |
|
292 # terms marked by IThesaurusLoaderTerm interface are used by custom loaders which only contains |
|
293 # synonyms definitions; so they shouldn't alter terms properties |
|
294 if term is None: |
|
295 return |
|
296 # assign basic attributes |
|
297 if not IThesaurusLoaderTerm.providedBy(term): |
|
298 for name in ('label', 'definition', 'note', 'status', 'micro_thesaurus', 'created', 'modified'): |
|
299 setattr(self, name, getattr(term, name, None)) |
|
300 # for term references, we have to check if the target term is already |
|
301 # in our parent thesaurus or not : |
|
302 # - if yes => we target the term actually in the thesaurus |
|
303 # - if not => we keep the same target, which will be included in the thesaurus after merging |
|
304 terms = self.__parent__ |
|
305 if IThesaurusLoaderTerm.providedBy(term): |
|
306 attrs = ('usage',) |
|
307 else: |
|
308 attrs = ('generic', 'usage') |
|
309 for name in attrs: |
|
310 target = getattr(term, name) |
|
311 if target is None: |
|
312 setattr(self, name, None) |
|
313 else: |
|
314 label = target.label |
|
315 if configuration.conflict_suffix: |
|
316 label = target.label + ' ' + configuration.conflict_suffix |
|
317 if label not in terms: |
|
318 label = target.label |
|
319 if label in terms: |
|
320 target_term = terms[label] |
|
321 else: |
|
322 target_term = target |
|
323 setattr(self, name, target_term) |
|
324 if name in REVERSE_LINK_ATTRIBUTES: |
|
325 attribute = REVERSE_LINK_ATTRIBUTES[name] |
|
326 setattr(target_term, attribute, set(getattr(target_term, attribute)) | {self}) |
|
327 if IThesaurusLoaderTerm.providedBy(term): |
|
328 attrs = ('used_for',) |
|
329 else: |
|
330 attrs = ('specifics', 'associations', 'used_for') |
|
331 for name in attrs: |
|
332 targets = getattr(term, name, []) |
|
333 if not targets: |
|
334 setattr(self, name, []) |
|
335 else: |
|
336 new_targets = [] |
|
337 for target in targets: |
|
338 label = target.label |
|
339 if configuration.conflict_suffix: |
|
340 label = target.label + ' ' + configuration.conflict_suffix |
|
341 if label not in terms: |
|
342 label = target.label |
|
343 if label in terms: |
|
344 target_term = terms[label] |
|
345 else: |
|
346 target_term = target |
|
347 new_targets.append(target_term) |
|
348 if name in REVERSE_LIST_ATTRIBUTES: |
|
349 attribute = REVERSE_LIST_ATTRIBUTES[name] |
|
350 setattr(target_term, attribute, self) |
|
351 setattr(self, name, new_targets) |
|
352 |
|
353 |
|
354 @subscriber(IObjectAddedEvent, context_selector=IThesaurusTerm) |
|
355 @subscriber(IObjectMovedEvent, context_selector=IThesaurusTerm) |
|
356 def handle_new_term(event): |
|
357 """Index term into inner catalog""" |
|
358 if IThesaurusLoaderTerm.providedBy(event.object): |
|
359 return |
|
360 if IThesaurusTermsContainer.providedBy(event.oldParent): |
|
361 thesaurus = event.oldParent.__parent__ |
|
362 if IThesaurus.providedBy(thesaurus): |
|
363 unindex_object(event.object, thesaurus.catalog) |
|
364 if IThesaurusTermsContainer.providedBy(event.newParent): |
|
365 thesaurus = event.newParent.__parent__ |
|
366 if IThesaurus.providedBy(thesaurus): |
|
367 index_object(event.object, thesaurus.catalog) |
|
368 |
|
369 |
|
370 @subscriber(IObjectModifiedEvent, context_selector=IThesaurusTerm) |
|
371 def handle_modified_term(event): |
|
372 """Update index term into inner catalog""" |
|
373 parent = get_parent(event.object, IThesaurusTermsContainer) |
|
374 if parent is not None: |
|
375 thesaurus = parent.__parent__ |
|
376 if IThesaurus.providedBy(thesaurus): |
|
377 reindex_object(event.object, thesaurus.catalog) |
|
378 |
|
379 |
|
380 @subscriber(IObjectRemovedEvent, context_selector=IThesaurusTerm) |
|
381 def handle_removed_term(event): |
|
382 """Unindex term into inner catalog""" |
|
383 parent = event.oldParent |
|
384 if IThesaurusTermsContainer.providedBy(parent): |
|
385 thesaurus = parent.__parent__ |
|
386 if IThesaurus.providedBy(thesaurus): |
|
387 unindex_object(event.object, thesaurus.catalog) |
|
388 |
|
389 |
|
390 @adapter_config(context=IThesaurusTerm, provides=INode) |
|
391 class ThesaurusTermTreeAdapter(ContextAdapter): |
|
392 """Thesaurus term tree node adapter""" |
|
393 |
|
394 @property |
|
395 def label(self): |
|
396 return self.context.label |
|
397 |
|
398 @property |
|
399 def css_class(self): |
|
400 return self.context.status |
|
401 |
|
402 def get_level(self): |
|
403 return self.context.level |
|
404 |
|
405 def has_children(self, filter_value=None): |
|
406 specifics = self.context.specifics |
|
407 if filter_value: |
|
408 specifics = list(filter(lambda x: filter_value in (x.extracts or ()), specifics)) |
|
409 return len(specifics) > 0 |
|
410 |
|
411 def get_children(self, filter_value=None): |
|
412 return self.context.specifics |