From 0564eea38bd7ce252311c9f7ede9d45e8111e35d Mon Sep 17 00:00:00 2001
From: Serghei Mihai <smihai@entrouvert.com>
Date: Tue, 10 Aug 2021 16:02:54 +0200
Subject: [PATCH] variables: handle filtering lists by any of values (#56089)

---
 tests/test_formdata.py | 24 ++++++++++++++++++++++--
 wcs/qommon/storage.py  | 17 +++++++++++++++++
 wcs/variables.py       | 14 +++++++++++---
 3 files changed, 50 insertions(+), 5 deletions(-)

diff --git a/tests/test_formdata.py b/tests/test_formdata.py
index 7564520f..a2402689 100644
--- a/tests/test_formdata.py
+++ b/tests/test_formdata.py
@@ -1567,17 +1567,18 @@ def test_lazy_global_forms(pub):
     formdef.fields = [
         fields.StringField(id='1', label='Test', type='string', varname='foo'),
         fields.StringField(id='2', label='key', type='string', varname='key'),
+        fields.ItemsField(id='3', label='Items', type='items', varname='items'),
     ]
     formdef.store()
     formdef.data_class().wipe()
     for i in range(6):
         formdata = formdef.data_class()()
-        formdata.data = {'1': 'bar', '2': str(i)}
+        formdata.data = {'1': 'bar', '2': str(i), '3': [str(i % 2), str(i % 3)]}
         formdata.just_created()
         formdata.store()
     for i in range(4):
         formdata = formdef.data_class()()
-        formdata.data = {'1': 'foo', '2': str(i + 6)}
+        formdata.data = {'1': 'foo', '2': str(i + 6), '3': [str(i)]}
         formdata.just_created()
         formdata.jump_status('finished')
         formdata.store()
@@ -1595,6 +1596,25 @@ def test_lazy_global_forms(pub):
     tmpl = Template('{{forms|objects:"foobarlazy"|filter_by:"foo"|filter_value:"bar"|count}}')
     assert tmpl.render(context) == '6'
 
+    # filter value by multiple valued field
+    tmpl = Template('{{forms|objects:"foobarlazy"|filter_by:"items"|filter_value:"3"|count}}')
+    assert tmpl.render(context) == '1'
+
+    tmpl = Template('{{forms|objects:"foobarlazy"|filter_by:"items"|filter_value:"2"|count}}')
+    assert tmpl.render(context) == '3'
+
+    tmpl = Template('{{forms|objects:"foobarlazy"|filter_by:"items"|filter_value:"1|2"|count}}')
+    assert tmpl.render(context) == '7'
+
+    tmpl = Template('{{forms|objects:"foobarlazy"|filter_by:"items"|filter_value:"6"|count}}')
+    assert tmpl.render(context) == '0'
+
+    tmpl = Template('{{forms|objects:"foobarlazy"|filter_by:"items"|exclude_value:"1|2"|count}}')
+    assert tmpl.render(context) == '3'
+
+    tmpl = Template('{{forms|objects:"foobarlazy"|filter_by:"items"|exclude_value:"6"|count}}')
+    assert tmpl.render(context) == '10'
+
     pub.custom_view_class.wipe()
 
     custom_view1 = pub.custom_view_class()
diff --git a/wcs/qommon/storage.py b/wcs/qommon/storage.py
index 36329cd2..5ee3374e 100644
--- a/wcs/qommon/storage.py
+++ b/wcs/qommon/storage.py
@@ -216,6 +216,23 @@ class Intersects(Criteria):
         return func
 
 
+class Not(Criteria):
+    def __init__(self, criteria, **kwargs):
+        self.criteria = criteria
+
+    def build_lambda(self):
+        func = lambda x: False
+
+        def combine_callables(x1, x2):
+            return lambda x: not x2(x)
+
+        func = combine_callables(func, self.criteria.build_lambda())
+        return func
+
+    def __repr__(self):
+        return '<%s (%r)>' % (self.__class__.__name__, self.criteria)
+
+
 class Or(Criteria):
     def __init__(self, criterias, **kwargs):
         self.criterias = criterias
diff --git a/wcs/variables.py b/wcs/variables.py
index dd5f0270..b0c3b555 100644
--- a/wcs/variables.py
+++ b/wcs/variables.py
@@ -25,7 +25,7 @@ from .carddef import CardDef
 from .formdef import FormDef
 from .qommon import _, force_str, misc
 from .qommon.evalutils import make_datetime
-from .qommon.storage import Equal, NotEqual, Null, Or
+from .qommon.storage import Equal, Intersects, Not, NotEqual, Null, Or
 from .qommon.substitution import CompatibilityNamesDict
 from .qommon.templatetags.qommon import parse_datetime
 
@@ -202,10 +202,18 @@ class LazyFormDefObjectsManager:
         from wcs import sql
 
         field_id = sql.get_field_id(field)
+
         if exclude:
-            criterias = NotEqual(field_id, value, field=field)
+            if isinstance(value, list):
+                criterias = Not(Intersects(field_id, value, field=field))
+            else:
+                criterias = NotEqual(field_id, value, field=field)
         else:
-            criterias = Equal(field_id, value, field=field)
+            if isinstance(value, list):
+                criterias = Intersects(field_id, value, field=field)
+            else:
+                criterias = Equal(field_id, value, field=field)
+
         return self._clone(self._criterias + [criterias])
 
     def apply_exclude_value(self, value):
-- 
2.33.0

