web/views/navigation.py
brancholdstable
changeset 8462 a14b6562082b
parent 8110 d743865ba7ed
child 8190 2a3c1b787688
equal deleted inserted replaced
8231:1bb43e31032d 8462:a14b6562082b
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
    14 # details.
    14 # details.
    15 #
    15 #
    16 # You should have received a copy of the GNU Lesser General Public License along
    16 # You should have received a copy of the GNU Lesser General Public License along
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    18 """navigation components definition for CubicWeb web client"""
    18 """This module provides some generic components to navigate in the web
       
    19 application.
       
    20 
       
    21 Pagination
       
    22 ----------
       
    23 
       
    24 Several implementations for large result set pagination are provided:
       
    25 
       
    26 .. autoclass:: PageNavigation
       
    27 .. autoclass:: PageNavigationSelect
       
    28 .. autoclass:: SortedNavigation
       
    29 
       
    30 Pagination will appear when needed according to the `page-size` ui property.
       
    31 
       
    32 This module monkey-patch the :func:`paginate` function to the base :class:`View`
       
    33 class, so that you can ask pagination explicitly on every result-set based views.
       
    34 
       
    35 .. autofunction:: paginate
       
    36 
       
    37 
       
    38 Previous / next navigation
       
    39 --------------------------
       
    40 
       
    41 An adapter and its related component for the somewhat usal "previous / next"
       
    42 navigation are provided.
       
    43 
       
    44   .. autoclass:: IPrevNextAdapter
       
    45   .. autoclass:: NextPrevNavigationComponent
       
    46 """
    19 
    47 
    20 __docformat__ = "restructuredtext en"
    48 __docformat__ = "restructuredtext en"
    21 _ = unicode
    49 _ = unicode
       
    50 
       
    51 from datetime import datetime
    22 
    52 
    23 from rql.nodes import VariableRef, Constant
    53 from rql.nodes import VariableRef, Constant
    24 
    54 
    25 from logilab.mtconverter import xml_escape
    55 from logilab.mtconverter import xml_escape
    26 from logilab.common.deprecation import deprecated
    56 from logilab.common.deprecation import deprecated
    31 from cubicweb.view import EntityAdapter, implements_adapter_compat
    61 from cubicweb.view import EntityAdapter, implements_adapter_compat
    32 from cubicweb.web.component import EmptyComponent, EntityCtxComponent, NavigationComponent
    62 from cubicweb.web.component import EmptyComponent, EntityCtxComponent, NavigationComponent
    33 
    63 
    34 
    64 
    35 class PageNavigation(NavigationComponent):
    65 class PageNavigation(NavigationComponent):
    36 
    66     """The default pagination component: display link to pages where each pages
       
    67     is identified by the item number of its first and last elements.
       
    68     """
    37     def call(self):
    69     def call(self):
    38         """displays a resultset by page"""
    70         """displays a resultset by page"""
    39         params = dict(self._cw.form)
    71         params = dict(self._cw.form)
    40         self.clean_params(params)
    72         self.clean_params(params)
    41         basepath = self._cw.relative_path(includeparams=False)
    73         basepath = self._cw.relative_path(includeparams=False)
    59                                  self.index_display(start, stop))
    91                                  self.index_display(start, stop))
    60             start = stop + 1
    92             start = stop + 1
    61 
    93 
    62 
    94 
    63 class PageNavigationSelect(PageNavigation):
    95 class PageNavigationSelect(PageNavigation):
    64     """displays a resultset by page as PageNavigationSelect but in a <select>,
    96     """This pagination component displays a result-set by page as
    65     better when there are a lot of results.
    97     :class:`PageNavigation` but in a <select>, which is better when there are a
       
    98     lot of results.
       
    99 
       
   100     By default it will be selected when there are more than 4 pages to be
       
   101     displayed.
    66     """
   102     """
    67     __select__ = paginated_rset(4)
   103     __select__ = paginated_rset(4)
    68 
   104 
    69     page_link_templ = u'<option value="%s" title="%s">%s</option>'
   105     page_link_templ = u'<option value="%s" title="%s">%s</option>'
    70     selected_page_link_templ = u'<option value="%s" selected="selected" title="%s">%s</option>'
   106     selected_page_link_templ = u'<option value="%s" selected="selected" title="%s">%s</option>'
    82         w(u'&#160;&#160;%s' % self.next_link(basepath, params))
   118         w(u'&#160;&#160;%s' % self.next_link(basepath, params))
    83         w(u'</div>')
   119         w(u'</div>')
    84 
   120 
    85 
   121 
    86 class SortedNavigation(NavigationComponent):
   122 class SortedNavigation(NavigationComponent):
    87     """sorted navigation apply if navigation is needed (according to page size)
   123     """This pagination component will be selected by default if there are less
    88     and if the result set is sorted
   124     than 4 pages and if the result set is sorted.
       
   125 
       
   126     Displayed links to navigate accross pages of a result set are done according
       
   127     to the first variable on which the sort is done, and looks like:
       
   128 
       
   129         [ana - cro] | [cro - ghe] | ... | [tim - zou]
       
   130 
       
   131     You may want to override this component to customize display in some cases.
       
   132 
       
   133     .. automethod:: sort_on
       
   134     .. automethod:: display_func
       
   135     .. automethod:: format_link_content
       
   136     .. automethod:: write_links
       
   137 
       
   138     Below an example from the tracker cube:
       
   139 
       
   140     .. sourcecode:: python
       
   141 
       
   142       class TicketsNavigation(navigation.SortedNavigation):
       
   143           __select__ = (navigation.SortedNavigation.__select__
       
   144                         & ~paginated_rset(4) & is_instance('Ticket'))
       
   145           def sort_on(self):
       
   146               col, attrname = super(TicketsNavigation, self).sort_on()
       
   147               if col == 6:
       
   148                   # sort on state, we don't want that
       
   149                   return None, None
       
   150               return col, attrname
       
   151 
       
   152     The idea is that in trackers'ticket tables, result set is first ordered on
       
   153     ticket's state while this doesn't make any sense in the navigation. So we
       
   154     override :meth:`sort_on` so that if we detect such sorting, we disable the
       
   155     feature to go back to item number in the pagination.
       
   156 
       
   157     Also notice the `~paginated_rset(4)` in the selector so that if there are
       
   158     more than 4 pages to display, :class:`PageNavigationSelect` will still be
       
   159     selected.
    89     """
   160     """
    90     __select__ = paginated_rset() & sorted_rset()
   161     __select__ = paginated_rset() & sorted_rset()
    91 
   162 
    92     # number of considered chars to build page links
   163     # number of considered chars to build page links
    93     nb_chars = 5
   164     nb_chars = 5
    94 
   165 
       
   166     def call(self):
       
   167         # attrname = the name of attribute according to which the sort
       
   168         # is done if any
       
   169         col, attrname = self.sort_on()
       
   170         index_display = self.display_func(self.cw_rset, col, attrname)
       
   171         basepath = self._cw.relative_path(includeparams=False)
       
   172         params = dict(self._cw.form)
       
   173         self.clean_params(params)
       
   174         blocklist = []
       
   175         start = 0
       
   176         total = self.cw_rset.rowcount
       
   177         while start < total:
       
   178             stop = min(start + self.page_size - 1, total - 1)
       
   179             cell = self.format_link_content(index_display(start), index_display(stop))
       
   180             blocklist.append(self.page_link(basepath, params, start, stop, cell))
       
   181             start = stop + 1
       
   182         self.write_links(basepath, params, blocklist)
       
   183 
    95     def display_func(self, rset, col, attrname):
   184     def display_func(self, rset, col, attrname):
       
   185         """Return a function that will be called with a row number as argument
       
   186         and should return a string to use as link for it.
       
   187         """
    96         if attrname is not None:
   188         if attrname is not None:
    97             def index_display(row):
   189             def index_display(row):
    98                 if not rset[row][col]: # outer join
   190                 if not rset[row][col]: # outer join
    99                     return u''
   191                     return u''
   100                 entity = rset.get_entity(row, col)
   192                 entity = rset.get_entity(row, col)
   101                 return entity.printable_value(attrname, format='text/plain')
   193                 return entity.printable_value(attrname, format='text/plain')
       
   194         elif col is None: # smart links disabled.
       
   195             def index_display(row):
       
   196                 return unicode(row)
   102         elif self._cw.vreg.schema.eschema(rset.description[0][col]).final:
   197         elif self._cw.vreg.schema.eschema(rset.description[0][col]).final:
   103             def index_display(row):
   198             def index_display(row):
   104                 return unicode(rset[row][col])
   199                 return unicode(rset[row][col])
   105         else:
   200         else:
   106             def index_display(row):
   201             def index_display(row):
   107                 return rset.get_entity(row, col).view('text')
   202                 return rset.get_entity(row, col).view('text')
   108         return index_display
   203         return index_display
   109 
   204 
   110     def call(self):
   205     def sort_on(self):
   111         """displays links to navigate accross pages of a result set
   206         """Return entity column number / attr name to use for nice display by
   112 
   207         inspecting the rset'syntax tree.
   113         Displayed result is done according to a variable on which the sort
       
   114         is done, and looks like:
       
   115         [ana - cro] | [cro - ghe] | ... | [tim - zou]
       
   116         """
   208         """
   117         w = self.w
       
   118         rset = self.cw_rset
       
   119         page_size = self.page_size
       
   120         rschema = self._cw.vreg.schema.rschema
   209         rschema = self._cw.vreg.schema.rschema
   121         # attrname = the name of attribute according to which the sort
   210         for sorterm in self.cw_rset.syntax_tree().children[0].orderby:
   122         # is done if any
       
   123         for sorterm in rset.syntax_tree().children[0].orderby:
       
   124             if isinstance(sorterm.term, Constant):
   211             if isinstance(sorterm.term, Constant):
   125                 col = sorterm.term.value - 1
   212                 col = sorterm.term.value - 1
   126                 index_display = self.display_func(rset, col, None)
   213                 return col, None
   127                 break
       
   128             var = sorterm.term.get_nodes(VariableRef)[0].variable
   214             var = sorterm.term.get_nodes(VariableRef)[0].variable
   129             col = None
   215             col = None
   130             for ref in var.references():
   216             for ref in var.references():
   131                 rel = ref.relation()
   217                 rel = ref.relation()
   132                 if rel is None:
   218                 if rel is None:
   149             else:
   235             else:
   150                 # no relation but maybe usable anyway if selected
   236                 # no relation but maybe usable anyway if selected
   151                 col = var.selected_index()
   237                 col = var.selected_index()
   152                 attrname = None
   238                 attrname = None
   153             if col is not None:
   239             if col is not None:
   154                 index_display = self.display_func(rset, col, attrname)
   240                 # if column type is date[time], set proper 'nb_chars'
   155                 break
   241                 if var.stinfo['possibletypes'] & frozenset(('TZDatetime', 'Datetime',
   156         else:
   242                                                             'Date')):
   157             # nothing usable found, use the first column
   243                     self.nb_chars = len(self._cw.format_date(datetime.today()))
   158             index_display = self.display_func(rset, 0, None)
   244                 return col, attrname
   159         blocklist = []
   245         # nothing usable found, use the first column
   160         params = dict(self._cw.form)
   246         return 0, None
   161         self.clean_params(params)
       
   162         start = 0
       
   163         basepath = self._cw.relative_path(includeparams=False)
       
   164         while start < rset.rowcount:
       
   165             stop = min(start + page_size - 1, rset.rowcount - 1)
       
   166             cell = self.format_link_content(index_display(start), index_display(stop))
       
   167             blocklist.append(self.page_link(basepath, params, start, stop, cell))
       
   168             start = stop + 1
       
   169         self.write_links(basepath, params, blocklist)
       
   170 
   247 
   171     def format_link_content(self, startstr, stopstr):
   248     def format_link_content(self, startstr, stopstr):
       
   249         """Return text for a page link, where `startstr` and `stopstr` are the
       
   250         text for the lower/upper boundaries of the page.
       
   251 
       
   252         By default text are stripped down to :attr:`nb_chars` characters.
       
   253         """
   172         text = u'%s - %s' % (startstr.lower()[:self.nb_chars],
   254         text = u'%s - %s' % (startstr.lower()[:self.nb_chars],
   173                              stopstr.lower()[:self.nb_chars])
   255                              stopstr.lower()[:self.nb_chars])
   174         return xml_escape(text)
   256         return xml_escape(text)
   175 
   257 
   176     def write_links(self, basepath, params, blocklist):
   258     def write_links(self, basepath, params, blocklist):
       
   259         """Return HTML for the whole navigation: `blocklist` is a list of HTML
       
   260         snippets for each page, `basepath` and `params` will be necessary to
       
   261         build previous/next links.
       
   262         """
   177         self.w(u'<div class="pagination">')
   263         self.w(u'<div class="pagination">')
   178         self.w(u'%s&#160;' % self.previous_link(basepath, params))
   264         self.w(u'%s&#160;' % self.previous_link(basepath, params))
   179         self.w(u'[&#160;%s&#160;]' % u'&#160;| '.join(blocklist))
   265         self.w(u'[&#160;%s&#160;]' % u'&#160;| '.join(blocklist))
   180         self.w(u'&#160;%s' % self.next_link(basepath, params))
   266         self.w(u'&#160;%s' % self.next_link(basepath, params))
   181         self.w(u'</div>')
   267         self.w(u'</div>')
   182 
   268 
   183 
   269 
       
   270 def do_paginate(view, rset=None, w=None, show_all_option=True, page_size=None):
       
   271     """write pages index in w stream (default to view.w) and then limit the
       
   272     result set (default to view.rset) to the currently displayed page if we're
       
   273     not explicitly told to display everything (by setting __force_display in
       
   274     req.form)
       
   275     """
       
   276     req = view._cw
       
   277     if rset is None:
       
   278         rset = view.cw_rset
       
   279     if w is None:
       
   280         w = view.w
       
   281     nav = req.vreg['components'].select_or_none(
       
   282         'navigation', req, rset=rset, page_size=page_size, view=view)
       
   283     if nav:
       
   284         if w is None:
       
   285             w = view.w
       
   286         if req.form.get('__force_display'):
       
   287             # allow to come back to the paginated view
       
   288             params = dict(req.form)
       
   289             basepath = req.relative_path(includeparams=False)
       
   290             del params['__force_display']
       
   291             url = nav.page_url(basepath, params)
       
   292             w(u'<div class="displayAllLink"><a href="%s">%s</a></div>\n'
       
   293               % (xml_escape(url), req._('back to pagination (%s results)')
       
   294                                   % nav.page_size))
       
   295         else:
       
   296             # get boundaries before component rendering
       
   297             start, stop = nav.page_boundaries()
       
   298             nav.render(w=w)
       
   299             params = dict(req.form)
       
   300             nav.clean_params(params)
       
   301             # make a link to see them all
       
   302             if show_all_option:
       
   303                 basepath = req.relative_path(includeparams=False)
       
   304                 params['__force_display'] = 1
       
   305                 params['__fromnavigation'] = 1
       
   306                 url = nav.page_url(basepath, params)
       
   307                 w(u'<div class="displayAllLink"><a href="%s">%s</a></div>\n'
       
   308                   % (xml_escape(url), req._('show %s results') % len(rset)))
       
   309             rset.limit(offset=start, limit=stop-start, inplace=True)
       
   310 
       
   311 
       
   312 def paginate(view, show_all_option=True, w=None, page_size=None, rset=None):
       
   313     """paginate results if the view is paginable
       
   314     """
       
   315     if view.paginable:
       
   316         do_paginate(view, rset, w, show_all_option, page_size)
       
   317 
       
   318 # monkey patch base View class to add a .paginate([...])
       
   319 # method to be called to write pages index in the view and then limit the result
       
   320 # set to the current page
       
   321 from cubicweb.view import View
       
   322 View.do_paginate = do_paginate
       
   323 View.paginate = paginate
       
   324 View.handle_pagination = False
       
   325 
       
   326 
   184 from cubicweb.interfaces import IPrevNext
   327 from cubicweb.interfaces import IPrevNext
   185 
   328 
   186 class IPrevNextAdapter(EntityAdapter):
   329 class IPrevNextAdapter(EntityAdapter):
   187     """interface for entities which can be linked to a previous and/or next
   330     """Interface for entities which can be linked to a previous and/or next
   188     entity
   331     entity
       
   332 
       
   333     .. automethod:: next_entity
       
   334     .. automethod:: previous_entity
   189     """
   335     """
   190     __needs_bw_compat__ = True
   336     __needs_bw_compat__ = True
   191     __regid__ = 'IPrevNext'
   337     __regid__ = 'IPrevNext'
   192     __select__ = implements(IPrevNext, warn=False) # XXX for bw compat, else should be abstract
   338     __select__ = implements(IPrevNext, warn=False) # XXX for bw compat, else should be abstract
   193 
   339 
   201         """return the 'previous' entity"""
   347         """return the 'previous' entity"""
   202         raise NotImplementedError
   348         raise NotImplementedError
   203 
   349 
   204 
   350 
   205 class NextPrevNavigationComponent(EntityCtxComponent):
   351 class NextPrevNavigationComponent(EntityCtxComponent):
       
   352     """Entities adaptable to the 'IPrevNext' should have this component
       
   353     automatically displayed. You may want to override this component to have a
       
   354     different look and feel.
       
   355     """
       
   356 
   206     __regid__ = 'prevnext'
   357     __regid__ = 'prevnext'
   207     # register msg not generated since no entity implements IPrevNext in cubicweb
   358     # register msg not generated since no entity implements IPrevNext in cubicweb
   208     # itself
   359     # itself
   209     help = _('ctxcomponents_prevnext_description')
   360     help = _('ctxcomponents_prevnext_description')
   210     __select__ = EntityCtxComponent.__select__ & adaptable('IPrevNext')
   361     __select__ = EntityCtxComponent.__select__ & adaptable('IPrevNext')
   260                                                xml_escape(title),
   411                                                xml_escape(title),
   261                                                content))
   412                                                content))
   262         w(u'</div>')
   413         w(u'</div>')
   263         self._cw.html_headers.add_raw('<link rel="%s" href="%s" />' % (
   414         self._cw.html_headers.add_raw('<link rel="%s" href="%s" />' % (
   264               type, xml_escape(url)))
   415               type, xml_escape(url)))
   265 
       
   266 
       
   267 def do_paginate(view, rset=None, w=None, show_all_option=True, page_size=None):
       
   268     """write pages index in w stream (default to view.w) and then limit the result
       
   269     set (default to view.rset) to the currently displayed page
       
   270     """
       
   271     req = view._cw
       
   272     if rset is None:
       
   273         rset = view.cw_rset
       
   274     if w is None:
       
   275         w = view.w
       
   276     nav = req.vreg['components'].select_or_none(
       
   277         'navigation', req, rset=rset, page_size=page_size, view=view)
       
   278     if nav:
       
   279         if w is None:
       
   280             w = view.w
       
   281         # get boundaries before component rendering
       
   282         start, stop = nav.page_boundaries()
       
   283         nav.render(w=w)
       
   284         params = dict(req.form)
       
   285         nav.clean_params(params)
       
   286         # make a link to see them all
       
   287         if show_all_option:
       
   288             basepath = req.relative_path(includeparams=False)
       
   289             params['__force_display'] = 1
       
   290             url = nav.page_url(basepath, params)
       
   291             w(u'<div class="displayAllLink"><a href="%s">%s</a></div>\n'
       
   292               % (xml_escape(url), req._('show %s results') % len(rset)))
       
   293         rset.limit(offset=start, limit=stop-start, inplace=True)
       
   294 
       
   295 
       
   296 def paginate(view, show_all_option=True, w=None, page_size=None, rset=None):
       
   297     """paginate results if the view is paginable and we're not explictly told to
       
   298     display everything (by setting __force_display in req.form)
       
   299     """
       
   300     if view.paginable and not view._cw.form.get('__force_display'):
       
   301         do_paginate(view, rset, w, show_all_option, page_size)
       
   302 
       
   303 # monkey patch base View class to add a .paginate([...])
       
   304 # method to be called to write pages index in the view and then limit the result
       
   305 # set to the current page
       
   306 from cubicweb.view import View
       
   307 View.do_paginate = do_paginate
       
   308 View.paginate = paginate
       
   309 
       
   310 
       
   311 #@deprecated (see below)
       
   312 def limit_rset_using_paged_nav(self, req, rset, w, forcedisplay=False,
       
   313                                show_all_option=True, page_size=None):
       
   314     if not (forcedisplay or req.form.get('__force_display') is not None):
       
   315         do_paginate(self, rset, w, show_all_option, page_size)
       
   316 
       
   317 View.pagination = deprecated('[3.2] .pagination is deprecated, use paginate')(
       
   318     limit_rset_using_paged_nav)
       
   319 limit_rset_using_paged_nav = deprecated('[3.6] limit_rset_using_paged_nav is deprecated, use do_paginate')(
       
   320     limit_rset_using_paged_nav)