From 952b339778c5c535c0de86d29b0f7f6c45911226 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  | 15 +++++++++++++++
 wcs/sql.py             | 11 +++++++++++
 wcs/variables.py       | 14 +++++++++++---
 4 files changed, 59 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..86b2b665 100644
--- a/wcs/qommon/storage.py
+++ b/wcs/qommon/storage.py
@@ -216,6 +216,21 @@ class Intersects(Criteria):
         return func
 
 
+class NotIntersects(Criteria):
+    def build_lambda(self):
+        value = set(self.value)
+
+        def func(x):
+            try:
+                return value.isdisjoint(set(getattr(x, self.attribute, []) or []))
+            except KeyError:
+                # this may happen if used to check a formdata field that didn't
+                # exist when the formdata was created.
+                return False
+
+        return func
+
+
 class Or(Criteria):
     def __init__(self, criterias, **kwargs):
         self.criterias = criterias
diff --git a/wcs/sql.py b/wcs/sql.py
index c52b549b..04da9461 100644
--- a/wcs/sql.py
+++ b/wcs/sql.py
@@ -269,6 +269,17 @@ class Intersects(Criteria):
         return {'c%s' % id(self.value): list(self.value)}
 
 
+class NotIntersects(Criteria):
+    def as_sql(self):
+        if not self.value:
+            return 'ARRAY_LENGTH(%s, 1) IS NULL' % self.attribute
+        else:
+            return 'NOT(%s && %%(c%s)s)' % (self.attribute, id(self.value))
+
+    def as_sql_param(self):
+        return {'c%s' % id(self.value): list(self.value)}
+
+
 class ILike(Criteria):
     def __init__(self, attribute, value, **kwargs):
         super().__init__(attribute, value, **kwargs)
diff --git a/wcs/variables.py b/wcs/variables.py
index dd5f0270..02310db7 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, NotEqual, NotIntersects, 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 = NotIntersects(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

