From 067bdc52f9eec231cda904360b6c366ba2412b52 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Mon, 24 Aug 2020 14:27:35 +0200
Subject: [PATCH 2/2] general: expose custom views as data sources (#44155)

---
 tests/test_admin_pages.py           | 36 ++++++++++++++
 tests/test_backoffice_pages.py      |  3 ++
 tests/test_form_pages.py            | 74 +++++++++++++++++++++++++++++
 wcs/backoffice/management.py        | 53 ++++++++++++++-------
 wcs/carddef.py                      | 36 ++++++++++++--
 wcs/qommon/static/css/dc2/admin.css |  4 ++
 6 files changed, 185 insertions(+), 21 deletions(-)

diff --git a/tests/test_admin_pages.py b/tests/test_admin_pages.py
index 43d41efe0..20a1674b8 100644
--- a/tests/test_admin_pages.py
+++ b/tests/test_admin_pages.py
@@ -3681,3 +3681,39 @@ def test_block_use_in_formdef(pub, blocks_feature):
     assert 'a block field' in resp.text
     resp = resp.click('Edit', href='1/')
     assert resp.form['max_items'].value == '1'
+
+
+def test_card_custom_view_data_source(pub, studio):
+    from test_backoffice_pages import create_environment, test_carddata_custom_view, create_superuser
+    create_environment(pub)
+    test_carddata_custom_view(pub, studio)
+
+    carddef = CardDef.select()[0]
+    carddef.digest_template = '{{ form_var_foo }}'
+    carddef.store()
+
+    create_superuser(pub)
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/data/foo/user-card-view/')
+    resp.forms['listing-settings']['filter-1'].checked = True
+    resp = resp.forms['listing-settings'].submit()
+    resp.forms['listing-settings']['filter-1-value'] = 'FOO 0'
+    resp = resp.forms['listing-settings'].submit()
+    resp.forms['save-custom-view']['visibility'] = 'datasource'
+    resp = resp.forms['save-custom-view'].submit()
+    assert pub.custom_view_class.count() == 1
+    assert pub.custom_view_class.select()[0].visibility == 'datasource'
+
+    formdef = FormDef.get_by_urlname('form-title')
+    resp = app.get('/backoffice/forms/%s/fields/' % formdef.id)
+
+    resp.forms[0]['label'] = 'foobar'
+    resp.forms[0]['type'] = 'item'
+    resp = resp.forms[0].submit()
+    resp = resp.follow()
+    formdef = FormDef.get_by_urlname('form-title')
+    resp = resp.click(href='%s/' % formdef.fields[-1].id, index=0)
+    assert 'carddef:foo' in [x[0] for x in resp.form['data_source$type'].options]
+    assert 'carddef:foo:card-view' in [x[0] for x in resp.form['data_source$type'].options]
+    assert len(CardDef.get_data_source_items('carddef:foo')) == 50
+    assert len(CardDef.get_data_source_items('carddef:foo:card-view')) == 1
diff --git a/tests/test_backoffice_pages.py b/tests/test_backoffice_pages.py
index 382a4e3da..32ba7c642 100644
--- a/tests/test_backoffice_pages.py
+++ b/tests/test_backoffice_pages.py
@@ -6228,6 +6228,7 @@ def test_studio_card_item_link(pub, studio):
 
     card = carddef.data_class()()
     card.data = {'1': 'plop'}
+    card.just_created()
     card.store()
 
     carddef2 = CardDef()
@@ -7228,10 +7229,12 @@ def test_block_card_item_link(pub, studio, blocks_feature):
 
     card = carddef.data_class()()
     card.data = {'1': 'plop'}
+    card.just_created()
     card.store()
 
     card2 = carddef.data_class()()
     card2.data = {'1': 'plop2'}
+    card2.just_created()
     card2.store()
 
     BlockDef.wipe()
diff --git a/tests/test_form_pages.py b/tests/test_form_pages.py
index 43f35d52c..b253b80cb 100644
--- a/tests/test_form_pages.py
+++ b/tests/test_form_pages.py
@@ -5598,6 +5598,80 @@ def test_item_field_from_cards(pub):
     assert formdef.data_class().select()[0].data['0_structured']['name'] == 'bar'
 
 
+def test_item_field_from_custom_view_on_cards(pub):
+    Role.wipe()
+    pub.custom_view_class.wipe()
+
+    user = create_user(pub)
+    role = Role(name='xxx')
+    role.store()
+    user.roles = [role.id]
+    user.is_admin = True
+    user.store()
+
+    formdef = create_formdef()
+    formdef.data_class().wipe()
+
+    CardDef.wipe()
+    carddef = CardDef()
+    carddef.name = 'items'
+    carddef.digest_template = '{{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()
+    baz_ids = set()
+    for i, value in enumerate(['foo', 'bar', 'baz'] * 10):
+        carddata = carddef.data_class()()
+        carddata.data = {
+            '0': value,
+            '0_display': value,
+            '1': 'attr%s' % i,
+        }
+        carddata.just_created()
+        carddata.store()
+        if value == 'baz':
+            baz_ids.add(str(carddata.id))
+
+    # create custom view
+    app = login(get_app(pub), username='foo', password='foo')
+
+    resp = app.get('/backoffice/data/items/')
+    if pub.is_using_postgresql():
+        assert resp.text.count('<tr') == 21  # thead + 20 items (max per page)
+    else:
+        assert resp.text.count('<tr') == 31  # thead + all items
+    resp.forms['listing-settings']['filter-0'].checked = True
+    resp = resp.forms['listing-settings'].submit()
+
+    resp.forms['listing-settings']['filter-0-value'] = 'baz'
+    resp = resp.forms['listing-settings'].submit()
+    assert resp.text.count('<tr') == 11  # thead + 10 items
+
+    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]
+
+    # 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) == 10
+    assert set([x[0] for x in resp.form['f0'].options]) == baz_ids
+    resp = resp.form.submit('submit')  # -> validation page
+    resp = resp.form.submit('submit')  # -> submit
+    assert formdef.data_class().select()[0].data['0'] in baz_ids
+    assert formdef.data_class().select()[0].data['0_structured']['item'] == 'baz'
+
+
 def test_item_field_with_disabled_items(http_requests, pub):
     user = create_user(pub)
     formdef = create_formdef()
diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py
index 19b5aaaeb..03adcd273 100644
--- a/wcs/backoffice/management.py
+++ b/wcs/backoffice/management.py
@@ -1030,18 +1030,20 @@ class FormPage(Directory):
     view = None
     admin_permission = 'forms'
 
-    def __init__(self, component=None, formdef=None, view=None):
+    def __init__(self, component=None, formdef=None, view=None, update_breadcrumbs=True):
         self.view_type = None
         if component:
             try:
                 self.formdef = FormDef.get_by_urlname(component)
             except KeyError:
                 raise errors.TraversalError()
-            get_response().breadcrumb.append((component + '/', self.formdef.name))
+            if update_breadcrumbs:
+                get_response().breadcrumb.append((component + '/', self.formdef.name))
         else:
             self.formdef = formdef
             self.view = view
-            get_response().breadcrumb.append((view.slug + '/', view.title))
+            if update_breadcrumbs:
+                get_response().breadcrumb.append((view.slug + '/', view.title))
 
     def check_access(self, api_name=None):
         session = get_session()
@@ -1092,9 +1094,12 @@ class FormPage(Directory):
                 if self.view:
                     active = bool(self.view.get_url_slug() == view.get_url_slug())
                     r += htmltext('<li class="active">' if active else '<li>')
-                    r += htmltext('<a href="../%s/%s">%s</a></li>') % (view.get_url_slug(), view_type, view.title)
+                    r += htmltext('<a href="../%s/%s">%s</a>') % (view.get_url_slug(), view_type, view.title)
                 else:
-                    r += htmltext('<li><a href="%s/%s">%s</a></li>') % (view.get_url_slug(), view_type, view.title)
+                    r += htmltext('<li><a href="%s/%s">%s</a>') % (view.get_url_slug(), view_type, view.title)
+                if view.visibility == 'datasource':
+                    r += htmltext(' <span class="as-data-source">(%s)</span>') % _('for data sources')
+                r += htmltext('</li>')
             r += htmltext('</ul>')
         return r.getvalue()
 
@@ -1464,12 +1469,17 @@ class FormPage(Directory):
                 value=self.view.title if self.view else None)
         if get_publisher().get_backoffice_root().is_accessible(self.admin_permission):
             # admins can create views accessible to everyone
+            options = [
+                ('owner', _('to me only'), 'owner'),
+                ('any', _('to any users'), 'any'),
+            ]
+
+            if isinstance(self.formdef, CardDef) and self.formdef.digest_template:
+                options.append(('datasource', _('as data source'), 'datasource'))
+
             form.add(RadiobuttonsWidget, 'visibility', title=_('Visibility'),
                     value=self.view.visibility if self.view else 'owner',
-                    options=[
-                        ('owner', _('to me only'), 'owner'),
-                        ('any', _('to any users'), 'any')
-                    ])
+                    options=options)
         if self.view and (self.view.user_id == get_request().user.id or
                           get_publisher().get_backoffice_root().is_accessible(self.admin_permission)):
             form.add(CheckboxWidget, 'update', title=_('Update existing view settings'), value=True)
@@ -1611,6 +1621,10 @@ class FormPage(Directory):
         return 'all'
 
     def get_criterias_from_query(self):
+        query_overrides = get_request().form
+        return self.get_view_criterias(query_overrides, request=get_request())
+
+    def get_view_criterias(self, query_overrides=None, request=None):
         fake_fields = [
             FakeField('start', 'period-date', _('Start')),
             FakeField('end', 'period-date', _('End')),
@@ -1623,8 +1637,13 @@ class FormPage(Directory):
 
         filters_dict = {}
         if self.view:
-            filters_dict.update(self.view.get_filters_dict())
-        filters_dict.update(get_request().form)
+            filters_dict.update(self.view.get_filters_dict() or {})
+        filters_dict.update(query_overrides or {})
+
+        if request and request.form:
+            request_form = request.form
+        else:
+            request_form = {}
 
         for filter_field in fake_fields + list(self.get_formdef_fields()):
             if filter_field.type not in self.get_filterable_field_types():
@@ -1643,26 +1662,26 @@ class FormPage(Directory):
                 name_id = filters_dict.get('filter-user-uuid')
                 if name_id:
                     nameid_users = get_publisher().user_class.get_users_with_name_identifier(name_id)
-                    get_request().form['filter-user'] = filters_dict['filter-user'] = 'on'
+                    request_form['filter-user'] = filters_dict['filter-user'] = 'on'
                     if nameid_users:
                         filters_dict['filter-user-value'] = str(nameid_users[0].id)
-                        get_request().form['filter-user-value'] = filters_dict['filter-user-value']
+                        request_form['filter-user-value'] = filters_dict['filter-user-value']
                     else:
                         filters_dict['filter-user-value'] = '-1'
-                        get_request().form['filter-user-value'] = '-1'
+                        request_form['filter-user-value'] = '-1'
 
             if filter_field.type == 'submission-agent-id':
                 # convert uuid based filter into local id filter
                 name_id = filters_dict.get('filter-submission-agent-uuid')
                 if name_id:
                     nameid_users = get_publisher().user_class.get_users_with_name_identifier(name_id)
-                    get_request().form['filter-submission-agent'] = filters_dict['filter-submission-agent'] = 'on'
+                    request_form['filter-submission-agent'] = filters_dict['filter-submission-agent'] = 'on'
                     if nameid_users:
                         filters_dict['filter-submission-agent-value'] = str(nameid_users[0].id)
-                        get_request().form['filter-submission-agent-value'] = filters_dict['filter-submission-agent-value']
+                        request_form['filter-submission-agent-value'] = filters_dict['filter-submission-agent-value']
                     else:
                         filters_dict['filter-submission-agent-value'] = '-1'
-                        get_request().form['filter-submission-agent-value'] = '-1'
+                        request_form['filter-submission-agent-value'] = '-1'
 
             if filters_dict.get('filter-%s' % filter_field.id):
                 # if there's a filter-%(id)s, it is used to enable the actual
diff --git a/wcs/carddef.py b/wcs/carddef.py
index 532c2e111..3684839c4 100644
--- a/wcs/carddef.py
+++ b/wcs/carddef.py
@@ -19,6 +19,7 @@ import types
 
 from quixote import get_publisher
 from .qommon import _, N_, misc
+from .qommon.storage import Equal, NotEqual
 
 from wcs.carddata import CardData
 from wcs.formdef import FormDef
@@ -137,21 +138,48 @@ class CardDef(FormDef):
 
     @classmethod
     def get_as_data_source_options(cls):
+        carddefs = {}
         for carddef in cls.select(lightweight=True, ignore_errors=True, order_by='name'):
             if not carddef.digest_template:
                 continue
             data_source_id = 'carddef:%s' % carddef.url_name
+            carddefs[carddef.id] = {'url_name': carddef.url_name, 'name': carddef.name}
             yield (data_source_id, carddef.name, data_source_id)
+        clauses = [Equal('formdef_type', 'carddef'), Equal('visibility', 'datasource')]
+        for custom_view in get_publisher().custom_view_class.select(clauses):
+            carddef = carddefs.get(custom_view.formdef_id)
+            data_source_id = 'carddef:%s:%s' % (carddef['url_name'], custom_view.slug)
+            yield (data_source_id, '%s - %s' % (carddef['name'], custom_view.title), data_source_id)
 
     @classmethod
     def get_data_source_items(cls, data_source_id):
         assert data_source_id.startswith('carddef:')
+        parts = data_source_id.split(':')
         try:
-            carddef = cls.get_by_urlname(data_source_id[8:])
+            carddef = cls.get_by_urlname(parts[1])
         except KeyError:
             return []
+        criterias = [NotEqual('status', 'draft')]
+        order_by = None
+        if len(parts) > 2:
+            lookup_criterias = [
+                Equal('formdef_type', 'carddef'),
+                Equal('visibility', 'datasource'),
+                Equal('slug', parts[2]),
+            ]
+            try:
+                custom_view = get_publisher().custom_view_class.select(lookup_criterias)[0]
+            except IndexError:
+                return []
+            from wcs.backoffice.management import FormPage
+            form_page = FormPage(formdef=carddef, view=custom_view, update_breadcrumbs=False)
+            criterias.extend(form_page.get_view_criterias())
+            order_by = custom_view.order_by
+
         items = [x.get_data_source_structured_item()
-                 for x in carddef.data_class().select()
-                 if not x.is_draft()]
-        items.sort(key=lambda x: misc.simplify(x['text']))
+                 for x in carddef.data_class().select(
+                     clause=criterias,
+                     order_by=order_by)]
+        if order_by is None:
+            items.sort(key=lambda x: misc.simplify(x['text']))
         return items
diff --git a/wcs/qommon/static/css/dc2/admin.css b/wcs/qommon/static/css/dc2/admin.css
index 84a74e27b..9ff177815 100644
--- a/wcs/qommon/static/css/dc2/admin.css
+++ b/wcs/qommon/static/css/dc2/admin.css
@@ -1937,3 +1937,7 @@ div.mail-body {
 #sidebar-custom-views .active {
 	font-weight: bold;
 }
+
+#sidebar-custom-views .as-data-source {
+	font-weight: normal;
+}
-- 
2.28.0

