--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/views/boxes.py Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,279 @@
+# 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 <http://www.gnu.org/licenses/>.
+"""Generic boxes for CubicWeb web client:
+
+* actions box
+* search box
+
+Additional boxes (disabled by default):
+* schema box
+* possible views box
+* startup views box
+"""
+__docformat__ = "restructuredtext en"
+from cubicweb import _
+
+from warnings import warn
+
+from six import text_type, add_metaclass
+
+from logilab.mtconverter import xml_escape
+from logilab.common.deprecation import class_deprecated
+
+from cubicweb import Unauthorized
+from cubicweb.predicates import (match_user_groups, match_kwargs,
+ non_final_entity, nonempty_rset,
+ match_context, contextual)
+from cubicweb.utils import wrap_on_write
+from cubicweb.view import EntityView
+from cubicweb.schema import display_name
+from cubicweb.web import component, box, htmlwidgets
+
+# XXX bw compat, some cubes import this class from here
+BoxTemplate = box.BoxTemplate
+BoxHtml = htmlwidgets.BoxHtml
+
+class EditBox(component.CtxComponent):
+ """
+ box with all actions impacting the entity displayed: edit, copy, delete
+ change state, add related entities...
+ """
+ __regid__ = 'edit_box'
+
+ title = _('actions')
+ order = 2
+ contextual = True
+ __select__ = component.CtxComponent.__select__ & non_final_entity()
+
+ def init_rendering(self):
+ super(EditBox, self).init_rendering()
+ _ = self._cw._
+ self._menus_in_order = []
+ self._menus_by_id = {}
+ # build list of actions
+ actions = self._cw.vreg['actions'].possible_actions(self._cw, self.cw_rset,
+ **self.cw_extra_kwargs)
+ other_menu = self._get_menu('moreactions', _('more actions'))
+ for category, defaultmenu in (('mainactions', self),
+ ('moreactions', other_menu),
+ ('addrelated', None)):
+ for action in actions.get(category, ()):
+ if action.submenu:
+ menu = self._get_menu(action.submenu)
+ else:
+ menu = defaultmenu
+ action.fill_menu(self, menu)
+ # if we've nothing but actions in the other_menu, add them directly into the box
+ 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(self, submenu)
+ if not self.items:
+ raise component.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, next(iter(etypes)), 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:
+ return self._menus_by_id[id]
+ except KeyError:
+ if title is None:
+ title = self._cw._(id)
+ self._menus_by_id[id] = menu = htmlwidgets.BoxMenu(title)
+ menu.label_prefix = label_prefix
+ self._menus_in_order.append(menu)
+ return menu
+
+ def add_submenu(self, box, submenu, label_prefix=None):
+ appendanyway = getattr(submenu, 'append_anyway', False)
+ if len(submenu.items) == 1 and not appendanyway:
+ boxlink = submenu.items[0]
+ if submenu.label_prefix:
+ # XXX iirk
+ if hasattr(boxlink, 'label'):
+ boxlink.label = u'%s %s' % (submenu.label_prefix, boxlink.label)
+ else:
+ boxlink = u'%s %s' % (submenu.label_prefix, boxlink)
+ box.append(boxlink)
+ elif submenu.items:
+ box.append(submenu)
+ elif appendanyway:
+ box.append(xml_escape(submenu.label))
+
+
+class SearchBox(component.CtxComponent):
+ """display a box with a simple search form"""
+ __regid__ = 'search_box'
+
+ title = _('search')
+ order = 0
+ formdef = u"""<form action="%(action)s">
+<table id="%(id)s"><tr><td>
+<input class="norql" type="text" accesskey="q" tabindex="%(tabindex1)s" title="search text" value="%(value)s" name="rql" />
+<input type="hidden" name="__fromsearchbox" value="1" />
+<input type="hidden" name="subvid" value="tsearch" />
+</td><td>
+<input tabindex="%(tabindex2)s" type="submit" class="rqlsubmit" value="" />
+ </td></tr></table>
+ </form>"""
+
+ 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 = ''
+ tabidx1 = self._cw.next_tabindex()
+ tabidx2 = self._cw.next_tabindex()
+ w(self.formdef % {'action': self._cw.build_url('view'),
+ 'value': xml_escape(rql),
+ 'id': self.cw_extra_kwargs.get('domid', 'tsearch'),
+ 'tabindex1': tabidx1,
+ 'tabindex2': tabidx2})
+
+
+# boxes disabled by default ###################################################
+
+class PossibleViewsBox(component.CtxComponent):
+ """display a box containing links to all possible views"""
+ __regid__ = 'possible_views_box'
+
+ contextual = True
+ title = _('possible views')
+ order = 10
+ visible = False # disabled by default
+
+ 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 component.EmptyComponent()
+ self.items = []
+
+ def render_body(self, w):
+ for category, views in box.sort_by_category(self.views):
+ menu = htmlwidgets.BoxMenu(self._cw._(category), ident=category)
+ for view in views:
+ menu.append(self.action_link(view))
+ self.append(menu)
+ self.render_items(w)
+
+
+class StartupViewsBox(PossibleViewsBox):
+ """display a box containing links to all startup views"""
+ __regid__ = 'startup_views_box'
+
+ contextual = False
+ title = _('startup views')
+ order = 70
+ visible = False # disabled by default
+
+ 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 component.EmptyComponent()
+ self.items = []
+
+
+class RsetBox(component.CtxComponent):
+ """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 + text_type(abs(id(self))) + text_type(abs(id(self.cw_rset)))
+
+ def render_title(self, w):
+ w(self.cw_extra_kwargs['title'])
+
+ def render_body(self, w):
+ if 'dispctrl' in self.cw_extra_kwargs:
+ # XXX do not modify dispctrl!
+ self.cw_extra_kwargs['dispctrl'].setdefault('subvid', 'outofcontext')
+ self.cw_extra_kwargs['dispctrl'].setdefault('use_list_limit', 1)
+ self._cw.view(self.cw_extra_kwargs['vid'], self.cw_rset, w=w,
+ initargs=self.cw_extra_kwargs)
+
+ # helper classes ##############################################################
+
+@add_metaclass(class_deprecated)
+class SideBoxView(EntityView):
+ """helper view class to display some entities in a sidebox"""
+ __deprecation_warning__ = '[3.10] SideBoxView is deprecated, use RsetBox instead (%(cls)s)'
+
+ __regid__ = 'sidebox'
+
+ def call(self, title=u'', **kwargs):
+ """display a list of entities by calling their <item_vid> view"""
+ if 'dispctrl' in self.cw_extra_kwargs:
+ # XXX do not modify dispctrl!
+ self.cw_extra_kwargs['dispctrl'].setdefault('subvid', 'outofcontext')
+ self.cw_extra_kwargs['dispctrl'].setdefault('use_list_limit', 1)
+ if title:
+ self.cw_extra_kwargs['title'] = title
+ self.cw_extra_kwargs.setdefault('context', 'incontext')
+ box = self._cw.vreg['ctxcomponents'].select(
+ 'rsetbox', self._cw, rset=self.cw_rset, vid='autolimited',
+ **self.cw_extra_kwargs)
+ box.render(self.w)
+
+
+class ContextualBoxLayout(component.Layout):
+ __select__ = match_context('incontext', 'left', 'right') & contextual()
+ # predefined class in cubicweb.css: contextualBox | contextFreeBox
+ cssclass = 'contextualBox'
+
+ def render(self, w):
+ if self.init_rendering():
+ view = self.cw_extra_kwargs['view']
+ w(u'<div class="%s %s" id="%s">' % (self.cssclass, view.cssclass,
+ view.domid))
+ with wrap_on_write(w, u'<div class="boxTitle"><span>',
+ u'</span></div>') as wow:
+ view.render_title(wow)
+ w(u'<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') & ~contextual()
+ cssclass = 'contextFreeBox'