Source code for cubicweb_web.views.navigation

# copyright 2003-2024 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact https://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 <https://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
"""


from datetime import datetime

from logilab.mtconverter import xml_escape
from rql import stmts
from rql.nodes import VariableRef, Constant, SubQuery

from cubicweb import _
from cubicweb.entity import EntityAdapter
from cubicweb.predicates import sorted_rset, adaptable
from cubicweb.uilib import cut

from cubicweb_web.view import View
from cubicweb_web.predicates import paginated_rset
from cubicweb_web.component import (
    EmptyComponent,
    EntityCtxComponent,
    NavigationComponent,
)








[docs]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)
[docs] 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 "" 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 str(row) elif self._cw.vreg.schema.entity_schema_for(rset.description[0][col]).final: def index_display(row): return str(rset[row][col]) else: def index_display(row): return rset.get_entity(row, col).view("text") return index_display
[docs] def sort_on(self): """Return entity column number / attr name to use for nice display by inspecting the rset'syntax tree. """ relation_schema_for = self._cw.vreg.schema.relation_schema_for 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 relation_schema_for(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 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"): nav.render_link_back_to_pagination(w=w) else: # get boundaries before component rendering start, stop = nav.page_boundaries() nav.render(w=w) if show_all_option: nav.render_link_display_all(w=w) rset.limit(offset=start, limit=stop - start, inplace=True)
[docs]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 View.do_paginate = do_paginate View.paginate = paginate View.handle_pagination = False
[docs]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
[docs] def next_entity(self): """return the 'next' entity""" raise NotImplementedError
[docs] def previous_entity(self): """return the 'previous' entity""" raise NotImplementedError
[docs]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="{}" alt="{}" />'.format( xml_escape(self._cw.data_url("go_prev.png")), self._cw._("previous page"), ) @property def next_icon(self): return '<img src="{}" alt="{}" />'.format( 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('<div class="prevnext">') self.prevnext(w) w("</div>") w('<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 = "previousEntity left" content = icon + "&#160;&#160;" + content else: title = self._cw._("i18nprevnext_next") icon = self.next_icon cssclass = "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('<div class="%s">' % cssclass) w( '<a href="%s" title="%s">%s</a>' % (xml_escape(url), xml_escape(title), content) ) w("</div>") self._cw.html_headers.add_raw( '<link rel="{}" href="{}" />'.format(type, xml_escape(url)) )
def limited_rql(rset): """returns a printable rql for the result set associated to the object, with limit/offset correctly set according to maximum page size and currently displayed page when necessary """ # try to get page boundaries from the navigation component # XXX we should probably not have a ref to this component here (eg in # cubicweb) nav = rset.req.vreg["components"].select_or_none("navigation", rset.req, rset=rset) if nav: start, stop = nav.page_boundaries() rql = _limit_offset_rql(rset, stop - start, start) # result set may have be limited manually in which case navigation won't # apply elif rset.limited: rql = _limit_offset_rql(rset, *rset.limited) # navigation component doesn't apply and rset has not been limited, no # need to limit query else: rql = rset.printable_rql() return rql def _limit_offset_rql(rset, limit, offset): rqlst = rset.syntax_tree() if len(rqlst.children) == 1: select = rqlst.children[0] olimit, ooffset = select.limit, select.offset select.limit, select.offset = limit, offset rql = rqlst.as_string(kwargs=rset.args) # restore original limit/offset select.limit, select.offset = olimit, ooffset else: newselect = stmts.Select() newselect.limit = limit newselect.offset = offset aliases = [ VariableRef(newselect.get_variable(chr(65 + i), i)) for i in range(len(rqlst.children[0].selection)) ] for vref in aliases: newselect.append_selected(VariableRef(vref.variable)) newselect.set_with([SubQuery(aliases, rqlst)], check=False) newunion = stmts.Union() newunion.append(newselect) rql = newunion.as_string(kwargs=rset.args) rqlst.parent = None return rql