# copyright 2003-2012 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 contains table views, with the following features that may beprovided (depending on the used implementation):* facets filtering* pagination* actions menu* properly sortable content* odd/row/hover line stylesThe three main implementation are described below. Each implementation issuitable for a particular case, but they each attempt to display tables thatlooks similar... autoclass:: cubicweb.web.views.tableview.RsetTableView :members:.. autoclass:: cubicweb.web.views.tableview.EntityTableView :members:.. autoclass:: cubicweb.web.views.pyviews.PyValTableView :members:All those classes are rendered using a *layout*:.. autoclass:: cubicweb.web.views.tableview.TableLayout :members:There is by default only on table layout, using the 'table_layout' identifier,that is referenced by table views:attr:`cubicweb.web.views.tableview.TableMixIn.layout_id`. If you want tocustomize the look and feel of your table, you can either replace the defaultone by yours, having multiple variants with proper selectors, or change the`layout_id` identifier of your table to use your table specific implementation.Notice you can gives options to the layout using a `layout_args` dictionary onyour class.If you can still find a view that suit your needs, you should take a look at theclass below that is the common abstract base class for the three views definedabove and implements you own class... autoclass:: cubicweb.web.views.tableview.TableMixIn :members:"""__docformat__="restructuredtext en"_=unicodefromwarningsimportwarnfromcopyimportcopyfromtypesimportMethodTypefromlogilab.mtconverterimportxml_escapefromlogilab.common.decoratorsimportcachedpropertyfromlogilab.common.deprecationimportclass_deprecatedfromlogilab.common.registryimportyesfromcubicwebimportNoSelectableObject,tagsfromcubicweb.predicatesimportnonempty_rset,match_kwargs,objectify_predicatefromcubicweb.schemaimportdisplay_namefromcubicweb.utilsimportmake_uid,js_dumps,JSString,UStringIOfromcubicweb.uilibimporttoggle_action,limitsize,htmlescape,sgml_attributes,domidfromcubicweb.viewimportEntityView,AnyRsetViewfromcubicweb.webimportjsonize,componentfromcubicweb.web.htmlwidgetsimport(TableWidget,TableColumn,MenuWidget,PopupBoxMenu)@objectify_predicatedefunreloadable_table(cls,req,rset=None,displaycols=None,headers=None,cellvids=None,paginate=False,displayactions=False,displayfilter=False,**kwargs):# one may wish to specify one of headers/displaycols/cellvids as long as he# doesn't want pagination nor actions nor facetsifnotkwargsand(displaycolsorheadersorcellvids)andnot(displayfilterordisplayactionsorpaginate):return1return0classTableLayout(component.Component):"""The default layout for table. When `render` is called, this will use the API described on :class:`TableMixIn` to feed the generated table. This layout behaviour may be customized using the following attributes / selection arguments: * `cssclass`, a string that should be used as HTML class attribute. Default to "listing". * `needs_css`, the CSS files that should be used together with this table. Default to ('cubicweb.tablesorter.css', 'cubicweb.tableview.css'). * `needs_js`, the Javascript files that should be used together with this table. Default to ('jquery.tablesorter.js',) * `display_filter`, tells if the facets filter should be displayed when possible. Allowed values are: - `None`, don't display it - 'top', display it above the table - 'bottom', display it below the table * `display_actions`, tells if a menu for available actions should be displayed when possible (see two following options). Allowed values are: - `None`, don't display it - 'top', display it above the table - 'bottom', display it below the table * `hide_filter`, when true (the default), facets filter will be hidden by default, with an action in the actions menu allowing to show / hide it. * `show_all_option`, when true, a *show all results* link will be displayed below the navigation component. * `add_view_actions`, when true, actions returned by view.table_actions() will be included in the actions menu. * `header_column_idx`, if not `None`, should be a colum index or a set of column index where <th> tags should be generated instead of <td> """#'# make emacs happier__regid__='table_layout'cssclass="listing"needs_css=('cubicweb.tableview.css',)needs_js=()display_filter=None# None / 'top' / 'bottom'display_actions='top'# None / 'top' / 'bottom'hide_filter=Trueshow_all_option=True# make navcomp generate a 'show all' results linkadd_view_actions=Falseheader_column_idx=Noneenable_sorting=Truesortvalue_limit=10tablesorter_settings={'textExtraction':JSString('cw.sortValueExtraction'),'selectorHeaders':"thead tr:first th[class='sortable']",# only plug on the first row}def_setup_tablesorter(self,divid):self._cw.add_css('cubicweb.tablesorter.css')self._cw.add_js('jquery.tablesorter.js')self._cw.add_onload('''$(document).ready(function() { $("#%s table").tablesorter(%s);});'''%(divid,js_dumps(self.tablesorter_settings)))def__init__(self,req,view,**kwargs):super(TableLayout,self).__init__(req,**kwargs)forkey,valinself.cw_extra_kwargs.items():ifhasattr(self.__class__,key)andnotkey[0]=='_':setattr(self,key,val)self.cw_extra_kwargs.pop(key)self.view=viewifself.header_column_idxisNone:self.header_column_idx=frozenset()elifisinstance(self.header_column_idx,int):self.header_column_idx=frozenset((self.header_column_idx,))@cachedpropertydefinitial_load(self):"""We detect a bit heuristically if we are built for the first time or from subsequent calls by the form filter or by the pagination hooks. """form=self._cw.formreturn'fromformfilter'notinformand'__fromnavigation'notinformdefrender(self,w,**kwargs):assertself.display_filterin(None,'top','bottom'),self.display_filterifself.needs_css:self._cw.add_css(self.needs_css)ifself.needs_js:self._cw.add_js(self.needs_js)ifself.enable_sorting:self._setup_tablesorter(self.view.domid)# Notice facets form must be rendered **outside** the main div as it# shouldn't be rendered on ajax call subsequent to facet restriction# (hence the 'fromformfilter' parameter added by the formgenerate_form=self.initial_loadifself.display_filterandgenerate_form:facetsform=self.view.facets_form()else:facetsform=Noneiffacetsformandself.display_filter=='top':cssclass=u'hidden'ifself.hide_filterelseu''facetsform.render(w,vid=self.view.__regid__,cssclass=cssclass,divid=self.view.domid)actions=[]ifself.display_actions:ifself.add_view_actions:actions=self.view.table_actions()ifself.display_filterandself.hide_filterand(facetsformornotgenerate_form):actions+=self.show_hide_filter_actions(notgenerate_form)self.render_table(w,actions,self.view.paginable)iffacetsformandself.display_filter=='bottom':cssclass=u'hidden'ifself.hide_filterelseu''facetsform.render(w,vid=self.view.__regid__,cssclass=cssclass,divid=self.view.domid)defrender_table_headers(self,w,colrenderers):w(u'<thead><tr>')forcolrendererincolrenderers:ifcolrenderer.sortable:w(u'<th class="sortable">')else:w(u'<th>')colrenderer.render_header(w)w(u'</th>')w(u'</tr></thead>\n')defrender_table_body(self,w,colrenderers):w(u'<tbody>')forrownuminxrange(self.view.table_size):self.render_row(w,rownum,colrenderers)w(u'</tbody>')defrender_table(self,w,actions,paginate):view=self.viewdivid=view.domidifdividisnotNone:w(u'<div id="%s">'%divid)else:assertnot(actionsorpaginate)nav_html=UStringIO()ifpaginate:view.paginate(w=nav_html.write,show_all_option=self.show_all_option)w(nav_html.getvalue())ifactionsandself.display_actions=='top':self.render_actions(w,actions)colrenderers=view.build_column_renderers()attrs=self.table_attributes()w(u'<table %s>'%sgml_attributes(attrs))ifself.view.has_headers:self.render_table_headers(w,colrenderers)self.render_table_body(w,colrenderers)w(u'</table>')ifactionsandself.display_actions=='bottom':self.render_actions(w,actions)w(nav_html.getvalue())ifdividisnotNone:w(u'</div>')deftable_attributes(self):return{'class':self.cssclass}defrender_row(self,w,rownum,renderers):attrs=self.row_attributes(rownum)w(u'<tr %s>'%sgml_attributes(attrs))forcolnum,rendererinenumerate(renderers):self.render_cell(w,rownum,colnum,renderer)w(u'</tr>\n')defrow_attributes(self,rownum):return{'class':'odd'if(rownum%2==1)else'even','onmouseover':'$(this).addClass("highlighted");','onmouseout':'$(this).removeClass("highlighted")'}defrender_cell(self,w,rownum,colnum,renderer):attrs=self.cell_attributes(rownum,colnum,renderer)ifcolnuminself.header_column_idx:tag=u'th'else:tag=u'td'w(u'<%s%s>'%(tag,sgml_attributes(attrs)))renderer.render_cell(w,rownum)w(u'</%s>'%tag)defcell_attributes(self,rownum,_colnum,renderer):attrs=renderer.attributes.copy()ifrenderer.sortable:sortvalue=renderer.sortvalue(rownum)ifisinstance(sortvalue,basestring):sortvalue=sortvalue[:self.sortvalue_limit]ifsortvalueisnotNone:attrs[u'cubicweb:sortvalue']=js_dumps(sortvalue)returnattrsdefrender_actions(self,w,actions):box=MenuWidget('','',_class='tableActionsBox',islist=False)label=tags.span(self._cw._('action menu'))menu=PopupBoxMenu(label,isitem=False,link_class='actionsBox',ident='%sActions'%self.view.domid)box.append(menu)foractioninactions:menu.append(action)box.render(w=w)w(u'<div class="clear"></div>')defshow_hide_filter_actions(self,currentlydisplayed=False):divid=self.view.domidshowhide=u';'.join(toggle_action('%s%s'%(divid,what))[11:]forwhatin('Form','Show','Hide','Actions'))showhide='javascript:'+showhideself._cw.add_onload(u'''\$(document).ready(function() { if ($('#%(id)sForm[class=\"hidden\"]').length) { $('#%(id)sHide').attr('class', 'hidden'); } else { $('#%(id)sShow').attr('class', 'hidden'); }});'''%{'id':divid})showlabel=self._cw._('show filter form')hidelabel=self._cw._('hide filter form')return[component.Link(showhide,showlabel,id='%sShow'%divid),component.Link(showhide,hidelabel,id='%sHide'%divid)]classAbstractColumnRenderer(object):"""Abstract base class for column renderer. Interface of a column renderer follows: .. automethod:: cubicweb.web.views.tableview.AbstractColumnRenderer.bind .. automethod:: cubicweb.web.views.tableview.AbstractColumnRenderer.render_header .. automethod:: cubicweb.web.views.tableview.AbstractColumnRenderer.render_cell .. automethod:: cubicweb.web.views.tableview.AbstractColumnRenderer.sortvalue Attributes on this base class are: :attr: `header`, the column header. If None, default to `_(colid)` :attr: `addcount`, if True, add the table size in parenthezis beside the header :attr: `trheader`, should the header be translated :attr: `escapeheader`, should the header be xml_escaped :attr: `sortable`, tell if the column is sortable :attr: `view`, the table view :attr: `_cw`, the request object :attr: `colid`, the column identifier :attr: `attributes`, dictionary of attributes to put on the HTML tag when the cell is rendered """#'# make emacsattributes={}empty_cell_content=u' 'def__init__(self,header=None,addcount=False,trheader=True,escapeheader=True,sortable=True):self.header=headerself.trheader=trheaderself.escapeheader=escapeheaderself.addcount=addcountself.sortable=sortableself.view=Noneself._cw=Noneself.colid=Nonedef__str__(self):return'<%s.%s (column %s) at 0x%x>'%(self.view.__class__.__name__,self.__class__.__name__,self.colid,id(self))defbind(self,view,colid):"""Bind the column renderer to its view. This is where `_cw`, `view`, `colid` are set and the method to override if you want to add more view/request depending attributes on your column render. """self.view=viewself._cw=view._cwself.colid=coliddefcopy(self):assertself.viewisNonereturncopy(self)defdefault_header(self):"""Return header for this column if one has not been specified."""returnself._cw._(self.colid)defrender_header(self,w):"""Write label for the specified column by calling w()."""header=self.headerifheaderisNone:header=self.default_header()elifself.trheaderandheader:header=self._cw._(header)ifself.addcount:header='%s (%s)'%(header,self.view.table_size)ifheader:ifself.escapeheader:header=xml_escape(header)else:header=self.empty_cell_contentifself.sortable:header=tags.span(header,escapecontent=False,title=self._cw._('Click to sort on this column'))w(header)defrender_cell(self,w,rownum):"""Write value for the specified cell by calling w(). :param `rownum`: the row number in the table """raiseNotImplementedError()defsortvalue(self,_rownum):"""Return typed value to be used for sorting on the specified column. :param `rownum`: the row number in the table """returnNoneclassTableMixIn(component.LayoutableMixIn):"""Abstract mix-in class for layout based tables. This default implementation's call method simply delegate to meth:`layout_render` that will select the renderer whose identifier is given by the :attr:`layout_id` attribute. Then it provides some default implementation for various parts of the API used by that layout. Abstract method you will have to override is: .. automethod:: build_column_renderers You may also want to overridde: .. autoattribute:: cubicweb.web.views.tableview.TableMixIn.table_size The :attr:`has_headers` boolean attribute tells if the table has some headers to be displayed. Default to `True`. """__abstract__=True# table layout to uselayout_id='table_layout'# true if the table has some headershas_headers=True# dictionary {colid : column renderer}column_renderers={}# default renderer class to use when no renderer specified for the columndefault_column_renderer_class=None# default layout handles inner paginationhandle_pagination=Truedefcall(self,**kwargs):self._cw.add_js('cubicweb.ajax.js')# for paginationself.layout_render(self.w)defcolumn_renderer(self,colid,*args,**kwargs):"""Return a column renderer for column of the given id."""try:crenderer=self.column_renderers[colid].copy()exceptKeyError:crenderer=self.default_column_renderer_class(*args,**kwargs)crenderer.bind(self,colid)returncrenderer# layout callbacks #########################################################deffacets_form(self,**kwargs):# XXX extracted from jqplot cubereturnself._cw.vreg['views'].select_or_none('facet.filtertable',self._cw,rset=self.cw_rset,view=self,**kwargs)@cachedpropertydefdomid(self):returnself._cw.form.get('divid')ordomid('%s-%s'%(self.__regid__,make_uid()))@propertydeftable_size(self):"""Return the number of rows (header excluded) to be displayed. By default return the number of rows in the view's result set. If your table isn't reult set based, override this method. """returnself.cw_rset.rowcountdefbuild_column_renderers(self):"""Return a list of column renderers, one for each column to be rendered. Prototype of a column renderer is described below: .. autoclass:: cubicweb.web.views.tableview.AbstractColumnRenderer """raiseNotImplementedError()deftable_actions(self):"""Return a list of actions (:class:`~cubicweb.web.component.Link`) that match the view's result set, and return those in the 'mainactions' category. """req=self._cwactions=[]actionsbycat=req.vreg['actions'].possible_actions(req,self.cw_rset)foractioninactionsbycat.get('mainactions',()):foractioninaction.actual_actions():actions.append(component.Link(action.url(),req._(action.title),klass=action.html_class()))returnactions# interaction with navigation component ####################################defpage_navigation_url(self,navcomp,_path,params):params['divid']=self.domidparams['vid']=self.__regid__returnnavcomp.ajax_page_url(**params)classRsetTableColRenderer(AbstractColumnRenderer):"""Default renderer for :class:`RsetTableView`."""def__init__(self,cellvid,**kwargs):super(RsetTableColRenderer,self).__init__(**kwargs)self.cellvid=cellviddefbind(self,view,colid):super(RsetTableColRenderer,self).bind(view,colid)self.cw_rset=view.cw_rsetdefrender_cell(self,w,rownum):self._cw.view(self.cellvid,self.cw_rset,'empty-cell',row=rownum,col=self.colid,w=w)# limit value's length as much as possible (e.g. by returning the 10 first# characters of a string)defsortvalue(self,rownum):colid=self.colidval=self.cw_rset[rownum][colid]ifvalisNone:returnu''etype=self.cw_rset.description[rownum][colid]ifetypeisNone:returnu''ifself._cw.vreg.schema.eschema(etype).final:entity,rtype=self.cw_rset.related_entity(rownum,colid)ifentityisNone:returnval# remove_html_tags() ?returnentity.sortvalue(rtype)entity=self.cw_rset.get_entity(rownum,colid)returnentity.sortvalue()classRsetTableView(TableMixIn,AnyRsetView):"""This table view accepts any non-empty rset. It uses introspection on the result set to compute column names and the proper way to display the cells. It is highly configurable and accepts a wealth of options, but take care to check what you're trying to achieve wouldn't be a job for the :class:`EntityTableView`. Basically the question is: does this view should be tied to the result set query's shape or no? If yes, than you're fine. If no, you should take a look at the other table implementation. The following class attributes may be used to control the table: * `finalvid`, a view identifier that should be called on final entities (e.g. attribute values). Default to 'final'. * `nonfinalvid`, a view identifier that should be called on entities. Default to 'incontext'. * `displaycols`, if not `None`, should be a list of rset's columns to be displayed. * `headers`, if not `None`, should be a list of headers for the table's columns. `None` values in the list will be replaced by computed column names. * `cellvids`, if not `None`, should be a dictionary with table column index as key and a view identifier as value, telling the view that should be used in the given column. Notice `displaycols`, `headers` and `cellvids` may be specified at selection time but then the table won't have pagination and shouldn't be configured to display the facets filter nor actions (as they wouldn't behave as expected). This table class use the :class:`RsetTableColRenderer` as default column renderer. .. autoclass:: RsetTableColRenderer """#'# make emacs happier__regid__='table'# selector trick for bw compath with the former :class:TableView__select__=AnyRsetView.__select__&(~match_kwargs('title','subvid','displayfilter','headers','displaycols','displayactions','actions','divid','cellvids','cellattrs','mainindex','paginate','page_size',mode='any')|unreloadable_table())title=_('table')# additional configuration parametersfinalvid='final'nonfinalvid='incontext'displaycols=Noneheaders=Nonecellvids=Nonedefault_column_renderer_class=RsetTableColRendererdeflinkable(self):# specific subclasses of this view usually don't want to be linkable# since they depends on a particular shape (being linkable meaning view# may be listed in possible viewsreturnself.__regid__=='table'defcall(self,headers=None,displaycols=None,cellvids=None,paginate=None,**kwargs):ifself.headers:self.headers=[handself._cw._(h)forhinself.headers]if(headersordisplaycolsorcellvidsorpaginate):ifheadersisnotNone:self.headers=headersifdisplaycolsisnotNone:self.displaycols=displaycolsifcellvidsisnotNone:self.cellvids=cellvidsifpaginateisnotNone:self.paginable=paginateifkwargs:# old table view arguments that we can safely ignore thanks to# selectorsiflen(kwargs)>1:msg='[3.14] %s arguments are deprecated'%', '.join(kwargs)else:msg='[3.14] %s argument is deprecated'%', '.join(kwargs)warn(msg,DeprecationWarning,stacklevel=2)super(RsetTableView,self).call(**kwargs)defmain_var_index(self):"""returns the index of the first non-attribute variable among the RQL selected variables """eschema=self._cw.vreg.schema.eschemafori,etypeinenumerate(self.cw_rset.description[0]):ifnoteschema(etype).final:returnireturnNone# layout callbacks #########################################################@propertydeftable_size(self):"""return the number of rows (header excluded) to be displayed"""returnself.cw_rset.rowcountdefbuild_column_renderers(self):headers=self.headers# compute displayed columnsifself.displaycolsisNone:ifheadersisnotNone:displaycols=range(len(headers))else:rqlst=self.cw_rset.syntax_tree()displaycols=range(len(rqlst.children[0].selection))else:displaycols=self.displaycols# compute table headersmain_var_index=self.main_var_index()computed_titles=self.columns_labels(main_var_index)# compute build rendererscellvids=self.cellvidsrenderers=[]forcolnum,colidinenumerate(displaycols):addcount=False# compute column headertitle=NoneifheadersisnotNone:title=headers[colnum]iftitleisNone:title=computed_titles[colid]ifcolid==main_var_index:addcount=True# compute cell vid for the columnifcellvidsisnotNoneandcolnumincellvids:cellvid=cellvids[colnum]else:coltype=self.cw_rset.description[0][colid]ifcoltypeisnotNoneandself._cw.vreg.schema.eschema(coltype).final:cellvid=self.finalvidelse:cellvid=self.nonfinalvid# get rendererrenderer=self.column_renderer(colid,header=title,trheader=False,addcount=addcount,cellvid=cellvid)renderers.append(renderer)returnrenderersclassEntityTableColRenderer(AbstractColumnRenderer):"""Default column renderer for :class:`EntityTableView`. You may use the :meth:`entity` method to retrieve the main entity for a given row number. .. automethod:: cubicweb.web.views.tableview.EntityTableColRenderer.entity .. automethod:: cubicweb.web.views.tableview.EntityTableColRenderer.render_entity .. automethod:: cubicweb.web.views.tableview.EntityTableColRenderer.entity_sortvalue """def__init__(self,renderfunc=None,sortfunc=None,sortable=None,**kwargs):ifrenderfuncisNone:renderfunc=self.render_entity# if renderfunc nor sortfunc nor sortable specified, column will be# sortable using the default implementation.ifsortableisNone:sortable=True# no sortfunc given but asked to be sortable: use the default sort# method. Sub-class may set `entity_sortvalue` to None if they don't# support sorting.ifsortfuncisNoneandsortable:sortfunc=self.entity_sortvalue# at this point `sortable` may still be unspecified while `sortfunc` is# sure to be set to someting else than None if the column is sortable.sortable=sortfuncisnotNonesuper(EntityTableColRenderer,self).__init__(sortable=sortable,**kwargs)self.renderfunc=renderfuncself.sortfunc=sortfuncdefcopy(self):assertself.viewisNone# copy of attribute referencing a method doesn't work with python < 2.7renderfunc=self.__dict__.pop('renderfunc')sortfunc=self.__dict__.pop('sortfunc')try:acopy=copy(self)foraname,memberin[('renderfunc',renderfunc),('sortfunc',sortfunc)]:ifisinstance(member,MethodType):member=MethodType(member.im_func,acopy,acopy.__class__)setattr(acopy,aname,member)returnacopyfinally:self.renderfunc=renderfuncself.sortfunc=sortfuncdefrender_cell(self,w,rownum):entity=self.entity(rownum)ifentityisNone:w(self.empty_cell_content)else:self.renderfunc(w,entity)defsortvalue(self,rownum):entity=self.entity(rownum)ifentityisNone:returnNoneelse:returnself.sortfunc(entity)defentity(self,rownum):"""Convenience method returning the table's main entity."""returnself.view.entity(rownum)defrender_entity(self,w,entity):"""Sort value if `renderfunc` nor `sortfunc` specified at initialization. This default implementation consider column id is an entity attribute and print its value. """w(entity.printable_value(self.colid))defentity_sortvalue(self,entity):"""Cell rendering implementation if `renderfunc` nor `sortfunc` specified at initialization. This default implementation consider column id is an entity attribute and return its sort value by calling `entity.sortvalue(colid)`. """returnentity.sortvalue(self.colid)classMainEntityColRenderer(EntityTableColRenderer):"""Renderer to be used for the column displaying the 'main entity' of a :class:`EntityTableView`. By default display it using the 'incontext' view. You may specify another view identifier using the `vid` argument. If header not specified, it would be built using entity types in the main column. """def__init__(self,vid='incontext',addcount=True,**kwargs):super(MainEntityColRenderer,self).__init__(addcount=addcount,**kwargs)self.vid=viddefdefault_header(self):view=self.viewiflen(view.cw_rset)>1:suffix='_plural'else:suffix=''returnu', '.join(self._cw.__(et+suffix)foretinview.cw_rset.column_types(view.cw_color0))defrender_entity(self,w,entity):entity.view(self.vid,w=w)defentity_sortvalue(self,entity):returnentity.sortvalue()classRelatedEntityColRenderer(MainEntityColRenderer):"""Renderer to be used for column displaying an entity related the 'main entity' of a :class:`EntityTableView`. By default display it using the 'incontext' view. You may specify another view identifier using the `vid` argument. If header not specified, it would be built by translating the column id. """def__init__(self,getrelated,addcount=False,**kwargs):super(RelatedEntityColRenderer,self).__init__(addcount=addcount,**kwargs)self.getrelated=getrelateddefentity(self,rownum):entity=super(RelatedEntityColRenderer,self).entity(rownum)returnself.getrelated(entity)defdefault_header(self):returnself._cw._(self.colid)classRelationColRenderer(EntityTableColRenderer):"""Renderer to be used for column displaying a list of entities related the 'main entity' of a :class:`EntityTableView`. By default, the main entity is considered as the subject of the relation but you may specify otherwise using the `role` argument. By default display the related rset using the 'csv' view, using 'outofcontext' sub-view for each entity. You may specify another view identifier using respectivly the `vid` and `subvid` arguments. If you specify a 'rtype view', such as 'reledit', you should add a is_rtype_view=True parameter. If header not specified, it would be built by translating the column id, properly considering role. """def__init__(self,role='subject',vid='csv',subvid=None,fallbackvid='empty-cell',is_rtype_view=False,**kwargs):super(RelationColRenderer,self).__init__(**kwargs)self.role=roleself.vid=vidifsubvidisNoneandvidin('csv','list'):subvid='outofcontext'self.subvid=subvidself.fallbackvid=fallbackvidself.is_rtype_view=is_rtype_viewdefrender_entity(self,w,entity):kwargs={'w':w}ifself.is_rtype_view:rset=Nonekwargs['entity']=entitykwargs['rtype']=self.colidkwargs['role']=self.roleelse:rset=entity.related(self.colid,self.role)ifself.subvidisnotNone:kwargs['subvid']=self.subvidself._cw.view(self.vid,rset,self.fallbackvid,**kwargs)defdefault_header(self):returndisplay_name(self._cw,self.colid,self.role)entity_sortvalue=None# column not sortable by defaultclassEntityTableView(TableMixIn,EntityView):"""This abstract table view is designed to be used with an :class:`is_instance()` or :class:`adaptable` predicate, hence doesn't depend the result set shape as the :class:`TableView` does. It will display columns that should be defined using the `columns` class attribute containing a list of column ids. By default, each column is renderered by :class:`EntityTableColRenderer` which consider that the column id is an attribute of the table's main entity (ie the one for which the view is selected). You may wish to specify :class:`MainEntityColRenderer` or :class:`RelatedEntityColRenderer` renderer for a column in the :attr:`column_renderers` dictionary. .. autoclass:: cubicweb.web.views.tableview.EntityTableColRenderer .. autoclass:: cubicweb.web.views.tableview.MainEntityColRenderer .. autoclass:: cubicweb.web.views.tableview.RelatedEntityColRenderer .. autoclass:: cubicweb.web.views.tableview.RelationColRenderer """__abstract__=Truedefault_column_renderer_class=EntityTableColRenderercolumns=None# to be defined in concret classdefcall(self,columns=None,**kwargs):ifcolumnsisnotNone:self.columns=columnsself.layout_render(self.w)@propertydeftable_size(self):returnself.cw_rset.rowcountdefbuild_column_renderers(self):return[self.column_renderer(colid)forcolidinself.columns]defentity(self,rownum):"""Return the table's main entity"""returnself.cw_rset.get_entity(rownum,self.cw_color0)classEmptyCellView(AnyRsetView):__regid__='empty-cell'__select__=yes()defcall(self,**kwargs):self.w(u' ')cell_call=call################################################################################# DEPRECATED tables ############################################################################################################################################classTableView(AnyRsetView):"""The table view accepts any non-empty rset. It uses introspection on the result set to compute column names and the proper way to display the cells. It is however highly configurable and accepts a wealth of options. """__metaclass__=class_deprecated__deprecation_warning__='[3.14] %(cls)s is deprecated'__regid__='table'title=_('table')finalview='final'table_widget_class=TableWidgettable_column_class=TableColumntablesorter_settings={'textExtraction':JSString('cw.sortValueExtraction'),'selectorHeaders':'thead tr:first th',# only plug on the first row}handle_pagination=Truedefform_filter(self,divid,displaycols,displayactions,displayfilter,paginate,hidden=True):try:filterform=self._cw.vreg['views'].select('facet.filtertable',self._cw,rset=self.cw_rset)exceptNoSelectableObject:return()vidargs={'paginate':paginate,'displaycols':displaycols,'displayactions':displayactions,'displayfilter':displayfilter}cssclass=hiddenand'hidden'or''filterform.render(self.w,vid=self.__regid__,divid=divid,vidargs=vidargs,cssclass=cssclass)returnself.show_hide_actions(divid,nothidden)defmain_var_index(self):"""Returns the index of the first non final variable of the rset. Used to select the main etype to help generate accurate column headers. XXX explain the concept May return None if none is found. """eschema=self._cw.vreg.schema.eschemafori,etypeinenumerate(self.cw_rset.description[0]):try:ifnoteschema(etype).final:returniexceptKeyError:# XXX possible?continuereturnNonedefdisplaycols(self,displaycols,headers):ifdisplaycolsisNone:if'displaycols'inself._cw.form:displaycols=[int(idx)foridxinself._cw.form['displaycols']]elifheadersisnotNone:displaycols=range(len(headers))else:displaycols=range(len(self.cw_rset.syntax_tree().children[0].selection))returndisplaycolsdef_setup_tablesorter(self,divid):req=self._cwreq.add_js('jquery.tablesorter.js')req.add_onload('''$(document).ready(function() { $("#%s table.listing").tablesorter(%s);});'''%(divid,js_dumps(self.tablesorter_settings)))req.add_css(('cubicweb.tablesorter.css','cubicweb.tableview.css'))@cachedpropertydefinitial_load(self):"""We detect a bit heuristically if we are built for the first time of from subsequent calls by the form filter or by the pagination hooks """form=self._cw.formreturn'fromformfilter'notinformand'__start'notinformdefcall(self,title=None,subvid=None,displayfilter=None,headers=None,displaycols=None,displayactions=None,actions=(),divid=None,cellvids=None,cellattrs=None,mainindex=None,paginate=False,page_size=None):"""Produces a table displaying a composite query :param title: title added before table :param subvid: cell view :param displayfilter: filter that selects rows to display :param headers: columns' titles :param displaycols: indexes of columns to display (first column is 0) :param displayactions: if True, display action menu """req=self._cwdivid=dividorreq.form.get('divid')or'rs%s'%make_uid(id(self.cw_rset))self._setup_tablesorter(divid)# compute label first since the filter form may remove some necessary# information from the rql syntax treeifmainindexisNone:mainindex=self.main_var_index()computed_labels=self.columns_labels(mainindex)ifnotsubvidand'subvid'inreq.form:subvid=req.form.pop('subvid')actions=list(actions)ifmainindexisNone:displayfilter,displayactions=False,Falseelse:ifdisplayfilterisNoneandreq.form.get('displayfilter'):displayfilter=TrueifdisplayactionsisNoneandreq.form.get('displayactions'):displayactions=Truedisplaycols=self.displaycols(displaycols,headers)ifself.initial_load:self.w(u'<div class="section">')ifnottitleand'title'inreq.form:title=req.form['title']iftitle:self.w(u'<h2 class="tableTitle">%s</h2>\n'%title)ifdisplayfilter:actions+=self.form_filter(divid,displaycols,displayfilter,displayactions,paginate)elifdisplayfilter:actions+=self.show_hide_actions(divid,True)self.w(u'<div id="%s">'%divid)ifdisplayactions:actionsbycat=self._cw.vreg['actions'].possible_actions(req,self.cw_rset)foractioninactionsbycat.get('mainactions',()):foractioninaction.actual_actions():actions.append((action.url(),req._(action.title),action.html_class(),None))# render actions menuifactions:self.render_actions(divid,actions)# render tableifpaginate:self.divid=divid# XXX iirk (see usage in page_navigation_url)self.paginate(page_size=page_size,show_all_option=False)table=self.table_widget_class(self)forcolumninself.get_columns(computed_labels,displaycols,headers,subvid,cellvids,cellattrs,mainindex):table.append_column(column)table.render(self.w)self.w(u'</div>\n')ifself.initial_load:self.w(u'</div>\n')defpage_navigation_url(self,navcomp,path,params):"""Build an url to the current view using the <navcomp> attributes :param navcomp: a NavigationComponent to call an url method on. :param path: expected to be json here ? :param params: params to give to build_url method this is called by :class:`cubiweb.web.component.NavigationComponent` """ifhasattr(self,'divid'):# XXX this assert a single callparams['divid']=self.dividparams['vid']=self.__regid__returnnavcomp.ajax_page_url(**params)defshow_hide_actions(self,divid,currentlydisplayed=False):showhide=u';'.join(toggle_action('%s%s'%(divid,what))[11:]forwhatin('Form','Show','Hide','Actions'))showhide='javascript:'+showhideshowlabel=self._cw._('show filter form')hidelabel=self._cw._('hide filter form')ifcurrentlydisplayed:return[(showhide,showlabel,'hidden','%sShow'%divid),(showhide,hidelabel,None,'%sHide'%divid)]return[(showhide,showlabel,None,'%sShow'%divid),(showhide,hidelabel,'hidden','%sHide'%divid)]defrender_actions(self,divid,actions):box=MenuWidget('','tableActionsBox',_class='',islist=False)label=tags.img(src=self._cw.uiprops['PUCE_DOWN'],alt=xml_escape(self._cw._('action(s) on this selection')))menu=PopupBoxMenu(label,isitem=False,link_class='actionsBox',ident='%sActions'%divid)box.append(menu)forurl,label,klass,identinactions:menu.append(component.Link(url,label,klass=klass,id=ident))box.render(w=self.w)self.w(u'<div class="clear"></div>')defget_columns(self,computed_labels,displaycols,headers,subvid,cellvids,cellattrs,mainindex):"""build columns description from various parameters : computed_labels: columns headers computed from rset to be used if there is no headers entry : displaycols: see :meth:`call` : headers: explicitly define columns headers : subvid: see :meth:`call` : cellvids: see :meth:`call` : cellattrs: see :meth:`call` : mainindex: see :meth:`call` return a list of columns description to be used by :class:`~cubicweb.web.htmlwidgets.TableWidget` """columns=[]eschema=self._cw.vreg.schema.eschemaforcolindex,labelinenumerate(computed_labels):ifcolindexnotindisplaycols:continue# compute column headerifheadersisnotNone:_label=headers[displaycols.index(colindex)]if_labelisnotNone:label=_labelifcolindex==mainindexandlabelisnotNone:label+=' (%s)'%self.cw_rset.rowcountcolumn=self.table_column_class(label,colindex)coltype=self.cw_rset.description[0][colindex]# compute column cell view (if coltype is None, it's a left outer# join, use the default non final subvid)ifcellvidsandcolindexincellvids:column.append_renderer(cellvids[colindex],colindex)elifcoltypeisnotNoneandeschema(coltype).final:column.append_renderer(self.finalview,colindex)else:column.append_renderer(subvidor'incontext',colindex)ifcellattrsandcolindexincellattrs:forname,valueincellattrs[colindex].iteritems():column.add_attr(name,value)# add columncolumns.append(column)returncolumnsdefrender_cell(self,cellvid,row,col,w):self._cw.view('cell',self.cw_rset,row=row,col=col,cellvid=cellvid,w=w)defget_rows(self):returnself.cw_rset@htmlescape@jsonize@limitsize(10)defsortvalue(self,row,col):# XXX it might be interesting to try to limit value's# length as much as possible (e.g. by returning the 10# first characters of a string)val=self.cw_rset[row][col]ifvalisNone:returnu''etype=self.cw_rset.description[row][col]ifetypeisNone:returnu''ifself._cw.vreg.schema.eschema(etype).final:entity,rtype=self.cw_rset.related_entity(row,col)ifentityisNone:returnval# remove_html_tags() ?returnentity.sortvalue(rtype)entity=self.cw_rset.get_entity(row,col)returnentity.sortvalue()classEditableTableView(TableView):__regid__='editable-table'finalview='editable-final'title=_('editable-table')classCellView(EntityView):__metaclass__=class_deprecated__deprecation_warning__='[3.14] %(cls)s is deprecated'__regid__='cell'__select__=nonempty_rset()defcell_call(self,row,col,cellvid=None):""" :param row, col: indexes locating the cell value in view's result set :param cellvid: cell view (defaults to 'outofcontext') """etype,val=self.cw_rset.description[row][col],self.cw_rset[row][col]ifetypeisNoneornotself._cw.vreg.schema.eschema(etype).final:ifvalisNone:# This is usually caused by a left outer join and in that case,# regular views will most certainly fail if they don't have# a real eid# XXX if cellvid is e.g. reledit, we may wanna call it anywayself.w(u' ')else:self.wview(cellvidor'outofcontext',self.cw_rset,row=row,col=col)else:# XXX why do we need a fallback view here?self.wview(cellvidor'final',self.cw_rset,'null',row=row,col=col)classInitialTableView(TableView):"""same display as table view but consider two rql queries : * the default query (ie `rql` form parameter), which is only used to select this view and to build the filter form. This query should have the same structure as the actual without actual restriction (but link to restriction variables) and usually with a limit for efficiency (limit set to 2 is advised) * the actual query (`actualrql` form parameter) whose results will be displayed with default restrictions set """__regid__='initialtable'__select__=nonempty_rset()# should not be displayed in possible view since it expects some specific# parameterstitle=Nonedefcall(self,title=None,subvid=None,headers=None,divid=None,paginate=False,displaycols=None,displayactions=None,mainindex=None):"""Dumps a table displaying a composite query"""try:actrql=self._cw.form['actualrql']exceptKeyError:actrql=self.cw_rset.printable_rql()else:self._cw.ensure_ro_rql(actrql)displaycols=self.displaycols(displaycols,headers)ifdisplayactionsisNoneand'displayactions'inself._cw.form:displayactions=TrueifdividisNoneand'divid'inself._cw.form:divid=self._cw.form['divid']self.w(u'<div class="section">')ifnottitleand'title'inself._cw.form:# pop title so it's not displayed by the table view as welltitle=self._cw.form.pop('title')iftitle:self.w(u'<h2>%s</h2>\n'%title)ifmainindexisNone:mainindex=self.main_var_index()ifmainindexisnotNone:actions=self.form_filter(divid,displaycols,displayactions,displayfilter=True,paginate=paginate,hidden=True)else:actions=()ifnotsubvidand'subvid'inself._cw.form:subvid=self._cw.form.pop('subvid')self._cw.view('table',self._cw.execute(actrql),'noresult',w=self.w,displayfilter=False,subvid=subvid,displayactions=displayactions,displaycols=displaycols,actions=actions,headers=headers,divid=divid)self.w(u'</div>\n')classEditableInitialTableTableView(InitialTableView):__regid__='editable-initialtable'finalview='editable-final'classEntityAttributesTableView(EntityView):"""This table displays entity attributes in a table and allow to set a specific method to help building cell content for each attribute as well as column header. Table will render entity cell by using the appropriate build_COLNAME_cell methods if defined otherwise cell content will be entity.COLNAME. Table will render column header using the method header_for_COLNAME if defined otherwise COLNAME will be used. """__metaclass__=class_deprecated__deprecation_warning__='[3.14] %(cls)s is deprecated'__abstract__=Truecolumns=()table_css="listing"css_files=()defcall(self,columns=None):ifself.css_files:self._cw.add_css(self.css_files)_=self._cw._self.columns=columnsorself.columnssample=self.cw_rset.get_entity(0,0)self.w(u'<table class="%s">'%self.table_css)self.table_header(sample)self.w(u'<tbody>')forrowinxrange(self.cw_rset.rowcount):self.cell_call(row=row,col=0)self.w(u'</tbody>')self.w(u'</table>')defcell_call(self,row,col):_=self._cw._entity=self.cw_rset.get_entity(row,col)entity.complete()infos={}forcolinself.columns:meth=getattr(self,'build_%s_cell'%col,None)# find the build method or try to find matching attributeifmeth:content=meth(entity)else:content=entity.printable_value(col)infos[col]=contentself.w(u"""<tr onmouseover="$(this).addClass('highlighted');" onmouseout="$(this).removeClass('highlighted')">""")line=u''.join(u'<td>%%(%s)s</td>'%colforcolinself.columns)self.w(line%infos)self.w(u'</tr>\n')deftable_header(self,sample):"""builds the table's header"""self.w(u'<thead><tr>')forcolumninself.columns:meth=getattr(self,'header_for_%s'%column,None)ifmeth:colname=meth(sample)else:colname=self._cw._(column)self.w(u'<th>%s</th>'%xml_escape(colname))self.w(u'</tr></thead>\n')