Projet

Général

Profil

0001-forms-add-option-for-prefilling-fields-as-readonly-3.patch

Frédéric Péters, 25 janvier 2020 15:54

Télécharger (20,9 ko)

Voir les différences:

Subject: [PATCH] forms: add option for prefilling fields as readonly (#39167)

 tests/test_admin_pages.py            | 10 +--
 tests/test_form_pages.py             | 92 +++++++++++++++++++++-------
 tests/test_formdef_import.py         | 29 +++++++++
 wcs/fields.py                        | 40 +++++++++---
 wcs/formdef.py                       |  5 ++
 wcs/forms/common.py                  |  3 +
 wcs/forms/root.py                    | 40 ++++++------
 wcs/qommon/form.py                   |  6 +-
 wcs/qommon/static/js/qommon.forms.js |  9 ++-
 9 files changed, 180 insertions(+), 54 deletions(-)
tests/test_admin_pages.py
1312 1312
    assert resp.location == 'http://example.net/backoffice/forms/1/fields/#itemId_1'
1313 1313
    resp = resp.follow()
1314 1314

  
1315
    assert FormDef.get(formdef.id).fields[0].prefill == {'type': 'string', 'value': 'test'}
1315
    assert FormDef.get(formdef.id).fields[0].prefill == {'type': 'string', 'value': 'test', 'readonly': False}
1316 1316

  
1317 1317
    # do the same with 'data sources' field
1318 1318
    resp = resp.click('Edit', href='1/')
......
1351 1351
    resp.form['prefill$type'] = 'String / Template'
1352 1352
    resp.form['prefill$value_string'] = 'test'
1353 1353
    resp = resp.form.submit('submit').follow()
1354
    assert FormDef.get(formdef.id).fields[0].prefill == {'type': 'string', 'value': 'test'}
1354
    assert FormDef.get(formdef.id).fields[0].prefill == {'type': 'string', 'value': 'test', 'readonly': False}
1355 1355

  
1356 1356
    resp = app.get('/backoffice/forms/1/fields/1/')
1357 1357
    resp.form['prefill$type'] = 'Python Expression'
1358 1358
    resp.form['prefill$value_formula'] = 'True'
1359 1359
    resp = resp.form.submit('submit').follow()
1360
    assert FormDef.get(formdef.id).fields[0].prefill == {'type': 'formula', 'value': 'True'}
1360
    assert FormDef.get(formdef.id).fields[0].prefill == {'type': 'formula', 'value': 'True', 'readonly': False}
1361 1361

  
1362 1362
    resp = app.get('/backoffice/forms/1/fields/1/')
1363 1363
    resp.form['prefill$type'] = 'String / Template'
1364 1364
    resp.form['prefill$value_string'] = '{{form_var_toto}}'
1365 1365
    resp = resp.form.submit('submit').follow()
1366
    assert FormDef.get(formdef.id).fields[0].prefill == {'type': 'string', 'value': '{{form_var_toto}}'}
1366
    assert FormDef.get(formdef.id).fields[0].prefill == {'type': 'string', 'value': '{{form_var_toto}}', 'readonly': False}
1367 1367

  
1368 1368
    # check error handling
1369 1369
    resp = app.get('/backoffice/forms/1/fields/1/')
......
1757 1757
    resp.form['prefill$value_geolocation'].value = 'Position'
1758 1758
    resp = resp.form.submit('submit')
1759 1759
    assert FormDef.get(formdef.id).fields[0].prefill == {
1760
            'type': 'geolocation', 'value': 'position'}
1760
            'type': 'geolocation', 'value': 'position', 'readonly': False}
1761 1761

  
1762 1762

  
1763 1763
def test_form_edit_field_warnings(pub):
tests/test_form_pages.py
5084 5084
    user.verified_fields = ['email']
5085 5085
    user.store()
5086 5086

  
5087
    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
5088
    assert resp.form['f0'].value == 'foo@localhost'
5089
    assert 'readonly' in resp.form['f0'].attrs
5087
    for prefill_settings in (
5088
            {'type': 'user', 'value': 'email'},  # verified profile
5089
            {'type': 'string', 'value': 'foo@localhost', 'readonly': True},  # readonly value
5090
        ):
5091
        formdef.confirmation = True
5092
        formdef.fields[0].prefill = prefill_settings
5093
        formdef.store()
5094
        formdef.data_class().wipe()
5095
        resp = login(get_app(pub), username='foo', password='foo').get('/test/')
5096
        assert resp.form['f0'].value == 'foo@localhost'
5097
        assert 'readonly' in resp.form['f0'].attrs
5090 5098

  
5091
    resp.form['f0'].value = 'Hello' # try changing the value
5092
    resp = resp.form.submit('submit')
5093
    assert 'Check values then click submit.' in resp.text
5094
    assert resp.form['f0'].value == 'foo@localhost'  # it is reverted
5099
        resp.form['f0'].value = 'Hello' # try changing the value
5100
        resp = resp.form.submit('submit')
5101
        assert 'Check values then click submit.' in resp.text
5102
        assert resp.form['f0'].value == 'foo@localhost'  # it is reverted
5095 5103

  
5096
    resp.form['f0'].value = 'Hello' # try again changing the value
5097
    resp = resp.form.submit('submit')
5104
        resp.form['f0'].value = 'Hello' # try again changing the value
5105
        resp = resp.form.submit('submit')
5098 5106

  
5099
    formdatas = [x for x in formdef.data_class().select() if not x.is_draft()]
5100
    assert len(formdatas) == 1
5101
    assert formdatas[0].data['0'] == 'foo@localhost'
5107
        formdatas = [x for x in formdef.data_class().select() if not x.is_draft()]
5108
        assert len(formdatas) == 1
5109
        assert formdatas[0].data['0'] == 'foo@localhost'
5102 5110

  
5103
    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
5104
    assert resp.form['f0'].value == 'foo@localhost'
5105
    resp = resp.form.submit('submit')
5106
    assert 'Check values then click submit.' in resp.text
5107
    resp.form['f0'].value = 'Hello' # try changing
5108
    resp = resp.form.submit('previous')
5109
    assert 'readonly' in resp.form['f0'].attrs
5110
    assert not 'Check values then click submit.' in resp.text
5111
    assert resp.form['f0'].value == 'foo@localhost'
5111
        resp = login(get_app(pub), username='foo', password='foo').get('/test/')
5112
        assert resp.form['f0'].value == 'foo@localhost'
5113
        resp = resp.form.submit('submit')
5114
        assert 'Check values then click submit.' in resp.text
5115
        resp.form['f0'].value = 'Hello' # try changing
5116
        resp = resp.form.submit('previous')
5117
        assert 'readonly' in resp.form['f0'].attrs
5118
        assert not 'Check values then click submit.' in resp.text
5119
        assert resp.form['f0'].value == 'foo@localhost'
5120

  
5121
        # try it without validation page
5122
        formdef.confirmation = False
5123
        formdef.store()
5124
        formdef.data_class().wipe()
5125

  
5126
        resp = login(get_app(pub), username='foo', password='foo').get('/test/')
5127
        assert resp.form['f0'].value == 'foo@localhost'
5128
        assert 'readonly' in resp.form['f0'].attrs
5129

  
5130
        resp.form['f0'].value = 'Hello' # try changing the value
5131
        resp = resp.form.submit('submit')
5132

  
5133
        formdatas = [x for x in formdef.data_class().select() if not x.is_draft()]
5134
        assert len(formdatas) == 1
5135
        assert formdatas[0].data['0'] == 'foo@localhost'
5112 5136

  
5113 5137

  
5114 5138
def test_form_page_profile_verified_date_prefill(pub):
......
7339 7363
    resp.forms[0]['f1'] = '2'
7340 7364
    resp = resp.forms[0].submit('submit')
7341 7365
    assert 'style="display: none"' in comment.search(resp.forms[0].text).group(0)
7366

  
7367

  
7368
def test_field_live_readonly_prefilled_field(pub, http_requests):
7369
    FormDef.wipe()
7370
    formdef = FormDef()
7371
    formdef.name = 'Foo'
7372
    formdef.fields = [
7373
        fields.StringField(type='string', id='1', label='Bar', size='40',
7374
            required=True, varname='bar'),
7375
        fields.StringField(type='string', id='2', label='readonly', size='40',
7376
            required=True,
7377
            prefill={'type': 'string', 'value': 'bla {{form_var_bar}} bla', 'readonly': True}),
7378
    ]
7379
    formdef.store()
7380
    formdef.data_class().wipe()
7381

  
7382
    app = get_app(pub)
7383
    resp = app.get('/foo/')
7384
    assert 'f1' in resp.form.fields
7385
    assert resp.html.find('div', {'data-field-id': '1'}).attrs['data-live-source'] == 'true'
7386
    resp.form['f1'] = 'hello'
7387
    live_resp = app.post('/foo/live', params=resp.form.submit_fields())
7388
    assert live_resp.json['result']['2']['content'] == 'bla hello bla'
7389
    resp.form['f1'] = 'toto'
7390
    live_resp = app.post('/foo/live?modified_field_id=1', params=resp.form.submit_fields())
7391
    assert live_resp.json['result']['2']['content'] == 'bla toto bla'
tests/test_formdef_import.py
593 593
    formdef.digest_template = '{{form_number}}'
594 594
    f2 = assert_xml_import_export_works(formdef)
595 595
    assert f2.digest_template == formdef.digest_template
596

  
597

  
598
def test_field_prefill():
599
    formdef = FormDef()
600
    formdef.name = 'Foo'
601
    formdef.fields = [
602
            fields.StringField(type='string', id=1, label='Bar', size='40',
603
                prefill={'type': 'string', 'value': 'plop'})
604
            ]
605
    f2 = assert_xml_import_export_works(formdef)
606
    assert len(f2.fields) == len(formdef.fields)
607
    assert f2.fields[0].prefill == {'type': 'string', 'value': 'plop'}
608

  
609
    formdef.fields = [
610
            fields.StringField(type='string', id=1, label='Bar', size='40',
611
                prefill={'type': 'string', 'value': 'plop', 'readonly': True})
612
            ]
613
    f2 = assert_xml_import_export_works(formdef)
614
    assert len(f2.fields) == len(formdef.fields)
615
    assert f2.fields[0].prefill == {'type': 'string', 'value': 'plop', 'readonly': True}
616

  
617
    formdef.fields = [
618
            fields.StringField(type='string', id=1, label='Bar', size='40',
619
                prefill={'type': 'string', 'value': 'plop', 'readonly': False})
620
            ]
621
    formdef_xml = formdef.export_to_xml()
622
    f2 = FormDef.import_from_xml_tree(formdef_xml)
623
    assert len(f2.fields) == len(formdef.fields)
624
    assert f2.fields[0].prefill == {'type': 'string', 'value': 'plop'}
wcs/fields.py
122 122
                 attrs={'data-dynamic-display-child-of': 'prefill$type',
123 123
                        'data-dynamic-display-value': prefill_types.get('geolocation')})
124 124

  
125
        # exclude geolocation from readonly prefill as the data necessarily
126
        # comes from the user device.
127
        self.add(CheckboxWidget,
128
                 'readonly',
129
                 value=value.get('readonly'),
130
                 attrs={'data-dynamic-display-child-of': 'prefill$type',
131
                        'data-dynamic-display-value-in': '|'.join(
132
                            [x[1] for x in options if x[0] not in ('none', 'geolocation')]),
133
                        'inline_title': _('Readonly'),
134
                        }
135
                 )
136

  
125 137
        self._parsed = False
126 138

  
127 139

  
......
130 142
        type_ = self.get('type')
131 143
        if type_:
132 144
            values['type'] = type_
145
            values['readonly'] = self.get('readonly')
133 146
            value = self.get('value_%s' % type_)
134 147
            if value:
135 148
                values['value'] = value
......
310 323
        elif node.text:
311 324
            self.condition = {'type': 'python', 'value': force_str(node.text).strip()}
312 325

  
326
    def prefill_init_with_xml(self, node, charset, include_id=False):
327
        self.prefill = {}
328
        if node is not None and node.findall('type'):
329
            self.prefill = {
330
                'type': force_str(node.find('type').text),
331
            }
332
            if self.prefill['type'] and self.prefill['type'] != 'none':
333
                self.prefill['value'] = force_str(node.find('value').text)
334
                if node.find('readonly') is not None and force_str(node.find('readonly').text) == 'True':
335
                    self.prefill['readonly'] = True
336

  
313 337
    def get_rst_view_value(self, value, indent=''):
314 338
        return indent + self.get_view_value(value)
315 339

  
......
327 351
    def get_prefill_value(self, user=None, force_string=True):
328 352
        # returns a tuple with two items,
329 353
        #  1. value[str], the value that will be used to prefill
330
        #  2. verified[bool], a flag to know if this is a "verified" value
331
        #     (that will therefore be marked as readonly etc.)
354
        #  2. readonly[bool], a flag to know if this is a readonly value
355
        #     (because it has been explicitely marked so or because it
356
        #     comes from verified identity data).
332 357
        t = self.prefill.get('type')
358
        explicit_readonly = bool(self.prefill.get('readonly'))
333 359
        if t == 'string':
334 360
            value = self.prefill.get('value')
335 361
            if not Template.is_template_string(value):
336
                return (value, False)
362
                return (value, explicit_readonly)
337 363

  
338 364
            context = get_publisher().substitutions.get_context_variables()
339 365
            try:
340
                return (Template(value, autoescape=False, raises=True).render(context), False)
366
                return (Template(value, autoescape=False, raises=True).render(context), explicit_readonly)
341 367
            except TemplateError:
342 368
                return (None, False)
343 369

  
344 370
        elif t == 'user' and user:
345 371
            x = self.prefill.get('value')
346 372
            if x == 'email':
347
                return (user.email, 'email' in (user.verified_fields or []))
373
                return (user.email, explicit_readonly or 'email' in (user.verified_fields or []))
348 374
            elif user.form_data:
349 375
                userform = user.get_formdef()
350 376
                for userfield in userform.fields:
351 377
                    if userfield.id == x:
352 378
                        return (user.form_data.get(x),
353
                                str(userfield.id) in (user.verified_fields or []))
379
                                explicit_readonly or str(userfield.id) in (user.verified_fields or []))
354 380

  
355 381
        elif t == 'formula':
356 382
            formula = self.prefill.get('value')
......
369 395
                        # (items field are prefilled with list of strings, and
370 396
                        # will get the native python object)
371 397
                        ret = str(ret)
372
                    return (ret, False)
398
                    return (ret, explicit_readonly)
373 399
            except:
374 400
                pass
375 401

  
wcs/formdef.py
684 684
                    if not varname in live_condition_fields:
685 685
                        live_condition_fields[varname] = []
686 686
                    live_condition_fields[varname].append(field)
687
            if field.prefill and field.prefill.get('readonly') and field.prefill.get('type') == 'string':
688
                for varname in field.get_referenced_varnames(formdef=self, value=field.prefill.get('value', '')):
689
                    if varname not in live_condition_fields:
690
                        live_condition_fields[varname] = []
691
                    live_condition_fields[varname].append(field)
687 692
            if field.key == 'comment':
688 693
                for varname in field.get_referenced_varnames(formdef=self, value=field.label):
689 694
                    if not varname in live_condition_fields:
wcs/forms/common.py
674 674
                continue
675 675
            if widget.field.key == 'comment':
676 676
                result[widget.field.id]['content'] = widget.content
677
            elif widget.field.prefill and widget.field.prefill.get('readonly') and widget.field.prefill.get('type') == 'string':
678
                value, verified = widget.field.get_prefill_value()
679
                result[widget.field.id]['content'] = value
677 680

  
678 681
        return json.dumps({'result': result})
679 682

  
wcs/forms/root.py
733 733
        except (TypeError, ValueError):
734 734
            step = 0
735 735

  
736
        # reset verified fields, making sure the user cannot alter them.
737
        prefill_user = get_request().user
738
        if get_request().is_in_backoffice():
739
            prefill_user = get_publisher().substitutions.get_context_variables().get('form_user')
740
        if prefill_user:
741
            for field in self.formdef.fields:
742
                if not field.prefill:
743
                    continue
744
                if not 'f%s' % field.id in get_request().form:
745
                    continue
746
                v, verified = field.get_prefill_value(user=prefill_user)
747
                if verified:
748
                    if not isinstance(v, six.string_types) and field.convert_value_to_str:
749
                        # convert structured data to strings as if they were
750
                        # submitted by the browser.
751
                        v = field.convert_value_to_str(v)
752
                    get_request().form['f%s' % field.id] = v
753

  
754 736
        if step == 0:
755 737
            try:
756 738
                page_no = int(form.get_widget('page').parse())
......
789 771
            form_data = session.get_by_magictoken(magictoken, {})
790 772
            with get_publisher().substitutions.temporary_feed(
791 773
                    transient_formdata, force_mode='lazy'):
774
                # reset readonly data with newly submitted values, this allows
775
                # for templates referencing fields from the sampe page.
776
                self.reset_readonly_data()
792 777
                data = self.formdef.get_data(form)
778

  
793 779
            form_data.update(data)
794 780

  
795 781
            if self.has_draft_support() and form.get_submit() == 'savedraft':
......
892 878
            else:
893 879
                return self.page(self.pages[page_no])
894 880

  
881
        self.reset_readonly_data()
895 882
        if step == 1:
896 883
            form.add_submit('previous')
897 884
            magictoken = form.get_widget('magictoken').parse()
......
942 929

  
943 930
            return self.submitted(form, existing_formdata)
944 931

  
932
    def reset_readonly_data(self):
933
        # reset readonly fields, making sure the user cannot alter them.
934
        prefill_user = get_request().user
935
        if get_request().is_in_backoffice():
936
            prefill_user = get_publisher().substitutions.get_context_variables().get('form_user')
937
        for field in self.formdef.fields:
938
            if not field.prefill:
939
                continue
940
            if not 'f%s' % field.id in get_request().form:
941
                continue
942
            v, verified = field.get_prefill_value(user=prefill_user)
943
            if verified:
944
                if not isinstance(v, six.string_types) and field.convert_value_to_str:
945
                    # convert structured data to strings as if they were
946
                    # submitted by the browser.
947
                    v = field.convert_value_to_str(v)
948
                get_request().form['f%s' % field.id] = v
945 949

  
946 950
    def previous_page(self, page_no, magictoken):
947 951
        session = get_session()
wcs/qommon/form.py
230 230
    attrs = {'id': 'form_' + self.name}
231 231
    if self.required:
232 232
        attrs['aria-required'] = 'true'
233
    inline_title = self.attrs.pop('inline_title', '')
233 234
    if self.attrs:
234 235
        attrs.update(self.attrs)
235 236
    checkbox = htmltag("input", xml_end=True, type="checkbox", name=self.name,
236 237
                       value="yes", checked=self.value and "checked" or None,
237 238
                       **attrs)
238 239
    if standalone:
239
        return htmltext('<label>%s<span></span></label>' % checkbox)  # for custom style
240
        data_attrs = ' '.join('%s="%s"' % x for x in attrs.items() if x[0].startswith('data-'))
241
        # more elaborate markup so standalone checkboxes can be applied a
242
        # custom style.
243
        return htmltext('<label %s>%s<span>' % (data_attrs, checkbox)) + inline_title + htmltext('</span></label>')
240 244
    return checkbox
241 245
CheckboxWidget.render_content = checkbox_render_content
242 246

  
wcs/qommon/static/js/qommon.forms.js
114 114
            }
115 115
          }
116 116
          if (value.content) {
117
            // replace comment content
118 117
            var $widget = $('[data-field-id="' + key + '"]');
119
            $widget.html(value.content);
118
            if ($widget.hasClass('comment-field')) {
119
              // replace comment content
120
              $widget.html(value.content);
121
            } else {
122
              // replace text input value
123
              $widget.find('input, textarea').val(value.content);
124
            }
120 125
          }
121 126
          if (value.source_url) {
122 127
            // json change of URL
123
-