mixins.py
brancholdstable
changeset 4985 02b52bf9f5f8
parent 4719 aaed3f813ef8
child 5309 e8567135a927
equal deleted inserted replaced
4563:c25da7573ebd 4985:02b52bf9f5f8
       
     1 """mixins of entity/views organized somewhat in a graph or tree structure
       
     2 
       
     3 
       
     4 :organization: Logilab
       
     5 :copyright: 2001-2010 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 itertools import chain
       
    12 
       
    13 from logilab.common.decorators import cached
       
    14 
       
    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 is 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         _done.add(self.eid)
       
    71         yield self
       
    72         for child in self.same_type_children():
       
    73             for entity in child.prefixiter(_done):
       
    74                 yield entity
       
    75 
       
    76     @cached
       
    77     def path(self):
       
    78         """returns the list of eids from the root object to this object"""
       
    79         path = []
       
    80         parent = self
       
    81         while parent:
       
    82             if parent.eid in path:
       
    83                 self.error('loop in %s tree', self.__regid__.lower())
       
    84                 break
       
    85             path.append(parent.eid)
       
    86             try:
       
    87                 # check we are not leaving the tree
       
    88                 if (parent.tree_attribute != self.tree_attribute or
       
    89                     parent.parent_target != self.parent_target):
       
    90                     break
       
    91                 parent = parent.parent()
       
    92             except AttributeError:
       
    93                 break
       
    94 
       
    95         path.reverse()
       
    96         return path
       
    97 
       
    98     def iterparents(self, strict=True):
       
    99         def _uptoroot(self):
       
   100             curr = self
       
   101             while True:
       
   102                 curr = curr.parent()
       
   103                 if curr is None:
       
   104                     break
       
   105                 yield curr
       
   106         if not strict:
       
   107             return chain([self], _uptoroot(self))
       
   108         return _uptoroot(self)
       
   109 
       
   110     def notification_references(self, view):
       
   111         """used to control References field of email send on notification
       
   112         for this entity. `view` is the notification view.
       
   113 
       
   114         Should return a list of eids which can be used to generate message ids
       
   115         of previously sent email
       
   116         """
       
   117         return self.path()[:-1]
       
   118 
       
   119 
       
   120     ## ITree interface ########################################################
       
   121     def parent(self):
       
   122         """return the parent entity if any, else None (e.g. if we are on the
       
   123         root
       
   124         """
       
   125         try:
       
   126             return self.related(self.tree_attribute, self.parent_target,
       
   127                                 entities=True)[0]
       
   128         except (KeyError, IndexError):
       
   129             return None
       
   130 
       
   131     def children(self, entities=True, sametype=False):
       
   132         """return children entities
       
   133 
       
   134         according to the `entities` parameter, return entity objects or the
       
   135         equivalent result set
       
   136         """
       
   137         if sametype:
       
   138             return self.same_type_children(entities)
       
   139         else:
       
   140             return self.related(self.tree_attribute, self.children_target,
       
   141                                 entities=entities)
       
   142 
       
   143     def children_rql(self):
       
   144         return self.related_rql(self.tree_attribute, self.children_target)
       
   145 
       
   146     def is_leaf(self):
       
   147         return len(self.children()) == 0
       
   148 
       
   149     def is_root(self):
       
   150         return self.parent() is None
       
   151 
       
   152     def root(self):
       
   153         """return the root object"""
       
   154         return self._cw.entity_from_eid(self.path()[0])
       
   155 
       
   156 
       
   157 class EmailableMixIn(object):
       
   158     """base mixin providing the default get_email() method used by
       
   159     the massmailing view
       
   160 
       
   161     NOTE: The default implementation is based on the
       
   162     primary_email / use_email scheme
       
   163     """
       
   164     __implements__ = (IEmailable,)
       
   165 
       
   166     def get_email(self):
       
   167         if getattr(self, 'primary_email', None):
       
   168             return self.primary_email[0].address
       
   169         if getattr(self, 'use_email', None):
       
   170             return self.use_email[0].address
       
   171         return None
       
   172 
       
   173     @classmethod
       
   174     def allowed_massmail_keys(cls):
       
   175         """returns a set of allowed email substitution keys
       
   176 
       
   177         The default is to return the entity's attribute list but an
       
   178         entity class might override this method to allow extra keys.
       
   179         For instance, the Person class might want to return a `companyname`
       
   180         key.
       
   181         """
       
   182         return set(rschema.type
       
   183                    for rschema, attrtype in cls.e_schema.attribute_definitions()
       
   184                    if attrtype.type not in ('Password', 'Bytes'))
       
   185 
       
   186     def as_email_context(self):
       
   187         """returns the dictionary as used by the sendmail controller to
       
   188         build email bodies.
       
   189 
       
   190         NOTE: the dictionary keys should match the list returned by the
       
   191         `allowed_massmail_keys` method.
       
   192         """
       
   193         return dict( (attr, getattr(self, attr)) for attr in self.allowed_massmail_keys() )
       
   194 
       
   195 
       
   196 """pluggable mixins system: plug classes registered in MI_REL_TRIGGERS on entity
       
   197 classes which have the relation described by the dict's key.
       
   198 
       
   199 NOTE: pluggable mixins can't override any method of the 'explicit' user classes tree
       
   200 (eg without plugged classes). This includes bases Entity and AnyEntity classes.
       
   201 """
       
   202 MI_REL_TRIGGERS = {
       
   203     ('primary_email',   'subject'): EmailableMixIn,
       
   204     ('use_email',   'subject'): EmailableMixIn,
       
   205     }
       
   206 
       
   207 
       
   208 
       
   209 def _done_init(done, view, row, col):
       
   210     """handle an infinite recursion safety belt"""
       
   211     if done is None:
       
   212         done = set()
       
   213     entity = view.cw_rset.get_entity(row, col)
       
   214     if entity.eid in done:
       
   215         msg = entity._cw._('loop in %(rel)s relation (%(eid)s)') % {
       
   216             'rel': entity.tree_attribute,
       
   217             'eid': entity.eid
       
   218             }
       
   219         return None, msg
       
   220     done.add(entity.eid)
       
   221     return done, entity
       
   222 
       
   223 
       
   224 class TreeViewMixIn(object):
       
   225     """a recursive tree view"""
       
   226     __regid__ = 'tree'
       
   227     item_vid = 'treeitem'
       
   228     __select__ = implements(ITree)
       
   229 
       
   230     def call(self, done=None, **kwargs):
       
   231         if done is None:
       
   232             done = set()
       
   233         super(TreeViewMixIn, self).call(done=done, **kwargs)
       
   234 
       
   235     def cell_call(self, row, col=0, vid=None, done=None, **kwargs):
       
   236         done, entity = _done_init(done, self, row, col)
       
   237         if done is None:
       
   238             # entity is actually an error message
       
   239             self.w(u'<li class="badcontent">%s</li>' % entity)
       
   240             return
       
   241         self.open_item(entity)
       
   242         entity.view(vid or self.item_vid, w=self.w, **kwargs)
       
   243         relatedrset = entity.children(entities=False)
       
   244         self.wview(self.__regid__, relatedrset, 'null', done=done, **kwargs)
       
   245         self.close_item(entity)
       
   246 
       
   247     def open_item(self, entity):
       
   248         self.w(u'<li class="%s">\n' % entity.__regid__.lower())
       
   249     def close_item(self, entity):
       
   250         self.w(u'</li>\n')
       
   251 
       
   252 
       
   253 class TreePathMixIn(object):
       
   254     """a recursive path view"""
       
   255     __regid__ = 'path'
       
   256     item_vid = 'oneline'
       
   257     separator = u'&#160;&gt;&#160;'
       
   258 
       
   259     def call(self, **kwargs):
       
   260         self.w(u'<div class="pathbar">')
       
   261         super(TreePathMixIn, self).call(**kwargs)
       
   262         self.w(u'</div>')
       
   263 
       
   264     def cell_call(self, row, col=0, vid=None, done=None, **kwargs):
       
   265         done, entity = _done_init(done, self, row, col)
       
   266         if done is None:
       
   267             # entity is actually an error message
       
   268             self.w(u'<span class="badcontent">%s</span>' % entity)
       
   269             return
       
   270         parent = entity.parent()
       
   271         if parent:
       
   272             parent.view(self.__regid__, w=self.w, done=done)
       
   273             self.w(self.separator)
       
   274         entity.view(vid or self.item_vid, w=self.w)
       
   275 
       
   276 
       
   277 class ProgressMixIn(object):
       
   278     """provide default implementations for IProgress interface methods"""
       
   279     # This is an adapter isn't it ?
       
   280 
       
   281     @property
       
   282     def cost(self):
       
   283         return self.progress_info()['estimated']
       
   284 
       
   285     @property
       
   286     def revised_cost(self):
       
   287         return self.progress_info().get('estimatedcorrected', self.cost)
       
   288 
       
   289     @property
       
   290     def done(self):
       
   291         return self.progress_info()['done']
       
   292 
       
   293     @property
       
   294     def todo(self):
       
   295         return self.progress_info()['todo']
       
   296 
       
   297     @cached
       
   298     def progress_info(self):
       
   299         raise NotImplementedError()
       
   300 
       
   301     def finished(self):
       
   302         return not self.in_progress()
       
   303 
       
   304     def in_progress(self):
       
   305         raise NotImplementedError()
       
   306 
       
   307     def progress(self):
       
   308         try:
       
   309             return 100. * self.done / self.revised_cost
       
   310         except ZeroDivisionError:
       
   311             # total cost is 0 : if everything was estimated, task is completed
       
   312             if self.progress_info().get('notestimated'):
       
   313                 return 0.
       
   314             return 100