web/views/boxes.py
changeset 6140 65a619eb31c4
parent 5839 53cbdc5e0426
child 6141 b8287e54b528
--- a/web/views/boxes.py	Wed Aug 25 09:43:12 2010 +0200
+++ b/web/views/boxes.py	Wed Aug 25 10:01:11 2010 +0200
@@ -18,10 +18,11 @@
 """Generic boxes for CubicWeb web client:
 
 * actions box
-* possible views box
+* search box
 
-additional (disabled by default) boxes
+Additional boxes (disabled by default):
 * schema box
+* possible views box
 * startup views box
 """
 
@@ -31,42 +32,41 @@
 from warnings import warn
 
 from logilab.mtconverter import xml_escape
+from logilab.common.deprecation import class_deprecated
 
-from cubicweb.selectors import match_user_groups, non_final_entity
+from cubicweb import Unauthorized
+from cubicweb.selectors import (match_user_groups, match_context, match_kwargs,
+                                non_final_entity, nonempty_rset)
 from cubicweb.view import EntityView
 from cubicweb.schema import display_name
-from cubicweb.web.htmlwidgets import BoxWidget, BoxMenu, BoxHtml, RawBoxItem
-from cubicweb.web.box import BoxTemplate
+from cubicweb.web import box, htmlwidgets
 
+# XXX bw compat, some cubes import this class from here
+BoxTemplate = box.BoxTemplate
+BoxHtml = htmlwidgets.BoxHtml
 
-class EditBox(BoxTemplate): # XXX rename to ActionsBox
+class EditBox(box.Box): # XXX rename to ActionsBox
     """
     box with all actions impacting the entity displayed: edit, copy, delete
     change state, add related entities
     """
     __regid__ = 'edit_box'
-    __select__ = BoxTemplate.__select__ & non_final_entity()
+    __select__ = box.Box.__select__ & non_final_entity()
 
     title = _('actions')
     order = 2
+    contextual = True
 
-    def call(self, view=None, **kwargs):
+    def init_rendering(self):
+        super(EditBox, self).init_rendering()
         _ = self._cw._
-        title = _(self.title)
-        if self.cw_rset:
-            etypes = self.cw_rset.column_types(0)
-            if len(etypes) == 1:
-                plural = self.cw_rset.rowcount > 1 and 'plural' or ''
-                etypelabel = display_name(self._cw, iter(etypes).next(), plural)
-                title = u'%s - %s' % (title, etypelabel.lower())
-        box = BoxWidget(title, self.__regid__, _class="greyBoxFrame")
         self._menus_in_order = []
         self._menus_by_id = {}
         # build list of actions
         actions = self._cw.vreg['actions'].possible_actions(self._cw, self.cw_rset,
-                                                            view=view)
+                                                            **self.cw_extra_kwargs)
         other_menu = self._get_menu('moreactions', _('more actions'))
-        for category, defaultmenu in (('mainactions', box),
+        for category, defaultmenu in (('mainactions', self),
                                       ('moreactions', other_menu),
                                       ('addrelated', None)):
             for action in actions.get(category, ()):
@@ -81,16 +81,28 @@
                     menu = defaultmenu
                 action.fill_menu(self, menu)
         # if we've nothing but actions in the other_menu, add them directly into the box
-        if box.is_empty() and len(self._menus_by_id) == 1 and not other_menu.is_empty():
-            box.items = other_menu.items
-            other_menu.items = []
+        if not self.items and len(self._menus_by_id) == 1 and not other_menu.is_empty():
+            self.items = other_menu.items
         else: # ensure 'more actions' menu appears last
             self._menus_in_order.remove(other_menu)
             self._menus_in_order.append(other_menu)
-        for submenu in self._menus_in_order:
-            self.add_submenu(box, submenu)
-        if not box.is_empty():
-            box.render(self.w)
+            for submenu in self._menus_in_order:
+                self.add_submenu(self, submenu)
+        if not self.items:
+            raise box.EmptyComponent()
+
+    def render_title(self, w):
+        title = self._cw._(self.title)
+        if self.cw_rset:
+            etypes = self.cw_rset.column_types(0)
+            if len(etypes) == 1:
+                plural = self.cw_rset.rowcount > 1 and 'plural' or ''
+                etypelabel = display_name(self._cw, iter(etypes).next(), plural)
+                title = u'%s - %s' % (title, etypelabel.lower())
+        w(title)
+
+    def render_body(self, w):
+        self.render_items(w)
 
     def _get_menu(self, id, title=None, label_prefix=None):
         try:
@@ -98,7 +110,7 @@
         except KeyError:
             if title is None:
                 title = self._cw._(id)
-            self._menus_by_id[id] = menu = BoxMenu(title)
+            self._menus_by_id[id] = menu = htmlwidgets.BoxMenu(title)
             menu.label_prefix = label_prefix
             self._menus_in_order.append(menu)
             return menu
@@ -108,19 +120,22 @@
         if len(submenu.items) == 1 and not appendanyway:
             boxlink = submenu.items[0]
             if submenu.label_prefix:
-                boxlink.label = u'%s %s' % (submenu.label_prefix, boxlink.label)
+                # XXX iirk
+                if hasattr(boxlink, 'label'):
+                    boxlink.label = u'%s %s' % (submenu.label_prefix, boxlink.label)
+                else:
+                    submenu.items[0] = u'%s %s' % (submenu.label_prefix, boxlink)
             box.append(boxlink)
         elif submenu.items:
             box.append(submenu)
         elif appendanyway:
-            box.append(RawBoxItem(xml_escape(submenu.label)))
+            box.append(xml_escape(submenu.label))
 
 
-class SearchBox(BoxTemplate):
+class SearchBox(box.Box):
     """display a box with a simple search form"""
     __regid__ = 'search_box'
 
-    visible = True # enabled by default
     title = _('search')
     order = 0
     formdef = u"""<form action="%s">
@@ -130,74 +145,123 @@
 <input type="hidden" name="subvid" value="tsearch" />
 </td><td>
 <input tabindex="%s" type="submit" id="rqlboxsubmit" class="rqlsubmit" value="" />
-</td></tr></table>
-</form>"""
+ </td></tr></table>
+ </form>"""
 
-    def call(self, view=None, **kwargs):
-        req = self._cw
-        if req.form.pop('__fromsearchbox', None):
-            rql = req.form.get('rql', '')
+    def render_title(self, w):
+        w(u"""<span onclick="javascript: toggleVisibility('rqlinput')">%s</span>"""
+          % self._cw._(self.title))
+
+    def render_body(self, w):
+        if self._cw.form.pop('__fromsearchbox', None):
+            rql = self._cw.form.get('rql', '')
         else:
             rql = ''
-        form = self.formdef % (req.build_url('view'), req.next_tabindex(),
-                               xml_escape(rql), req.next_tabindex())
-        title = u"""<span onclick="javascript: toggleVisibility('rqlinput')">%s</span>""" % req._(self.title)
-        box = BoxWidget(title, self.__regid__, _class="searchBoxFrame", islist=False, escape=False)
-        box.append(BoxHtml(form))
-        box.render(self.w)
+        w(self.formdef % (self._cw.build_url('view'), self._cw.next_tabindex(),
+                          xml_escape(rql), self._cw.next_tabindex()))
 
 
 # boxes disabled by default ###################################################
 
-class PossibleViewsBox(BoxTemplate):
+class PossibleViewsBox(box.Box):
     """display a box containing links to all possible views"""
     __regid__ = 'possible_views_box'
-    __select__ = BoxTemplate.__select__ & match_user_groups('users', 'managers')
 
     visible = False
     title = _('possible views')
     order = 10
 
-    def call(self, **kwargs):
-        box = BoxWidget(self._cw._(self.title), self.__regid__)
-        views = [v for v in self._cw.vreg['views'].possible_views(self._cw,
-                                                              rset=self.cw_rset)
-                 if v.category != 'startupview']
-        for category, views in self.sort_actions(views):
-            menu = BoxMenu(category)
+    def init_rendering(self):
+        self.views = [v for v in self._cw.vreg['views'].possible_views(self._cw,
+                                                                       rset=self.cw_rset)
+                      if v.category != 'startupview']
+        if not self.views:
+            raise box.EmptyComponent()
+        self.items = []
+
+    def render_body(self, w):
+        for category, views in box.sort_by_category(self.views):
+            menu = htmlwidgets.BoxMenu(category)
             for view in views:
                 menu.append(self.box_action(view))
-            box.append(menu)
-        if not box.is_empty():
-            box.render(self.w)
+            self.append(menu)
+        self.render_items(w)
 
 
-class StartupViewsBox(BoxTemplate):
+class StartupViewsBox(PossibleViewsBox):
     """display a box containing links to all startup views"""
     __regid__ = 'startup_views_box'
+
     visible = False # disabled by default
     title = _('startup views')
     order = 70
 
-    def call(self, **kwargs):
-        box = BoxWidget(self._cw._(self.title), self.__regid__)
-        for view in self._cw.vreg['views'].possible_views(self._cw, None):
-            if view.category == 'startupview':
-                box.append(self.box_action(view))
-        if not box.is_empty():
-            box.render(self.w)
+    def init_rendering(self):
+        self.views = [v for v in self._cw.vreg['views'].possible_views(self._cw)
+                      if v.category == 'startupview']
+        if not self.views:
+            raise box.EmptyComponent()
+        self.items = []
 
 
-# helper classes ##############################################################
+class RsetBox(box.Box):
+    """helper view class to display an rset in a sidebox"""
+    __select__ = nonempty_rset() & match_kwargs('title', 'vid')
+    __regid__ = 'rsetbox'
+    cw_property_defs = {}
+    context = 'incontext'
+
+    @property
+    def domid(self):
+        return super(RsetBox, self).domid + unicode(abs(id(self)))
+    def render_title(self, w):
+        w(self.cw_extra_kwargs['title'])
+
+    def render_body(self, w):
+        self._cw.view(self.cw_extra_kwargs['vid'], self.cw_rset, w=w)
+
+ # helper classes ##############################################################
 
 class SideBoxView(EntityView):
     """helper view class to display some entities in a sidebox"""
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = 'SideBoxView is deprecated, use RsetBox instead'
+
     __regid__ = 'sidebox'
 
-    def call(self, boxclass='sideBox', title=u''):
+    def call(self, **kwargs):
         """display a list of entities by calling their <item_vid> view"""
-        if title:
-            self.w(u'<div class="sideBoxTitle"><span>%s</span></div>' % title)
-        self.w(u'<div class="%s"><div class="sideBoxBody">' % boxclass)
-        self.wview('autolimited', self.cw_rset, **self.cw_extra_kwargs)
-        self.w(u'</div>\n</div>\n')
+        box = self._cw.vreg['boxes'].select('rsetbox', self._cw, rset=self.cw_rset,
+                                            vid='autolimited', title=title,
+                                            **self.cw_extra_kwargs)
+        box.render(self.w)
+
+
+class ContextualBoxLayout(box.Layout):
+    __select__ = match_context('incontext', 'left', 'right') & box.contextual()
+    # predefined class in cubicweb.css: contextualBox | contextFreeBox
+    # XXX: navigationBox | actionBox
+    cssclass = 'contextualBox'
+
+    def render(self, w):
+        view = self.cw_extra_kwargs['view']
+        try:
+            view.init_rendering()
+        except Unauthorized, ex:
+            self.warning("can't render %s: %s", view, ex)
+            return
+        except box.EmptyComponent:
+            return
+        w(u'<div class="%s %s" id="%s">' % (self.cssclass, view.cssclass,
+                                            view.domid))
+        w(u'<div class="boxTitle"><span>')
+        view.render_title(w)
+        w(u'</span></div>\n<div class="boxBody">')
+        view.render_body(w)
+        # boxFooter div is a CSS place holder (for shadow for example)
+        w(u'</div><div class="boxFooter"></div></div>\n')
+
+
+class ContextFreeBoxLayout(ContextualBoxLayout):
+    __select__ = match_context('incontext', 'left', 'right') & ~box.contextual()
+    cssclass = 'contextFreeBox'