mixins.py
changeset 4023 eae23c40627a
parent 3998 94cc7cad3d2d
child 4252 6c4f109c2b03
equal deleted inserted replaced
4022:934e758a73ef 4023:eae23c40627a
       
     1 """mixins of entity/views organized somewhat in a graph or tree structure
       
     2 
       
     3 
       
     4 :organization: Logilab
       
     5 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
       
     6 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     7 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
       
     8 """
       
     9 __docformat__ = "restructuredtext en"
       
    10 
       
    11 from logilab.common.deprecation import deprecated
       
    12 from logilab.common.decorators import cached
       
    13 
       
    14 from cubicweb import typed_eid
       
    15 from cubicweb.selectors import implements
       
    16 from cubicweb.interfaces import IEmailable, ITree
       
    17 
       
    18 
       
    19 class TreeMixIn(object):
       
    20     """base tree-mixin providing the tree interface
       
    21 
       
    22     This mixin has to be inherited explicitly and configured using the
       
    23     tree_attribute, parent_target and children_target class attribute to
       
    24     benefit from this default implementation
       
    25     """
       
    26     tree_attribute = None
       
    27     # XXX misnamed
       
    28     parent_target = 'subject'
       
    29     children_target = 'object'
       
    30 
       
    31     def different_type_children(self, entities=True):
       
    32         """return children entities of different type as this entity.
       
    33 
       
    34         according to the `entities` parameter, return entity objects or the
       
    35         equivalent result set
       
    36         """
       
    37         res = self.related(self.tree_attribute, self.children_target,
       
    38                            entities=entities)
       
    39         if entities:
       
    40             return [e for e in res if e.e_schema != self.e_schema]
       
    41         return res.filtered_rset(lambda x: x.e_schema != self.e_schema, self.cw_col)
       
    42 
       
    43     def same_type_children(self, entities=True):
       
    44         """return children entities of the same type as this entity.
       
    45 
       
    46         according to the `entities` parameter, return entity objects or the
       
    47         equivalent result set
       
    48         """
       
    49         res = self.related(self.tree_attribute, self.children_target,
       
    50                            entities=entities)
       
    51         if entities:
       
    52             return [e for e in res if e.e_schema == self.e_schema]
       
    53         return res.filtered_rset(lambda x: x.e_schema == self.e_schema, self.cw_col)
       
    54 
       
    55     def iterchildren(self, _done=None):
       
    56         if _done is None:
       
    57             _done = set()
       
    58         for child in self.children():
       
    59             if child.eid in _done:
       
    60                 self.error('loop in %s tree', self.__regid__.lower())
       
    61                 continue
       
    62             yield child
       
    63             _done.add(child.eid)
       
    64 
       
    65     def prefixiter(self, _done=None):
       
    66         if _done is None:
       
    67             _done = set()
       
    68         if self.eid in _done:
       
    69             return
       
    70         yield self
       
    71         _done.add(self.eid)
       
    72         for child in self.iterchildren(_done):
       
    73             try:
       
    74                 for entity in child.prefixiter(_done):
       
    75                     yield entity
       
    76             except AttributeError:
       
    77                 pass
       
    78 
       
    79     @cached
       
    80     def path(self):
       
    81         """returns the list of eids from the root object to this object"""
       
    82         path = []
       
    83         parent = self
       
    84         while parent:
       
    85             if parent.eid in path:
       
    86                 self.error('loop in %s tree', self.__regid__.lower())
       
    87                 break
       
    88             path.append(parent.eid)
       
    89             try:
       
    90                 # check we are not leaving the tree
       
    91                 if (parent.tree_attribute != self.tree_attribute or
       
    92                     parent.parent_target != self.parent_target):
       
    93                     break
       
    94                 parent = parent.parent()
       
    95             except AttributeError:
       
    96                 break
       
    97 
       
    98         path.reverse()
       
    99         return path
       
   100 
       
   101     def iterparents(self):
       
   102         def _uptoroot(self):
       
   103             curr = self
       
   104             while True:
       
   105                 curr = curr.parent()
       
   106                 if curr is None:
       
   107                     break
       
   108                 yield curr
       
   109         return _uptoroot(self)
       
   110 
       
   111     def notification_references(self, view):
       
   112         """used to control References field of email send on notification
       
   113         for this entity. `view` is the notification view.
       
   114 
       
   115         Should return a list of eids which can be used to generate message ids
       
   116         of previously sent email
       
   117         """
       
   118         return self.path()[:-1]
       
   119 
       
   120 
       
   121     ## ITree interface ########################################################
       
   122     def parent(self):
       
   123         """return the parent entity if any, else None (e.g. if we are on the
       
   124         root
       
   125         """
       
   126         try:
       
   127             return self.related(self.tree_attribute, self.parent_target,
       
   128                                 entities=True)[0]
       
   129         except (KeyError, IndexError):
       
   130             return None
       
   131 
       
   132     def children(self, entities=True, sametype=False):
       
   133         """return children entities
       
   134 
       
   135         according to the `entities` parameter, return entity objects or the
       
   136         equivalent result set
       
   137         """
       
   138         if sametype:
       
   139             return self.same_type_children(entities)
       
   140         else:
       
   141             return self.related(self.tree_attribute, self.children_target,
       
   142                                 entities=entities)
       
   143 
       
   144     def children_rql(self):
       
   145         return self.related_rql(self.tree_attribute, self.children_target)
       
   146 
       
   147     def is_leaf(self):
       
   148         return len(self.children()) == 0
       
   149 
       
   150     def is_root(self):
       
   151         return self.parent() is None
       
   152 
       
   153     def root(self):
       
   154         """return the root object"""
       
   155         return self._cw.entity_from_eid(self.path()[0])
       
   156 
       
   157 
       
   158 class EmailableMixIn(object):
       
   159     """base mixin providing the default get_email() method used by
       
   160     the massmailing view
       
   161 
       
   162     NOTE: The default implementation is based on the
       
   163     primary_email / use_email scheme
       
   164     """
       
   165     __implements__ = (IEmailable,)
       
   166 
       
   167     def get_email(self):
       
   168         if getattr(self, 'primary_email', None):
       
   169             return self.primary_email[0].address
       
   170         if getattr(self, 'use_email', None):
       
   171             return self.use_email[0].address
       
   172         return None
       
   173 
       
   174     @classmethod
       
   175     def allowed_massmail_keys(cls):
       
   176         """returns a set of allowed email substitution keys
       
   177 
       
   178         The default is to return the entity's attribute list but an
       
   179         entity class might override this method to allow extra keys.
       
   180         For instance, the Person class might want to return a `companyname`
       
   181         key.
       
   182         """
       
   183         return set(rschema.type
       
   184                    for rschema, attrtype in cls.e_schema.attribute_definitions()
       
   185                    if attrtype.type not in ('Password', 'Bytes'))
       
   186 
       
   187     def as_email_context(self):
       
   188         """returns the dictionary as used by the sendmail controller to
       
   189         build email bodies.
       
   190 
       
   191         NOTE: the dictionary keys should match the list returned by the
       
   192         `allowed_massmail_keys` method.
       
   193         """
       
   194         return dict( (attr, getattr(self, attr)) for attr in self.allowed_massmail_keys() )
       
   195 
       
   196 
       
   197 """pluggable mixins system: plug classes registered in MI_REL_TRIGGERS on entity
       
   198 classes which have the relation described by the dict's key.
       
   199 
       
   200 NOTE: pluggable mixins can't override any method of the 'explicit' user classes tree
       
   201 (eg without plugged classes). This includes bases Entity and AnyEntity classes.
       
   202 """
       
   203 MI_REL_TRIGGERS = {
       
   204     ('primary_email',   'subject'): EmailableMixIn,
       
   205     ('use_email',   'subject'): EmailableMixIn,
       
   206     }
       
   207 
       
   208 
       
   209 
       
   210 def _done_init(done, view, row, col):
       
   211     """handle an infinite recursion safety belt"""
       
   212     if done is None:
       
   213         done = set()
       
   214     entity = view.cw_rset.get_entity(row, col)
       
   215     if entity.eid in done:
       
   216         msg = entity._cw._('loop in %(rel)s relation (%(eid)s)') % {
       
   217             'rel': entity.tree_attribute,
       
   218             'eid': entity.eid
       
   219             }
       
   220         return None, msg
       
   221     done.add(entity.eid)
       
   222     return done, entity
       
   223 
       
   224 
       
   225 class TreeViewMixIn(object):
       
   226     """a recursive tree view"""
       
   227     __regid__ = 'tree'
       
   228     item_vid = 'treeitem'
       
   229     __select__ = implements(ITree)
       
   230 
       
   231     def call(self, done=None, **kwargs):
       
   232         if done is None:
       
   233             done = set()
       
   234         super(TreeViewMixIn, self).call(done=done, **kwargs)
       
   235 
       
   236     def cell_call(self, row, col=0, vid=None, done=None, **kwargs):
       
   237         done, entity = _done_init(done, self, row, col)
       
   238         if done is None:
       
   239             # entity is actually an error message
       
   240             self.w(u'<li class="badcontent">%s</li>' % entity)
       
   241             return
       
   242         self.open_item(entity)
       
   243         entity.view(vid or self.item_vid, w=self.w, **kwargs)
       
   244         relatedrset = entity.children(entities=False)
       
   245         self.wview(self.__regid__, relatedrset, 'null', done=done, **kwargs)
       
   246         self.close_item(entity)
       
   247 
       
   248     def open_item(self, entity):
       
   249         self.w(u'<li class="%s">\n' % entity.__regid__.lower())
       
   250     def close_item(self, entity):
       
   251         self.w(u'</li>\n')
       
   252 
       
   253 
       
   254 class TreePathMixIn(object):
       
   255     """a recursive path view"""
       
   256     __regid__ = 'path'
       
   257     item_vid = 'oneline'
       
   258     separator = u'&#160;&gt;&#160;'
       
   259 
       
   260     def call(self, **kwargs):
       
   261         self.w(u'<div class="pathbar">')
       
   262         super(TreePathMixIn, self).call(**kwargs)
       
   263         self.w(u'</div>')
       
   264 
       
   265     def cell_call(self, row, col=0, vid=None, done=None, **kwargs):
       
   266         done, entity = _done_init(done, self, row, col)
       
   267         if done is None:
       
   268             # entity is actually an error message
       
   269             self.w(u'<span class="badcontent">%s</span>' % entity)
       
   270             return
       
   271         parent = entity.parent()
       
   272         if parent:
       
   273             parent.view(self.__regid__, w=self.w, done=done)
       
   274             self.w(self.separator)
       
   275         entity.view(vid or self.item_vid, w=self.w)
       
   276 
       
   277 
       
   278 class ProgressMixIn(object):
       
   279     """provide default implementations for IProgress interface methods"""
       
   280     # This is an adapter isn't it ?
       
   281 
       
   282     @property
       
   283     def cost(self):
       
   284         return self.progress_info()['estimated']
       
   285 
       
   286     @property
       
   287     def revised_cost(self):
       
   288         return self.progress_info().get('estimatedcorrected', self.cost)
       
   289 
       
   290     @property
       
   291     def done(self):
       
   292         return self.progress_info()['done']
       
   293 
       
   294     @property
       
   295     def todo(self):
       
   296         return self.progress_info()['todo']
       
   297 
       
   298     @cached
       
   299     def progress_info(self):
       
   300         raise NotImplementedError()
       
   301 
       
   302     def finished(self):
       
   303         return not self.in_progress()
       
   304 
       
   305     def in_progress(self):
       
   306         raise NotImplementedError()
       
   307 
       
   308     def progress(self):
       
   309         try:
       
   310             return 100. * self.done / self.revised_cost
       
   311         except ZeroDivisionError:
       
   312             # total cost is 0 : if everything was estimated, task is completed
       
   313             if self.progress_info().get('notestimated'):
       
   314                 return 0.
       
   315             return 100