# 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 webapplication.Pagination----------Several implementations for large result set pagination are provided:.. autoclass:: PageNavigation.. autoclass:: PageNavigationSelect.. autoclass:: SortedNavigationPagination 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:: paginatePrevious / next navigation--------------------------An adapter and its related component for the somewhat usal "previous / next"navigation are provided. .. autoclass:: IPrevNextAdapter .. autoclass:: NextPrevNavigationComponent"""__docformat__="restructuredtext en"_=unicodefromdatetimeimportdatetimefromrql.nodesimportVariableRef,Constantfromlogilab.mtconverterimportxml_escapefromlogilab.common.deprecationimportdeprecatedfromcubicweb.utilsimportjson_dumpsfromcubicweb.predicatesimportpaginated_rset,sorted_rset,adaptablefromcubicweb.uilibimportcutfromcubicweb.viewimportEntityAdapterfromcubicweb.web.componentimportEmptyComponent,EntityCtxComponent,NavigationComponentclassPageNavigation(NavigationComponent):"""The default pagination component: display link to pages where each pages is identified by the item number of its first and last elements. """defcall(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'[ %s ]'%u' | '.join(self.iter_page_links(basepath,params)))self.w(u'  %s'%self.next_link(basepath,params))self.w(u'</div>')defindex_display(self,start,stop):returnu'%s - %s'%(start+1,stop+1)defiter_page_links(self,basepath,params):rset=self.cw_rsetpage_size=self.page_sizestart=0whilestart<rset.rowcount:stop=min(start+page_size-1,rset.rowcount-1)yieldself.page_link(basepath,params,start,stop,self.index_display(start,stop))start=stop+1classPageNavigationSelect(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>'defcall(self):params=dict(self._cw.form)self.clean_params(params)basepath=self._cw.relative_path(includeparams=False)w=self.ww(u'<div class="pagination">')w(self.previous_link(basepath,params))w(u'<select onchange="javascript: document.location=this.options[this.selectedIndex].value">')foroptioninself.iter_page_links(basepath,params):w(option)w(u'</select>')w(u'  %s'%self.next_link(basepath,params))w(u'</div>')classSortedNavigation(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 linksnb_chars=5defcall(self):# attrname = the name of attribute according to which the sort# is done if anycol,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=0total=self.cw_rset.rowcountwhilestart<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+1self.write_links(basepath,params,blocklist)defdisplay_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. """ifattrnameisnotNone:defindex_display(row):ifnotrset[row][col]:# outer joinreturnu''entity=rset.get_entity(row,col)returnentity.printable_value(attrname,format='text/plain')elifcolisNone:# smart links disabled.defindex_display(row):returnunicode(row)elifself._cw.vreg.schema.eschema(rset.description[0][col]).final:defindex_display(row):returnunicode(rset[row][col])else:defindex_display(row):returnrset.get_entity(row,col).view('text')returnindex_displaydefsort_on(self):"""Return entity column number / attr name to use for nice display by inspecting the rset'syntax tree. """rschema=self._cw.vreg.schema.rschemaforsorterminself.cw_rset.syntax_tree().children[0].orderby:ifisinstance(sorterm.term,Constant):col=sorterm.term.value-1returncol,Nonevar=sorterm.term.get_nodes(VariableRef)[0].variablecol=Noneforrefinvar.references():rel=ref.relation()ifrelisNone:continueattrname=rel.r_typeifattrnamein('is','has_text'):continueifnotrschema(attrname).final:col=var.selected_index()attrname=NoneifcolisNone:# final relation or not selected non final relationifvarisrel.children[0]:relvar=rel.children[1].children[0].get_nodes(VariableRef)[0]else:relvar=rel.children[0].variablecol=relvar.selected_index()ifcolisnotNone:breakelse:# no relation but maybe usable anyway if selectedcol=var.selected_index()attrname=NoneifcolisnotNone:# if column type is date[time], set proper 'nb_chars'ifvar.stinfo['possibletypes']&frozenset(('TZDatetime','Datetime','Date')):self.nb_chars=len(self._cw.format_date(datetime.today()))returncol,attrname# nothing usable found, use the first columnreturn0,Nonedefformat_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])returnxml_escape(text)defwrite_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>')defdo_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._cwifrsetisNone:rset=view.cw_rsetifwisNone:w=view.wnav=req.vreg['components'].select_or_none('navigation',req,rset=rset,page_size=page_size,view=view)ifnav:domid=getattr(view,'domid','pageContent')view._cw.add_onload(''' jQuery('div.displayAllLink a, div.pagination a').click(function() { cw.jqNode(%s).loadxhtml(this.href, null, 'get', 'swap'); return false; }); '''%json_dumps(domid))ifwisNone:w=view.wifreq.form.get('__force_display'):# allow to come back to the paginated viewparams=dict(req.form)basepath=req.relative_path(includeparams=False)delparams['__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 renderingstart,stop=nav.page_boundaries()nav.render(w=w)params=dict(req.form)nav.clean_params(params)# make a link to see them allifshow_all_option:basepath=req.relative_path(includeparams=False)params['__force_display']=1params['__fromnavigation']=1url=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)defpaginate(view,show_all_option=True,w=None,page_size=None,rset=None):"""paginate results if the view is paginable """ifview.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 pagefromcubicweb.viewimportViewView.do_paginate=do_paginateView.paginate=paginateView.handle_pagination=FalseclassIPrevNextAdapter(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__=Truedefnext_entity(self):"""return the 'next' entity"""raiseNotImplementedErrordefprevious_entity(self):"""return the 'previous' entity"""raiseNotImplementedErrorclassNextPrevNavigationComponent(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# itselfhelp=_('ctxcomponents_prevnext_description')__select__=EntityCtxComponent.__select__&adaptable('IPrevNext')context='navbottom'order=10@propertydefprev_icon(self):return'<img src="%s" alt="%s" />'%(xml_escape(self._cw.data_url('go_prev.png')),self._cw._('previous page'))@propertydefnext_icon(self):return'<img src="%s" alt="%s" />'%(xml_escape(self._cw.data_url('go_next.png')),self._cw._('next page'))definit_rendering(self):adapter=self.entity.cw_adapt_to('IPrevNext')self.previous=adapter.previous_entity()self.next=adapter.next_entity()ifnot(self.previousorself.next):raiseEmptyComponent()defrender_body(self,w):w(u'<div class="prevnext">')self.prevnext(w)w(u'</div>')w(u'<div class="clear"></div>')defprevnext(self,w):ifself.previous:self.prevnext_entity(w,self.previous,'prev')ifself.next:self.prevnext_entity(w,self.next,'next')defprevnext_entity(self,w,entity,type):textsize=self._cw.property_value('navigation.short-line-size')content=xml_escape(cut(entity.dc_title(),textsize))iftype=='prev':title=self._cw._('i18nprevnext_previous')icon=self.prev_iconcssclass=u'previousEntity left'content=icon+'  '+contentelse:title=self._cw._('i18nprevnext_next')icon=self.next_iconcssclass=u'nextEntity right'content=content+'  '+iconself.prevnext_div(w,type,cssclass,entity.absolute_url(),title,content)defprevnext_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)))