web/views/treeview.py
changeset 999 999198995a53
parent 923 7c184924d492
parent 998 9c6ce9d6384f
child 1001 7d0fccdb8125
equal deleted inserted replaced
994:98f19cf755a1 999:999198995a53
       
     1 """Set of tree-building widgets, based on jQuery treeview plugin
       
     2 
       
     3 :organization: Logilab
       
     4 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     6 """
       
     7 __docformat__ = "restructuredtext en"
       
     8 import uuid
       
     9 
     1 from logilab.mtconverter import html_escape
    10 from logilab.mtconverter import html_escape
     2 
       
     3 from cubicweb.interfaces import ITree
    11 from cubicweb.interfaces import ITree
     4 from cubicweb.common.selectors import implement_interface, yes
    12 from cubicweb.common.selectors import implement_interface, yes
     5 from cubicweb.common.view import EntityView
    13 from cubicweb.common.view import EntityView
     6 
    14 
     7 from cubicweb.web.views.baseviews import OneLineView
    15 def treecookiename(treeid):
       
    16     return str('treestate-%s' % treeid)
     8 
    17 
     9 class TreeView(EntityView):
    18 class TreeView(EntityView):
    10     id = 'treeview'
    19     id = 'treeview'
    11     accepts = ('Any',)
    20     accepts = ('Any',)
    12     itemvid = 'treeitemview'
    21     itemvid = 'treeitemview'
    13     css_classes = 'treeview widget'
    22     css_classes = 'treeview widget'
    14     title = _('tree view')
    23     title = _('tree view')
    15     
    24 
    16     def call(self, subvid=None):
    25     def call(self, subvid=None, treeid=None, initial_load=True):
    17         if subvid is None and 'subvid' in self.req.form:
       
    18             subvid = self.req.form.pop('subvid') # consume it
       
    19         if subvid is None:
    26         if subvid is None:
    20             subvid = 'oneline'
    27             if 'subvid' in self.req.form:
    21         self.req.add_css('jquery.treeview.css')
    28                 subvid = self.req.form.pop('subvid') # consume it
    22         self.req.add_js(('cubicweb.ajax.js', 'jquery.treeview.js', 'cubicweb.widgets.js'))
    29             else:
    23         # XXX noautoload is a quick hack to avoid treeview to be rebuilt
    30                 subvid = 'oneline'
    24         #     after a json query and avoid double toggling bugs.
    31         if treeid is None:
    25         #     Need to find a way to do that cleanly.
    32             if 'treeid' in self.req.form:
    26         if 'noautoload' in self.req.form:
    33                 treeid = self.req.form.pop('treeid')
    27             self.w(u'<ul class="%s" cubicweb:wdgtype="TreeView">' % self.css_classes)
    34             else:
    28         else:
    35                 treeid = uuid.uuid1().hex
    29             self.w(u'<ul class="%s" cubicweb:loadtype="auto" cubicweb:wdgtype="TreeView">'
    36                 self.warning('Tree state won\'t be properly restored after next reload')
    30                    % self.css_classes)
    37         if initial_load:
       
    38             self.req.add_css('jquery.treeview.css')
       
    39             self.req.add_js(('cubicweb.ajax.js', 'jquery.treeview.js'))
       
    40             self.req.html_headers.add_onload(u"""
       
    41                  jQuery("#tree-%s").treeview({toggle: toggleTree,
       
    42                                               prerendered: true});""" % treeid)
       
    43         self.w(u'<ul id="tree-%s" class="%s">' % (treeid, self.css_classes))
    31         for rowidx in xrange(len(self.rset)):
    44         for rowidx in xrange(len(self.rset)):
    32             self.wview(self.itemvid, self.rset, row=rowidx, col=0,
    45             self.wview(self.itemvid, self.rset, row=rowidx, col=0,
    33                        vid=subvid, parentvid=self.id)
    46                        vid=subvid, parentvid=self.id, treeid=treeid)
    34         self.w(u'</ul>')
    47         self.w(u'</ul>')
    35         
       
    36 
    48 
    37 class FileTreeView(TreeView):
    49 class FileTreeView(TreeView):
    38     """specific version of the treeview to display file trees
    50     """specific version of the treeview to display file trees
    39     """
    51     """
    40     id = 'filetree'
    52     id = 'filetree'
    41     css_classes = 'treeview widget filetree'
    53     css_classes = 'treeview widget filetree'
    42     title = _('file tree view')
    54     title = _('file tree view')
    43 
    55 
    44     def call(self, subvid=None):
    56     def call(self, subvid=None, treeid=None, initial_load=True):
    45         super(FileTreeView, self).call(subvid='filetree-oneline')
    57         super(FileTreeView, self).call(treeid=treeid, subvid='filetree-oneline', initial_load=initial_load)
    46 
       
    47 
       
    48 
    58 
    49 class FileItemInnerView(EntityView):
    59 class FileItemInnerView(EntityView):
    50     """inner view used by the TreeItemView instead of oneline view
    60     """inner view used by the TreeItemView instead of oneline view
    51 
    61 
    52     This view adds an enclosing <span> with some specific CSS classes
    62     This view adds an enclosing <span> with some specific CSS classes
    55     id = 'filetree-oneline'
    65     id = 'filetree-oneline'
    56 
    66 
    57     def cell_call(self, row, col):
    67     def cell_call(self, row, col):
    58         entity = self.entity(row, col)
    68         entity = self.entity(row, col)
    59         if ITree.is_implemented_by(entity.__class__) and not entity.is_leaf():
    69         if ITree.is_implemented_by(entity.__class__) and not entity.is_leaf():
    60             self.w(u'<div class="folder">%s</div>' % entity.view('oneline'))
    70             self.w(u'<div class="folder">%s</div>\n' % entity.view('oneline'))
    61         else:
    71         else:
    62             # XXX define specific CSS classes according to mime types
    72             # XXX define specific CSS classes according to mime types
    63             self.w(u'<div class="file">%s</div>' % entity.view('oneline'))
    73             self.w(u'<div class="file">%s</div>\n' % entity.view('oneline'))
    64 
    74 
    65 
    75 
    66 class DefaultTreeViewItemView(EntityView):
    76 class DefaultTreeViewItemView(EntityView):
    67     """default treeitem view for entities which don't implement ITree
    77     """default treeitem view for entities which don't implement ITree
    68     """
    78     """
    69     id = 'treeitemview'
    79     id = 'treeitemview'
    70     accepts = ('Any',)
    80     accepts = ('Any',)
    71     
    81 
    72     def cell_call(self, row, col, vid='oneline', parentvid='treeview'):
    82     def cell_call(self, row, col, vid='oneline', parentvid='treeview', treeid=None):
       
    83         assert treeid is not None
    73         entity = self.entity(row, col)
    84         entity = self.entity(row, col)
    74         itemview = self.view(vid, self.rset, row=row, col=col)
    85         itemview = self.view(vid, self.rset, row=row, col=col)
    75         if row == len(self.rset) - 1:
    86         if row == len(self.rset) - 1:
    76             self.w(u'<li class="last">%s</li>' % itemview)
    87             self.w(u'<li class="last">%s</li>' % itemview)
    77         else:
    88         else:
    78             self.w(u'<li>%s</li>' % itemview)
    89             self.w(u'<li>%s</li>' % itemview)
    79 
    90 
    80 
    91 
    81 class TreeViewItemView(EntityView):
    92 class TreeViewItemView(EntityView):
    82     """specific treeitem view for entities which implement ITree
    93     """specific treeitem view for entities which implement ITree
    83     
    94 
    84     (each item should be exandable if it's not a tree leaf)
    95     (each item should be exandable if it's not a tree leaf)
    85     """
    96     """
    86     id = 'treeitemview'
    97     id = 'treeitemview'
    87     # XXX append yes to make sure we get an higher score than
    98     # XXX append yes to make sure we get an higher score than
    88     #     the default treeitem view
    99     #     the default treeitem view
    89     __selectors__ = (implement_interface, yes)
   100     __selectors__ = (implement_interface, yes)
    90     accepts_interfaces = (ITree,)
   101     accepts_interfaces = (ITree,)
    91     
   102 
    92     def cell_call(self, row, col, vid='oneline', parentvid='treeview'):
   103     def open_state(self, eeid, treeid):
       
   104         cookies = self.req.get_cookie()
       
   105         treestate = cookies.get(treecookiename(treeid))
       
   106         if treestate:
       
   107             return str(eeid) in treestate.value.split(';')
       
   108         return False
       
   109 
       
   110     def cell_call(self, row, col, treeid, vid='oneline', parentvid='treeview'):
       
   111         w = self.w
    93         entity = self.entity(row, col)
   112         entity = self.entity(row, col)
    94         cssclasses = []
   113         liclasses = []
    95         is_leaf = False
   114         is_leaf = False
    96         if row == len(self.rset) - 1:
   115         is_last = row == len(self.rset) - 1
    97             is_leaf = True
   116         is_open = self.open_state(entity.eid, treeid)
    98         if not hasattr(entity, 'is_leaf') or entity.is_leaf():
   117         if not hasattr(entity, 'is_leaf') or entity.is_leaf():
    99             if is_leaf : cssclasses.append('last')
   118             if is_last:
   100             self.w(u'<li class="%s">' % u' '.join(cssclasses))
   119                 liclasses.append('last')
       
   120             w(u'<li class="%s">' % u' '.join(liclasses))
   101         else:
   121         else:
   102             rql = entity.children_rql() % {'x': entity.eid}
   122             rql = entity.children_rql() % {'x': entity.eid}
   103             url = html_escape(self.build_url('json', rql=rql, vid=parentvid,
   123             url = html_escape(self.build_url('json', rql=rql, vid=parentvid,
   104                                              pageid=self.req.pageid,
   124                                              pageid=self.req.pageid,
   105                                              subvid=vid,
   125                                              treeid=treeid,
   106                                              noautoload=True))
   126                                              subvid=vid))
   107             cssclasses.append('expandable')
   127             divclasses = ['hitarea']
   108             divclasses = ['hitarea expandable-hitarea']
   128             if is_open:
   109             if is_leaf :
   129                 liclasses.append('collapsable')
   110                 cssclasses.append('lastExpandable')
   130                 divclasses.append('collapsable-hitarea')
   111                 divclasses.append('lastExpandable-hitarea')
   131             else:
   112             self.w(u'<li cubicweb:loadurl="%s" class="%s">' % (url, u' '.join(cssclasses)))
   132                 liclasses.append('expandable')
   113             self.w(u'<div class="%s"> </div>' % u' '.join(divclasses))
   133                 divclasses.append('closed-hitarea expandable-hitarea')
   114                 
   134             if is_last:
       
   135                 if is_open:
       
   136                     liclasses.append('lastCollapsable')
       
   137                     divclasses.append('lastCollapsable-hitarea')
       
   138                 else:
       
   139                     liclasses.append('lastExpandable')
       
   140                     divclasses.append('lastExpandable-hitarea')
       
   141             if is_open:
       
   142                 w(u'<li class="%s">' % u' '.join(liclasses))
       
   143             else:
       
   144                 w(u'<li cubicweb:loadurl="%s" class="%s">' % (url, u' '.join(liclasses)))
       
   145             if is_leaf:
       
   146                 divtail = ''
       
   147             else:
       
   148                 divtail = ''' onclick="async_remote_exec('node_clicked', '%s', '%s')"''' % \
       
   149                     (treeid, entity.eid)
       
   150             w(u'<div class="%s"%s></div>' % (u' '.join(divclasses), divtail))
       
   151 
   115             # add empty <ul> because jquery's treeview plugin checks for
   152             # add empty <ul> because jquery's treeview plugin checks for
   116             # sublists presence
   153             # sublists presence
   117             self.w(u'<ul class="placeholder"><li>place holder</li></ul>')
   154             if not is_open:
       
   155                 w(u'<ul class="placeholder"><li>place holder</li></ul>')
       
   156         # the local node info
   118         self.wview(vid, self.rset, row=row, col=col)
   157         self.wview(vid, self.rset, row=row, col=col)
   119         self.w(u'</li>')
   158         if is_open: # recurse if needed
       
   159             self.wview(parentvid, self.req.execute(rql), treeid=treeid, initial_load=False)
       
   160         w(u'</li>')
   120 
   161 
       
   162 from logilab.common.decorators import monkeypatch
       
   163 from cubicweb.web.views.basecontrollers import JSonController
       
   164 
       
   165 @monkeypatch(JSonController)
       
   166 def js_node_clicked(self, treeid, nodeeid):
       
   167     """add/remove eid in treestate cookie"""
       
   168     cookies = self.req.get_cookie()
       
   169     statename = treecookiename(treeid)
       
   170     treestate = cookies.get(statename)
       
   171     if treestate is None:
       
   172         cookies[statename] = nodeeid
       
   173         self.req.set_cookie(cookies, statename)
       
   174     else:
       
   175         marked = set(filter(None, treestate.value.split(';')))
       
   176         if nodeeid in marked:
       
   177             marked.remove(nodeeid)
       
   178         else:
       
   179             marked.add(nodeeid)
       
   180         cookies[statename] = ';'.join(marked)
       
   181         self.req.set_cookie(cookies, statename)