From 5d21f0f1aac4aea3e63d1f802bc299d4943ef408 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Mon, 27 Dec 2021 16:21:16 +0100 Subject: [PATCH] carddef: add custom view dynamic filter on items field (#48386) --- tests/form_pages/test_all.py | 76 +++++++++++++++++ tests/form_pages/test_live.py | 148 ++++++++++++++++++++++++++++++++++ wcs/carddef.py | 44 ++++++++-- 3 files changed, 263 insertions(+), 5 deletions(-) diff --git a/tests/form_pages/test_all.py b/tests/form_pages/test_all.py index 3c37b70be..8cbb4f113 100644 --- a/tests/form_pages/test_all.py +++ b/tests/form_pages/test_all.py @@ -1,5 +1,6 @@ import hashlib import io +import itertools import json import os import re @@ -5512,6 +5513,81 @@ def test_item_field_from_custom_view_on_cards(pub): assert formdef.data_class().select()[0].data['0_structured']['item'] == 'baz' +@pytest.mark.parametrize('filter_value', ['{{ "foo,bar" }}', 'foo,bar', '{{ "foo,bar"|split:","|list }}']) +def test_items_field_from_custom_view_on_cards(pub, filter_value): + 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() + + items = ['foo', 'bar', 'baz', 'buz'] + CardDef.wipe() + carddef = CardDef() + carddef.name = 'items' + carddef.digest_templates = {'default': '{{form_var_attr}} - {{form_var_item}}'} + carddef.workflow_roles = {'_editor': user.roles[0]} + carddef.fields = [ + fields.ItemsField(id='0', type='items', label='item', varname='item', items=items), + fields.StringField(id='1', type='string', label='string', varname='attr'), + ] + carddef.store() + carddef.data_class().wipe() + foo_bar_ids = set() + for i, (v1, v2) in enumerate([(v1, v2) for (v1, v2) in itertools.product(items, items) if v1 != v2]): + carddata = carddef.data_class()() + carddata.data = { + '0': [v1, v2], + '0_display': '%s,%s' % (v1, v2), + '1': 'attr%s' % i, + } + carddata.just_created() + carddata.store() + if {v1, v2} & {'foo', 'bar'}: + foo_bar_ids.add(str(carddata.id)) + + # create custom view + app = login(get_app(pub), username='foo', password='foo') + + # we must force the ordering to have a determinist test + resp = app.get('/backoffice/data/items/?order_by=id') + assert resp.text.count(' validation page + resp = resp.form.submit('submit') # -> submit + formdata = formdef.data_class().select()[0] + assert formdata.data['0'] in foo_bar_ids + assert formdata.data['0_structured']['text'] == 'attr0 - foo,bar' + + def test_item_field_with_disabled_items(http_requests, pub): create_user(pub) formdef = create_formdef() diff --git a/tests/form_pages/test_live.py b/tests/form_pages/test_live.py index 5c1c6a81e..61348e37b 100644 --- a/tests/form_pages/test_live.py +++ b/tests/form_pages/test_live.py @@ -1,5 +1,6 @@ import datetime import io +import itertools import json from unittest import mock @@ -1321,6 +1322,153 @@ def test_dynamic_item_field_from_custom_view_on_cards(pub): assert logged_error.summary == '[DATASOURCE] Unknown custom view "as-data-source" for CardDef "items"' +def test_dynamic_items_field_from_custom_view_on_cards(pub): + if not pub.is_using_postgresql(): + pytest.skip('this requires SQL') + return + + 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.wipe() + formdef = FormDef() + formdef.name = 'test' + formdef.fields = [] + formdef.store() + formdef.data_class().wipe() + + items = ['foo', 'bar', 'baz', 'buz'] + CardDef.wipe() + carddef = CardDef() + carddef.name = 'items' + carddef.digest_templates = {'default': '{{form_var_attr}} - {{form_var_items}}'} + carddef.workflow_roles = {'_editor': user.roles[0]} + carddef.fields = [ + fields.ItemsField(id='0', type='items', label='items', varname='items', items=items), + fields.StringField(id='1', type='string', label='string', varname='attr'), + ] + carddef.store() + carddef.data_class().wipe() + foo_bar_ids = set() + for i, (v1, v2) in enumerate(itertools.product(items, items)): + if v1 == v2: + continue + carddata = carddef.data_class()() + carddata.data = { + '0': [v1, v2], + '0_display': '%s,%s' % (v1, v2), + '1': 'attr%s' % i, + } + carddata.just_created() + carddata.store() + if {v1, v2} & {'foo', 'bar'}: + foo_bar_ids.add(str(carddata.id)) + + # create custom view + app = login(get_app(pub), username='foo', password='foo') + + resp = app.get('/backoffice/data/items/?order_by=id') + assert resp.text.count(' + resp.form['f1'].options.append((str(item['id']), False, item['text'])) + + resp.form['f1'] = resp.form['f1'].options[0][0] + resp = resp.form.submit('submit') # -> validation page + assert 'Technical error' not in resp.text + resp = resp.form.submit('submit') # -> submit + assert formdef.data_class().select()[0].data['1'] in foo_bar_ids + assert formdef.data_class().select()[0].data['1_structured']['text'] == 'attr1 - foo,bar' + + # same in autocomplete mode + formdef.fields[2].display_mode = 'autocomplete' + formdef.store() + app = get_app(pub) + resp = app.get('/test/') + # simulate select2 mode, with qommon.forms.js adding an extra hidden widget + resp.form.fields['f1_display'] = Hidden(form=resp.form, tag='input', name='f1_display', pos=10) + select2_url = resp.pyquery('select:last').attr['data-select2-url'] + resp_json = app.get(select2_url + '?q=') + assert len(resp_json.json['data']) == 0 + resp.form['f0$element0'] = 'foo' + resp.form['f0$element1'] = 'bar' + + live_resp = app.post('/test/live?modified_field_id=0', params=resp.form.submit_fields()) + new_select2_url = live_resp.json['result']['1']['source_url'] + resp_json = app.get(new_select2_url + '?q=') + assert len(resp_json.json['data']) == 10 + assert {str(x['id']) for x in resp_json.json['data']} == foo_bar_ids + + resp.form['f1'].force_value(str(resp_json.json['data'][0]['id'])) + resp.form.fields['f1_display'].force_value(resp_json.json['data'][0]['text']) + + resp = resp.form.submit('submit') # -> validation page + resp = resp.form.submit('submit') # -> submit + assert formdef.data_class().select()[0].data['1'] in foo_bar_ids + assert formdef.data_class().select()[0].data['1_structured']['text'] == 'attr1 - foo,bar' + + # delete custom view + if pub.is_using_postgresql(): + pub.loggederror_class.wipe() + custom_view.remove_self() + resp = get_app(pub).get('/test/') + assert resp.form['f1'].options == [] + if pub.is_using_postgresql(): + assert pub.loggederror_class.count() == 1 + logged_error = pub.loggederror_class.select()[0] + assert logged_error.formdef_id == formdef.id + assert logged_error.summary == '[DATASOURCE] Unknown custom view "as-data-source" for CardDef "items"' + + def test_item_field_from_cards_check_lazy_live(pub): create_user(pub) diff --git a/wcs/carddef.py b/wcs/carddef.py index e0f420281..a753f32cd 100644 --- a/wcs/carddef.py +++ b/wcs/carddef.py @@ -213,9 +213,34 @@ class CardDef(FormDef): order_by = custom_view.order_by criterias.extend(custom_view.get_criterias(formdef=carddef)) for criteria in criterias: - if not Template.is_template_string(criteria.value): - continue - criteria.value = WorkflowStatusItem.compute(criteria.value) + if Template.is_template_string(criteria.value): + criteria.value = WorkflowStatusItem.compute(criteria.value) + # case of items + elif ( + criteria.__class__.__name__ == 'Intersects' + and isinstance(criteria.value, list) + and len(criteria.value) == 1 + and isinstance(criteria.value[0], str) + ): + cvalue = criteria.value[0] + + if Template.is_template_string(cvalue): + publisher = get_publisher() + with publisher.complex_data(): + cvalue_evaluated = WorkflowStatusItem.compute(cvalue, allow_complex=True) + cvalue_complex = publisher.get_cached_complex_data(cvalue_evaluated) + if isinstance(cvalue_complex, list): + cvalue = ','.join(str(item) for item in cvalue_complex) + elif isinstance(cvalue_complex, str): + # get_cached_complex_data remove Unicode + # characters from the private use area (0xE000-0xF8FF) + cvalue = cvalue_complex + else: + # cvalue_complex is something else (file, etc..) + cvalue = cvalue_evaluated + cvalue = list(cvalue.split(',')) + criteria.value = [item.strip() for item in cvalue] + if custom_view: view_digest_key = 'custom-view:%s' % custom_view.get_url_slug() if view_digest_key in (carddef.digest_templates or {}): @@ -288,9 +313,18 @@ class CardDef(FormDef): from .fields import Field for criteria in custom_view.get_criterias(formdef=carddef): - if not isinstance(criteria.value, str): + if isinstance(criteria.value, str): + cvalue = criteria.value + elif ( + criteria.__class__.__name__ == 'Intersects' + and isinstance(criteria.value, list) + and len(criteria.value) == 1 + and isinstance(criteria.value[0], str) + ): + cvalue = criteria.value[0] + else: continue - varnames.extend(Field.get_referenced_varnames(formdef, criteria.value)) + varnames.extend(Field.get_referenced_varnames(formdef, cvalue)) return varnames -- 2.34.1