web/views/treeview.py
changeset 5556 9ab2b4c74baf
parent 5424 8ecbcbff9777
child 5557 1a534c596bff
equal deleted inserted replaced
5555:a64f48dd5fe4 5556:9ab2b4c74baf
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
    14 # details.
    14 # details.
    15 #
    15 #
    16 # You should have received a copy of the GNU Lesser General Public License along
    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/>.
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    18 """Set of tree-building widgets, based on jQuery treeview plugin
    18 """Set of tree views / tree-building widgets, some based on jQuery treeview
    19 
    19 plugin.
    20 """
    20 """
    21 __docformat__ = "restructuredtext en"
    21 __docformat__ = "restructuredtext en"
    22 
    22 
       
    23 from warnings import warn
       
    24 
    23 from logilab.mtconverter import xml_escape
    25 from logilab.mtconverter import xml_escape
       
    26 from logilab.common.decorators import cached
       
    27 
    24 from cubicweb.utils import make_uid
    28 from cubicweb.utils import make_uid
       
    29 from cubicweb.selectors import implements, adaptable
       
    30 from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
       
    31 from cubicweb.web import json
    25 from cubicweb.interfaces import ITree
    32 from cubicweb.interfaces import ITree
    26 from cubicweb.selectors import implements
    33 from cubicweb.web.views import baseviews
    27 from cubicweb.view import EntityView
       
    28 from cubicweb.web import json
       
    29 
    34 
    30 def treecookiename(treeid):
    35 def treecookiename(treeid):
    31     return str('%s-treestate' % treeid)
    36     return str('%s-treestate' % treeid)
    32 
    37 
       
    38 
       
    39 class ITreeAdapter(EntityAdapter):
       
    40     """This adapter has to be overriden to be configured using the
       
    41     tree_relation, child_role and parent_role class attributes to
       
    42     benefit from this default implementation
       
    43     """
       
    44     __regid__ = 'ITree'
       
    45     __select__ = implements(ITree) # XXX for bw compat, else should be abstract
       
    46 
       
    47     tree_relation = None
       
    48     child_role = 'subject'
       
    49     parent_role = 'object'
       
    50 
       
    51     @implements_adapter_compat('ITree')
       
    52     def children_rql(self):
       
    53         """returns RQL to get children
       
    54 
       
    55         XXX should be removed from the public interface
       
    56         """
       
    57         return self.entity.related_rql(self.tree_relation, self.parent_role)
       
    58 
       
    59     @implements_adapter_compat('ITree')
       
    60     def different_type_children(self, entities=True):
       
    61         """return children entities of different type as this entity.
       
    62 
       
    63         according to the `entities` parameter, return entity objects or the
       
    64         equivalent result set
       
    65         """
       
    66         res = self.entity.related(self.tree_relation, self.parent_role,
       
    67                                   entities=entities)
       
    68         eschema = self.entity.e_schema
       
    69         if entities:
       
    70             return [e for e in res if e.e_schema != eschema]
       
    71         return res.filtered_rset(lambda x: x.e_schema != eschema, self.entity.cw_col)
       
    72 
       
    73     @implements_adapter_compat('ITree')
       
    74     def same_type_children(self, entities=True):
       
    75         """return children entities of the same type as this entity.
       
    76 
       
    77         according to the `entities` parameter, return entity objects or the
       
    78         equivalent result set
       
    79         """
       
    80         res = self.entity.related(self.tree_relation, self.parent_role,
       
    81                                   entities=entities)
       
    82         eschema = self.entity.e_schema
       
    83         if entities:
       
    84             return [e for e in res if e.e_schema == eschema]
       
    85         return res.filtered_rset(lambda x: x.e_schema is eschema, self.entity.cw_col)
       
    86 
       
    87     @implements_adapter_compat('ITree')
       
    88     def is_leaf(self):
       
    89         """returns true if this node as no child"""
       
    90         return len(self.children()) == 0
       
    91 
       
    92     @implements_adapter_compat('ITree')
       
    93     def is_root(self):
       
    94         """returns true if this node has no parent"""
       
    95         return self.parent() is None
       
    96 
       
    97     @implements_adapter_compat('ITree')
       
    98     def root(self):
       
    99         """return the root object"""
       
   100         return self._cw.entity_from_eid(self.path()[0])
       
   101 
       
   102     @implements_adapter_compat('ITree')
       
   103     def parent(self):
       
   104         """return the parent entity if any, else None (e.g. if we are on the
       
   105         root)
       
   106         """
       
   107         try:
       
   108             return self.entity.related(self.tree_relation, self.child_role,
       
   109                                        entities=True)[0]
       
   110         except (KeyError, IndexError):
       
   111             return None
       
   112 
       
   113     @implements_adapter_compat('ITree')
       
   114     def children(self, entities=True, sametype=False):
       
   115         """return children entities
       
   116 
       
   117         according to the `entities` parameter, return entity objects or the
       
   118         equivalent result set
       
   119         """
       
   120         if sametype:
       
   121             return self.same_type_children(entities)
       
   122         else:
       
   123             return self.entity.related(self.tree_relation, self.parent_role,
       
   124                                        entities=entities)
       
   125 
       
   126     @implements_adapter_compat('ITree')
       
   127     def iterparents(self, strict=True):
       
   128         def _uptoroot(self):
       
   129             curr = self
       
   130             while True:
       
   131                 curr = curr.parent()
       
   132                 if curr is None:
       
   133                     break
       
   134                 yield curr
       
   135                 curr = curr.cw_adapt_to('ITree')
       
   136         if not strict:
       
   137             return chain([self.entity], _uptoroot(self))
       
   138         return _uptoroot(self)
       
   139 
       
   140     @implements_adapter_compat('ITree')
       
   141     def iterchildren(self, _done=None):
       
   142         """iterates over the item's children"""
       
   143         if _done is None:
       
   144             _done = set()
       
   145         for child in self.children():
       
   146             if child.eid in _done:
       
   147                 self.error('loop in %s tree', child.__regid__.lower())
       
   148                 continue
       
   149             yield child
       
   150             _done.add(child.eid)
       
   151 
       
   152     @implements_adapter_compat('ITree')
       
   153     def prefixiter(self, _done=None):
       
   154         if _done is None:
       
   155             _done = set()
       
   156         if self.entity.eid in _done:
       
   157             return
       
   158         _done.add(self.entity.eid)
       
   159         yield self.entity
       
   160         for child in self.same_type_children():
       
   161             for entity in child.cw_adapt_to('ITree').prefixiter(_done):
       
   162                 yield entity
       
   163 
       
   164     @cached
       
   165     @implements_adapter_compat('ITree')
       
   166     def path(self):
       
   167         """returns the list of eids from the root object to this object"""
       
   168         path = []
       
   169         adapter = self
       
   170         entity = adapter.entity
       
   171         while entity is not None:
       
   172             if entity.eid in path:
       
   173                 self.error('loop in %s tree', entity.__regid__.lower())
       
   174                 break
       
   175             path.append(entity.eid)
       
   176             try:
       
   177                 # check we are not jumping to another tree
       
   178                 if (adapter.tree_relation != self.tree_relation or
       
   179                     adapter.child_role != self.child_role):
       
   180                     break
       
   181                 entity = adapter.parent()
       
   182                 adapter = entity.cw_adapt_to('ITree')
       
   183             except AttributeError:
       
   184                 break
       
   185         path.reverse()
       
   186         return path
       
   187 
       
   188 
       
   189 def _done_init(done, view, row, col):
       
   190     """handle an infinite recursion safety belt"""
       
   191     if done is None:
       
   192         done = set()
       
   193     entity = view.cw_rset.get_entity(row, col)
       
   194     if entity.eid in done:
       
   195         msg = entity._cw._('loop in %(rel)s relation (%(eid)s)') % {
       
   196             'rel': entity.tree_attribute,
       
   197             'eid': entity.eid
       
   198             }
       
   199         return None, msg
       
   200     done.add(entity.eid)
       
   201     return done, entity
       
   202 
       
   203 
       
   204 class BaseTreeView(baseviews.ListView):
       
   205     """base tree view"""
       
   206     __regid__ = 'tree'
       
   207     __select__ = adaptable('ITree')
       
   208     item_vid = 'treeitem'
       
   209 
       
   210     def call(self, done=None, **kwargs):
       
   211         if done is None:
       
   212             done = set()
       
   213         super(TreeViewMixIn, self).call(done=done, **kwargs)
       
   214 
       
   215     def cell_call(self, row, col=0, vid=None, done=None, **kwargs):
       
   216         done, entity = _done_init(done, self, row, col)
       
   217         if done is None:
       
   218             # entity is actually an error message
       
   219             self.w(u'<li class="badcontent">%s</li>' % entity)
       
   220             return
       
   221         self.open_item(entity)
       
   222         entity.view(vid or self.item_vid, w=self.w, **kwargs)
       
   223         relatedrset = entity.cw_adapt_to('ITree').children(entities=False)
       
   224         self.wview(self.__regid__, relatedrset, 'null', done=done, **kwargs)
       
   225         self.close_item(entity)
       
   226 
       
   227     def open_item(self, entity):
       
   228         self.w(u'<li class="%s">\n' % entity.__regid__.lower())
       
   229     def close_item(self, entity):
       
   230         self.w(u'</li>\n')
       
   231 
       
   232 
       
   233 
       
   234 class TreePathView(EntityView):
       
   235     """a recursive path view"""
       
   236     __regid__ = 'path'
       
   237     __select__ = adaptable('ITree')
       
   238     item_vid = 'oneline'
       
   239     separator = u'&#160;&gt;&#160;'
       
   240 
       
   241     def call(self, **kwargs):
       
   242         self.w(u'<div class="pathbar">')
       
   243         super(TreePathMixIn, self).call(**kwargs)
       
   244         self.w(u'</div>')
       
   245 
       
   246     def cell_call(self, row, col=0, vid=None, done=None, **kwargs):
       
   247         done, entity = _done_init(done, self, row, col)
       
   248         if done is None:
       
   249             # entity is actually an error message
       
   250             self.w(u'<span class="badcontent">%s</span>' % entity)
       
   251             return
       
   252         parent = entity.cw_adapt_to('ITree').parent_entity()
       
   253         if parent:
       
   254             parent.view(self.__regid__, w=self.w, done=done)
       
   255             self.w(self.separator)
       
   256         entity.view(vid or self.item_vid, w=self.w)
       
   257 
       
   258 
       
   259 # XXX rename regid to ajaxtree/foldabletree or something like that (same for
       
   260 # treeitemview)
    33 class TreeView(EntityView):
   261 class TreeView(EntityView):
       
   262     """ajax tree view, click to expand folder"""
       
   263 
    34     __regid__ = 'treeview'
   264     __regid__ = 'treeview'
    35     itemvid = 'treeitemview'
   265     itemvid = 'treeitemview'
    36     subvid = 'oneline'
   266     subvid = 'oneline'
    37     css_classes = 'treeview widget'
   267     css_classes = 'treeview widget'
    38     title = _('tree view')
   268     title = _('tree view')
   110     """
   340     """
   111     __regid__ = 'filetree-oneline'
   341     __regid__ = 'filetree-oneline'
   112 
   342 
   113     def cell_call(self, row, col):
   343     def cell_call(self, row, col):
   114         entity = self.cw_rset.get_entity(row, col)
   344         entity = self.cw_rset.get_entity(row, col)
   115         if ITree.is_implemented_by(entity.__class__) and not entity.is_leaf():
   345         if entity.cw_adapt_to('ITree') and not entity.is_leaf():
   116             self.w(u'<div class="folder">%s</div>\n' % entity.view('oneline'))
   346             self.w(u'<div class="folder">%s</div>\n' % entity.view('oneline'))
   117         else:
   347         else:
   118             # XXX define specific CSS classes according to mime types
   348             # XXX define specific CSS classes according to mime types
   119             self.w(u'<div class="file">%s</div>\n' % entity.view('oneline'))
   349             self.w(u'<div class="file">%s</div>\n' % entity.view('oneline'))
   120 
   350 
   121 
   351 
   122 class DefaultTreeViewItemView(EntityView):
   352 class DefaultTreeViewItemView(EntityView):
   123     """default treeitem view for entities which don't implement ITree"""
   353     """default treeitem view for entities which don't adapt to ITree"""
   124     __regid__ = 'treeitemview'
   354     __regid__ = 'treeitemview'
   125 
   355 
   126     def cell_call(self, row, col, vid='oneline', treeid=None, **morekwargs):
   356     def cell_call(self, row, col, vid='oneline', treeid=None, **morekwargs):
   127         assert treeid is not None
   357         assert treeid is not None
   128         itemview = self._cw.view(vid, self.cw_rset, row=row, col=col)
   358         itemview = self._cw.view(vid, self.cw_rset, row=row, col=col)
   129         last_class = morekwargs['is_last'] and ' class="last"' or ''
   359         last_class = morekwargs['is_last'] and ' class="last"' or ''
   130         self.w(u'<li%s>%s</li>' % (last_class, itemview))
   360         self.w(u'<li%s>%s</li>' % (last_class, itemview))
   131 
   361 
   132 
   362 
   133 class TreeViewItemView(EntityView):
   363 class TreeViewItemView(EntityView):
   134     """specific treeitem view for entities which implement ITree
   364     """specific treeitem view for entities which adapt to ITree
   135 
   365 
   136     (each item should be expandable if it's not a tree leaf)
   366     (each item should be expandable if it's not a tree leaf)
   137     """
   367     """
   138     __regid__ = 'treeitemview'
   368     __regid__ = 'treeitemview'
   139     __select__ = implements(ITree)
   369     __select__ = adaptable('ITree')
   140     default_branch_state_is_open = False
   370     default_branch_state_is_open = False
   141 
   371 
   142     def open_state(self, eeid, treeid):
   372     def open_state(self, eeid, treeid):
   143         cookies = self._cw.get_cookie()
   373         cookies = self._cw.get_cookie()
   144         treestate = cookies.get(treecookiename(treeid))
   374         treestate = cookies.get(treecookiename(treeid))
   148 
   378 
   149     def cell_call(self, row, col, treeid, vid='oneline', parentvid='treeview',
   379     def cell_call(self, row, col, treeid, vid='oneline', parentvid='treeview',
   150                   is_last=False, **morekwargs):
   380                   is_last=False, **morekwargs):
   151         w = self.w
   381         w = self.w
   152         entity = self.cw_rset.get_entity(row, col)
   382         entity = self.cw_rset.get_entity(row, col)
       
   383         itree = entity.cw_adapt_to('ITree')
   153         liclasses = []
   384         liclasses = []
   154         is_open = self.open_state(entity.eid, treeid)
   385         is_open = self.open_state(entity.eid, treeid)
   155         is_leaf = not hasattr(entity, 'is_leaf') or entity.is_leaf()
   386         is_leaf = not hasattr(entity, 'is_leaf') or itree.is_leaf()
   156         if is_leaf:
   387         if is_leaf:
   157             if is_last:
   388             if is_last:
   158                 liclasses.append('last')
   389                 liclasses.append('last')
   159             w(u'<li class="%s">' % u' '.join(liclasses))
   390             w(u'<li class="%s">' % u' '.join(liclasses))
   160         else:
   391         else:
   161             rql = entity.children_rql() % {'x': entity.eid}
   392             rql = itree.children_rql() % {'x': entity.eid}
   162             url = xml_escape(self._cw.build_url('json', rql=rql, vid=parentvid,
   393             url = xml_escape(self._cw.build_url('json', rql=rql, vid=parentvid,
   163                                                 pageid=self._cw.pageid,
   394                                                 pageid=self._cw.pageid,
   164                                                 treeid=treeid,
   395                                                 treeid=treeid,
   165                                                 fname='view',
   396                                                 fname='view',
   166                                                 treesubvid=vid,
   397                                                 treesubvid=vid,
   195             if not is_open:
   426             if not is_open:
   196                 w(u'<ul class="placeholder"><li>place holder</li></ul>')
   427                 w(u'<ul class="placeholder"><li>place holder</li></ul>')
   197         # the local node info
   428         # the local node info
   198         self.wview(vid, self.cw_rset, row=row, col=col, **morekwargs)
   429         self.wview(vid, self.cw_rset, row=row, col=col, **morekwargs)
   199         if is_open and not is_leaf: #  => rql is defined
   430         if is_open and not is_leaf: #  => rql is defined
   200             self.wview(parentvid, entity.children(entities=False), subvid=vid,
   431             self.wview(parentvid, itree.children(entities=False), subvid=vid,
   201                        treeid=treeid, initial_load=False, **morekwargs)
   432                        treeid=treeid, initial_load=False, **morekwargs)
   202         w(u'</li>')
   433         w(u'</li>')
   203 
   434