diff -r 058bb3dc685f -r 0b59724cb3f2 cubicweb/web/views/boxes.py --- /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 . +"""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"""
+
+ + + + + +
+
""" + + def render_title(self, w): + w(u"""%s""" + % 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 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'
' % (self.cssclass, view.cssclass, + view.domid)) + with wrap_on_write(w, u'
', + u'
') as wow: + view.render_title(wow) + w(u'
') + view.render_body(w) + # boxFooter div is a CSS place holder (for shadow for example) + w(u'
\n') + + +class ContextFreeBoxLayout(ContextualBoxLayout): + __select__ = match_context('incontext', 'left', 'right') & ~contextual() + cssclass = 'contextFreeBox'