From 4217941100f8138954337eef80990d39f42953ff Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Fri, 22 Jan 2021 11:16:36 +0100
Subject: [PATCH] formdata: return missing boolean value as False, not None
 (#50428)

---
 tests/test_formdata.py            |  8 ++++++--
 tests/test_workflows.py           |  9 +++++++++
 wcs/qommon/templatetags/qommon.py |  4 ++++
 wcs/variables.py                  | 24 ++++++++++++++++++++----
 4 files changed, 39 insertions(+), 6 deletions(-)

diff --git a/tests/test_formdata.py b/tests/test_formdata.py
index e18f4c6c1..34005c4da 100644
--- a/tests/test_formdata.py
+++ b/tests/test_formdata.py
@@ -591,8 +591,9 @@ def variable_test_data(pub):
     formdef.name = 'foobarlazy'
     formdef.fields = [
         fields.StringField(id='0', label='string', varname='foo_foo'),
-        fields.BoolField(id='1', label='checkbox', varname='boolfield'),
-        fields.BoolField(id='2', label='checkbox', varname='boolfield2'),
+        fields.BoolField(id='1', label='checkbox', varname='boolfield', type='bool'),
+        fields.BoolField(id='2', label='checkbox', varname='boolfield2', type='bool'),
+        fields.BoolField(id='2b', label='checkbox', varname='boolfield3', type='bool'),
         fields.DateField(id='3', label='date', varname='datefield'),
         fields.ItemsField(id='4', label='items', items=['aa', 'ab', 'ac'], varname='itemsfield'),
         fields.FileField(id='5', label='file', varname='filefield'),
@@ -669,8 +670,11 @@ def test_lazy_formdata(pub, variable_test_data):
     assert lazy_formdata.var.foo_foo == 'bar'
     assert lazy_formdata.var.boolfield == 'False'
     assert bool(lazy_formdata.var.boolfield) is False
+    assert lazy_formdata.var.boolfield.raw is False
     assert lazy_formdata.var.boolfield2 == 'True'
+    assert lazy_formdata.var.boolfield2.raw is True
     assert bool(lazy_formdata.var.boolfield2) is True
+    assert lazy_formdata.var.boolfield3.raw is False
     assert lazy_formdata.var.datefield.raw == time.strptime('2018-07-31', '%Y-%m-%d')
     assert lazy_formdata.var.datefield.tm_year == 2018
     assert lazy_formdata.var.datefield.tm_mon == 7
diff --git a/tests/test_workflows.py b/tests/test_workflows.py
index cf1f20b04..d51520207 100644
--- a/tests/test_workflows.py
+++ b/tests/test_workflows.py
@@ -2294,6 +2294,15 @@ def test_webservice_with_complex_data(http_requests, pub):
         'bool': False,
     }
 
+    # check an empty boolean field is sent as False
+    del formdata.data['6']
+    with get_publisher().complex_data():
+        item.perform(formdata)
+    assert http_requests.get_last('url') == 'http://remote.example.net'
+    assert http_requests.get_last('method') == 'POST'
+    payload = json.loads(http_requests.get_last('body'))
+    assert payload['bool'] is False
+
     # check it's possible to disable complex data support
     pub.load_site_options()
     pub.site_options.set('options', 'complex-data', 'false')
diff --git a/wcs/qommon/templatetags/qommon.py b/wcs/qommon/templatetags/qommon.py
index f9f3eec50..525aba0af 100644
--- a/wcs/qommon/templatetags/qommon.py
+++ b/wcs/qommon/templatetags/qommon.py
@@ -216,6 +216,10 @@ def time(value, arg=None):
 def parse_decimal(value, do_raise=False):
     if hasattr(value, 'get_value'):
         value = value.get_value()  # unlazy
+    if isinstance(value, bool):
+        # treat all booleans as 0 (contrary to Python behaviour where
+        # decimal(True) == 1).
+        value = 0
     if isinstance(value, six.string_types):
         # replace , by . for French users comfort
         value = value.replace(',', '.')
diff --git a/wcs/variables.py b/wcs/variables.py
index b20634931..a432faf1c 100644
--- a/wcs/variables.py
+++ b/wcs/variables.py
@@ -605,16 +605,22 @@ class LazyFormDataVar(object):
                 raise
             maybe_varname = key.rsplit('_', 1)[0]
             field = self.varnames[maybe_varname]
+            if key.endswith('_raw') and field.type == 'bool':
+                # turn None into False so boolean fields are always a boolean.
+                return bool(self._data.get(field.id))
+
             if self._data.get(field.id) in (None, True, False):
                 # valid suffix and data of the correct type
                 return self._data.get(field.id)
             raise KeyError(key)
 
-        if self._data.get(field.id) is None:
-            return None
+        # let boolean pass through, to get None handled as False
+        if field.type != 'bool':
+            if self._data.get(field.id) is None:
+                return None
 
-        if str(field.id) not in self._data:
-            raise KeyError(key)
+            if str(field.id) not in self._data:
+                raise KeyError(key)
 
         klass = LazyFieldVar
         if field.store_structured_value:
@@ -625,6 +631,7 @@ class LazyFormDataVar(object):
             'password': LazyFieldVarPassword,
             'file': LazyFieldVarFile,
             'block': LazyFieldVarBlock,
+            'bool': LazyFieldVarBool,
         }.get(field.key, klass)
 
         return klass(**self.get_field_kwargs(field))
@@ -922,6 +929,15 @@ class LazyFieldVarMap(LazyFieldVarStructured):
         return ['lat', 'lon']
 
 
+class LazyFieldVarBool(LazyFieldVar):
+    def get_value(self):
+        return bool(self._data.get(self._field.id))
+
+    @property
+    def raw(self):
+        return self.get_value()
+
+
 class LazyFieldVarPassword(LazyFieldVar):
     def __getitem__(self, key):
         # get subpart (cleartext, md5, sha1) if it exists
-- 
2.30.0

