cubicweb/web/views/tabs.py
changeset 11057 0b59724cb3f2
parent 10666 7f6b5f023884
child 11767 432f87a63057
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/views/tabs.py	Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,249 @@
+# copyright 2003-2012 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 <http://www.gnu.org/licenses/>.
+"""base classes to handle tabbed views"""
+
+__docformat__ = "restructuredtext en"
+from cubicweb import _
+
+from six import string_types
+
+from logilab.common.deprecation import class_renamed
+from logilab.mtconverter import xml_escape
+
+from cubicweb import NoSelectableObject, role
+from cubicweb import tags, uilib, utils
+from cubicweb.predicates import partial_has_related_entities
+from cubicweb.view import EntityView
+from cubicweb.web.views import primary
+
+class LazyViewMixin(object):
+    """provides two convenience methods for the tab machinery.
+
+    Can also be used to lazy-load arbitrary views.
+    """
+
+    def _prepare_bindings(self, vid, reloadable):
+        self._cw.add_onload(u"""
+  jQuery('#lazy-%(vid)s').bind('%(event)s', function(event) {
+     loadNow('#lazy-%(vid)s', '#%(vid)s-hole', %(reloadable)s);
+  });""" % {'event': 'load_%s' % vid, 'vid': vid,
+            'reloadable' : str(reloadable).lower()})
+
+    def lazyview(self, vid, rql=None, eid=None, rset=None, tabid=None,
+                 reloadable=False, show_spinbox=True, w=None):
+        """a lazy version of wview"""
+        w = w or self.w
+        self._cw.add_js('cubicweb.ajax.js')
+        # the form is copied into urlparams to please the inner views
+        # that might want to take params from it
+        # beware of already present rql or eid elements
+        # to be safe of collision a proper argument passing protocol
+        # (with namespaces) should be used instead of the current
+        # ad-hockery
+        urlparams = self._cw.form.copy()
+        urlparams.pop('rql', None)
+        urlparams.pop('eid', None)
+        urlparams.update({'vid' : vid, 'fname' : 'view'})
+        if rql:
+            urlparams['rql'] = rql
+        elif eid:
+            urlparams['eid'] = eid
+        elif rset:
+            urlparams['rql'] = rset.printable_rql()
+        if tabid is None:
+            tabid = uilib.domid(vid)
+        w(u'<div id="lazy-%s" cubicweb:loadurl="%s">' % (
+            tabid, xml_escape(self._cw.build_url('ajax', **urlparams))))
+        if show_spinbox:
+            # Don't use ``alt`` since image is a *visual* helper for ajax
+            w(u'<img style="display: none" src="%s" alt="" id="%s-hole"/>'
+              % (xml_escape(self._cw.data_url('loading.gif')), tabid))
+        else:
+            w(u'<div id="%s-hole"></div>' % tabid)
+        w(u'<noscript><p>%s <a id="seo-%s" href="%s">%s</a></p></noscript>'
+          % (xml_escape(self._cw._('Link:')),
+             tabid,
+             xml_escape(self._cw.build_url(**urlparams)),
+             xml_escape(self._cw._(tabid))))
+        w(u'</div>')
+        self._prepare_bindings(tabid, reloadable)
+
+    def forceview(self, vid):
+        """trigger an event that will force immediate loading of the view on dom
+        readyness
+        """
+        self._cw.add_onload(uilib.js.triggerLoad(vid))
+
+
+class TabsMixin(LazyViewMixin):
+    """a tab mixin to easily get jQuery based, lazy, ajax tabs"""
+    lazy = True
+
+    @property
+    def cookie_name(self):
+        return str('%s_active_tab' % self._cw.vreg.config.appid)
+
+    def active_tab(self, default):
+        if 'tab' in self._cw.form:
+            return self._cw.form['tab']
+        cookies = self._cw.get_cookie()
+        cookiename = self.cookie_name
+        activetab = cookies.get(cookiename)
+        if activetab is None:
+            domid = uilib.domid(default)
+            self._cw.set_cookie(cookiename, domid)
+            return domid
+        return activetab.value
+
+    def prune_tabs(self, tabs, default_tab):
+        selected_tabs = []
+        may_be_active_tab = self.active_tab(default_tab)
+        active_tab = uilib.domid(default_tab)
+        viewsvreg = self._cw.vreg['views']
+        for tab in tabs:
+            if isinstance(tab, string_types):
+                tabid, tabkwargs = tab, {}
+            else:
+                tabid, tabkwargs = tab
+                tabkwargs = tabkwargs.copy()
+            tabkwargs.setdefault('rset', self.cw_rset)
+            vid = tabkwargs.get('vid', tabid)
+            domid = uilib.domid(tabid)
+            try:
+                viewsvreg.select(vid, self._cw, tabid=domid, **tabkwargs)
+            except NoSelectableObject:
+                continue
+            selected_tabs.append((tabid, domid, tabkwargs))
+            if domid == may_be_active_tab:
+                active_tab = domid
+        return selected_tabs, active_tab
+
+    def render_tabs(self, tabs, default, entity=None):
+        # delegate to the default tab if there is more than one entity
+        # in the result set (tabs are pretty useless there)
+        if entity and len(self.cw_rset) > 1:
+            entity.view(default, w=self.w)
+            return
+        self._cw.add_css('jquery.ui.css')
+        self._cw.add_js(('jquery.ui.js', 'cubicweb.ajax.js', 'jquery.cookie.js'))
+        # prune tabs : not all are to be shown
+        tabs, active_tab = self.prune_tabs(tabs, default)
+        # build the html structure
+        w = self.w
+        uid = entity and entity.eid or utils.make_uid('tab')
+        w(u'<div id="entity-tabs-%s">' % uid)
+        w(u'<ul>')
+        active_tab_idx = None
+        for i, (tabid, domid, tabkwargs) in enumerate(tabs):
+            w(u'<li>')
+            w(u'<a href="#%s">' % domid)
+            w(tabkwargs.pop('label', self._cw._(tabid)))
+            w(u'</a>')
+            w(u'</li>')
+            if domid == active_tab:
+                active_tab_idx = i
+        w(u'</ul>')
+        for tabid, domid, tabkwargs in tabs:
+            w(u'<div id="%s">' % domid)
+            if self.lazy:
+                tabkwargs.setdefault('tabid', domid)
+                tabkwargs.setdefault('vid', tabid)
+                self.lazyview(**tabkwargs)
+            else:
+                self._cw.view(tabid, w=self.w, **tabkwargs)
+            w(u'</div>')
+        w(u'</div>')
+        # call the setTab() JS function *after* each tab is generated
+        # because the callback binding needs to be done before
+        # XXX make work history: true
+        if self.lazy:
+            self._cw.add_onload(u"""
+  jQuery('#entity-tabs-%(uid)s').tabs(
+    { active: %(tabindex)s,
+      activate: function(event, ui) {
+        setTab(ui.newPanel.attr('id'), '%(cookiename)s');
+      }
+    });
+  setTab('%(domid)s', '%(cookiename)s');
+""" % {'tabindex'   : active_tab_idx,
+       'domid'      : active_tab,
+       'uid'        : uid,
+       'cookiename' : self.cookie_name})
+        else:
+            self._cw.add_onload(
+                u"jQuery('#entity-tabs-%(uid)s').tabs({active: %(tabindex)s});"
+                % {'tabindex': active_tab_idx, 'uid': uid})
+
+
+class EntityRelationView(EntityView):
+    """view displaying entity related stuff.
+    Such a view _must_ provide the rtype, target and vid attributes :
+
+    Example :
+
+    class ProjectScreenshotsView(EntityRelationView):
+        '''display project's screenshots'''
+        __regid__ = title = _('projectscreenshots')
+        __select__ = EntityRelationView.__select__ & is_instance('Project')
+        rtype = 'screenshot'
+        role = 'subject'
+        vid = 'gallery'
+
+    in this example, entities related to project entity by the 'screenshot'
+    relation (where the project is subject of the relation) will be displayed
+    using the 'gallery' view.
+    """
+    __select__ = EntityView.__select__ & partial_has_related_entities()
+    vid = 'list'
+    # to be defined in concrete classes
+    rtype = title = None
+
+    def cell_call(self, row, col):
+        rset = self.cw_rset.get_entity(row, col).related(self.rtype, role(self))
+        self.w(u'<div class="mainInfo">')
+        if self.title:
+            self.w(tags.h1(self._cw._(self.title)))
+        self.wview(self.vid, rset, 'noresult')
+        self.w(u'</div>')
+
+
+class TabbedPrimaryView(TabsMixin, primary.PrimaryView):
+    __abstract__ = True # don't register
+
+    tabs = [_('main_tab')]
+    default_tab = 'main_tab'
+
+    def render_entity(self, entity):
+        self.render_entity_toolbox(entity)
+        self.w(u'<div class="tabbedprimary"></div>')
+        self.render_entity_title(entity)
+        self.render_tabs(self.tabs, self.default_tab, entity)
+
+TabedPrimaryView = class_renamed('TabedPrimaryView', TabbedPrimaryView)
+
+class PrimaryTab(primary.PrimaryView):
+    __regid__ = 'main_tab'
+    title = None # should not appear in possible views
+
+    def is_primary(self):
+        return True
+
+    def render_entity_title(self, entity):
+        pass
+    def render_entity_toolbox(self, entity):
+        pass