[dbapi] fix user handling on dbapi request. Avoid getting None as _user and remove the need for a property.
"""contains utility functions and some visual component to restrict results ofa search:organization: Logilab:copyright: 2008-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses"""__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.htmlwidgetsimportHTMLWidget## 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):"""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)ifrqlst.groupby:rqlst.add_group_var(newvar)rqlst.add_selected(newvar)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):"""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)# 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)# add is restriction if necessaryifmainvar.stinfo['typerel']isNone:etypes=frozenset(sol[mainvar.name]forsolinrqlst.solutions)rqlst.add_type_restriction(mainvar,etypes)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__=True__registry__='facets'cw_property_defs={_('visible'):dict(type='Boolean',default=True,help=_('display the box or not')),_('order'):dict(type='Int',default=99,help=_('display order of the box')),_('context'):dict(type='String',default='',# None <-> bothvocabulary=(_('tablefilter'),_('facetbox'),''),help=_('context where this box should be displayed')),}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):# OR between selected values by defaultreturnself._cw.form.get(self.__regid__+'_andor','OR')defget_widget(self):"""return the widget instance to use to display this facet """raiseNotImplementedErrordefadd_rql_restrictions(self):"""add restriction for this facet into the rql syntax tree"""raiseNotImplementedErrorclassVocabularyFacet(AbstractFacet):needs_update=Truedefget_widget(self):"""return the widget instance to use to display this facet default implentation 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):returnFalsedefrqlexec(self,rql,args=None):try:returnself._cw.execute(rql,args)exceptUnauthorized:return[]classRelationFacet(VocabularyFacet):__select__=partial_relation_possible()&match_context_prop()# class attributes to configure the relation facetrtype=Nonerole='subject'target_attr='eid'target_type=None# 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@propertydeftitle(self):returndisplay_name(self._cw,self.rtype,form=self.role)defvocabulary(self):"""return vocabulary for this facet, eg a list of 2-uple (label, value) """rqlst=self.rqlstrqlst.save_state()ifself.label_vidisnotNoneandself.sortfuncisNone:sort=None# will be sorted on labelelse:sort=self.sortasctry:mainvar=self.filtered_variablevar=insert_attr_select_relation(rqlst,mainvar,self.rtype,self.role,self.target_attr,self.sortfunc,sort)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)_prepare_vocabulary_rqlst(rqlst,self.filtered_variable,self.rtype,self.role)return[str(x)forx,inself.rqlexec(rqlst.as_string())]finally:rqlst.recover()defrset_vocabulary(self,rset):ifself.label_vidisNone:_=self._cw._return[(_(label),eid)foreid,labelinrset]ifself.sortfuncisNone:returnsorted((entity.view(self.label_vid),entity.eid)forentityinrset.entities())return[(entity.view(self.label_vid),entity.eid)forentityinrset.entities()]@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':# multiple values with OR operator# set_distinct only if rtype cardinality is > 1ifself.support_and():self.rqlst.set_distinct(True)self.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())classAttributeFacet(RelationFacet):# attribute typeattrtype='String'# type of comparison: default is an exact match on the attribute valuecomparator='='# could be '<', '<=', '>', '>='defvocabulary(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)defrset_vocabulary(self,rset):_=self._cw._return[(_(value),value)forvalue,inrset]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)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(',')select=parse(form['baserql']).children[0]# XXX Union unsupported yetmainvar=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(),toupdateclassRangeFacet(AttributeFacet):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):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):rtype=None# override me in subclassrole='subject'# role of filtered entity in the relation@propertydeftitle(self):returndisplay_name(self._cw,self.rtype,self.role)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