diff -r 058bb3dc685f -r 0b59724cb3f2 web/views/navigation.py --- a/web/views/navigation.py Mon Jan 04 18:40:30 2016 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,415 +0,0 @@ -# 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. -# -# CubicWeb is free software: you can redistribute it and/or modify it under the -# terms of the GNU Lesser General Public License as published by the Free -# Software Foundation, either version 2.1 of the License, or (at your option) -# any later version. -# -# CubicWeb is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License along -# with CubicWeb. If not, see . -"""This module provides some generic components to navigate in the web -application. - -Pagination ----------- - -Several implementations for large result set pagination are provided: - -.. autoclass:: PageNavigation -.. autoclass:: PageNavigationSelect -.. autoclass:: SortedNavigation - -Pagination will appear when needed according to the `page-size` ui property. - -This module monkey-patch the :func:`paginate` function to the base :class:`View` -class, so that you can ask pagination explicitly on every result-set based views. - -.. autofunction:: paginate - - -Previous / next navigation --------------------------- - -An adapter and its related component for the somewhat usal "previous / next" -navigation are provided. - - .. autoclass:: IPrevNextAdapter - .. autoclass:: NextPrevNavigationComponent -""" - -__docformat__ = "restructuredtext en" -from cubicweb import _ - -from datetime import datetime - -from six import text_type - -from rql.nodes import VariableRef, Constant - -from logilab.mtconverter import xml_escape -from logilab.common.deprecation import deprecated - -from cubicweb.predicates import paginated_rset, sorted_rset, adaptable -from cubicweb.uilib import cut -from cubicweb.view import EntityAdapter -from cubicweb.web.component import EmptyComponent, EntityCtxComponent, NavigationComponent - - -class PageNavigation(NavigationComponent): - """The default pagination component: display link to pages where each pages - is identified by the item number of its first and last elements. - """ - def call(self): - """displays a resultset by page""" - params = dict(self._cw.form) - self.clean_params(params) - basepath = self._cw.relative_path(includeparams=False) - self.w(u'') - - def index_display(self, start, stop): - return u'%s - %s' % (start+1, stop+1) - - def iter_page_links(self, basepath, params): - rset = self.cw_rset - page_size = self.page_size - start = 0 - while start < rset.rowcount: - stop = min(start + page_size - 1, rset.rowcount - 1) - yield self.page_link(basepath, params, start, stop, - self.index_display(start, stop)) - start = stop + 1 - - -class PageNavigationSelect(PageNavigation): - """This pagination component displays a result-set by page as - :class:`PageNavigation` but in a ') - for option in self.iter_page_links(basepath, params): - w(option) - w(u'') - w(u'  %s' % self.next_link(basepath, params)) - w(u'') - - -class SortedNavigation(NavigationComponent): - """This pagination component will be selected by default if there are less - than 4 pages and if the result set is sorted. - - Displayed links to navigate accross pages of a result set are done according - to the first variable on which the sort is done, and looks like: - - [ana - cro] | [cro - ghe] | ... | [tim - zou] - - You may want to override this component to customize display in some cases. - - .. automethod:: sort_on - .. automethod:: display_func - .. automethod:: format_link_content - .. automethod:: write_links - - Below an example from the tracker cube: - - .. sourcecode:: python - - class TicketsNavigation(navigation.SortedNavigation): - __select__ = (navigation.SortedNavigation.__select__ - & ~paginated_rset(4) & is_instance('Ticket')) - def sort_on(self): - col, attrname = super(TicketsNavigation, self).sort_on() - if col == 6: - # sort on state, we don't want that - return None, None - return col, attrname - - The idea is that in trackers'ticket tables, result set is first ordered on - ticket's state while this doesn't make any sense in the navigation. So we - override :meth:`sort_on` so that if we detect such sorting, we disable the - feature to go back to item number in the pagination. - - Also notice the `~paginated_rset(4)` in the selector so that if there are - more than 4 pages to display, :class:`PageNavigationSelect` will still be - selected. - """ - __select__ = paginated_rset() & sorted_rset() - - # number of considered chars to build page links - nb_chars = 5 - - def call(self): - # attrname = the name of attribute according to which the sort - # is done if any - col, attrname = self.sort_on() - index_display = self.display_func(self.cw_rset, col, attrname) - basepath = self._cw.relative_path(includeparams=False) - params = dict(self._cw.form) - self.clean_params(params) - blocklist = [] - start = 0 - total = self.cw_rset.rowcount - while start < total: - stop = min(start + self.page_size - 1, total - 1) - cell = self.format_link_content(index_display(start), index_display(stop)) - blocklist.append(self.page_link(basepath, params, start, stop, cell)) - start = stop + 1 - self.write_links(basepath, params, blocklist) - - def display_func(self, rset, col, attrname): - """Return a function that will be called with a row number as argument - and should return a string to use as link for it. - """ - if attrname is not None: - def index_display(row): - if not rset[row][col]: # outer join - return u'' - entity = rset.get_entity(row, col) - return entity.printable_value(attrname, format='text/plain') - elif col is None: # smart links disabled. - def index_display(row): - return text_type(row) - elif self._cw.vreg.schema.eschema(rset.description[0][col]).final: - def index_display(row): - return text_type(rset[row][col]) - else: - def index_display(row): - return rset.get_entity(row, col).view('text') - return index_display - - def sort_on(self): - """Return entity column number / attr name to use for nice display by - inspecting the rset'syntax tree. - """ - rschema = self._cw.vreg.schema.rschema - for sorterm in self.cw_rset.syntax_tree().children[0].orderby: - if isinstance(sorterm.term, Constant): - col = sorterm.term.value - 1 - return col, None - var = sorterm.term.get_nodes(VariableRef)[0].variable - col = None - for ref in var.references(): - rel = ref.relation() - if rel is None: - continue - attrname = rel.r_type - if attrname in ('is', 'has_text'): - continue - if not rschema(attrname).final: - col = var.selected_index() - attrname = None - if col is None: - # final relation or not selected non final relation - if var is rel.children[0]: - relvar = rel.children[1].children[0].get_nodes(VariableRef)[0] - else: - relvar = rel.children[0].variable - col = relvar.selected_index() - if col is not None: - break - else: - # no relation but maybe usable anyway if selected - col = var.selected_index() - attrname = None - if col is not None: - # if column type is date[time], set proper 'nb_chars' - if var.stinfo['possibletypes'] & frozenset(('TZDatetime', 'Datetime', - 'Date')): - self.nb_chars = len(self._cw.format_date(datetime.today())) - return col, attrname - # nothing usable found, use the first column - return 0, None - - def format_link_content(self, startstr, stopstr): - """Return text for a page link, where `startstr` and `stopstr` are the - text for the lower/upper boundaries of the page. - - By default text are stripped down to :attr:`nb_chars` characters. - """ - text = u'%s - %s' % (startstr.lower()[:self.nb_chars], - stopstr.lower()[:self.nb_chars]) - return xml_escape(text) - - def write_links(self, basepath, params, blocklist): - """Return HTML for the whole navigation: `blocklist` is a list of HTML - snippets for each page, `basepath` and `params` will be necessary to - build previous/next links. - """ - self.w(u'') - - -def do_paginate(view, rset=None, w=None, show_all_option=True, page_size=None): - """write pages index in w stream (default to view.w) and then limit the - result set (default to view.rset) to the currently displayed page if we're - not explicitly told to display everything (by setting __force_display in - req.form) - """ - req = view._cw - if rset is None: - rset = view.cw_rset - if w is None: - w = view.w - nav = req.vreg['components'].select_or_none( - 'navigation', req, rset=rset, page_size=page_size, view=view) - if nav: - if w is None: - w = view.w - if req.form.get('__force_display'): - # allow to come back to the paginated view - params = dict(req.form) - basepath = req.relative_path(includeparams=False) - del params['__force_display'] - url = nav.page_url(basepath, params) - w(u'\n' - % (xml_escape(url), req._('back to pagination (%s results)') - % nav.page_size)) - else: - # get boundaries before component rendering - start, stop = nav.page_boundaries() - nav.render(w=w) - params = dict(req.form) - nav.clean_params(params) - # make a link to see them all - if show_all_option: - basepath = req.relative_path(includeparams=False) - params['__force_display'] = 1 - params['__fromnavigation'] = 1 - url = nav.page_url(basepath, params) - w(u'\n' - % (xml_escape(url), req._('show %s results') % len(rset))) - rset.limit(offset=start, limit=stop-start, inplace=True) - - -def paginate(view, show_all_option=True, w=None, page_size=None, rset=None): - """paginate results if the view is paginable - """ - if view.paginable: - do_paginate(view, rset, w, show_all_option, page_size) - -# monkey patch base View class to add a .paginate([...]) -# method to be called to write pages index in the view and then limit the result -# set to the current page -from cubicweb.view import View -View.do_paginate = do_paginate -View.paginate = paginate -View.handle_pagination = False - - - -class IPrevNextAdapter(EntityAdapter): - """Interface for entities which can be linked to a previous and/or next - entity - - .. automethod:: next_entity - .. automethod:: previous_entity - """ - __needs_bw_compat__ = True - __regid__ = 'IPrevNext' - __abstract__ = True - - def next_entity(self): - """return the 'next' entity""" - raise NotImplementedError - - def previous_entity(self): - """return the 'previous' entity""" - raise NotImplementedError - - -class NextPrevNavigationComponent(EntityCtxComponent): - """Entities adaptable to the 'IPrevNext' should have this component - automatically displayed. You may want to override this component to have a - different look and feel. - """ - - __regid__ = 'prevnext' - # register msg not generated since no entity implements IPrevNext in cubicweb - # itself - help = _('ctxcomponents_prevnext_description') - __select__ = EntityCtxComponent.__select__ & adaptable('IPrevNext') - context = 'navbottom' - order = 10 - - @property - def prev_icon(self): - return '%s' % ( - xml_escape(self._cw.data_url('go_prev.png')), self._cw._('previous page')) - - @property - def next_icon(self): - return '%s' % ( - xml_escape(self._cw.data_url('go_next.png')), self._cw._('next page')) - - def init_rendering(self): - adapter = self.entity.cw_adapt_to('IPrevNext') - self.previous = adapter.previous_entity() - self.next = adapter.next_entity() - if not (self.previous or self.next): - raise EmptyComponent() - - def render_body(self, w): - w(u'
') - self.prevnext(w) - w(u'
') - w(u'
') - - def prevnext(self, w): - if self.previous: - self.prevnext_entity(w, self.previous, 'prev') - if self.next: - self.prevnext_entity(w, self.next, 'next') - - def prevnext_entity(self, w, entity, type): - textsize = self._cw.property_value('navigation.short-line-size') - content = xml_escape(cut(entity.dc_title(), textsize)) - if type == 'prev': - title = self._cw._('i18nprevnext_previous') - icon = self.prev_icon - cssclass = u'previousEntity left' - content = icon + '  ' + content - else: - title = self._cw._('i18nprevnext_next') - icon = self.next_icon - cssclass = u'nextEntity right' - content = content + '  ' + icon - self.prevnext_div(w, type, cssclass, entity.absolute_url(), - title, content) - - def prevnext_div(self, w, type, cssclass, url, title, content): - w(u'
' % cssclass) - w(u'%s' % (xml_escape(url), - xml_escape(title), - content)) - w(u'
') - self._cw.html_headers.add_raw('' % ( - type, xml_escape(url)))