[components] refactor main template header: make it much more flexible by using CtxComponent
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Sat, 09 Oct 2010 00:38:02 +0200
changeset 6428 de95bbed8781
parent 6427 c8a5ac2d1eaa
child 6429 72669e7950c1
[components] refactor main template header: make it much more flexible by using CtxComponent * rename userlink class (not private) with somewhat bw compat * make some cosmetic adjustements on log-in/registration components * update to component api, though this may breaks some sub-classes (for application customizing the breadcrumbs component for instance)
vregistry.py
web/data/cubicweb.css
web/data/cubicweb.login.css
web/data/cubicweb.old.css
web/test/unittest_breadcrumbs.py
web/views/basecomponents.py
web/views/basetemplates.py
web/views/ibreadcrumbs.py
--- a/vregistry.py	Sat Oct 09 00:05:52 2010 +0200
+++ b/vregistry.py	Sat Oct 09 00:38:02 2010 +0200
@@ -195,18 +195,6 @@
     select_object = deprecated('[3.6] use select_or_none instead of select_object'
                                )(select_or_none)
 
-    def selectable(self, oid, *args, **kwargs):
-        """return all appobjects having the given oid that are
-        selectable against the given context, in score order
-        """
-        objects = []
-        for appobject in self[oid]:
-            score = appobject.__select__(appobject, *args, **kwargs)
-            if score > 0:
-                objects.append((score, appobject))
-        return [obj(*args, **kwargs)
-                for _score, obj in sorted(objects)]
-
     def possible_objects(self, *args, **kwargs):
         """return an iterator on possible objects in this registry for the given
         context
--- a/web/data/cubicweb.css	Sat Oct 09 00:05:52 2010 +0200
+++ b/web/data/cubicweb.css	Sat Oct 09 00:38:02 2010 +0200
@@ -222,6 +222,7 @@
 table#header {
   background: %(headerBgColor)s url("banner.png") repeat-x top left;
   text-align: left;
+  width: 100%;
 }
 
 table#header td {
@@ -232,6 +233,15 @@
   color: %(defaultColor)s;
 }
 
+table#header td#headtext {
+  float: left;
+}
+
+table#header td#header-right {
+  padding-top: 1em;
+  float: right;
+}
+
 table#header img#logo{
   vertical-align: middle;
 }
@@ -242,10 +252,6 @@
   white-space: nowrap;
 }
 
-table#header td#headtext {
-  width: 100%;
-}
-
 /* Popup on login box and userActionBox */
 div.popupWrapper {
   position: relative;
--- a/web/data/cubicweb.login.css	Sat Oct 09 00:05:52 2010 +0200
+++ b/web/data/cubicweb.login.css	Sat Oct 09 00:38:02 2010 +0200
@@ -78,6 +78,9 @@
   width: 12em;
 }
 
+/* This is seriously debatable
+   These buttons render much more clearly as standard/default buttons
+ */
 .loginButton {
   border: 1px solid #edecd2;
   border-color: #edecd2 %(incontextBoxBodyBgColor)s %(incontextBoxBodyBgColor)s  #edecd2;
--- a/web/data/cubicweb.old.css	Sat Oct 09 00:05:52 2010 +0200
+++ b/web/data/cubicweb.old.css	Sat Oct 09 00:38:02 2010 +0200
@@ -229,6 +229,7 @@
 table#header {
   background: #ff7700 url("banner.png") left top repeat-x;
   text-align: left;
+  width: 100%;
 }
 
 table#header td {
@@ -236,17 +237,22 @@
 }
 
 table#header a {
-color: #000;
+  color: #000;
+}
+
+table#header td#headtext {
+  float: left;
+}
+
+table#header td#header-right {
+  padding-top: 1em;
+  float: right;
 }
 
 span#appliName {
- font-weight: bold;
- color: #000;
- white-space: nowrap;
-}
-
-table#header td#headtext {
-  width: 100%;
+  font-weight: bold;
+  color: #000;
+  white-space: nowrap;
 }
 
 /* FIXME appear with 4px width in IE6 */
--- a/web/test/unittest_breadcrumbs.py	Sat Oct 09 00:05:52 2010 +0200
+++ b/web/test/unittest_breadcrumbs.py	Sat Oct 09 00:38:02 2010 +0200
@@ -31,8 +31,10 @@
         self.assertEqual(f2.view('breadcrumbs'),
                           '<a href="http://testing.fr/cubicweb/folder/%s" title="">chi&amp;ld</a>' % f2.eid)
         childrset = f2.as_rset()
-        ibc = self.vreg['components'].select('breadcrumbs', self.request(), rset=childrset)
-        self.assertEqual(ibc.render(),
+        ibc = self.vreg['ctxcomponents'].select('breadcrumbs', self.request(), rset=childrset)
+        l = []
+        ibc.render(l.append)
+        self.assertEqual(''.join(l),
                           """<span id="breadcrumbs" class="pathbar">&#160;&gt;&#160;<a href="http://testing.fr/cubicweb/Folder">folder_plural</a>&#160;&gt;&#160;<a href="http://testing.fr/cubicweb/folder/%s" title="">par&amp;ent</a>&#160;&gt;&#160;
 <a href="http://testing.fr/cubicweb/folder/%s" title="">chi&amp;ld</a></span>""" % (f1.eid, f2.eid))
 
--- a/web/views/basecomponents.py	Sat Oct 09 00:05:52 2010 +0200
+++ b/web/views/basecomponents.py	Sat Oct 09 00:38:02 2010 +0200
@@ -72,70 +72,95 @@
         self.w(u'</form></div>')
 
 
-class ApplLogo(component.Component):
-    """build the instance logo, usually displayed in the header"""
-    __regid__ = 'logo'
-    cw_property_defs = VISIBLE_PROP_DEF
-    # don't want user to hide this component using an cwproperty
-    site_wide = True
 
-    def call(self):
-        self.w(u'<a href="%s"><img id="logo" src="%s" alt="logo"/></a>'
-               % (self._cw.base_url(), self._cw.uiprops['LOGO']))
-
-
-class ApplHelp(component.Component):
-    """build the help button, usually displayed in the header"""
-    __regid__ = 'help'
-    cw_property_defs = VISIBLE_PROP_DEF
-    def call(self):
-        self.w(u'<a href="%s" class="help" title="%s">&#160;</a>'
-               % (self._cw.build_url(_restpath='doc/main'),
-                  self._cw._(u'help'),))
-
-
-class _UserLink(component.Component):
+class HeaderComponent(component.CtxComponent): # XXX rename properly along with related context
     """if the user is the anonymous user, build a link to login else display a menu
     with user'action (preference, logout, etc...)
     """
     __abstract__ = True
-    __regid__ = 'loggeduserlink'
-    cw_property_defs = VISIBLE_PROP_DEF
+    cw_property_defs = component.override_ctx(
+        component.CtxComponent,
+        vocabulary=['header-left', 'header-center', 'header-right'])
     # don't want user to hide this component using an cwproperty
     site_wide = True
+    context = _('header-center')
+
+
+class ApplLogo(HeaderComponent):
+    """build the instance logo, usually displayed in the header"""
+    __regid__ = 'logo'
+    order = -1
+
+    def render(self, w):
+        w(u'<a href="%s"><img id="logo" src="%s" alt="logo"/></a>'
+          % (self._cw.base_url(), self._cw.uiprops['LOGO']))
 
 
-class CookieAnonUserLink(_UserLink):
-    __select__ = (_UserLink.__select__ & anonymous_user()
+class ApplicationName(HeaderComponent):
+    """display the instance name"""
+    __regid__ = 'appliname'
+    context = _('header-center')
+
+    def render(self, w):
+        title = self._cw.property_value('ui.site-title')
+        if title:
+            w(u'<span id="appliName"><a href="%s">%s</a></span>' % (
+                self._cw.base_url(), xml_escape(title)))
+
+
+class CookieLoginComponent(HeaderComponent):
+    __regid__ = 'anonuserlink'
+    __select__ = (HeaderComponent.__select__ & anonymous_user()
                   & configuration_values('auth-mode', 'cookie'))
+    context = 'header-right'
     loginboxid = 'popupLoginBox'
+    _html = u"""[<a class="logout" title="%s" href="javascript:
+cw.htmlhelpers.popupLoginBox('%s', '__login');">%s</a>]"""
+
+    def render(self, w):
+        # XXX bw compat, though should warn about subclasses redefining call
+        self.w = w
+        self.call()
 
     def call(self):
-        w = self.w
-        w(self._cw._('anonymous'))
-        w(u"""[<a class="logout" href="javascript: cw.htmlhelpers.popupLoginBox('%s', '__login');">%s</a>]"""
-          % (self.loginboxid, self._cw._('i18n_login_popup')))
+        self.w(self._html % (self._cw._('login / password'),
+                             self.loginboxid, self._cw._('i18n_login_popup')))
         self.wview('logform', rset=self.cw_rset, id=self.loginboxid,
                    klass='hidden', title=False, showmessage=False)
 
-AnonUserLink = class_renamed('AnonUserLink', CookieAnonUserLink)
 
-class HTTPAnonUserLink(_UserLink):
-    __select__ = (_UserLink.__select__ & anonymous_user()
+class HTTPLoginComponent(CookieLoginComponent):
+    __select__ = (HeaderComponent.__select__ & anonymous_user()
                   & configuration_values('auth-mode', 'http'))
 
-    def call(self):
-        w = self.w
-        w(self._cw._('anonymous'))
+    def render(self, w):
         # this redirects to the 'login' controller which in turn
         # will raise a 401/Unauthorized
-        w(u'&#160;[<a class="logout" href="%s">%s</a>]'
-          % (self._cw.build_url('login'), self._cw._('login')))
+        req = self._cw
+        w(u'[<a class="logout" title="%s" href="%s">%s</a>]'
+          % (req._('login / password'), req.build_url('login'), req._('login')))
+
+
+_UserLink = class_renamed('_UserLink', HeaderComponent)
+AnonUserLink = class_renamed('AnonUserLink', CookieLoginComponent)
+AnonUserLink.__abstract__ = True
+AnonUserLink.__select__ &= yes(1)
+
 
-class UserLink(_UserLink):
-    __select__ = _UserLink.__select__ & authenticated_user()
+class AnonUserStatusLink(HeaderComponent):
+    __regid__ = 'userstatus'
+    __select__ = HeaderComponent.__select__ & anonymous_user()
+    context = _('header-right')
+    order = HeaderComponent.order - 10
 
-    def call(self):
+    def render(self, w):
+        w(u'<span class="caption">%s</span>' % self._cw._('anonymous'))
+
+
+class AuthenticatedUserStatus(AnonUserStatusLink):
+    __select__ = HeaderComponent.__select__ & authenticated_user()
+
+    def render(self, w):
         # display useractions and siteactions
         actions = self._cw.vreg['actions'].possible_actions(self._cw, rset=self.cw_rset)
         box = MenuWidget('', 'userActionsBox', _class='', islist=False)
@@ -149,7 +174,7 @@
         for action in actions.get('siteactions', ()):
             menu.append(BoxLink(action.url(), self._cw._(action.title),
                                 action.html_class()))
-        box.render(w=self.w)
+        box.render(w=w)
 
 
 class ApplicationMessage(component.Component):
@@ -171,20 +196,6 @@
         self.w(u'</div>')
 
 
-class ApplicationName(component.Component):
-    """display the instance name"""
-    __regid__ = 'appliname'
-    cw_property_defs = VISIBLE_PROP_DEF
-    # don't want user to hide this component using an cwproperty
-    site_wide = True
-
-    def call(self):
-        title = self._cw.property_value('ui.site-title')
-        if title:
-            self.w(u'<span id="appliName"><a href="%s">%s</a></span>' % (
-                self._cw.base_url(), xml_escape(title)))
-
-
 class EtypeRestrictionComponent(component.Component):
     """displays the list of entity types contained in the resultset
     to be able to filter accordingly.
--- a/web/views/basetemplates.py	Sat Oct 09 00:05:52 2010 +0200
+++ b/web/views/basetemplates.py	Sat Oct 09 00:38:02 2010 +0200
@@ -338,27 +338,16 @@
         """build the top menu with authentification info and the rql box"""
         w = self.w
         w(u'<table id="header"><tr>\n')
-        w(u'<td id="firstcolumn">')
-        logo = self._cw.vreg['components'].select_or_none(
-            'logo', self._cw, rset=self.cw_rset)
-        if logo and logo.cw_propval('visible'):
-            logo.render(w=w)
-        w(u'</td>\n')
-        # appliname and breadcrumbs
-        w(u'<td id="headtext">')
-        for cid in self.main_cell_components:
-            comp = self._cw.vreg['components'].select_or_none(
-                cid, self._cw, rset=self.cw_rset)
-            if comp and comp.cw_propval('visible'):
-                comp.render(w=self.w)
-        w(u'</td>')
-        # logged user and help
-        login_components = self._cw.vreg['components'].selectable(
-            'loggeduserlink', self._cw, rset=self.cw_rset)
-        for comp in login_components:
-            w(u'<td>\n')
-            if comp.cw_propval('visible'):
+        for colid, context in (('firstcolumn', 'header-left'),
+                               ('headtext', 'header-center'),
+                               ('header-right', 'header-right'),
+                               ):
+            w(u'<td id="%s">' % colid)
+            components = self._cw.vreg['ctxcomponents'].poss_visible_objects(
+                self._cw, rset=self.cw_rset, view=view, context=context)
+            for comp in components:
                 comp.render(w=w)
+                w(u'&nbsp;')
             w(u'</td>')
         w(u'</tr></table>\n')
 
--- a/web/views/ibreadcrumbs.py	Sat Oct 09 00:05:52 2010 +0200
+++ b/web/views/ibreadcrumbs.py	Sat Oct 09 00:38:02 2010 +0200
@@ -25,12 +25,13 @@
 from logilab.mtconverter import xml_escape
 
 #from cubicweb.interfaces import IBreadCrumbs
+from cubicweb import tags, uilib
+from cubicweb.entity import Entity
 from cubicweb.selectors import (is_instance, one_line_rset, adaptable,
                                 one_etype_rset, multi_lines_rset, any_rset)
-from cubicweb.view import EntityView, Component, EntityAdapter
+from cubicweb.view import EntityView, EntityAdapter
+from cubicweb.web.views import basecomponents
 # don't use AnyEntity since this may cause bug with isinstance() due to reloading
-from cubicweb.entity import Entity
-from cubicweb import tags, uilib
 
 
 # ease bw compat
@@ -90,84 +91,82 @@
         return path
 
 
-class BreadCrumbEntityVComponent(Component):
+class BreadCrumbEntityVComponent(basecomponents.HeaderComponent):
     __regid__ = 'breadcrumbs'
-    __select__ = one_line_rset() & adaptable('IBreadCrumbs')
-
-    cw_property_defs = {
-        _('visible'):  dict(type='Boolean', default=True,
-                            help=_('display the component or not')),
-        }
-    # title = _('ctxcomponents_breadcrumbs')
-    # help = _('ctxcomponents_breadcrumbs_description')
+    __select__ = (basecomponents.HeaderComponent.__select__
+                  & one_line_rset() & adaptable('IBreadCrumbs'))
+    order = basecomponents.ApplicationName.order + 1
     separator = u'&#160;&gt;&#160;'
     link_template = u'<a href="%s">%s</a>'
+    first_separator = True
 
-    def call(self, view=None, first_separator=True):
+    def render(self, w):
         entity = self.cw_rset.get_entity(0, 0)
         adapter = ibreadcrumb_adapter(entity)
+        view = self.cw_extra_kwargs.get('view')
         path = adapter.breadcrumbs(view)
         if path:
-            self.open_breadcrumbs()
-            if first_separator:
-                self.w(self.separator)
-            self.render_breadcrumbs(entity, path)
-            self.close_breadcrumbs()
+            self.open_breadcrumbs(w)
+            if self.first_separator:
+                w(self.separator)
+            self.render_breadcrumbs(w, entity, path)
+            self.close_breadcrumbs(w)
 
-    def open_breadcrumbs(self):
-        self.w(u'<span id="breadcrumbs" class="pathbar">')
+    def open_breadcrumbs(self, w):
+        w(u'<span id="breadcrumbs" class="pathbar">')
 
-    def close_breadcrumbs(self):
-        self.w(u'</span>')
+    def close_breadcrumbs(self, w):
+        w(u'</span>')
 
-    def render_breadcrumbs(self, contextentity, path):
+    def render_breadcrumbs(self, w, contextentity, path):
         root = path.pop(0)
         if isinstance(root, Entity):
-            self.w(self.link_template % (self._cw.build_url(root.__regid__),
+            w(self.link_template % (self._cw.build_url(root.__regid__),
                                          root.dc_type('plural')))
-            self.w(self.separator)
-        self.wpath_part(root, contextentity, not path)
+            w(self.separator)
+        self.wpath_part(w, root, contextentity, not path)
         for i, parent in enumerate(path):
-            self.w(self.separator)
-            self.w(u"\n")
-            self.wpath_part(parent, contextentity, i == len(path) - 1)
+            w(self.separator)
+            w(u"\n")
+            self.wpath_part(w, parent, contextentity, i == len(path) - 1)
 
-    def wpath_part(self, part, contextentity, last=False): # XXX deprecates last argument?
+    def wpath_part(self, w, part, contextentity, last=False): # XXX deprecates last argument?
         if isinstance(part, Entity):
-            self.w(part.view('breadcrumbs'))
+            w(part.view('breadcrumbs'))
         elif isinstance(part, tuple):
             url, title = part
             textsize = self._cw.property_value('navigation.short-line-size')
-            self.w(self.link_template % (
+            w(self.link_template % (
                 xml_escape(url), xml_escape(uilib.cut(title, textsize))))
         else:
             textsize = self._cw.property_value('navigation.short-line-size')
-            self.w(uilib.cut(unicode(part), textsize))
+            w(uilib.cut(unicode(part), textsize))
 
 
 class BreadCrumbETypeVComponent(BreadCrumbEntityVComponent):
-    __select__ = multi_lines_rset() & one_etype_rset() & \
-                 adaptable('IBreadCrumbs')
+    __select__ = (basecomponents.HeaderComponent.__select__
+                  & multi_lines_rset() & one_etype_rset()
+                  & adaptable('IBreadCrumbs'))
 
-    def render_breadcrumbs(self, contextentity, path):
+    def render_breadcrumbs(self, w, contextentity, path):
         # XXX hack: only display etype name or first non entity path part
         root = path.pop(0)
         if isinstance(root, Entity):
-            self.w(u'<a href="%s">%s</a>' % (self._cw.build_url(root.__regid__),
-                                             root.dc_type('plural')))
+            w(u'<a href="%s">%s</a>' % (self._cw.build_url(root.__regid__),
+                                        root.dc_type('plural')))
         else:
-            self.wpath_part(root, contextentity, not path)
+            self.wpath_part(w, root, contextentity, not path)
 
 
 class BreadCrumbAnyRSetVComponent(BreadCrumbEntityVComponent):
-    __select__ = any_rset()
+    __select__ = basecomponents.HeaderComponent.__select__ & any_rset()
 
-    def call(self, view=None, first_separator=True):
-        self.w(u'<span id="breadcrumbs" class="pathbar">')
-        if first_separator:
-            self.w(self.separator)
-        self.w(self._cw._('search'))
-        self.w(u'</span>')
+    def render(self, w):
+        w(u'<span id="breadcrumbs" class="pathbar">')
+        if self.first_separator:
+            w(self.separator)
+        w(self._cw._('search'))
+        w(u'</span>')
 
 
 class BreadCrumbView(EntityView):