web/facet.py
changeset 467 a6f056bc7d1d
parent 446 3a3ab6bbccc5
child 496 e25a3c2f5393
equal deleted inserted replaced
466:bef394c66f10 467:a6f056bc7d1d
    27 
    27 
    28 ## rqlst manipulation functions used by facets ################################
    28 ## rqlst manipulation functions used by facets ################################
    29 
    29 
    30 def prepare_facets_rqlst(rqlst, args=None):
    30 def prepare_facets_rqlst(rqlst, args=None):
    31     """prepare a syntax tree to generate facet filters
    31     """prepare a syntax tree to generate facet filters
    32     
    32 
    33     * remove ORDERBY clause
    33     * remove ORDERBY clause
    34     * cleanup selection (remove everything)
    34     * cleanup selection (remove everything)
    35     * undefine unnecessary variables
    35     * undefine unnecessary variables
    36     * set DISTINCT
    36     * set DISTINCT
    37     * unset LIMIT/OFFSET
    37     * unset LIMIT/OFFSET
    62 
    62 
    63 
    63 
    64 def get_facet(req, facetid, rqlst, mainvar):
    64 def get_facet(req, facetid, rqlst, mainvar):
    65     return req.vreg.object_by_id('facets', facetid, req, rqlst=rqlst,
    65     return req.vreg.object_by_id('facets', facetid, req, rqlst=rqlst,
    66                                  filtered_variable=mainvar)
    66                                  filtered_variable=mainvar)
    67     
    67 
    68 
    68 
    69 def filter_hiddens(w, **kwargs):
    69 def filter_hiddens(w, **kwargs):
    70     for key, val in kwargs.items():
    70     for key, val in kwargs.items():
    71         w(u'<input type="hidden" name="%s" value="%s" />' % (
    71         w(u'<input type="hidden" name="%s" value="%s" />' % (
    72             key, html_escape(val)))
    72             key, html_escape(val)))
   137     newvar, rel = _add_rtype_relation(rqlst, mainvar, rtype, role)
   137     newvar, rel = _add_rtype_relation(rqlst, mainvar, rtype, role)
   138     if rqlst.groupby:
   138     if rqlst.groupby:
   139         rqlst.add_group_var(newvar)
   139         rqlst.add_group_var(newvar)
   140     rqlst.add_selected(newvar)
   140     rqlst.add_selected(newvar)
   141     return newvar, rel
   141     return newvar, rel
   142         
   142 
   143 def _remove_relation(rqlst, rel, var):
   143 def _remove_relation(rqlst, rel, var):
   144     """remove a constraint relation from the syntax tree"""
   144     """remove a constraint relation from the syntax tree"""
   145     # remove the relation
   145     # remove the relation
   146     rqlst.remove_node(rel)
   146     rqlst.remove_node(rel)
   147     # remove relations where the filtered variable appears on the
   147     # remove relations where the filtered variable appears on the
   227         # and have no path to the main variable
   227         # and have no path to the main variable
   228         for ovarname in linkedvars:
   228         for ovarname in linkedvars:
   229             if ovarname == mainvar.name:
   229             if ovarname == mainvar.name:
   230                 continue
   230                 continue
   231             if not has_path(vargraph, ovarname, mainvar.name):
   231             if not has_path(vargraph, ovarname, mainvar.name):
   232                 toremove.add(rqlst.defined_vars[ovarname])            
   232                 toremove.add(rqlst.defined_vars[ovarname])
   233 
   233 
   234         
   234 
   235         
   235 
   236 ## base facet classes #########################################################
   236 ## base facet classes #########################################################
   237 class AbstractFacet(AcceptMixIn, AppRsetObject):
   237 class AbstractFacet(AcceptMixIn, AppRsetObject):
   238     __registerer__ = priority_registerer
   238     __registerer__ = priority_registerer
   239     __abstract__ = True
   239     __abstract__ = True
   240     __registry__ = 'facets'
   240     __registry__ = 'facets'
   250         }
   250         }
   251     visible = True
   251     visible = True
   252     context = ''
   252     context = ''
   253     needs_update = False
   253     needs_update = False
   254     start_unfolded = True
   254     start_unfolded = True
   255     
   255 
   256     @classmethod
   256     @classmethod
   257     def selected(cls, req, rset=None, rqlst=None, context=None,
   257     def selected(cls, req, rset=None, rqlst=None, context=None,
   258                  filtered_variable=None):
   258                  filtered_variable=None):
   259         assert rset is not None or rqlst is not None
   259         assert rset is not None or rqlst is not None
   260         assert filtered_variable
   260         assert filtered_variable
   278 
   278 
   279     @property
   279     @property
   280     def operator(self):
   280     def operator(self):
   281         # OR between selected values by default
   281         # OR between selected values by default
   282         return self.req.form.get(self.id + '_andor', 'OR')
   282         return self.req.form.get(self.id + '_andor', 'OR')
   283     
   283 
   284     def get_widget(self):
   284     def get_widget(self):
   285         """return the widget instance to use to display this facet
   285         """return the widget instance to use to display this facet
   286         """
   286         """
   287         raise NotImplementedError
   287         raise NotImplementedError
   288     
   288 
   289     def add_rql_restrictions(self):
   289     def add_rql_restrictions(self):
   290         """add restriction for this facet into the rql syntax tree"""
   290         """add restriction for this facet into the rql syntax tree"""
   291         raise NotImplementedError
   291         raise NotImplementedError
   292     
   292 
   293 
   293 
   294 class VocabularyFacet(AbstractFacet):
   294 class VocabularyFacet(AbstractFacet):
   295     needs_update = True
   295     needs_update = True
   296     
   296 
   297     def get_widget(self):
   297     def get_widget(self):
   298         """return the widget instance to use to display this facet
   298         """return the widget instance to use to display this facet
   299 
   299 
   300         default implentation expects a .vocabulary method on the facet and
   300         default implentation expects a .vocabulary method on the facet and
   301         return a combobox displaying this vocabulary
   301         return a combobox displaying this vocabulary
   309             if value is None:
   309             if value is None:
   310                 wdg.append(FacetSeparator(label))
   310                 wdg.append(FacetSeparator(label))
   311             else:
   311             else:
   312                 wdg.append(FacetItem(self.req, label, value, value in selected))
   312                 wdg.append(FacetItem(self.req, label, value, value in selected))
   313         return wdg
   313         return wdg
   314     
   314 
   315     def vocabulary(self):
   315     def vocabulary(self):
   316         """return vocabulary for this facet, eg a list of 2-uple (label, value)
   316         """return vocabulary for this facet, eg a list of 2-uple (label, value)
   317         """
   317         """
   318         raise NotImplementedError
   318         raise NotImplementedError
   319     
   319 
   320     def possible_values(self):
   320     def possible_values(self):
   321         """return a list of possible values (as string since it's used to
   321         """return a list of possible values (as string since it's used to
   322         compare to a form value in javascript) for this facet
   322         compare to a form value in javascript) for this facet
   323         """
   323         """
   324         raise NotImplementedError
   324         raise NotImplementedError
   325 
   325 
   326     def support_and(self):
   326     def support_and(self):
   327         return False
   327         return False
   328     
   328 
   329     def rqlexec(self, rql, args=None, cachekey=None):
   329     def rqlexec(self, rql, args=None, cachekey=None):
   330         try:
   330         try:
   331             return self.req.execute(rql, args, cachekey)
   331             return self.req.execute(rql, args, cachekey)
   332         except Unauthorized:
   332         except Unauthorized:
   333             return []
   333             return []
   334         
   334 
   335 
   335 
   336 class RelationFacet(VocabularyFacet):
   336 class RelationFacet(VocabularyFacet):
   337     __selectors__ = (one_has_relation, match_context_prop)
   337     __selectors__ = (one_has_relation, match_context_prop)
   338     # class attributes to configure the relation facet
   338     # class attributes to configure the relation facet
   339     rtype = None
   339     rtype = None
   342     # set this to a stored procedure name if you want to sort on the result of
   342     # set this to a stored procedure name if you want to sort on the result of
   343     # this function's result instead of direct value
   343     # this function's result instead of direct value
   344     sortfunc = None
   344     sortfunc = None
   345     # ascendant/descendant sorting
   345     # ascendant/descendant sorting
   346     sortasc = True
   346     sortasc = True
   347     
   347 
   348     @property
   348     @property
   349     def title(self):
   349     def title(self):
   350         return display_name(self.req, self.rtype, form=self.role)        
   350         return display_name(self.req, self.rtype, form=self.role)
   351 
   351 
   352     def vocabulary(self):
   352     def vocabulary(self):
   353         """return vocabulary for this facet, eg a list of 2-uple (label, value)
   353         """return vocabulary for this facet, eg a list of 2-uple (label, value)
   354         """
   354         """
   355         rqlst = self.rqlst
   355         rqlst = self.rqlst
   365                                self, rqlst.as_string())
   365                                self, rqlst.as_string())
   366                 return ()
   366                 return ()
   367         finally:
   367         finally:
   368             rqlst.recover()
   368             rqlst.recover()
   369         return self.rset_vocabulary(rset)
   369         return self.rset_vocabulary(rset)
   370     
   370 
   371     def possible_values(self):
   371     def possible_values(self):
   372         """return a list of possible values (as string since it's used to
   372         """return a list of possible values (as string since it's used to
   373         compare to a form value in javascript) for this facet
   373         compare to a form value in javascript) for this facet
   374         """
   374         """
   375         rqlst = self.rqlst
   375         rqlst = self.rqlst
   378             _cleanup_rqlst(rqlst, self.filtered_variable)
   378             _cleanup_rqlst(rqlst, self.filtered_variable)
   379             _prepare_vocabulary_rqlst(rqlst, self.filtered_variable, self.rtype, self.role)
   379             _prepare_vocabulary_rqlst(rqlst, self.filtered_variable, self.rtype, self.role)
   380             return [str(x) for x, in self.rqlexec(rqlst.as_string())]
   380             return [str(x) for x, in self.rqlexec(rqlst.as_string())]
   381         finally:
   381         finally:
   382             rqlst.recover()
   382             rqlst.recover()
   383     
   383 
   384     def rset_vocabulary(self, rset):
   384     def rset_vocabulary(self, rset):
   385         _ = self.req._
   385         _ = self.req._
   386         return [(_(label), eid) for eid, label in rset]
   386         return [(_(label), eid) for eid, label in rset]
   387 
   387 
   388     @cached
   388     @cached
   430 
   430 
   431 
   431 
   432 class AttributeFacet(RelationFacet):
   432 class AttributeFacet(RelationFacet):
   433     # attribute type
   433     # attribute type
   434     attrtype = 'String'
   434     attrtype = 'String'
   435     
   435     # type of comparison: default is an exact match on the attribute value
       
   436     comparator = '=' # could be '<', '<=', '>', '>='
       
   437 
   436     def vocabulary(self):
   438     def vocabulary(self):
   437         """return vocabulary for this facet, eg a list of 2-uple (label, value)
   439         """return vocabulary for this facet, eg a list of 2-uple (label, value)
   438         """
   440         """
   439         rqlst = self.rqlst
   441         rqlst = self.rqlst
   440         rqlst.save_state()
   442         rqlst.save_state()
   450                                self, rqlst.as_string())
   452                                self, rqlst.as_string())
   451                 return ()
   453                 return ()
   452         finally:
   454         finally:
   453             rqlst.recover()
   455             rqlst.recover()
   454         return self.rset_vocabulary(rset)
   456         return self.rset_vocabulary(rset)
   455     
   457 
   456     def rset_vocabulary(self, rset):
   458     def rset_vocabulary(self, rset):
   457         _ = self.req._
   459         _ = self.req._
   458         return [(_(value), value) for value, in rset]
   460         return [(_(value), value) for value, in rset]
   459 
   461 
   460     def support_and(self):
   462     def support_and(self):
   461         return False
   463         return False
   462             
   464 
   463     def add_rql_restrictions(self):
   465     def add_rql_restrictions(self):
   464         """add restriction for this facet into the rql syntax tree"""
   466         """add restriction for this facet into the rql syntax tree"""
   465         value = self.req.form.get(self.id)
   467         value = self.req.form.get(self.id)
   466         if not value:
   468         if not value:
   467             return
   469             return
   468         mainvar = self.filtered_variable
   470         mainvar = self.filtered_variable
   469         self.rqlst.add_constant_restriction(mainvar, self.rtype, value,
   471         self.rqlst.add_constant_restriction(mainvar, self.rtype, value,
   470                                             self.attrtype)
   472                                             self.attrtype, self.comparator)
   471 
   473 
   472 
   474 
   473         
   475 
   474 class FilterRQLBuilder(object):
   476 class FilterRQLBuilder(object):
   475     """called by javascript to get a rql string from filter form"""
   477     """called by javascript to get a rql string from filter form"""
   476 
   478 
   477     def __init__(self, req):
   479     def __init__(self, req):
   478         self.req = req
   480         self.req = req
   479                 
   481 
   480     def build_rql(self):#, tablefilter=False):
   482     def build_rql(self):#, tablefilter=False):
   481         form = self.req.form
   483         form = self.req.form
   482         facetids = form['facets'].split(',')
   484         facetids = form['facets'].split(',')
   483         select = parse(form['baserql']).children[0] # XXX Union unsupported yet
   485         select = parse(form['baserql']).children[0] # XXX Union unsupported yet
   484         mainvar = filtered_variable(select)
   486         mainvar = filtered_variable(select)
   488             facet.add_rql_restrictions()
   490             facet.add_rql_restrictions()
   489             if facet.needs_update:
   491             if facet.needs_update:
   490                 toupdate.append(facetid)
   492                 toupdate.append(facetid)
   491         return select.as_string(), toupdate
   493         return select.as_string(), toupdate
   492 
   494 
   493         
   495 
   494 ## html widets ################################################################
   496 ## html widets ################################################################
   495 
   497 
   496 class FacetVocabularyWidget(HTMLWidget):
   498 class FacetVocabularyWidget(HTMLWidget):
   497     
   499 
   498     def __init__(self, facet):
   500     def __init__(self, facet):
   499         self.facet = facet
   501         self.facet = facet
   500         self.items = []
   502         self.items = []
   501 
   503 
   502     def append(self, item):
   504     def append(self, item):
   503         self.items.append(item)
   505         self.items.append(item)
   504             
   506 
   505     def _render(self):
   507     def _render(self):
   506         title = html_escape(self.facet.title)
   508         title = html_escape(self.facet.title)
   507         facetid = html_escape(self.facet.id)
   509         facetid = html_escape(self.facet.id)
   508         if len(self.items) > 6:
   510         if len(self.items) > 6:
   509             self.w(u'<div id="%s" class="facet overflowed">\n' % facetid)
   511             self.w(u'<div id="%s" class="facet overflowed">\n' % facetid)
   525         for item in self.items:
   527         for item in self.items:
   526             item.render(self.w)
   528             item.render(self.w)
   527         self.w(u'</div>\n')
   529         self.w(u'</div>\n')
   528         self.w(u'</div>\n')
   530         self.w(u'</div>\n')
   529 
   531 
   530         
   532 
   531 class FacetStringWidget(HTMLWidget):
   533 class FacetStringWidget(HTMLWidget):
   532     def __init__(self, facet):
   534     def __init__(self, facet):
   533         self.facet = facet
   535         self.facet = facet
   534         self.value = None
   536         self.value = None
   535 
   537 
   558         if self.selected:
   560         if self.selected:
   559             cssclass = ' facetValueSelected'
   561             cssclass = ' facetValueSelected'
   560             imgsrc = self.req.datadir_url + self.selected_img
   562             imgsrc = self.req.datadir_url + self.selected_img
   561         else:
   563         else:
   562             cssclass = ''
   564             cssclass = ''
   563             imgsrc = self.req.datadir_url + self.unselected_img            
   565             imgsrc = self.req.datadir_url + self.unselected_img
   564         self.w(u'<div class="facetValue facetCheckBox%s" cubicweb:value="%s">\n'
   566         self.w(u'<div class="facetValue facetCheckBox%s" cubicweb:value="%s">\n'
   565                % (cssclass, html_escape(unicode(self.value))))
   567                % (cssclass, html_escape(unicode(self.value))))
   566         self.w(u'<img src="%s" />&nbsp;' % imgsrc)
   568         self.w(u'<img src="%s" />&nbsp;' % imgsrc)
   567         self.w(u'<a href="javascript: {}">%s</a>' % html_escape(self.label))
   569         self.w(u'<a href="javascript: {}">%s</a>' % html_escape(self.label))
   568         self.w(u'</div>')
   570         self.w(u'</div>')
   569 
   571 
   570 
   572 
   571 class FacetSeparator(HTMLWidget):
   573 class FacetSeparator(HTMLWidget):
   572     def __init__(self, label=None):
   574     def __init__(self, label=None):
   573         self.label = label or u'&nbsp;'
   575         self.label = label or u'&nbsp;'
   574         
   576 
   575     def _render(self):
   577     def _render(self):
   576         pass
   578         pass
   577 
   579