diff -r 058bb3dc685f -r 0b59724cb3f2 web/views/tableview.py --- a/web/views/tableview.py Mon Jan 04 18:40:30 2016 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1338 +0,0 @@ -# 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. -# -# CubicWeb is free software: you can redistribute it and/or modify it under the -# terms of the GNU Lesser General Public License as published by the Free -# Software Foundation, either version 2.1 of the License, or (at your option) -# any later version. -# -# CubicWeb is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License along -# with CubicWeb. If not, see . -"""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 one 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 still can't 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 implement your own class. - -.. autoclass:: cubicweb.web.views.tableview.TableMixIn - :members: -""" - -__docformat__ = "restructuredtext en" -from cubicweb import _ - -from warnings import warn -from copy import copy -from types import MethodType - -from six import string_types, add_metaclass, create_bound_method -from six.moves import range - -from logilab.mtconverter import xml_escape -from logilab.common.decorators import cachedproperty -from logilab.common.deprecation import class_deprecated -from logilab.common.registry import yes - -from cubicweb import NoSelectableObject, tags -from cubicweb.predicates import nonempty_rset, match_kwargs, objectify_predicate -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.web import jsonize, component -from cubicweb.web.htmlwidgets import (TableWidget, TableColumn, MenuWidget, - PopupBoxMenu) - - -@objectify_predicate -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 tags should be generated instead of - """ #'# 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 list(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.display_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'') - for colrenderer in colrenderers: - if colrenderer.sortable: - w(u'') - else: - w(u'') - colrenderer.render_header(w) - w(u'') - w(u'\n') - - def render_table_body(self, w, colrenderers): - w(u'') - for rownum in range(self.view.table_size): - self.render_row(w, rownum, colrenderers) - w(u'') - - def render_table(self, w, actions, paginate): - view = self.view - divid = view.domid - if divid is not None: - w(u'
' % 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'' % sgml_attributes(attrs)) - if self.view.has_headers: - self.render_table_headers(w, colrenderers) - self.render_table_body(w, colrenderers) - w(u'
') - if actions and self.display_actions == 'bottom': - self.render_actions(w, actions) - w(nav_html.getvalue()) - if divid is not None: - w(u'
') - - def table_attributes(self): - return {'class': self.cssclass} - - def render_row(self, w, rownum, renderers): - attrs = self.row_attributes(rownum) - w(u'' % sgml_attributes(attrs)) - for colnum, renderer in enumerate(renderers): - self.render_cell(w, rownum, colnum, renderer) - w(u'\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'' % tag) - - def cell_attributes(self, rownum, _colnum, renderer): - attrs = renderer.attributes.copy() - if renderer.sortable: - sortvalue = renderer.sortvalue(rownum) - if isinstance(sortvalue, string_types): - 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'
') - - 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) at 0x%x>' % (self.view.__class__.__name__, - self.__class__.__name__, - self.colid, id(self)) - - 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._cw.add_js('cubicweb.ajax.js') # for pagination - 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].copy() - 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 - return self._cw.vreg['views'].select_or_none( - 'facet.filtertable', self._cw, rset=self.cw_rset, view=self, - **kwargs) - - @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) - super(RsetTableView, self).call(**kwargs) - - 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 = list(range(len(headers))) - else: - rqlst = self.cw_rset.syntax_tree() - displaycols = list(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 = create_bound_method(member.__func__, acopy) - 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` predicate, hence doesn't depend - the result set shape as the :class:`RsetTableView` 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 ############################################################ -################################################################################ - - -@add_metaclass(class_deprecated) -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. - """ - __deprecation_warning__ = '[3.14] %(cls)s is deprecated' - __regid__ = 'table' - title = _('table') - finalview = 'final' - - table_widget_class = TableWidget - table_column_class = TableColumn - - tablesorter_settings = { - '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): - try: - filterform = self._cw.vreg['views'].select( - 'facet.filtertable', self._cw, rset=self.cw_rset) - except NoSelectableObject: - return () - vidargs = {'paginate': paginate, - 'displaycols': displaycols, - 'displayactions': displayactions, - 'displayfilter': displayfilter} - cssclass = hidden and 'hidden' or '' - filterform.render(self.w, vid=self.__regid__, divid=divid, - vidargs=vidargs, cssclass=cssclass) - return self.show_hide_actions(divid, not hidden) - - def main_var_index(self): - """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]): - try: - if not eschema(etype).final: - return i - except KeyError: # XXX possible? - continue - return None - - def displaycols(self, displaycols, headers): - if displaycols is None: - if 'displaycols' in self._cw.form: - displaycols = [int(idx) for idx in self._cw.form['displaycols']] - elif headers is not None: - displaycols = list(range(len(headers))) - else: - displaycols = list(range(len(self.cw_rset.syntax_tree().children[0].selection))) - return displaycols - - def _setup_tablesorter(self, divid): - req = self._cw - req.add_js('jquery.tablesorter.js') - req.add_onload('''$(document).ready(function() { - $("#%s table.listing").tablesorter(%s); -});''' % (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 or - 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, - cellvids=None, cellattrs=None, mainindex=None, - paginate=False, page_size=None): - """Produces a table displaying a composite query - - :param title: title added before table - :param subvid: cell view - :param displayfilter: filter that selects rows to display - :param headers: columns' titles - :param displaycols: indexes of columns to display (first column is 0) - :param displayactions: if True, display action menu - """ - req = self._cw - divid = divid or req.form.get('divid') or 'rs%s' % make_uid(id(self.cw_rset)) - self._setup_tablesorter(divid) - # compute label first since the filter form may remove some necessary - # information from the rql syntax tree - if mainindex is None: - mainindex = self.main_var_index() - computed_labels = self.columns_labels(mainindex) - if not subvid and 'subvid' in req.form: - subvid = req.form.pop('subvid') - actions = list(actions) - if mainindex is None: - displayfilter, displayactions = False, False - else: - if displayfilter is None and req.form.get('displayfilter'): - displayfilter = True - if displayactions is None and req.form.get('displayactions'): - displayactions = True - displaycols = self.displaycols(displaycols, headers) - if self.initial_load: - self.w(u'
') - if not title and 'title' in req.form: - title = req.form['title'] - if title: - self.w(u'

%s

\n' % title) - if displayfilter: - actions += self.form_filter(divid, displaycols, displayfilter, - displayactions, paginate) - elif displayfilter: - actions += self.show_hide_actions(divid, True) - self.w(u'
' % divid) - if displayactions: - actionsbycat = self._cw.vreg['actions'].possible_actions(req, self.cw_rset) - for action in actionsbycat.get('mainactions', ()): - for action in action.actual_actions(): - actions.append( (action.url(), req._(action.title), - action.html_class(), None) ) - # render actions menu - if actions: - self.render_actions(divid, actions) - # render table - if paginate: - self.divid = divid # XXX iirk (see usage in page_navigation_url) - self.paginate(page_size=page_size, show_all_option=False) - table = self.table_widget_class(self) - for column in self.get_columns(computed_labels, displaycols, headers, - subvid, cellvids, cellattrs, mainindex): - table.append_column(column) - table.render(self.w) - self.w(u'
\n') - if self.initial_load: - self.w(u'
\n') - - def page_navigation_url(self, navcomp, path, params): - """Build a URL to the current view using the attributes - - :param navcomp: a NavigationComponent to call a 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) - - def show_hide_actions(self, divid, currentlydisplayed=False): - showhide = u';'.join(toggle_action('%s%s' % (divid, what))[11:] - for what in ('Form', 'Show', 'Hide', 'Actions')) - showhide = 'javascript:' + showhide - showlabel = self._cw._('show filter form') - hidelabel = self._cw._('hide filter form') - if currentlydisplayed: - return [(showhide, showlabel, 'hidden', '%sShow' % divid), - (showhide, hidelabel, None, '%sHide' % divid)] - return [(showhide, showlabel, None, '%sShow' % divid), - (showhide, hidelabel, 'hidden', '%sHide' % divid)] - - def render_actions(self, divid, actions): - box = MenuWidget('', 'tableActionsBox', _class='', islist=False) - label = tags.img(src=self._cw.uiprops['PUCE_DOWN'], - alt=xml_escape(self._cw._('action(s) on this selection'))) - menu = PopupBoxMenu(label, isitem=False, link_class='actionsBox', - ident='%sActions' % divid) - box.append(menu) - 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'
') - - 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): - if colindex not in displaycols: - continue - # compute column header - if headers is not None: - _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) - coltype = self.cw_rset.description[0][colindex] - # compute column cell view (if coltype is None, it's a left outer - # join, use the default non final subvid) - if cellvids and colindex in cellvids: - column.append_renderer(cellvids[colindex], colindex) - elif coltype is not None and eschema(coltype).final: - column.append_renderer(self.finalview, colindex) - else: - column.append_renderer(subvid or 'incontext', colindex) - if cellattrs and colindex in cellattrs: - for name, value in cellattrs[colindex].items(): - column.add_attr(name, value) - # add column - columns.append(column) - return columns - - - def render_cell(self, cellvid, row, col, w): - self._cw.view('cell', self.cw_rset, row=row, col=col, cellvid=cellvid, w=w) - - def get_rows(self): - return self.cw_rset - - @htmlescape - @jsonize - @limitsize(10) - def sortvalue(self, row, col): - # XXX it might be interesting to try to limit value's - # length as much as possible (e.g. by returning the 10 - # first characters of a string) - val = self.cw_rset[row][col] - if val is None: - return u'' - etype = self.cw_rset.description[row][col] - if etype is None: - return u'' - if self._cw.vreg.schema.eschema(etype).final: - entity, rtype = self.cw_rset.related_entity(row, col) - if entity is None: - return val # remove_html_tags() ? - return entity.sortvalue(rtype) - entity = self.cw_rset.get_entity(row, col) - return entity.sortvalue() - - -class EditableTableView(TableView): - __regid__ = 'editable-table' - finalview = 'editable-final' - title = _('editable-table') - - -@add_metaclass(class_deprecated) -class CellView(EntityView): - __deprecation_warning__ = '[3.14] %(cls)s is deprecated' - __regid__ = 'cell' - __select__ = nonempty_rset() - - def cell_call(self, row, col, cellvid=None): - """ - :param row, col: indexes locating the cell value in view's result set - :param cellvid: cell view (defaults to 'outofcontext') - """ - etype, val = self.cw_rset.description[row][col], self.cw_rset[row][col] - if etype is None or not self._cw.vreg.schema.eschema(etype).final: - if val is None: - # This is usually caused by a left outer join and in that case, - # regular views will most certainly fail if they don't have - # a real eid - # XXX if cellvid is e.g. reledit, we may wanna call it anyway - self.w(u' ') - else: - self.wview(cellvid or 'outofcontext', self.cw_rset, row=row, col=col) - else: - # XXX why do we need a fallback view here? - self.wview(cellvid or 'final', self.cw_rset, 'null', row=row, col=col) - - -class InitialTableView(TableView): - """same display as table view but consider two rql queries : - - * the default query (ie `rql` form parameter), which is only used to select - this view and to build the filter form. This query should have the same - structure as the actual without actual restriction (but link to - restriction variables) and usually with a limit for efficiency (limit set - to 2 is advised) - - * the actual query (`actualrql` form parameter) whose results will be - displayed with default restrictions set - """ - __regid__ = 'initialtable' - __select__ = nonempty_rset() - # should not be displayed in possible view since it expects some specific - # parameters - title = None - - def call(self, title=None, subvid=None, headers=None, divid=None, - paginate=False, displaycols=None, displayactions=None, - mainindex=None): - """Dumps a table displaying a composite query""" - try: - actrql = self._cw.form['actualrql'] - except KeyError: - actrql = self.cw_rset.printable_rql() - else: - self._cw.ensure_ro_rql(actrql) - displaycols = self.displaycols(displaycols, headers) - if displayactions is None and 'displayactions' in self._cw.form: - displayactions = True - if divid is None and 'divid' in self._cw.form: - divid = self._cw.form['divid'] - self.w(u'
') - if not title and 'title' in self._cw.form: - # pop title so it's not displayed by the table view as well - title = self._cw.form.pop('title') - if title: - self.w(u'

%s

\n' % title) - if mainindex is None: - mainindex = self.main_var_index() - if mainindex is not None: - actions = self.form_filter(divid, displaycols, displayactions, - displayfilter=True, paginate=paginate, - hidden=True) - else: - actions = () - if not subvid and 'subvid' in self._cw.form: - subvid = self._cw.form.pop('subvid') - self._cw.view('table', self._cw.execute(actrql), - 'noresult', w=self.w, displayfilter=False, subvid=subvid, - displayactions=displayactions, displaycols=displaycols, - actions=actions, headers=headers, divid=divid) - self.w(u'
\n') - - -class EditableInitialTableTableView(InitialTableView): - __regid__ = 'editable-initialtable' - finalview = 'editable-final' - - -@add_metaclass(class_deprecated) -class EntityAttributesTableView(EntityView): - """This table displays entity attributes in a table and allow to set a - specific method to help building cell content for each attribute as well as - column header. - - Table will render entity cell by using the appropriate build_COLNAME_cell - methods if defined otherwise cell content will be entity.COLNAME. - - Table will render column header using the method header_for_COLNAME if - defined otherwise COLNAME will be used. - """ - __deprecation_warning__ = '[3.14] %(cls)s is deprecated' - __abstract__ = True - columns = () - table_css = "listing" - css_files = () - - def call(self, columns=None): - if self.css_files: - self._cw.add_css(self.css_files) - _ = self._cw._ - self.columns = columns or self.columns - sample = self.cw_rset.get_entity(0, 0) - self.w(u'' % self.table_css) - self.table_header(sample) - self.w(u'') - for row in range(self.cw_rset.rowcount): - self.cell_call(row=row, col=0) - self.w(u'') - self.w(u'
') - - def cell_call(self, row, col): - _ = self._cw._ - entity = self.cw_rset.get_entity(row, col) - entity.complete() - infos = {} - for col in self.columns: - meth = getattr(self, 'build_%s_cell' % col, None) - # find the build method or try to find matching attribute - if meth: - content = meth(entity) - else: - content = entity.printable_value(col) - infos[col] = content - self.w(u"""""") - line = u''.join(u'%%(%s)s' % col for col in self.columns) - self.w(line % infos) - self.w(u'\n') - - def table_header(self, sample): - """builds the table's header""" - self.w(u'') - for column in self.columns: - meth = getattr(self, 'header_for_%s' % column, None) - if meth: - colname = meth(sample) - else: - colname = self._cw._(column) - self.w(u'%s' % xml_escape(colname)) - self.w(u'\n')