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') -