diff -r 058bb3dc685f -r 0b59724cb3f2 cubicweb/web/views/treeview.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cubicweb/web/views/treeview.py Sat Jan 16 13:48:51 2016 +0100 @@ -0,0 +1,315 @@ +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of CubicWeb. +# +# CubicWeb is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# CubicWeb is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with CubicWeb. If not, see . +"""Set of tree views / tree-building widgets, some based on jQuery treeview +plugin. +""" + +__docformat__ = "restructuredtext en" +from cubicweb import _ + +from warnings import warn + +from logilab.mtconverter import xml_escape + +from cubicweb.utils import make_uid, json +from cubicweb.predicates import adaptable +from cubicweb.view import EntityView +from cubicweb.web.views import baseviews +from cubicweb.web.views.ajaxcontroller import ajaxfunc + +def treecookiename(treeid): + return str('%s-treestate' % treeid) + +def _done_init(done, view, row, col): + """handle an infinite recursion safety belt""" + if done is None: + done = set() + entity = view.cw_rset.get_entity(row, col) + if entity.eid in done: + msg = entity._cw._('loop in %(rel)s relation (%(eid)s)') % { + 'rel': entity.cw_adapt_to('ITree').tree_relation, + 'eid': entity.eid + } + return None, msg + done.add(entity.eid) + return done, entity + + +class BaseTreeView(baseviews.ListView): + """base tree view""" + __regid__ = 'tree' + __select__ = adaptable('ITree') + item_vid = 'treeitem' + + def call(self, done=None, **kwargs): + if done is None: + done = set() + super(BaseTreeView, self).call(done=done, **kwargs) + + def cell_call(self, row, col=0, vid=None, done=None, maxlevel=None, klass=None, **kwargs): + assert maxlevel is None or maxlevel > 0 + done, entity = _done_init(done, self, row, col) + if done is None: + # entity is actually an error message + self.w(u'
  • %s
  • ' % entity) + return + self.open_item(entity) + entity.view(vid or self.item_vid, w=self.w, **kwargs) + if maxlevel is not None: + maxlevel -= 1 + if maxlevel == 0: + self.close_item(entity) + return + relatedrset = entity.cw_adapt_to('ITree').children(entities=False) + self.wview(self.__regid__, relatedrset, 'null', done=done, + maxlevel=maxlevel, klass=klass, **kwargs) + self.close_item(entity) + + def open_item(self, entity): + self.w(u'
  • \n' % entity.cw_etype.lower()) + def close_item(self, entity): + self.w(u'
  • \n') + + +class TreePathView(EntityView): + """a recursive path view""" + __regid__ = 'path' + __select__ = adaptable('ITree') + item_vid = 'oneline' + separator = u' > ' + + def call(self, **kwargs): + self.w(u'
    ') + super(TreePathView, self).call(**kwargs) + self.w(u'
    ') + + def cell_call(self, row, col=0, vid=None, done=None, **kwargs): + done, entity = _done_init(done, self, row, col) + if done is None: + # entity is actually an error message + self.w(u'%s' % entity) + return + parent = entity.cw_adapt_to('ITree').parent() + if parent: + parent.view(self.__regid__, w=self.w, done=done) + self.w(self.separator) + entity.view(vid or self.item_vid, w=self.w) + + +class TreeComboBoxView(TreePathView): + """display folder in edition's combobox""" + __regid__ = 'combobox' + item_vid = 'text' + separator = u' > ' + +# XXX rename regid to ajaxtree/foldabletree or something like that (same for +# treeitemview) +class TreeView(EntityView): + """ajax tree view, click to expand folder""" + + __regid__ = 'treeview' + itemvid = 'treeitemview' + subvid = 'oneline' + cssclass = 'treeview widget' + title = _('tree view') + + def _init_params(self, subvid, treeid, initial_load, initial_thru_ajax, morekwargs): + form = self._cw.form + if subvid is None: + subvid = form.pop('treesubvid', self.subvid) # consume it + if treeid is None: + treeid = form.pop('treeid', None) + if treeid is None: + treeid = 'throw_away' + make_uid('uid') + if 'morekwargs' in self._cw.form: + ajaxargs = json.loads(form.pop('morekwargs')) + # got unicode & python keywords must be strings + morekwargs.update(dict((str(k), v) + for k, v in ajaxargs.items())) + toplevel_thru_ajax = form.pop('treeview_top', False) or initial_thru_ajax + toplevel = toplevel_thru_ajax or (initial_load and not form.get('fname')) + return subvid, treeid, toplevel_thru_ajax, toplevel + + def _init_headers(self, treeid): + self._cw.add_css(('jquery-treeview/jquery.treeview.css', 'cubicweb.treeview.css')) + self._cw.add_js(('cubicweb.ajax.js', 'cubicweb.widgets.js', 'jquery-treeview/jquery.treeview.js')) + self._cw.html_headers.add_onload(u""" +jQuery("#tree-%s").treeview({toggle: toggleTree, prerendered: true});""" % treeid) + + def call(self, subvid=None, treeid=None, + initial_load=True, initial_thru_ajax=False, **morekwargs): + subvid, treeid, toplevel_thru_ajax, toplevel = self._init_params( + subvid, treeid, initial_load, initial_thru_ajax, morekwargs) + ulid = ' ' + if toplevel: + self._init_headers(treeid) + ulid = ' id="tree-%s"' % treeid + self.w(u'' % (ulid, self.cssclass)) + # XXX force sorting on x.sortvalue() (which return dc_title by default) + # we need proper ITree & co specification to avoid this. + # (pb when type ambiguity at the other side of the tree relation, + # unability to provide generic implementation on eg Folder...) + for i, entity in enumerate(sorted(self.cw_rset.entities(), + key=lambda x: x.sortvalue())): + if i+1 < len(self.cw_rset): + morekwargs['is_last'] = False + else: + morekwargs['is_last'] = True + entity.view(self.itemvid, vid=subvid, parentvid=self.__regid__, + treeid=treeid, w=self.w, **morekwargs) + self.w(u'') + + def cell_call(self, *args, **allargs): + """ does not makes much sense until you have to invoke + somentity.view('treeview') """ + allargs.pop('row') + allargs.pop('col') + self.call(*args, **allargs) + + +class FileTreeView(TreeView): + """specific version of the treeview to display file trees + """ + __regid__ = 'filetree' + cssclass = 'treeview widget filetree' + title = _('file tree view') + + def call(self, subvid=None, treeid=None, initial_load=True, **kwargs): + super(FileTreeView, self).call(treeid=treeid, subvid='filetree-oneline', + initial_load=initial_load, **kwargs) + +class FileItemInnerView(EntityView): + """inner view used by the TreeItemView instead of oneline view + + This view adds an enclosing with some specific CSS classes + around the oneline view. This is needed by the jquery treeview plugin. + """ + __regid__ = 'filetree-oneline' + + def cell_call(self, row, col): + entity = self.cw_rset.get_entity(row, col) + itree = entity.cw_adapt_to('ITree') + if itree and not itree.is_leaf(): + self.w(u'
    %s
    \n' % entity.view('oneline')) + else: + # XXX define specific CSS classes according to mime types + self.w(u'
    %s
    \n' % entity.view('oneline')) + + +class DefaultTreeViewItemView(EntityView): + """default treeitem view for entities which don't adapt to ITree""" + __regid__ = 'treeitemview' + + def cell_call(self, row, col, vid='oneline', treeid=None, **morekwargs): + assert treeid is not None + itemview = self._cw.view(vid, self.cw_rset, row=row, col=col) + last_class = morekwargs['is_last'] and ' class="last"' or '' + self.w(u'%s' % (last_class, itemview)) + + +class TreeViewItemView(EntityView): + """specific treeitem view for entities which adapt to ITree + + (each item should be expandable if it's not a tree leaf) + """ + __regid__ = 'treeitemview' + __select__ = adaptable('ITree') + default_branch_state_is_open = False + + def open_state(self, eeid, treeid): + cookies = self._cw.get_cookie() + treestate = cookies.get(treecookiename(treeid)) + if treestate: + return str(eeid) in treestate.value.split(':') + return self.default_branch_state_is_open + + def cell_call(self, row, col, treeid, vid='oneline', parentvid='treeview', + is_last=False, **morekwargs): + w = self.w + entity = self.cw_rset.get_entity(row, col) + itree = entity.cw_adapt_to('ITree') + liclasses = [] + if self._cw.url(includeparams=False) == entity.absolute_url(): + liclasses.append(u'selected') + is_open = self.open_state(entity.eid, treeid) + is_leaf = itree is None or itree.is_leaf() + if is_leaf: + if is_last: + liclasses.append('last') + w(u'
  • ' % u' '.join(liclasses)) + else: + rql = itree.children_rql() % {'x': entity.eid} + url = xml_escape(self._cw.build_url('ajax', rql=rql, vid=parentvid, + pageid=self._cw.pageid, + treeid=treeid, + fname='view', + treesubvid=vid, + morekwargs=json.dumps(morekwargs))) + divclasses = ['hitarea'] + if is_open: + liclasses.append('collapsable') + divclasses.append('collapsable-hitarea') + else: + liclasses.append('expandable') + divclasses.append('expandable-hitarea') + if is_last: + if is_open: + liclasses.append('lastCollapsable') + divclasses.append('lastCollapsable-hitarea') + else: + liclasses.append('lastExpandable') + divclasses.append('lastExpandable-hitarea') + if is_open: + w(u'
  • ' % u' '.join(liclasses)) + else: + w(u'
  • ' % (url, u' '.join(liclasses))) + if treeid.startswith('throw_away'): + divtail = '' + else: + divtail = """ onclick="asyncRemoteExec('node_clicked', '%s', '%s')" """ % ( + treeid, entity.eid) + w(u'
    ' % (u' '.join(divclasses), divtail)) + + # add empty
      because jquery's treeview plugin checks for + # sublists presence + if not is_open: + w(u'
      • place holder
      ') + # the local node info + self.wview(vid, self.cw_rset, row=row, col=col, **morekwargs) + if is_open and not is_leaf: # => rql is defined + self.wview(parentvid, itree.children(entities=False), subvid=vid, + treeid=treeid, initial_load=False, **morekwargs) + w(u'') + + + +@ajaxfunc +def node_clicked(self, treeid, nodeeid): + """add/remove eid in treestate cookie""" + cookies = self._cw.get_cookie() + statename = treecookiename(treeid) + treestate = cookies.get(statename) + if treestate is None: + self._cw.set_cookie(statename, nodeeid) + else: + marked = set(filter(None, treestate.value.split(':'))) + if nodeeid in marked: + marked.remove(nodeeid) + else: + marked.add(nodeeid) + self._cw.set_cookie(statename, ':'.join(marked))