# HG changeset patch # User Katia Saurfelt # Date 1390989430 -3600 # Node ID 0509880fec01fe34b6fd2514d1dcdb4b58244da4 # Parent 3bdf85279c67dbeb6bb85ccc85af826a334e39f8 [facet] create a RangeRQLPathFacet (closes #2852512) This facet is a mix between a RQLPathFacet and a RangeFacet, allowing to use the FacetRangeWidget on the RQLPathFacet target attribute diff -r 3bdf85279c67 -r 0509880fec01 web/facet.py --- a/web/facet.py Thu Oct 31 16:12:37 2013 +0100 +++ b/web/facet.py Wed Jan 29 10:57:10 2014 +0100 @@ -34,6 +34,9 @@ .. autoclass:: cubicweb.web.facet.RangeFacet .. autoclass:: cubicweb.web.facet.DateRangeFacet .. autoclass:: cubicweb.web.facet.BitFieldFacet +.. autoclass:: cubicweb.web.facet.AbstractRangeRQLPathFacet +.. autoclass:: cubicweb.web.facet.RangeRQLPathFacet +.. autoclass:: cubicweb.web.facet.DateRangeRQLPathFacet Classes for facets implementor ------------------------------ @@ -1301,7 +1304,6 @@ self.target_attr_type, operator) - class DateRangeFacet(RangeFacet): """This class works similarly as the :class:`RangeFacet` but for attribute of date type. @@ -1325,6 +1327,110 @@ return '"%s"' % ustrftime(date_value, '%Y/%m/%d') +class AbstractRangeRQLPathFacet(RQLPathFacet): + """ + The :class:`AbstractRangeRQLPathFacet` is the base class for + RQLPathFacet-type facets allowing the use of RangeWidgets-like + widgets (such as (:class:`FacetRangeWidget`, + class:`DateFacetRangeWidget`) on the parent :class:`RQLPathFacet` + target attribute. + """ + __abstract__ = True + + def vocabulary(self): + """return vocabulary for this facet, eg a list of (label, + value)""" + select = self.select + select.save_state() + try: + filtered_variable = self.filtered_variable + cleanup_select(select, filtered_variable) + varmap, restrvar = self.add_path_to_select() + if self.label_variable: + attrvar = varmap[self.label_variable] + else: + attrvar = restrvar + # start RangeRQLPathFacet + minf = nodes.Function('MIN') + minf.append(nodes.VariableRef(restrvar)) + select.add_selected(minf) + maxf = nodes.Function('MAX') + maxf.append(nodes.VariableRef(restrvar)) + select.add_selected(maxf) + # add is restriction if necessary + if filtered_variable.stinfo['typerel'] is None: + etypes = frozenset(sol[filtered_variable.name] for sol in select.solutions) + select.add_type_restriction(filtered_variable, etypes) + # end RangeRQLPathFacet + try: + rset = self.rqlexec(select.as_string(), self.cw_rset.args) + except Exception: + self.exception('error while getting vocabulary for %s, rql: %s', + self, select.as_string()) + return () + finally: + select.recover() + # don't call rset_vocabulary on empty result set, it may be an empty + # *list* (see rqlexec implementation) + if rset: + minv, maxv = rset[0] + return [(unicode(minv), minv), (unicode(maxv), maxv)] + return [] + + + def possible_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 + """ + return [strval for strval, val in self.vocabulary()] + + def add_rql_restrictions(self): + infvalue = self.infvalue() + supvalue = self.supvalue() + if infvalue is None or supvalue is None: # nothing sent + return + varmap, restrvar = self.add_path_to_select( + skiplabel=True, skipattrfilter=True) + restrel = None + for part in self.path: + if isinstance(part, basestring): + part = part.split() + subject, rtype, object = part + if object == self.filter_variable: + restrel = rtype + assert restrel + # when a value is equal to one of the limit, don't add the restriction, + # else we filter out NULL values implicitly + if infvalue != self.infvalue(min=True): + + self._add_restriction(infvalue, '>=', restrvar, restrel) + if supvalue != self.supvalue(max=True): + self._add_restriction(supvalue, '<=', restrvar, restrel) + + def _add_restriction(self, value, operator, restrvar, restrel): + self.select.add_constant_restriction(restrvar, + restrel, + self.formatvalue(value), + self.target_attr_type, operator) + + +class RangeRQLPathFacet(AbstractRangeRQLPathFacet, RQLPathFacet): + """ + The :class:`RangeRQLPathFacet` uses the :class:`FacetRangeWidget` + on the :class:`AbstractRangeRQLPathFacet` target attribute + """ + pass + + +class DateRangeRQLPathFacet(AbstractRangeRQLPathFacet, DateRangeFacet): + """ + The :class:`DateRangeRQLPathFacet` uses the + :class:`DateFacetRangeWidget` on the + :class:`AbstractRangeRQLPathFacet` target attribute + """ + pass + + class HasRelationFacet(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 diff -r 3bdf85279c67 -r 0509880fec01 web/test/unittest_facet.py --- a/web/test/unittest_facet.py Thu Oct 31 16:12:37 2013 +0100 +++ b/web/test/unittest_facet.py Wed Jan 29 10:57:10 2014 +0100 @@ -303,6 +303,34 @@ select=rqlst.children[0], filtered_variable=filtered_variable) + + def test_rqlpath_range(self): + req, rset, rqlst, filtered_variable = self.prepare_rqlst() + class RRF(facet.DateRangeRQLPathFacet): + path = [('X created_by U'), ('U owned_by O'), ('O creation_date OL')] + filter_variable = 'OL' + f = RRF(req, rset=rset, select=rqlst.children[0], + filtered_variable=filtered_variable) + mind, maxd = self.execute('Any MIN(CD), MAX(CD) WHERE X is CWUser, X created_by U, U owned_by O, O creation_date CD')[0] + self.assertEqual(f.vocabulary(), [(str(mind), mind), + (str(maxd), maxd)]) + # ensure rqlst is left unmodified + self.assertEqual(rqlst.as_string(), 'DISTINCT Any WHERE X is CWUser') + self.assertEqual(f.possible_values(), + [str(mind), str(maxd)]) + # ensure rqlst is left unmodified + self.assertEqual(rqlst.as_string(), 'DISTINCT Any WHERE X is CWUser') + req.form['%s_inf' % f.__regid__] = str(datetime2ticks(mind)) + req.form['%s_sup' % f.__regid__] = str(datetime2ticks(mind)) + f.add_rql_restrictions() + # selection is cluttered because rqlst has been prepared for facet (it + # is not in real life) + self.assertEqual(f.select.as_string(), + 'DISTINCT Any WHERE X is CWUser, X created_by G, G owned_by H, H creation_date >= "%s", ' + 'H creation_date <= "%s"' + % (mind.strftime('%Y/%m/%d'), + mind.strftime('%Y/%m/%d'))) + def prepareg_aggregat_rqlst(self): return self.prepare_rqlst( 'Any 1, COUNT(X) WHERE X is CWUser, X creation_date XD, '