web/views/tableview.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 # copyright 2003-2012 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 """This module contains table views, with the following features that may be
       
    19 provided (depending on the used implementation):
       
    20 
       
    21 * facets filtering
       
    22 * pagination
       
    23 * actions menu
       
    24 * properly sortable content
       
    25 * odd/row/hover line styles
       
    26 
       
    27 The three main implementation are described below. Each implementation is
       
    28 suitable for a particular case, but they each attempt to display tables that
       
    29 looks similar.
       
    30 
       
    31 .. autoclass:: cubicweb.web.views.tableview.RsetTableView
       
    32    :members:
       
    33 
       
    34 .. autoclass:: cubicweb.web.views.tableview.EntityTableView
       
    35    :members:
       
    36 
       
    37 .. autoclass:: cubicweb.web.views.pyviews.PyValTableView
       
    38    :members:
       
    39 
       
    40 All those classes are rendered using a *layout*:
       
    41 
       
    42 .. autoclass:: cubicweb.web.views.tableview.TableLayout
       
    43    :members:
       
    44 
       
    45 There is by default only one table layout, using the 'table_layout' identifier,
       
    46 that is referenced by table views
       
    47 :attr:`cubicweb.web.views.tableview.TableMixIn.layout_id`.  If you want to
       
    48 customize the look and feel of your table, you can either replace the default
       
    49 one by yours, having multiple variants with proper selectors, or change the
       
    50 `layout_id` identifier of your table to use your table specific implementation.
       
    51 
       
    52 Notice you can gives options to the layout using a `layout_args` dictionary on
       
    53 your class.
       
    54 
       
    55 If you still can't find a view that suit your needs, you should take a look at the
       
    56 class below that is the common abstract base class for the three views defined
       
    57 above and implement your own class.
       
    58 
       
    59 .. autoclass:: cubicweb.web.views.tableview.TableMixIn
       
    60    :members:
       
    61 """
       
    62 
       
    63 __docformat__ = "restructuredtext en"
       
    64 from cubicweb import _
       
    65 
       
    66 from warnings import warn
       
    67 from copy import copy
       
    68 from types import MethodType
       
    69 
       
    70 from six import string_types, add_metaclass, create_bound_method
       
    71 from six.moves import range
       
    72 
       
    73 from logilab.mtconverter import xml_escape
       
    74 from logilab.common.decorators import cachedproperty
       
    75 from logilab.common.deprecation import class_deprecated
       
    76 from logilab.common.registry import yes
       
    77 
       
    78 from cubicweb import NoSelectableObject, tags
       
    79 from cubicweb.predicates import nonempty_rset, match_kwargs, objectify_predicate
       
    80 from cubicweb.schema import display_name
       
    81 from cubicweb.utils import make_uid, js_dumps, JSString, UStringIO
       
    82 from cubicweb.uilib import toggle_action, limitsize, htmlescape, sgml_attributes, domid
       
    83 from cubicweb.view import EntityView, AnyRsetView
       
    84 from cubicweb.web import jsonize, component
       
    85 from cubicweb.web.htmlwidgets import (TableWidget, TableColumn, MenuWidget,
       
    86                                       PopupBoxMenu)
       
    87 
       
    88 
       
    89 @objectify_predicate
       
    90 def unreloadable_table(cls, req, rset=None,
       
    91                        displaycols=None, headers=None, cellvids=None,
       
    92                        paginate=False, displayactions=False, displayfilter=False,
       
    93                        **kwargs):
       
    94     # one may wish to specify one of headers/displaycols/cellvids as long as he
       
    95     # doesn't want pagination nor actions nor facets
       
    96     if not kwargs and (displaycols or headers or cellvids) and not (
       
    97         displayfilter or displayactions or paginate):
       
    98         return 1
       
    99     return 0
       
   100 
       
   101 
       
   102 class TableLayout(component.Component):
       
   103     """The default layout for table. When `render` is called, this will use
       
   104     the API described on :class:`TableMixIn` to feed the generated table.
       
   105 
       
   106     This layout behaviour may be customized using the following attributes /
       
   107     selection arguments:
       
   108 
       
   109     * `cssclass`, a string that should be used as HTML class attribute. Default
       
   110       to "listing".
       
   111 
       
   112     * `needs_css`, the CSS files that should be used together with this
       
   113       table. Default to ('cubicweb.tablesorter.css', 'cubicweb.tableview.css').
       
   114 
       
   115     * `needs_js`, the Javascript files that should be used together with this
       
   116       table. Default to ('jquery.tablesorter.js',)
       
   117 
       
   118     * `display_filter`, tells if the facets filter should be displayed when
       
   119       possible. Allowed values are:
       
   120       - `None`, don't display it
       
   121       - 'top', display it above the table
       
   122       - 'bottom', display it below the table
       
   123 
       
   124     * `display_actions`, tells if a menu for available actions should be
       
   125       displayed when possible (see two following options). Allowed values are:
       
   126       - `None`, don't display it
       
   127       - 'top', display it above the table
       
   128       - 'bottom', display it below the table
       
   129 
       
   130     * `hide_filter`, when true (the default), facets filter will be hidden by
       
   131       default, with an action in the actions menu allowing to show / hide it.
       
   132 
       
   133     * `show_all_option`, when true, a *show all results* link will be displayed
       
   134       below the navigation component.
       
   135 
       
   136     * `add_view_actions`, when true, actions returned by view.table_actions()
       
   137       will be included in the actions menu.
       
   138 
       
   139     * `header_column_idx`, if not `None`, should be a colum index or a set of
       
   140       column index where <th> tags should be generated instead of <td>
       
   141     """ #'# make emacs happier
       
   142     __regid__ = 'table_layout'
       
   143     cssclass = "listing"
       
   144     needs_css = ('cubicweb.tableview.css',)
       
   145     needs_js = ()
       
   146     display_filter = None    # None / 'top' / 'bottom'
       
   147     display_actions = 'top'  # None / 'top' / 'bottom'
       
   148     hide_filter = True
       
   149     show_all_option = True   # make navcomp generate a 'show all' results link
       
   150     add_view_actions = False
       
   151     header_column_idx = None
       
   152     enable_sorting = True
       
   153     sortvalue_limit = 10
       
   154     tablesorter_settings = {
       
   155         'textExtraction': JSString('cw.sortValueExtraction'),
       
   156         'selectorHeaders': "thead tr:first th[class='sortable']", # only plug on the first row
       
   157         }
       
   158 
       
   159     def _setup_tablesorter(self, divid):
       
   160         self._cw.add_css('cubicweb.tablesorter.css')
       
   161         self._cw.add_js('jquery.tablesorter.js')
       
   162         self._cw.add_onload('''$(document).ready(function() {
       
   163     $("#%s table").tablesorter(%s);
       
   164 });''' % (divid, js_dumps(self.tablesorter_settings)))
       
   165 
       
   166     def __init__(self, req, view, **kwargs):
       
   167         super(TableLayout, self).__init__(req, **kwargs)
       
   168         for key, val in list(self.cw_extra_kwargs.items()):
       
   169             if hasattr(self.__class__, key) and not key[0] == '_':
       
   170                 setattr(self, key, val)
       
   171                 self.cw_extra_kwargs.pop(key)
       
   172         self.view = view
       
   173         if self.header_column_idx is None:
       
   174             self.header_column_idx = frozenset()
       
   175         elif isinstance(self.header_column_idx, int):
       
   176             self.header_column_idx = frozenset( (self.header_column_idx,) )
       
   177 
       
   178     @cachedproperty
       
   179     def initial_load(self):
       
   180         """We detect a bit heuristically if we are built for the first time or
       
   181         from subsequent calls by the form filter or by the pagination hooks.
       
   182         """
       
   183         form = self._cw.form
       
   184         return 'fromformfilter' not in form and '__fromnavigation' not in form
       
   185 
       
   186     def render(self, w, **kwargs):
       
   187         assert self.display_filter in (None, 'top', 'bottom'), self.display_filter
       
   188         if self.needs_css:
       
   189             self._cw.add_css(self.needs_css)
       
   190         if self.needs_js:
       
   191             self._cw.add_js(self.needs_js)
       
   192         if self.enable_sorting:
       
   193             self._setup_tablesorter(self.view.domid)
       
   194         # Notice facets form must be rendered **outside** the main div as it
       
   195         # shouldn't be rendered on ajax call subsequent to facet restriction
       
   196         # (hence the 'fromformfilter' parameter added by the form
       
   197         generate_form = self.initial_load
       
   198         if self.display_filter and generate_form:
       
   199             facetsform = self.view.facets_form()
       
   200         else:
       
   201             facetsform = None
       
   202         if facetsform and self.display_filter == 'top':
       
   203             cssclass = u'hidden' if self.hide_filter else u''
       
   204             facetsform.render(w, vid=self.view.__regid__, cssclass=cssclass,
       
   205                               divid=self.view.domid)
       
   206         actions = []
       
   207         if self.display_actions:
       
   208             if self.add_view_actions:
       
   209                 actions = self.view.table_actions()
       
   210             if self.display_filter and self.hide_filter and (facetsform or not generate_form):
       
   211                 actions += self.show_hide_filter_actions(not generate_form)
       
   212         self.render_table(w, actions, self.view.paginable)
       
   213         if facetsform and self.display_filter == 'bottom':
       
   214             cssclass = u'hidden' if self.hide_filter else u''
       
   215             facetsform.render(w, vid=self.view.__regid__, cssclass=cssclass,
       
   216                               divid=self.view.domid)
       
   217 
       
   218     def render_table_headers(self, w, colrenderers):
       
   219         w(u'<thead><tr>')
       
   220         for colrenderer in colrenderers:
       
   221             if colrenderer.sortable:
       
   222                 w(u'<th class="sortable">')
       
   223             else:
       
   224                 w(u'<th>')
       
   225             colrenderer.render_header(w)
       
   226             w(u'</th>')
       
   227         w(u'</tr></thead>\n')
       
   228 
       
   229     def render_table_body(self, w, colrenderers):
       
   230         w(u'<tbody>')
       
   231         for rownum in range(self.view.table_size):
       
   232             self.render_row(w, rownum, colrenderers)
       
   233         w(u'</tbody>')
       
   234 
       
   235     def render_table(self, w, actions, paginate):
       
   236         view = self.view
       
   237         divid = view.domid
       
   238         if divid is not None:
       
   239             w(u'<div id="%s">' % divid)
       
   240         else:
       
   241             assert not (actions or paginate)
       
   242         nav_html = UStringIO()
       
   243         if paginate:
       
   244             view.paginate(w=nav_html.write, show_all_option=self.show_all_option)
       
   245         w(nav_html.getvalue())
       
   246         if actions and self.display_actions == 'top':
       
   247             self.render_actions(w, actions)
       
   248         colrenderers = view.build_column_renderers()
       
   249         attrs = self.table_attributes()
       
   250         w(u'<table %s>' % sgml_attributes(attrs))
       
   251         if self.view.has_headers:
       
   252             self.render_table_headers(w, colrenderers)
       
   253         self.render_table_body(w, colrenderers)
       
   254         w(u'</table>')
       
   255         if actions and self.display_actions == 'bottom':
       
   256             self.render_actions(w, actions)
       
   257         w(nav_html.getvalue())
       
   258         if divid is not None:
       
   259             w(u'</div>')
       
   260 
       
   261     def table_attributes(self):
       
   262         return {'class': self.cssclass}
       
   263 
       
   264     def render_row(self, w, rownum, renderers):
       
   265         attrs = self.row_attributes(rownum)
       
   266         w(u'<tr %s>' % sgml_attributes(attrs))
       
   267         for colnum, renderer in enumerate(renderers):
       
   268             self.render_cell(w, rownum, colnum, renderer)
       
   269         w(u'</tr>\n')
       
   270 
       
   271     def row_attributes(self, rownum):
       
   272         return {'class': 'odd' if (rownum%2==1) else 'even',
       
   273                 'onmouseover': '$(this).addClass("highlighted");',
       
   274                 'onmouseout': '$(this).removeClass("highlighted")'}
       
   275 
       
   276     def render_cell(self, w, rownum, colnum, renderer):
       
   277         attrs = self.cell_attributes(rownum, colnum, renderer)
       
   278         if colnum in self.header_column_idx:
       
   279             tag = u'th'
       
   280         else:
       
   281             tag = u'td'
       
   282         w(u'<%s %s>' % (tag, sgml_attributes(attrs)))
       
   283         renderer.render_cell(w, rownum)
       
   284         w(u'</%s>' % tag)
       
   285 
       
   286     def cell_attributes(self, rownum, _colnum, renderer):
       
   287         attrs = renderer.attributes.copy()
       
   288         if renderer.sortable:
       
   289             sortvalue = renderer.sortvalue(rownum)
       
   290             if isinstance(sortvalue, string_types):
       
   291                 sortvalue = sortvalue[:self.sortvalue_limit]
       
   292             if sortvalue is not None:
       
   293                 attrs[u'cubicweb:sortvalue'] = js_dumps(sortvalue)
       
   294         return attrs
       
   295 
       
   296     def render_actions(self, w, actions):
       
   297         box = MenuWidget('', '', _class='tableActionsBox', islist=False)
       
   298         label = tags.span(self._cw._('action menu'))
       
   299         menu = PopupBoxMenu(label, isitem=False, link_class='actionsBox',
       
   300                             ident='%sActions' % self.view.domid)
       
   301         box.append(menu)
       
   302         for action in actions:
       
   303             menu.append(action)
       
   304         box.render(w=w)
       
   305         w(u'<div class="clear"></div>')
       
   306 
       
   307     def show_hide_filter_actions(self, currentlydisplayed=False):
       
   308         divid = self.view.domid
       
   309         showhide = u';'.join(toggle_action('%s%s' % (divid, what))[11:]
       
   310                              for what in ('Form', 'Show', 'Hide', 'Actions'))
       
   311         showhide = 'javascript:' + showhide
       
   312         self._cw.add_onload(u'''\
       
   313 $(document).ready(function() {
       
   314   if ($('#%(id)sForm[class=\"hidden\"]').length) {
       
   315     $('#%(id)sHide').attr('class', 'hidden');
       
   316   } else {
       
   317     $('#%(id)sShow').attr('class', 'hidden');
       
   318   }
       
   319 });''' % {'id': divid})
       
   320         showlabel = self._cw._('show filter form')
       
   321         hidelabel = self._cw._('hide filter form')
       
   322         return [component.Link(showhide, showlabel, id='%sShow' % divid),
       
   323                 component.Link(showhide, hidelabel, id='%sHide' % divid)]
       
   324 
       
   325 
       
   326 class AbstractColumnRenderer(object):
       
   327     """Abstract base class for column renderer. Interface of a column renderer follows:
       
   328 
       
   329     .. automethod:: cubicweb.web.views.tableview.AbstractColumnRenderer.bind
       
   330     .. automethod:: cubicweb.web.views.tableview.AbstractColumnRenderer.render_header
       
   331     .. automethod:: cubicweb.web.views.tableview.AbstractColumnRenderer.render_cell
       
   332     .. automethod:: cubicweb.web.views.tableview.AbstractColumnRenderer.sortvalue
       
   333 
       
   334     Attributes on this base class are:
       
   335 
       
   336     :attr: `header`, the column header. If None, default to `_(colid)`
       
   337     :attr: `addcount`, if True, add the table size in parenthezis beside the header
       
   338     :attr: `trheader`, should the header be translated
       
   339     :attr: `escapeheader`, should the header be xml_escaped
       
   340     :attr: `sortable`, tell if the column is sortable
       
   341     :attr: `view`, the table view
       
   342     :attr: `_cw`, the request object
       
   343     :attr: `colid`, the column identifier
       
   344     :attr: `attributes`, dictionary of attributes to put on the HTML tag when
       
   345             the cell is rendered
       
   346     """ #'# make emacs
       
   347     attributes = {}
       
   348     empty_cell_content = u'&#160;'
       
   349 
       
   350     def __init__(self, header=None, addcount=False, trheader=True,
       
   351                  escapeheader=True, sortable=True):
       
   352         self.header = header
       
   353         self.trheader = trheader
       
   354         self.escapeheader = escapeheader
       
   355         self.addcount = addcount
       
   356         self.sortable = sortable
       
   357         self.view = None
       
   358         self._cw = None
       
   359         self.colid = None
       
   360 
       
   361     def __str__(self):
       
   362         return '<%s.%s (column %s) at 0x%x>' % (self.view.__class__.__name__,
       
   363                                         self.__class__.__name__,
       
   364                                         self.colid, id(self))
       
   365 
       
   366     def bind(self, view, colid):
       
   367         """Bind the column renderer to its view. This is where `_cw`, `view`,
       
   368         `colid` are set and the method to override if you want to add more
       
   369         view/request depending attributes on your column render.
       
   370         """
       
   371         self.view = view
       
   372         self._cw = view._cw
       
   373         self.colid = colid
       
   374 
       
   375     def copy(self):
       
   376         assert self.view is None
       
   377         return copy(self)
       
   378 
       
   379     def default_header(self):
       
   380         """Return header for this column if one has not been specified."""
       
   381         return self._cw._(self.colid)
       
   382 
       
   383     def render_header(self, w):
       
   384         """Write label for the specified column by calling w()."""
       
   385         header = self.header
       
   386         if header is None:
       
   387             header = self.default_header()
       
   388         elif self.trheader and header:
       
   389            header = self._cw._(header)
       
   390         if self.addcount:
       
   391             header = '%s (%s)' % (header, self.view.table_size)
       
   392         if header:
       
   393             if self.escapeheader:
       
   394                 header = xml_escape(header)
       
   395         else:
       
   396             header = self.empty_cell_content
       
   397         if self.sortable:
       
   398             header = tags.span(
       
   399                 header, escapecontent=False,
       
   400                 title=self._cw._('Click to sort on this column'))
       
   401         w(header)
       
   402 
       
   403     def render_cell(self, w, rownum):
       
   404         """Write value for the specified cell by calling w().
       
   405 
       
   406          :param `rownum`: the row number in the table
       
   407          """
       
   408         raise NotImplementedError()
       
   409 
       
   410     def sortvalue(self, _rownum):
       
   411         """Return typed value to be used for sorting on the specified column.
       
   412 
       
   413         :param `rownum`: the row number in the table
       
   414         """
       
   415         return None
       
   416 
       
   417 
       
   418 class TableMixIn(component.LayoutableMixIn):
       
   419     """Abstract mix-in class for layout based tables.
       
   420 
       
   421     This default implementation's call method simply delegate to
       
   422     meth:`layout_render` that will select the renderer whose identifier is given
       
   423     by the :attr:`layout_id` attribute.
       
   424 
       
   425     Then it provides some default implementation for various parts of the API
       
   426     used by that layout.
       
   427 
       
   428     Abstract method you will have to override is:
       
   429 
       
   430     .. automethod:: build_column_renderers
       
   431 
       
   432     You may also want to overridde:
       
   433 
       
   434     .. autoattribute:: cubicweb.web.views.tableview.TableMixIn.table_size
       
   435 
       
   436     The :attr:`has_headers` boolean attribute tells if the table has some
       
   437     headers to be displayed. Default to `True`.
       
   438     """
       
   439     __abstract__ = True
       
   440     # table layout to use
       
   441     layout_id = 'table_layout'
       
   442     # true if the table has some headers
       
   443     has_headers = True
       
   444     # dictionary {colid : column renderer}
       
   445     column_renderers = {}
       
   446     # default renderer class to use when no renderer specified for the column
       
   447     default_column_renderer_class = None
       
   448     # default layout handles inner pagination
       
   449     handle_pagination = True
       
   450 
       
   451     def call(self, **kwargs):
       
   452         self._cw.add_js('cubicweb.ajax.js') # for pagination
       
   453         self.layout_render(self.w)
       
   454 
       
   455     def column_renderer(self, colid, *args, **kwargs):
       
   456         """Return a column renderer for column of the given id."""
       
   457         try:
       
   458             crenderer = self.column_renderers[colid].copy()
       
   459         except KeyError:
       
   460             crenderer = self.default_column_renderer_class(*args, **kwargs)
       
   461         crenderer.bind(self, colid)
       
   462         return crenderer
       
   463 
       
   464     # layout callbacks #########################################################
       
   465 
       
   466     def facets_form(self, **kwargs):# XXX extracted from jqplot cube
       
   467         return self._cw.vreg['views'].select_or_none(
       
   468             'facet.filtertable', self._cw, rset=self.cw_rset, view=self,
       
   469             **kwargs)
       
   470 
       
   471     @cachedproperty
       
   472     def domid(self):
       
   473         return self._cw.form.get('divid') or domid('%s-%s' % (self.__regid__, make_uid()))
       
   474 
       
   475     @property
       
   476     def table_size(self):
       
   477         """Return the number of rows (header excluded) to be displayed.
       
   478 
       
   479         By default return the number of rows in the view's result set. If your
       
   480         table isn't reult set based, override this method.
       
   481         """
       
   482         return self.cw_rset.rowcount
       
   483 
       
   484     def build_column_renderers(self):
       
   485         """Return a list of column renderers, one for each column to be
       
   486         rendered. Prototype of a column renderer is described below:
       
   487 
       
   488         .. autoclass:: cubicweb.web.views.tableview.AbstractColumnRenderer
       
   489         """
       
   490         raise NotImplementedError()
       
   491 
       
   492     def table_actions(self):
       
   493         """Return a list of actions (:class:`~cubicweb.web.component.Link`) that
       
   494         match the view's result set, and return those in the 'mainactions'
       
   495         category.
       
   496         """
       
   497         req = self._cw
       
   498         actions = []
       
   499         actionsbycat = req.vreg['actions'].possible_actions(req, self.cw_rset)
       
   500         for action in actionsbycat.get('mainactions', ()):
       
   501             for action in action.actual_actions():
       
   502                 actions.append(component.Link(action.url(), req._(action.title),
       
   503                                               klass=action.html_class()) )
       
   504         return actions
       
   505 
       
   506     # interaction with navigation component ####################################
       
   507 
       
   508     def page_navigation_url(self, navcomp, _path, params):
       
   509         params['divid'] = self.domid
       
   510         params['vid'] = self.__regid__
       
   511         return navcomp.ajax_page_url(**params)
       
   512 
       
   513 
       
   514 class RsetTableColRenderer(AbstractColumnRenderer):
       
   515     """Default renderer for :class:`RsetTableView`."""
       
   516 
       
   517     def __init__(self, cellvid, **kwargs):
       
   518         super(RsetTableColRenderer, self).__init__(**kwargs)
       
   519         self.cellvid = cellvid
       
   520 
       
   521     def bind(self, view, colid):
       
   522         super(RsetTableColRenderer, self).bind(view, colid)
       
   523         self.cw_rset = view.cw_rset
       
   524     def render_cell(self, w, rownum):
       
   525         self._cw.view(self.cellvid, self.cw_rset, 'empty-cell',
       
   526                       row=rownum, col=self.colid, w=w)
       
   527 
       
   528     # limit value's length as much as possible (e.g. by returning the 10 first
       
   529     # characters of a string)
       
   530     def sortvalue(self, rownum):
       
   531         colid = self.colid
       
   532         val = self.cw_rset[rownum][colid]
       
   533         if val is None:
       
   534             return u''
       
   535         etype = self.cw_rset.description[rownum][colid]
       
   536         if etype is None:
       
   537             return u''
       
   538         if self._cw.vreg.schema.eschema(etype).final:
       
   539             entity, rtype = self.cw_rset.related_entity(rownum, colid)
       
   540             if entity is None:
       
   541                 return val # remove_html_tags() ?
       
   542             return entity.sortvalue(rtype)
       
   543         entity = self.cw_rset.get_entity(rownum, colid)
       
   544         return entity.sortvalue()
       
   545 
       
   546 
       
   547 class RsetTableView(TableMixIn, AnyRsetView):
       
   548     """This table view accepts any non-empty rset. It uses introspection on the
       
   549     result set to compute column names and the proper way to display the cells.
       
   550 
       
   551     It is highly configurable and accepts a wealth of options, but take care to
       
   552     check what you're trying to achieve wouldn't be a job for the
       
   553     :class:`EntityTableView`. Basically the question is: does this view should
       
   554     be tied to the result set query's shape or no? If yes, than you're fine. If
       
   555     no, you should take a look at the other table implementation.
       
   556 
       
   557     The following class attributes may be used to control the table:
       
   558 
       
   559     * `finalvid`, a view identifier that should be called on final entities
       
   560       (e.g. attribute values). Default to 'final'.
       
   561 
       
   562     * `nonfinalvid`, a view identifier that should be called on
       
   563       entities. Default to 'incontext'.
       
   564 
       
   565     * `displaycols`, if not `None`, should be a list of rset's columns to be
       
   566       displayed.
       
   567 
       
   568     * `headers`, if not `None`, should be a list of headers for the table's
       
   569       columns.  `None` values in the list will be replaced by computed column
       
   570       names.
       
   571 
       
   572     * `cellvids`, if not `None`, should be a dictionary with table column index
       
   573       as key and a view identifier as value, telling the view that should be
       
   574       used in the given column.
       
   575 
       
   576     Notice `displaycols`, `headers` and `cellvids` may be specified at selection
       
   577     time but then the table won't have pagination and shouldn't be configured to
       
   578     display the facets filter nor actions (as they wouldn't behave as expected).
       
   579 
       
   580     This table class use the :class:`RsetTableColRenderer` as default column
       
   581     renderer.
       
   582 
       
   583     .. autoclass:: RsetTableColRenderer
       
   584     """    #'# make emacs happier
       
   585     __regid__ = 'table'
       
   586     # selector trick for bw compath with the former :class:TableView
       
   587     __select__ = AnyRsetView.__select__ & (~match_kwargs(
       
   588         'title', 'subvid', 'displayfilter', 'headers', 'displaycols',
       
   589         'displayactions', 'actions', 'divid', 'cellvids', 'cellattrs',
       
   590         'mainindex', 'paginate', 'page_size', mode='any')
       
   591                                             | unreloadable_table())
       
   592     title = _('table')
       
   593     # additional configuration parameters
       
   594     finalvid = 'final'
       
   595     nonfinalvid = 'incontext'
       
   596     displaycols = None
       
   597     headers = None
       
   598     cellvids = None
       
   599     default_column_renderer_class = RsetTableColRenderer
       
   600 
       
   601     def linkable(self):
       
   602         # specific subclasses of this view usually don't want to be linkable
       
   603         # since they depends on a particular shape (being linkable meaning view
       
   604         # may be listed in possible views
       
   605         return self.__regid__ == 'table'
       
   606 
       
   607     def call(self, headers=None, displaycols=None, cellvids=None,
       
   608              paginate=None, **kwargs):
       
   609         if self.headers:
       
   610             self.headers = [h and self._cw._(h) for h in self.headers]
       
   611         if (headers or displaycols or cellvids or paginate):
       
   612             if headers is not None:
       
   613                 self.headers = headers
       
   614             if displaycols is not None:
       
   615                 self.displaycols = displaycols
       
   616             if cellvids is not None:
       
   617                 self.cellvids = cellvids
       
   618             if paginate is not None:
       
   619                 self.paginable = paginate
       
   620         if kwargs:
       
   621             # old table view arguments that we can safely ignore thanks to
       
   622             # selectors
       
   623             if len(kwargs) > 1:
       
   624                 msg = '[3.14] %s arguments are deprecated' % ', '.join(kwargs)
       
   625             else:
       
   626                 msg = '[3.14] %s argument is deprecated' % ', '.join(kwargs)
       
   627             warn(msg, DeprecationWarning, stacklevel=2)
       
   628         super(RsetTableView, self).call(**kwargs)
       
   629 
       
   630     def main_var_index(self):
       
   631         """returns the index of the first non-attribute variable among the RQL
       
   632         selected variables
       
   633         """
       
   634         eschema = self._cw.vreg.schema.eschema
       
   635         for i, etype in enumerate(self.cw_rset.description[0]):
       
   636             if not eschema(etype).final:
       
   637                 return i
       
   638         return None
       
   639 
       
   640     # layout callbacks #########################################################
       
   641 
       
   642     @property
       
   643     def table_size(self):
       
   644         """return the number of rows (header excluded) to be displayed"""
       
   645         return self.cw_rset.rowcount
       
   646 
       
   647     def build_column_renderers(self):
       
   648         headers = self.headers
       
   649         # compute displayed columns
       
   650         if self.displaycols is None:
       
   651             if headers is not None:
       
   652                 displaycols = list(range(len(headers)))
       
   653             else:
       
   654                 rqlst = self.cw_rset.syntax_tree()
       
   655                 displaycols = list(range(len(rqlst.children[0].selection)))
       
   656         else:
       
   657             displaycols = self.displaycols
       
   658         # compute table headers
       
   659         main_var_index = self.main_var_index()
       
   660         computed_titles = self.columns_labels(main_var_index)
       
   661         # compute build renderers
       
   662         cellvids = self.cellvids
       
   663         renderers = []
       
   664         for colnum, colid in enumerate(displaycols):
       
   665             addcount = False
       
   666             # compute column header
       
   667             title = None
       
   668             if headers is not None:
       
   669                 title = headers[colnum]
       
   670             if title is None:
       
   671                 title = computed_titles[colid]
       
   672             if colid == main_var_index:
       
   673                 addcount = True
       
   674             # compute cell vid for the column
       
   675             if cellvids is not None and colnum in cellvids:
       
   676                 cellvid = cellvids[colnum]
       
   677             else:
       
   678                 coltype = self.cw_rset.description[0][colid]
       
   679                 if coltype is not None and self._cw.vreg.schema.eschema(coltype).final:
       
   680                     cellvid = self.finalvid
       
   681                 else:
       
   682                     cellvid = self.nonfinalvid
       
   683             # get renderer
       
   684             renderer = self.column_renderer(colid, header=title, trheader=False,
       
   685                                             addcount=addcount, cellvid=cellvid)
       
   686             renderers.append(renderer)
       
   687         return renderers
       
   688 
       
   689 
       
   690 class EntityTableColRenderer(AbstractColumnRenderer):
       
   691     """Default column renderer for :class:`EntityTableView`.
       
   692 
       
   693     You may use the :meth:`entity` method to retrieve the main entity for a
       
   694     given row number.
       
   695 
       
   696     .. automethod:: cubicweb.web.views.tableview.EntityTableColRenderer.entity
       
   697     .. automethod:: cubicweb.web.views.tableview.EntityTableColRenderer.render_entity
       
   698     .. automethod:: cubicweb.web.views.tableview.EntityTableColRenderer.entity_sortvalue
       
   699     """
       
   700     def __init__(self, renderfunc=None, sortfunc=None, sortable=None, **kwargs):
       
   701         if renderfunc is None:
       
   702             renderfunc = self.render_entity
       
   703             # if renderfunc nor sortfunc nor sortable specified, column will be
       
   704             # sortable using the default implementation.
       
   705             if sortable is None:
       
   706                 sortable = True
       
   707         # no sortfunc given but asked to be sortable: use the default sort
       
   708         # method. Sub-class may set `entity_sortvalue` to None if they don't
       
   709         # support sorting.
       
   710         if sortfunc is None and sortable:
       
   711             sortfunc = self.entity_sortvalue
       
   712         # at this point `sortable` may still be unspecified while `sortfunc` is
       
   713         # sure to be set to someting else than None if the column is sortable.
       
   714         sortable = sortfunc is not None
       
   715         super(EntityTableColRenderer, self).__init__(sortable=sortable, **kwargs)
       
   716         self.renderfunc = renderfunc
       
   717         self.sortfunc = sortfunc
       
   718 
       
   719     def copy(self):
       
   720         assert self.view is None
       
   721         # copy of attribute referencing a method doesn't work with python < 2.7
       
   722         renderfunc = self.__dict__.pop('renderfunc')
       
   723         sortfunc = self.__dict__.pop('sortfunc')
       
   724         try:
       
   725             acopy =  copy(self)
       
   726             for aname, member in[('renderfunc', renderfunc),
       
   727                                  ('sortfunc', sortfunc)]:
       
   728                 if isinstance(member, MethodType):
       
   729                     member = create_bound_method(member.__func__, acopy)
       
   730                 setattr(acopy, aname, member)
       
   731             return acopy
       
   732         finally:
       
   733             self.renderfunc = renderfunc
       
   734             self.sortfunc = sortfunc
       
   735 
       
   736     def render_cell(self, w, rownum):
       
   737         entity = self.entity(rownum)
       
   738         if entity is None:
       
   739             w(self.empty_cell_content)
       
   740         else:
       
   741             self.renderfunc(w, entity)
       
   742 
       
   743     def sortvalue(self, rownum):
       
   744         entity = self.entity(rownum)
       
   745         if entity is None:
       
   746             return None
       
   747         else:
       
   748             return self.sortfunc(entity)
       
   749 
       
   750     def entity(self, rownum):
       
   751         """Convenience method returning the table's main entity."""
       
   752         return self.view.entity(rownum)
       
   753 
       
   754     def render_entity(self, w, entity):
       
   755         """Sort value if `renderfunc` nor `sortfunc` specified at
       
   756         initialization.
       
   757 
       
   758         This default implementation consider column id is an entity attribute
       
   759         and print its value.
       
   760         """
       
   761         w(entity.printable_value(self.colid))
       
   762 
       
   763     def entity_sortvalue(self, entity):
       
   764         """Cell rendering implementation if `renderfunc` nor `sortfunc`
       
   765         specified at initialization.
       
   766 
       
   767         This default implementation consider column id is an entity attribute
       
   768         and return its sort value by calling `entity.sortvalue(colid)`.
       
   769         """
       
   770         return entity.sortvalue(self.colid)
       
   771 
       
   772 
       
   773 class MainEntityColRenderer(EntityTableColRenderer):
       
   774     """Renderer to be used for the column displaying the 'main entity' of a
       
   775     :class:`EntityTableView`.
       
   776 
       
   777     By default display it using the 'incontext' view. You may specify another
       
   778     view identifier using the `vid` argument.
       
   779 
       
   780     If header not specified, it would be built using entity types in the main
       
   781     column.
       
   782     """
       
   783     def __init__(self, vid='incontext', addcount=True, **kwargs):
       
   784         super(MainEntityColRenderer, self).__init__(addcount=addcount, **kwargs)
       
   785         self.vid = vid
       
   786 
       
   787     def default_header(self):
       
   788         view = self.view
       
   789         if len(view.cw_rset) > 1:
       
   790             suffix = '_plural'
       
   791         else:
       
   792             suffix = ''
       
   793         return u', '.join(self._cw.__(et + suffix)
       
   794                           for et in view.cw_rset.column_types(view.cw_col or 0))
       
   795 
       
   796     def render_entity(self, w, entity):
       
   797         entity.view(self.vid, w=w)
       
   798 
       
   799     def entity_sortvalue(self, entity):
       
   800         return entity.sortvalue()
       
   801 
       
   802 
       
   803 class RelatedEntityColRenderer(MainEntityColRenderer):
       
   804     """Renderer to be used for column displaying an entity related the 'main
       
   805     entity' of a :class:`EntityTableView`.
       
   806 
       
   807     By default display it using the 'incontext' view. You may specify another
       
   808     view identifier using the `vid` argument.
       
   809 
       
   810     If header not specified, it would be built by translating the column id.
       
   811     """
       
   812     def __init__(self, getrelated, addcount=False, **kwargs):
       
   813         super(RelatedEntityColRenderer, self).__init__(addcount=addcount, **kwargs)
       
   814         self.getrelated = getrelated
       
   815 
       
   816     def entity(self, rownum):
       
   817         entity = super(RelatedEntityColRenderer, self).entity(rownum)
       
   818         return self.getrelated(entity)
       
   819 
       
   820     def default_header(self):
       
   821         return self._cw._(self.colid)
       
   822 
       
   823 
       
   824 class RelationColRenderer(EntityTableColRenderer):
       
   825     """Renderer to be used for column displaying a list of entities related the
       
   826     'main entity' of a :class:`EntityTableView`. By default, the main entity is
       
   827     considered as the subject of the relation but you may specify otherwise
       
   828     using the `role` argument.
       
   829 
       
   830     By default display the related rset using the 'csv' view, using
       
   831     'outofcontext' sub-view for each entity. You may specify another view
       
   832     identifier using respectivly the `vid` and `subvid` arguments.
       
   833 
       
   834     If you specify a 'rtype view', such as 'reledit', you should add a
       
   835     is_rtype_view=True parameter.
       
   836 
       
   837     If header not specified, it would be built by translating the column id,
       
   838     properly considering role.
       
   839     """
       
   840     def __init__(self, role='subject', vid='csv', subvid=None,
       
   841                  fallbackvid='empty-cell', is_rtype_view=False, **kwargs):
       
   842         super(RelationColRenderer, self).__init__(**kwargs)
       
   843         self.role = role
       
   844         self.vid = vid
       
   845         if subvid is None and vid in ('csv', 'list'):
       
   846             subvid = 'outofcontext'
       
   847         self.subvid = subvid
       
   848         self.fallbackvid = fallbackvid
       
   849         self.is_rtype_view = is_rtype_view
       
   850 
       
   851     def render_entity(self, w, entity):
       
   852         kwargs = {'w': w}
       
   853         if self.is_rtype_view:
       
   854             rset = None
       
   855             kwargs['entity'] = entity
       
   856             kwargs['rtype'] = self.colid
       
   857             kwargs['role'] = self.role
       
   858         else:
       
   859             rset = entity.related(self.colid, self.role)
       
   860         if self.subvid is not None:
       
   861             kwargs['subvid'] = self.subvid
       
   862         self._cw.view(self.vid, rset, self.fallbackvid, **kwargs)
       
   863 
       
   864     def default_header(self):
       
   865         return display_name(self._cw, self.colid, self.role)
       
   866 
       
   867     entity_sortvalue = None # column not sortable by default
       
   868 
       
   869 
       
   870 class EntityTableView(TableMixIn, EntityView):
       
   871     """This abstract table view is designed to be used with an
       
   872     :class:`is_instance()` or :class:`adaptable` predicate, hence doesn't depend
       
   873     the result set shape as the :class:`RsetTableView` does.
       
   874 
       
   875     It will display columns that should be defined using the `columns` class
       
   876     attribute containing a list of column ids. By default, each column is
       
   877     renderered by :class:`EntityTableColRenderer` which consider that the column
       
   878     id is an attribute of the table's main entity (ie the one for which the view
       
   879     is selected).
       
   880 
       
   881     You may wish to specify :class:`MainEntityColRenderer` or
       
   882     :class:`RelatedEntityColRenderer` renderer for a column in the
       
   883     :attr:`column_renderers` dictionary.
       
   884 
       
   885     .. autoclass:: cubicweb.web.views.tableview.EntityTableColRenderer
       
   886     .. autoclass:: cubicweb.web.views.tableview.MainEntityColRenderer
       
   887     .. autoclass:: cubicweb.web.views.tableview.RelatedEntityColRenderer
       
   888     .. autoclass:: cubicweb.web.views.tableview.RelationColRenderer
       
   889     """
       
   890     __abstract__ = True
       
   891     default_column_renderer_class = EntityTableColRenderer
       
   892     columns = None # to be defined in concret class
       
   893 
       
   894     def call(self, columns=None, **kwargs):
       
   895         if columns is not None:
       
   896             self.columns = columns
       
   897         self.layout_render(self.w)
       
   898 
       
   899     @property
       
   900     def table_size(self):
       
   901         return self.cw_rset.rowcount
       
   902 
       
   903     def build_column_renderers(self):
       
   904         return [self.column_renderer(colid) for colid in self.columns]
       
   905 
       
   906     def entity(self, rownum):
       
   907         """Return the table's main entity"""
       
   908         return self.cw_rset.get_entity(rownum, self.cw_col or 0)
       
   909 
       
   910 
       
   911 class EmptyCellView(AnyRsetView):
       
   912     __regid__ = 'empty-cell'
       
   913     __select__ = yes()
       
   914     def call(self, **kwargs):
       
   915         self.w(u'&#160;')
       
   916     cell_call = call
       
   917 
       
   918 
       
   919 ################################################################################
       
   920 # DEPRECATED tables ############################################################
       
   921 ################################################################################
       
   922 
       
   923 
       
   924 @add_metaclass(class_deprecated)
       
   925 class TableView(AnyRsetView):
       
   926     """The table view accepts any non-empty rset. It uses introspection on the
       
   927     result set to compute column names and the proper way to display the cells.
       
   928 
       
   929     It is however highly configurable and accepts a wealth of options.
       
   930     """
       
   931     __deprecation_warning__ = '[3.14] %(cls)s is deprecated'
       
   932     __regid__ = 'table'
       
   933     title = _('table')
       
   934     finalview = 'final'
       
   935 
       
   936     table_widget_class = TableWidget
       
   937     table_column_class = TableColumn
       
   938 
       
   939     tablesorter_settings = {
       
   940         'textExtraction': JSString('cw.sortValueExtraction'),
       
   941         'selectorHeaders': 'thead tr:first th', # only plug on the first row
       
   942         }
       
   943     handle_pagination = True
       
   944 
       
   945     def form_filter(self, divid, displaycols, displayactions, displayfilter,
       
   946                     paginate, hidden=True):
       
   947         try:
       
   948             filterform = self._cw.vreg['views'].select(
       
   949                 'facet.filtertable', self._cw, rset=self.cw_rset)
       
   950         except NoSelectableObject:
       
   951             return ()
       
   952         vidargs = {'paginate': paginate,
       
   953                    'displaycols': displaycols,
       
   954                    'displayactions': displayactions,
       
   955                    'displayfilter': displayfilter}
       
   956         cssclass = hidden and 'hidden' or ''
       
   957         filterform.render(self.w, vid=self.__regid__, divid=divid,
       
   958                           vidargs=vidargs, cssclass=cssclass)
       
   959         return self.show_hide_actions(divid, not hidden)
       
   960 
       
   961     def main_var_index(self):
       
   962         """Returns the index of the first non final variable of the rset.
       
   963 
       
   964         Used to select the main etype to help generate accurate column headers.
       
   965         XXX explain the concept
       
   966 
       
   967         May return None if none is found.
       
   968         """
       
   969         eschema = self._cw.vreg.schema.eschema
       
   970         for i, etype in enumerate(self.cw_rset.description[0]):
       
   971             try:
       
   972                 if not eschema(etype).final:
       
   973                     return i
       
   974             except KeyError: # XXX possible?
       
   975                 continue
       
   976         return None
       
   977 
       
   978     def displaycols(self, displaycols, headers):
       
   979         if displaycols is None:
       
   980             if 'displaycols' in self._cw.form:
       
   981                 displaycols = [int(idx) for idx in self._cw.form['displaycols']]
       
   982             elif headers is not None:
       
   983                 displaycols = list(range(len(headers)))
       
   984             else:
       
   985                 displaycols = list(range(len(self.cw_rset.syntax_tree().children[0].selection)))
       
   986         return displaycols
       
   987 
       
   988     def _setup_tablesorter(self, divid):
       
   989         req = self._cw
       
   990         req.add_js('jquery.tablesorter.js')
       
   991         req.add_onload('''$(document).ready(function() {
       
   992     $("#%s table.listing").tablesorter(%s);
       
   993 });''' % (divid, js_dumps(self.tablesorter_settings)))
       
   994         req.add_css(('cubicweb.tablesorter.css', 'cubicweb.tableview.css'))
       
   995 
       
   996     @cachedproperty
       
   997     def initial_load(self):
       
   998         """We detect a bit heuristically if we are built for the first time or
       
   999         from subsequent calls by the form filter or by the pagination
       
  1000         hooks.
       
  1001 
       
  1002         """
       
  1003         form = self._cw.form
       
  1004         return 'fromformfilter' not in form and '__start' not in form
       
  1005 
       
  1006     def call(self, title=None, subvid=None, displayfilter=None, headers=None,
       
  1007              displaycols=None, displayactions=None, actions=(), divid=None,
       
  1008              cellvids=None, cellattrs=None, mainindex=None,
       
  1009              paginate=False, page_size=None):
       
  1010         """Produces a table displaying a composite query
       
  1011 
       
  1012         :param title: title added before table
       
  1013         :param subvid: cell view
       
  1014         :param displayfilter: filter that selects rows to display
       
  1015         :param headers: columns' titles
       
  1016         :param displaycols: indexes of columns to display (first column is 0)
       
  1017         :param displayactions: if True, display action menu
       
  1018         """
       
  1019         req = self._cw
       
  1020         divid = divid or req.form.get('divid') or 'rs%s' % make_uid(id(self.cw_rset))
       
  1021         self._setup_tablesorter(divid)
       
  1022         # compute label first  since the filter form may remove some necessary
       
  1023         # information from the rql syntax tree
       
  1024         if mainindex is None:
       
  1025             mainindex = self.main_var_index()
       
  1026         computed_labels = self.columns_labels(mainindex)
       
  1027         if not subvid and 'subvid' in req.form:
       
  1028             subvid = req.form.pop('subvid')
       
  1029         actions = list(actions)
       
  1030         if mainindex is None:
       
  1031             displayfilter, displayactions = False, False
       
  1032         else:
       
  1033             if displayfilter is None and req.form.get('displayfilter'):
       
  1034                 displayfilter = True
       
  1035             if displayactions is None and req.form.get('displayactions'):
       
  1036                 displayactions = True
       
  1037         displaycols = self.displaycols(displaycols, headers)
       
  1038         if self.initial_load:
       
  1039             self.w(u'<div class="section">')
       
  1040             if not title and 'title' in req.form:
       
  1041                 title = req.form['title']
       
  1042             if title:
       
  1043                 self.w(u'<h2 class="tableTitle">%s</h2>\n' % title)
       
  1044             if displayfilter:
       
  1045                 actions += self.form_filter(divid, displaycols, displayfilter,
       
  1046                                             displayactions, paginate)
       
  1047         elif displayfilter:
       
  1048             actions += self.show_hide_actions(divid, True)
       
  1049         self.w(u'<div id="%s">' % divid)
       
  1050         if displayactions:
       
  1051             actionsbycat = self._cw.vreg['actions'].possible_actions(req, self.cw_rset)
       
  1052             for action in actionsbycat.get('mainactions', ()):
       
  1053                 for action in action.actual_actions():
       
  1054                     actions.append( (action.url(), req._(action.title),
       
  1055                                      action.html_class(), None) )
       
  1056         # render actions menu
       
  1057         if actions:
       
  1058             self.render_actions(divid, actions)
       
  1059         # render table
       
  1060         if paginate:
       
  1061             self.divid = divid # XXX iirk (see usage in page_navigation_url)
       
  1062             self.paginate(page_size=page_size, show_all_option=False)
       
  1063         table = self.table_widget_class(self)
       
  1064         for column in self.get_columns(computed_labels, displaycols, headers,
       
  1065                                        subvid, cellvids, cellattrs, mainindex):
       
  1066             table.append_column(column)
       
  1067         table.render(self.w)
       
  1068         self.w(u'</div>\n')
       
  1069         if self.initial_load:
       
  1070             self.w(u'</div>\n')
       
  1071 
       
  1072     def page_navigation_url(self, navcomp, path, params):
       
  1073         """Build a URL to the current view using the <navcomp> attributes
       
  1074 
       
  1075         :param navcomp: a NavigationComponent to call a URL method on.
       
  1076         :param path:    expected to be json here?
       
  1077         :param params: params to give to build_url method
       
  1078 
       
  1079         this is called by :class:`cubiweb.web.component.NavigationComponent`
       
  1080         """
       
  1081         if hasattr(self, 'divid'):
       
  1082             # XXX this assert a single call
       
  1083             params['divid'] = self.divid
       
  1084         params['vid'] = self.__regid__
       
  1085         return navcomp.ajax_page_url(**params)
       
  1086 
       
  1087     def show_hide_actions(self, divid, currentlydisplayed=False):
       
  1088         showhide = u';'.join(toggle_action('%s%s' % (divid, what))[11:]
       
  1089                              for what in ('Form', 'Show', 'Hide', 'Actions'))
       
  1090         showhide = 'javascript:' + showhide
       
  1091         showlabel = self._cw._('show filter form')
       
  1092         hidelabel = self._cw._('hide filter form')
       
  1093         if currentlydisplayed:
       
  1094             return [(showhide, showlabel, 'hidden', '%sShow' % divid),
       
  1095                     (showhide, hidelabel, None, '%sHide' % divid)]
       
  1096         return [(showhide, showlabel, None, '%sShow' % divid),
       
  1097                 (showhide, hidelabel, 'hidden', '%sHide' % divid)]
       
  1098 
       
  1099     def render_actions(self, divid, actions):
       
  1100         box = MenuWidget('', 'tableActionsBox', _class='', islist=False)
       
  1101         label = tags.img(src=self._cw.uiprops['PUCE_DOWN'],
       
  1102                          alt=xml_escape(self._cw._('action(s) on this selection')))
       
  1103         menu = PopupBoxMenu(label, isitem=False, link_class='actionsBox',
       
  1104                             ident='%sActions' % divid)
       
  1105         box.append(menu)
       
  1106         for url, label, klass, ident in actions:
       
  1107             menu.append(component.Link(url, label, klass=klass, id=ident))
       
  1108         box.render(w=self.w)
       
  1109         self.w(u'<div class="clear"></div>')
       
  1110 
       
  1111     def get_columns(self, computed_labels, displaycols, headers, subvid,
       
  1112                     cellvids, cellattrs, mainindex):
       
  1113         """build columns description from various parameters
       
  1114 
       
  1115         : computed_labels: columns headers computed from rset to be used if there is no headers entry
       
  1116         : displaycols: see :meth:`call`
       
  1117         : headers: explicitly define columns headers
       
  1118         : subvid: see :meth:`call`
       
  1119         : cellvids: see :meth:`call`
       
  1120         : cellattrs: see :meth:`call`
       
  1121         : mainindex: see :meth:`call`
       
  1122 
       
  1123         return a list of columns description to be used by
       
  1124                :class:`~cubicweb.web.htmlwidgets.TableWidget`
       
  1125         """
       
  1126         columns = []
       
  1127         eschema = self._cw.vreg.schema.eschema
       
  1128         for colindex, label in enumerate(computed_labels):
       
  1129             if colindex not in displaycols:
       
  1130                 continue
       
  1131             # compute column header
       
  1132             if headers is not None:
       
  1133                 _label = headers[displaycols.index(colindex)]
       
  1134                 if _label is not None:
       
  1135                     label = _label
       
  1136             if colindex == mainindex and label is not None:
       
  1137                 label += ' (%s)' % self.cw_rset.rowcount
       
  1138             column = self.table_column_class(label, colindex)
       
  1139             coltype = self.cw_rset.description[0][colindex]
       
  1140             # compute column cell view (if coltype is None, it's a left outer
       
  1141             # join, use the default non final subvid)
       
  1142             if cellvids and colindex in cellvids:
       
  1143                 column.append_renderer(cellvids[colindex], colindex)
       
  1144             elif coltype is not None and eschema(coltype).final:
       
  1145                 column.append_renderer(self.finalview, colindex)
       
  1146             else:
       
  1147                 column.append_renderer(subvid or 'incontext', colindex)
       
  1148             if cellattrs and colindex in cellattrs:
       
  1149                 for name, value in cellattrs[colindex].items():
       
  1150                     column.add_attr(name, value)
       
  1151             # add column
       
  1152             columns.append(column)
       
  1153         return columns
       
  1154 
       
  1155 
       
  1156     def render_cell(self, cellvid, row, col, w):
       
  1157         self._cw.view('cell', self.cw_rset, row=row, col=col, cellvid=cellvid, w=w)
       
  1158 
       
  1159     def get_rows(self):
       
  1160         return self.cw_rset
       
  1161 
       
  1162     @htmlescape
       
  1163     @jsonize
       
  1164     @limitsize(10)
       
  1165     def sortvalue(self, row, col):
       
  1166         # XXX it might be interesting to try to limit value's
       
  1167         #     length as much as possible (e.g. by returning the 10
       
  1168         #     first characters of a string)
       
  1169         val = self.cw_rset[row][col]
       
  1170         if val is None:
       
  1171             return u''
       
  1172         etype = self.cw_rset.description[row][col]
       
  1173         if etype is None:
       
  1174             return u''
       
  1175         if self._cw.vreg.schema.eschema(etype).final:
       
  1176             entity, rtype = self.cw_rset.related_entity(row, col)
       
  1177             if entity is None:
       
  1178                 return val # remove_html_tags() ?
       
  1179             return entity.sortvalue(rtype)
       
  1180         entity = self.cw_rset.get_entity(row, col)
       
  1181         return entity.sortvalue()
       
  1182 
       
  1183 
       
  1184 class EditableTableView(TableView):
       
  1185     __regid__ = 'editable-table'
       
  1186     finalview = 'editable-final'
       
  1187     title = _('editable-table')
       
  1188 
       
  1189 
       
  1190 @add_metaclass(class_deprecated)
       
  1191 class CellView(EntityView):
       
  1192     __deprecation_warning__ = '[3.14] %(cls)s is deprecated'
       
  1193     __regid__ = 'cell'
       
  1194     __select__ = nonempty_rset()
       
  1195 
       
  1196     def cell_call(self, row, col, cellvid=None):
       
  1197         """
       
  1198         :param row, col: indexes locating the cell value in view's result set
       
  1199         :param cellvid: cell view (defaults to 'outofcontext')
       
  1200         """
       
  1201         etype, val = self.cw_rset.description[row][col], self.cw_rset[row][col]
       
  1202         if etype is None or not self._cw.vreg.schema.eschema(etype).final:
       
  1203             if val is None:
       
  1204                 # This is usually caused by a left outer join and in that case,
       
  1205                 # regular views will most certainly fail if they don't have
       
  1206                 # a real eid
       
  1207                 # XXX if cellvid is e.g. reledit, we may wanna call it anyway
       
  1208                 self.w(u'&#160;')
       
  1209             else:
       
  1210                 self.wview(cellvid or 'outofcontext', self.cw_rset, row=row, col=col)
       
  1211         else:
       
  1212             # XXX why do we need a fallback view here?
       
  1213             self.wview(cellvid or 'final', self.cw_rset, 'null', row=row, col=col)
       
  1214 
       
  1215 
       
  1216 class InitialTableView(TableView):
       
  1217     """same display as  table view but consider two rql queries :
       
  1218 
       
  1219     * the default query (ie `rql` form parameter), which is only used to select
       
  1220       this view and to build the filter form. This query should have the same
       
  1221       structure as the actual without actual restriction (but link to
       
  1222       restriction variables) and usually with a limit for efficiency (limit set
       
  1223       to 2 is advised)
       
  1224 
       
  1225     * the actual query (`actualrql` form parameter) whose results will be
       
  1226       displayed with default restrictions set
       
  1227     """
       
  1228     __regid__ = 'initialtable'
       
  1229     __select__ = nonempty_rset()
       
  1230     # should not be displayed in possible view since it expects some specific
       
  1231     # parameters
       
  1232     title = None
       
  1233 
       
  1234     def call(self, title=None, subvid=None, headers=None, divid=None,
       
  1235              paginate=False, displaycols=None, displayactions=None,
       
  1236              mainindex=None):
       
  1237         """Dumps a table displaying a composite query"""
       
  1238         try:
       
  1239             actrql = self._cw.form['actualrql']
       
  1240         except KeyError:
       
  1241             actrql = self.cw_rset.printable_rql()
       
  1242         else:
       
  1243             self._cw.ensure_ro_rql(actrql)
       
  1244         displaycols = self.displaycols(displaycols, headers)
       
  1245         if displayactions is None and 'displayactions' in self._cw.form:
       
  1246             displayactions = True
       
  1247         if divid is None and 'divid' in self._cw.form:
       
  1248             divid = self._cw.form['divid']
       
  1249         self.w(u'<div class="section">')
       
  1250         if not title and 'title' in self._cw.form:
       
  1251             # pop title so it's not displayed by the table view as well
       
  1252             title = self._cw.form.pop('title')
       
  1253         if title:
       
  1254             self.w(u'<h2>%s</h2>\n' % title)
       
  1255         if mainindex is None:
       
  1256             mainindex = self.main_var_index()
       
  1257         if mainindex is not None:
       
  1258             actions = self.form_filter(divid, displaycols, displayactions,
       
  1259                                        displayfilter=True, paginate=paginate,
       
  1260                                        hidden=True)
       
  1261         else:
       
  1262             actions = ()
       
  1263         if not subvid and 'subvid' in self._cw.form:
       
  1264             subvid = self._cw.form.pop('subvid')
       
  1265         self._cw.view('table', self._cw.execute(actrql),
       
  1266                       'noresult', w=self.w, displayfilter=False, subvid=subvid,
       
  1267                       displayactions=displayactions, displaycols=displaycols,
       
  1268                       actions=actions, headers=headers, divid=divid)
       
  1269         self.w(u'</div>\n')
       
  1270 
       
  1271 
       
  1272 class EditableInitialTableTableView(InitialTableView):
       
  1273     __regid__ = 'editable-initialtable'
       
  1274     finalview = 'editable-final'
       
  1275 
       
  1276 
       
  1277 @add_metaclass(class_deprecated)
       
  1278 class EntityAttributesTableView(EntityView):
       
  1279     """This table displays entity attributes in a table and allow to set a
       
  1280     specific method to help building cell content for each attribute as well as
       
  1281     column header.
       
  1282 
       
  1283     Table will render entity cell by using the appropriate build_COLNAME_cell
       
  1284     methods if defined otherwise cell content will be entity.COLNAME.
       
  1285 
       
  1286     Table will render column header using the method header_for_COLNAME if
       
  1287     defined otherwise COLNAME will be used.
       
  1288     """
       
  1289     __deprecation_warning__ = '[3.14] %(cls)s is deprecated'
       
  1290     __abstract__ = True
       
  1291     columns = ()
       
  1292     table_css = "listing"
       
  1293     css_files = ()
       
  1294 
       
  1295     def call(self, columns=None):
       
  1296         if self.css_files:
       
  1297             self._cw.add_css(self.css_files)
       
  1298         _ = self._cw._
       
  1299         self.columns = columns or self.columns
       
  1300         sample = self.cw_rset.get_entity(0, 0)
       
  1301         self.w(u'<table class="%s">' % self.table_css)
       
  1302         self.table_header(sample)
       
  1303         self.w(u'<tbody>')
       
  1304         for row in range(self.cw_rset.rowcount):
       
  1305             self.cell_call(row=row, col=0)
       
  1306         self.w(u'</tbody>')
       
  1307         self.w(u'</table>')
       
  1308 
       
  1309     def cell_call(self, row, col):
       
  1310         _ = self._cw._
       
  1311         entity = self.cw_rset.get_entity(row, col)
       
  1312         entity.complete()
       
  1313         infos = {}
       
  1314         for col in self.columns:
       
  1315             meth = getattr(self, 'build_%s_cell' % col, None)
       
  1316             # find the build method or try to find matching attribute
       
  1317             if meth:
       
  1318                 content = meth(entity)
       
  1319             else:
       
  1320                 content = entity.printable_value(col)
       
  1321             infos[col] = content
       
  1322         self.w(u"""<tr onmouseover="$(this).addClass('highlighted');"
       
  1323             onmouseout="$(this).removeClass('highlighted')">""")
       
  1324         line = u''.join(u'<td>%%(%s)s</td>' % col for col in self.columns)
       
  1325         self.w(line % infos)
       
  1326         self.w(u'</tr>\n')
       
  1327 
       
  1328     def table_header(self, sample):
       
  1329         """builds the table's header"""
       
  1330         self.w(u'<thead><tr>')
       
  1331         for column in self.columns:
       
  1332             meth = getattr(self, 'header_for_%s' % column, None)
       
  1333             if meth:
       
  1334                 colname = meth(sample)
       
  1335             else:
       
  1336                 colname = self._cw._(column)
       
  1337             self.w(u'<th>%s</th>' % xml_escape(colname))
       
  1338         self.w(u'</tr></thead>\n')