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'  %s' % self.next_link(basepath, params)) w(u'</div>')
def test_paginated_rset(self): default_nb_pages = 1 web_request = self.admin_access.web_request with web_request() as req: rset = req.execute('Any G WHERE G is CWGroup') self.assertEqual(len(rset), 34) with web_request(vid='list', page_size='10') as req: self.assertEqual(paginated_rset()(None, req, rset), default_nb_pages) with web_request(vid='list', page_size='20') as req: self.assertEqual(paginated_rset()(None, req, rset), default_nb_pages) with web_request(vid='list', page_size='50') as req: self.assertEqual(paginated_rset()(None, req, rset), 0) with web_request(vid='list', page_size='10/') as req: self.assertEqual(paginated_rset()(None, req, rset), 0) with web_request(vid='list', page_size='.1') as req: self.assertEqual(paginated_rset()(None, req, rset), 0) with web_request(vid='list', page_size='not_an_int') as req: self.assertEqual(paginated_rset()(None, req, rset), 0)
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 str(row) elif self._cw.vreg.schema.eschema(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 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 ' % self.previous_link(basepath, params)) self.w(u'[ %s ]' % u' | '.join(blocklist)) self.w(u' %s' % self.next_link(basepath, params)) self.w(u'</div>')
class NavigationComponent(Component): """abstract base class for navigation components""" __regid__ = 'navigation' __select__ = paginated_rset() cw_property_defs = { _('visible'): dict(type='Boolean', default=True, help=_('display the component or not')), } page_size_property = 'navigation.page-size' start_param = '__start' stop_param = '__stop' page_link_templ = u'<span class="slice"><a href="%s" title="%s">%s</a></span>' selected_page_link_templ = u'<span class="selectedSlice"><a href="%s" title="%s">%s</a></span>' previous_page_link_templ = next_page_link_templ = page_link_templ def __init__(self, req, rset, **kwargs): super(NavigationComponent, self).__init__(req, rset=rset, **kwargs) self.starting_from = 0 self.total = rset.rowcount def get_page_size(self): try: return self._page_size except AttributeError: page_size = self.cw_extra_kwargs.get('page_size') if page_size is None: try: page_size = int(self._cw.form.get('page_size')) except (ValueError, TypeError): # no or invalid value, fall back pass if page_size is None: page_size = self._cw.property_value(self.page_size_property) self._page_size = page_size return page_size def set_page_size(self, page_size): self._page_size = page_size page_size = property(get_page_size, set_page_size) def page_boundaries(self): try: stop = int(self._cw.form[self.stop_param]) + 1 start = int(self._cw.form[self.start_param]) except KeyError: start, stop = 0, self.page_size if start >= len(self.cw_rset): start, stop = 0, self.page_size self.starting_from = start return start, stop def clean_params(self, params): if self.start_param in params: del params[self.start_param] if self.stop_param in params: del params[self.stop_param] def page_url(self, path, params, start=None, stop=None): params = dict(params) params['__fromnavigation'] = 1 if start is not None: params[self.start_param] = start if stop is not None: params[self.stop_param] = stop view = self.cw_extra_kwargs.get('view') if view is not None and hasattr(view, 'page_navigation_url'): url = view.page_navigation_url(self, path, params) elif path in ('json', 'ajax'): # 'ajax' is the new correct controller, but the old 'json' # controller should still be supported url = self.ajax_page_url(**params) else: url = self._cw.build_url(path, **params) # XXX hack to avoid opening a new page containing the evaluation of the # js expression on ajax call if url.startswith('javascript:'): url += '; $.noop();' return url def ajax_page_url(self, **params): divid = params.setdefault('divid', 'contentmain') params['rql'] = self.cw_rset.printable_rql() return js_href( "$(%s).loadxhtml(AJAX_PREFIX_URL, %s, 'get', 'swap')" % (json_dumps('#' + divid), js.ajaxFuncArgs('view', params))) def page_link(self, path, params, start, stop, content): url = xml_escape(self.page_url(path, params, start, stop)) if start == self.starting_from: return self.selected_page_link_templ % (url, content, content) return self.page_link_templ % (url, content, content) @property def prev_icon_url(self): return xml_escape(self._cw.data_url('go_prev.png')) @property def next_icon_url(self): return xml_escape(self._cw.data_url('go_next.png')) @property def no_previous_page_link(self): return (u'<img src="%s" alt="%s" class="prevnext_nogo"/>' % (self.prev_icon_url, self._cw._('there is no previous page'))) @property def no_next_page_link(self): return (u'<img src="%s" alt="%s" class="prevnext_nogo"/>' % (self.next_icon_url, self._cw._('there is no next page'))) @property def no_content_prev_link(self): return (u'<img src="%s" alt="%s" class="prevnext"/>' % ((self.prev_icon_url, self._cw._('no content prev link')))) @property def no_content_next_link(self): return (u'<img src="%s" alt="%s" class="prevnext"/>' % (self.next_icon_url, self._cw._('no content next link'))) def previous_link(self, path, params, content=None, title=_('previous_results')): if not content: content = self.no_content_prev_link start = self.starting_from if not start: return self.no_previous_page_link start = max(0, start - self.page_size) stop = start + self.page_size - 1 url = xml_escape(self.page_url(path, params, start, stop)) return self.previous_page_link_templ % (url, self._cw._(title), content) def next_link(self, path, params, content=None, title=_('next_results')): if not content: content = self.no_content_next_link start = self.starting_from + self.page_size if start >= self.total: return self.no_next_page_link stop = start + self.page_size - 1 url = xml_escape(self.page_url(path, params, start, stop)) return self.next_page_link_templ % (url, self._cw._(title), content) def render_link_back_to_pagination(self, w): """allow to come back to the paginated view""" req = self._cw params = dict(req.form) basepath = req.relative_path(includeparams=False) del params['__force_display'] url = self.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)') % self.page_size)) def render_link_display_all(self, w): """make a link to see them all""" req = self._cw params = dict(req.form) self.clean_params(params) basepath = req.relative_path(includeparams=False) params['__force_display'] = 1 params['__fromnavigation'] = 1 url = self.page_url(basepath, params) w(u'<div class="displayAllLink"><a href="%s">%s</a></div>\n' % (xml_escape(url), req._('show %s results') % len(self.cw_rset)))