src/pyams_thesaurus/term.py
changeset 0 47700a43ef3f
child 87 dfe19304d980
equal deleted inserted replaced
-1:000000000000 0:47700a43ef3f
       
     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