From e3aa6bc68042fa7ea3b89509c2d4449bf6c925b2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Thu, 29 Apr 2021 14:20:32 +0200
Subject: [PATCH] misc: add |exclude_value template filter (#53416)

---
 tests/test_formdata.py            | 43 +++++++++++++++++++++++++++++--
 wcs/qommon/templatetags/qommon.py |  7 +++++
 wcs/variables.py                  | 13 +++++++---
 3 files changed, 58 insertions(+), 5 deletions(-)

diff --git a/tests/test_formdata.py b/tests/test_formdata.py
index a8d14665..4da25782 100644
--- a/tests/test_formdata.py
+++ b/tests/test_formdata.py
@@ -1174,9 +1174,11 @@ def test_lazy_formdata_queryset_filter(pub, variable_test_data):
     formdef.workflow = wf
     formdef.store()
     data_class = lazy_formdata._formdef.data_class()
-    for _ in range(6):
+    for i in range(6):
         formdata = data_class()
         formdata.data = {'0': 'bar', '1': True, 'bo1': 'plop1', '10': '3'}
+        if i == 5:
+            formdata.data['3'] = datetime.date(2018, 8, 31).timetuple()
         formdata.just_created()
         formdata.store()
     for _ in range(4):
@@ -1237,6 +1239,25 @@ def test_lazy_formdata_queryset_filter(pub, variable_test_data):
         logged_error = pub.loggederror_class.select()[1]
         assert logged_error.summary == 'Invalid value "not a date" for filter "datefield"'
 
+    queryset = lazy_formdata.objects.filter_by('datefield').apply_exclude_value(
+        datetime.date(2018, 7, 31).timetuple()
+    )
+    assert queryset.count == 6
+    queryset = lazy_formdata.objects.filter_by('datefield').apply_exclude_value(datetime.date(2018, 7, 31))
+    assert queryset.count == 6
+    queryset = lazy_formdata.objects.filter_by('datefield').apply_exclude_value(
+        datetime.datetime(2018, 7, 31)
+    )
+    assert queryset.count == 6
+    queryset = lazy_formdata.objects.filter_by('datefield').apply_exclude_value('2018-07-31')
+    assert queryset.count == 6
+    queryset = lazy_formdata.objects.filter_by('datefield').apply_exclude_value('still not a date')
+    assert queryset.count == 0
+    if pub.is_using_postgresql():
+        assert pub.loggederror_class.count() == 3
+        logged_error = pub.loggederror_class.select()[2]
+        assert logged_error.summary == 'Invalid value "still not a date" for filter "datefield"'
+
     queryset = lazy_formdata.objects.filter_by('boolfield').apply_filter_value(True)
     assert queryset.count == 6
     queryset = lazy_formdata.objects.filter_by('boolfield').apply_filter_value(1)
@@ -1247,6 +1268,8 @@ def test_lazy_formdata_queryset_filter(pub, variable_test_data):
     assert queryset.count == 5
     queryset = lazy_formdata.objects.filter_by('boolfield').apply_filter_value(0)
     assert queryset.count == 5
+    queryset = lazy_formdata.objects.filter_by('boolfield').apply_exclude_value(0)
+    assert queryset.count == 6
 
     queryset = lazy_formdata.objects.filter_by('term1').apply_filter_value('3')
     assert queryset.count == 7
@@ -1254,6 +1277,10 @@ def test_lazy_formdata_queryset_filter(pub, variable_test_data):
     assert queryset.count == 7
     queryset = lazy_formdata.objects.filter_by('term1').apply_filter_value('foobar')
     assert queryset.count == 0
+    queryset = lazy_formdata.objects.filter_by('term1').apply_exclude_value('3')
+    assert queryset.count == 4
+    queryset = lazy_formdata.objects.filter_by('term1').apply_exclude_value('foobar')
+    assert queryset.count == 11
 
     queryset = lazy_formdata.objects.filter_by('email').apply_filter_value('bar')
     assert queryset.count == 0
@@ -1310,6 +1337,10 @@ def test_lazy_formdata_queryset_filter(pub, variable_test_data):
     assert tmpl.render(context) == '7'
     tmpl = Template('{{form_objects|filter_by:"foo_foo"|filter_value:"bar"|length}}')
     assert tmpl.render(context) == '7'
+    tmpl = Template('{{form_objects|filter_by:"foo_foo"|exclude_value:"bar"|count}}')
+    assert tmpl.render(context) == '4'
+    tmpl = Template('{{form_objects|filter_by:"foo_foo"|exclude_value:"bar"|length}}')
+    assert tmpl.render(context) == '4'
 
     pub.substitutions.feed(formdata)
     tmpl = Template('{{form_objects|filter_by:"foo_foo"|filter_value:form_var_foo_foo|count}}')
@@ -1320,6 +1351,14 @@ def test_lazy_formdata_queryset_filter(pub, variable_test_data):
     assert tmpl.render(context) == '5'
     tmpl = Template('{{form_objects|filter_by:"term1"|filter_value:form_var_term1|count}}')
     assert tmpl.render(context) == '7'
+    tmpl = Template('{{form_objects|filter_by:"foo_foo"|exclude_value:form_var_foo_foo|count}}')
+    assert tmpl.render(context) == '4'
+    tmpl = Template('{{form_objects|filter_by:"datefield"|exclude_value:form_var_datefield|count}}')
+    assert tmpl.render(context) == '6'
+    tmpl = Template('{{form_objects|filter_by:"boolfield"|exclude_value:form_var_boolfield|count}}')
+    assert tmpl.render(context) == '6'
+    tmpl = Template('{{form_objects|filter_by:"term1"|exclude_value:form_var_term1|count}}')
+    assert tmpl.render(context) == '4'
 
     tmpl = Template('{{form.objects|exclude_self|filter_by:"foo_foo"|filter_value:form_var_foo_foo|count}}')
     assert tmpl.render(context) == '6'
@@ -1402,7 +1441,7 @@ def test_lazy_formdata_queryset_filter(pub, variable_test_data):
     tmpl = Template('{% for v in form_objects|order_by:"id"|getlist:"foo_foo" %}{{ v }},{% endfor %}')
     assert tmpl.render(context) == 'bar,bar,bar,bar,bar,bar,bar,foo,foo,foo,foo,'
     tmpl = Template('{% for v in form_objects|order_by:"id"|getlist:"datefield" %}{{ v|date }},{% endfor %}')
-    assert tmpl.render(context) == '2018-07-31,,,,,,,2018-07-31,2018-07-31,2018-07-31,2018-07-31,'
+    assert tmpl.render(context) == '2018-07-31,,,,,,2018-08-31,2018-07-31,2018-07-31,2018-07-31,2018-07-31,'
     tmpl = Template('{% if "foo" in form_objects|getlist:"foo_foo" %}OK{% else %}KO{% endif%}')
     assert tmpl.render(context) == 'OK'
     tmpl = Template('{% if "fooooooooooooooo" in form_objects|getlist:"foo_foo" %}OK{% else %}KO{% endif%}')
diff --git a/wcs/qommon/templatetags/qommon.py b/wcs/qommon/templatetags/qommon.py
index 45960a1f..94f0cfc8 100644
--- a/wcs/qommon/templatetags/qommon.py
+++ b/wcs/qommon/templatetags/qommon.py
@@ -683,6 +683,13 @@ def filter_value(queryset, value):
     return queryset.apply_filter_value(value)
 
 
+@register.filter
+def exclude_value(queryset, value):
+    if hasattr(value, 'get_value'):
+        value = value.get_value()  # unlazy
+    return queryset.apply_exclude_value(value)
+
+
 @register.filter
 def count(queryset):
     if hasattr(queryset, 'get_value'):
diff --git a/wcs/variables.py b/wcs/variables.py
index 62109761..8822aba1 100644
--- a/wcs/variables.py
+++ b/wcs/variables.py
@@ -175,7 +175,7 @@ class LazyFormDefObjectsManager:
             if getattr(field, 'varname', None) == key:
                 return field
 
-    def apply_filter_value(self, value):
+    def apply_filter_value(self, value, exclude=False):
         assert self.pending_attr
 
         field = self.get_field(self.pending_attr)
@@ -197,8 +197,15 @@ class LazyFormDefObjectsManager:
 
         from wcs import sql
 
-        criteria = Equal(sql.get_field_id(field), value)
-        return self._clone(self._criterias + [criteria])
+        field_id = sql.get_field_id(field)
+        if exclude:
+            criterias = [Or([Null(field_id), NotEqual(field_id, value)])]
+        else:
+            criterias = [Equal(field_id, value)]
+        return self._clone(self._criterias + criterias)
+
+    def apply_exclude_value(self, value):
+        return self.apply_filter_value(value, exclude=True)
 
     def getlist(self, key):
         return LazyList(self, key)
-- 
2.30.2

