web/facet.py
changeset 7600 75d208ab8444
parent 7414 de802bcb6348
child 7604 1eb6090311ff
equal deleted inserted replaced
7599:9cbf4c86f57a 7600:75d208ab8444
    53 from logilab.mtconverter import xml_escape
    53 from logilab.mtconverter import xml_escape
    54 from logilab.common.graph import has_path
    54 from logilab.common.graph import has_path
    55 from logilab.common.decorators import cached
    55 from logilab.common.decorators import cached
    56 from logilab.common.date import datetime2ticks, ustrftime, ticks2datetime
    56 from logilab.common.date import datetime2ticks, ustrftime, ticks2datetime
    57 from logilab.common.compat import all
    57 from logilab.common.compat import all
       
    58 from logilab.common.deprecation import deprecated
    58 
    59 
    59 from rql import parse, nodes, utils
    60 from rql import parse, nodes, utils
    60 
    61 
    61 from cubicweb import Unauthorized, typed_eid
    62 from cubicweb import Unauthorized, typed_eid
    62 from cubicweb.schema import display_name
    63 from cubicweb.schema import display_name
    71     if len(ptypes) == 1:
    72     if len(ptypes) == 1:
    72         return display_name(facet._cw, facet.rtype, form=facet.role,
    73         return display_name(facet._cw, facet.rtype, form=facet.role,
    73                             context=iter(ptypes).next())
    74                             context=iter(ptypes).next())
    74     return display_name(facet._cw, facet.rtype, form=facet.role)
    75     return display_name(facet._cw, facet.rtype, form=facet.role)
    75 
    76 
       
    77 def filtered_variable(rqlst):
       
    78     vref = rqlst.selection[0].iget_nodes(nodes.VariableRef).next()
       
    79     return vref.variable
       
    80 
       
    81 def get_facet(req, facetid, rqlst, mainvar):
       
    82     return req.vreg['facets'].object_by_id(facetid, req, rqlst=rqlst,
       
    83                                            filtered_variable=mainvar)
       
    84 
       
    85 @deprecated('[3.13] filter_hiddens moved to cubicweb.web.views.facets with '
       
    86             'slightly modified prototype')
       
    87 def filter_hiddens(w, **kwargs):
       
    88     from cubicweb.web.views.facets import filter_hiddens
       
    89     return filter_hiddens(w, wdgs=kwargs.pop('facets'))
       
    90 
       
    91 
    76 ## rqlst manipulation functions used by facets ################################
    92 ## rqlst manipulation functions used by facets ################################
    77 
    93 
    78 def prepare_facets_rqlst(rqlst, args=None):
    94 def prepare_facets_rqlst(rqlst, args=None):
    79     """prepare a syntax tree to generate facet filters
    95     """prepare a syntax tree to generate facet filters
    80 
    96 
    82     * cleanup selection (remove everything)
    98     * cleanup selection (remove everything)
    83     * undefine unnecessary variables
    99     * undefine unnecessary variables
    84     * set DISTINCT
   100     * set DISTINCT
    85     * unset LIMIT/OFFSET
   101     * unset LIMIT/OFFSET
    86     """
   102     """
    87     if len(rqlst.children) > 1:
   103     assert len(rqlst.children) == 1, 'FIXME: union not yet supported'
    88         raise NotImplementedError('FIXME: union not yet supported')
       
    89     select = rqlst.children[0]
   104     select = rqlst.children[0]
    90     mainvar = filtered_variable(select)
   105     mainvar = filtered_variable(select)
    91     select.set_limit(None)
   106     select.set_limit(None)
    92     select.set_offset(None)
   107     select.set_offset(None)
    93     baserql = select.as_string(kwargs=args)
   108     baserql = select.as_string(kwargs=args)
   102             select.undefine_variable(dvar)
   117             select.undefine_variable(dvar)
   103     # global tree config: DISTINCT, LIMIT, OFFSET
   118     # global tree config: DISTINCT, LIMIT, OFFSET
   104     select.set_distinct(True)
   119     select.set_distinct(True)
   105     return mainvar, baserql
   120     return mainvar, baserql
   106 
   121 
   107 def filtered_variable(rqlst):
   122 
   108     vref = rqlst.selection[0].iget_nodes(nodes.VariableRef).next()
   123 def prepare_vocabulary_rqlst(rqlst, mainvar, rtype, role,
   109     return vref.variable
       
   110 
       
   111 
       
   112 def get_facet(req, facetid, rqlst, mainvar):
       
   113     return req.vreg['facets'].object_by_id(facetid, req, rqlst=rqlst,
       
   114                                            filtered_variable=mainvar)
       
   115 
       
   116 
       
   117 def filter_hiddens(w, **kwargs):
       
   118     for key, val in kwargs.items():
       
   119         w(u'<input type="hidden" name="%s" value="%s" />' % (
       
   120             key, xml_escape(val)))
       
   121 
       
   122 
       
   123 def _may_be_removed(rel, schema, mainvar):
       
   124     """if the given relation may be removed from the tree, return the variable
       
   125     on the other side of `mainvar`, else return None
       
   126     Conditions:
       
   127     * the relation is an attribute selection of the main variable
       
   128     * the relation is optional relation linked to the main variable
       
   129     * the relation is a mandatory relation linked to the main variable
       
   130       without any restriction on the other variable
       
   131     """
       
   132     lhs, rhs = rel.get_variable_parts()
       
   133     rschema = schema.rschema(rel.r_type)
       
   134     if lhs.variable is mainvar:
       
   135         try:
       
   136             ovar = rhs.variable
       
   137         except AttributeError:
       
   138             # constant restriction
       
   139             # XXX: X title LOWER(T) if it makes sense?
       
   140             return None
       
   141         if rschema.final:
       
   142             if len(ovar.stinfo['relations']) == 1:
       
   143                 # attribute selection
       
   144                 return ovar
       
   145             return None
       
   146         opt = 'right'
       
   147         cardidx = 0
       
   148     elif getattr(rhs, 'variable', None) is mainvar:
       
   149         ovar = lhs.variable
       
   150         opt = 'left'
       
   151         cardidx = 1
       
   152     else:
       
   153         # not directly linked to the main variable
       
   154         return None
       
   155     if rel.optional in (opt, 'both'):
       
   156         # optional relation
       
   157         return ovar
       
   158     if all(rdef.cardinality[cardidx] in '1+'
       
   159            for rdef in rschema.rdefs.values()):
       
   160         # mandatory relation without any restriction on the other variable
       
   161         for orel in ovar.stinfo['relations']:
       
   162             if rel is orel:
       
   163                 continue
       
   164             if _may_be_removed(orel, schema, ovar) is None:
       
   165                 return None
       
   166         return ovar
       
   167     return None
       
   168 
       
   169 def _make_relation(rqlst, mainvar, rtype, role):
       
   170     newvar = rqlst.make_variable()
       
   171     if role == 'object':
       
   172         rel = nodes.make_relation(newvar, rtype, (mainvar,), nodes.VariableRef)
       
   173     else:
       
   174         rel = nodes.make_relation(mainvar, rtype, (newvar,), nodes.VariableRef)
       
   175     return newvar, rel
       
   176 
       
   177 def _add_rtype_relation(rqlst, mainvar, rtype, role):
       
   178     """add a relation relying `mainvar` to entities linked by the `rtype`
       
   179     relation (where `mainvar` has `role`)
       
   180 
       
   181     return the inserted variable for linked entities.
       
   182     """
       
   183     newvar, newrel = _make_relation(rqlst, mainvar, rtype, role)
       
   184     rqlst.add_restriction(newrel)
       
   185     return newvar, newrel
       
   186 
       
   187 def _add_eid_restr(rel, restrvar, value):
       
   188     rrel = nodes.make_constant_restriction(restrvar, 'eid', value, 'Int')
       
   189     rel.parent.replace(rel, nodes.And(rel, rrel))
       
   190 
       
   191 def _prepare_vocabulary_rqlst(rqlst, mainvar, rtype, role,
       
   192                               select_target_entity=True):
   124                               select_target_entity=True):
   193     """prepare a syntax tree to generate a filter vocabulary rql using the given
   125     """prepare a syntax tree to generate a filter vocabulary rql using the given
   194     relation:
   126     relation:
   195     * create a variable to filter on this relation
   127     * create a variable to filter on this relation
   196     * add the relation
   128     * add the relation
   206     if mainvar.stinfo['typerel'] is None:
   138     if mainvar.stinfo['typerel'] is None:
   207         etypes = frozenset(sol[mainvar.name] for sol in rqlst.solutions)
   139         etypes = frozenset(sol[mainvar.name] for sol in rqlst.solutions)
   208         rqlst.add_type_restriction(mainvar, etypes)
   140         rqlst.add_type_restriction(mainvar, etypes)
   209     return newvar
   141     return newvar
   210 
   142 
   211 def _remove_relation(rqlst, rel, var):
       
   212     """remove a constraint relation from the syntax tree"""
       
   213     # remove the relation
       
   214     rqlst.remove_node(rel)
       
   215     # remove relations where the filtered variable appears on the
       
   216     # lhs and rhs is a constant restriction
       
   217     extra = []
       
   218     for vrel in var.stinfo['relations']:
       
   219         if vrel is rel:
       
   220             continue
       
   221         if vrel.children[0].variable is var:
       
   222             if not vrel.children[1].get_nodes(nodes.Constant):
       
   223                 extra.append(vrel)
       
   224             rqlst.remove_node(vrel)
       
   225     return extra
       
   226 
       
   227 def _set_orderby(rqlst, newvar, sortasc, sortfuncname):
       
   228     if sortfuncname is None:
       
   229         rqlst.add_sort_var(newvar, sortasc)
       
   230     else:
       
   231         vref = nodes.variable_ref(newvar)
       
   232         vref.register_reference()
       
   233         sortfunc = nodes.Function(sortfuncname)
       
   234         sortfunc.append(vref)
       
   235         term = nodes.SortTerm(sortfunc, sortasc)
       
   236         rqlst.add_sort_term(term)
       
   237 
   143 
   238 def insert_attr_select_relation(rqlst, mainvar, rtype, role, attrname,
   144 def insert_attr_select_relation(rqlst, mainvar, rtype, role, attrname,
   239                                 sortfuncname=None, sortasc=True,
   145                                 sortfuncname=None, sortasc=True,
   240                                 select_target_entity=True):
   146                                 select_target_entity=True):
   241     """modify a syntax tree to :
   147     """modify a syntax tree to :
   245     Sorting:
   151     Sorting:
   246     * on `attrname` ascendant (`sortasc`=True) or descendant (`sortasc`=False)
   152     * on `attrname` ascendant (`sortasc`=True) or descendant (`sortasc`=False)
   247     * on `sortfuncname`(`attrname`) if `sortfuncname` is specified
   153     * on `sortfuncname`(`attrname`) if `sortfuncname` is specified
   248     * no sort if `sortasc` is None
   154     * no sort if `sortasc` is None
   249     """
   155     """
   250     _cleanup_rqlst(rqlst, mainvar)
   156     cleanup_rqlst(rqlst, mainvar)
   251     var = _prepare_vocabulary_rqlst(rqlst, mainvar, rtype, role,
   157     var = prepare_vocabulary_rqlst(rqlst, mainvar, rtype, role,
   252                                     select_target_entity)
   158                                    select_target_entity)
   253     attrvar = rqlst.make_variable()
   159     attrvar = rqlst.make_variable()
   254     rqlst.add_relation(var, attrname, attrvar)
   160     rqlst.add_relation(var, attrname, attrvar)
   255     # if query is grouped, we have to add the attribute variable
   161     # if query is grouped, we have to add the attribute variable
   256     if rqlst.groupby:
   162     if rqlst.groupby:
   257         if not attrvar in rqlst.groupby:
   163         if not attrvar in rqlst.groupby:
   260         _set_orderby(rqlst, attrvar, sortasc, sortfuncname)
   166         _set_orderby(rqlst, attrvar, sortasc, sortfuncname)
   261     # add attribute variable to selection
   167     # add attribute variable to selection
   262     rqlst.add_selected(attrvar)
   168     rqlst.add_selected(attrvar)
   263     return var
   169     return var
   264 
   170 
   265 def _cleanup_rqlst(rqlst, mainvar):
   171 
   266     """cleanup tree from unnecessary restriction:
   172 def cleanup_rqlst(rqlst, mainvar):
       
   173     """cleanup tree from unnecessary restrictions:
   267     * attribute selection
   174     * attribute selection
   268     * optional relations linked to the main variable
   175     * optional relations linked to the main variable
   269     * mandatory relations linked to the main variable
   176     * mandatory relations linked to the main variable
   270     """
   177     """
   271     if rqlst.where is None:
   178     if rqlst.where is None:
   308                 continue
   215                 continue
   309             if not has_path(vargraph, ovarname, mainvar.name):
   216             if not has_path(vargraph, ovarname, mainvar.name):
   310                 toremove.add(rqlst.defined_vars[ovarname])
   217                 toremove.add(rqlst.defined_vars[ovarname])
   311 
   218 
   312 
   219 
       
   220 def _may_be_removed(rel, schema, mainvar):
       
   221     """if the given relation may be removed from the tree, return the variable
       
   222     on the other side of `mainvar`, else return None
       
   223     Conditions:
       
   224     * the relation is an attribute selection of the main variable
       
   225     * the relation is optional relation linked to the main variable
       
   226     * the relation is a mandatory relation linked to the main variable
       
   227       without any restriction on the other variable
       
   228     """
       
   229     lhs, rhs = rel.get_variable_parts()
       
   230     rschema = schema.rschema(rel.r_type)
       
   231     if lhs.variable is mainvar:
       
   232         try:
       
   233             ovar = rhs.variable
       
   234         except AttributeError:
       
   235             # constant restriction
       
   236             # XXX: X title LOWER(T) if it makes sense?
       
   237             return None
       
   238         if rschema.final:
       
   239             if len(ovar.stinfo['relations']) == 1:
       
   240                 # attribute selection
       
   241                 return ovar
       
   242             return None
       
   243         opt = 'right'
       
   244         cardidx = 0
       
   245     elif getattr(rhs, 'variable', None) is mainvar:
       
   246         ovar = lhs.variable
       
   247         opt = 'left'
       
   248         cardidx = 1
       
   249     else:
       
   250         # not directly linked to the main variable
       
   251         return None
       
   252     if rel.optional in (opt, 'both'):
       
   253         # optional relation
       
   254         return ovar
       
   255     if all(rdef.cardinality[cardidx] in '1+'
       
   256            for rdef in rschema.rdefs.values()):
       
   257         # mandatory relation without any restriction on the other variable
       
   258         for orel in ovar.stinfo['relations']:
       
   259             if rel is orel:
       
   260                 continue
       
   261             if _may_be_removed(orel, schema, ovar) is None:
       
   262                 return None
       
   263         return ovar
       
   264     return None
       
   265 
       
   266 def _make_relation(rqlst, mainvar, rtype, role):
       
   267     newvar = rqlst.make_variable()
       
   268     if role == 'object':
       
   269         rel = nodes.make_relation(newvar, rtype, (mainvar,), nodes.VariableRef)
       
   270     else:
       
   271         rel = nodes.make_relation(mainvar, rtype, (newvar,), nodes.VariableRef)
       
   272     return newvar, rel
       
   273 
       
   274 def _add_rtype_relation(rqlst, mainvar, rtype, role):
       
   275     """add a relation relying `mainvar` to entities linked by the `rtype`
       
   276     relation (where `mainvar` has `role`)
       
   277 
       
   278     return the inserted variable for linked entities.
       
   279     """
       
   280     newvar, newrel = _make_relation(rqlst, mainvar, rtype, role)
       
   281     rqlst.add_restriction(newrel)
       
   282     return newvar, newrel
       
   283 
       
   284 def _add_eid_restr(rel, restrvar, value):
       
   285     rrel = nodes.make_constant_restriction(restrvar, 'eid', value, 'Int')
       
   286     rel.parent.replace(rel, nodes.And(rel, rrel))
       
   287 
       
   288 def _remove_relation(rqlst, rel, var):
       
   289     """remove a constraint relation from the syntax tree"""
       
   290     # remove the relation
       
   291     rqlst.remove_node(rel)
       
   292     # remove relations where the filtered variable appears on the
       
   293     # lhs and rhs is a constant restriction
       
   294     extra = []
       
   295     for vrel in var.stinfo['relations']:
       
   296         if vrel is rel:
       
   297             continue
       
   298         if vrel.children[0].variable is var:
       
   299             if not vrel.children[1].get_nodes(nodes.Constant):
       
   300                 extra.append(vrel)
       
   301             rqlst.remove_node(vrel)
       
   302     return extra
       
   303 
       
   304 def _set_orderby(rqlst, newvar, sortasc, sortfuncname):
       
   305     if sortfuncname is None:
       
   306         rqlst.add_sort_var(newvar, sortasc)
       
   307     else:
       
   308         vref = nodes.variable_ref(newvar)
       
   309         vref.register_reference()
       
   310         sortfunc = nodes.Function(sortfuncname)
       
   311         sortfunc.append(vref)
       
   312         term = nodes.SortTerm(sortfunc, sortasc)
       
   313         rqlst.add_sort_term(term)
       
   314 
       
   315 
       
   316 _prepare_vocabulary_rqlst = deprecated('[3.13] renamed prepare_vocabulary_rqlst ')(
       
   317     prepare_vocabulary_rqlst)
       
   318 _cleanup_rqlst = deprecated('[3.13] renamed to cleanup_rqlst')(cleanup_rqlst)
       
   319 
       
   320 
   313 ## base facet classes ##########################################################
   321 ## base facet classes ##########################################################
   314 
   322 
   315 class AbstractFacet(AppObject):
   323 class AbstractFacet(AppObject):
   316     """Abstract base class for all facets. Facets are stored in their own
   324     """Abstract base class for all facets. Facets are stored in their own
   317     'facets' registry. They are similar to contextual components since the use
   325     'facets' registry. They are similar to contextual components since the use
   603         compare to a form value in javascript) for this facet
   611         compare to a form value in javascript) for this facet
   604         """
   612         """
   605         rqlst = self.rqlst
   613         rqlst = self.rqlst
   606         rqlst.save_state()
   614         rqlst.save_state()
   607         try:
   615         try:
   608             _cleanup_rqlst(rqlst, self.filtered_variable)
   616             cleanup_rqlst(rqlst, self.filtered_variable)
   609             if self._select_target_entity:
   617             if self._select_target_entity:
   610                 _prepare_vocabulary_rqlst(rqlst, self.filtered_variable, self.rtype,
   618                 prepare_vocabulary_rqlst(rqlst, self.filtered_variable, self.rtype,
   611                                           self.role, select_target_entity=True)
   619                                          self.role, select_target_entity=True)
   612             else:
   620             else:
   613                 insert_attr_select_relation(
   621                 insert_attr_select_relation(
   614                     rqlst, self.filtered_variable, self.rtype, self.role, self.target_attr,
   622                     rqlst, self.filtered_variable, self.rtype, self.role, self.target_attr,
   615                     select_target_entity=False)
   623                     select_target_entity=False)
   616             values = [unicode(x) for x, in self.rqlexec(rqlst.as_string())]
   624             values = [unicode(x) for x, in self.rqlexec(rqlst.as_string())]
   875         """
   883         """
   876         rqlst = self.rqlst
   884         rqlst = self.rqlst
   877         rqlst.save_state()
   885         rqlst.save_state()
   878         try:
   886         try:
   879             mainvar = self.filtered_variable
   887             mainvar = self.filtered_variable
   880             _cleanup_rqlst(rqlst, mainvar)
   888             cleanup_rqlst(rqlst, mainvar)
   881             newvar = _prepare_vocabulary_rqlst(rqlst, mainvar, self.rtype, self.role)
   889             newvar = prepare_vocabulary_rqlst(rqlst, mainvar, self.rtype, self.role)
   882             _set_orderby(rqlst, newvar, self.sortasc, self.sortfunc)
   890             _set_orderby(rqlst, newvar, self.sortasc, self.sortfunc)
   883             try:
   891             try:
   884                 rset = self.rqlexec(rqlst.as_string(), self.cw_rset.args)
   892                 rset = self.rqlexec(rqlst.as_string(), self.cw_rset.args)
   885             except:
   893             except:
   886                 self.exception('error while getting vocabulary for %s, rql: %s',
   894                 self.exception('error while getting vocabulary for %s, rql: %s',