[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
--- 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
--- 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, '