web/views/navigation.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
--- 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 <http://www.gnu.org/licenses/>.
-"""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'<div class="pagination">')
-        self.w(self.previous_link(basepath, params))
-        self.w(u'[&#160;%s&#160;]' %
-               u'&#160;| '.join(self.iter_page_links(basepath, params)))
-        self.w(u'&#160;&#160;%s' % self.next_link(basepath, params))
-        self.w(u'</div>')
-
-    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 <select>, which is better when there are a
-    lot of results.
-
-    By default it will be selected when there are more than 4 pages to be
-    displayed.
-    """
-    __select__ = paginated_rset(4)
-
-    page_link_templ = u'<option value="%s" title="%s">%s</option>'
-    selected_page_link_templ = u'<option value="%s" selected="selected" title="%s">%s</option>'
-    def call(self):
-        params = dict(self._cw.form)
-        self.clean_params(params)
-        basepath = self._cw.relative_path(includeparams=False)
-        w = self.w
-        w(u'<div class="pagination">')
-        w(self.previous_link(basepath, params))
-        w(u'<select onchange="javascript: document.location=this.options[this.selectedIndex].value">')
-        for option in self.iter_page_links(basepath, params):
-            w(option)
-        w(u'</select>')
-        w(u'&#160;&#160;%s' % self.next_link(basepath, params))
-        w(u'</div>')
-
-
-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'<div class="pagination">')
-        self.w(u'%s&#160;' % self.previous_link(basepath, params))
-        self.w(u'[&#160;%s&#160;]' % u'&#160;| '.join(blocklist))
-        self.w(u'&#160;%s' % self.next_link(basepath, params))
-        self.w(u'</div>')
-
-
-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'<div class="displayAllLink"><a href="%s">%s</a></div>\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'<div class="displayAllLink"><a href="%s">%s</a></div>\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 '<img src="%s" alt="%s" />' % (
-            xml_escape(self._cw.data_url('go_prev.png')), self._cw._('previous page'))
-
-    @property
-    def next_icon(self):
-        return '<img src="%s" alt="%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'<div class="prevnext">')
-        self.prevnext(w)
-        w(u'</div>')
-        w(u'<div class="clear"></div>')
-
-    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 + '&#160;&#160;' + content
-        else:
-            title = self._cw._('i18nprevnext_next')
-            icon = self.next_icon
-            cssclass = u'nextEntity right'
-            content = content + '&#160;&#160;' + icon
-        self.prevnext_div(w, type, cssclass, entity.absolute_url(),
-                          title, content)
-
-    def prevnext_div(self, w, type, cssclass, url, title, content):
-        w(u'<div class="%s">' % cssclass)
-        w(u'<a href="%s" title="%s">%s</a>' % (xml_escape(url),
-                                               xml_escape(title),
-                                               content))
-        w(u'</div>')
-        self._cw.html_headers.add_raw('<link rel="%s" href="%s" />' % (
-              type, xml_escape(url)))