[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

authorKatia Saurfelt <katia.saurfelt@logilab.fr>
changeset0509880fec01
branchdefault
phasepublic
hiddenno
parent revision#3bdf85279c67 [request] Make sure set_message() actually displays its given message (closes #3003425)
child revision#88ae6d83eb0f [book] Update documentation for new repoapi, #d75a15158611 [web/request] deprecate user_callback, #995d605cb2e7 [web/request] deprecate user_callback, #48f0ff3e2a32 [wsgi] make sure request.content is available for consumption
files modified by this revision
web/facet.py
web/test/unittest_facet.py
# HG changeset patch
# User Katia Saurfelt <katia.saurfelt@logilab.fr>
# Date 1390989430 -3600
# Wed Jan 29 10:57:10 2014 +0100
# 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 --git a/web/facet.py b/web/facet.py
@@ -32,10 +32,13 @@
1  .. autoclass:: cubicweb.web.facet.AttributeFacet
2  .. autoclass:: cubicweb.web.facet.RQLPathFacet
3  .. autoclass:: cubicweb.web.facet.RangeFacet
4  .. autoclass:: cubicweb.web.facet.DateRangeFacet
5  .. autoclass:: cubicweb.web.facet.BitFieldFacet
6 +.. autoclass:: cubicweb.web.facet.AbstractRangeRQLPathFacet
7 +.. autoclass:: cubicweb.web.facet.RangeRQLPathFacet
8 +.. autoclass:: cubicweb.web.facet.DateRangeRQLPathFacet
9 
10  Classes for facets implementor
11  ------------------------------
12  Unless you didn't find the class that does the job you want above, you may want
13  to skip those classes...
@@ -1299,11 +1302,10 @@
14                                               self.rtype,
15                                               self.formatvalue(value),
16                                               self.target_attr_type, operator)
17 
18 
19 -
20  class DateRangeFacet(RangeFacet):
21      """This class works similarly as the :class:`RangeFacet` but for attribute
22      of date type.
23 
24      The image below display the rendering of the slider for a date range:
@@ -1323,10 +1325,114 @@
25          except (ValueError, OverflowError):
26              return u'"date out-of-range"'
27          return '"%s"' % ustrftime(date_value, '%Y/%m/%d')
28 
29 
30 +class AbstractRangeRQLPathFacet(RQLPathFacet):
31 +    """
32 +    The :class:`AbstractRangeRQLPathFacet` is the base class for
33 +    RQLPathFacet-type facets allowing the use of RangeWidgets-like
34 +    widgets (such as (:class:`FacetRangeWidget`,
35 +    class:`DateFacetRangeWidget`) on the parent :class:`RQLPathFacet`
36 +    target attribute.
37 +    """
38 +    __abstract__ = True
39 +
40 +    def vocabulary(self):
41 +        """return vocabulary for this facet, eg a list of (label,
42 +        value)"""
43 +        select = self.select
44 +        select.save_state()
45 +        try:
46 +            filtered_variable = self.filtered_variable
47 +            cleanup_select(select, filtered_variable)
48 +            varmap, restrvar = self.add_path_to_select()
49 +            if self.label_variable:
50 +                attrvar = varmap[self.label_variable]
51 +            else:
52 +                attrvar = restrvar
53 +            # start RangeRQLPathFacet
54 +            minf = nodes.Function('MIN')
55 +            minf.append(nodes.VariableRef(restrvar))
56 +            select.add_selected(minf)
57 +            maxf = nodes.Function('MAX')
58 +            maxf.append(nodes.VariableRef(restrvar))
59 +            select.add_selected(maxf)
60 +            # add is restriction if necessary
61 +            if filtered_variable.stinfo['typerel'] is None:
62 +                etypes = frozenset(sol[filtered_variable.name] for sol in select.solutions)
63 +                select.add_type_restriction(filtered_variable, etypes)
64 +            # end RangeRQLPathFacet
65 +            try:
66 +                rset = self.rqlexec(select.as_string(), self.cw_rset.args)
67 +            except Exception:
68 +                self.exception('error while getting vocabulary for %s, rql: %s',
69 +                               self, select.as_string())
70 +                return ()
71 +        finally:
72 +            select.recover()
73 +        # don't call rset_vocabulary on empty result set, it may be an empty
74 +        # *list* (see rqlexec implementation)
75 +        if rset:
76 +            minv, maxv = rset[0]
77 +            return [(unicode(minv), minv), (unicode(maxv), maxv)]
78 +        return []
79 +
80 +
81 +    def possible_values(self):
82 +        """return a list of possible values (as string since it's used to
83 +        compare to a form value in javascript) for this facet
84 +        """
85 +        return [strval for strval, val in self.vocabulary()]
86 +
87 +    def add_rql_restrictions(self):
88 +        infvalue = self.infvalue()
89 +        supvalue = self.supvalue()
90 +        if infvalue is None or supvalue is None: # nothing sent
91 +            return
92 +        varmap, restrvar = self.add_path_to_select(
93 +            skiplabel=True, skipattrfilter=True)
94 +        restrel = None
95 +        for part in self.path:
96 +            if isinstance(part, basestring):
97 +                part = part.split()
98 +            subject, rtype, object = part
99 +            if object == self.filter_variable:
100 +                restrel = rtype
101 +        assert restrel
102 +        # when a value is equal to one of the limit, don't add the restriction,
103 +        # else we filter out NULL values implicitly
104 +        if infvalue != self.infvalue(min=True):
105 +
106 +            self._add_restriction(infvalue, '>=', restrvar, restrel)
107 +        if supvalue != self.supvalue(max=True):
108 +            self._add_restriction(supvalue, '<=', restrvar, restrel)
109 +
110 +    def _add_restriction(self, value, operator, restrvar, restrel):
111 +        self.select.add_constant_restriction(restrvar,
112 +                                             restrel,
113 +                                             self.formatvalue(value),
114 +                                             self.target_attr_type, operator)
115 +
116 +
117 +class RangeRQLPathFacet(AbstractRangeRQLPathFacet, RQLPathFacet):
118 +    """
119 +    The :class:`RangeRQLPathFacet` uses the :class:`FacetRangeWidget`
120 +    on the :class:`AbstractRangeRQLPathFacet` target attribute
121 +    """
122 +    pass
123 +
124 +
125 +class DateRangeRQLPathFacet(AbstractRangeRQLPathFacet, DateRangeFacet):
126 +    """
127 +    The :class:`DateRangeRQLPathFacet` uses the
128 +    :class:`DateFacetRangeWidget` on the
129 +    :class:`AbstractRangeRQLPathFacet` target attribute
130 +    """
131 +    pass
132 +
133 +
134  class HasRelationFacet(AbstractFacet):
135      """This class simply filter according to the presence of a relation
136      (whatever the entity at the other end). It display a simple checkbox that
137      lets you refine your selection in order to get only entities that actually
138      have this relation. You simply have to define which relation using the
diff --git a/web/test/unittest_facet.py b/web/test/unittest_facet.py
@@ -301,10 +301,38 @@
139              label_variable = 'OL'
140          self.assertRaises(AssertionError, RPF, req, rset=rset,
141                            select=rqlst.children[0],
142                            filtered_variable=filtered_variable)
143 
144 +
145 +    def test_rqlpath_range(self):
146 +        req, rset, rqlst, filtered_variable = self.prepare_rqlst()
147 +        class RRF(facet.DateRangeRQLPathFacet):
148 +            path = [('X created_by U'), ('U owned_by O'), ('O creation_date OL')]
149 +            filter_variable = 'OL'
150 +        f = RRF(req, rset=rset, select=rqlst.children[0],
151 +                filtered_variable=filtered_variable)
152 +        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]
153 +        self.assertEqual(f.vocabulary(), [(str(mind), mind),
154 +                                          (str(maxd), maxd)])
155 +        # ensure rqlst is left unmodified
156 +        self.assertEqual(rqlst.as_string(), 'DISTINCT Any  WHERE X is CWUser')
157 +        self.assertEqual(f.possible_values(),
158 +                         [str(mind), str(maxd)])
159 +        # ensure rqlst is left unmodified
160 +        self.assertEqual(rqlst.as_string(), 'DISTINCT Any  WHERE X is CWUser')
161 +        req.form['%s_inf' % f.__regid__] = str(datetime2ticks(mind))
162 +        req.form['%s_sup' % f.__regid__] = str(datetime2ticks(mind))
163 +        f.add_rql_restrictions()
164 +        # selection is cluttered because rqlst has been prepared for facet (it
165 +        # is not in real life)
166 +        self.assertEqual(f.select.as_string(),
167 +                         'DISTINCT Any  WHERE X is CWUser, X created_by G, G owned_by H, H creation_date >= "%s", '
168 +                         'H creation_date <= "%s"'
169 +                         % (mind.strftime('%Y/%m/%d'),
170 +                            mind.strftime('%Y/%m/%d')))
171 +
172      def prepareg_aggregat_rqlst(self):
173          return self.prepare_rqlst(
174              'Any 1, COUNT(X) WHERE X is CWUser, X creation_date XD, '
175              'X modification_date XM, Y creation_date YD, Y is CWGroup '
176              'HAVING DAY(XD)>=DAY(YD) AND DAY(XM)<=DAY(YD)', 'X',