From 39bf3ebe06325de3c288fc4e4fe6f04d3717697a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Mon, 10 Oct 2022 12:24:55 +0200
Subject: [PATCH 2/2] misc: allow operators for |filter_by internal-id/status
 fields (#68943)

---
 tests/test_formdata.py | 71 ++++++++++++++++++++++++++++++++++++++++--
 wcs/variables.py       | 67 ++++++++++++++++++++++++++-------------
 2 files changed, 114 insertions(+), 24 deletions(-)

diff --git a/tests/test_formdata.py b/tests/test_formdata.py
index 2f1dd549f..f4502c72a 100644
--- a/tests/test_formdata.py
+++ b/tests/test_formdata.py
@@ -1528,6 +1528,17 @@ def test_lazy_formdata_queryset_filter(pub, variable_test_data):
         .count
         == 1
     )
+    tmpl = Template(
+        '{{form_objects|filter_by:"user"|equal|filter_value:form_user|filter_by:"foo_foo"|filter_value:"foo"|count}}'
+    )
+    assert tmpl.render(context) == '1'
+    for operator in ['not_equal', 'less_than', 'less_than_or_equal', 'greater_than', 'greater_than_or_equal']:
+        pub.loggederror_class.wipe()
+        tmpl = Template('{{form_objects|filter_by:"user"|%s|filter_value:"foo"|count}}' % operator)
+        assert tmpl.render(context) == '0'
+        assert pub.loggederror_class.count() == 1
+        logged_error = pub.loggederror_class.select(order_by='id')[0]
+        assert logged_error.summary == 'Invalid operator "%s" for filter "user"' % operator
 
     # test |current_user
     pub.get_request()._user = ()  # reset cache
@@ -1576,14 +1587,15 @@ def test_lazy_formdata_queryset_filter(pub, variable_test_data):
     # test |filter_by_internal_id
     context = pub.substitutions.get_context_variables(mode='lazy')
     for tpl in ['filter_by_internal_id', 'filter_by:"internal_id"|filter_value']:
+        pub.loggederror_class.wipe()
         tmpl = Template('{{form_objects|%s:"%s"|count}}' % (tpl, finished_formdata.id))
         assert tmpl.render(context) == '1'
         tmpl = Template('{{form_objects|%s:"%s"|count}}' % (tpl, '0'))
         assert tmpl.render(context) == '0'
         tmpl = Template('{{form_objects|%s:"%s"|count}}' % (tpl, 'invalid value'))
         assert tmpl.render(context) == '0'
-        assert pub.loggederror_class.count() == 4
-        logged_error = pub.loggederror_class.select(order_by='id')[3]
+        assert pub.loggederror_class.count() == 1
+        logged_error = pub.loggederror_class.select(order_by='id')[0]
         assert logged_error.summary == 'Invalid value "invalid value" for filter "internal_id"'
 
     # test |filter_by_number
@@ -1592,6 +1604,21 @@ def test_lazy_formdata_queryset_filter(pub, variable_test_data):
         assert tmpl.render(context) == '1'
         tmpl = Template('{{form_objects|%s:"%s"|count}}' % (tpl, 'invalid value'))
         assert tmpl.render(context) == '0'
+    tmpl = Template(
+        '{{form_objects|filter_by:"number"|equal|filter_value:"%s"|count}}'
+        % finished_formdata.get_display_id()
+    )
+    assert tmpl.render(context) == '1'
+    for operator in ['not_equal', 'less_than', 'less_than_or_equal', 'greater_than', 'greater_than_or_equal']:
+        pub.loggederror_class.wipe()
+        tmpl = Template(
+            '{{form_objects|filter_by:"number"|%s|filter_value:"%s"|count}}'
+            % (operator, finished_formdata.get_display_id())
+        )
+        assert tmpl.render(context) == '0'
+        assert pub.loggederror_class.count() == 1
+        logged_error = pub.loggederror_class.select(order_by='id')[0]
+        assert logged_error.summary == 'Invalid operator "%s" for filter "number"' % operator
 
     # test |is_empty
     tmpl = Template('{{form_objects|pending|is_empty}}')
@@ -3573,7 +3600,8 @@ def test_formdata_filtering_on_fields(pub):
                 '5': 'a@localhost' if i % 2 else 'b@localhost',
             }
         formdata.just_created()
-        formdata.jump_status('new')
+        if i % 3:
+            formdata.jump_status('finished')
         formdata.store()
 
     context = pub.substitutions.get_context_variables(mode='lazy')
@@ -3770,6 +3798,43 @@ def test_formdata_filtering_on_fields(pub):
         logged_error = pub.loggederror_class.select(order_by='id')[0]
         assert logged_error.summary == 'Operator filter is not allowed for exclude_value filter'
 
+    # internal_id
+    params = [
+        ('equal', '1', '1'),
+        ('equal', '01', '1'),
+        ('not_equal', '1', '10'),
+        ('less_than', '1', '0'),
+        ('less_than_or_equal', '1', '1'),
+        ('greater_than', '1', '10'),
+        ('greater_than', '10', '1'),
+        ('greater_than_or_equal', '1', '11'),
+    ]
+    for operator, value, result in params:
+        tmpl = Template(
+            '{{forms|objects:"test"|filter_by:"internal_id"|%s|filter_value:"%s"|count}}' % (operator, value)
+        )
+        assert tmpl.render(context) == result
+
+    # status
+    params = [
+        ('equal', 'Just Submitted', '4'),
+        ('equal', 'Finished', '7'),
+        ('equal', 'Unknown', '0'),
+        ('not_equal', 'Finished', '4'),
+    ]
+    for operator, value, result in params:
+        tmpl = Template(
+            '{{forms|objects:"test"|filter_by:"status"|%s|filter_value:"%s"|count}}' % (operator, value)
+        )
+        assert tmpl.render(context) == result
+    for operator in ['less_than', 'less_than_or_equal', 'greater_than', 'greater_than_or_equal']:
+        pub.loggederror_class.wipe()
+        tmpl = Template('{{forms|objects:"test"|filter_by:"status"|%s|filter_value:"plop"|count}}' % operator)
+        assert tmpl.render(context) == '0'
+        assert pub.loggederror_class.count() == 1
+        logged_error = pub.loggederror_class.select(order_by='id')[0]
+        assert logged_error.summary == 'Invalid operator "%s" for filter "status"' % operator
+
 
 def test_formdata_filtering_on_block_fields(pub):
     NamedDataSource.wipe()
diff --git a/wcs/variables.py b/wcs/variables.py
index 2fd441155..bb0785f8d 100644
--- a/wcs/variables.py
+++ b/wcs/variables.py
@@ -116,6 +116,12 @@ class LazyFormDefObjectsManager:
         return self.filter_by_user(get_request().user)
 
     def filter_by_user(self, user, op='eq'):
+        if op not in ['eq']:
+            get_publisher().record_error(
+                _('Invalid operator "%s" for filter "%s"') % (self.get_operator_name(op), self.pending_attr),
+                formdata=self._formdata,
+            )
+            return self.none()
         if isinstance(user, str):
             user = get_publisher().user_class.lookup_by_string(user)
         if not user:
@@ -123,10 +129,17 @@ class LazyFormDefObjectsManager:
         return self._clone(self._criterias + [Equal('user_id', str(user.id))])
 
     def filter_by_status(self, status, op='eq'):
+        criteria_class = self.get_operator_class(op)
+        if op not in ['eq', 'ne']:
+            get_publisher().record_error(
+                _('Invalid operator "%s" for filter "%s"') % (self.get_operator_name(op), self.pending_attr),
+                formdata=self._formdata,
+            )
+            return self.none()
         for wfs in self._formdef.workflow.possible_status:
             if wfs.name == status:
                 wf_status = 'wf-%s' % wfs.id
-                return self._clone(self._criterias + [Equal('status', wf_status)])
+                return self._clone(self._criterias + [criteria_class('status', wf_status)])
         return self.none()
 
     def with_custom_view(self, custom_view_slug):
@@ -203,9 +216,16 @@ class LazyFormDefObjectsManager:
                 formdata=self._formdata,
             )
             return self.none()
-        return self._clone(self._criterias + [Equal('id', str(value))])
+        criteria_class = self.get_operator_class(op)
+        return self._clone(self._criterias + [criteria_class('id', str(value))])
 
     def filter_by_number(self, value, op='eq'):
+        if op not in ['eq']:
+            get_publisher().record_error(
+                _('Invalid operator "%s" for filter "%s"') % (self.get_operator_name(op), self.pending_attr),
+                formdata=self._formdata,
+            )
+            return self.none()
         return self._clone(self._criterias + [Equal('id_display', str(value))])
 
     def get_fields(self, key):
@@ -230,6 +250,28 @@ class LazyFormDefObjectsManager:
             return operators
         return None
 
+    def get_operator_class(self, op):
+        operators_mapping = {
+            'eq': Equal,
+            'ne': NotEqual,
+            'lt': Less,
+            'lte': LessOrEqual,
+            'gt': Greater,
+            'gte': GreaterOrEqual,
+        }
+        return operators_mapping[op]
+
+    def get_operator_name(self, op):
+        operator_names_mapping = {
+            'eq': 'equal',
+            'ne': 'not_equal',
+            'lt': 'less_than',
+            'lte': 'less_than_or_equal',
+            'gt': 'greater_than',
+            'gte': 'greater_than_or_equal',
+        }
+        return operator_names_mapping[op]
+
     def apply_filter_value(self, value, exclude=False):
         assert self.pending_attr
         op = 'ne' if exclude else getattr(self, 'pending_op', 'eq')
@@ -265,23 +307,6 @@ class LazyFormDefObjectsManager:
 
         from wcs import sql
 
-        operators_mapping = {
-            'eq': Equal,
-            'ne': NotEqual,
-            'lt': Less,
-            'lte': LessOrEqual,
-            'gt': Greater,
-            'gte': GreaterOrEqual,
-        }
-        operator_names_mapping = {
-            'eq': 'equal',
-            'ne': 'not_equal',
-            'lt': 'less_than',
-            'lte': 'less_than_or_equal',
-            'gt': 'greater_than',
-            'gte': 'greater_than_or_equal',
-        }
-
         criterias = []
         for field in fields:
             field_id = sql.get_field_id(field)
@@ -303,11 +328,11 @@ class LazyFormDefObjectsManager:
                 if op not in operators:
                     get_publisher().record_error(
                         _('Invalid operator "%s" for filter "%s"')
-                        % (operator_names_mapping[op], self.pending_attr),
+                        % (self.get_operator_name(op), self.pending_attr),
                         formdata=self._formdata,
                     )
                     return self.none()
-                criteria_class = operators_mapping[op]
+                criteria_class = self.get_operator_class(op)
                 if field.type in ['string', 'item', 'items']:
                     try:
                         # cast to integer so it can be used with numerical operators
-- 
2.35.1

