From da90b073d617fbdc3af7104a69bd276150110e5c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Fri, 13 Aug 2021 23:17:46 +0200
Subject: [PATCH] misc: allow items fields to be prefilled by template (#56159)

---
 tests/admin_pages/test_form.py |  3 +-
 tests/form_pages/test_all.py   | 73 ++++++++++++++++++++++++++++++++++
 wcs/fields.py                  | 20 +++++++---
 3 files changed, 90 insertions(+), 6 deletions(-)

diff --git a/tests/admin_pages/test_form.py b/tests/admin_pages/test_form.py
index 56b9c3b25..ae668749d 100644
--- a/tests/admin_pages/test_form.py
+++ b/tests/admin_pages/test_form.py
@@ -1909,11 +1909,12 @@ def test_form_edit_items_field(pub):
     assert FormDef.get(1).fields[0].min_choices == 2
     assert FormDef.get(1).fields[0].max_choices == 5
 
-    # check prefilling is only possible with Python
+    # check prefilling is possible with a template or a Python expression
     resp = resp.follow()
     resp = resp.click('Edit', href='1/')
     assert resp.forms[0]['prefill$type'].options == [
         ('None', True, 'None'),
+        ('String / Template', False, 'String / Template'),
         ('Python Expression', False, 'Python Expression'),
     ]
 
diff --git a/tests/form_pages/test_all.py b/tests/form_pages/test_all.py
index b559846a9..57dd72525 100644
--- a/tests/form_pages/test_all.py
+++ b/tests/form_pages/test_all.py
@@ -3180,6 +3180,79 @@ def test_form_page_formula_prefill_items_field(pub):
         assert resp.form['f0$element2'].checked
 
 
+def test_form_page_template_prefill_items_field(pub):
+    create_user(pub)
+    formdef = create_formdef()
+    formdef.data_class().wipe()
+    formdef.fields = [
+        fields.ItemsField(
+            id='0',
+            label='items',
+            items=['foo', 'bar', 'baz'],
+            prefill={'type': 'string', 'value': 'foo,baz'},
+        )
+    ]
+    formdef.store()
+
+    resp = get_app(pub).get('/test/')
+    assert resp.form['f0$element0'].checked
+    assert not resp.form['f0$element1'].checked
+    assert resp.form['f0$element2'].checked
+    # this selection will be reused in the complex data test
+    resp.form['f0$element1'].checked = True
+    resp.form['f0$element2'].checked = False
+    assert 'widget-prefilled' in resp.text
+    resp = resp.form.submit('submit')
+    resp = resp.form.submit('submit')
+    assert formdef.data_class().count() == 1
+
+    # check with remote json
+    ds = {'type': 'json', 'value': 'http://remote.example.net/json'}
+    formdef.fields = [
+        fields.ItemsField(
+            id='0',
+            label='items',
+            data_source=ds,
+            display_disabled_items=True,
+            prefill={'type': 'string', 'value': 'foo,baz'},
+        )
+    ]
+    formdef.store()
+
+    with mock.patch('wcs.qommon.misc.urlopen') as urlopen:
+        data = {
+            'data': [
+                {'id': 'foo', 'text': 'hello'},
+                {'id': 'bar', 'text': 'world'},
+                {'id': 'baz', 'text': '!'},
+            ]
+        }
+        urlopen.side_effect = lambda *args: io.StringIO(json.dumps(data))
+        resp = get_app(pub).get('/test/')
+        assert resp.form['f0$elementfoo'].checked
+        assert not resp.form['f0$elementbar'].checked
+        assert resp.form['f0$elementbaz'].checked
+
+        # check with template returning a complex data
+        formdef.fields = [
+            fields.ItemsField(
+                id='0',
+                varname='items',
+                label='items',
+                data_source=ds,
+                display_disabled_items=True,
+                prefill={'type': 'string', 'value': '{{form_objects|first|get:"form_var_items_raw"}}'},
+            )
+        ]
+        formdef.store()
+
+        # it will use foo,bar as selected in the first part of this test
+        resp = get_app(pub).get('/test/')
+        assert resp.form['f0$elementfoo'].checked
+        assert resp.form['f0$elementbar'].checked
+        assert not resp.form['f0$elementbaz'].checked
+
+
 def test_form_page_changing_prefill(pub):
     formdef = create_formdef()
     formdef.data_class().wipe()
diff --git a/wcs/fields.py b/wcs/fields.py
index d48907a4c..44259d1d8 100644
--- a/wcs/fields.py
+++ b/wcs/fields.py
@@ -106,9 +106,9 @@ class PrefillSelectionWidget(CompositeWidget):
         ]
 
         if field and field.type == 'items':
-            # limit choices to python as items field are prefilled with list
-            # of strings
-            options = [x for x in options if x[0] in ('none', 'formula')]
+            # limit choices strings (must be templates giving complex data) or
+            # python; items field are prefilled with list of strings
+            options = [x for x in options if x[0] in ('none', 'string', 'formula')]
         elif field and field.type == 'map':
             # limit choices to geolocation
             options = [x for x in options if x[0] in ('none', 'string', 'geolocation')]
@@ -463,9 +463,19 @@ class Field:
             if not Template.is_template_string(value):
                 return (value, explicit_lock)
 
-            context = get_publisher().substitutions.get_context_variables(mode='lazy')
+            from wcs.workflows import WorkflowStatusItem
+
             try:
-                return (Template(value, autoescape=False, raises=True).render(context), explicit_lock)
+                with get_publisher().complex_data():
+                    v = WorkflowStatusItem.compute(
+                        value,
+                        raises=True,
+                        allow_complex=self.allow_complex and not force_string,
+                        record_errors=False,
+                    )
+                    if v and self.allow_complex:
+                        v = get_publisher().get_cached_complex_data(v)
+                return (v, explicit_lock)
             except TemplateError:
                 return ('', explicit_lock)
             except AttributeError as e:
-- 
2.33.0

