entities/adapters.py
changeset 8037 a36bd56f33bb
parent 7879 9aae456abab5
child 8190 2a3c1b787688
equal deleted inserted replaced
8036:140a093015f8 8037:a36bd56f33bb
    24 from itertools import chain
    24 from itertools import chain
    25 from warnings import warn
    25 from warnings import warn
    26 
    26 
    27 from logilab.mtconverter import TransformError
    27 from logilab.mtconverter import TransformError
    28 from logilab.common.decorators import cached
    28 from logilab.common.decorators import cached
    29 
    29 from logilab.common.deprecation import class_deprecated
    30 from cubicweb import ValidationError
    30 
    31 from cubicweb.view import EntityAdapter, implements_adapter_compat
    31 from cubicweb import ValidationError, view
    32 from cubicweb.selectors import (implements, is_instance, relation_possible,
    32 from cubicweb.selectors import (implements, is_instance, relation_possible,
    33                                 match_exception)
    33                                 match_exception)
    34 from cubicweb.interfaces import IDownloadable, ITree, IProgress, IMileStone
    34 from cubicweb.interfaces import IDownloadable, ITree, IProgress, IMileStone
    35 
    35 
    36 
    36 
    37 class IEmailableAdapter(EntityAdapter):
    37 class IEmailableAdapter(view.EntityAdapter):
    38     __regid__ = 'IEmailable'
    38     __regid__ = 'IEmailable'
    39     __select__ = relation_possible('primary_email') | relation_possible('use_email')
    39     __select__ = relation_possible('primary_email') | relation_possible('use_email')
    40 
    40 
    41     def get_email(self):
    41     def get_email(self):
    42         if getattr(self.entity, 'primary_email', None):
    42         if getattr(self.entity, 'primary_email', None):
    65         """
    65         """
    66         return dict( (attr, getattr(self.entity, attr))
    66         return dict( (attr, getattr(self.entity, attr))
    67                      for attr in self.allowed_massmail_keys() )
    67                      for attr in self.allowed_massmail_keys() )
    68 
    68 
    69 
    69 
    70 class INotifiableAdapter(EntityAdapter):
    70 class INotifiableAdapter(view.EntityAdapter):
    71     __needs_bw_compat__ = True
    71     __needs_bw_compat__ = True
    72     __regid__ = 'INotifiable'
    72     __regid__ = 'INotifiable'
    73     __select__ = is_instance('Any')
    73     __select__ = is_instance('Any')
    74 
    74 
    75     @implements_adapter_compat('INotifiableAdapter')
    75     @view.implements_adapter_compat('INotifiableAdapter')
    76     def notification_references(self, view):
    76     def notification_references(self, view):
    77         """used to control References field of email send on notification
    77         """used to control References field of email send on notification
    78         for this entity. `view` is the notification view.
    78         for this entity. `view` is the notification view.
    79 
    79 
    80         Should return a list of eids which can be used to generate message
    80         Should return a list of eids which can be used to generate message
    84         if itree is not None:
    84         if itree is not None:
    85             return itree.path()[:-1]
    85             return itree.path()[:-1]
    86         return ()
    86         return ()
    87 
    87 
    88 
    88 
    89 class IFTIndexableAdapter(EntityAdapter):
    89 class IFTIndexableAdapter(view.EntityAdapter):
    90     __regid__ = 'IFTIndexable'
    90     __regid__ = 'IFTIndexable'
    91     __select__ = is_instance('Any')
    91     __select__ = is_instance('Any')
    92 
    92 
    93     def fti_containers(self, _done=None):
    93     def fti_containers(self, _done=None):
    94         if _done is None:
    94         if _done is None:
   154 
   154 
   155 def merge_weight_dict(maindict, newdict):
   155 def merge_weight_dict(maindict, newdict):
   156     for weight, words in newdict.iteritems():
   156     for weight, words in newdict.iteritems():
   157         maindict.setdefault(weight, []).extend(words)
   157         maindict.setdefault(weight, []).extend(words)
   158 
   158 
   159 class IDownloadableAdapter(EntityAdapter):
   159 class IDownloadableAdapter(view.EntityAdapter):
   160     """interface for downloadable entities"""
   160     """interface for downloadable entities"""
   161     __needs_bw_compat__ = True
   161     __needs_bw_compat__ = True
   162     __regid__ = 'IDownloadable'
   162     __regid__ = 'IDownloadable'
   163     __select__ = implements(IDownloadable, warn=False) # XXX for bw compat, else should be abstract
   163     __select__ = implements(IDownloadable, warn=False) # XXX for bw compat, else should be abstract
   164 
   164 
   165     @implements_adapter_compat('IDownloadable')
   165     @view.implements_adapter_compat('IDownloadable')
   166     def download_url(self, **kwargs): # XXX not really part of this interface
   166     def download_url(self, **kwargs): # XXX not really part of this interface
   167         """return an url to download entity's content"""
   167         """return an url to download entity's content"""
   168         raise NotImplementedError
   168         raise NotImplementedError
   169     @implements_adapter_compat('IDownloadable')
   169     @view.implements_adapter_compat('IDownloadable')
   170     def download_content_type(self):
   170     def download_content_type(self):
   171         """return MIME type of the downloadable content"""
   171         """return MIME type of the downloadable content"""
   172         raise NotImplementedError
   172         raise NotImplementedError
   173     @implements_adapter_compat('IDownloadable')
   173     @view.implements_adapter_compat('IDownloadable')
   174     def download_encoding(self):
   174     def download_encoding(self):
   175         """return encoding of the downloadable content"""
   175         """return encoding of the downloadable content"""
   176         raise NotImplementedError
   176         raise NotImplementedError
   177     @implements_adapter_compat('IDownloadable')
   177     @view.implements_adapter_compat('IDownloadable')
   178     def download_file_name(self):
   178     def download_file_name(self):
   179         """return file name of the downloadable content"""
   179         """return file name of the downloadable content"""
   180         raise NotImplementedError
   180         raise NotImplementedError
   181     @implements_adapter_compat('IDownloadable')
   181     @view.implements_adapter_compat('IDownloadable')
   182     def download_data(self):
   182     def download_data(self):
   183         """return actual data of the downloadable content"""
   183         """return actual data of the downloadable content"""
   184         raise NotImplementedError
   184         raise NotImplementedError
   185 
   185 
   186 # XXX should propose to use two different relations for children/parent
   186 # XXX should propose to use two different relations for children/parent
   187 class ITreeAdapter(EntityAdapter):
   187 class ITreeAdapter(view.EntityAdapter):
   188     """This adapter has to be overriden to be configured using the
   188     """This adapter has to be overriden to be configured using the
   189     tree_relation, child_role and parent_role class attributes to benefit from
   189     tree_relation, child_role and parent_role class attributes to benefit from
   190     this default implementation.
   190     this default implementation.
   191 
   191 
   192     This adapter provides a tree interface. It has to be overriden to be
   192     This adapter provides a tree interface. It has to be overriden to be
   223              'ITree for %s instead' % (self.entity.__class__),
   223              'ITree for %s instead' % (self.entity.__class__),
   224              DeprecationWarning)
   224              DeprecationWarning)
   225         return self.entity.tree_attribute
   225         return self.entity.tree_attribute
   226 
   226 
   227     # XXX should be removed from the public interface
   227     # XXX should be removed from the public interface
   228     @implements_adapter_compat('ITree')
   228     @view.implements_adapter_compat('ITree')
   229     def children_rql(self):
   229     def children_rql(self):
   230         """Returns RQL to get the children of the entity."""
   230         """Returns RQL to get the children of the entity."""
   231         return self.entity.cw_related_rql(self.tree_relation, self.parent_role)
   231         return self.entity.cw_related_rql(self.tree_relation, self.parent_role)
   232 
   232 
   233     @implements_adapter_compat('ITree')
   233     @view.implements_adapter_compat('ITree')
   234     def different_type_children(self, entities=True):
   234     def different_type_children(self, entities=True):
   235         """Return children entities of different type as this entity.
   235         """Return children entities of different type as this entity.
   236 
   236 
   237         According to the `entities` parameter, return entity objects or the
   237         According to the `entities` parameter, return entity objects or the
   238         equivalent result set.
   238         equivalent result set.
   242         eschema = self.entity.e_schema
   242         eschema = self.entity.e_schema
   243         if entities:
   243         if entities:
   244             return [e for e in res if e.e_schema != eschema]
   244             return [e for e in res if e.e_schema != eschema]
   245         return res.filtered_rset(lambda x: x.e_schema != eschema, self.entity.cw_col)
   245         return res.filtered_rset(lambda x: x.e_schema != eschema, self.entity.cw_col)
   246 
   246 
   247     @implements_adapter_compat('ITree')
   247     @view.implements_adapter_compat('ITree')
   248     def same_type_children(self, entities=True):
   248     def same_type_children(self, entities=True):
   249         """Return children entities of the same type as this entity.
   249         """Return children entities of the same type as this entity.
   250 
   250 
   251         According to the `entities` parameter, return entity objects or the
   251         According to the `entities` parameter, return entity objects or the
   252         equivalent result set.
   252         equivalent result set.
   256         eschema = self.entity.e_schema
   256         eschema = self.entity.e_schema
   257         if entities:
   257         if entities:
   258             return [e for e in res if e.e_schema == eschema]
   258             return [e for e in res if e.e_schema == eschema]
   259         return res.filtered_rset(lambda x: x.e_schema is eschema, self.entity.cw_col)
   259         return res.filtered_rset(lambda x: x.e_schema is eschema, self.entity.cw_col)
   260 
   260 
   261     @implements_adapter_compat('ITree')
   261     @view.implements_adapter_compat('ITree')
   262     def is_leaf(self):
   262     def is_leaf(self):
   263         """Returns True if the entity does not have any children."""
   263         """Returns True if the entity does not have any children."""
   264         return len(self.children()) == 0
   264         return len(self.children()) == 0
   265 
   265 
   266     @implements_adapter_compat('ITree')
   266     @view.implements_adapter_compat('ITree')
   267     def is_root(self):
   267     def is_root(self):
   268         """Returns true if the entity is root of the tree (e.g. has no parent).
   268         """Returns true if the entity is root of the tree (e.g. has no parent).
   269         """
   269         """
   270         return self.parent() is None
   270         return self.parent() is None
   271 
   271 
   272     @implements_adapter_compat('ITree')
   272     @view.implements_adapter_compat('ITree')
   273     def root(self):
   273     def root(self):
   274         """Return the root entity of the tree."""
   274         """Return the root entity of the tree."""
   275         return self._cw.entity_from_eid(self.path()[0])
   275         return self._cw.entity_from_eid(self.path()[0])
   276 
   276 
   277     @implements_adapter_compat('ITree')
   277     @view.implements_adapter_compat('ITree')
   278     def parent(self):
   278     def parent(self):
   279         """Returns the parent entity if any, else None (e.g. if we are on the
   279         """Returns the parent entity if any, else None (e.g. if we are on the
   280         root).
   280         root).
   281         """
   281         """
   282         try:
   282         try:
   283             return self.entity.related(self.tree_relation, self.child_role,
   283             return self.entity.related(self.tree_relation, self.child_role,
   284                                        entities=True)[0]
   284                                        entities=True)[0]
   285         except (KeyError, IndexError):
   285         except (KeyError, IndexError):
   286             return None
   286             return None
   287 
   287 
   288     @implements_adapter_compat('ITree')
   288     @view.implements_adapter_compat('ITree')
   289     def children(self, entities=True, sametype=False):
   289     def children(self, entities=True, sametype=False):
   290         """Return children entities.
   290         """Return children entities.
   291 
   291 
   292         According to the `entities` parameter, return entity objects or the
   292         According to the `entities` parameter, return entity objects or the
   293         equivalent result set.
   293         equivalent result set.
   296             return self.same_type_children(entities)
   296             return self.same_type_children(entities)
   297         else:
   297         else:
   298             return self.entity.related(self.tree_relation, self.parent_role,
   298             return self.entity.related(self.tree_relation, self.parent_role,
   299                                        entities=entities)
   299                                        entities=entities)
   300 
   300 
   301     @implements_adapter_compat('ITree')
   301     @view.implements_adapter_compat('ITree')
   302     def iterparents(self, strict=True):
   302     def iterparents(self, strict=True):
   303         """Return an iterator on the parents of the entity."""
   303         """Return an iterator on the parents of the entity."""
   304         def _uptoroot(self):
   304         def _uptoroot(self):
   305             curr = self
   305             curr = self
   306             while True:
   306             while True:
   311                 curr = curr.cw_adapt_to('ITree')
   311                 curr = curr.cw_adapt_to('ITree')
   312         if not strict:
   312         if not strict:
   313             return chain([self.entity], _uptoroot(self))
   313             return chain([self.entity], _uptoroot(self))
   314         return _uptoroot(self)
   314         return _uptoroot(self)
   315 
   315 
   316     @implements_adapter_compat('ITree')
   316     @view.implements_adapter_compat('ITree')
   317     def iterchildren(self, _done=None):
   317     def iterchildren(self, _done=None):
   318         """Return an iterator over the item's children."""
   318         """Return an iterator over the item's children."""
   319         if _done is None:
   319         if _done is None:
   320             _done = set()
   320             _done = set()
   321         for child in self.children():
   321         for child in self.children():
   323                 self.error('loop in %s tree: %s', child.__regid__.lower(), child)
   323                 self.error('loop in %s tree: %s', child.__regid__.lower(), child)
   324                 continue
   324                 continue
   325             yield child
   325             yield child
   326             _done.add(child.eid)
   326             _done.add(child.eid)
   327 
   327 
   328     @implements_adapter_compat('ITree')
   328     @view.implements_adapter_compat('ITree')
   329     def prefixiter(self, _done=None):
   329     def prefixiter(self, _done=None):
   330         """Return an iterator over the item's descendants in a prefixed order."""
   330         """Return an iterator over the item's descendants in a prefixed order."""
   331         if _done is None:
   331         if _done is None:
   332             _done = set()
   332             _done = set()
   333         if self.entity.eid in _done:
   333         if self.entity.eid in _done:
   336         yield self.entity
   336         yield self.entity
   337         for child in self.same_type_children():
   337         for child in self.same_type_children():
   338             for entity in child.cw_adapt_to('ITree').prefixiter(_done):
   338             for entity in child.cw_adapt_to('ITree').prefixiter(_done):
   339                 yield entity
   339                 yield entity
   340 
   340 
   341     @implements_adapter_compat('ITree')
   341     @view.implements_adapter_compat('ITree')
   342     @cached
   342     @cached
   343     def path(self):
   343     def path(self):
   344         """Returns the list of eids from the root object to this object."""
   344         """Returns the list of eids from the root object to this object."""
   345         path = []
   345         path = []
   346         adapter = self
   346         adapter = self
   361                 break
   361                 break
   362         path.reverse()
   362         path.reverse()
   363         return path
   363         return path
   364 
   364 
   365 
   365 
   366 class IProgressAdapter(EntityAdapter):
   366 # error handling adapters ######################################################
       
   367 
       
   368 from cubicweb import UniqueTogetherError
       
   369 
       
   370 class IUserFriendlyError(view.EntityAdapter):
       
   371     __regid__ = 'IUserFriendlyError'
       
   372     __abstract__ = True
       
   373     def __init__(self, *args, **kwargs):
       
   374         self.exc = kwargs.pop('exc')
       
   375         super(IUserFriendlyError, self).__init__(*args, **kwargs)
       
   376 
       
   377 
       
   378 class IUserFriendlyUniqueTogether(IUserFriendlyError):
       
   379     __select__ = match_exception(UniqueTogetherError)
       
   380     def raise_user_exception(self):
       
   381         etype, rtypes = self.exc.args
       
   382         msg = self._cw._('violates unique_together constraints (%s)') % (
       
   383             ', '.join([self._cw._(rtype) for rtype in rtypes]))
       
   384         raise ValidationError(self.entity.eid, dict((col, msg) for col in rtypes))
       
   385 
       
   386 # deprecated ###################################################################
       
   387 
       
   388 
       
   389 class adapter_deprecated(view.auto_unwrap_bw_compat):
       
   390     """metaclass to print a warning on instantiation of a deprecated class"""
       
   391 
       
   392     def __call__(cls, *args, **kwargs):
       
   393         msg = getattr(cls, "__deprecation_warning__",
       
   394                       "%(cls)s is deprecated") % {'cls': cls.__name__}
       
   395         warn(msg, DeprecationWarning, stacklevel=2)
       
   396         return type.__call__(cls, *args, **kwargs)
       
   397 
       
   398 
       
   399 class IProgressAdapter(view.EntityAdapter):
   367     """something that has a cost, a state and a progression.
   400     """something that has a cost, a state and a progression.
   368 
   401 
   369     You should at least override progress_info an in_progress methods on
   402     You should at least override progress_info an in_progress methods on
   370     concrete implementations.
   403     concrete implementations.
   371     """
   404     """
       
   405     __metaclass__ = adapter_deprecated
       
   406     __deprecation_warning__ = '[3.14] IProgressAdapter has been moved to iprogress cube'
   372     __needs_bw_compat__ = True
   407     __needs_bw_compat__ = True
   373     __regid__ = 'IProgress'
   408     __regid__ = 'IProgress'
   374     __select__ = implements(IProgress, warn=False) # XXX for bw compat, should be abstract
   409     __select__ = implements(IProgress, warn=False) # XXX for bw compat, should be abstract
   375 
   410 
   376     @property
   411     @property
   377     @implements_adapter_compat('IProgress')
   412     @view.implements_adapter_compat('IProgress')
   378     def cost(self):
   413     def cost(self):
   379         """the total cost"""
   414         """the total cost"""
   380         return self.progress_info()['estimated']
   415         return self.progress_info()['estimated']
   381 
   416 
   382     @property
   417     @property
   383     @implements_adapter_compat('IProgress')
   418     @view.implements_adapter_compat('IProgress')
   384     def revised_cost(self):
   419     def revised_cost(self):
   385         return self.progress_info().get('estimatedcorrected', self.cost)
   420         return self.progress_info().get('estimatedcorrected', self.cost)
   386 
   421 
   387     @property
   422     @property
   388     @implements_adapter_compat('IProgress')
   423     @view.implements_adapter_compat('IProgress')
   389     def done(self):
   424     def done(self):
   390         """what is already done"""
   425         """what is already done"""
   391         return self.progress_info()['done']
   426         return self.progress_info()['done']
   392 
   427 
   393     @property
   428     @property
   394     @implements_adapter_compat('IProgress')
   429     @view.implements_adapter_compat('IProgress')
   395     def todo(self):
   430     def todo(self):
   396         """what remains to be done"""
   431         """what remains to be done"""
   397         return self.progress_info()['todo']
   432         return self.progress_info()['todo']
   398 
   433 
   399     @implements_adapter_compat('IProgress')
   434     @view.implements_adapter_compat('IProgress')
   400     def progress_info(self):
   435     def progress_info(self):
   401         """returns a dictionary describing progress/estimated cost of the
   436         """returns a dictionary describing progress/estimated cost of the
   402         version.
   437         version.
   403 
   438 
   404         - mandatory keys are (''estimated', 'done', 'todo')
   439         - mandatory keys are (''estimated', 'done', 'todo')
   409         'noestimated' and 'notestimatedcorrected' should default to 0
   444         'noestimated' and 'notestimatedcorrected' should default to 0
   410         'estimatedcorrected' should default to 'estimated'
   445         'estimatedcorrected' should default to 'estimated'
   411         """
   446         """
   412         raise NotImplementedError
   447         raise NotImplementedError
   413 
   448 
   414     @implements_adapter_compat('IProgress')
   449     @view.implements_adapter_compat('IProgress')
   415     def finished(self):
   450     def finished(self):
   416         """returns True if status is finished"""
   451         """returns True if status is finished"""
   417         return not self.in_progress()
   452         return not self.in_progress()
   418 
   453 
   419     @implements_adapter_compat('IProgress')
   454     @view.implements_adapter_compat('IProgress')
   420     def in_progress(self):
   455     def in_progress(self):
   421         """returns True if status is not finished"""
   456         """returns True if status is not finished"""
   422         raise NotImplementedError
   457         raise NotImplementedError
   423 
   458 
   424     @implements_adapter_compat('IProgress')
   459     @view.implements_adapter_compat('IProgress')
   425     def progress(self):
   460     def progress(self):
   426         """returns the % progress of the task item"""
   461         """returns the % progress of the task item"""
   427         try:
   462         try:
   428             return 100. * self.done / self.revised_cost
   463             return 100. * self.done / self.revised_cost
   429         except ZeroDivisionError:
   464         except ZeroDivisionError:
   430             # total cost is 0 : if everything was estimated, task is completed
   465             # total cost is 0 : if everything was estimated, task is completed
   431             if self.progress_info().get('notestimated'):
   466             if self.progress_info().get('notestimated'):
   432                 return 0.
   467                 return 0.
   433             return 100
   468             return 100
   434 
   469 
   435     @implements_adapter_compat('IProgress')
   470     @view.implements_adapter_compat('IProgress')
   436     def progress_class(self):
   471     def progress_class(self):
   437         return ''
   472         return ''
   438 
   473 
   439 
   474 
   440 class IMileStoneAdapter(IProgressAdapter):
   475 class IMileStoneAdapter(IProgressAdapter):
       
   476     __metaclass__ = adapter_deprecated
       
   477     __deprecation_warning__ = '[3.14] IMileStoneAdapter has been moved to iprogress cube'
   441     __needs_bw_compat__ = True
   478     __needs_bw_compat__ = True
   442     __regid__ = 'IMileStone'
   479     __regid__ = 'IMileStone'
   443     __select__ = implements(IMileStone, warn=False) # XXX for bw compat, should be abstract
   480     __select__ = implements(IMileStone, warn=False) # XXX for bw compat, should be abstract
   444 
   481 
   445     parent_type = None # specify main task's type
   482     parent_type = None # specify main task's type
   446 
   483 
   447     @implements_adapter_compat('IMileStone')
   484     @view.implements_adapter_compat('IMileStone')
   448     def get_main_task(self):
   485     def get_main_task(self):
   449         """returns the main ITask entity"""
   486         """returns the main ITask entity"""
   450         raise NotImplementedError
   487         raise NotImplementedError
   451 
   488 
   452     @implements_adapter_compat('IMileStone')
   489     @view.implements_adapter_compat('IMileStone')
   453     def initial_prevision_date(self):
   490     def initial_prevision_date(self):
   454         """returns the initial expected end of the milestone"""
   491         """returns the initial expected end of the milestone"""
   455         raise NotImplementedError
   492         raise NotImplementedError
   456 
   493 
   457     @implements_adapter_compat('IMileStone')
   494     @view.implements_adapter_compat('IMileStone')
   458     def eta_date(self):
   495     def eta_date(self):
   459         """returns expected date of completion based on what remains
   496         """returns expected date of completion based on what remains
   460         to be done
   497         to be done
   461         """
   498         """
   462         raise NotImplementedError
   499         raise NotImplementedError
   463 
   500 
   464     @implements_adapter_compat('IMileStone')
   501     @view.implements_adapter_compat('IMileStone')
   465     def completion_date(self):
   502     def completion_date(self):
   466         """returns date on which the subtask has been completed"""
   503         """returns date on which the subtask has been completed"""
   467         raise NotImplementedError
   504         raise NotImplementedError
   468 
   505 
   469     @implements_adapter_compat('IMileStone')
   506     @view.implements_adapter_compat('IMileStone')
   470     def contractors(self):
   507     def contractors(self):
   471         """returns the list of persons supposed to work on this task"""
   508         """returns the list of persons supposed to work on this task"""
   472         raise NotImplementedError
   509         raise NotImplementedError
   473 
   510 
   474 
       
   475 # error handling adapters ######################################################
       
   476 
       
   477 from cubicweb import UniqueTogetherError
       
   478 
       
   479 class IUserFriendlyError(EntityAdapter):
       
   480     __regid__ = 'IUserFriendlyError'
       
   481     __abstract__ = True
       
   482     def __init__(self, *args, **kwargs):
       
   483         self.exc = kwargs.pop('exc')
       
   484         super(IUserFriendlyError, self).__init__(*args, **kwargs)
       
   485 
       
   486 
       
   487 class IUserFriendlyUniqueTogether(IUserFriendlyError):
       
   488     __select__ = match_exception(UniqueTogetherError)
       
   489     def raise_user_exception(self):
       
   490         etype, rtypes = self.exc.args
       
   491         msg = self._cw._('violates unique_together constraints (%s)') % (
       
   492             ', '.join([self._cw._(rtype) for rtype in rtypes]))
       
   493         raise ValidationError(self.entity.eid, dict((col, msg) for col in rtypes))