From cac25074da810985d16749370ef3e8c855dfd3f5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Sun, 28 Jun 2020 08:51:38 +0200
Subject: [PATCH 1/2] general: add filtering methods to lazy querysets (#43328)

---
 tests/test_formdata.py            | 47 +++++++++++++++++++++++++++++++
 wcs/qommon/templatetags/qommon.py | 15 ++++++++++
 wcs/variables.py                  | 29 ++++++++++++++++++-
 3 files changed, 90 insertions(+), 1 deletion(-)

diff --git a/tests/test_formdata.py b/tests/test_formdata.py
index 105e43eb7..a1ed08fdf 100644
--- a/tests/test_formdata.py
+++ b/tests/test_formdata.py
@@ -882,6 +882,53 @@ def test_lazy_formdata_queryset_distance(pub, variable_test_data):
     assert len(nearby) == 0
 
 
+def test_lazy_formdata_queryset_filter(pub, variable_test_data):
+    lazy_formdata = variable_test_data
+    formdef = lazy_formdata._formdef
+    formdef.store()
+    data_class = lazy_formdata._formdef.data_class()
+    for i in range(6):
+        formdata = data_class()
+        formdata.data = {'0': 'bar'}
+        formdata.just_created()
+        formdata.store()
+    for i in range(4):
+        formdata = data_class()
+        formdata.data = {'0': 'foo'}
+        formdata.just_created()
+        formdata.jump_status('finished')
+        formdata.store()
+
+    formdata = data_class()
+    formdata.data = {'0': 'bar'}
+    formdata.status = 'draft'
+    formdata.store()
+
+    # filter function
+    queryset = lazy_formdata.objects.filter_by('foo_foo').apply_filter_value('bar')
+    assert queryset.count == 7
+    queryset = lazy_formdata.objects.filter_by('foo_foo').apply_filter_value('foo')
+    assert queryset.count == 4
+    queryset = lazy_formdata.objects.filter_by('foo_foo').apply_filter_value('X')
+    assert queryset.count == 0
+
+    # filter using attribute name
+    queryset = lazy_formdata.objects.filter_by_foo_foo().apply_filter_value('bar')
+    assert queryset.count == 7
+
+    # filter + exclude current formdata
+    queryset = lazy_formdata.objects.exclude_self().filter_by('foo_foo').apply_filter_value('bar')
+    assert queryset.count == 6
+
+    queryset = lazy_formdata.objects.exclude_self().filter_by('foo_foo').apply_filter_value('foo')
+    assert queryset.count == 4
+
+    # template tags
+    context = pub.substitutions.get_context_variables(mode='lazy')
+    tmpl = Template('{{form_objects|filter_by:"foo_foo"|filter_value:"bar"|count}}')
+    assert tmpl.render(context) == '7'
+
+
 def test_lazy_variables(pub, variable_test_data):
     formdata = FormDef.select()[0].data_class().select()[0]
     for mode in (None, 'lazy'):
diff --git a/wcs/qommon/templatetags/qommon.py b/wcs/qommon/templatetags/qommon.py
index 2f688a401..35a0d9bda 100644
--- a/wcs/qommon/templatetags/qommon.py
+++ b/wcs/qommon/templatetags/qommon.py
@@ -460,6 +460,21 @@ def order_by(queryset, attribute):
     return queryset.order_by(attribute)
 
 
+@register.filter
+def filter_by(queryset, attribute):
+    return queryset.filter_by(attribute)
+
+
+@register.filter
+def filter_value(queryset, value):
+    return queryset.apply_filter_value(value)
+
+
+@register.filter
+def count(queryset):
+    return queryset.count
+
+
 @register.filter
 def reproj(coords, projection_name):
     proj = pyproj.Proj(init='EPSG:4326')
diff --git a/wcs/variables.py b/wcs/variables.py
index 05f5fdd8a..5aa1452ba 100644
--- a/wcs/variables.py
+++ b/wcs/variables.py
@@ -74,6 +74,10 @@ class LazyFormDefObjectsManager(object):
         user = get_request().user
         return self._clone(self._criterias + [Equal('user_id', str(user.id) if user else '-1')])
 
+    def exclude_self(self):
+        assert self._formdata
+        return self._clone(self._criterias + [NotEqual('id', str(self._formdata.id))])
+
     def drafts(self):
         criterias = [x for x in self._criterias if not getattr(x, 'exclude_drafts', False)]
         return self._clone(criterias + [Equal('status', 'draft')])
@@ -97,12 +101,31 @@ class LazyFormDefObjectsManager(object):
             return bool(obj._distance < distance)
         return self._clone(self._criterias + [distance_check])
 
+    def filter_by(self, attribute):
+        qs = self._clone(self._criterias)
+        qs.pending_attr = attribute
+        return qs
+
+    def apply_filter_value(self, value):
+        assert self.pending_attr
+        for field in self._formdef.fields:
+            if getattr(field, 'varname', None) == self.pending_attr:
+                from wcs import sql
+                criteria = Equal(sql.get_field_id(field), value)
+                break
+        else:
+            raise Exception('invalid filter')
+        return self._clone(self._criterias + [criteria])
+
     def __getattr__(self, attribute):
-        # backward compatibility
         if attribute.startswith('count_status_'):
+            # backward compatibility
             status = attribute[len('count_status_'):]
             return len(self._formdef.data_class().get_ids_with_indexed_value(
                 'status', 'wf-%s' % status))
+        if attribute.startswith('filter_by_'):
+            attribute_name = attribute[len('filter_by_'):]
+            return lambda: self.filter_by(attribute_name)
         if attribute == 'formdef':
             warnings.warn('Deprecated access to formdef', DeprecationWarning)
             return self._formdef
@@ -421,6 +444,10 @@ class LazyFormData(LazyFormDef):
         # this gets used to generate an email attachment :/
         return self._formdata.export_to_json(include_files=include_files)
 
+    def get(self, key):
+        # compatibility with |get filter, to return a field by varname
+        return getattr(self.var, key, None)
+
     def __getitem__(self, key):
         try:
             return getattr(self, key)
-- 
2.27.0

