web/views/treeview.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 # copyright 2003-2011 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 """Set of tree views / tree-building widgets, some based on jQuery treeview
       
    19 plugin.
       
    20 """
       
    21 
       
    22 __docformat__ = "restructuredtext en"
       
    23 from cubicweb import _
       
    24 
       
    25 from warnings import warn
       
    26 
       
    27 from logilab.mtconverter import xml_escape
       
    28 
       
    29 from cubicweb.utils import make_uid, json
       
    30 from cubicweb.predicates import adaptable
       
    31 from cubicweb.view import EntityView
       
    32 from cubicweb.web.views import baseviews
       
    33 from cubicweb.web.views.ajaxcontroller import ajaxfunc
       
    34 
       
    35 def treecookiename(treeid):
       
    36     return str('%s-treestate' % treeid)
       
    37 
       
    38 def _done_init(done, view, row, col):
       
    39     """handle an infinite recursion safety belt"""
       
    40     if done is None:
       
    41         done = set()
       
    42     entity = view.cw_rset.get_entity(row, col)
       
    43     if entity.eid in done:
       
    44         msg = entity._cw._('loop in %(rel)s relation (%(eid)s)') % {
       
    45             'rel': entity.cw_adapt_to('ITree').tree_relation,
       
    46             'eid': entity.eid
       
    47             }
       
    48         return None, msg
       
    49     done.add(entity.eid)
       
    50     return done, entity
       
    51 
       
    52 
       
    53 class BaseTreeView(baseviews.ListView):
       
    54     """base tree view"""
       
    55     __regid__ = 'tree'
       
    56     __select__ = adaptable('ITree')
       
    57     item_vid = 'treeitem'
       
    58 
       
    59     def call(self, done=None, **kwargs):
       
    60         if done is None:
       
    61             done = set()
       
    62         super(BaseTreeView, self).call(done=done, **kwargs)
       
    63 
       
    64     def cell_call(self, row, col=0, vid=None, done=None, maxlevel=None, klass=None, **kwargs):
       
    65         assert maxlevel is None or maxlevel > 0
       
    66         done, entity = _done_init(done, self, row, col)
       
    67         if done is None:
       
    68             # entity is actually an error message
       
    69             self.w(u'<li class="badcontent">%s</li>' % entity)
       
    70             return
       
    71         self.open_item(entity)
       
    72         entity.view(vid or self.item_vid, w=self.w, **kwargs)
       
    73         if maxlevel is not None:
       
    74             maxlevel -= 1
       
    75             if maxlevel == 0:
       
    76                 self.close_item(entity)
       
    77                 return
       
    78         relatedrset = entity.cw_adapt_to('ITree').children(entities=False)
       
    79         self.wview(self.__regid__, relatedrset, 'null', done=done,
       
    80                    maxlevel=maxlevel, klass=klass, **kwargs)
       
    81         self.close_item(entity)
       
    82 
       
    83     def open_item(self, entity):
       
    84         self.w(u'<li class="%s">\n' % entity.cw_etype.lower())
       
    85     def close_item(self, entity):
       
    86         self.w(u'</li>\n')
       
    87 
       
    88 
       
    89 class TreePathView(EntityView):
       
    90     """a recursive path view"""
       
    91     __regid__ = 'path'
       
    92     __select__ = adaptable('ITree')
       
    93     item_vid = 'oneline'
       
    94     separator = u'&#160;&gt;&#160;'
       
    95 
       
    96     def call(self, **kwargs):
       
    97         self.w(u'<div class="pathbar">')
       
    98         super(TreePathView, self).call(**kwargs)
       
    99         self.w(u'</div>')
       
   100 
       
   101     def cell_call(self, row, col=0, vid=None, done=None, **kwargs):
       
   102         done, entity = _done_init(done, self, row, col)
       
   103         if done is None:
       
   104             # entity is actually an error message
       
   105             self.w(u'<span class="badcontent">%s</span>' % entity)
       
   106             return
       
   107         parent = entity.cw_adapt_to('ITree').parent()
       
   108         if parent:
       
   109             parent.view(self.__regid__, w=self.w, done=done)
       
   110             self.w(self.separator)
       
   111         entity.view(vid or self.item_vid, w=self.w)
       
   112 
       
   113 
       
   114 class TreeComboBoxView(TreePathView):
       
   115     """display folder in edition's combobox"""
       
   116     __regid__ = 'combobox'
       
   117     item_vid = 'text'
       
   118     separator = u' > '
       
   119 
       
   120 # XXX rename regid to ajaxtree/foldabletree or something like that (same for
       
   121 # treeitemview)
       
   122 class TreeView(EntityView):
       
   123     """ajax tree view, click to expand folder"""
       
   124 
       
   125     __regid__ = 'treeview'
       
   126     itemvid = 'treeitemview'
       
   127     subvid = 'oneline'
       
   128     cssclass = 'treeview widget'
       
   129     title = _('tree view')
       
   130 
       
   131     def _init_params(self, subvid, treeid, initial_load, initial_thru_ajax, morekwargs):
       
   132         form = self._cw.form
       
   133         if subvid is None:
       
   134             subvid = form.pop('treesubvid', self.subvid) # consume it
       
   135         if treeid is None:
       
   136             treeid = form.pop('treeid', None)
       
   137             if treeid is None:
       
   138                 treeid = 'throw_away' + make_uid('uid')
       
   139         if 'morekwargs' in self._cw.form:
       
   140             ajaxargs = json.loads(form.pop('morekwargs'))
       
   141             # got unicode & python keywords must be strings
       
   142             morekwargs.update(dict((str(k), v)
       
   143                                    for k, v in ajaxargs.items()))
       
   144         toplevel_thru_ajax = form.pop('treeview_top', False) or initial_thru_ajax
       
   145         toplevel = toplevel_thru_ajax or (initial_load and not form.get('fname'))
       
   146         return subvid, treeid, toplevel_thru_ajax, toplevel
       
   147 
       
   148     def _init_headers(self, treeid):
       
   149         self._cw.add_css(('jquery-treeview/jquery.treeview.css', 'cubicweb.treeview.css'))
       
   150         self._cw.add_js(('cubicweb.ajax.js', 'cubicweb.widgets.js', 'jquery-treeview/jquery.treeview.js'))
       
   151         self._cw.html_headers.add_onload(u"""
       
   152 jQuery("#tree-%s").treeview({toggle: toggleTree, prerendered: true});""" % treeid)
       
   153 
       
   154     def call(self, subvid=None, treeid=None,
       
   155              initial_load=True, initial_thru_ajax=False, **morekwargs):
       
   156         subvid, treeid, toplevel_thru_ajax, toplevel = self._init_params(
       
   157             subvid, treeid, initial_load, initial_thru_ajax, morekwargs)
       
   158         ulid = ' '
       
   159         if toplevel:
       
   160             self._init_headers(treeid)
       
   161             ulid = ' id="tree-%s"' % treeid
       
   162         self.w(u'<ul%s class="%s">' % (ulid, self.cssclass))
       
   163         # XXX force sorting on x.sortvalue() (which return dc_title by default)
       
   164         # we need proper ITree & co specification to avoid this.
       
   165         # (pb when type ambiguity at the other side of the tree relation,
       
   166         # unability to provide generic implementation on eg Folder...)
       
   167         for i, entity in enumerate(sorted(self.cw_rset.entities(),
       
   168                                           key=lambda x: x.sortvalue())):
       
   169             if i+1 < len(self.cw_rset):
       
   170                 morekwargs['is_last'] = False
       
   171             else:
       
   172                 morekwargs['is_last'] = True
       
   173             entity.view(self.itemvid, vid=subvid, parentvid=self.__regid__,
       
   174                         treeid=treeid, w=self.w, **morekwargs)
       
   175         self.w(u'</ul>')
       
   176 
       
   177     def cell_call(self, *args, **allargs):
       
   178         """ does not makes much sense until you have to invoke
       
   179         somentity.view('treeview') """
       
   180         allargs.pop('row')
       
   181         allargs.pop('col')
       
   182         self.call(*args, **allargs)
       
   183 
       
   184 
       
   185 class FileTreeView(TreeView):
       
   186     """specific version of the treeview to display file trees
       
   187     """
       
   188     __regid__ = 'filetree'
       
   189     cssclass = 'treeview widget filetree'
       
   190     title = _('file tree view')
       
   191 
       
   192     def call(self, subvid=None, treeid=None, initial_load=True, **kwargs):
       
   193         super(FileTreeView, self).call(treeid=treeid, subvid='filetree-oneline',
       
   194                                        initial_load=initial_load, **kwargs)
       
   195 
       
   196 class FileItemInnerView(EntityView):
       
   197     """inner view used by the TreeItemView instead of oneline view
       
   198 
       
   199     This view adds an enclosing <span> with some specific CSS classes
       
   200     around the oneline view. This is needed by the jquery treeview plugin.
       
   201     """
       
   202     __regid__ = 'filetree-oneline'
       
   203 
       
   204     def cell_call(self, row, col):
       
   205         entity = self.cw_rset.get_entity(row, col)
       
   206         itree = entity.cw_adapt_to('ITree')
       
   207         if itree and not itree.is_leaf():
       
   208             self.w(u'<div class="folder">%s</div>\n' % entity.view('oneline'))
       
   209         else:
       
   210             # XXX define specific CSS classes according to mime types
       
   211             self.w(u'<div class="file">%s</div>\n' % entity.view('oneline'))
       
   212 
       
   213 
       
   214 class DefaultTreeViewItemView(EntityView):
       
   215     """default treeitem view for entities which don't adapt to ITree"""
       
   216     __regid__ = 'treeitemview'
       
   217 
       
   218     def cell_call(self, row, col, vid='oneline', treeid=None, **morekwargs):
       
   219         assert treeid is not None
       
   220         itemview = self._cw.view(vid, self.cw_rset, row=row, col=col)
       
   221         last_class = morekwargs['is_last'] and ' class="last"' or ''
       
   222         self.w(u'<li%s>%s</li>' % (last_class, itemview))
       
   223 
       
   224 
       
   225 class TreeViewItemView(EntityView):
       
   226     """specific treeitem view for entities which adapt to ITree
       
   227 
       
   228     (each item should be expandable if it's not a tree leaf)
       
   229     """
       
   230     __regid__ = 'treeitemview'
       
   231     __select__ = adaptable('ITree')
       
   232     default_branch_state_is_open = False
       
   233 
       
   234     def open_state(self, eeid, treeid):
       
   235         cookies = self._cw.get_cookie()
       
   236         treestate = cookies.get(treecookiename(treeid))
       
   237         if treestate:
       
   238             return str(eeid) in treestate.value.split(':')
       
   239         return self.default_branch_state_is_open
       
   240 
       
   241     def cell_call(self, row, col, treeid, vid='oneline', parentvid='treeview',
       
   242                   is_last=False, **morekwargs):
       
   243         w = self.w
       
   244         entity = self.cw_rset.get_entity(row, col)
       
   245         itree = entity.cw_adapt_to('ITree')
       
   246         liclasses = []
       
   247         if self._cw.url(includeparams=False) == entity.absolute_url():
       
   248             liclasses.append(u'selected')
       
   249         is_open = self.open_state(entity.eid, treeid)
       
   250         is_leaf = itree is None or itree.is_leaf()
       
   251         if is_leaf:
       
   252             if is_last:
       
   253                 liclasses.append('last')
       
   254             w(u'<li class="%s">' % u' '.join(liclasses))
       
   255         else:
       
   256             rql = itree.children_rql() % {'x': entity.eid}
       
   257             url = xml_escape(self._cw.build_url('ajax', rql=rql, vid=parentvid,
       
   258                                                 pageid=self._cw.pageid,
       
   259                                                 treeid=treeid,
       
   260                                                 fname='view',
       
   261                                                 treesubvid=vid,
       
   262                                                 morekwargs=json.dumps(morekwargs)))
       
   263             divclasses = ['hitarea']
       
   264             if is_open:
       
   265                 liclasses.append('collapsable')
       
   266                 divclasses.append('collapsable-hitarea')
       
   267             else:
       
   268                 liclasses.append('expandable')
       
   269                 divclasses.append('expandable-hitarea')
       
   270             if is_last:
       
   271                 if is_open:
       
   272                     liclasses.append('lastCollapsable')
       
   273                     divclasses.append('lastCollapsable-hitarea')
       
   274                 else:
       
   275                     liclasses.append('lastExpandable')
       
   276                     divclasses.append('lastExpandable-hitarea')
       
   277             if is_open:
       
   278                 w(u'<li class="%s">' % u' '.join(liclasses))
       
   279             else:
       
   280                 w(u'<li cubicweb:loadurl="%s" class="%s">' % (url, u' '.join(liclasses)))
       
   281             if treeid.startswith('throw_away'):
       
   282                 divtail = ''
       
   283             else:
       
   284                 divtail = """ onclick="asyncRemoteExec('node_clicked', '%s', '%s')" """ % (
       
   285                     treeid, entity.eid)
       
   286             w(u'<div class="%s"%s></div>' % (u' '.join(divclasses), divtail))
       
   287 
       
   288             # add empty <ul> because jquery's treeview plugin checks for
       
   289             # sublists presence
       
   290             if not is_open:
       
   291                 w(u'<ul class="placeholder"><li>place holder</li></ul>')
       
   292         # the local node info
       
   293         self.wview(vid, self.cw_rset, row=row, col=col, **morekwargs)
       
   294         if is_open and not is_leaf: #  => rql is defined
       
   295             self.wview(parentvid, itree.children(entities=False), subvid=vid,
       
   296                        treeid=treeid, initial_load=False, **morekwargs)
       
   297         w(u'</li>')
       
   298 
       
   299 
       
   300 
       
   301 @ajaxfunc
       
   302 def node_clicked(self, treeid, nodeeid):
       
   303     """add/remove eid in treestate cookie"""
       
   304     cookies = self._cw.get_cookie()
       
   305     statename = treecookiename(treeid)
       
   306     treestate = cookies.get(statename)
       
   307     if treestate is None:
       
   308         self._cw.set_cookie(statename, nodeeid)
       
   309     else:
       
   310         marked = set(filter(None, treestate.value.split(':')))
       
   311         if nodeeid in marked:
       
   312             marked.remove(nodeeid)
       
   313         else:
       
   314             marked.add(nodeeid)
       
   315         self._cw.set_cookie(statename, ':'.join(marked))