web/views/boxes.py
changeset 6140 65a619eb31c4
parent 5839 53cbdc5e0426
child 6141 b8287e54b528
equal deleted inserted replaced
6139:f76599a96238 6140:65a619eb31c4
    16 # You should have received a copy of the GNU Lesser General Public License along
    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/>.
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    18 """Generic boxes for CubicWeb web client:
    18 """Generic boxes for CubicWeb web client:
    19 
    19 
    20 * actions box
    20 * actions box
       
    21 * search box
       
    22 
       
    23 Additional boxes (disabled by default):
       
    24 * schema box
    21 * possible views box
    25 * possible views box
    22 
       
    23 additional (disabled by default) boxes
       
    24 * schema box
       
    25 * startup views box
    26 * startup views box
    26 """
    27 """
    27 
    28 
    28 __docformat__ = "restructuredtext en"
    29 __docformat__ = "restructuredtext en"
    29 _ = unicode
    30 _ = unicode
    30 
    31 
    31 from warnings import warn
    32 from warnings import warn
    32 
    33 
    33 from logilab.mtconverter import xml_escape
    34 from logilab.mtconverter import xml_escape
    34 
    35 from logilab.common.deprecation import class_deprecated
    35 from cubicweb.selectors import match_user_groups, non_final_entity
    36 
       
    37 from cubicweb import Unauthorized
       
    38 from cubicweb.selectors import (match_user_groups, match_context, match_kwargs,
       
    39                                 non_final_entity, nonempty_rset)
    36 from cubicweb.view import EntityView
    40 from cubicweb.view import EntityView
    37 from cubicweb.schema import display_name
    41 from cubicweb.schema import display_name
    38 from cubicweb.web.htmlwidgets import BoxWidget, BoxMenu, BoxHtml, RawBoxItem
    42 from cubicweb.web import box, htmlwidgets
    39 from cubicweb.web.box import BoxTemplate
    43 
    40 
    44 # XXX bw compat, some cubes import this class from here
    41 
    45 BoxTemplate = box.BoxTemplate
    42 class EditBox(BoxTemplate): # XXX rename to ActionsBox
    46 BoxHtml = htmlwidgets.BoxHtml
       
    47 
       
    48 class EditBox(box.Box): # XXX rename to ActionsBox
    43     """
    49     """
    44     box with all actions impacting the entity displayed: edit, copy, delete
    50     box with all actions impacting the entity displayed: edit, copy, delete
    45     change state, add related entities
    51     change state, add related entities
    46     """
    52     """
    47     __regid__ = 'edit_box'
    53     __regid__ = 'edit_box'
    48     __select__ = BoxTemplate.__select__ & non_final_entity()
    54     __select__ = box.Box.__select__ & non_final_entity()
    49 
    55 
    50     title = _('actions')
    56     title = _('actions')
    51     order = 2
    57     order = 2
    52 
    58     contextual = True
    53     def call(self, view=None, **kwargs):
    59 
       
    60     def init_rendering(self):
       
    61         super(EditBox, self).init_rendering()
    54         _ = self._cw._
    62         _ = self._cw._
    55         title = _(self.title)
       
    56         if self.cw_rset:
       
    57             etypes = self.cw_rset.column_types(0)
       
    58             if len(etypes) == 1:
       
    59                 plural = self.cw_rset.rowcount > 1 and 'plural' or ''
       
    60                 etypelabel = display_name(self._cw, iter(etypes).next(), plural)
       
    61                 title = u'%s - %s' % (title, etypelabel.lower())
       
    62         box = BoxWidget(title, self.__regid__, _class="greyBoxFrame")
       
    63         self._menus_in_order = []
    63         self._menus_in_order = []
    64         self._menus_by_id = {}
    64         self._menus_by_id = {}
    65         # build list of actions
    65         # build list of actions
    66         actions = self._cw.vreg['actions'].possible_actions(self._cw, self.cw_rset,
    66         actions = self._cw.vreg['actions'].possible_actions(self._cw, self.cw_rset,
    67                                                             view=view)
    67                                                             **self.cw_extra_kwargs)
    68         other_menu = self._get_menu('moreactions', _('more actions'))
    68         other_menu = self._get_menu('moreactions', _('more actions'))
    69         for category, defaultmenu in (('mainactions', box),
    69         for category, defaultmenu in (('mainactions', self),
    70                                       ('moreactions', other_menu),
    70                                       ('moreactions', other_menu),
    71                                       ('addrelated', None)):
    71                                       ('addrelated', None)):
    72             for action in actions.get(category, ()):
    72             for action in actions.get(category, ()):
    73                 if category == 'addrelated':
    73                 if category == 'addrelated':
    74                     warn('[3.5] "addrelated" category is deprecated, use '
    74                     warn('[3.5] "addrelated" category is deprecated, use '
    79                     menu = self._get_menu(action.submenu)
    79                     menu = self._get_menu(action.submenu)
    80                 else:
    80                 else:
    81                     menu = defaultmenu
    81                     menu = defaultmenu
    82                 action.fill_menu(self, menu)
    82                 action.fill_menu(self, menu)
    83         # if we've nothing but actions in the other_menu, add them directly into the box
    83         # if we've nothing but actions in the other_menu, add them directly into the box
    84         if box.is_empty() and len(self._menus_by_id) == 1 and not other_menu.is_empty():
    84         if not self.items and len(self._menus_by_id) == 1 and not other_menu.is_empty():
    85             box.items = other_menu.items
    85             self.items = other_menu.items
    86             other_menu.items = []
       
    87         else: # ensure 'more actions' menu appears last
    86         else: # ensure 'more actions' menu appears last
    88             self._menus_in_order.remove(other_menu)
    87             self._menus_in_order.remove(other_menu)
    89             self._menus_in_order.append(other_menu)
    88             self._menus_in_order.append(other_menu)
    90         for submenu in self._menus_in_order:
    89             for submenu in self._menus_in_order:
    91             self.add_submenu(box, submenu)
    90                 self.add_submenu(self, submenu)
    92         if not box.is_empty():
    91         if not self.items:
    93             box.render(self.w)
    92             raise box.EmptyComponent()
       
    93 
       
    94     def render_title(self, w):
       
    95         title = self._cw._(self.title)
       
    96         if self.cw_rset:
       
    97             etypes = self.cw_rset.column_types(0)
       
    98             if len(etypes) == 1:
       
    99                 plural = self.cw_rset.rowcount > 1 and 'plural' or ''
       
   100                 etypelabel = display_name(self._cw, iter(etypes).next(), plural)
       
   101                 title = u'%s - %s' % (title, etypelabel.lower())
       
   102         w(title)
       
   103 
       
   104     def render_body(self, w):
       
   105         self.render_items(w)
    94 
   106 
    95     def _get_menu(self, id, title=None, label_prefix=None):
   107     def _get_menu(self, id, title=None, label_prefix=None):
    96         try:
   108         try:
    97             return self._menus_by_id[id]
   109             return self._menus_by_id[id]
    98         except KeyError:
   110         except KeyError:
    99             if title is None:
   111             if title is None:
   100                 title = self._cw._(id)
   112                 title = self._cw._(id)
   101             self._menus_by_id[id] = menu = BoxMenu(title)
   113             self._menus_by_id[id] = menu = htmlwidgets.BoxMenu(title)
   102             menu.label_prefix = label_prefix
   114             menu.label_prefix = label_prefix
   103             self._menus_in_order.append(menu)
   115             self._menus_in_order.append(menu)
   104             return menu
   116             return menu
   105 
   117 
   106     def add_submenu(self, box, submenu, label_prefix=None):
   118     def add_submenu(self, box, submenu, label_prefix=None):
   107         appendanyway = getattr(submenu, 'append_anyway', False)
   119         appendanyway = getattr(submenu, 'append_anyway', False)
   108         if len(submenu.items) == 1 and not appendanyway:
   120         if len(submenu.items) == 1 and not appendanyway:
   109             boxlink = submenu.items[0]
   121             boxlink = submenu.items[0]
   110             if submenu.label_prefix:
   122             if submenu.label_prefix:
   111                 boxlink.label = u'%s %s' % (submenu.label_prefix, boxlink.label)
   123                 # XXX iirk
       
   124                 if hasattr(boxlink, 'label'):
       
   125                     boxlink.label = u'%s %s' % (submenu.label_prefix, boxlink.label)
       
   126                 else:
       
   127                     submenu.items[0] = u'%s %s' % (submenu.label_prefix, boxlink)
   112             box.append(boxlink)
   128             box.append(boxlink)
   113         elif submenu.items:
   129         elif submenu.items:
   114             box.append(submenu)
   130             box.append(submenu)
   115         elif appendanyway:
   131         elif appendanyway:
   116             box.append(RawBoxItem(xml_escape(submenu.label)))
   132             box.append(xml_escape(submenu.label))
   117 
   133 
   118 
   134 
   119 class SearchBox(BoxTemplate):
   135 class SearchBox(box.Box):
   120     """display a box with a simple search form"""
   136     """display a box with a simple search form"""
   121     __regid__ = 'search_box'
   137     __regid__ = 'search_box'
   122 
   138 
   123     visible = True # enabled by default
       
   124     title = _('search')
   139     title = _('search')
   125     order = 0
   140     order = 0
   126     formdef = u"""<form action="%s">
   141     formdef = u"""<form action="%s">
   127 <table id="tsearch"><tr><td>
   142 <table id="tsearch"><tr><td>
   128 <input id="norql" type="text" accesskey="q" tabindex="%s" title="search text" value="%s" name="rql" />
   143 <input id="norql" type="text" accesskey="q" tabindex="%s" title="search text" value="%s" name="rql" />
   129 <input type="hidden" name="__fromsearchbox" value="1" />
   144 <input type="hidden" name="__fromsearchbox" value="1" />
   130 <input type="hidden" name="subvid" value="tsearch" />
   145 <input type="hidden" name="subvid" value="tsearch" />
   131 </td><td>
   146 </td><td>
   132 <input tabindex="%s" type="submit" id="rqlboxsubmit" class="rqlsubmit" value="" />
   147 <input tabindex="%s" type="submit" id="rqlboxsubmit" class="rqlsubmit" value="" />
   133 </td></tr></table>
   148  </td></tr></table>
   134 </form>"""
   149  </form>"""
   135 
   150 
   136     def call(self, view=None, **kwargs):
   151     def render_title(self, w):
   137         req = self._cw
   152         w(u"""<span onclick="javascript: toggleVisibility('rqlinput')">%s</span>"""
   138         if req.form.pop('__fromsearchbox', None):
   153           % self._cw._(self.title))
   139             rql = req.form.get('rql', '')
   154 
       
   155     def render_body(self, w):
       
   156         if self._cw.form.pop('__fromsearchbox', None):
       
   157             rql = self._cw.form.get('rql', '')
   140         else:
   158         else:
   141             rql = ''
   159             rql = ''
   142         form = self.formdef % (req.build_url('view'), req.next_tabindex(),
   160         w(self.formdef % (self._cw.build_url('view'), self._cw.next_tabindex(),
   143                                xml_escape(rql), req.next_tabindex())
   161                           xml_escape(rql), self._cw.next_tabindex()))
   144         title = u"""<span onclick="javascript: toggleVisibility('rqlinput')">%s</span>""" % req._(self.title)
       
   145         box = BoxWidget(title, self.__regid__, _class="searchBoxFrame", islist=False, escape=False)
       
   146         box.append(BoxHtml(form))
       
   147         box.render(self.w)
       
   148 
   162 
   149 
   163 
   150 # boxes disabled by default ###################################################
   164 # boxes disabled by default ###################################################
   151 
   165 
   152 class PossibleViewsBox(BoxTemplate):
   166 class PossibleViewsBox(box.Box):
   153     """display a box containing links to all possible views"""
   167     """display a box containing links to all possible views"""
   154     __regid__ = 'possible_views_box'
   168     __regid__ = 'possible_views_box'
   155     __select__ = BoxTemplate.__select__ & match_user_groups('users', 'managers')
       
   156 
   169 
   157     visible = False
   170     visible = False
   158     title = _('possible views')
   171     title = _('possible views')
   159     order = 10
   172     order = 10
   160 
   173 
   161     def call(self, **kwargs):
   174     def init_rendering(self):
   162         box = BoxWidget(self._cw._(self.title), self.__regid__)
   175         self.views = [v for v in self._cw.vreg['views'].possible_views(self._cw,
   163         views = [v for v in self._cw.vreg['views'].possible_views(self._cw,
   176                                                                        rset=self.cw_rset)
   164                                                               rset=self.cw_rset)
   177                       if v.category != 'startupview']
   165                  if v.category != 'startupview']
   178         if not self.views:
   166         for category, views in self.sort_actions(views):
   179             raise box.EmptyComponent()
   167             menu = BoxMenu(category)
   180         self.items = []
       
   181 
       
   182     def render_body(self, w):
       
   183         for category, views in box.sort_by_category(self.views):
       
   184             menu = htmlwidgets.BoxMenu(category)
   168             for view in views:
   185             for view in views:
   169                 menu.append(self.box_action(view))
   186                 menu.append(self.box_action(view))
   170             box.append(menu)
   187             self.append(menu)
   171         if not box.is_empty():
   188         self.render_items(w)
   172             box.render(self.w)
   189 
   173 
   190 
   174 
   191 class StartupViewsBox(PossibleViewsBox):
   175 class StartupViewsBox(BoxTemplate):
       
   176     """display a box containing links to all startup views"""
   192     """display a box containing links to all startup views"""
   177     __regid__ = 'startup_views_box'
   193     __regid__ = 'startup_views_box'
       
   194 
   178     visible = False # disabled by default
   195     visible = False # disabled by default
   179     title = _('startup views')
   196     title = _('startup views')
   180     order = 70
   197     order = 70
   181 
   198 
   182     def call(self, **kwargs):
   199     def init_rendering(self):
   183         box = BoxWidget(self._cw._(self.title), self.__regid__)
   200         self.views = [v for v in self._cw.vreg['views'].possible_views(self._cw)
   184         for view in self._cw.vreg['views'].possible_views(self._cw, None):
   201                       if v.category == 'startupview']
   185             if view.category == 'startupview':
   202         if not self.views:
   186                 box.append(self.box_action(view))
   203             raise box.EmptyComponent()
   187         if not box.is_empty():
   204         self.items = []
   188             box.render(self.w)
   205 
   189 
   206 
   190 
   207 class RsetBox(box.Box):
   191 # helper classes ##############################################################
   208     """helper view class to display an rset in a sidebox"""
       
   209     __select__ = nonempty_rset() & match_kwargs('title', 'vid')
       
   210     __regid__ = 'rsetbox'
       
   211     cw_property_defs = {}
       
   212     context = 'incontext'
       
   213 
       
   214     @property
       
   215     def domid(self):
       
   216         return super(RsetBox, self).domid + unicode(abs(id(self)))
       
   217     def render_title(self, w):
       
   218         w(self.cw_extra_kwargs['title'])
       
   219 
       
   220     def render_body(self, w):
       
   221         self._cw.view(self.cw_extra_kwargs['vid'], self.cw_rset, w=w)
       
   222 
       
   223  # helper classes ##############################################################
   192 
   224 
   193 class SideBoxView(EntityView):
   225 class SideBoxView(EntityView):
   194     """helper view class to display some entities in a sidebox"""
   226     """helper view class to display some entities in a sidebox"""
       
   227     __metaclass__ = class_deprecated
       
   228     __deprecation_warning__ = 'SideBoxView is deprecated, use RsetBox instead'
       
   229 
   195     __regid__ = 'sidebox'
   230     __regid__ = 'sidebox'
   196 
   231 
   197     def call(self, boxclass='sideBox', title=u''):
   232     def call(self, **kwargs):
   198         """display a list of entities by calling their <item_vid> view"""
   233         """display a list of entities by calling their <item_vid> view"""
   199         if title:
   234         box = self._cw.vreg['boxes'].select('rsetbox', self._cw, rset=self.cw_rset,
   200             self.w(u'<div class="sideBoxTitle"><span>%s</span></div>' % title)
   235                                             vid='autolimited', title=title,
   201         self.w(u'<div class="%s"><div class="sideBoxBody">' % boxclass)
   236                                             **self.cw_extra_kwargs)
   202         self.wview('autolimited', self.cw_rset, **self.cw_extra_kwargs)
   237         box.render(self.w)
   203         self.w(u'</div>\n</div>\n')
   238 
       
   239 
       
   240 class ContextualBoxLayout(box.Layout):
       
   241     __select__ = match_context('incontext', 'left', 'right') & box.contextual()
       
   242     # predefined class in cubicweb.css: contextualBox | contextFreeBox
       
   243     # XXX: navigationBox | actionBox
       
   244     cssclass = 'contextualBox'
       
   245 
       
   246     def render(self, w):
       
   247         view = self.cw_extra_kwargs['view']
       
   248         try:
       
   249             view.init_rendering()
       
   250         except Unauthorized, ex:
       
   251             self.warning("can't render %s: %s", view, ex)
       
   252             return
       
   253         except box.EmptyComponent:
       
   254             return
       
   255         w(u'<div class="%s %s" id="%s">' % (self.cssclass, view.cssclass,
       
   256                                             view.domid))
       
   257         w(u'<div class="boxTitle"><span>')
       
   258         view.render_title(w)
       
   259         w(u'</span></div>\n<div class="boxBody">')
       
   260         view.render_body(w)
       
   261         # boxFooter div is a CSS place holder (for shadow for example)
       
   262         w(u'</div><div class="boxFooter"></div></div>\n')
       
   263 
       
   264 
       
   265 class ContextFreeBoxLayout(ContextualBoxLayout):
       
   266     __select__ = match_context('incontext', 'left', 'right') & ~box.contextual()
       
   267     cssclass = 'contextFreeBox'