From 5828cfdb798ef2a4a5681cb5534cdfd220b6fbdf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Sat, 14 May 2022 12:44:39 +0200
Subject: [PATCH 2/2] cards: add status operator support to custom card data
 sources (#65248)

---
 tests/form_pages/test_all.py | 91 ++++++++++++++++++++++++++++++++++++
 wcs/backoffice/management.py |  2 +-
 wcs/custom_views.py          | 16 +++++--
 3 files changed, 105 insertions(+), 4 deletions(-)

diff --git a/tests/form_pages/test_all.py b/tests/form_pages/test_all.py
index b2c821bac..6fd2dda72 100644
--- a/tests/form_pages/test_all.py
+++ b/tests/form_pages/test_all.py
@@ -5897,6 +5897,97 @@ def test_item_field_from_custom_view_on_cards(pub):
     assert formdef.data_class().select()[0].data['0_display'] == 'Xattr%sY' % baz_id
 
 
+def test_item_field_from_custom_view_on_cards_filter_status(pub):
+    pub.role_class.wipe()
+    pub.custom_view_class.wipe()
+
+    user = create_user(pub)
+    role = pub.role_class(name='xxx')
+    role.store()
+    user.roles = [role.id]
+    user.is_admin = True
+    user.store()
+
+    formdef = create_formdef()
+    formdef.data_class().wipe()
+
+    card_workflow = CardDef.get_default_workflow()
+    st1 = card_workflow.add_status('Status1', 'st1')
+    card_workflow.id = None
+    card_workflow.store()
+
+    CardDef.wipe()
+    carddef = CardDef()
+    carddef.workflow_id = card_workflow.id
+    carddef.name = 'items'
+    carddef.digest_templates = {'default': '{{form_var_attr}}'}
+    carddef.workflow_roles = {'_editor': user.roles[0]}
+    carddef.fields = [
+        fields.ItemField(id='0', type='item', label='item', varname='item', items=['foo', 'bar', 'baz']),
+        fields.StringField(id='1', type='string', label='string', varname='attr'),
+    ]
+    carddef.store()
+    carddef.data_class().wipe()
+    for i, value in enumerate(['foo', 'bar', 'baz']):
+        carddata = carddef.data_class()()
+        carddata.data = {
+            '0': value,
+            '0_display': value,
+            '1': 'attr%s' % (i + 1),
+        }
+        carddata.just_created()
+        carddata.store()
+
+    carddata.jump_status(st1.id)
+    carddata.store()
+
+    # create custom view
+    app = login(get_app(pub), username='foo', password='foo')
+
+    resp = app.get('/backoffice/data/items/')
+    resp.forms['listing-settings']['filter-status'].checked = True
+    resp = resp.forms['listing-settings'].submit()
+    resp.forms['listing-settings']['filter-operator'].value = 'ne'
+    resp.forms['listing-settings']['filter'].value = 'st1'
+    resp = resp.forms['listing-settings'].submit()
+    assert resp.pyquery('tbody tr').length == 2
+
+    resp.forms['save-custom-view']['title'] = 'as data source'
+    resp.forms['save-custom-view']['visibility'] = 'datasource'
+    resp = resp.forms['save-custom-view'].submit()
+
+    custom_view = pub.custom_view_class.select()[0]
+    assert custom_view.filters == {'filter-operator': 'ne', 'filter': 'st1', 'filter-status': 'on'}
+
+    # use custom view as source
+    ds = {'type': 'carddef:%s:%s' % (carddef.url_name, custom_view.slug)}
+    formdef.fields = [
+        fields.ItemField(id='0', label='string', type='item', data_source=ds, display_disabled_items=True)
+    ]
+    formdef.store()
+
+    resp = get_app(pub).get('/test/')
+    assert len(resp.form['f0'].options) == 2
+    assert {x[2] for x in resp.form['f0'].options} == {'attr1', 'attr2'}
+
+    custom_view.filters['filter-operator'] = 'eq'
+    custom_view.store()
+    resp = get_app(pub).get('/test/')
+    assert [x[2] for x in resp.form['f0'].options] == ['attr3']
+
+    custom_view.filters['filter'] = 'all'
+    custom_view.store()
+    resp = get_app(pub).get('/test/')
+    assert len(resp.form['f0'].options) == 3
+
+    custom_view.filters['filter'] = 'all'
+    custom_view.filters['filter-operator'] = 'ne'
+    custom_view.store()
+    resp = get_app(pub).get('/test/')
+    assert len(resp.form['f0'].options) == 1
+    assert [x[2] for x in resp.form['f0'].options] == ['---']
+
+
 @pytest.mark.parametrize('filter_value', ['{{ "foo" }}', 'foo'])
 def test_items_field_from_custom_view_on_cards(pub, filter_value):
     pub.role_class.wipe()
diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py
index 490453b00..7d017da16 100644
--- a/wcs/backoffice/management.py
+++ b/wcs/backoffice/management.py
@@ -1767,7 +1767,7 @@ class FormPage(Directory):
     def get_filter_operator_from_query(self):
         default_filter_operator = 'eq'
         if self.view:
-            default_filter_operator = self.view.filters.get('filter-operator', 'eq')
+            default_filter_operator = self.view.get_status_filter_operator()
         operator = get_request().form.get('filter-operator') or default_filter_operator
         if operator not in ['eq', 'ne']:
             raise RequestError('Invalid operator "%s" for "filter-operator"' % operator)
diff --git a/wcs/custom_views.py b/wcs/custom_views.py
index c1a005764..b7f07a2b0 100644
--- a/wcs/custom_views.py
+++ b/wcs/custom_views.py
@@ -23,7 +23,7 @@ from quixote import get_publisher
 from wcs.carddef import CardDef
 from wcs.formdef import FormDef
 from wcs.qommon.misc import simplify
-from wcs.qommon.storage import Contains, Equal, StorableObject
+from wcs.qommon.storage import Contains, Equal, NotContains, StorableObject
 
 from .qommon.misc import xml_node_text
 
@@ -144,6 +144,9 @@ class CustomView(StorableObject):
     def get_filter(self):
         return self.filters.get('filter')
 
+    def get_status_filter_operator(self):
+        return self.filters.get('filter-operator', 'eq')
+
     def get_filters_dict(self):
         return self.filters
 
@@ -166,7 +169,11 @@ class CustomView(StorableObject):
         )
 
         selected_filter = self.get_filter()
-        if selected_filter and selected_filter != 'all':
+        selected_status_filter_operator = self.get_status_filter_operator()
+        if selected_filter and selected_filter == 'all':
+            if selected_status_filter_operator == 'ne':
+                criterias.append(Equal('status', '_none'))
+        elif selected_filter:
             if selected_filter == 'pending':
                 applied_filters = ['wf-%s' % x.id for x in formdef.workflow.get_not_endpoint_status()]
             elif selected_filter == 'done':
@@ -174,7 +181,10 @@ class CustomView(StorableObject):
             else:
                 applied_filters = ['wf-%s' % selected_filter]
             if applied_filters:
-                criterias.append(Contains('status', applied_filters))
+                if selected_status_filter_operator == 'eq':
+                    criterias.append(Contains('status', applied_filters))
+                elif selected_status_filter_operator == 'ne':
+                    criterias.append(NotContains('status', applied_filters))
 
         return criterias
 
-- 
2.36.1

