web/views/boxes.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """Generic boxes for CubicWeb web client:
       
    19 
       
    20 * actions box
       
    21 * search box
       
    22 
       
    23 Additional boxes (disabled by default):
       
    24 * schema box
       
    25 * possible views box
       
    26 * startup views box
       
    27 """
       
    28 __docformat__ = "restructuredtext en"
       
    29 from cubicweb import _
       
    30 
       
    31 from warnings import warn
       
    32 
       
    33 from six import text_type, add_metaclass
       
    34 
       
    35 from logilab.mtconverter import xml_escape
       
    36 from logilab.common.deprecation import class_deprecated
       
    37 
       
    38 from cubicweb import Unauthorized
       
    39 from cubicweb.predicates import (match_user_groups, match_kwargs,
       
    40                                 non_final_entity, nonempty_rset,
       
    41                                 match_context, contextual)
       
    42 from cubicweb.utils import wrap_on_write
       
    43 from cubicweb.view import EntityView
       
    44 from cubicweb.schema import display_name
       
    45 from cubicweb.web import component, box, htmlwidgets
       
    46 
       
    47 # XXX bw compat, some cubes import this class from here
       
    48 BoxTemplate = box.BoxTemplate
       
    49 BoxHtml = htmlwidgets.BoxHtml
       
    50 
       
    51 class EditBox(component.CtxComponent):
       
    52     """
       
    53     box with all actions impacting the entity displayed: edit, copy, delete
       
    54     change state, add related entities...
       
    55     """
       
    56     __regid__ = 'edit_box'
       
    57 
       
    58     title = _('actions')
       
    59     order = 2
       
    60     contextual = True
       
    61     __select__ = component.CtxComponent.__select__ & non_final_entity()
       
    62 
       
    63     def init_rendering(self):
       
    64         super(EditBox, self).init_rendering()
       
    65         _ = self._cw._
       
    66         self._menus_in_order = []
       
    67         self._menus_by_id = {}
       
    68         # build list of actions
       
    69         actions = self._cw.vreg['actions'].possible_actions(self._cw, self.cw_rset,
       
    70                                                             **self.cw_extra_kwargs)
       
    71         other_menu = self._get_menu('moreactions', _('more actions'))
       
    72         for category, defaultmenu in (('mainactions', self),
       
    73                                       ('moreactions', other_menu),
       
    74                                       ('addrelated', None)):
       
    75             for action in actions.get(category, ()):
       
    76                 if action.submenu:
       
    77                     menu = self._get_menu(action.submenu)
       
    78                 else:
       
    79                     menu = defaultmenu
       
    80                 action.fill_menu(self, menu)
       
    81         # if we've nothing but actions in the other_menu, add them directly into the box
       
    82         if not self.items and len(self._menus_by_id) == 1 and not other_menu.is_empty():
       
    83             self.items = other_menu.items
       
    84         else: # ensure 'more actions' menu appears last
       
    85             self._menus_in_order.remove(other_menu)
       
    86             self._menus_in_order.append(other_menu)
       
    87             for submenu in self._menus_in_order:
       
    88                 self.add_submenu(self, submenu)
       
    89         if not self.items:
       
    90             raise component.EmptyComponent()
       
    91 
       
    92     def render_title(self, w):
       
    93         title = self._cw._(self.title)
       
    94         if self.cw_rset:
       
    95             etypes = self.cw_rset.column_types(0)
       
    96             if len(etypes) == 1:
       
    97                 plural = self.cw_rset.rowcount > 1 and 'plural' or ''
       
    98                 etypelabel = display_name(self._cw, next(iter(etypes)), plural)
       
    99                 title = u'%s - %s' % (title, etypelabel.lower())
       
   100         w(title)
       
   101 
       
   102     def render_body(self, w):
       
   103         self.render_items(w)
       
   104 
       
   105     def _get_menu(self, id, title=None, label_prefix=None):
       
   106         try:
       
   107             return self._menus_by_id[id]
       
   108         except KeyError:
       
   109             if title is None:
       
   110                 title = self._cw._(id)
       
   111             self._menus_by_id[id] = menu = htmlwidgets.BoxMenu(title)
       
   112             menu.label_prefix = label_prefix
       
   113             self._menus_in_order.append(menu)
       
   114             return menu
       
   115 
       
   116     def add_submenu(self, box, submenu, label_prefix=None):
       
   117         appendanyway = getattr(submenu, 'append_anyway', False)
       
   118         if len(submenu.items) == 1 and not appendanyway:
       
   119             boxlink = submenu.items[0]
       
   120             if submenu.label_prefix:
       
   121                 # XXX iirk
       
   122                 if hasattr(boxlink, 'label'):
       
   123                     boxlink.label = u'%s %s' % (submenu.label_prefix, boxlink.label)
       
   124                 else:
       
   125                     boxlink = u'%s %s' % (submenu.label_prefix, boxlink)
       
   126             box.append(boxlink)
       
   127         elif submenu.items:
       
   128             box.append(submenu)
       
   129         elif appendanyway:
       
   130             box.append(xml_escape(submenu.label))
       
   131 
       
   132 
       
   133 class SearchBox(component.CtxComponent):
       
   134     """display a box with a simple search form"""
       
   135     __regid__ = 'search_box'
       
   136 
       
   137     title = _('search')
       
   138     order = 0
       
   139     formdef = u"""<form action="%(action)s">
       
   140 <table id="%(id)s"><tr><td>
       
   141 <input class="norql" type="text" accesskey="q" tabindex="%(tabindex1)s" title="search text" value="%(value)s" name="rql" />
       
   142 <input type="hidden" name="__fromsearchbox" value="1" />
       
   143 <input type="hidden" name="subvid" value="tsearch" />
       
   144 </td><td>
       
   145 <input tabindex="%(tabindex2)s" type="submit" class="rqlsubmit" value="" />
       
   146  </td></tr></table>
       
   147  </form>"""
       
   148 
       
   149     def render_title(self, w):
       
   150         w(u"""<span onclick="javascript: toggleVisibility('rqlinput')">%s</span>"""
       
   151           % self._cw._(self.title))
       
   152 
       
   153     def render_body(self, w):
       
   154         if self._cw.form.pop('__fromsearchbox', None):
       
   155             rql = self._cw.form.get('rql', '')
       
   156         else:
       
   157             rql = ''
       
   158         tabidx1 = self._cw.next_tabindex()
       
   159         tabidx2 = self._cw.next_tabindex()
       
   160         w(self.formdef % {'action': self._cw.build_url('view'),
       
   161                           'value': xml_escape(rql),
       
   162                           'id': self.cw_extra_kwargs.get('domid', 'tsearch'),
       
   163                           'tabindex1': tabidx1,
       
   164                           'tabindex2': tabidx2})
       
   165 
       
   166 
       
   167 # boxes disabled by default ###################################################
       
   168 
       
   169 class PossibleViewsBox(component.CtxComponent):
       
   170     """display a box containing links to all possible views"""
       
   171     __regid__ = 'possible_views_box'
       
   172 
       
   173     contextual = True
       
   174     title = _('possible views')
       
   175     order = 10
       
   176     visible = False # disabled by default
       
   177 
       
   178     def init_rendering(self):
       
   179         self.views = [v for v in self._cw.vreg['views'].possible_views(self._cw,
       
   180                                                                        rset=self.cw_rset)
       
   181                       if v.category != 'startupview']
       
   182         if not self.views:
       
   183             raise component.EmptyComponent()
       
   184         self.items = []
       
   185 
       
   186     def render_body(self, w):
       
   187         for category, views in box.sort_by_category(self.views):
       
   188             menu = htmlwidgets.BoxMenu(self._cw._(category), ident=category)
       
   189             for view in views:
       
   190                 menu.append(self.action_link(view))
       
   191             self.append(menu)
       
   192         self.render_items(w)
       
   193 
       
   194 
       
   195 class StartupViewsBox(PossibleViewsBox):
       
   196     """display a box containing links to all startup views"""
       
   197     __regid__ = 'startup_views_box'
       
   198 
       
   199     contextual = False
       
   200     title = _('startup views')
       
   201     order = 70
       
   202     visible = False # disabled by default
       
   203 
       
   204     def init_rendering(self):
       
   205         self.views = [v for v in self._cw.vreg['views'].possible_views(self._cw)
       
   206                       if v.category == 'startupview']
       
   207         if not self.views:
       
   208             raise component.EmptyComponent()
       
   209         self.items = []
       
   210 
       
   211 
       
   212 class RsetBox(component.CtxComponent):
       
   213     """helper view class to display an rset in a sidebox"""
       
   214     __select__ = nonempty_rset() & match_kwargs('title', 'vid')
       
   215     __regid__ = 'rsetbox'
       
   216     cw_property_defs = {}
       
   217     context = 'incontext'
       
   218 
       
   219     @property
       
   220     def domid(self):
       
   221         return super(RsetBox, self).domid + text_type(abs(id(self))) + text_type(abs(id(self.cw_rset)))
       
   222 
       
   223     def render_title(self, w):
       
   224         w(self.cw_extra_kwargs['title'])
       
   225 
       
   226     def render_body(self, w):
       
   227         if 'dispctrl' in self.cw_extra_kwargs:
       
   228             # XXX do not modify dispctrl!
       
   229             self.cw_extra_kwargs['dispctrl'].setdefault('subvid', 'outofcontext')
       
   230             self.cw_extra_kwargs['dispctrl'].setdefault('use_list_limit', 1)
       
   231         self._cw.view(self.cw_extra_kwargs['vid'], self.cw_rset, w=w,
       
   232                       initargs=self.cw_extra_kwargs)
       
   233 
       
   234  # helper classes ##############################################################
       
   235 
       
   236 @add_metaclass(class_deprecated)
       
   237 class SideBoxView(EntityView):
       
   238     """helper view class to display some entities in a sidebox"""
       
   239     __deprecation_warning__ = '[3.10] SideBoxView is deprecated, use RsetBox instead (%(cls)s)'
       
   240 
       
   241     __regid__ = 'sidebox'
       
   242 
       
   243     def call(self, title=u'', **kwargs):
       
   244         """display a list of entities by calling their <item_vid> view"""
       
   245         if 'dispctrl' in self.cw_extra_kwargs:
       
   246             # XXX do not modify dispctrl!
       
   247             self.cw_extra_kwargs['dispctrl'].setdefault('subvid', 'outofcontext')
       
   248             self.cw_extra_kwargs['dispctrl'].setdefault('use_list_limit', 1)
       
   249         if title:
       
   250             self.cw_extra_kwargs['title'] = title
       
   251         self.cw_extra_kwargs.setdefault('context', 'incontext')
       
   252         box = self._cw.vreg['ctxcomponents'].select(
       
   253             'rsetbox', self._cw, rset=self.cw_rset, vid='autolimited',
       
   254             **self.cw_extra_kwargs)
       
   255         box.render(self.w)
       
   256 
       
   257 
       
   258 class ContextualBoxLayout(component.Layout):
       
   259     __select__ = match_context('incontext', 'left', 'right') & contextual()
       
   260     # predefined class in cubicweb.css: contextualBox | contextFreeBox
       
   261     cssclass = 'contextualBox'
       
   262 
       
   263     def render(self, w):
       
   264         if self.init_rendering():
       
   265             view = self.cw_extra_kwargs['view']
       
   266             w(u'<div class="%s %s" id="%s">' % (self.cssclass, view.cssclass,
       
   267                                                 view.domid))
       
   268             with wrap_on_write(w, u'<div class="boxTitle"><span>',
       
   269                                u'</span></div>') as wow:
       
   270                 view.render_title(wow)
       
   271             w(u'<div class="boxBody">')
       
   272             view.render_body(w)
       
   273             # boxFooter div is a CSS place holder (for shadow for example)
       
   274             w(u'</div><div class="boxFooter"></div></div>\n')
       
   275 
       
   276 
       
   277 class ContextFreeBoxLayout(ContextualBoxLayout):
       
   278     __select__ = match_context('incontext', 'left', 'right') & ~contextual()
       
   279     cssclass = 'contextFreeBox'