entities/adapters.py
brancholdstable
changeset 6665 90f2f20367bc
parent 6155 16fad8d00787
child 6465 6401a9d0b5aa
equal deleted inserted replaced
6018:f4d1d5d9ccbb 6665:90f2f20367bc
       
     1 # copyright 2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """some basic entity adapter implementations, for interfaces used in the
       
    19 framework itself.
       
    20 """
       
    21 
       
    22 __docformat__ = "restructuredtext en"
       
    23 
       
    24 from itertools import chain
       
    25 from warnings import warn
       
    26 
       
    27 from logilab.mtconverter import TransformError
       
    28 from logilab.common.decorators import cached
       
    29 
       
    30 from cubicweb.view import EntityAdapter, implements_adapter_compat
       
    31 from cubicweb.selectors import implements, is_instance, relation_possible
       
    32 from cubicweb.interfaces import IDownloadable, ITree, IProgress, IMileStone
       
    33 
       
    34 
       
    35 class IEmailableAdapter(EntityAdapter):
       
    36     __regid__ = 'IEmailable'
       
    37     __select__ = relation_possible('primary_email') | relation_possible('use_email')
       
    38 
       
    39     def get_email(self):
       
    40         if getattr(self.entity, 'primary_email', None):
       
    41             return self.entity.primary_email[0].address
       
    42         if getattr(self.entity, 'use_email', None):
       
    43             return self.entity.use_email[0].address
       
    44         return None
       
    45 
       
    46     def allowed_massmail_keys(self):
       
    47         """returns a set of allowed email substitution keys
       
    48 
       
    49         The default is to return the entity's attribute list but you might
       
    50         override this method to allow extra keys.  For instance, a Person
       
    51         class might want to return a `companyname` key.
       
    52         """
       
    53         return set(rschema.type
       
    54                    for rschema, attrtype in self.entity.e_schema.attribute_definitions()
       
    55                    if attrtype.type not in ('Password', 'Bytes'))
       
    56 
       
    57     def as_email_context(self):
       
    58         """returns the dictionary as used by the sendmail controller to
       
    59         build email bodies.
       
    60 
       
    61         NOTE: the dictionary keys should match the list returned by the
       
    62         `allowed_massmail_keys` method.
       
    63         """
       
    64         return dict( (attr, getattr(self.entity, attr))
       
    65                      for attr in self.allowed_massmail_keys() )
       
    66 
       
    67 
       
    68 class INotifiableAdapter(EntityAdapter):
       
    69     __regid__ = 'INotifiable'
       
    70     __select__ = is_instance('Any')
       
    71 
       
    72     @implements_adapter_compat('INotifiableAdapter')
       
    73     def notification_references(self, view):
       
    74         """used to control References field of email send on notification
       
    75         for this entity. `view` is the notification view.
       
    76 
       
    77         Should return a list of eids which can be used to generate message
       
    78         identifiers of previously sent email(s)
       
    79         """
       
    80         itree = self.entity.cw_adapt_to('ITree')
       
    81         if itree is not None:
       
    82             return itree.path()[:-1]
       
    83         return ()
       
    84 
       
    85 
       
    86 class IFTIndexableAdapter(EntityAdapter):
       
    87     __regid__ = 'IFTIndexable'
       
    88     __select__ = is_instance('Any')
       
    89 
       
    90     def fti_containers(self, _done=None):
       
    91         if _done is None:
       
    92             _done = set()
       
    93         entity = self.entity
       
    94         _done.add(entity.eid)
       
    95         containers = tuple(entity.e_schema.fulltext_containers())
       
    96         if containers:
       
    97             for rschema, target in containers:
       
    98                 if target == 'object':
       
    99                     targets = getattr(entity, rschema.type)
       
   100                 else:
       
   101                     targets = getattr(entity, 'reverse_%s' % rschema)
       
   102                 for entity in targets:
       
   103                     if entity.eid in _done:
       
   104                         continue
       
   105                     for container in entity.cw_adapt_to('IFTIndexable').fti_containers(_done):
       
   106                         yield container
       
   107                         yielded = True
       
   108         else:
       
   109             yield entity
       
   110 
       
   111     # weight in ABCD
       
   112     entity_weight = 1.0
       
   113     attr_weight = {}
       
   114 
       
   115     def get_words(self):
       
   116         """used by the full text indexer to get words to index
       
   117 
       
   118         this method should only be used on the repository side since it depends
       
   119         on the logilab.database package
       
   120 
       
   121         :rtype: list
       
   122         :return: the list of indexable word of this entity
       
   123         """
       
   124         from logilab.database.fti import tokenize
       
   125         # take care to cases where we're modyfying the schema
       
   126         entity = self.entity
       
   127         pending = self._cw.transaction_data.setdefault('pendingrdefs', set())
       
   128         words = {}
       
   129         for rschema in entity.e_schema.indexable_attributes():
       
   130             if (entity.e_schema, rschema) in pending:
       
   131                 continue
       
   132             weight = self.attr_weight.get(rschema, 'C')
       
   133             try:
       
   134                 value = entity.printable_value(rschema, format='text/plain')
       
   135             except TransformError:
       
   136                 continue
       
   137             except:
       
   138                 self.exception("can't add value of %s to text index for entity %s",
       
   139                                rschema, entity.eid)
       
   140                 continue
       
   141             if value:
       
   142                 words.setdefault(weight, []).extend(tokenize(value))
       
   143         for rschema, role in entity.e_schema.fulltext_relations():
       
   144             if role == 'subject':
       
   145                 for entity_ in getattr(entity, rschema.type):
       
   146                     merge_weight_dict(words, entity_.cw_adapt_to('IFTIndexable').get_words())
       
   147             else: # if role == 'object':
       
   148                 for entity_ in getattr(entity, 'reverse_%s' % rschema.type):
       
   149                     merge_weight_dict(words, entity_.cw_adapt_to('IFTIndexable').get_words())
       
   150         return words
       
   151 
       
   152 def merge_weight_dict(maindict, newdict):
       
   153     for weight, words in newdict.iteritems():
       
   154         maindict.setdefault(weight, []).extend(words)
       
   155 
       
   156 class IDownloadableAdapter(EntityAdapter):
       
   157     """interface for downloadable entities"""
       
   158     __regid__ = 'IDownloadable'
       
   159     __select__ = implements(IDownloadable, warn=False) # XXX for bw compat, else should be abstract
       
   160 
       
   161     @implements_adapter_compat('IDownloadable')
       
   162     def download_url(self, **kwargs): # XXX not really part of this interface
       
   163         """return an url to download entity's content"""
       
   164         raise NotImplementedError
       
   165     @implements_adapter_compat('IDownloadable')
       
   166     def download_content_type(self):
       
   167         """return MIME type of the downloadable content"""
       
   168         raise NotImplementedError
       
   169     @implements_adapter_compat('IDownloadable')
       
   170     def download_encoding(self):
       
   171         """return encoding of the downloadable content"""
       
   172         raise NotImplementedError
       
   173     @implements_adapter_compat('IDownloadable')
       
   174     def download_file_name(self):
       
   175         """return file name of the downloadable content"""
       
   176         raise NotImplementedError
       
   177     @implements_adapter_compat('IDownloadable')
       
   178     def download_data(self):
       
   179         """return actual data of the downloadable content"""
       
   180         raise NotImplementedError
       
   181 
       
   182 
       
   183 class ITreeAdapter(EntityAdapter):
       
   184     """This adapter has to be overriden to be configured using the
       
   185     tree_relation, child_role and parent_role class attributes to benefit from
       
   186     this default implementation.
       
   187 
       
   188     This adapter provides a tree interface. It has to be overriden to be
       
   189     configured using the tree_relation, child_role and parent_role class
       
   190     attributes to benefit from this default implementation.
       
   191 
       
   192     This class provides the following methods:
       
   193 
       
   194     .. automethod: iterparents
       
   195     .. automethod: iterchildren
       
   196     .. automethod: prefixiter
       
   197 
       
   198     .. automethod: is_leaf
       
   199     .. automethod: is_root
       
   200 
       
   201     .. automethod: root
       
   202     .. automethod: parent
       
   203     .. automethod: children
       
   204     .. automethod: different_type_children
       
   205     .. automethod: same_type_children
       
   206     .. automethod: children_rql
       
   207     .. automethod: path
       
   208     """
       
   209     __regid__ = 'ITree'
       
   210     __select__ = implements(ITree, warn=False) # XXX for bw compat, else should be abstract
       
   211 
       
   212     child_role = 'subject'
       
   213     parent_role = 'object'
       
   214 
       
   215     @property
       
   216     def tree_relation(self):
       
   217         warn('[3.9] tree_attribute is deprecated, define tree_relation on a custom '
       
   218              'ITree for %s instead' % (self.entity.__class__),
       
   219              DeprecationWarning)
       
   220         return self.entity.tree_attribute
       
   221 
       
   222     # XXX should be removed from the public interface
       
   223     @implements_adapter_compat('ITree')
       
   224     def children_rql(self):
       
   225         """Returns RQL to get the children of the entity."""
       
   226         return self.entity.cw_related_rql(self.tree_relation, self.parent_role)
       
   227 
       
   228     @implements_adapter_compat('ITree')
       
   229     def different_type_children(self, entities=True):
       
   230         """Return children entities of different type as this entity.
       
   231 
       
   232         According to the `entities` parameter, return entity objects or the
       
   233         equivalent result set.
       
   234         """
       
   235         res = self.entity.related(self.tree_relation, self.parent_role,
       
   236                                   entities=entities)
       
   237         eschema = self.entity.e_schema
       
   238         if entities:
       
   239             return [e for e in res if e.e_schema != eschema]
       
   240         return res.filtered_rset(lambda x: x.e_schema != eschema, self.entity.cw_col)
       
   241 
       
   242     @implements_adapter_compat('ITree')
       
   243     def same_type_children(self, entities=True):
       
   244         """Return children entities of the same type as this entity.
       
   245 
       
   246         According to the `entities` parameter, return entity objects or the
       
   247         equivalent result set.
       
   248         """
       
   249         res = self.entity.related(self.tree_relation, self.parent_role,
       
   250                                   entities=entities)
       
   251         eschema = self.entity.e_schema
       
   252         if entities:
       
   253             return [e for e in res if e.e_schema == eschema]
       
   254         return res.filtered_rset(lambda x: x.e_schema is eschema, self.entity.cw_col)
       
   255 
       
   256     @implements_adapter_compat('ITree')
       
   257     def is_leaf(self):
       
   258         """Returns True if the entity does not have any children."""
       
   259         return len(self.children()) == 0
       
   260 
       
   261     @implements_adapter_compat('ITree')
       
   262     def is_root(self):
       
   263         """Returns true if the entity is root of the tree (e.g. has no parent).
       
   264         """
       
   265         return self.parent() is None
       
   266 
       
   267     @implements_adapter_compat('ITree')
       
   268     def root(self):
       
   269         """Return the root entity of the tree."""
       
   270         return self._cw.entity_from_eid(self.path()[0])
       
   271 
       
   272     @implements_adapter_compat('ITree')
       
   273     def parent(self):
       
   274         """Returns the parent entity if any, else None (e.g. if we are on the
       
   275         root).
       
   276         """
       
   277         try:
       
   278             return self.entity.related(self.tree_relation, self.child_role,
       
   279                                        entities=True)[0]
       
   280         except (KeyError, IndexError):
       
   281             return None
       
   282 
       
   283     @implements_adapter_compat('ITree')
       
   284     def children(self, entities=True, sametype=False):
       
   285         """Return children entities.
       
   286 
       
   287         According to the `entities` parameter, return entity objects or the
       
   288         equivalent result set.
       
   289         """
       
   290         if sametype:
       
   291             return self.same_type_children(entities)
       
   292         else:
       
   293             return self.entity.related(self.tree_relation, self.parent_role,
       
   294                                        entities=entities)
       
   295 
       
   296     @implements_adapter_compat('ITree')
       
   297     def iterparents(self, strict=True):
       
   298         """Return an iterator on the parents of the entity."""
       
   299         def _uptoroot(self):
       
   300             curr = self
       
   301             while True:
       
   302                 curr = curr.parent()
       
   303                 if curr is None:
       
   304                     break
       
   305                 yield curr
       
   306                 curr = curr.cw_adapt_to('ITree')
       
   307         if not strict:
       
   308             return chain([self.entity], _uptoroot(self))
       
   309         return _uptoroot(self)
       
   310 
       
   311     @implements_adapter_compat('ITree')
       
   312     def iterchildren(self, _done=None):
       
   313         """Return an iterator over the item's children."""
       
   314         if _done is None:
       
   315             _done = set()
       
   316         for child in self.children():
       
   317             if child.eid in _done:
       
   318                 self.error('loop in %s tree: %s', child.__regid__.lower(), child)
       
   319                 continue
       
   320             yield child
       
   321             _done.add(child.eid)
       
   322 
       
   323     @implements_adapter_compat('ITree')
       
   324     def prefixiter(self, _done=None):
       
   325         """Return an iterator over the item's descendants in a prefixed order."""
       
   326         if _done is None:
       
   327             _done = set()
       
   328         if self.entity.eid in _done:
       
   329             return
       
   330         _done.add(self.entity.eid)
       
   331         yield self.entity
       
   332         for child in self.same_type_children():
       
   333             for entity in child.cw_adapt_to('ITree').prefixiter(_done):
       
   334                 yield entity
       
   335 
       
   336     @cached
       
   337     @implements_adapter_compat('ITree')
       
   338     def path(self):
       
   339         """Returns the list of eids from the root object to this object."""
       
   340         path = []
       
   341         adapter = self
       
   342         entity = adapter.entity
       
   343         while entity is not None:
       
   344             if entity.eid in path:
       
   345                 self.error('loop in %s tree: %s', entity.__regid__.lower(), entity)
       
   346                 break
       
   347             path.append(entity.eid)
       
   348             try:
       
   349                 # check we are not jumping to another tree
       
   350                 if (adapter.tree_relation != self.tree_relation or
       
   351                     adapter.child_role != self.child_role):
       
   352                     break
       
   353                 entity = adapter.parent()
       
   354                 adapter = entity.cw_adapt_to('ITree')
       
   355             except AttributeError:
       
   356                 break
       
   357         path.reverse()
       
   358         return path
       
   359 
       
   360 
       
   361 class IProgressAdapter(EntityAdapter):
       
   362     """something that has a cost, a state and a progression.
       
   363 
       
   364     You should at least override progress_info an in_progress methods on concret
       
   365     implementations.
       
   366     """
       
   367     __regid__ = 'IProgress'
       
   368     __select__ = implements(IProgress, warn=False) # XXX for bw compat, should be abstract
       
   369 
       
   370     @property
       
   371     @implements_adapter_compat('IProgress')
       
   372     def cost(self):
       
   373         """the total cost"""
       
   374         return self.progress_info()['estimated']
       
   375 
       
   376     @property
       
   377     @implements_adapter_compat('IProgress')
       
   378     def revised_cost(self):
       
   379         return self.progress_info().get('estimatedcorrected', self.cost)
       
   380 
       
   381     @property
       
   382     @implements_adapter_compat('IProgress')
       
   383     def done(self):
       
   384         """what is already done"""
       
   385         return self.progress_info()['done']
       
   386 
       
   387     @property
       
   388     @implements_adapter_compat('IProgress')
       
   389     def todo(self):
       
   390         """what remains to be done"""
       
   391         return self.progress_info()['todo']
       
   392 
       
   393     @implements_adapter_compat('IProgress')
       
   394     def progress_info(self):
       
   395         """returns a dictionary describing progress/estimated cost of the
       
   396         version.
       
   397 
       
   398         - mandatory keys are (''estimated', 'done', 'todo')
       
   399 
       
   400         - optional keys are ('notestimated', 'notestimatedcorrected',
       
   401           'estimatedcorrected')
       
   402 
       
   403         'noestimated' and 'notestimatedcorrected' should default to 0
       
   404         'estimatedcorrected' should default to 'estimated'
       
   405         """
       
   406         raise NotImplementedError
       
   407 
       
   408     @implements_adapter_compat('IProgress')
       
   409     def finished(self):
       
   410         """returns True if status is finished"""
       
   411         return not self.in_progress()
       
   412 
       
   413     @implements_adapter_compat('IProgress')
       
   414     def in_progress(self):
       
   415         """returns True if status is not finished"""
       
   416         raise NotImplementedError
       
   417 
       
   418     @implements_adapter_compat('IProgress')
       
   419     def progress(self):
       
   420         """returns the % progress of the task item"""
       
   421         try:
       
   422             return 100. * self.done / self.revised_cost
       
   423         except ZeroDivisionError:
       
   424             # total cost is 0 : if everything was estimated, task is completed
       
   425             if self.progress_info().get('notestimated'):
       
   426                 return 0.
       
   427             return 100
       
   428 
       
   429     @implements_adapter_compat('IProgress')
       
   430     def progress_class(self):
       
   431         return ''
       
   432 
       
   433 
       
   434 class IMileStoneAdapter(IProgressAdapter):
       
   435     __regid__ = 'IMileStone'
       
   436     __select__ = implements(IMileStone, warn=False) # XXX for bw compat, should be abstract
       
   437 
       
   438     parent_type = None # specify main task's type
       
   439 
       
   440     @implements_adapter_compat('IMileStone')
       
   441     def get_main_task(self):
       
   442         """returns the main ITask entity"""
       
   443         raise NotImplementedError
       
   444 
       
   445     @implements_adapter_compat('IMileStone')
       
   446     def initial_prevision_date(self):
       
   447         """returns the initial expected end of the milestone"""
       
   448         raise NotImplementedError
       
   449 
       
   450     @implements_adapter_compat('IMileStone')
       
   451     def eta_date(self):
       
   452         """returns expected date of completion based on what remains
       
   453         to be done
       
   454         """
       
   455         raise NotImplementedError
       
   456 
       
   457     @implements_adapter_compat('IMileStone')
       
   458     def completion_date(self):
       
   459         """returns date on which the subtask has been completed"""
       
   460         raise NotImplementedError
       
   461 
       
   462     @implements_adapter_compat('IMileStone')
       
   463     def contractors(self):
       
   464         """returns the list of persons supposed to work on this task"""
       
   465         raise NotImplementedError