mixins.py
branchstable
changeset 9542 79b9bf88be28
parent 9540 43b4895a150f
parent 9541 e8040107b97e
child 9543 39f981482e34
child 9558 1a719ca9c585
equal deleted inserted replaced
9540:43b4895a150f 9542:79b9bf88be28
     1 # copyright 2003-2012 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 """mixins of entity/views organized somewhat in a graph or tree structure"""
       
    19 __docformat__ = "restructuredtext en"
       
    20 
       
    21 from itertools import chain
       
    22 
       
    23 from logilab.common.decorators import cached
       
    24 from logilab.common.deprecation import deprecated, class_deprecated
       
    25 
       
    26 from cubicweb.predicates import implements
       
    27 from cubicweb.interfaces import ITree
       
    28 
       
    29 
       
    30 class TreeMixIn(object):
       
    31     """base tree-mixin implementing the tree interface
       
    32 
       
    33     This mixin has to be inherited explicitly and configured using the
       
    34     tree_attribute, parent_target and children_target class attribute to
       
    35     benefit from this default implementation
       
    36     """
       
    37     __metaclass__ = class_deprecated
       
    38     __deprecation_warning__ = '[3.9] TreeMixIn is deprecated, use/override ITreeAdapter instead (%(cls)s)'
       
    39 
       
    40     tree_attribute = None
       
    41     # XXX misnamed
       
    42     parent_target = 'subject'
       
    43     children_target = 'object'
       
    44 
       
    45     def different_type_children(self, entities=True):
       
    46         """return children entities of different type as this entity.
       
    47 
       
    48         according to the `entities` parameter, return entity objects or the
       
    49         equivalent result set
       
    50         """
       
    51         res = self.related(self.tree_attribute, self.children_target,
       
    52                            entities=entities)
       
    53         if entities:
       
    54             return [e for e in res if e.e_schema != self.e_schema]
       
    55         return res.filtered_rset(lambda x: x.e_schema != self.e_schema, self.cw_col)
       
    56 
       
    57     def same_type_children(self, entities=True):
       
    58         """return children entities of the same type as this entity.
       
    59 
       
    60         according to the `entities` parameter, return entity objects or the
       
    61         equivalent result set
       
    62         """
       
    63         res = self.related(self.tree_attribute, self.children_target,
       
    64                            entities=entities)
       
    65         if entities:
       
    66             return [e for e in res if e.e_schema == self.e_schema]
       
    67         return res.filtered_rset(lambda x: x.e_schema is self.e_schema, self.cw_col)
       
    68 
       
    69     def iterchildren(self, _done=None):
       
    70         if _done is None:
       
    71             _done = set()
       
    72         for child in self.children():
       
    73             if child.eid in _done:
       
    74                 self.error('loop in %s tree: %s', self.__regid__.lower(), child)
       
    75                 continue
       
    76             yield child
       
    77             _done.add(child.eid)
       
    78 
       
    79     def prefixiter(self, _done=None):
       
    80         if _done is None:
       
    81             _done = set()
       
    82         if self.eid in _done:
       
    83             return
       
    84         _done.add(self.eid)
       
    85         yield self
       
    86         for child in self.same_type_children():
       
    87             for entity in child.prefixiter(_done):
       
    88                 yield entity
       
    89 
       
    90     @cached
       
    91     def path(self):
       
    92         """returns the list of eids from the root object to this object"""
       
    93         path = []
       
    94         parent = self
       
    95         while parent:
       
    96             if parent.eid in path:
       
    97                 self.error('loop in %s tree: %s', self.__regid__.lower(), parent)
       
    98                 break
       
    99             path.append(parent.eid)
       
   100             try:
       
   101                 # check we are not leaving the tree
       
   102                 if (parent.tree_attribute != self.tree_attribute or
       
   103                     parent.parent_target != self.parent_target):
       
   104                     break
       
   105                 parent = parent.parent()
       
   106             except AttributeError:
       
   107                 break
       
   108 
       
   109         path.reverse()
       
   110         return path
       
   111 
       
   112     def iterparents(self, strict=True):
       
   113         def _uptoroot(self):
       
   114             curr = self
       
   115             while True:
       
   116                 curr = curr.parent()
       
   117                 if curr is None:
       
   118                     break
       
   119                 yield curr
       
   120         if not strict:
       
   121             return chain([self], _uptoroot(self))
       
   122         return _uptoroot(self)
       
   123 
       
   124     ## ITree interface ########################################################
       
   125     def parent(self):
       
   126         """return the parent entity if any, else None (e.g. if we are on the
       
   127         root
       
   128         """
       
   129         try:
       
   130             return self.related(self.tree_attribute, self.parent_target,
       
   131                                 entities=True)[0]
       
   132         except (KeyError, IndexError):
       
   133             return None
       
   134 
       
   135     def children(self, entities=True, sametype=False):
       
   136         """return children entities
       
   137 
       
   138         according to the `entities` parameter, return entity objects or the
       
   139         equivalent result set
       
   140         """
       
   141         if sametype:
       
   142             return self.same_type_children(entities)
       
   143         else:
       
   144             return self.related(self.tree_attribute, self.children_target,
       
   145                                 entities=entities)
       
   146 
       
   147     def children_rql(self):
       
   148         return self.cw_related_rql(self.tree_attribute, self.children_target)
       
   149 
       
   150     def is_leaf(self):
       
   151         return len(self.children()) == 0
       
   152 
       
   153     def is_root(self):
       
   154         return self.parent() is None
       
   155 
       
   156     def root(self):
       
   157         """return the root object"""
       
   158         return self._cw.entity_from_eid(self.path()[0])
       
   159 
       
   160 
       
   161 class EmailableMixIn(object):
       
   162     """base mixin providing the default get_email() method used by
       
   163     the massmailing view
       
   164 
       
   165     NOTE: The default implementation is based on the
       
   166     primary_email / use_email scheme
       
   167     """
       
   168     @deprecated("[3.9] use entity.cw_adapt_to('IEmailable').get_email()")
       
   169     def get_email(self):
       
   170         if getattr(self, 'primary_email', None):
       
   171             return self.primary_email[0].address
       
   172         if getattr(self, 'use_email', None):
       
   173             return self.use_email[0].address
       
   174         return None
       
   175 
       
   176 
       
   177 """pluggable mixins system: plug classes registered in MI_REL_TRIGGERS on entity
       
   178 classes which have the relation described by the dict's key.
       
   179 
       
   180 NOTE: pluggable mixins can't override any method of the 'explicit' user classes tree
       
   181 (eg without plugged classes). This includes bases Entity and AnyEntity classes.
       
   182 """
       
   183 MI_REL_TRIGGERS = {
       
   184     ('primary_email',   'subject'): EmailableMixIn,
       
   185     ('use_email',   'subject'): EmailableMixIn,
       
   186     }
       
   187 
       
   188 
       
   189 # XXX move to cubicweb.web.views.treeview once we delete usage from this file
       
   190 def _done_init(done, view, row, col):
       
   191     """handle an infinite recursion safety belt"""
       
   192     if done is None:
       
   193         done = set()
       
   194     entity = view.cw_rset.get_entity(row, col)
       
   195     if entity.eid in done:
       
   196         msg = entity._cw._('loop in %(rel)s relation (%(eid)s)') % {
       
   197             'rel': entity.cw_adapt_to('ITree').tree_relation,
       
   198             'eid': entity.eid
       
   199             }
       
   200         return None, msg
       
   201     done.add(entity.eid)
       
   202     return done, entity
       
   203 
       
   204 
       
   205 class TreeViewMixIn(object):
       
   206     """a recursive tree view"""
       
   207     __metaclass__ = class_deprecated
       
   208     __deprecation_warning__ = '[3.9] TreeViewMixIn is deprecated, use/override BaseTreeView instead (%(cls)s)'
       
   209 
       
   210     __regid__ = 'tree'
       
   211     __select__ = implements(ITree, warn=False)
       
   212     item_vid = 'treeitem'
       
   213 
       
   214     def call(self, done=None, **kwargs):
       
   215         if done is None:
       
   216             done = set()
       
   217         super(TreeViewMixIn, self).call(done=done, **kwargs)
       
   218 
       
   219     def cell_call(self, row, col=0, vid=None, done=None, maxlevel=None, **kwargs):
       
   220         assert maxlevel is None or maxlevel > 0
       
   221         done, entity = _done_init(done, self, row, col)
       
   222         if done is None:
       
   223             # entity is actually an error message
       
   224             self.w(u'<li class="badcontent">%s</li>' % entity)
       
   225             return
       
   226         self.open_item(entity)
       
   227         entity.view(vid or self.item_vid, w=self.w, **kwargs)
       
   228         if maxlevel is not None:
       
   229             maxlevel -= 1
       
   230             if maxlevel == 0:
       
   231                 self.close_item(entity)
       
   232                 return
       
   233         relatedrset = entity.children(entities=False)
       
   234         self.wview(self.__regid__, relatedrset, 'null', done=done,
       
   235                    maxlevel=maxlevel, **kwargs)
       
   236         self.close_item(entity)
       
   237 
       
   238     def open_item(self, entity):
       
   239         self.w(u'<li class="%s">\n' % entity.cw_etype.lower())
       
   240     def close_item(self, entity):
       
   241         self.w(u'</li>\n')
       
   242 
       
   243 
       
   244 class TreePathMixIn(object):
       
   245     """a recursive path view"""
       
   246     __metaclass__ = class_deprecated
       
   247     __deprecation_warning__ = '[3.9] TreePathMixIn is deprecated, use/override TreePathView instead (%(cls)s)'
       
   248     __regid__ = 'path'
       
   249     item_vid = 'oneline'
       
   250     separator = u'&#160;&gt;&#160;'
       
   251 
       
   252     def call(self, **kwargs):
       
   253         self.w(u'<div class="pathbar">')
       
   254         super(TreePathMixIn, self).call(**kwargs)
       
   255         self.w(u'</div>')
       
   256 
       
   257     def cell_call(self, row, col=0, vid=None, done=None, **kwargs):
       
   258         done, entity = _done_init(done, self, row, col)
       
   259         if done is None:
       
   260             # entity is actually an error message
       
   261             self.w(u'<span class="badcontent">%s</span>' % entity)
       
   262             return
       
   263         parent = entity.parent()
       
   264         if parent:
       
   265             parent.view(self.__regid__, w=self.w, done=done)
       
   266             self.w(self.separator)
       
   267         entity.view(vid or self.item_vid, w=self.w)
       
   268 
       
   269 
       
   270 class ProgressMixIn(object):
       
   271     """provide a default implementations for IProgress interface methods"""
       
   272     __metaclass__ = class_deprecated
       
   273     __deprecation_warning__ = '[3.9] ProgressMixIn is deprecated, use/override IProgressAdapter instead (%(cls)s)'
       
   274 
       
   275     @property
       
   276     def cost(self):
       
   277         return self.progress_info()['estimated']
       
   278 
       
   279     @property
       
   280     def revised_cost(self):
       
   281         return self.progress_info().get('estimatedcorrected', self.cost)
       
   282 
       
   283     @property
       
   284     def done(self):
       
   285         return self.progress_info()['done']
       
   286 
       
   287     @property
       
   288     def todo(self):
       
   289         return self.progress_info()['todo']
       
   290 
       
   291     @cached
       
   292     def progress_info(self):
       
   293         raise NotImplementedError()
       
   294 
       
   295     def finished(self):
       
   296         return not self.in_progress()
       
   297 
       
   298     def in_progress(self):
       
   299         raise NotImplementedError()
       
   300 
       
   301     def progress(self):
       
   302         try:
       
   303             return 100. * self.done / self.revised_cost
       
   304         except ZeroDivisionError:
       
   305             # total cost is 0 : if everything was estimated, task is completed
       
   306             if self.progress_info().get('notestimated'):
       
   307                 return 0.
       
   308             return 100