web/views/boxes.py
author Julien Cristau <julien.cristau@logilab.fr>
Tue, 13 Mar 2012 14:20:16 +0100
branchstable
changeset 8301 1385e05afe52
parent 7990 a673d1d9a738
child 8183 7d658ea27ec5
permissions -rw-r--r--
[uilib] don't print timedeltas between 0 and 23h as negative (closes #2236352)

# 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
"""
from __future__ import with_statement

__docformat__ = "restructuredtext en"
_ = unicode

from warnings import warn

from logilab.mtconverter import xml_escape
from logilab.common.deprecation import class_deprecated

from cubicweb import Unauthorized
from cubicweb.selectors 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): # XXX rename to ActionsBox
    """
    box with all actions impacting the entity displayed: edit, copy, delete
    change state, add related entities...
    """
    __regid__ = 'edit_box'
    __select__ = component.CtxComponent.__select__ & non_final_entity()

    title = _('actions')
    order = 2
    contextual = True

    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, 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:
            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="%s">
<table id="tsearch"><tr><td>
<input id="norql" type="text" accesskey="q" tabindex="%s" title="search text" value="%s" name="rql" />
<input type="hidden" name="__fromsearchbox" value="1" />
<input type="hidden" name="subvid" value="tsearch" />
</td><td>
<input tabindex="%s" type="submit" id="rqlboxsubmit" 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 = ''
        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(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 + unicode(abs(id(self))) + unicode(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 ##############################################################

class SideBoxView(EntityView):
    """helper view class to display some entities in a sidebox"""
    __metaclass__ = class_deprecated
    __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'