# HG changeset patch # User Sylvain Thénault # Date 1319200357 -7200 # Node ID 4ff9f25cb06ebcbfa98f84d35da2e95dc58af5a0 # Parent dcc5a4d4812262b00b83c0f82c17b6e5cbaa6bcc [table views] closes #1986413: refactor TableView, EntityAttributesTableView, PyValTableView See the tickets for a description of the why. * one should now use RsetTableView instead of TableView and EntityTableView instead of EntityAttributesTableView. * a flexible layout object has been extracted, TableLayout * PyVaTableView has been rewritten using the same base class / renderer diff -r dcc5a4d48122 -r 4ff9f25cb06e __pkginfo__.py --- a/__pkginfo__.py Fri Oct 21 14:32:37 2011 +0200 +++ b/__pkginfo__.py Fri Oct 21 14:32:37 2011 +0200 @@ -40,7 +40,7 @@ ] __depends__ = { - 'logilab-common': '>= 0.56.3', + 'logilab-common': '>= 0.57.0', 'logilab-mtconverter': '>= 0.8.0', 'rql': '>= 0.28.0', 'yams': '>= 0.34.0', diff -r dcc5a4d48122 -r 4ff9f25cb06e debian/control --- a/debian/control Fri Oct 21 14:32:37 2011 +0200 +++ b/debian/control Fri Oct 21 14:32:37 2011 +0200 @@ -99,7 +99,7 @@ Package: cubicweb-common Architecture: all XB-Python-Version: ${python:Versions} -Depends: ${misc:Depends}, ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.56.3), python-yams (>= 0.34.0), python-rql (>= 0.28.0), python-lxml +Depends: ${misc:Depends}, ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.57.0), python-yams (>= 0.34.0), python-rql (>= 0.28.0), python-lxml Recommends: python-simpletal (>= 4.0), python-crypto Conflicts: cubicweb-core Replaces: cubicweb-core diff -r dcc5a4d48122 -r 4ff9f25cb06e doc/3.14.rst --- a/doc/3.14.rst Fri Oct 21 14:32:37 2011 +0200 +++ b/doc/3.14.rst Fri Oct 21 14:32:37 2011 +0200 @@ -67,6 +67,15 @@ * jQuery has been updated to 1.6.4. No backward compat issue known (yet...) +* Table views refactoring : new RsetTableView and EntityTableView, as well as + rewritten an enhanced version of PyValTableView on the same bases. Those + deprecates former `TableView`, `EntityAttributesTableView` and `CellView`, + which are however kept for backward compat, with some warnings that may not be + very clear unfortunatly (you may see your own table view subclass name here, + which doesn't make the problem that clear). Notice that `_cw.view('table', + rset, *kwargs)` will be routed to the new `RsetTableView` or to the old + `TableView` depending on given extra arguments. See #1986413. + Unintrusive API changes ----------------------- diff -r dcc5a4d48122 -r 4ff9f25cb06e doc/book/en/devweb/views/table.rst --- a/doc/book/en/devweb/views/table.rst Fri Oct 21 14:32:37 2011 +0200 +++ b/doc/book/en/devweb/views/table.rst Fri Oct 21 14:32:37 2011 +0200 @@ -1,26 +1,7 @@ -Table view ----------- - -(:mod:`cubicweb.web.views.tableview`) - -*table* - Creates a HTML table (``) and call the view `cell` for each cell of - the result set. Applicable on any result set. +Table views +----------- -*editable-table* - Creates an **editable** HTML table (`
`) and call the view `cell` for each cell of - the result set. Applicable on any result set. - -*cell* - By default redirects to the `final` view if this is a final entity or - `outofcontext` view otherwise - - -API -``` - -.. autoclass:: cubicweb.web.views.tableview.TableView - :members: +.. automodule:: cubicweb.web.views.tableview Example ``````` @@ -29,50 +10,135 @@ .. sourcecode:: python - class ActivityTable(EntityView): - __regid__ = 'activitytable' + class ActivityResourcesTable(EntityView): + __regid__ = 'activity.resources.table' __select__ = is_instance('Activity') - title = _('activitytable') def call(self, showresource=True): - _ = self._cw._ - headers = [_("diem"), _("duration"), _("workpackage"), _("description"), _("state"), u""] eids = ','.join(str(row[0]) for row in self.cw_rset) - rql = ('Any R, D, DUR, WO, DESCR, S, A, SN, RT, WT ORDERBY D DESC ' + rql = ('Any R,D,DUR,WO,DESCR,S,A, SN,RT,WT ORDERBY D DESC ' 'WHERE ' ' A is Activity, A done_by R, R title RT, ' ' A diem D, A duration DUR, ' ' A done_for WO, WO title WT, ' - ' A description DESCR, A in_state S, S name SN, A eid IN (%s)' % eids) - if showresource: - displaycols = range(7) - headers.insert(0, display_name(self._cw, 'Resource')) - else: # skip resource column if asked to - displaycols = range(1, 7) + ' A description DESCR, A in_state S, S name SN, ' + ' A eid IN (%s)' % eids) rset = self._cw.execute(rql) - self.wview('editable-table', rset, 'null', - displayfilter=True, displayactions=False, - headers=headers, displaycols=displaycols, - cellvids={3: 'editable-final'}) + self.wview('resource.table', rset, 'null') -To obtain an editable table, specify 'edtitable-table' as vid. You -have to select the entity in the rql request too (in order to kwnow -which entity must be edited). You can specify an optional -`displaycols` argument which defines column's indexes that will be -displayed. In the above example, setting `showresource` to `False` -will only render columns from index 1 to 7. + class ResourcesTable(RsetTableView): + __regid__ = 'resource.table' + # notice you may wish a stricter selector to check rql's shape + __select__ = is_instance('Resource') + # my table headers + headers = ['Resource', 'diem', 'duration', 'workpackage', 'description', 'state'] + # I want a table where attributes are editable (reledit inside) + finalvid = 'editable-final' + + cellvids = {3: 'editable-final'} + # display facets and actions with a menu + layout_args = {'display_filter': 'top', + 'add_view_actions': None} + +To obtain an editable table, you may specify the 'editable-table' view identifier +using some of `cellvids`, `finalvid` or `nonfinalvid`. The previous example results in: .. image:: ../../images/views-table-shadow.png - -In order to activate table filter mechanism, set the `displayfilter` -argument to True. A small arrow will be displayed at the table's top -right corner. Clicking on `show filter form` action, will display the -filter form as below: +In order to activate table filter mechanism, the `display_filter` option is given +as a layout argument. A small arrow will be displayed at the table's top right +corner. Clicking on `show filter form` action, will display the filter form as +below: .. image:: ../../images/views-table-filter-shadow.png -By the same way, you can display all registered actions for the -selected entity, setting `displayactions` argument to True. +By the same way, you can display additional actions for the selected entities +by setting `add_view_actions` layout option to `True`. This will add actions +returned by the view's :meth:`~cubicweb.web.views.TableMixIn.table_actions`. + +You can notice that all columns of the result set are not displayed. This is +because of given `headers`, implying to display only columns from 0 to +len(headers). + +Also Notice that the `ResourcesTable` view relies on a particular rql shape +(which is not ensured by the way, the only checked thing is that the result set +contains instance of the `Resource` type). That usually implies that you can't +use this view for user specific queries (e.g. generated by facets or typed +manually). + + +So another option would be to write this view using +:class:`~cubicweb.web.views.tableview.EntityTableView`, as below. + + + class ResourcesTable(EntityTableView): + __regid__ = 'resource.table' + __select__ = is_instance('Resource') + # table columns definition + columns = ['resource', 'diem', 'duration', 'workpackage', 'description', 'in_state'] + # I want a table where attributes are editable (reledit inside) + finalvid = 'editable-final' + # display facets and actions with a menu + layout_args = {'display_filter': 'top', + 'add_view_actions': None} + + def workpackage_cell(entity): + activity = entity.reverse_done_in[0] + activity.view('reledit', rtype='done_for', role='subject', w=w) + def workpackage_sortvalue(entity): + activity = entity.reverse_done_in[0] + return activity.done_for[0].sortvalue() + + column_renderers = { + 'resource': MainEntityColRenderer(), + 'workpackage': EntityTableColRenderer( + header='Workpackage', + renderfunc=worpackage_cell, + sortfunc=worpackage_sortvalue,), + 'in_state': EntityTableColRenderer( + renderfunc=lambda w,x: w(x.cw_adapt_to('IWorkflowable').printable_state), + sortfunc=lambda x: x.cw_adapt_to('IWorkflowable').printable_state), + } + +Notice the following point: + +* `cell_(w, entity)` will be searched for rendering the content of a + cell. If not found, `column` is expected to be an attribute of `entity`. + +* `cell_sortvalue_(entity)` should return a typed value to use for + javascript sorting or None for not sortable columns (the default). + +* The :func:`etable_entity_sortvalue` decorator will set a 'sortvalue' function + for the column containing the main entity (the one given as argument to all + methods), which will call `entity.sortvalue()`. + +* You can set a column header using the :func:`etable_header_title` decorator. + This header will be translated. If it's not an already existing msgid, think + to mark it using `_()` (the example supposes headers are schema defined msgid). + + +Pro/cons of each approach +````````````````````````` +:class:`EntityTableView` and :class:`RsetableView` provides basically the same +set of features, though they don't share the same properties. Let's try to sum +up pro and cons of each class. + +* `EntityTableView` view is: + + - more verbose, but usually easier to understand + + - easily extended (easy to add/remove columns for instance) + + - doesn't rely on a particular rset shape. Simply give it a title and will be + listed in the 'possible views' box if any. + +* `RsetTableView` view is: + + - hard to beat to display barely a result set, or for cases where some of + `headers`, `displaycols` or `cellvids` could be defined to enhance the table + while you don't care about e.g. pagination or facets. + + - hardly extensible, as you usually have to change places where the view is + called to modify the RQL (hence the view's result set shape). diff -r dcc5a4d48122 -r 4ff9f25cb06e web/test/test_views.py --- a/web/test/test_views.py Fri Oct 21 14:32:37 2011 +0200 +++ b/web/test/test_views.py Fri Oct 21 14:32:37 2011 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -45,10 +45,6 @@ rset = self.execute('CWUser X WHERE X login "admin"') self.view('copy', rset) - def test_manual_tests(self): - rset = self.execute('Any P,F,S WHERE P is CWUser, P firstname F, P surname S') - self.view('table', rset, template=None, displayfilter=True, displaycols=[0,2]) - def test_sortable_js_added(self): rset = self.execute('CWUser X') # sortable.js should not be included by default diff -r dcc5a4d48122 -r 4ff9f25cb06e web/test/unittest_urlrewrite.py --- a/web/test/unittest_urlrewrite.py Fri Oct 21 14:32:37 2011 +0200 +++ b/web/test/unittest_urlrewrite.py Fri Oct 21 14:32:37 2011 +0200 @@ -54,7 +54,7 @@ 'tab': 'cw_users_management'}), ('/cwgroup$', {'vid': 'cw.users-and-groups-management', 'tab': 'cw_groups_management'}), - ('/cwsource$', {'vid': 'cw.source-management'}), + ('/cwsource$', {'vid': 'cw.sources-management'}), ('/schema/([^/]+?)/?$', {'rql': r'Any X WHERE X is CWEType, X name "\1"', 'vid': 'primary'}), ('/add/([^/]+?)/?$' , dict(vid='creation', etype=r'\1')), ('/doc/images/(.+?)/?$', dict(fid='\\1', vid='wdocimages')), diff -r dcc5a4d48122 -r 4ff9f25cb06e web/test/unittest_views_baseviews.py --- a/web/test/unittest_views_baseviews.py Fri Oct 21 14:32:37 2011 +0200 +++ b/web/test/unittest_views_baseviews.py Fri Oct 21 14:32:37 2011 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -111,22 +111,13 @@ def test_sortvalue(self): e, _, view = self._prepare_entity() - expected = ['', 'loo"ong blabla'[:10], e.creation_date.strftime('%Y/%m/%d %H:%M:%S')] - got = [loadjson(view.sortvalue(0, i)) for i in xrange(3)] - self.assertListEqual(got, expected) + colrenderers = view.build_column_renderers()[:3] + self.assertListEqual([renderer.sortvalue(0) for renderer in colrenderers], + [u'', u'loo"ong blabla'[:10], e.creation_date]) # XXX sqlite does not handle Interval correctly # value = loadjson(view.sortvalue(0, 3)) # self.assertAlmostEquals(value, rset.rows[0][3].seconds) - def test_sortvalue_with_display_col(self): - e, rset, view = self._prepare_entity() - labels = view.columns_labels() - table = TableWidget(view) - table.columns = view.get_columns(labels, [1, 2], None, None, None, None, 0) - expected = ['loo"ong blabla'[:10], e.creation_date.strftime('%Y/%m/%d %H:%M:%S')] - got = [loadjson(value) for _, value in table.itercols(0)] - self.assertListEqual(got, expected) - class HTMLStreamTests(CubicWebTC): diff -r dcc5a4d48122 -r 4ff9f25cb06e web/test/unittest_views_pyviews.py --- a/web/test/unittest_views_pyviews.py Fri Oct 21 14:32:37 2011 +0200 +++ b/web/test/unittest_views_pyviews.py Fri Oct 21 14:32:37 2011 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -25,10 +25,9 @@ pyvalue=[[1, 'a'], [2, 'b']]) content = view.render(pyvalue=[[1, 'a'], [2, 'b']], headers=['num', 'char']) - self.assertEqual(content.strip(), '''
- - - + self.assertEqual(content.strip(), '''
numchar
1a
2b
\ + +
1a
2b
''') def test_pyvallist(self): diff -r dcc5a4d48122 -r 4ff9f25cb06e web/test/unittest_viewselector.py --- a/web/test/unittest_viewselector.py Fri Oct 21 14:32:37 2011 +0200 +++ b/web/test/unittest_viewselector.py Fri Oct 21 14:32:37 2011 +0200 @@ -31,7 +31,7 @@ primary, baseviews, tableview, editforms, calendar, management, embedding, actions, startup, cwuser, schema, xbel, vcard, owl, treeview, idownloadable, wdoc, debug, cwuser, cwproperties, cwsources, workflow, xmlrss, rdf, - csvexport) + csvexport, json) from cubes.folder import views as folderviews @@ -117,8 +117,9 @@ self.assertListEqual(self.pviews(req, rset), [('csvexport', csvexport.CSVRsetView), ('ecsvexport', csvexport.CSVEntityView), - ('editable-table', tableview.EditableTableView), + ('ejsonexport', json.JsonEntityView), ('filetree', treeview.FileTreeView), + ('jsonexport', json.JsonRsetView), ('list', baseviews.ListView), ('oneline', baseviews.OneLineView), ('owlabox', owl.OWLABOXView), @@ -127,7 +128,7 @@ ('rss', xmlrss.RSSView), ('sameetypelist', baseviews.SameETypeListView), ('security', management.SecurityManagementView), - ('table', tableview.TableView), + ('table', tableview.RsetTableView), ('text', baseviews.TextView), ('treeview', treeview.TreeView), ('xbel', xbel.XbelView), @@ -140,8 +141,9 @@ self.assertListEqual(self.pviews(req, rset), [('csvexport', csvexport.CSVRsetView), ('ecsvexport', csvexport.CSVEntityView), - ('editable-table', tableview.EditableTableView), + ('ejsonexport', json.JsonEntityView), ('filetree', treeview.FileTreeView), + ('jsonexport', json.JsonRsetView), ('list', baseviews.ListView), ('oneline', baseviews.OneLineView), ('owlabox', owl.OWLABOXView), @@ -150,7 +152,7 @@ ('rss', xmlrss.RSSView), ('sameetypelist', baseviews.SameETypeListView), ('security', management.SecurityManagementView), - ('table', tableview.TableView), + ('table', tableview.RsetTableView), ('text', baseviews.TextView), ('treeview', treeview.TreeView), ('xbel', xbel.XbelView), @@ -194,8 +196,9 @@ self.assertListEqual(self.pviews(req, rset), [('csvexport', csvexport.CSVRsetView), ('ecsvexport', csvexport.CSVEntityView), - ('editable-table', tableview.EditableTableView), + ('ejsonexport', json.JsonEntityView), ('filetree', treeview.FileTreeView), + ('jsonexport', json.JsonRsetView), ('list', baseviews.ListView), ('oneline', baseviews.OneLineView), ('owlabox', owl.OWLABOXView), @@ -203,7 +206,7 @@ ('rsetxml', xmlrss.XMLRsetView), ('rss', xmlrss.RSSView), ('security', management.SecurityManagementView), - ('table', tableview.TableView), + ('table', tableview.RsetTableView), ('text', baseviews.TextView), ('treeview', treeview.TreeView), ('xbel', xbel.XbelView), @@ -215,9 +218,9 @@ rset = req.execute('Any N, X WHERE X in_group Y, Y name N') self.assertListEqual(self.pviews(req, rset), [('csvexport', csvexport.CSVRsetView), - ('editable-table', tableview.EditableTableView), + ('jsonexport', json.JsonRsetView), ('rsetxml', xmlrss.XMLRsetView), - ('table', tableview.TableView), + ('table', tableview.RsetTableView), ]) def test_possible_views_multiple_eusers(self): @@ -225,11 +228,11 @@ rset = req.execute('CWUser X') self.assertListEqual(self.pviews(req, rset), [('csvexport', csvexport.CSVRsetView), - ('cw.users-table', cwuser.CWUsersTable), ('ecsvexport', csvexport.CSVEntityView), - ('editable-table', tableview.EditableTableView), + ('ejsonexport', json.JsonEntityView), ('filetree', treeview.FileTreeView), ('foaf', cwuser.FoafView), + ('jsonexport', json.JsonRsetView), ('list', baseviews.ListView), ('oneline', baseviews.OneLineView), ('owlabox', owl.OWLABOXView), @@ -238,7 +241,7 @@ ('rss', xmlrss.RSSView), ('sameetypelist', baseviews.SameETypeListView), ('security', management.SecurityManagementView), - ('table', tableview.TableView), + ('table', tableview.RsetTableView), ('text', baseviews.TextView), ('treeview', treeview.TreeView), ('vcard', vcard.VCardCWUserView), @@ -366,7 +369,7 @@ self.assertIsInstance(self.vreg['views'].select('edition', req, rset=rset), editforms.EditionFormView) self.assertIsInstance(self.vreg['views'].select('table', req, rset=rset), - tableview.TableView) + tableview.RsetTableView) self.assertRaises(NoSelectableObject, self.vreg['views'].select, 'creation', req, rset=rset) self.assertRaises(NoSelectableObject, @@ -379,7 +382,7 @@ self.assertIsInstance(self.vreg['views'].select('list', req, rset=rset), baseviews.ListView) self.assertIsInstance(self.vreg['views'].select('table', req, rset=rset), - tableview.TableView) + tableview.RsetTableView) self.assertRaises(NoSelectableObject, self.vreg['views'].select, 'creation', req, rset=rset) # list of entities of different types @@ -390,7 +393,7 @@ self.assertIsInstance(self.vreg['views'].select('list', req, rset=rset), baseviews.ListView) self.assertIsInstance(self.vreg['views'].select('table', req, rset=rset), - tableview.TableView) + tableview.RsetTableView) self.assertRaises(NoSelectableObject, self.vreg['views'].select, 'creation', req, rset=rset) self.assertRaises(NoSelectableObject, @@ -399,7 +402,7 @@ req = self.request() rset = req.execute('Any N, X WHERE X in_group Y, Y name N') self.assertIsInstance(self.vreg['views'].select('table', req, rset=rset), - tableview.TableView) + tableview.RsetTableView) self.assertRaises(NoSelectableObject, self.vreg['views'].select, 'index', req, rset=rset) self.assertRaises(NoSelectableObject, @@ -418,7 +421,7 @@ self.assertRaises(NoSelectableObject, self.vreg['views'].select, 'creation', req, rset=rset) self.assertIsInstance(self.vreg['views'].select('table', req, rset=rset), - tableview.TableView) + tableview.RsetTableView) def test_interface_selector(self): image = self.request().create_entity('File', data_name=u'bim.png', data=Binary('bim')) diff -r dcc5a4d48122 -r 4ff9f25cb06e web/views/cwsources.py --- a/web/views/cwsources.py Fri Oct 21 14:32:37 2011 +0200 +++ b/web/views/cwsources.py Fri Oct 21 14:32:37 2011 +0200 @@ -84,8 +84,9 @@ else: if hostconfig: self.w(u'

%s

' % self._cw._('CWSourceHostConfig_plural')) - self._cw.view('editable-table', hostconfig, - displaycols=range(2), w=self.w) + self._cw.view('table', hostconfig, w=self.w, + displaycols=range(2), + cellvids={1: 'editable-final'}) MAPPED_SOURCE_TYPES = set( ('pyrorql', 'datafeed') ) diff -r dcc5a4d48122 -r 4ff9f25cb06e web/views/pyviews.py --- a/web/views/pyviews.py Fri Oct 21 14:32:37 2011 +0200 +++ b/web/views/pyviews.py Fri Oct 21 14:32:37 2011 +0200 @@ -21,18 +21,38 @@ from cubicweb.view import View from cubicweb.selectors import match_kwargs +from cubicweb.web.views import tableview -class PyValTableView(View): - """display a list of list of values into an HTML table. +class PyValTableColRenderer(tableview.AbstractColumnRenderer): + """Default column renderer for :class:`PyValTableView`.""" + def bind(self, view, colid): + super(PyValTableColRenderer, self).bind(view, colid) + self.header = view.headers[colid] if view.headers else None + self.data = view.pyvalue + + def render_header(self, w): + if self.header: + w(self._cw._(self.header)) + else: + w(self.empty_cell_content) - Take care, content is NOT xml-escaped. + def render_cell(self, w, rownum): + w(unicode(self.data[rownum][self.colid])) + - If `headers` is specfied, it is expected to be a list of headers to be +class PyValTableView(tableview.TableMixIn, View): + """This table view is designed to be used a list of list of unicode values + given as a mandatory `pyvalue` argument. Take care, content is NOT + xml-escaped. + + It's configured through the following selection arguments. + + If `headers` is specified, it is expected to be a list of headers to be inserted as first row (in ). - If `colheaders` is True, the first column will be considered as an headers - column an its values will be inserted inside instead of . + `header_column_idx` may be used to specify a column index or a set of column + indiced where values should be inserted inside tag instead of . `cssclass` is the CSS class used on the tag, and default to 'listing' (so that the table will look similar to those generated by the @@ -40,31 +60,53 @@ """ __regid__ = 'pyvaltable' __select__ = match_kwargs('pyvalue') + default_column_renderer_class = PyValTableColRenderer + paginable = False # not supported + headers = None + cssclass = None + domid = None - def call(self, pyvalue, headers=None, colheaders=False, - cssclass='listing'): - if headers is None: - headers = self._cw.form.get('headers') - w = self.w - w(u'
\n' % cssclass) - if headers: - w(u'') - w(u'') - for header in headers: - w(u'' % header) - w(u'\n') - w(u'') - w(u'') - for row in pyvalue: - w(u'') - if colheaders: - w(u'' % row[0]) - row = row[1:] - for cell in row: - w(u'' % cell) - w(u'\n') - w(u'') - w(u'
%s
%s%s
\n') + def __init__(self, req, pyvalue, headers=None, cssclass=None, + header_column_idx=None, **kwargs): + super(PyValTableView, self).__init__(req, **kwargs) + self.pyvalue = pyvalue + if headers is not None: + self.headers = headers + elif self.headers: # headers set on a class attribute, translate + self.headers = [self._cw._(header) for header in self.headers] + if cssclass is not None: + self.cssclass = cssclass + self.header_column_idx = header_column_idx + + @property + def layout_args(self): + args = {} + if self.cssclass: + args['cssclass'] = self.cssclass + if self.header_column_idx is not None: + args['header_column_idx'] = self.header_column_idx + return args + + # layout callbacks ######################################################### + + @property + def table_size(self): + """return the number of rows (header excluded) to be displayed""" + return len(self.pyvalue) + + @property + def has_headers(self): + return self.headers + + def build_column_renderers(self): + return [self.column_renderer(colid) + for colid in xrange(len(self.pyvalue[0]))] + + def facets_form(self, mainvar=None): + return None # not supported + + def table_actions(self): + return [] # not supported class PyValListView(View): diff -r dcc5a4d48122 -r 4ff9f25cb06e web/views/tableview.py --- a/web/views/tableview.py Fri Oct 21 14:32:37 2011 +0200 +++ b/web/views/tableview.py Fri Oct 21 14:32:37 2011 +0200 @@ -15,29 +15,783 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""generic table view, including filtering abilities using facets""" +"""This module contains table views, with the following features that may be +provided (depending on the used implementation): + +* facets filtering +* pagination +* actions menu +* properly sortable content +* odd/row/hover line styles + +The three main implementation are described below. Each implementation is +suitable for a particular case, but they each attempt to display tables that +looks similar. + +.. autoclass:: cubicweb.web.views.tableview.RsetTableView + :members: + +.. autoclass:: cubicweb.web.views.tableview.EntityTableView + :members: + +.. autoclass:: cubicweb.web.views.pyview.PyValTableView + :members: + +All those classes are rendered using a *layout*: + +.. autoclass:: cubicweb.web.views.pyview.TableLayout + :members: + +There is by default only on table layout, using the 'table_layout' identifier, +that is referenced by table views +:attr:`cubicweb.web.views.tableview.TableMixIn.layout_id`. If you want to +customize the look and feel of your table, you can either replace the default +one by yours, having multiple variants with proper selectors, or change the +`layout_id` identifier of your table to use your table specific implementation. + +Notice you can gives options to the layout using a `layout_args` dictionary on +your class. + +If you can still find a view that suit your needs, you should take a look at the +class below that is the common abstract base class for the three views defined +above and implements you own class. + +.. autoclass:: cubicweb.web.views.pyview.TableMixIn + :members: +""" __docformat__ = "restructuredtext en" _ = unicode +from warnings import warn + from logilab.mtconverter import xml_escape +from logilab.common.decorators import cachedproperty +from logilab.common.deprecation import class_deprecated from cubicweb import NoSelectableObject, tags -from cubicweb.selectors import nonempty_rset +from cubicweb.selectors import yes, nonempty_rset, match_kwargs, objectify_selector from cubicweb.utils import make_uid, js_dumps, JSString from cubicweb.view import EntityView, AnyRsetView -from cubicweb.uilib import toggle_action, limitsize, htmlescape -from cubicweb.web import jsonize, component, facet +from cubicweb.uilib import toggle_action, limitsize, htmlescape, sgml_attributes, domid +from cubicweb.web import jsonize, component from cubicweb.web.htmlwidgets import (TableWidget, TableColumn, MenuWidget, PopupBoxMenu) +@objectify_selector +def unreloadable_table(cls, req, rset=None, + displaycols=None, headers=None, cellvids=None, + paginate=False, displayactions=False, displayfilter=False, + **kwargs): + # one may wish to specify one of headers/displaycols/cellvids as long as he + # doesn't want pagination nor actions nor facets + if not kwargs and (displaycols or headers or cellvids) and not ( + displayfilter or displayactions or paginate): + return 1 + return 0 + + +class TableLayout(component.Component): + """The default layout for table. When `render` is called, this will use + the API described on :class:`TableMixIn` to feed the generated table. + + This layout behaviour may be customized using the following attributes / + selection arguments: + + * `cssclass`, a string that should be used as HTML class attribute. Default + to "listing". + + * `needs_css`, the CSS files that should be used together with this + table. Default to ('cubicweb.tablesorter.css', 'cubicweb.tableview.css'). + + * `needs_js`, the Javascript files that should be used together with this + table. Default to ('jquery.tablesorter.js',) + + * `display_filter`, tells if the facets filter should be displayed when + possible. Allowed values are: + - `None`, don't display it + - 'top', display it above the table + - 'bottom', display it below the table + + * `display_actions`, tells if a menu for available actions should be + displayed when possible (see two following options). Allowed values are: + - `None`, don't display it + - 'top', display it above the table + - 'bottom', display it below the table + + * `hide_filter`, when true (the default), facets filter will be hidden by + default, with an action in the actions menu allowing to show / hide it. + + * `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 + """ + __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 + add_view_actions = False + header_column_idx = None + enable_sorting = True + tablesorter_settings = { + 'textExtraction': JSString('cw.sortValueExtraction'), + 'selectorHeaders': "thead tr:first th", # only plug on the first row + } + + def _setup_tablesorter(self, divid): + self._cw.add_css('cubicweb.tablesorter.css') + self._cw.add_js('jquery.tablesorter.js') + self._cw.add_onload('''$(document).ready(function() { + $("#%s table").tablesorter(%s); +});''' % (divid, js_dumps(self.tablesorter_settings))) + + def __init__(self, req, view, **kwargs): + super(TableLayout, self).__init__(req, **kwargs) + for key, val in self.cw_extra_kwargs.items(): + if hasattr(self.__class__, key) and not key[0] == '_': + setattr(self, key, val) + self.cw_extra_kwargs.pop(key) + self.view = view + if self.header_column_idx is None: + self.header_column_idx = frozenset() + elif isinstance(self.header_column_idx, int): + self.header_column_idx = frozenset( (self.header_column_idx,) ) + + @cachedproperty + def initial_load(self): + """We detect a bit heuristically if we are built for the first time of + from subsequent calls by the form filter or by the pagination hooks + """ + form = self._cw.form + return 'fromformfilter' not in form and '__start' not in form + + def render(self, w, **kwargs): + assert self.display_filter in (None, 'top', 'bottom'), self.display_filter + if self.needs_css: + self._cw.add_css(self.needs_css) + if self.needs_js: + self._cw.add_js(self.needs_js) + if self.enable_sorting: + self._setup_tablesorter(self.view.domid) + # Notice facets form must be rendered **outside** the main div as it + # shouldn't be rendered on ajax call subsequent to facet restriction + # (hence the 'fromformfilter' parameter added by the form + generate_form = self.initial_load + if self.display_filter and generate_form: + facetsform = self.view.facets_form() + else: + facetsform = None + if facetsform and self.display_filter == 'top': + cssclass = u'hidden' if self.hide_filter else u'' + facetsform.render(w, vid=self.view.__regid__, cssclass=cssclass, + divid=self.view.domid) + actions = [] + if self.add_view_actions: + actions = self.view.table_actions() + if self.display_filter and self.hide_filter and (facetsform or not generate_form): + actions += self.show_hide_filter_actions(not generate_form) + self.render_table(w, actions, self.view.paginable) + if facetsform and self.display_filter == 'bottom': + cssclass = u'hidden' if self.hide_filter else u'' + facetsform.render(w, vid=self.view.__regid__, cssclass=cssclass, + divid=self.view.domid) + + def render_table(self, w, actions, paginate): + view = self.view + divid = view.domid + if divid is not None: + w(u'
' % divid) + else: + assert not (actions or paginate) + if paginate: + view.paginate(w=w, show_all_option=False) + 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 view.has_headers: + w(u'') + for colrenderer in colrenderers: + w(u'') + w(u'\n') + w(u'') + for rownum in xrange(view.table_size): + self.render_row(w, rownum, colrenderers) + w(u'') + w(u'
') + colrenderer.render_header(w) + w(u'
') + if actions and self.display_actions == 'bottom': + self.render_actions(w, actions) + 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, basestring): + sortvalue = sortvalue[:10] + if sortvalue is not None: + attrs[u'cubicweb:sortvalue'] = js_dumps(sortvalue) + return attrs + + def render_actions(self, w, 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' % 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 + showlabel = self._cw._('show filter form') + hidelabel = self._cw._('hide filter form') + if currentlydisplayed: + c1, d1 = 'hidden', '%sShow' % divid + c2, d2 = None, '%sHide' % divid + else: + c1, d1 = None, '%sShow' % divid + c2, d2 = 'hidden', '%sHide' % divid + return [component.Link(showhide, showlabel, klass=c1, ident=d1), + component.Link(showhide, hidelabel, klass=c2, ident=d2)] + + +class AbstractColumnRenderer(object): + """Abstract base class for column renderer. Interface of a column renderer follows: + + .. automethod:: bind + .. automethod:: render_header + .. automethod:: render_cell + .. automethod:: 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_escape'd + :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 + """ + attributes = {} + empty_cell_content = u' ' + + def __init__(self, header=None, addcount=False, trheader=True, + escapeheader=True, sortable=True): + self.header = header + self.trheader = trheader + self.escapeheader = escapeheader + self.addcount = addcount + self.sortable = sortable + self.view = None + self._cw = None + self.colid = None + + def __str__(self): + return '<%s.%s (column %s)>' % (self.view.__class__.__name__, + self.__class__.__name__, + self.colid) + + def bind(self, view, colid): + """Bind the column renderer to its view. This is where `_cw`, `view`, + `colid` are set and the method to override if you want to add more + view/request depending attributes on your column render. + """ + self.view = view + self._cw = view._cw + self.colid = colid + + def 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: + + .. automethod:: 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 + + def call(self, **kwargs): + self.layout_render(self.w) + + def column_renderer(self, colid, *args, **kwargs): + """Return a column renderer for column of the given id.""" + try: + crenderer = self.column_renderers[colid] + except KeyError: + crenderer = self.default_column_renderer_class(*args, **kwargs) + crenderer.bind(self, colid) + return crenderer + + # layout callbacks ######################################################### + + def facets_form(self, **kwargs):# XXX extracted from jqplot cube + try: + return self._cw.vreg['views'].select( + 'facet.filtertable', self._cw, rset=self.cw_rset, view=self, + **kwargs) + except NoSelectableObject: + return None + + @cachedproperty + def domid(self): + return self._cw.form.get('divid') or domid('%s-%s' % (self.__regid__, make_uid())) + + @property + def table_size(self): + """Return the number of rows (header excluded) to be displayed. + + By default return the number of rows in the view's result set. If your + table isn't reult set based, override this method. + """ + return self.cw_rset.rowcount + + def build_column_renderers(self): + """Return a list of column renderers, one for each column to be + rendered. Prototype of a column renderer is described below: + + .. autoclass:: 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): + # we don't need 'divid' once assumed a view can compute its domid + params['divid'] = self.domid + params['vid'] = self.__regid__ + return navcomp.ajax_page_url(**params) + + +class RsetTableColRenderer(AbstractColumnRenderer): + """Default renderer for :class:`RsetTableView`.""" + default_cellvid = 'incontext' + + def __init__(self, cellvid=None, **kwargs): + super(RsetTableColRenderer, self).__init__(**kwargs) + self.cellvid = cellvid or self.default_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 + """ + __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, **kwargs): + if self.headers: + self.headers = [h and self._cw._(h) for h in self.headers] + if (headers or displaycols or cellvids): + 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 kwargs: + # old table view arguments that we can safely ignore thanks to + # selectors + if len(kwargs) > 1: + msg = '[3.14] %s arguments are deprecated' % ', '.join(kwargs) + else: + msg = '[3.14] %s argument is deprecated' % ', '.join(kwargs) + warn(msg, DeprecationWarning, stacklevel=2) + self.layout_render(self.w) + + def main_var_index(self): + """returns the index of the first non-attribute variable among the RQL + selected variables + """ + eschema = self._cw.vreg.schema.eschema + for i, etype in enumerate(self.cw_rset.description[0]): + if not eschema(etype).final: + return i + return None + + # layout callbacks ######################################################### + + @property + def table_size(self): + """return the number of rows (header excluded) to be displayed""" + return self.cw_rset.rowcount + + def build_column_renderers(self): + headers = self.headers + # compute displayed columns + if self.displaycols is None: + if headers is not None: + displaycols = range(len(headers)) + else: + rqlst = self.cw_rset.syntax_tree() + displaycols = range(len(rqlst.children[0].selection)) + else: + displaycols = self.displaycols + # compute table headers + main_var_index = self.main_var_index() + computed_titles = self.columns_labels(main_var_index) + # compute build renderers + cellvids = self.cellvids + renderers = [] + for colnum, colid in enumerate(displaycols): + addcount = False + # compute column header + title = None + if headers is not None: + title = headers[colnum] + if title is None: + title = computed_titles[colid] + if colid == main_var_index: + addcount = True + # compute cell vid for the column + if cellvids is not None and colnum in cellvids: + cellvid = cellvids[colnum] + else: + coltype = self.cw_rset.description[0][colid] + if coltype is not None and self._cw.vreg.schema.eschema(coltype).final: + cellvid = self.finalvid + else: + cellvid = self.nonfinalvid + # get renderer + renderer = self.column_renderer(colid, header=title, trheader=False, + addcount=addcount, cellvid=cellvid) + renderers.append(renderer) + return renderers + + +class EntityTableColRenderer(AbstractColumnRenderer): + """Default column renderer for :class:`EntityTableView`. + + You may use the :meth:`entity` method to retrieve the main entity for a + given row number. + + .. automethod:: entity + """ + def __init__(self, renderfunc=None, sortfunc=None, **kwargs): + if renderfunc is None: + renderfunc = lambda w,x: w(x.printable_value(self.colid)) + if sortfunc is None: + sortfunc = lambda x: x.sortvalue(self.colid) + kwargs.setdefault('sortable', sortfunc is not None) + super(EntityTableColRenderer, self).__init__(**kwargs) + self.renderfunc = renderfunc + self.sortfunc = sortfunc + + def render_cell(self, w, rownum): + entity = self.entity(rownum) + if entity: + self.renderfunc(w, entity) + else: + w(self.empty_cell_content) + + def sortvalue(self, rownum): + entity = self.entity(rownum) + if entity: + return self.sortfunc(self.entity(rownum)) + return None + + def entity(self, rownum): + """Return the table's main entity""" + return self.view.cw_rset.get_entity(rownum, self.view.cw_col or 0) + + +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', **kwargs): + kwargs.setdefault('renderfunc', lambda w, x: x.view(vid, w=w)) + kwargs.setdefault('sortfunc', lambda x: x.sortvalue()) + super(MainEntityColRenderer, self).__init__(**kwargs) + + def default_header(self): + view = self.view + return u', '.join(self._cw.__(et+'_plural') + for et in view.cw_rset.column_types(view.cw_col or 0)) + + +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 using entity types in the main + column. + """ + def __init__(self, getrelated, **kwargs): + super(RelatedEntityColRenderer, self).__init__(**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 EntityTableView(TableMixIn, EntityView): + """This abstract table view is designed to be used with an + :class:`is_instance()` or :class:`adaptable` selector, hence doesn't depend + the result set shape as the :class:`TableView` does. + + It will display columns that should be defined using the `columns` class + attribute containing a list of column ids. By default, each column is + renderered by :class:`EntityTableColRenderer` which consider that the column + id is an attribute of the table's main entity (ie the one for which the view + is selected). + + You may wish to specify :class:`MainEntityColRenderer` or + :class:`RelatedEntityColRenderer` renderer for a column in the + :attr:`column_renderers` dictionary. + + .. autoclass:: EntityTableColRenderer + .. autoclass:: MainEntityColRenderer + .. autoclass:: RelatedEntityColRenderer + """ + __abstract__ = True + default_column_renderer_class = EntityTableColRenderer + columns = None # to be defined in concret class + + def call(self, columns=None): + 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] + + +class EmptyCellView(AnyRsetView): + __regid__ = 'empty-cell' + __select__ = yes() + def call(self, **kwargs): + self.w(u' ') + cell_call = call + + +################################################################################ +# DEPRECATED tables ############################################################ +################################################################################ + + class TableView(AnyRsetView): """The table view accepts any non-empty rset. It uses introspection on the result set to compute column names and the proper way to display the cells. It is however highly configurable and accepts a wealth of options. """ + __metaclass__ = class_deprecated + __deprecation_warning__ = '[3.14] %(cls)s is deprecated' __regid__ = 'table' title = _('table') finalview = 'final' @@ -97,6 +851,13 @@ });''' % (divid, js_dumps(self.tablesorter_settings))) req.add_css(('cubicweb.tablesorter.css', 'cubicweb.tableview.css')) + @cachedproperty + def initial_load(self): + """We detect a bit heuristically if we are built for the first time of + from subsequent calls by the form filter or by the pagination hooks + """ + form = self._cw.form + return 'fromformfilter' not in form and '__start' not in form def call(self, title=None, subvid=None, displayfilter=None, headers=None, displaycols=None, displayactions=None, actions=(), divid=None, @@ -119,7 +880,6 @@ if mainindex is None: mainindex = self.main_var_index() computed_labels = self.columns_labels(mainindex) - hidden = True if not subvid and 'subvid' in req.form: subvid = req.form.pop('subvid') actions = list(actions) @@ -128,16 +888,10 @@ else: if displayfilter is None and req.form.get('displayfilter'): displayfilter = True - if req.form['displayfilter'] == 'shown': - hidden = False if displayactions is None and req.form.get('displayactions'): displayactions = True displaycols = self.displaycols(displaycols, headers) - fromformfilter = 'fromformfilter' in req.form - # if fromformfilter is true, this is an ajax call and we only want to - # replace the inner div, so don't regenerate everything under the if - # below - if not fromformfilter: + if self.initial_load: self.w(u'
') if not title and 'title' in req.form: title = req.form['title'] @@ -168,7 +922,7 @@ table.append_column(column) table.render(self.w) self.w(u'
\n') - if not fromformfilter: + if not self.initial_load: self.w(u'\n') def page_navigation_url(self, navcomp, path, params): @@ -199,7 +953,7 @@ 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'
') + self.w(u'
') def get_columns(self, computed_labels, displaycols, headers, subvid, cellvids, cellattrs, mainindex): @@ -268,6 +1022,8 @@ class CellView(EntityView): + __metaclass__ = class_deprecated + __deprecation_warning__ = '[3.14] %(cls)s is deprecated' __regid__ = 'cell' __select__ = nonempty_rset() @@ -363,6 +1119,8 @@ Table will render column header using the method header_for_COLNAME if defined otherwise COLNAME will be used. """ + __metaclass__ = class_deprecated + __deprecation_warning__ = '[3.14] %(cls)s is deprecated' __abstract__ = True columns = () table_css = "listing" @@ -413,4 +1171,3 @@ self.w(u'%s' % xml_escape(colname)) self.w(u'\n') - diff -r dcc5a4d48122 -r 4ff9f25cb06e web/views/workflow.py --- a/web/views/workflow.py Fri Oct 21 14:32:37 2011 +0200 +++ b/web/views/workflow.py Fri Oct 21 14:32:37 2011 +0200 @@ -143,12 +143,10 @@ if self._cw.vreg.schema.eschema('CWUser').has_perm(self._cw, 'read'): sel += ',U,C' rql += ', WF owned_by U?' - displaycols = range(5) headers = (_('from_state'), _('to_state'), _('comment'), _('date'), _('CWUser')) else: sel += ',C' - displaycols = range(4) headers = (_('from_state'), _('to_state'), _('comment'), _('date')) rql = '%s %s, X eid %%(x)s' % (sel, rql) try: @@ -157,9 +155,8 @@ return if rset: if title: - title = _(title) - self.wview('table', rset, title=title, displayactions=False, - displaycols=displaycols, headers=headers) + self.w(u'

%s

\n' % _(title)) + self.wview('table', rset, headers=headers) class WFHistoryVComponent(component.EntityCtxComponent): @@ -284,7 +281,7 @@ rset = self._cw.execute( 'Any T,T,DS,T,TT ORDERBY TN WHERE T transition_of WF, WF eid %(x)s,' 'T type TT, T name TN, T destination_state DS?', {'x': entity.eid}) - self.wview('editable-table', rset, 'null', + self.wview('table', rset, 'null', cellvids={ 1: 'trfromstates', 2: 'outofcontext', 3:'trsecurity',}, headers = (_('Transition'), _('from_state'), _('to_state'), _('permissions'), _('type') ),