[ui lib] facet and form widget for Integer used to store binary mask. Closes #2054771
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Thu, 27 Oct 2011 10:38:16 +0200
changeset 8029 805d4e121b65
parent 8028 58e9bc8a1f2c
child 8030 552d85fcb587
[ui lib] facet and form widget for Integer used to store binary mask. Closes #2054771
web/facet.py
web/formwidgets.py
web/test/unittest_facet.py
web/test/unittest_formwidgets.py
--- a/web/facet.py	Thu Oct 27 10:38:03 2011 +0200
+++ b/web/facet.py	Thu Oct 27 10:38:16 2011 +0200
@@ -33,6 +33,7 @@
 .. autoclass:: cubicweb.web.facet.RQLPathFacet
 .. autoclass:: cubicweb.web.facet.RangeFacet
 .. autoclass:: cubicweb.web.facet.DateRangeFacet
+.. autoclass:: cubicweb.web.facet.BitFieldFacet
 
 Classes for facets implementor
 ------------------------------
@@ -977,8 +978,12 @@
             cleanup_select(select, filtered_variable)
             newvar = prepare_vocabulary_select(select, filtered_variable, self.rtype, self.role)
             _set_orderby(select, newvar, self.sortasc, self.sortfunc)
+            if self.cw_rset:
+                args = self.cw_rset.args
+            else: # vocabulary used for possible_values
+                args = None
             try:
-                rset = self.rqlexec(select.as_string(), self.cw_rset.args)
+                rset = self.rqlexec(select.as_string(), args)
             except Exception:
                 self.exception('error while getting vocabulary for %s, rql: %s',
                                self, select.as_string())
@@ -1364,6 +1369,42 @@
             self.select.add_relation(var, self.rtype, self.filtered_variable)
 
 
+class BitFieldFacet(AttributeFacet):
+    """Base facet class for Int field holding some bit values using binary
+    masks.
+
+    label / value for each bit should be given using the :attr:`choices`
+    attribute.
+
+    See also :class:`~cubicweb.web.formwidgets.BitSelect`.
+    """
+    choices = None # to be set on concret class
+    def add_rql_restrictions(self):
+        value = self._cw.form.get(self.__regid__)
+        if not value:
+            return
+        if isinstance(value, list):
+            value = reduce(lambda x, y: int(x) | int(y), value)
+        attr_var = self.select.make_variable()
+        self.select.add_relation(self.filtered_variable, self.rtype, attr_var)
+        comp = nodes.Comparison('=', nodes.Constant(value, 'Int'))
+        comp.append(nodes.MathExpression('&', nodes.variable_ref(attr_var),
+                                         nodes.Constant(value, 'Int')))
+        having = self.select.having
+        if having:
+            self.select.replace(having[0], nodes.And(having[0], comp))
+        else:
+            self.select.set_having([comp])
+
+    def rset_vocabulary(self, rset):
+        mask = reduce(lambda x, y: x | (y[0] or 0), rset, 0)
+        return sorted([(self._cw._(label), val) for label, val in self.choices
+                       if val & mask])
+
+    def possible_values(self):
+        return [unicode(val) for label, val in self.vocabulary()]
+
+
 ## html widets ################################################################
 _DEFAULT_CONSTANT_VOCAB_WIDGET_HEIGHT = 12
 
--- a/web/formwidgets.py	Thu Oct 27 10:38:03 2011 +0200
+++ b/web/formwidgets.py	Thu Oct 27 10:38:16 2011 +0200
@@ -75,6 +75,7 @@
 
 .. autoclass:: cubicweb.web.formwidgets.PasswordInput
 .. autoclass:: cubicweb.web.formwidgets.IntervalWidget
+.. autoclass:: cubicweb.web.formwidgets.BitSelect
 .. autoclass:: cubicweb.web.formwidgets.HorizontalLayoutWidget
 .. autoclass:: cubicweb.web.formwidgets.EditableURLWidget
 
@@ -452,7 +453,7 @@
                 oattrs.setdefault('label', label or '')
                 options.append(u'<optgroup %s>' % uilib.sgml_attributes(oattrs))
                 optgroup_opened = True
-            elif value in curvalues:
+            elif self.value_selected(value, curvalues):
                 options.append(tags.option(label, value=value,
                                            selected='selected', **oattrs))
             else:
@@ -468,6 +469,36 @@
         return tags.select(name=field.input_name(form, self.suffix),
                            multiple=self._multiple, options=options, **attrs)
 
+    def value_selected(self, value, curvalues):
+        return value in curvalues
+
+
+class BitSelect(Select):
+    """Select widget for IntField using a vocabulary with bit masks as values.
+
+    See also :class:`~cubicweb.web.facet.BitFieldFacet`.
+    """
+    def __init__(self, attrs=None, multiple=True, **kwargs):
+        super(BitSelect, self).__init__(attrs, multiple=multiple, **kwargs)
+
+    def value_selected(self, value, curvalues):
+        mask = reduce(lambda x, y: int(x) | int(y), curvalues, 0)
+        return int(value) & mask
+
+    def process_field_data(self, form, field):
+        """Return process posted value(s) for widget and return something
+        understandable by the associated `field`. That value may be correctly
+        typed or a string that the field may parse.
+        """
+        val = super(BitSelect, self).process_field_data(form, field)
+        if isinstance(val, list):
+            val = reduce(lambda x, y: int(x) | int(y), val, 0)
+        elif val:
+            val = int(val)
+        else:
+            val = 0
+        return val
+
 
 class CheckBox(Input):
     """Simple <input type='checkbox'>, for field having a specific
--- a/web/test/unittest_facet.py	Thu Oct 27 10:38:03 2011 +0200
+++ b/web/test/unittest_facet.py	Thu Oct 27 10:38:16 2011 +0200
@@ -195,6 +195,32 @@
         self.assertEqual(f.select.as_string(),
                           "DISTINCT Any  WHERE X is CWUser, X login 'admin'")
 
+    def test_bitfield(self):
+        req, rset, rqlst, filtered_variable = self.prepare_rqlst(
+            'CWAttribute X WHERE X ordernum XO',
+            expected_baserql='Any X WHERE X ordernum XO, X is CWAttribute',
+            expected_preparedrql='DISTINCT Any  WHERE X ordernum XO, X is CWAttribute')
+        f = facet.BitFieldFacet(req, rset=rset,
+                                select=rqlst.children[0],
+                                filtered_variable=filtered_variable)
+        f.choices = [('un', 1,), ('deux', 2,)]
+        f.rtype = 'ordernum'
+        self.assertEqual(f.vocabulary(),
+                          [(u'deux', 2), (u'un', 1)])
+        # ensure rqlst is left unmodified
+        self.assertEqual(rqlst.as_string(), 'DISTINCT Any  WHERE X ordernum XO, X is CWAttribute')
+        #rqlst = rset.syntax_tree()
+        self.assertEqual(f.possible_values(),
+                          ['2', '1'])
+        # ensure rqlst is left unmodified
+        self.assertEqual(rqlst.as_string(), 'DISTINCT Any  WHERE X ordernum XO, X is CWAttribute')
+        req.form[f.__regid__] = '3'
+        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 ordernum XO, X is CWAttribute, X ordernum C HAVING 3 = (C & 3)")
+
     def test_rql_path_eid(self):
         req, rset, rqlst, filtered_variable = self.prepare_rqlst()
         class RPF(facet.RQLPathFacet):
--- a/web/test/unittest_formwidgets.py	Thu Oct 27 10:38:03 2011 +0200
+++ b/web/test/unittest_formwidgets.py	Thu Oct 27 10:38:16 2011 +0200
@@ -32,7 +32,7 @@
 
 class WidgetsTC(TestCase):
 
-    def test_state_fields(self):
+    def test_editableurl_widget(self):
         field = formfields.guess_field(schema['Bookmark'], schema['path'])
         widget = formwidgets.EditableURLWidget()
         req = fake.FakeRequest(form={'path-subjectfqs:A': 'param=value&vid=view'})
@@ -40,5 +40,21 @@
         self.assertEqual(widget.process_field_data(form, field),
                          '?param=value%26vid%3Dview')
 
+    def test_bitselect_widget(self):
+        field = formfields.guess_field(schema['CWAttribute'], schema['ordernum'])
+        field.choices = [('un', '1',), ('deux', '2',)]
+        widget = formwidgets.BitSelect(settabindex=False)
+        req = fake.FakeRequest(form={'ordernum-subject:A': ['1', '2']})
+        form = mock(_cw=req, formvalues={}, edited_entity=mock(eid='A'),
+                    form_previous_values=())
+        self.assertMultiLineEqual(widget._render(form, field, None),
+                             '''\
+<select id="ordernum-subject:A" multiple="multiple" name="ordernum-subject:A" size="2">
+<option selected="selected" value="2">deux</option>
+<option selected="selected" value="1">un</option>
+</select>''')
+        self.assertEqual(widget.process_field_data(form, field),
+                         3)
+
 if __name__ == '__main__':
     unittest_main()