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