web/views/navigation.py
changeset 8057 0f128fd3cbc8
parent 8054 11b6589352b6
child 8101 f9fa2f47572c
equal deleted inserted replaced
8056:8909800a8c51 8057:0f128fd3cbc8
    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
    22 
    50 
    23 from datetime import datetime
    51 from datetime import datetime
    33 from cubicweb.view import EntityAdapter, implements_adapter_compat
    61 from cubicweb.view import EntityAdapter, implements_adapter_compat
    34 from cubicweb.web.component import EmptyComponent, EntityCtxComponent, NavigationComponent
    62 from cubicweb.web.component import EmptyComponent, EntityCtxComponent, NavigationComponent
    35 
    63 
    36 
    64 
    37 class PageNavigation(NavigationComponent):
    65 class PageNavigation(NavigationComponent):
    38 
    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     """
    39     def call(self):
    69     def call(self):
    40         """displays a resultset by page"""
    70         """displays a resultset by page"""
    41         params = dict(self._cw.form)
    71         params = dict(self._cw.form)
    42         self.clean_params(params)
    72         self.clean_params(params)
    43         basepath = self._cw.relative_path(includeparams=False)
    73         basepath = self._cw.relative_path(includeparams=False)
    61                                  self.index_display(start, stop))
    91                                  self.index_display(start, stop))
    62             start = stop + 1
    92             start = stop + 1
    63 
    93 
    64 
    94 
    65 class PageNavigationSelect(PageNavigation):
    95 class PageNavigationSelect(PageNavigation):
    66     """displays a resultset by page as PageNavigationSelect but in a <select>,
    96     """This pagination component displays a result-set by page as
    67     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.
    68     """
   102     """
    69     __select__ = paginated_rset(4)
   103     __select__ = paginated_rset(4)
    70 
   104 
    71     page_link_templ = u'<option value="%s" title="%s">%s</option>'
   105     page_link_templ = u'<option value="%s" title="%s">%s</option>'
    72     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>'
    84         w(u'&#160;&#160;%s' % self.next_link(basepath, params))
   118         w(u'&#160;&#160;%s' % self.next_link(basepath, params))
    85         w(u'</div>')
   119         w(u'</div>')
    86 
   120 
    87 
   121 
    88 class SortedNavigation(NavigationComponent):
   122 class SortedNavigation(NavigationComponent):
    89     """sorted navigation apply if navigation is needed (according to page size)
   123     """This pagination component will be selected by default if there are less
    90     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.
    91     """
   160     """
    92     __select__ = paginated_rset() & sorted_rset()
   161     __select__ = paginated_rset() & sorted_rset()
    93 
   162 
    94     # number of considered chars to build page links
   163     # number of considered chars to build page links
    95     nb_chars = 5
   164     nb_chars = 5
    96 
   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 
    97     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         """
    98         if attrname is not None:
   188         if attrname is not None:
    99             def index_display(row):
   189             def index_display(row):
   100                 if not rset[row][col]: # outer join
   190                 if not rset[row][col]: # outer join
   101                     return u''
   191                     return u''
   102                 entity = rset.get_entity(row, col)
   192                 entity = rset.get_entity(row, col)
   103                 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)
   104         elif self._cw.vreg.schema.eschema(rset.description[0][col]).final:
   197         elif self._cw.vreg.schema.eschema(rset.description[0][col]).final:
   105             def index_display(row):
   198             def index_display(row):
   106                 return unicode(rset[row][col])
   199                 return unicode(rset[row][col])
   107         else:
   200         else:
   108             def index_display(row):
   201             def index_display(row):
   109                 return rset.get_entity(row, col).view('text')
   202                 return rset.get_entity(row, col).view('text')
   110         return index_display
   203         return index_display
   111 
   204 
   112     def call(self):
   205     def sort_on(self):
   113         """displays links to navigate accross pages of a result set
   206         """Return entity column number / attr name to use for nice display by
   114 
   207         inspecting the rset'syntax tree.
   115         Displayed result is done according to a variable on which the sort
       
   116         is done, and looks like:
       
   117         [ana - cro] | [cro - ghe] | ... | [tim - zou]
       
   118         """
   208         """
   119         w = self.w
       
   120         rset = self.cw_rset
       
   121         page_size = self.page_size
       
   122         rschema = self._cw.vreg.schema.rschema
   209         rschema = self._cw.vreg.schema.rschema
   123         # attrname = the name of attribute according to which the sort
   210         for sorterm in self.cw_rset.syntax_tree().children[0].orderby:
   124         # is done if any
       
   125         for sorterm in rset.syntax_tree().children[0].orderby:
       
   126             if isinstance(sorterm.term, Constant):
   211             if isinstance(sorterm.term, Constant):
   127                 col = sorterm.term.value - 1
   212                 col = sorterm.term.value - 1
   128                 index_display = self.display_func(rset, col, None)
   213                 return col, None
   129                 break
       
   130             var = sorterm.term.get_nodes(VariableRef)[0].variable
   214             var = sorterm.term.get_nodes(VariableRef)[0].variable
   131             col = None
   215             col = None
   132             for ref in var.references():
   216             for ref in var.references():
   133                 rel = ref.relation()
   217                 rel = ref.relation()
   134                 if rel is None:
   218                 if rel is None:
   145                         relvar = rel.children[1].children[0].get_nodes(VariableRef)[0]
   229                         relvar = rel.children[1].children[0].get_nodes(VariableRef)[0]
   146                     else:
   230                     else:
   147                         relvar = rel.children[0].variable
   231                         relvar = rel.children[0].variable
   148                     col = relvar.selected_index()
   232                     col = relvar.selected_index()
   149                 if col is not None:
   233                 if col is not None:
   150                     break
   234                     return col, attrname
   151             else:
   235             # no relation but maybe usable anyway if selected
   152                 # no relation but maybe usable anyway if selected
   236             col = var.selected_index()
   153                 col = var.selected_index()
   237             attrname = None
   154                 attrname = None
       
   155             if col is not None:
   238             if col is not None:
   156                 # if column type is date[time], set proper 'nb_chars'
   239                 # if column type is date[time], set proper 'nb_chars'
   157                 if var.stinfo['possibletypes'] & frozenset(('TZDatetime', 'Datetime',
   240                 if var.stinfo['possibletypes'] & frozenset(('TZDatetime', 'Datetime',
   158                                                             'Date')):
   241                                                             'Date')):
   159                     self.nb_chars = len(self._cw.format_date(datetime.today()))
   242                     self.nb_chars = len(self._cw.format_date(datetime.today()))
   160                 index_display = self.display_func(rset, col, attrname)
   243                 return col, attrname
   161                 break
   244         # nothing usable found, use the first column
   162         else:
   245         return 0, None
   163             # nothing usable found, use the first column
       
   164             index_display = self.display_func(rset, 0, None)
       
   165         blocklist = []
       
   166         params = dict(self._cw.form)
       
   167         self.clean_params(params)
       
   168         start = 0
       
   169         basepath = self._cw.relative_path(includeparams=False)
       
   170         while start < rset.rowcount:
       
   171             stop = min(start + page_size - 1, rset.rowcount - 1)
       
   172             cell = self.format_link_content(index_display(start), index_display(stop))
       
   173             blocklist.append(self.page_link(basepath, params, start, stop, cell))
       
   174             start = stop + 1
       
   175         self.write_links(basepath, params, blocklist)
       
   176 
   246 
   177     def format_link_content(self, startstr, stopstr):
   247     def format_link_content(self, startstr, stopstr):
       
   248         """Return text for a page link, where `startstr` and `stopstr` are the
       
   249         text for the lower/upper boundaries of the page.
       
   250 
       
   251         By default text are stripped down to :attr:`nb_chars` characters.
       
   252         """
   178         text = u'%s - %s' % (startstr.lower()[:self.nb_chars],
   253         text = u'%s - %s' % (startstr.lower()[:self.nb_chars],
   179                              stopstr.lower()[:self.nb_chars])
   254                              stopstr.lower()[:self.nb_chars])
   180         return xml_escape(text)
   255         return xml_escape(text)
   181 
   256 
   182     def write_links(self, basepath, params, blocklist):
   257     def write_links(self, basepath, params, blocklist):
       
   258         """Return HTML for the whole navigation: `blocklist` is a list of HTML
       
   259         snippets for each page, `basepath` and `params` will be necessary to
       
   260         build previous/next links.
       
   261         """
   183         self.w(u'<div class="pagination">')
   262         self.w(u'<div class="pagination">')
   184         self.w(u'%s&#160;' % self.previous_link(basepath, params))
   263         self.w(u'%s&#160;' % self.previous_link(basepath, params))
   185         self.w(u'[&#160;%s&#160;]' % u'&#160;| '.join(blocklist))
   264         self.w(u'[&#160;%s&#160;]' % u'&#160;| '.join(blocklist))
   186         self.w(u'&#160;%s' % self.next_link(basepath, params))
   265         self.w(u'&#160;%s' % self.next_link(basepath, params))
   187         self.w(u'</div>')
   266         self.w(u'</div>')
   188 
       
   189 
       
   190 from cubicweb.interfaces import IPrevNext
       
   191 
       
   192 class IPrevNextAdapter(EntityAdapter):
       
   193     """interface for entities which can be linked to a previous and/or next
       
   194     entity
       
   195     """
       
   196     __needs_bw_compat__ = True
       
   197     __regid__ = 'IPrevNext'
       
   198     __select__ = implements(IPrevNext, warn=False) # XXX for bw compat, else should be abstract
       
   199 
       
   200     @implements_adapter_compat('IPrevNext')
       
   201     def next_entity(self):
       
   202         """return the 'next' entity"""
       
   203         raise NotImplementedError
       
   204 
       
   205     @implements_adapter_compat('IPrevNext')
       
   206     def previous_entity(self):
       
   207         """return the 'previous' entity"""
       
   208         raise NotImplementedError
       
   209 
       
   210 
       
   211 class NextPrevNavigationComponent(EntityCtxComponent):
       
   212     __regid__ = 'prevnext'
       
   213     # register msg not generated since no entity implements IPrevNext in cubicweb
       
   214     # itself
       
   215     help = _('ctxcomponents_prevnext_description')
       
   216     __select__ = EntityCtxComponent.__select__ & adaptable('IPrevNext')
       
   217     context = 'navbottom'
       
   218     order = 10
       
   219 
       
   220     @property
       
   221     def prev_icon(self):
       
   222         return '<img src="%s"/>' % xml_escape(self._cw.data_url('go_prev.png'))
       
   223 
       
   224     @property
       
   225     def next_icon(self):
       
   226         return '<img src="%s"/>' % xml_escape(self._cw.data_url('go_next.png'))
       
   227 
       
   228     def init_rendering(self):
       
   229         adapter = self.entity.cw_adapt_to('IPrevNext')
       
   230         self.previous = adapter.previous_entity()
       
   231         self.next = adapter.next_entity()
       
   232         if not (self.previous or self.next):
       
   233             raise EmptyComponent()
       
   234 
       
   235     def render_body(self, w):
       
   236         w(u'<div class="prevnext">')
       
   237         self.prevnext(w)
       
   238         w(u'</div>')
       
   239         w(u'<div class="clear"></div>')
       
   240 
       
   241     def prevnext(self, w):
       
   242         if self.previous:
       
   243             self.prevnext_entity(w, self.previous, 'prev')
       
   244         if self.next:
       
   245             self.prevnext_entity(w, self.next, 'next')
       
   246 
       
   247     def prevnext_entity(self, w, entity, type):
       
   248         textsize = self._cw.property_value('navigation.short-line-size')
       
   249         content = xml_escape(cut(entity.dc_title(), textsize))
       
   250         if type == 'prev':
       
   251             title = self._cw._('i18nprevnext_previous')
       
   252             icon = self.prev_icon
       
   253             cssclass = u'previousEntity left'
       
   254             content = icon + content
       
   255         else:
       
   256             title = self._cw._('i18nprevnext_next')
       
   257             icon = self.next_icon
       
   258             cssclass = u'nextEntity right'
       
   259             content = content + '&#160;&#160;' + icon
       
   260         self.prevnext_div(w, type, cssclass, entity.absolute_url(),
       
   261                           title, content)
       
   262 
       
   263     def prevnext_div(self, w, type, cssclass, url, title, content):
       
   264         w(u'<div class="%s">' % cssclass)
       
   265         w(u'<a href="%s" title="%s">%s</a>' % (xml_escape(url),
       
   266                                                xml_escape(title),
       
   267                                                content))
       
   268         w(u'</div>')
       
   269         self._cw.html_headers.add_raw('<link rel="%s" href="%s" />' % (
       
   270               type, xml_escape(url)))
       
   271 
   267 
   272 
   268 
   273 def do_paginate(view, rset=None, w=None, show_all_option=True, page_size=None):
   269 def do_paginate(view, rset=None, w=None, show_all_option=True, page_size=None):
   274     """write pages index in w stream (default to view.w) and then limit the result
   270     """write pages index in w stream (default to view.w) and then limit the result
   275     set (default to view.rset) to the currently displayed page
   271     set (default to view.rset) to the currently displayed page
   311 # set to the current page
   307 # set to the current page
   312 from cubicweb.view import View
   308 from cubicweb.view import View
   313 View.do_paginate = do_paginate
   309 View.do_paginate = do_paginate
   314 View.paginate = paginate
   310 View.paginate = paginate
   315 View.handle_pagination = False
   311 View.handle_pagination = False
       
   312 
       
   313 
       
   314 from cubicweb.interfaces import IPrevNext
       
   315 
       
   316 class IPrevNextAdapter(EntityAdapter):
       
   317     """Interface for entities which can be linked to a previous and/or next
       
   318     entity
       
   319 
       
   320     .. automethod:: next_entity
       
   321     .. automethod:: previous_entity
       
   322     """
       
   323     __needs_bw_compat__ = True
       
   324     __regid__ = 'IPrevNext'
       
   325     __select__ = implements(IPrevNext, warn=False) # XXX for bw compat, else should be abstract
       
   326 
       
   327     @implements_adapter_compat('IPrevNext')
       
   328     def next_entity(self):
       
   329         """return the 'next' entity"""
       
   330         raise NotImplementedError
       
   331 
       
   332     @implements_adapter_compat('IPrevNext')
       
   333     def previous_entity(self):
       
   334         """return the 'previous' entity"""
       
   335         raise NotImplementedError
       
   336 
       
   337 
       
   338 class NextPrevNavigationComponent(EntityCtxComponent):
       
   339     """Entities adaptable to the 'IPrevNext' should have this component
       
   340     automatically displayed. You may want to override this component to have a
       
   341     different look and feel.
       
   342     """
       
   343 
       
   344     __regid__ = 'prevnext'
       
   345     # register msg not generated since no entity implements IPrevNext in cubicweb
       
   346     # itself
       
   347     help = _('ctxcomponents_prevnext_description')
       
   348     __select__ = EntityCtxComponent.__select__ & adaptable('IPrevNext')
       
   349     context = 'navbottom'
       
   350     order = 10
       
   351 
       
   352     @property
       
   353     def prev_icon(self):
       
   354         return '<img src="%s"/>' % xml_escape(self._cw.data_url('go_prev.png'))
       
   355 
       
   356     @property
       
   357     def next_icon(self):
       
   358         return '<img src="%s"/>' % xml_escape(self._cw.data_url('go_next.png'))
       
   359 
       
   360     def init_rendering(self):
       
   361         adapter = self.entity.cw_adapt_to('IPrevNext')
       
   362         self.previous = adapter.previous_entity()
       
   363         self.next = adapter.next_entity()
       
   364         if not (self.previous or self.next):
       
   365             raise EmptyComponent()
       
   366 
       
   367     def render_body(self, w):
       
   368         w(u'<div class="prevnext">')
       
   369         self.prevnext(w)
       
   370         w(u'</div>')
       
   371         w(u'<div class="clear"></div>')
       
   372 
       
   373     def prevnext(self, w):
       
   374         if self.previous:
       
   375             self.prevnext_entity(w, self.previous, 'prev')
       
   376         if self.next:
       
   377             self.prevnext_entity(w, self.next, 'next')
       
   378 
       
   379     def prevnext_entity(self, w, entity, type):
       
   380         textsize = self._cw.property_value('navigation.short-line-size')
       
   381         content = xml_escape(cut(entity.dc_title(), textsize))
       
   382         if type == 'prev':
       
   383             title = self._cw._('i18nprevnext_previous')
       
   384             icon = self.prev_icon
       
   385             cssclass = u'previousEntity left'
       
   386             content = icon + content
       
   387         else:
       
   388             title = self._cw._('i18nprevnext_next')
       
   389             icon = self.next_icon
       
   390             cssclass = u'nextEntity right'
       
   391             content = content + '&#160;&#160;' + icon
       
   392         self.prevnext_div(w, type, cssclass, entity.absolute_url(),
       
   393                           title, content)
       
   394 
       
   395     def prevnext_div(self, w, type, cssclass, url, title, content):
       
   396         w(u'<div class="%s">' % cssclass)
       
   397         w(u'<a href="%s" title="%s">%s</a>' % (xml_escape(url),
       
   398                                                xml_escape(title),
       
   399                                                content))
       
   400         w(u'</div>')
       
   401         self._cw.html_headers.add_raw('<link rel="%s" href="%s" />' % (
       
   402               type, xml_escape(url)))