# copyright 2003-2010 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/>."""The :mod:`cubicweb.web.facet` module contains a set of abstract classes to useas bases to build your own facetsAll facet classes inherits from the :class:`AbstractFacet` class, though you'llusually find some more handy class that do what you want.Let's see available classes.Classes you'll want to use--------------------------.. autoclass:: cubicweb.web.facet.RelationFacet.. autoclass:: cubicweb.web.facet.RelationAttributeFacet.. autoclass:: cubicweb.web.facet.HasRelationFacet.. autoclass:: cubicweb.web.facet.AttributeFacet.. autoclass:: cubicweb.web.facet.RangeFacet.. autoclass:: cubicweb.web.facet.DateRangeFacetClasses for facets implementor------------------------------Unless you didn't find the class that does the job you want above, you may wantto skip those classes..... autoclass:: cubicweb.web.facet.AbstractFacet.. autoclass:: cubicweb.web.facet.VocabularyFacet.. comment: XXX widgets"""__docformat__="restructuredtext en"fromcopyimportdeepcopyfromdatetimeimportdate,datetime,timedeltafromlogilab.mtconverterimportxml_escapefromlogilab.common.graphimporthas_pathfromlogilab.common.decoratorsimportcachedfromlogilab.common.dateimportdatetime2ticksfromlogilab.common.compatimportallfromrqlimportparse,nodesfromcubicwebimportUnauthorized,typed_eidfromcubicweb.schemaimportdisplay_namefromcubicweb.utilsimportmake_uidfromcubicweb.selectorsimportmatch_context_prop,partial_relation_possiblefromcubicweb.appobjectimportAppObjectfromcubicweb.web.htmlwidgetsimportHTMLWidgetdefrtype_facet_title(facet):ptypes=facet.cw_rset.column_types(0)iflen(ptypes)==1:returndisplay_name(facet._cw,facet.rtype,form=facet.role,context=iter(ptypes).next())returndisplay_name(facet._cw,facet.rtype,form=facet.role)## rqlst manipulation functions used by facets ################################defprepare_facets_rqlst(rqlst,args=None):"""prepare a syntax tree to generate facet filters * remove ORDERBY clause * cleanup selection (remove everything) * undefine unnecessary variables * set DISTINCT * unset LIMIT/OFFSET """iflen(rqlst.children)>1:raiseNotImplementedError('FIXME: union not yet supported')select=rqlst.children[0]mainvar=filtered_variable(select)select.set_limit(None)select.set_offset(None)baserql=select.as_string(kwargs=args)# cleanup sort termsselect.remove_sort_terms()# selection: only vocabulary entityforterminselect.selection[:]:select.remove_selected(term)# remove unbound variables which only have some type restrictionfordvarinselect.defined_vars.values():ifnot(dvarismainvarordvar.stinfo['relations']):select.undefine_variable(dvar)# global tree config: DISTINCT, LIMIT, OFFSETselect.set_distinct(True)returnmainvar,baserqldeffiltered_variable(rqlst):vref=rqlst.selection[0].iget_nodes(nodes.VariableRef).next()returnvref.variabledefget_facet(req,facetid,rqlst,mainvar):returnreq.vreg['facets'].object_by_id(facetid,req,rqlst=rqlst,filtered_variable=mainvar)deffilter_hiddens(w,**kwargs):forkey,valinkwargs.items():w(u'<input type="hidden" name="%s" value="%s" />'%(key,xml_escape(val)))def_may_be_removed(rel,schema,mainvar):"""if the given relation may be removed from the tree, return the variable on the other side of `mainvar`, else return None Conditions: * the relation is an attribute selection of the main variable * the relation is optional relation linked to the main variable * the relation is a mandatory relation linked to the main variable without any restriction on the other variable """lhs,rhs=rel.get_variable_parts()rschema=schema.rschema(rel.r_type)iflhs.variableismainvar:try:ovar=rhs.variableexceptAttributeError:# constant restriction# XXX: X title LOWER(T) if it makes sense?returnNoneifrschema.final:iflen(ovar.stinfo['relations'])==1:# attribute selectionreturnovarreturnNoneopt='right'cardidx=0elifgetattr(rhs,'variable',None)ismainvar:ovar=lhs.variableopt='left'cardidx=1else:# not directly linked to the main variablereturnNoneifrel.optionalin(opt,'both'):# optional relationreturnovarifall(rdef.cardinality[cardidx]in'1+'forrdefinrschema.rdefs.values()):# mandatory relation without any restriction on the other variablefororelinovar.stinfo['relations']:ifrelisorel:continueif_may_be_removed(orel,schema,ovar)isNone:returnNonereturnovarreturnNonedef_add_rtype_relation(rqlst,mainvar,rtype,role):"""add a relation relying `mainvar` to entities linked by the `rtype` relation (where `mainvar` has `role`) return the inserted variable for linked entities. """newvar=rqlst.make_variable()ifrole=='object':rqlst.add_relation(newvar,rtype,mainvar)else:rqlst.add_relation(mainvar,rtype,newvar)returnnewvardef_prepare_vocabulary_rqlst(rqlst,mainvar,rtype,role,select_target_entity=True):"""prepare a syntax tree to generate a filter vocabulary rql using the given relation: * create a variable to filter on this relation * add the relation * add the new variable to GROUPBY clause if necessary * add the new variable to the selection """newvar=_add_rtype_relation(rqlst,mainvar,rtype,role)ifselect_target_entity:ifrqlst.groupby:rqlst.add_group_var(newvar)rqlst.add_selected(newvar)# add is restriction if necessaryifmainvar.stinfo['typerel']isNone:etypes=frozenset(sol[mainvar.name]forsolinrqlst.solutions)rqlst.add_type_restriction(mainvar,etypes)returnnewvardef_remove_relation(rqlst,rel,var):"""remove a constraint relation from the syntax tree"""# remove the relationrqlst.remove_node(rel)# remove relations where the filtered variable appears on the# lhs and rhs is a constant restrictionextra=[]forvrelinvar.stinfo['relations']:ifvrelisrel:continueifvrel.children[0].variableisvar:ifnotvrel.children[1].get_nodes(nodes.Constant):extra.append(vrel)rqlst.remove_node(vrel)returnextradef_set_orderby(rqlst,newvar,sortasc,sortfuncname):ifsortfuncnameisNone:rqlst.add_sort_var(newvar,sortasc)else:vref=nodes.variable_ref(newvar)vref.register_reference()sortfunc=nodes.Function(sortfuncname)sortfunc.append(vref)term=nodes.SortTerm(sortfunc,sortasc)rqlst.add_sort_term(term)definsert_attr_select_relation(rqlst,mainvar,rtype,role,attrname,sortfuncname=None,sortasc=True,select_target_entity=True):"""modify a syntax tree to : * link a new variable to `mainvar` through `rtype` (where mainvar has `role`) * retrieve only the newly inserted variable and its `attrname` Sorting: * on `attrname` ascendant (`sortasc`=True) or descendant (`sortasc`=False) * on `sortfuncname`(`attrname`) if `sortfuncname` is specified * no sort if `sortasc` is None """_cleanup_rqlst(rqlst,mainvar)var=_prepare_vocabulary_rqlst(rqlst,mainvar,rtype,role,select_target_entity)# not found, create oneattrvar=rqlst.make_variable()rqlst.add_relation(var,attrname,attrvar)# if query is grouped, we have to add the attribute variableifrqlst.groupby:ifnotattrvarinrqlst.groupby:rqlst.add_group_var(attrvar)ifsortascisnotNone:_set_orderby(rqlst,attrvar,sortasc,sortfuncname)# add attribute variable to selectionrqlst.add_selected(attrvar)returnvardef_cleanup_rqlst(rqlst,mainvar):"""cleanup tree from unnecessary restriction: * attribute selection * optional relations linked to the main variable * mandatory relations linked to the main variable """ifrqlst.whereisNone:returnschema=rqlst.root.schematoremove=set()vargraph=deepcopy(rqlst.vargraph)# graph representing links between variableforrelinrqlst.where.get_nodes(nodes.Relation):ovar=_may_be_removed(rel,schema,mainvar)ifovarisnotNone:toremove.add(ovar)removed=set()whiletoremove:trvar=toremove.pop()trvarname=trvar.name# remove paths using this variable from the graphlinkedvars=vargraph.pop(trvarname)forovarnameinlinkedvars:vargraph[ovarname].remove(trvarname)# remove relation using this variableforrelintrvar.stinfo['relations']:ifrelinremoved:# already removedcontinuerqlst.remove_node(rel)removed.add(rel)rel=trvar.stinfo['typerel']ifrelisnotNoneandnotrelinremoved:rqlst.remove_node(rel)removed.add(rel)# cleanup groupby clauseifrqlst.groupby:forvrefinrqlst.groupby[:]:ifvref.name==trvarname:rqlst.remove_group_var(vref)# we can also remove all variables which are linked to this variable# and have no path to the main variableforovarnameinlinkedvars:ifovarname==mainvar.name:continueifnothas_path(vargraph,ovarname,mainvar.name):toremove.add(rqlst.defined_vars[ovarname])## base facet classes ##########################################################classAbstractFacet(AppObject):"""Abstract base class for all facets. Facets are stored in their own 'facets' registry. They are similar to contextual components since the use the following configurable properties: * `visible`, boolean flag telling if a facet should be displayed or not * `order`, integer to control facets display order * `context`, telling if a facet should be displayed in the table form filter (context = 'tablefilter') or in the facet box (context = 'facetbox') or in both (context = '') The following methods define the facet API: .. automethod:: cubicweb.web.facet.AbstractFacet.get_widget .. automethod:: cubicweb.web.facet.AbstractFacet.add_rql_restrictions Facets will have the following attributes set (beside the standard :class:`~cubicweb.appobject.AppObject` ones): * `rqlst`, the rql syntax tree being facetted * `filtered_variable`, the variable node in this rql syntax tree that we're interested in filtering Facets implementors may also be interested in the following properties / methods: .. automethod:: cubicweb.web.facet.AbstractFacet.operator .. automethod:: cubicweb.web.facet.AbstractFacet.rqlexec """__abstract__=True__registry__='facets'cw_property_defs={_('visible'):dict(type='Boolean',default=True,help=_('display the facet or not')),_('order'):dict(type='Int',default=99,help=_('display order of the facet')),_('context'):dict(type='String',default='',# None <-> bothvocabulary=(_('tablefilter'),_('facetbox'),''),help=_('context where this facet should be displayed, ''leave empty for both')),}visible=Truecontext=''needs_update=Falsestart_unfolded=Truecw_rset=None# ensure facets have a cw_rset attributedef__init__(self,req,rqlst=None,filtered_variable=None,**kwargs):super(AbstractFacet,self).__init__(req,**kwargs)assertrqlstisnotNoneassertfiltered_variable# take care: facet may be retreived using `object_by_id` from an ajax call# or from `select` using the result set to filterself.rqlst=rqlstself.filtered_variable=filtered_variable@propertydefoperator(self):"""Return the operator (AND or OR) to use for this facet when multiple values are selected. """# OR between selected values by defaultreturnself._cw.form.get(self.__regid__+'_andor','OR')defrqlexec(self,rql,args=None):"""Utility method to execute some rql queries, and simply returning an empty list if :exc:`Unauthorized` is raised. """try:returnself._cw.execute(rql,args)exceptUnauthorized:return[]defget_widget(self):"""Return the widget instance to use to display this facet, or None if the facet can't do anything valuable (only one value in the vocabulary for instance). """raiseNotImplementedErrordefadd_rql_restrictions(self):"""When some facet criteria has been updated, this method is called to add restriction for this facet into the rql syntax tree. It should get back its value in form parameters, and modify the syntax tree (`self.rqlst`) accordingly. """raiseNotImplementedErrorclassVocabularyFacet(AbstractFacet):"""This abstract class extend :class:`AbstractFacet` to use the :class:`FacetVocabularyWidget` as widget, suitable for facets that may restrict values according to a (usually computed) vocabulary. A class which inherits from VocabularyFacet must define at least these methods: .. automethod:: cubicweb.web.facet.VocabularyFacet.vocabulary .. automethod:: cubicweb.web.facet.VocabularyFacet.possible_values """needs_update=Truedefget_widget(self):"""Return the widget instance to use to display this facet. This implementation expects a .vocabulary method on the facet and return a combobox displaying this vocabulary. """vocab=self.vocabulary()iflen(vocab)<=1:returnNonewdg=FacetVocabularyWidget(self)selected=frozenset(typed_eid(eid)foreidinself._cw.list_form_param(self.__regid__))forlabel,valueinvocab:ifvalueisNone:wdg.append(FacetSeparator(label))else:wdg.append(FacetItem(self._cw,label,value,valueinselected))returnwdgdefvocabulary(self):"""Return vocabulary for this facet, eg a list of 2-uple (label, value). """raiseNotImplementedErrordefpossible_values(self):"""Return a list of possible values (as string since it's used to compare to a form value in javascript) for this facet. """raiseNotImplementedErrordefsupport_and(self):returnFalseclassRelationFacet(VocabularyFacet):"""Base facet to filter some entities according to other entities to which they are related. Create concret facet by inheriting from this class an then configuring it by setting class attribute described below. The relation is defined by the `rtype` and `role` attributes. The values displayed for related entities will be: * result of calling their `label_vid` view if specified * else their `target_attr` attribute value if specified * else their eid (you usually want something nicer...) When no `label_vid` is set, you will get translated value if `i18nable` is set. You can filter out target entity types by specifying `target_type` By default, vocabulary will be displayed sorted on `target_attr` value in an ascending way. You can control sorting with: * `sortfunc`: set this to a stored procedure name if you want to sort on the result of this function's result instead of direct value * `sortasc`: boolean flag to control ascendant/descendant sorting To illustrate this facet, let's take for example an *excerpt* of the schema of an office location search application: .. sourcecode:: python class Office(WorkflowableEntityType): price = Int(description='euros / m2 / HC / HT') surface = Int(description='m2') has_address = SubjectRelation('PostalAddress', cardinality='1?', composite='subject') proposed_by = SubjectRelation('Agency') We can simply define a facet to filter offices according to the agency proposing it: .. sourcecode:: python class AgencyFacet(RelationFacet): __regid__ = 'agency' # this facet should only be selected when visualizing offices __select__ = RelationFacet.__select__ & is_instance('Office') # this facet is a filter on the 'Agency' entities linked to the office # through the 'proposed_by' relation, where the office is the subject # of the relation rtype = 'has_address' # 'subject' is the default but setting it explicitly doesn't hurt... role = 'subject' # we want to display the agency's name target_attr = 'name' """__select__=partial_relation_possible()&match_context_prop()# class attributes to configure the relation facetrtype=Nonerole='subject'target_attr='eid'target_type=None# should value be internationalized (XXX could be guessed from the schema)i18nable=True# set this to a stored procedure name if you want to sort on the result of# this function's result instead of direct valuesortfunc=None# ascendant/descendant sortingsortasc=True# if you want to call a view on the entity instead of using `target_attr`label_vid=None# internal purpose_select_target_entity=Truetitle=property(rtype_facet_title)@propertydefrql_sort(self):"""return true if we can handle sorting in the rql query. E.g. if sortfunc is set or if we have not to transform the returned value (eg no label_vid and not i18nable) """returnself.sortfuncisnotNoneor(self.label_vidisNoneandnotself.i18nable)defvocabulary(self):"""return vocabulary for this facet, eg a list of 2-uple (label, value) """rqlst=self.rqlstrqlst.save_state()ifself.rql_sort:sort=self.sortascelse:sort=None# will be sorted on labeltry:mainvar=self.filtered_variablevar=insert_attr_select_relation(rqlst,mainvar,self.rtype,self.role,self.target_attr,self.sortfunc,sort,self._select_target_entity)ifself.target_typeisnotNone:rqlst.add_type_restriction(var,self.target_type)try:rset=self.rqlexec(rqlst.as_string(),self.cw_rset.args)except:self.exception('error while getting vocabulary for %s, rql: %s',self,rqlst.as_string())return()finally:rqlst.recover()# don't call rset_vocabulary on empty result set, it may be an empty# *list* (see rqlexec implementation)returnrsetandself.rset_vocabulary(rset)defpossible_values(self):"""return a list of possible values (as string since it's used to compare to a form value in javascript) for this facet """rqlst=self.rqlstrqlst.save_state()try:_cleanup_rqlst(rqlst,self.filtered_variable)ifself._select_target_entity:_prepare_vocabulary_rqlst(rqlst,self.filtered_variable,self.rtype,self.role,select_target_entity=True)else:insert_attr_select_relation(rqlst,self.filtered_variable,self.rtype,self.role,self.target_attr,select_target_entity=False)return[str(x)forx,inself.rqlexec(rqlst.as_string())]except:importtracebacktraceback.print_exc()finally:rqlst.recover()defrset_vocabulary(self,rset):ifself.i18nable:_=self._cw._else:_=unicodeifself.rql_sort:return[(_(label),eid)foreid,labelinrset]ifself.label_vidisNone:assertself.i18nablevalues=[(_(label),eid)foreid,labelinrset]else:values=[(entity.view(self.label_vid),entity.eid)forentityinrset.entities()]values=sorted(values)ifself.sortasc:returnvaluesreturnreversed(values)@cacheddefsupport_and(self):rschema=self._cw.vreg.schema.rschema(self.rtype)# XXX when called via ajax, no rset to compute possible typespossibletypes=self.cw_rsetandself.cw_rset.column_types(0)forrdefinrschema.rdefs.itervalues():ifpossibletypesisnotNone:ifself.role=='subject':ifnotrdef.subjectinpossibletypes:continueelifnotrdef.objectinpossibletypes:continueifrdef.role_cardinality(self.role)in'+*':returnTruereturnFalsedefadd_rql_restrictions(self):"""add restriction for this facet into the rql syntax tree"""value=self._cw.form.get(self.__regid__)ifnotvalue:returnmainvar=self.filtered_variablerestrvar=_add_rtype_relation(self.rqlst,mainvar,self.rtype,self.role)ifisinstance(value,basestring):# only one value selectedself.rqlst.add_eid_restriction(restrvar,value)elifself.operator=='OR':# set_distinct only if rtype cardinality is > 1ifself.support_and():self.rqlst.set_distinct(True)# multiple ORed values: using IN is fineself.rqlst.add_eid_restriction(restrvar,value)else:# multiple values with AND operatorself.rqlst.add_eid_restriction(restrvar,value.pop())whilevalue:restrvar=_add_rtype_relation(self.rqlst,mainvar,self.rtype,self.role)self.rqlst.add_eid_restriction(restrvar,value.pop())classRelationAttributeFacet(RelationFacet):"""Base facet to filter some entities according to an attribute of other entities to which they are related. Most things work similarly as :class:`RelationFacet`, except that: * `label_vid` doesn't make sense here * you should specify the attribute type using `attrtype` if it's not a String * you can specify a comparison operator using `comparator` Back to our example... if you want to search office by postal code and that you use a :class:`RelationFacet` for that, you won't get the expected behaviour: if two offices have the same postal code, they've however two different addresses. So you'll see in the facet the same postal code twice, though linked to a different address entity. There is a great chance your users won't understand that... That's where this class come in ! It's used to said that you want to filter according to the *attribute value* of a relatied entity, not to the entity itself. Now here is the source code for the facet: .. sourcecode:: python class PostalCodeFacet(RelationAttributeFacet): __regid__ = 'postalcode' # this facet should only be selected when visualizing offices __select__ = RelationAttributeFacet.__select__ & is_instance('Office') # this facet is a filter on the PostalAddress entities linked to the # office through the 'has_address' relation, where the office is the # subject of the relation rtype = 'has_address' role = 'subject' # we want to search according to address 'postal_code' attribute target_attr = 'postalcode' """_select_target_entity=False# attribute typeattrtype='String'# type of comparison: default is an exact match on the attribute valuecomparator='='# could be '<', '<=', '>', '>='defrset_vocabulary(self,rset):ifself.i18nable:_=self._cw._else:_=unicodeifself.rql_sort:return[(_(value),value)forvalue,inrset]values=[(_(value),value)forvalue,inrset]ifself.sortasc:returnsorted(values)returnreversed(sorted(values))defadd_rql_restrictions(self):"""add restriction for this facet into the rql syntax tree"""value=self._cw.form.get(self.__regid__)ifnotvalue:returnmainvar=self.filtered_variablerestrvar=_add_rtype_relation(self.rqlst,mainvar,self.rtype,self.role)self.rqlst.set_distinct(True)ifisinstance(value,basestring)orself.operator=='OR':# only one value selected or multiple ORed values: using IN is fineself.rqlst.add_constant_restriction(restrvar,self.target_attr,value,self.attrtype,self.comparator)else:# multiple values with AND operatorself.rqlst.add_constant_restriction(restrvar,self.target_attr,value.pop(),self.attrtype,self.comparator)whilevalue:restrvar=_add_rtype_relation(self.rqlst,mainvar,self.rtype,self.role)self.rqlst.add_constant_restriction(restrvar,self.target_attr,value.pop(),self.attrtype,self.comparator)classAttributeFacet(RelationAttributeFacet):"""Base facet to filter some entities according one of their attribute. Configuration is mostly similarly as :class:`RelationAttributeFacet`, except that: * `target_attr` doesn't make sense here (you specify the attribute using `rtype` * `role` neither, it's systematically 'subject' So, suppose that in our office search example you want to refine search according to the office's surface. Here is a code snippet achieving this: .. sourcecode:: python class SurfaceFacet(AttributeFacet): __regid__ = 'surface' __select__ = AttributeFacet.__select__ & is_instance('Office') # this facet is a filter on the office'surface rtype = 'surface' # override the default value of operator since we want to filter # according to a minimal value, not an exact one comparator = '>=' def vocabulary(self): '''override the default vocabulary method since we want to hard-code our threshold values. Not overriding would generate a filter containing all existing surfaces defined in the database. ''' return [('> 200', '200'), ('> 250', '250'), ('> 275', '275'), ('> 300', '300')] """_select_target_entity=Truedefvocabulary(self):"""return vocabulary for this facet, eg a list of 2-uple (label, value) """rqlst=self.rqlstrqlst.save_state()try:mainvar=self.filtered_variable_cleanup_rqlst(rqlst,mainvar)newvar=_prepare_vocabulary_rqlst(rqlst,mainvar,self.rtype,self.role)_set_orderby(rqlst,newvar,self.sortasc,self.sortfunc)try:rset=self.rqlexec(rqlst.as_string(),self.cw_rset.args)except:self.exception('error while getting vocabulary for %s, rql: %s',self,rqlst.as_string())return()finally:rqlst.recover()# don't call rset_vocabulary on empty result set, it may be an empty# *list* (see rqlexec implementation)returnrsetandself.rset_vocabulary(rset)defsupport_and(self):returnFalsedefadd_rql_restrictions(self):"""add restriction for this facet into the rql syntax tree"""value=self._cw.form.get(self.__regid__)ifnotvalue:returnmainvar=self.filtered_variableself.rqlst.add_constant_restriction(mainvar,self.rtype,value,self.attrtype,self.comparator)classRangeFacet(AttributeFacet):"""This class allows to filter entities according to an attribute of numerical type. It displays a slider using `jquery`_ to choose a lower bound and an upper bound. The example below provides an alternative to the surface facet seen earlier, in a more powerful way since * lower/upper boundaries are computed according to entities to filter * user can specify lower/upper boundaries, not only the lower one .. sourcecode:: python class SurfaceFacet(RangeFacet): __regid__ = 'surface' __select__ = RangeFacet.__select__ & is_instance('Office') # this facet is a filter on the office'surface rtype = 'surface' All this with even less code! The image below display the rendering of the slider: .. image:: ../images/facet_range.png .. _jquery: http://www.jqueryui.com/ """attrtype='Float'# only numerical types are supported@propertydefwdgclass(self):returnFacetRangeWidgetdefget_widget(self):"""return the widget instance to use to display this facet"""values=set(valuefor_,valueinself.vocabulary()ifvalueisnotNone)# Rset with entities (the facet is selected) but without valuesiflen(values)==0:returnNonereturnself.wdgclass(self,min(values),max(values))definfvalue(self):returnself._cw.form.get('%s_inf'%self.__regid__)defsupvalue(self):returnself._cw.form.get('%s_sup'%self.__regid__)defformatvalue(self,value):"""format `value` before in order to insert it in the RQL query"""returnunicode(value)defadd_rql_restrictions(self):infvalue=self.infvalue()ifinfvalueisNone:# nothing sentreturnsupvalue=self.supvalue()self.rqlst.add_constant_restriction(self.filtered_variable,self.rtype,self.formatvalue(infvalue),self.attrtype,'>=')self.rqlst.add_constant_restriction(self.filtered_variable,self.rtype,self.formatvalue(supvalue),self.attrtype,'<=')classDateRangeFacet(RangeFacet):"""This class works similarly as the :class:`RangeFacet` but for attribute of date type. The image below display the rendering of the slider for a date range: .. image:: ../images/facet_date_range.png """attrtype='Date'# only date types are supported@propertydefwdgclass(self):returnDateFacetRangeWidgetdefformatvalue(self,value):"""format `value` before in order to insert it in the RQL query"""return'"%s"'%date.fromtimestamp(float(value)/1000).strftime('%Y/%m/%d')classHasRelationFacet(AbstractFacet):"""This class simply filter according to the presence of a relation (whatever the entity at the other end). It display a simple checkbox that lets you refine your selection in order to get only entities that actually have this relation. You simply have to define which relation using the `rtype` and `role` attributes. Here is an example of the rendering of thos facet to filter book with image and the corresponding code: .. image:: ../images/facet_has_image.png .. sourcecode:: python class HasImageFacet(HasRelationFacet): __regid__ = 'hasimage' __select__ = HasRelationFacet.__select__ & is_instance('Book') rtype = 'has_image' role = 'subject' """rtype=None# override me in subclassrole='subject'# role of filtered entity in the relationtitle=property(rtype_facet_title)defsupport_and(self):returnFalsedefget_widget(self):returnCheckBoxFacetWidget(self._cw,self,'%s:%s'%(self.rtype,self),self._cw.form.get(self.__regid__))defadd_rql_restrictions(self):"""add restriction for this facet into the rql syntax tree"""self.rqlst.set_distinct(True)# XXXvalue=self._cw.form.get(self.__regid__)ifnotvalue:# no value sent for this facetreturnvar=self.rqlst.make_variable()ifself.role=='subject':self.rqlst.add_relation(self.filtered_variable,self.rtype,var)else:self.rqlst.add_relation(var,self.rtype,self.filtered_variable)## html widets ################################################################classFacetVocabularyWidget(HTMLWidget):def__init__(self,facet):self.facet=facetself.items=[]defappend(self,item):self.items.append(item)def_render(self):title=xml_escape(self.facet.title)facetid=xml_escape(self.facet.__regid__)self.w(u'<div id="%s" class="facet">\n'%facetid)self.w(u'<div class="facetTitle" cubicweb:facetName="%s">%s</div>\n'%(xml_escape(facetid),title))ifself.facet.support_and():_=self.facet._cw._self.w(u'''<select name="%s" class="radio facetOperator" title="%s"> <option value="OR">%s</option> <option value="AND">%s</option></select>'''%(facetid+'_andor',_('and/or between different values'),_('OR'),_('AND')))cssclass=''ifnotself.facet.start_unfolded:cssclass+=' hidden'iflen(self.items)>6:cssclass+=' overflowed'self.w(u'<div class="facetBody%s">\n'%cssclass)foriteminself.items:item.render(w=self.w)self.w(u'</div>\n')self.w(u'</div>\n')classFacetStringWidget(HTMLWidget):def__init__(self,facet):self.facet=facetself.value=Nonedef_render(self):title=xml_escape(self.facet.title)facetid=xml_escape(self.facet.__regid__)self.w(u'<div id="%s" class="facet">\n'%facetid)self.w(u'<div class="facetTitle" cubicweb:facetName="%s">%s</div>\n'%(facetid,title))self.w(u'<input name="%s" type="text" value="%s" />\n'%(facetid,self.valueoru''))self.w(u'</div>\n')classFacetRangeWidget(HTMLWidget):formatter='function (value) {return value;}'onload=u''' var _formatter = %(formatter)s; jQuery("#%(sliderid)s").slider({ range: true, min: %(minvalue)s, max: %(maxvalue)s, values: [%(minvalue)s, %(maxvalue)s], stop: function(event, ui) { // submit when the user stops sliding var form = $('#%(sliderid)s').closest('form'); buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs'))); }, slide: function(event, ui) { jQuery('#%(sliderid)s_inf').html(_formatter(ui.values[0])); jQuery('#%(sliderid)s_sup').html(_formatter(ui.values[1])); jQuery('input[name=%(facetid)s_inf]').val(ui.values[0]); jQuery('input[name=%(facetid)s_sup]').val(ui.values[1]); } }); // use JS formatter to format value on page load jQuery('#%(sliderid)s_inf').html(_formatter(jQuery('input[name=%(facetid)s_inf]').val())); jQuery('#%(sliderid)s_sup').html(_formatter(jQuery('input[name=%(facetid)s_sup]').val()));'''#'# make emacs happierdef__init__(self,facet,minvalue,maxvalue):self.facet=facetself.minvalue=minvalueself.maxvalue=maxvaluedef_render(self):facet=self.facetfacet._cw.add_js('ui.slider.js')facet._cw.add_css('ui.all.css')sliderid=make_uid('theslider')facetid=xml_escape(self.facet.__regid__)facet._cw.html_headers.add_onload(self.onload%{'sliderid':sliderid,'facetid':facetid,'minvalue':self.minvalue,'maxvalue':self.maxvalue,'formatter':self.formatter,})title=xml_escape(self.facet.title)self.w(u'<div id="%s" class="facet">\n'%facetid)self.w(u'<div class="facetTitle" cubicweb:facetName="%s">%s</div>\n'%(facetid,title))self.w(u'<span id="%s_inf"></span> - <span id="%s_sup"></span>'%(sliderid,sliderid))self.w(u'<input type="hidden" name="%s_inf" value="%s" />'%(facetid,self.minvalue))self.w(u'<input type="hidden" name="%s_sup" value="%s" />'%(facetid,self.maxvalue))self.w(u'<div id="%s"></div>'%sliderid)self.w(u'</div>\n')classDateFacetRangeWidget(FacetRangeWidget):formatter='function (value) {return (new Date(parseFloat(value))).strftime(DATE_FMT);}'defround_max_value(self,d):'round to upper value to avoid filtering out the max value'returndatetime(d.year,d.month,d.day)+timedelta(days=1)def__init__(self,facet,minvalue,maxvalue):maxvalue=self.round_max_value(maxvalue)super(DateFacetRangeWidget,self).__init__(facet,datetime2ticks(minvalue),datetime2ticks(maxvalue))fmt=facet._cw.property_value('ui.date-format')facet._cw.html_headers.define_var('DATE_FMT',fmt)classFacetItem(HTMLWidget):selected_img="black-check.png"unselected_img="no-check-no-border.png"def__init__(self,req,label,value,selected=False):self._cw=reqself.label=labelself.value=valueself.selected=selecteddef_render(self):ifself.selected:cssclass=' facetValueSelected'imgsrc=self._cw.datadir_url+self.selected_imgimgalt=self._cw._('selected')else:cssclass=''imgsrc=self._cw.datadir_url+self.unselected_imgimgalt=self._cw._('not selected')self.w(u'<div class="facetValue facetCheckBox%s" cubicweb:value="%s">\n'%(cssclass,xml_escape(unicode(self.value))))self.w(u'<img src="%s" alt="%s"/> '%(imgsrc,imgalt))self.w(u'<a href="javascript: {}">%s</a>'%xml_escape(self.label))self.w(u'</div>')classCheckBoxFacetWidget(HTMLWidget):selected_img="black-check.png"unselected_img="black-uncheck.png"def__init__(self,req,facet,value,selected):self._cw=reqself.facet=facetself.value=valueself.selected=selecteddef_render(self):title=xml_escape(self.facet.title)facetid=xml_escape(self.facet.__regid__)self.w(u'<div id="%s" class="facet">\n'%facetid)ifself.selected:cssclass=' facetValueSelected'imgsrc=self._cw.datadir_url+self.selected_imgimgalt=self._cw._('selected')else:cssclass=''imgsrc=self._cw.datadir_url+self.unselected_imgimgalt=self._cw._('not selected')self.w(u'<div class="facetValue facetCheckBox%s" cubicweb:value="%s">\n'%(cssclass,xml_escape(unicode(self.value))))self.w(u'<div class="facetCheckBoxWidget">')self.w(u'<img src="%s" alt="%s" cubicweb:unselimg="true" /> '%(imgsrc,imgalt))self.w(u'<label class="facetTitle" cubicweb:facetName="%s"><a href="javascript: {}">%s</a></label>'%(facetid,title))self.w(u'</div>\n')self.w(u'</div>\n')self.w(u'</div>\n')classFacetSeparator(HTMLWidget):def__init__(self,label=None):self.label=labeloru' 'def_render(self):pass# other classes ################################################################classFilterRQLBuilder(object):"""called by javascript to get a rql string from filter form"""def__init__(self,req):self._cw=reqdefbuild_rql(self):#, tablefilter=False):form=self._cw.formfacetids=form['facets'].split(',')# XXX Union unsupported yetselect=self._cw.vreg.parse(self._cw,form['baserql']).children[0]mainvar=filtered_variable(select)toupdate=[]forfacetidinfacetids:facet=get_facet(self._cw,facetid,select,mainvar)facet.add_rql_restrictions()iffacet.needs_update:toupdate.append(facetid)returnselect.as_string(),toupdate