[facet] create a RangeRQLPathFacet (closes #2852512)
authorKatia Saurfelt <katia.saurfelt@logilab.fr>
Wed, 29 Jan 2014 10:57:10 +0100
changeset 9562 0509880fec01
parent 9561 3bdf85279c67
child 9563 48f0ff3e2a32
[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
web/facet.py
web/test/unittest_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
--- 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, '