From cb336042e93f9f690d54f3cea1a570370dc2c6d0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Mon, 22 Feb 2021 15:50:20 +0100
Subject: [PATCH 1/2] forms: recompute prefills on live changes (#51330)

---
 tests/backoffice_pages/test_submission.py | 41 ++++++++++++++++++---
 tests/form_pages/test_all.py              | 44 +++++++++++++++++++++--
 wcs/formdef.py                            |  2 +-
 wcs/forms/common.py                       | 23 ++++++++----
 wcs/forms/root.py                         |  8 +++--
 wcs/qommon/static/js/qommon.admin.js      |  4 +++
 wcs/qommon/static/js/qommon.forms.js      | 10 +++++-
 7 files changed, 114 insertions(+), 18 deletions(-)

diff --git a/tests/backoffice_pages/test_submission.py b/tests/backoffice_pages/test_submission.py
index ff53445c4..82adfa036 100644
--- a/tests/backoffice_pages/test_submission.py
+++ b/tests/backoffice_pages/test_submission.py
@@ -1569,10 +1569,6 @@ def test_backoffice_submission_user_selection_then_live(pub):
     assert resp.form['f3'].value == random_user.email
     assert resp.form['f4'].value == random_user.email
 
-    live_url = resp.html.find('form').attrs['data-live-url']
-    live_resp = app.post(live_url, params=resp.form.submit_fields())
-    assert live_resp.json['result']['4']['content'] == random_user.email
-
     resp.form['f4'] = 'altered value'  # alter value
 
     resp = resp.form.submit('submit')  # -> validation page
@@ -1580,6 +1576,43 @@ def test_backoffice_submission_user_selection_then_live(pub):
     assert 'altered value' not in resp
 
 
+def test_backoffice_submission_user_selection_then_live_prefill(pub):
+    user = create_user(pub)
+
+    for i in range(10):
+        random_user = pub.user_class()
+        random_user.name = 'random user %s' % i
+        random_user.email = 'test%s@invalid' % i
+        random_user.store()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'form title'
+    formdef.fields = [
+        fields.PageField(id='0', label='1st page', type='page'),
+        fields.StringField(
+            id='3',
+            label='Field on 2nd page',
+            type='string',
+            varname='plop',
+            prefill={'type': 'user', 'value': 'email'},
+        ),
+    ]
+    formdef.backoffice_submission_roles = user.roles[:]
+    formdef.workflow_roles = {'_receiver': 1}
+    formdef.store()
+
+    app = login(get_app(pub))
+    url = '/backoffice/submission/%s/' % formdef.url_name
+    resp = app.get(url)
+
+    live_url = resp.html.find('form').attrs['data-live-url']
+    assert resp.pyquery('.submit-user-selection')
+    resp.form['user_id'] = str(random_user.id)  # happens via javascript
+    live_resp = app.post(live_url + '?modified_field_id=user', params=resp.form.submit_fields())
+    assert live_resp.json == {'result': {'3': {'visible': True, 'content': 'test9@invalid'}}}
+
+
 def test_backoffice_submission_sidebar_lateral_block(pub):
     user = create_user(pub)
 
diff --git a/tests/form_pages/test_all.py b/tests/form_pages/test_all.py
index 16ff2b6d4..c79fe87ec 100644
--- a/tests/form_pages/test_all.py
+++ b/tests/form_pages/test_all.py
@@ -2936,7 +2936,8 @@ def test_form_page_template_prefill(pub):
 
     resp = app.get('/test/')
     assert resp.form['f0'].value == ''
-    assert 'widget-prefilled' not in resp.text
+    # still marked with a css class, in case of live changes.
+    assert 'widget-prefilled' in resp.text
 
 
 def test_form_page_session_var_prefill(pub):
@@ -7436,6 +7437,43 @@ def test_field_live_comment_content_from_structured_item_data(pub, http_requests
     assert live_resp.json['result']['7']['content'] == '<p>bla bar bla</p>'
 
 
+def test_field_live_string_prefill(pub, http_requests):
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'Foo'
+    formdef.fields = [
+        fields.StringField(type='string', id='1', label='Bar', size='40', required=True, varname='bar'),
+        fields.StringField(
+            type='string',
+            id='2',
+            label='Bar2',
+            size='40',
+            required=True,
+            varname='bar2',
+            prefill={'type': 'string', 'value': '{{form_var_bar|default:""}}'},
+        ),
+    ]
+    formdef.store()
+    formdef.data_class().wipe()
+
+    app = get_app(pub)
+    resp = app.get('/foo/')
+    assert resp.html.find('div', {'data-field-id': '1'}).attrs['data-live-source'] == 'true'
+    assert resp.pyquery('#var_bar2.widget-prefilled')  # second field is marked as prefilled
+    assert resp.form['f2'].value == ''
+    resp.form['f1'] = 'hello'
+    live_resp = app.post('/foo/live?modified_field_id=1&prefilled_2=on', params=resp.form.submit_fields())
+    assert live_resp.json['result']['2'] == {'visible': True, 'content': 'hello'}
+
+    resp.form['f2'] = 'other'  # manually changed -> widget-prefilled class will be removed
+    resp.form['f1'] = 'xxx'
+    live_resp = app.post('/foo/live?modified_field_id=1', params=resp.form.submit_fields())
+    assert live_resp.json['result']['2'] == {'visible': True}
+
+    # check it's not possible to declare user change from frontoffice
+    live_resp = app.post('/foo/live?modified_field_id=user', params=resp.form.submit_fields(), status=403)
+
+
 def test_form_edit_and_backoffice_field_change(pub):
     create_user(pub)
 
@@ -8394,10 +8432,10 @@ def test_field_live_locked_prefilled_field(pub, http_requests):
     assert 'f1' in resp.form.fields
     assert resp.html.find('div', {'data-field-id': '1'}).attrs['data-live-source'] == 'true'
     resp.form['f1'] = 'hello'
-    live_resp = app.post('/foo/live', params=resp.form.submit_fields())
+    live_resp = app.post('/foo/live?prefilled_2=on', params=resp.form.submit_fields())
     assert live_resp.json['result']['2']['content'] == 'bla hello bla'
     resp.form['f1'] = 'toto'
-    live_resp = app.post('/foo/live?modified_field_id=1', params=resp.form.submit_fields())
+    live_resp = app.post('/foo/live?modified_field_id=1&prefilled_2=on', params=resp.form.submit_fields())
     assert live_resp.json['result']['2']['content'] == 'bla toto bla'
 
 
diff --git a/wcs/formdef.py b/wcs/formdef.py
index c339b5beb..b8129b498 100644
--- a/wcs/formdef.py
+++ b/wcs/formdef.py
@@ -741,7 +741,7 @@ class FormDef(StorableObject):
                     if not varname in live_condition_fields:
                         live_condition_fields[varname] = []
                     live_condition_fields[varname].append(field)
-            if field.prefill and field.prefill.get('locked') and field.prefill.get('type') == 'string':
+            if field.prefill and field.prefill.get('type') == 'string':
                 for varname in field.get_referenced_varnames(
                     formdef=self, value=field.prefill.get('value', '')
                 ):
diff --git a/wcs/forms/common.py b/wcs/forms/common.py
index 98ecaf383..5509b80e8 100644
--- a/wcs/forms/common.py
+++ b/wcs/forms/common.py
@@ -706,6 +706,12 @@ class FormStatusPage(Directory, FormTemplateMixin):
             for field in displayed_fields:
                 if field.key == 'item' and field.display_mode == 'list' and field.varname:
                     modified_field_varnames.add(field.varname)
+        elif get_request().form.get('modified_field_id') == 'user' and get_request().is_in_frontoffice():
+            # not allowed in frontoffice.
+            raise errors.AccessForbiddenError()
+        elif get_request().form.get('modified_field_id') == 'user':
+            # user selection in sidebar
+            formdata.user_id = get_request().form.get('user_id')
         elif get_request().form.get('modified_field_id'):
             for field in displayed_fields:
                 if field.id == get_request().form.get('modified_field_id'):
@@ -737,13 +743,16 @@ class FormStatusPage(Directory, FormTemplateMixin):
                 continue
             if widget.field.key == 'comment':
                 result[widget.field.id]['content'] = widget.content
-            elif (
-                widget.field.prefill
-                and widget.field.prefill.get('locked')
-                and widget.field.prefill.get('type') == 'string'
-            ):
-                value, locked = widget.field.get_prefill_value()
-                result[widget.field.id]['content'] = value
+            elif widget.field.prefill and widget.field.prefill.get('type') == 'string':
+                update_prefill = bool('prefilled_%s' % widget.field.id in get_request().form)
+                if update_prefill:
+                    value, locked = widget.field.get_prefill_value()
+                    result[widget.field.id]['content'] = value
+            elif widget.field.prefill and widget.field.prefill.get('type') == 'user':
+                update_prefill = bool(get_request().form.get('modified_field_id') == 'user')
+                if update_prefill:
+                    value, locked = widget.field.get_prefill_value(user=formdata.user)
+                    result[widget.field.id]['content'] = value
 
         return json.dumps({'result': result})
 
diff --git a/wcs/forms/root.py b/wcs/forms/root.py
index 0d9eb6f52..dcb182ded 100644
--- a/wcs/forms/root.py
+++ b/wcs/forms/root.py
@@ -416,8 +416,12 @@ class FormPage(Directory, FormTemplateMixin):
                     # turn off prefilling from geolocation attributes if
                     # the form is filled from the backoffice
                     v = None
-                if v:
-                    prefilled = True
+                else:
+                    if v:
+                        prefilled = True
+                    # always mark widget as prefilled, even for empty content,
+                    # this will add a widget-prefilled CSS class that will be
+                    # used for live prefill changes.
                     widget.prefilled = True
 
             if not prefilled and widget:
diff --git a/wcs/qommon/static/js/qommon.admin.js b/wcs/qommon/static/js/qommon.admin.js
index 771df409b..15c29df85 100644
--- a/wcs/qommon/static/js/qommon.admin.js
+++ b/wcs/qommon/static/js/qommon.admin.js
@@ -163,6 +163,10 @@ $(function() {
       $('div.submit-user-selection select').select2(select2_options);
       $('div.submit-user-selection').show().find('select').on('change', function() {
         $('input[type=hidden][name=user_id]').val($(this).val());
+        $('form[data-live-url]').trigger(
+            'wcs:change',
+            {modified_field: 'user', selected_user_id: $(this).val()}
+        );
       });
     }
 
diff --git a/wcs/qommon/static/js/qommon.forms.js b/wcs/qommon/static/js/qommon.forms.js
index 17444b3fd..31facb6a6 100644
--- a/wcs/qommon/static/js/qommon.forms.js
+++ b/wcs/qommon/static/js/qommon.forms.js
@@ -291,6 +291,9 @@ $(function() {
     if (data && data.modified_field) {
       new_data += '&modified_field_id=' + data.modified_field;
     }
+    $('.widget-prefilled').each(function(idx, elem) {
+      new_data += '&prefilled_' + $(elem).data('field-id') + '=true';
+    });
     var live_url = $(this).data('live-url');
     live_evaluation = $.ajax({
       type: 'POST',
@@ -355,7 +358,9 @@ $(function() {
               $widget.html(value.content);
             } else {
               // replace text input value
-              $widget.find('input, textarea').val(value.content);
+              if ($widget.is('.widget-prefilled') || $widget.is('.widget-readonly') || data.modified_field == 'user') {
+                $widget.find('input, textarea').val(value.content);
+              }
             }
           }
           if (value.source_url) {
@@ -375,6 +380,9 @@ $(function() {
     });
   }
   $('form div[data-live-source]').parents('form').trigger('wcs:change', {modified_field: 'init'});
+  $('div.widget-prefilled').on('change input paste', function(ev) {
+    $(this).removeClass('widget-prefilled');
+  });
 
   /* searchable select */
   $('select[data-autocomplete]').each(function(i, elem) {
-- 
2.30.1

