--- 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 <http://www.gnu.org/licenses/>.
-"""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 <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 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'<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 range(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, 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'<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) 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'<div class="section">')
- if not title and 'title' in req.form:
- title = req.form['title']
- if title:
- self.w(u'<h2 class="tableTitle">%s</h2>\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'<div id="%s">' % 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'</div>\n')
- if self.initial_load:
- self.w(u'</div>\n')
-
- def page_navigation_url(self, navcomp, path, params):
- """Build a URL to the current view using the <navcomp> 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'<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):
- 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'<div class="section">')
- 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'<h2>%s</h2>\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'</div>\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'<table class="%s">' % self.table_css)
- self.table_header(sample)
- self.w(u'<tbody>')
- for row in range(self.cw_rset.rowcount):
- self.cell_call(row=row, col=0)
- self.w(u'</tbody>')
- self.w(u'</table>')
-
- 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"""<tr onmouseover="$(this).addClass('highlighted');"
- onmouseout="$(this).removeClass('highlighted')">""")
- line = u''.join(u'<td>%%(%s)s</td>' % col for col in self.columns)
- self.w(line % infos)
- self.w(u'</tr>\n')
-
- def table_header(self, sample):
- """builds the table's header"""
- self.w(u'<thead><tr>')
- 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'<th>%s</th>' % xml_escape(colname))
- self.w(u'</tr></thead>\n')