From 833fde9df73bfcd86807357cb0e73f1f2acd9086 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Fri, 6 May 2022 10:18:08 +0200
Subject: [PATCH] backoffice: get items filter options from existing cards
 (#48881)

---
 tests/backoffice_pages/test_filters.py | 88 ++++++++++++++++++++++++++
 wcs/backoffice/management.py           |  8 +--
 wcs/fields.py                          | 48 ++++++++++----
 3 files changed, 125 insertions(+), 19 deletions(-)

diff --git a/tests/backoffice_pages/test_filters.py b/tests/backoffice_pages/test_filters.py
index 574958cb2..72079a9c2 100644
--- a/tests/backoffice_pages/test_filters.py
+++ b/tests/backoffice_pages/test_filters.py
@@ -6,6 +6,7 @@ import pytest
 
 from wcs import fields
 from wcs.blocks import BlockDef
+from wcs.carddef import CardDef
 from wcs.data_sources import NamedDataSource
 from wcs.formdef import FormDef
 from wcs.qommon.http_request import HTTPRequest
@@ -699,6 +700,93 @@ def test_backoffice_items_filter(pub):
         assert resp.text.count('data-link') == 0  # no rows
 
 
+def test_backoffice_items_cards_filter(pub):
+    pub.user_class.wipe()
+    create_superuser(pub)
+    pub.role_class.wipe()
+    role = pub.role_class(name='test')
+    role.store()
+
+    CardDef.wipe()
+    carddef = CardDef()
+    carddef.name = 'foo'
+    carddef.fields = [
+        fields.StringField(id='1', label='Test', type='string', varname='foo'),
+    ]
+    carddef.digest_templates = {'default': 'card {{ form_var_foo }}'}
+    carddef.store()
+
+    card_ids = {}
+    for label in ('foo', 'bar', 'baz'):
+        card = carddef.data_class()()
+        card.data = {'1': label}
+        card.just_created()
+        card.store()
+        card_ids[label] = str(card.id)
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'form-title'
+    formdef.fields = [
+        fields.ItemsField(
+            id='1',
+            label='items',
+            type='items',
+            data_source={'type': 'carddef:foo', 'value': ''},
+        )
+    ]
+    formdef.workflow_roles = {'_receiver': role.id}
+    formdef.store()
+
+    data_class = formdef.data_class()
+    data_class.wipe()
+
+    formdata = data_class()
+    formdata.data = {'1': [card_ids['foo']]}
+    formdata.data['1_display'] = formdef.fields[0].store_display_value(formdata.data, '1')
+    formdata.data['1_structured'] = formdef.fields[0].store_structured_value(formdata.data, '1')
+    formdata.just_created()
+    formdata.jump_status('new')
+    formdata.store()
+
+    formdata = data_class()
+    formdata.data = {'1': [card_ids['foo'], card_ids['baz']]}
+    formdata.data['1_display'] = formdef.fields[0].store_display_value(formdata.data, '1')
+    formdata.data['1_structured'] = formdef.fields[0].store_structured_value(formdata.data, '1')
+    formdata.just_created()
+    formdata.jump_status('new')
+    formdata.store()
+
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/management/%s/' % formdef.url_name)
+    assert resp.pyquery('tbody tr').length == 2
+    resp.forms['listing-settings']['filter-1'].checked = True
+    resp = resp.forms['listing-settings'].submit()
+
+    if pub.is_using_postgresql():
+        # option 'bar' is never used so not even listed
+        assert [x[2] for x in resp.forms['listing-settings']['filter-1-value'].options] == [
+            '',
+            'card baz',
+            'card foo',
+        ]
+    else:
+        assert [x[2] for x in resp.forms['listing-settings']['filter-1-value'].options] == [
+            '',
+            'card bar',
+            'card baz',
+            'card foo',
+        ]
+
+    resp.forms['listing-settings']['filter-1-value'].value = card_ids['foo']
+    resp = resp.forms['listing-settings'].submit()
+    assert resp.pyquery('tbody tr').length == 2
+
+    resp.forms['listing-settings']['filter-1-value'].value = card_ids['baz']
+    resp = resp.forms['listing-settings'].submit()
+    assert resp.pyquery('tbody tr').length == 1
+
+
 def test_backoffice_string_filter(pub):
     pub.user_class.wipe()
     create_superuser(pub)
diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py
index 9092eaafc..58685c688 100644
--- a/wcs/backoffice/management.py
+++ b/wcs/backoffice/management.py
@@ -1002,13 +1002,7 @@ class FormPage(Directory):
             )
 
         if filter_field.type == 'items':
-            # unnest key/values
-            exploded_options = {}
-            for option_keys, option_label in options:
-                if option_keys and option_label:
-                    for option_key, option_label in zip(option_keys, option_label.split(', ')):
-                        exploded_options[option_key] = option_label
-            options = list(sorted(exploded_options.items(), key=lambda x: x[1]))
+            options = list(sorted(filter_field.get_exploded_options(options), key=lambda x: x[1]))
 
         options = [(force_str(x), force_str(y)) for x, y in options if x and y]
 
diff --git a/wcs/fields.py b/wcs/fields.py
index 6b8553e87..e98703ac9 100644
--- a/wcs/fields.py
+++ b/wcs/fields.py
@@ -82,6 +82,7 @@ from .qommon.misc import (
 )
 from .qommon.ods import NS as OD_NS
 from .qommon.ods import clean_text as od_clean_text
+from .qommon.storage import Contains
 from .qommon.template import Template, TemplateError
 from .qommon.upload_storage import PicklableUpload
 from .sessions import BasicSession
@@ -1906,6 +1907,22 @@ class ItemFieldMixin:
             data_source_type.set_value(None)
             data_source_type.transfer_form_value(get_request())
 
+    def get_carddef(self):
+        from wcs.carddef import CardDef
+
+        if (
+            not self.data_source
+            or not self.data_source.get('type')
+            or not self.data_source['type'].startswith('carddef:')
+        ):
+            return None
+
+        carddef_slug = self.data_source['type'].split(':')[1]
+        try:
+            return CardDef.get_by_urlname(carddef_slug)
+        except KeyError:
+            return None
+
 
 class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin):
     key = 'item'
@@ -1980,18 +1997,6 @@ class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin):
 
         return self.display_mode
 
-    def get_carddef(self):
-        from wcs.carddef import CardDef
-
-        if not self.data_source['type'].startswith('carddef:'):
-            return None
-
-        carddef_slug = self.data_source['type'].split(':')[1]
-        try:
-            return CardDef.get_by_urlname(carddef_slug)
-        except KeyError:
-            return None
-
     def perform_more_widget_changes(self, form, kwargs, edit=True):
         data_source = data_sources.get_object(self.data_source)
         display_mode = self.get_display_mode(data_source)
@@ -2537,6 +2542,25 @@ class ItemsField(WidgetField, ItemFieldMixin):
             return value
         return []
 
+    def get_exploded_options(self, options):
+        carddef = self.get_carddef()
+        if not carddef:
+            # unnest key/values
+            exploded_options = {}
+            for option_keys, option_label in options:
+                if option_keys and option_label:
+                    for option_key, option_label in zip(option_keys, option_label.split(', ')):
+                        exploded_options[option_key] = option_label
+            return exploded_options.items()
+
+        options_ids = set()
+        for option in options:
+            options_ids.update(set(option[0]))
+
+        cards = carddef.data_class().select([Contains('id', options_ids)])
+
+        return [(str(x.id), x.get_display_label()) for x in cards]
+
 
 register_field_class(ItemsField)
 
-- 
2.36.0

