--- a/web/views/tableview.py Tue Feb 14 15:14:22 2012 +0100
+++ b/web/views/tableview.py Tue Jul 10 15:07:23 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -15,29 +15,917 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""generic table view, including filtering abilities using facets"""
+"""This module contains table views, with the following features that may be
+provided (depending on the used implementation):
+
+* facets filtering
+* pagination
+* actions menu
+* properly sortable content
+* odd/row/hover line styles
+
+The three main implementation are described below. Each implementation is
+suitable for a particular case, but they each attempt to display tables that
+looks similar.
+
+.. autoclass:: cubicweb.web.views.tableview.RsetTableView
+ :members:
+
+.. autoclass:: cubicweb.web.views.tableview.EntityTableView
+ :members:
+
+.. autoclass:: cubicweb.web.views.pyviews.PyValTableView
+ :members:
+
+All those classes are rendered using a *layout*:
+
+.. autoclass:: cubicweb.web.views.tableview.TableLayout
+ :members:
+
+There is by default only on table layout, using the 'table_layout' identifier,
+that is referenced by table views
+:attr:`cubicweb.web.views.tableview.TableMixIn.layout_id`. If you want to
+customize the look and feel of your table, you can either replace the default
+one by yours, having multiple variants with proper selectors, or change the
+`layout_id` identifier of your table to use your table specific implementation.
+
+Notice you can gives options to the layout using a `layout_args` dictionary on
+your class.
+
+If you can still find a view that suit your needs, you should take a look at the
+class below that is the common abstract base class for the three views defined
+above and implements you own class.
+
+.. autoclass:: cubicweb.web.views.tableview.TableMixIn
+ :members:
+"""
__docformat__ = "restructuredtext en"
_ = unicode
+from warnings import warn
+from copy import copy
+from types import MethodType
+
from logilab.mtconverter import xml_escape
+from logilab.common.decorators import cachedproperty
+from logilab.common.deprecation import class_deprecated
from cubicweb import NoSelectableObject, tags
-from cubicweb.selectors import nonempty_rset
-from cubicweb.utils import make_uid, js_dumps, JSString
+from cubicweb.selectors import yes, nonempty_rset, match_kwargs, objectify_selector
+from cubicweb.schema import display_name
+from cubicweb.utils import make_uid, js_dumps, JSString, UStringIO
+from cubicweb.uilib import toggle_action, limitsize, htmlescape, sgml_attributes, domid
from cubicweb.view import EntityView, AnyRsetView
-from cubicweb.uilib import toggle_action, limitsize, htmlescape
-from cubicweb.web import jsonize, component, facet
+from cubicweb.web import jsonize, component
from cubicweb.web.htmlwidgets import (TableWidget, TableColumn, MenuWidget,
PopupBoxMenu)
+@objectify_selector
+def unreloadable_table(cls, req, rset=None,
+ displaycols=None, headers=None, cellvids=None,
+ paginate=False, displayactions=False, displayfilter=False,
+ **kwargs):
+ # one may wish to specify one of headers/displaycols/cellvids as long as he
+ # doesn't want pagination nor actions nor facets
+ if not kwargs and (displaycols or headers or cellvids) and not (
+ displayfilter or displayactions or paginate):
+ return 1
+ return 0
+
+
+class TableLayout(component.Component):
+ """The default layout for table. When `render` is called, this will use
+ the API described on :class:`TableMixIn` to feed the generated table.
+
+ This layout behaviour may be customized using the following attributes /
+ selection arguments:
+
+ * `cssclass`, a string that should be used as HTML class attribute. Default
+ to "listing".
+
+ * `needs_css`, the CSS files that should be used together with this
+ table. Default to ('cubicweb.tablesorter.css', 'cubicweb.tableview.css').
+
+ * `needs_js`, the Javascript files that should be used together with this
+ table. Default to ('jquery.tablesorter.js',)
+
+ * `display_filter`, tells if the facets filter should be displayed when
+ possible. Allowed values are:
+ - `None`, don't display it
+ - 'top', display it above the table
+ - 'bottom', display it below the table
+
+ * `display_actions`, tells if a menu for available actions should be
+ displayed when possible (see two following options). Allowed values are:
+ - `None`, don't display it
+ - 'top', display it above the table
+ - 'bottom', display it below the table
+
+ * `hide_filter`, when true (the default), facets filter will be hidden by
+ default, with an action in the actions menu allowing to show / hide it.
+
+ * `show_all_option`, when true, a *show all results* link will be displayed
+ below the navigation component.
+
+ * `add_view_actions`, when true, actions returned by view.table_actions()
+ will be included in the actions menu.
+
+ * `header_column_idx`, if not `None`, should be a colum index or a set of
+ column index where <th> tags should be generated instead of <td>
+ """ #'# make emacs happier
+ __regid__ = 'table_layout'
+ cssclass = "listing"
+ needs_css = ('cubicweb.tableview.css',)
+ needs_js = ()
+ display_filter = None # None / 'top' / 'bottom'
+ display_actions = 'top' # None / 'top' / 'bottom'
+ hide_filter = True
+ show_all_option = True # make navcomp generate a 'show all' results link
+ add_view_actions = False
+ header_column_idx = None
+ enable_sorting = True
+ sortvalue_limit = 10
+ tablesorter_settings = {
+ 'textExtraction': JSString('cw.sortValueExtraction'),
+ 'selectorHeaders': "thead tr:first th[class='sortable']", # only plug on the first row
+ }
+
+ def _setup_tablesorter(self, divid):
+ self._cw.add_css('cubicweb.tablesorter.css')
+ self._cw.add_js('jquery.tablesorter.js')
+ self._cw.add_onload('''$(document).ready(function() {
+ $("#%s table").tablesorter(%s);
+});''' % (divid, js_dumps(self.tablesorter_settings)))
+
+ def __init__(self, req, view, **kwargs):
+ super(TableLayout, self).__init__(req, **kwargs)
+ for key, val in self.cw_extra_kwargs.items():
+ if hasattr(self.__class__, key) and not key[0] == '_':
+ setattr(self, key, val)
+ self.cw_extra_kwargs.pop(key)
+ self.view = view
+ if self.header_column_idx is None:
+ self.header_column_idx = frozenset()
+ elif isinstance(self.header_column_idx, int):
+ self.header_column_idx = frozenset( (self.header_column_idx,) )
+
+ @cachedproperty
+ def initial_load(self):
+ """We detect a bit heuristically if we are built for the first time.
+ or from subsequent calls by the form filter or by the pagination hooks.
+ """
+ form = self._cw.form
+ return 'fromformfilter' not in form and '__fromnavigation' not in form
+
+ def render(self, w, **kwargs):
+ assert self.display_filter in (None, 'top', 'bottom'), self.display_filter
+ if self.needs_css:
+ self._cw.add_css(self.needs_css)
+ if self.needs_js:
+ self._cw.add_js(self.needs_js)
+ if self.enable_sorting:
+ self._setup_tablesorter(self.view.domid)
+ # Notice facets form must be rendered **outside** the main div as it
+ # shouldn't be rendered on ajax call subsequent to facet restriction
+ # (hence the 'fromformfilter' parameter added by the form
+ generate_form = self.initial_load
+ if self.display_filter and generate_form:
+ facetsform = self.view.facets_form()
+ else:
+ facetsform = None
+ if facetsform and self.display_filter == 'top':
+ cssclass = u'hidden' if self.hide_filter else u''
+ facetsform.render(w, vid=self.view.__regid__, cssclass=cssclass,
+ divid=self.view.domid)
+ actions = []
+ if self.add_view_actions:
+ actions = self.view.table_actions()
+ if self.display_filter and self.hide_filter and (facetsform or not generate_form):
+ actions += self.show_hide_filter_actions(not generate_form)
+ self.render_table(w, actions, self.view.paginable)
+ if facetsform and self.display_filter == 'bottom':
+ cssclass = u'hidden' if self.hide_filter else u''
+ facetsform.render(w, vid=self.view.__regid__, cssclass=cssclass,
+ divid=self.view.domid)
+
+ def render_table_headers(self, w, colrenderers):
+ w(u'<thead><tr>')
+ for colrenderer in colrenderers:
+ if colrenderer.sortable:
+ w(u'<th class="sortable">')
+ else:
+ w(u'<th>')
+ colrenderer.render_header(w)
+ w(u'</th>')
+ w(u'</tr></thead>\n')
+
+ def render_table_body(self, w, colrenderers):
+ w(u'<tbody>')
+ for rownum in xrange(self.view.table_size):
+ self.render_row(w, rownum, colrenderers)
+ w(u'</tbody>')
+
+ def render_table(self, w, actions, paginate):
+ view = self.view
+ divid = view.domid
+ if divid is not None:
+ w(u'<div id="%s">' % divid)
+ else:
+ assert not (actions or paginate)
+ nav_html = UStringIO()
+ if paginate:
+ view.paginate(w=nav_html.write, show_all_option=self.show_all_option)
+ w(nav_html.getvalue())
+ if actions and self.display_actions == 'top':
+ self.render_actions(w, actions)
+ colrenderers = view.build_column_renderers()
+ attrs = self.table_attributes()
+ w(u'<table %s>' % sgml_attributes(attrs))
+ if self.view.has_headers:
+ self.render_table_headers(w, colrenderers)
+ self.render_table_body(w, colrenderers)
+ w(u'</table>')
+ if actions and self.display_actions == 'bottom':
+ self.render_actions(w, actions)
+ w(nav_html.getvalue())
+ if divid is not None:
+ w(u'</div>')
+
+ def table_attributes(self):
+ return {'class': self.cssclass}
+
+ def render_row(self, w, rownum, renderers):
+ attrs = self.row_attributes(rownum)
+ w(u'<tr %s>' % sgml_attributes(attrs))
+ for colnum, renderer in enumerate(renderers):
+ self.render_cell(w, rownum, colnum, renderer)
+ w(u'</tr>\n')
+
+ def row_attributes(self, rownum):
+ return {'class': 'odd' if (rownum%2==1) else 'even',
+ 'onmouseover': '$(this).addClass("highlighted");',
+ 'onmouseout': '$(this).removeClass("highlighted")'}
+
+ def render_cell(self, w, rownum, colnum, renderer):
+ attrs = self.cell_attributes(rownum, colnum, renderer)
+ if colnum in self.header_column_idx:
+ tag = u'th'
+ else:
+ tag = u'td'
+ w(u'<%s %s>' % (tag, sgml_attributes(attrs)))
+ renderer.render_cell(w, rownum)
+ w(u'</%s>' % tag)
+
+ def cell_attributes(self, rownum, _colnum, renderer):
+ attrs = renderer.attributes.copy()
+ if renderer.sortable:
+ sortvalue = renderer.sortvalue(rownum)
+ if isinstance(sortvalue, basestring):
+ sortvalue = sortvalue[:self.sortvalue_limit]
+ if sortvalue is not None:
+ attrs[u'cubicweb:sortvalue'] = js_dumps(sortvalue)
+ return attrs
+
+ def render_actions(self, w, actions):
+ box = MenuWidget('', '', _class='tableActionsBox', islist=False)
+ label = tags.span(self._cw._('action menu'))
+ menu = PopupBoxMenu(label, isitem=False, link_class='actionsBox',
+ ident='%sActions' % self.view.domid)
+ box.append(menu)
+ for action in actions:
+ menu.append(action)
+ box.render(w=w)
+ w(u'<div class="clear"></div>')
+
+ def show_hide_filter_actions(self, currentlydisplayed=False):
+ divid = self.view.domid
+ showhide = u';'.join(toggle_action('%s%s' % (divid, what))[11:]
+ for what in ('Form', 'Show', 'Hide', 'Actions'))
+ showhide = 'javascript:' + showhide
+ self._cw.add_onload(u'''\
+$(document).ready(function() {
+ if ($('#%(id)sForm[class=\"hidden\"]').length) {
+ $('#%(id)sHide').attr('class', 'hidden');
+ } else {
+ $('#%(id)sShow').attr('class', 'hidden');
+ }
+});''' % {'id': divid})
+ showlabel = self._cw._('show filter form')
+ hidelabel = self._cw._('hide filter form')
+ return [component.Link(showhide, showlabel, id='%sShow' % divid),
+ component.Link(showhide, hidelabel, id='%sHide' % divid)]
+
+
+class AbstractColumnRenderer(object):
+ """Abstract base class for column renderer. Interface of a column renderer follows:
+
+ .. automethod:: cubicweb.web.views.tableview.AbstractColumnRenderer.bind
+ .. automethod:: cubicweb.web.views.tableview.AbstractColumnRenderer.render_header
+ .. automethod:: cubicweb.web.views.tableview.AbstractColumnRenderer.render_cell
+ .. automethod:: cubicweb.web.views.tableview.AbstractColumnRenderer.sortvalue
+
+ Attributes on this base class are:
+
+ :attr: `header`, the column header. If None, default to `_(colid)`
+ :attr: `addcount`, if True, add the table size in parenthezis beside the header
+ :attr: `trheader`, should the header be translated
+ :attr: `escapeheader`, should the header be xml_escaped
+ :attr: `sortable`, tell if the column is sortable
+ :attr: `view`, the table view
+ :attr: `_cw`, the request object
+ :attr: `colid`, the column identifier
+ :attr: `attributes`, dictionary of attributes to put on the HTML tag when
+ the cell is rendered
+ """ #'# make emacs
+ attributes = {}
+ empty_cell_content = u' '
+
+ def __init__(self, header=None, addcount=False, trheader=True,
+ escapeheader=True, sortable=True):
+ self.header = header
+ self.trheader = trheader
+ self.escapeheader = escapeheader
+ self.addcount = addcount
+ self.sortable = sortable
+ self.view = None
+ self._cw = None
+ self.colid = None
+
+ def __str__(self):
+ return '<%s.%s (column %s)>' % (self.view.__class__.__name__,
+ self.__class__.__name__,
+ self.colid)
+
+ def bind(self, view, colid):
+ """Bind the column renderer to its view. This is where `_cw`, `view`,
+ `colid` are set and the method to override if you want to add more
+ view/request depending attributes on your column render.
+ """
+ self.view = view
+ self._cw = view._cw
+ self.colid = colid
+
+ def copy(self):
+ assert self.view is None
+ return copy(self)
+
+ def default_header(self):
+ """Return header for this column if one has not been specified."""
+ return self._cw._(self.colid)
+
+ def render_header(self, w):
+ """Write label for the specified column by calling w()."""
+ header = self.header
+ if header is None:
+ header = self.default_header()
+ elif self.trheader and header:
+ header = self._cw._(header)
+ if self.addcount:
+ header = '%s (%s)' % (header, self.view.table_size)
+ if header:
+ if self.escapeheader:
+ header = xml_escape(header)
+ else:
+ header = self.empty_cell_content
+ if self.sortable:
+ header = tags.span(
+ header, escapecontent=False,
+ title=self._cw._('Click to sort on this column'))
+ w(header)
+
+ def render_cell(self, w, rownum):
+ """Write value for the specified cell by calling w().
+
+ :param `rownum`: the row number in the table
+ """
+ raise NotImplementedError()
+
+ def sortvalue(self, _rownum):
+ """Return typed value to be used for sorting on the specified column.
+
+ :param `rownum`: the row number in the table
+ """
+ return None
+
+
+class TableMixIn(component.LayoutableMixIn):
+ """Abstract mix-in class for layout based tables.
+
+ This default implementation's call method simply delegate to
+ meth:`layout_render` that will select the renderer whose identifier is given
+ by the :attr:`layout_id` attribute.
+
+ Then it provides some default implementation for various parts of the API
+ used by that layout.
+
+ Abstract method you will have to override is:
+
+ .. automethod:: build_column_renderers
+
+ You may also want to overridde:
+
+ .. autoattribute:: cubicweb.web.views.tableview.TableMixIn.table_size
+
+ The :attr:`has_headers` boolean attribute tells if the table has some
+ headers to be displayed. Default to `True`.
+ """
+ __abstract__ = True
+ # table layout to use
+ layout_id = 'table_layout'
+ # true if the table has some headers
+ has_headers = True
+ # dictionary {colid : column renderer}
+ column_renderers = {}
+ # default renderer class to use when no renderer specified for the column
+ default_column_renderer_class = None
+ # default layout handles inner pagination
+ handle_pagination = True
+
+ def call(self, **kwargs):
+ self.layout_render(self.w)
+
+ def column_renderer(self, colid, *args, **kwargs):
+ """Return a column renderer for column of the given id."""
+ try:
+ crenderer = self.column_renderers[colid]
+ except KeyError:
+ crenderer = self.default_column_renderer_class(*args, **kwargs)
+ crenderer.bind(self, colid)
+ return crenderer
+
+ # layout callbacks #########################################################
+
+ def facets_form(self, **kwargs):# XXX extracted from jqplot cube
+ try:
+ return self._cw.vreg['views'].select(
+ 'facet.filtertable', self._cw, rset=self.cw_rset, view=self,
+ **kwargs)
+ except NoSelectableObject:
+ return None
+
+ @cachedproperty
+ def domid(self):
+ return self._cw.form.get('divid') or domid('%s-%s' % (self.__regid__, make_uid()))
+
+ @property
+ def table_size(self):
+ """Return the number of rows (header excluded) to be displayed.
+
+ By default return the number of rows in the view's result set. If your
+ table isn't reult set based, override this method.
+ """
+ return self.cw_rset.rowcount
+
+ def build_column_renderers(self):
+ """Return a list of column renderers, one for each column to be
+ rendered. Prototype of a column renderer is described below:
+
+ .. autoclass:: cubicweb.web.views.tableview.AbstractColumnRenderer
+ """
+ raise NotImplementedError()
+
+ def table_actions(self):
+ """Return a list of actions (:class:`~cubicweb.web.component.Link`) that
+ match the view's result set, and return those in the 'mainactions'
+ category.
+ """
+ req = self._cw
+ actions = []
+ actionsbycat = req.vreg['actions'].possible_actions(req, self.cw_rset)
+ for action in actionsbycat.get('mainactions', ()):
+ for action in action.actual_actions():
+ actions.append(component.Link(action.url(), req._(action.title),
+ klass=action.html_class()) )
+ return actions
+
+ # interaction with navigation component ####################################
+
+ def page_navigation_url(self, navcomp, _path, params):
+ params['divid'] = self.domid
+ params['vid'] = self.__regid__
+ return navcomp.ajax_page_url(**params)
+
+
+class RsetTableColRenderer(AbstractColumnRenderer):
+ """Default renderer for :class:`RsetTableView`."""
+
+ def __init__(self, cellvid, **kwargs):
+ super(RsetTableColRenderer, self).__init__(**kwargs)
+ self.cellvid = cellvid
+
+ def bind(self, view, colid):
+ super(RsetTableColRenderer, self).bind(view, colid)
+ self.cw_rset = view.cw_rset
+ def render_cell(self, w, rownum):
+ self._cw.view(self.cellvid, self.cw_rset, 'empty-cell',
+ row=rownum, col=self.colid, w=w)
+
+ # limit value's length as much as possible (e.g. by returning the 10 first
+ # characters of a string)
+ def sortvalue(self, rownum):
+ colid = self.colid
+ val = self.cw_rset[rownum][colid]
+ if val is None:
+ return u''
+ etype = self.cw_rset.description[rownum][colid]
+ if etype is None:
+ return u''
+ if self._cw.vreg.schema.eschema(etype).final:
+ entity, rtype = self.cw_rset.related_entity(rownum, colid)
+ if entity is None:
+ return val # remove_html_tags() ?
+ return entity.sortvalue(rtype)
+ entity = self.cw_rset.get_entity(rownum, colid)
+ return entity.sortvalue()
+
+
+class RsetTableView(TableMixIn, AnyRsetView):
+ """This table view accepts any non-empty rset. It uses introspection on the
+ result set to compute column names and the proper way to display the cells.
+
+ It is highly configurable and accepts a wealth of options, but take care to
+ check what you're trying to achieve wouldn't be a job for the
+ :class:`EntityTableView`. Basically the question is: does this view should
+ be tied to the result set query's shape or no? If yes, than you're fine. If
+ no, you should take a look at the other table implementation.
+
+ The following class attributes may be used to control the table:
+
+ * `finalvid`, a view identifier that should be called on final entities
+ (e.g. attribute values). Default to 'final'.
+
+ * `nonfinalvid`, a view identifier that should be called on
+ entities. Default to 'incontext'.
+
+ * `displaycols`, if not `None`, should be a list of rset's columns to be
+ displayed.
+
+ * `headers`, if not `None`, should be a list of headers for the table's
+ columns. `None` values in the list will be replaced by computed column
+ names.
+
+ * `cellvids`, if not `None`, should be a dictionary with table column index
+ as key and a view identifier as value, telling the view that should be
+ used in the given column.
+
+ Notice `displaycols`, `headers` and `cellvids` may be specified at selection
+ time but then the table won't have pagination and shouldn't be configured to
+ display the facets filter nor actions (as they wouldn't behave as expected).
+
+ This table class use the :class:`RsetTableColRenderer` as default column
+ renderer.
+
+ .. autoclass:: RsetTableColRenderer
+ """ #'# make emacs happier
+ __regid__ = 'table'
+ # selector trick for bw compath with the former :class:TableView
+ __select__ = AnyRsetView.__select__ & (~match_kwargs(
+ 'title', 'subvid', 'displayfilter', 'headers', 'displaycols',
+ 'displayactions', 'actions', 'divid', 'cellvids', 'cellattrs',
+ 'mainindex', 'paginate', 'page_size', mode='any')
+ | unreloadable_table())
+ title = _('table')
+ # additional configuration parameters
+ finalvid = 'final'
+ nonfinalvid = 'incontext'
+ displaycols = None
+ headers = None
+ cellvids = None
+ default_column_renderer_class = RsetTableColRenderer
+
+ def linkable(self):
+ # specific subclasses of this view usually don't want to be linkable
+ # since they depends on a particular shape (being linkable meaning view
+ # may be listed in possible views
+ return self.__regid__ == 'table'
+
+ def call(self, headers=None, displaycols=None, cellvids=None,
+ paginate=None, **kwargs):
+ if self.headers:
+ self.headers = [h and self._cw._(h) for h in self.headers]
+ if (headers or displaycols or cellvids or paginate):
+ if headers is not None:
+ self.headers = headers
+ if displaycols is not None:
+ self.displaycols = displaycols
+ if cellvids is not None:
+ self.cellvids = cellvids
+ if paginate is not None:
+ self.paginable = paginate
+ if kwargs:
+ # old table view arguments that we can safely ignore thanks to
+ # selectors
+ if len(kwargs) > 1:
+ msg = '[3.14] %s arguments are deprecated' % ', '.join(kwargs)
+ else:
+ msg = '[3.14] %s argument is deprecated' % ', '.join(kwargs)
+ warn(msg, DeprecationWarning, stacklevel=2)
+ self.layout_render(self.w)
+
+ def main_var_index(self):
+ """returns the index of the first non-attribute variable among the RQL
+ selected variables
+ """
+ eschema = self._cw.vreg.schema.eschema
+ for i, etype in enumerate(self.cw_rset.description[0]):
+ if not eschema(etype).final:
+ return i
+ return None
+
+ # layout callbacks #########################################################
+
+ @property
+ def table_size(self):
+ """return the number of rows (header excluded) to be displayed"""
+ return self.cw_rset.rowcount
+
+ def build_column_renderers(self):
+ headers = self.headers
+ # compute displayed columns
+ if self.displaycols is None:
+ if headers is not None:
+ displaycols = range(len(headers))
+ else:
+ rqlst = self.cw_rset.syntax_tree()
+ displaycols = range(len(rqlst.children[0].selection))
+ else:
+ displaycols = self.displaycols
+ # compute table headers
+ main_var_index = self.main_var_index()
+ computed_titles = self.columns_labels(main_var_index)
+ # compute build renderers
+ cellvids = self.cellvids
+ renderers = []
+ for colnum, colid in enumerate(displaycols):
+ addcount = False
+ # compute column header
+ title = None
+ if headers is not None:
+ title = headers[colnum]
+ if title is None:
+ title = computed_titles[colid]
+ if colid == main_var_index:
+ addcount = True
+ # compute cell vid for the column
+ if cellvids is not None and colnum in cellvids:
+ cellvid = cellvids[colnum]
+ else:
+ coltype = self.cw_rset.description[0][colid]
+ if coltype is not None and self._cw.vreg.schema.eschema(coltype).final:
+ cellvid = self.finalvid
+ else:
+ cellvid = self.nonfinalvid
+ # get renderer
+ renderer = self.column_renderer(colid, header=title, trheader=False,
+ addcount=addcount, cellvid=cellvid)
+ renderers.append(renderer)
+ return renderers
+
+
+class EntityTableColRenderer(AbstractColumnRenderer):
+ """Default column renderer for :class:`EntityTableView`.
+
+ You may use the :meth:`entity` method to retrieve the main entity for a
+ given row number.
+
+ .. automethod:: cubicweb.web.views.tableview.EntityTableColRenderer.entity
+ .. automethod:: cubicweb.web.views.tableview.EntityTableColRenderer.render_entity
+ .. automethod:: cubicweb.web.views.tableview.EntityTableColRenderer.entity_sortvalue
+ """
+ def __init__(self, renderfunc=None, sortfunc=None, sortable=None, **kwargs):
+ if renderfunc is None:
+ renderfunc = self.render_entity
+ # if renderfunc nor sortfunc nor sortable specified, column will be
+ # sortable using the default implementation.
+ if sortable is None:
+ sortable = True
+ # no sortfunc given but asked to be sortable: use the default sort
+ # method. Sub-class may set `entity_sortvalue` to None if they don't
+ # support sorting.
+ if sortfunc is None and sortable:
+ sortfunc = self.entity_sortvalue
+ # at this point `sortable` may still be unspecified while `sortfunc` is
+ # sure to be set to someting else than None if the column is sortable.
+ sortable = sortfunc is not None
+ super(EntityTableColRenderer, self).__init__(sortable=sortable, **kwargs)
+ self.renderfunc = renderfunc
+ self.sortfunc = sortfunc
+
+ def copy(self):
+ assert self.view is None
+ # copy of attribute referencing a method doesn't work with python < 2.7
+ renderfunc = self.__dict__.pop('renderfunc')
+ sortfunc = self.__dict__.pop('sortfunc')
+ try:
+ acopy = copy(self)
+ for aname, member in[('renderfunc', renderfunc),
+ ('sortfunc', sortfunc)]:
+ if isinstance(member, MethodType):
+ member = MethodType(member.im_func, acopy, acopy.__class__)
+ setattr(acopy, aname, member)
+ return acopy
+ finally:
+ self.renderfunc = renderfunc
+ self.sortfunc = sortfunc
+
+ def render_cell(self, w, rownum):
+ entity = self.entity(rownum)
+ if entity is None:
+ w(self.empty_cell_content)
+ else:
+ self.renderfunc(w, entity)
+
+ def sortvalue(self, rownum):
+ entity = self.entity(rownum)
+ if entity is None:
+ return None
+ else:
+ return self.sortfunc(entity)
+
+ def entity(self, rownum):
+ """Convenience method returning the table's main entity."""
+ return self.view.entity(rownum)
+
+ def render_entity(self, w, entity):
+ """Sort value if `renderfunc` nor `sortfunc` specified at
+ initialization.
+
+ This default implementation consider column id is an entity attribute
+ and print its value.
+ """
+ w(entity.printable_value(self.colid))
+
+ def entity_sortvalue(self, entity):
+ """Cell rendering implementation if `renderfunc` nor `sortfunc`
+ specified at initialization.
+
+ This default implementation consider column id is an entity attribute
+ and return its sort value by calling `entity.sortvalue(colid)`.
+ """
+ return entity.sortvalue(self.colid)
+
+
+class MainEntityColRenderer(EntityTableColRenderer):
+ """Renderer to be used for the column displaying the 'main entity' of a
+ :class:`EntityTableView`.
+
+ By default display it using the 'incontext' view. You may specify another
+ view identifier using the `vid` argument.
+
+ If header not specified, it would be built using entity types in the main
+ column.
+ """
+ def __init__(self, vid='incontext', addcount=True, **kwargs):
+ super(MainEntityColRenderer, self).__init__(addcount=addcount, **kwargs)
+ self.vid = vid
+
+ def default_header(self):
+ view = self.view
+ if len(view.cw_rset) > 1:
+ suffix = '_plural'
+ else:
+ suffix = ''
+ return u', '.join(self._cw.__(et + suffix)
+ for et in view.cw_rset.column_types(view.cw_col or 0))
+
+ def render_entity(self, w, entity):
+ entity.view(self.vid, w=w)
+
+ def entity_sortvalue(self, entity):
+ return entity.sortvalue()
+
+
+class RelatedEntityColRenderer(MainEntityColRenderer):
+ """Renderer to be used for column displaying an entity related the 'main
+ entity' of a :class:`EntityTableView`.
+
+ By default display it using the 'incontext' view. You may specify another
+ view identifier using the `vid` argument.
+
+ If header not specified, it would be built by translating the column id.
+ """
+ def __init__(self, getrelated, addcount=False, **kwargs):
+ super(RelatedEntityColRenderer, self).__init__(addcount=addcount, **kwargs)
+ self.getrelated = getrelated
+
+ def entity(self, rownum):
+ entity = super(RelatedEntityColRenderer, self).entity(rownum)
+ return self.getrelated(entity)
+
+ def default_header(self):
+ return self._cw._(self.colid)
+
+
+class RelationColRenderer(EntityTableColRenderer):
+ """Renderer to be used for column displaying a list of entities related the
+ 'main entity' of a :class:`EntityTableView`. By default, the main entity is
+ considered as the subject of the relation but you may specify otherwise
+ using the `role` argument.
+
+ By default display the related rset using the 'csv' view, using
+ 'outofcontext' sub-view for each entity. You may specify another view
+ identifier using respectivly the `vid` and `subvid` arguments.
+
+ If you specify a 'rtype view', such as 'reledit', you should add a
+ is_rtype_view=True parameter.
+
+ If header not specified, it would be built by translating the column id,
+ properly considering role.
+ """
+ def __init__(self, role='subject', vid='csv', subvid=None,
+ fallbackvid='empty-cell', is_rtype_view=False, **kwargs):
+ super(RelationColRenderer, self).__init__(**kwargs)
+ self.role = role
+ self.vid = vid
+ if subvid is None and vid in ('csv', 'list'):
+ subvid = 'outofcontext'
+ self.subvid = subvid
+ self.fallbackvid = fallbackvid
+ self.is_rtype_view = is_rtype_view
+
+ def render_entity(self, w, entity):
+ kwargs = {'w': w}
+ if self.is_rtype_view:
+ rset = None
+ kwargs['entity'] = entity
+ kwargs['rtype'] = self.colid
+ kwargs['role'] = self.role
+ else:
+ rset = entity.related(self.colid, self.role)
+ if self.subvid is not None:
+ kwargs['subvid'] = self.subvid
+ self._cw.view(self.vid, rset, self.fallbackvid, **kwargs)
+
+ def default_header(self):
+ return display_name(self._cw, self.colid, self.role)
+
+ entity_sortvalue = None # column not sortable by default
+
+
+class EntityTableView(TableMixIn, EntityView):
+ """This abstract table view is designed to be used with an
+ :class:`is_instance()` or :class:`adaptable` selector, hence doesn't depend
+ the result set shape as the :class:`TableView` does.
+
+ It will display columns that should be defined using the `columns` class
+ attribute containing a list of column ids. By default, each column is
+ renderered by :class:`EntityTableColRenderer` which consider that the column
+ id is an attribute of the table's main entity (ie the one for which the view
+ is selected).
+
+ You may wish to specify :class:`MainEntityColRenderer` or
+ :class:`RelatedEntityColRenderer` renderer for a column in the
+ :attr:`column_renderers` dictionary.
+
+ .. autoclass:: cubicweb.web.views.tableview.EntityTableColRenderer
+ .. autoclass:: cubicweb.web.views.tableview.MainEntityColRenderer
+ .. autoclass:: cubicweb.web.views.tableview.RelatedEntityColRenderer
+ .. autoclass:: cubicweb.web.views.tableview.RelationColRenderer
+ """
+ __abstract__ = True
+ default_column_renderer_class = EntityTableColRenderer
+ columns = None # to be defined in concret class
+
+ def call(self, columns=None, **kwargs):
+ if columns is not None:
+ self.columns = columns
+ self.layout_render(self.w)
+
+ @property
+ def table_size(self):
+ return self.cw_rset.rowcount
+
+ def build_column_renderers(self):
+ return [self.column_renderer(colid) for colid in self.columns]
+
+ def entity(self, rownum):
+ """Return the table's main entity"""
+ return self.cw_rset.get_entity(rownum, self.cw_col or 0)
+
+
+class EmptyCellView(AnyRsetView):
+ __regid__ = 'empty-cell'
+ __select__ = yes()
+ def call(self, **kwargs):
+ self.w(u' ')
+ cell_call = call
+
+
+################################################################################
+# DEPRECATED tables ############################################################
+################################################################################
+
+
class TableView(AnyRsetView):
"""The table view accepts any non-empty rset. It uses introspection on the
result set to compute column names and the proper way to display the cells.
It is however highly configurable and accepts a wealth of options.
"""
+ __metaclass__ = class_deprecated
+ __deprecation_warning__ = '[3.14] %(cls)s is deprecated'
__regid__ = 'table'
title = _('table')
finalview = 'final'
@@ -46,8 +934,10 @@
table_column_class = TableColumn
tablesorter_settings = {
- 'textExtraction': JSString('cubicwebSortValueExtraction'),
+ 'textExtraction': JSString('cw.sortValueExtraction'),
+ 'selectorHeaders': 'thead tr:first th', # only plug on the first row
}
+ handle_pagination = True
def form_filter(self, divid, displaycols, displayactions, displayfilter,
paginate, hidden=True):
@@ -66,8 +956,12 @@
return self.show_hide_actions(divid, not hidden)
def main_var_index(self):
- """returns the index of the first non-attribute variable among the RQL
- selected variables
+ """Returns the index of the first non final variable of the rset.
+
+ Used to select the main etype to help generate accurate column headers.
+ XXX explain the concept
+
+ May return None if none is found.
"""
eschema = self._cw.vreg.schema.eschema
for i, etype in enumerate(self.cw_rset.description[0]):
@@ -96,6 +990,13 @@
});''' % (divid, js_dumps(self.tablesorter_settings)))
req.add_css(('cubicweb.tablesorter.css', 'cubicweb.tableview.css'))
+ @cachedproperty
+ def initial_load(self):
+ """We detect a bit heuristically if we are built for the first time of
+ from subsequent calls by the form filter or by the pagination hooks
+ """
+ form = self._cw.form
+ return 'fromformfilter' not in form and '__start' not in form
def call(self, title=None, subvid=None, displayfilter=None, headers=None,
displaycols=None, displayactions=None, actions=(), divid=None,
@@ -118,7 +1019,6 @@
if mainindex is None:
mainindex = self.main_var_index()
computed_labels = self.columns_labels(mainindex)
- hidden = True
if not subvid and 'subvid' in req.form:
subvid = req.form.pop('subvid')
actions = list(actions)
@@ -127,16 +1027,10 @@
else:
if displayfilter is None and req.form.get('displayfilter'):
displayfilter = True
- if req.form['displayfilter'] == 'shown':
- hidden = False
if displayactions is None and req.form.get('displayactions'):
displayactions = True
displaycols = self.displaycols(displaycols, headers)
- fromformfilter = 'fromformfilter' in req.form
- # if fromformfilter is true, this is an ajax call and we only want to
- # replace the inner div, so don't regenerate everything under the if
- # below
- if not fromformfilter:
+ if self.initial_load:
self.w(u'<div class="section">')
if not title and 'title' in req.form:
title = req.form['title']
@@ -167,11 +1061,20 @@
table.append_column(column)
table.render(self.w)
self.w(u'</div>\n')
- if not fromformfilter:
+ if self.initial_load:
self.w(u'</div>\n')
def page_navigation_url(self, navcomp, path, params):
+ """Build an url to the current view using the <navcomp> attributes
+
+ :param navcomp: a NavigationComponent to call an url method on.
+ :param path: expected to be json here ?
+ :param params: params to give to build_url method
+
+ this is called by :class:`cubiweb.web.component.NavigationComponent`
+ """
if hasattr(self, 'divid'):
+ # XXX this assert a single call
params['divid'] = self.divid
params['vid'] = self.__regid__
return navcomp.ajax_page_url(**params)
@@ -198,10 +1101,23 @@
for url, label, klass, ident in actions:
menu.append(component.Link(url, label, klass=klass, id=ident))
box.render(w=self.w)
- self.w(u'<div class="clear"/>')
+ self.w(u'<div class="clear"></div>')
def get_columns(self, computed_labels, displaycols, headers, subvid,
cellvids, cellattrs, mainindex):
+ """build columns description from various parameters
+
+ : computed_labels: columns headers computed from rset to be used if there is no headers entry
+ : displaycols: see :meth:`call`
+ : headers: explicitly define columns headers
+ : subvid: see :meth:`call`
+ : cellvids: see :meth:`call`
+ : cellattrs: see :meth:`call`
+ : mainindex: see :meth:`call`
+
+ return a list of columns description to be used by
+ :class:`~cubicweb.web.htmlwidgets.TableWidget`
+ """
columns = []
eschema = self._cw.vreg.schema.eschema
for colindex, label in enumerate(computed_labels):
@@ -209,7 +1125,9 @@
continue
# compute column header
if headers is not None:
- label = headers[displaycols.index(colindex)]
+ _label = headers[displaycols.index(colindex)]
+ if _label is not None:
+ label = _label
if colindex == mainindex and label is not None:
label += ' (%s)' % self.cw_rset.rowcount
column = self.table_column_class(label, colindex)
@@ -265,6 +1183,8 @@
class CellView(EntityView):
+ __metaclass__ = class_deprecated
+ __deprecation_warning__ = '[3.14] %(cls)s is deprecated'
__regid__ = 'cell'
__select__ = nonempty_rset()
@@ -360,6 +1280,8 @@
Table will render column header using the method header_for_COLNAME if
defined otherwise COLNAME will be used.
"""
+ __metaclass__ = class_deprecated
+ __deprecation_warning__ = '[3.14] %(cls)s is deprecated'
__abstract__ = True
columns = ()
table_css = "listing"
@@ -410,4 +1332,3 @@
self.w(u'<th>%s</th>' % xml_escape(colname))
self.w(u'</tr></thead>\n')
-