Projet

Général

Profil

0001-general-add-support-for-prefilling-blocks-45264.patch

Frédéric Péters, 13 octobre 2020 09:04

Télécharger (21,1 ko)

Voir les différences:

Subject: [PATCH] general: add support for prefilling blocks (#45264)

 tests/test_form_pages.py | 145 ++++++++++++++++++++++++++++
 wcs/forms/root.py        | 203 ++++++++++++++++++++++++---------------
 wcs/wf/form.py           |  22 +----
 3 files changed, 272 insertions(+), 98 deletions(-)
tests/test_form_pages.py
8841 8841
    assert resp.html.find('div', {'data-geolocation': 'road'})
8842 8842

  
8843 8843

  
8844
def test_block_string_prefill(pub, blocks_feature):
8845
    create_user(pub)
8846
    FormDef.wipe()
8847
    BlockDef.wipe()
8848

  
8849
    block = BlockDef()
8850
    block.name = 'foobar'
8851
    block.fields = [
8852
        fields.StringField(id='123', required=True, label='Test', type='string',
8853
            prefill={'type': 'string', 'value': '{{ form_var_foo }} World'}),
8854
    ]
8855
    block.store()
8856

  
8857
    formdef = FormDef()
8858
    formdef.name = 'form title'
8859
    formdef.fields = [
8860
        fields.PageField(id='0', label='1st page', type='page'),
8861
        fields.StringField(id='1', label='string', varname='foo'),
8862
        fields.PageField(id='2', label='2nd page', type='page'),
8863
        fields.BlockField(id='3', label='test', type='block:foobar'),
8864
    ]
8865
    formdef.store()
8866
    formdef.data_class().wipe()
8867

  
8868
    app = get_app(pub)
8869
    resp = app.get(formdef.get_url())
8870
    resp.form['f1'] = 'Hello'
8871
    resp = resp.form.submit('submit')  # -> 2nd page
8872
    assert resp.form['f3$element0$f123'].value == 'Hello World'
8873
    resp = resp.form.submit('submit')  # -> validation page
8874
    resp = resp.form.submit('submit')  # -> end page
8875
    resp = resp.follow()
8876

  
8877
    formdata = formdef.data_class().select()[0]
8878
    assert formdata.data['3']['data'][0]['123'] == 'Hello World'
8879

  
8880
    # check unmodified prefilled field
8881
    app = get_app(pub)
8882
    resp = app.get(formdef.get_url())
8883
    resp.form['f1'] = 'Hello'
8884
    resp = resp.form.submit('submit')  # -> 2nd page
8885
    assert resp.form['f3$element0$f123'].value == 'Hello World'
8886
    resp = resp.form.submit('submit')  # -> validation page
8887
    resp = resp.form.submit('previous')  # -> 2nd page
8888
    resp = resp.form.submit('previous')  # -> 1st page
8889
    resp.form['f1'] = 'Test'
8890
    resp = resp.form.submit('submit')  # -> 2nd page
8891
    assert resp.form['f3$element0$f123'].value == 'Test World'
8892

  
8893
    # check modified prefilled field
8894
    app = get_app(pub)
8895
    resp = app.get(formdef.get_url())
8896
    resp.form['f1'] = 'Hello'
8897
    resp = resp.form.submit('submit')  # -> 2nd page
8898
    assert resp.form['f3$element0$f123'].value == 'Hello World'
8899
    resp.form['f3$element0$f123'] = 'Foobar'
8900
    resp = resp.form.submit('submit')  # -> validation page
8901
    resp = resp.form.submit('previous')  # -> 2nd page
8902
    resp = resp.form.submit('previous')  # -> 1st page
8903
    resp.form['f1'] = 'Test'
8904
    resp = resp.form.submit('submit')  # -> 2nd page
8905
    assert resp.form['f3$element0$f123'].value == 'Foobar'
8906

  
8907

  
8908
def test_block_locked_prefill(pub, blocks_feature):
8909
    create_user(pub)
8910
    FormDef.wipe()
8911
    BlockDef.wipe()
8912

  
8913
    block = BlockDef()
8914
    block.name = 'foobar'
8915
    block.fields = [
8916
        fields.StringField(id='123', required=True, label='Test', type='string',
8917
            prefill={'type': 'string', 'value': '{{ form_var_foo }} World', 'locked': True}),
8918
    ]
8919
    block.store()
8920

  
8921
    formdef = FormDef()
8922
    formdef.name = 'form title'
8923
    formdef.fields = [
8924
        fields.PageField(id='0', label='1st page', type='page'),
8925
        fields.StringField(id='1', label='string', varname='foo'),
8926
        fields.PageField(id='2', label='2nd page', type='page'),
8927
        fields.BlockField(id='3', label='test', type='block:foobar'),
8928
    ]
8929
    formdef.store()
8930
    formdef.data_class().wipe()
8931

  
8932
    app = get_app(pub)
8933
    resp = app.get(formdef.get_url())
8934
    resp.form['f1'] = 'Hello'
8935
    resp = resp.form.submit('submit')  # -> 2nd page
8936
    assert resp.form['f3$element0$f123'].value == 'Hello World'
8937
    assert 'readonly' in resp.form['f3$element0$f123'].attrs
8938
    resp.form['f3$element0$f123'].value = 'Hello'  # try changing the value
8939
    resp = resp.form.submit('submit')  # -> validation page
8940
    resp = resp.form.submit('submit')  # -> end page
8941
    resp = resp.follow()
8942

  
8943
    formdata = formdef.data_class().select()[0]
8944
    assert formdata.data['3']['data'][0]['123'] == 'Hello World'  # value got reverted
8945

  
8946

  
8947
def test_workflow_form_block_prefill(pub):
8948
    create_user(pub)
8949
    FormDef.wipe()
8950
    BlockDef.wipe()
8951

  
8952
    block = BlockDef()
8953
    block.name = 'foobar'
8954
    block.fields = [
8955
        fields.StringField(id='123', required=True, label='Test', type='string',
8956
            prefill={'type': 'user', 'value': 'email'}),
8957
    ]
8958
    block.store()
8959

  
8960
    wf = Workflow(name='status')
8961
    st1 = wf.add_status('Status1', 'st1')
8962

  
8963
    display_form = FormWorkflowStatusItem()
8964
    display_form.id = '_x'
8965
    display_form.by = ['_submitter']
8966
    display_form.varname = 'xxx'
8967
    display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
8968
    display_form.formdef.fields.append(
8969
        fields.BlockField(id='3', label='test', type='block:foobar')
8970
    )
8971
    st1.items.append(display_form)
8972
    display_form.parent = st1
8973

  
8974
    wf.store()
8975

  
8976
    formdef = create_formdef()
8977
    formdef.workflow_id = wf.id
8978
    formdef.fields = []
8979
    formdef.store()
8980
    formdef.data_class().wipe()
8981

  
8982
    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
8983
    resp = resp.form.submit('submit')  # -> validation
8984
    resp = resp.form.submit('submit').follow()
8985
    assert 'The form has been recorded' in resp
8986
    assert resp.form['f3$element0$f123'].value == 'foo@localhost'
8987

  
8988

  
8844 8989
def test_block_title_and_comment(pub, blocks_feature):
8845 8990
    create_user(pub)
8846 8991
    FormDef.wipe()
wcs/forms/root.py
298 298
                  'current_page_no': current_position,
299 299
                })
300 300

  
301
    @classmethod
302
    def iter_with_block_fields(cls, form, fields):
303
        for field in fields:
304
            field_key = '%s' % field.id
305
            widget = form.get_widget('f%s' % field_key) if form else None
306
            yield field, field_key, widget, None
307
            if field.key == 'block':
308
                # we only ever prefill the first item
309
                subwidget = widget.widgets[0] if widget else None
310
                for subfield in field.block.fields:
311
                    subfield_key = '%s$%s' % (field.id, subfield.id)
312
                    subfield_widget = subwidget.get_widget('f%s' % subfield.id) if subwidget else None
313
                    yield subfield, subfield_key, subfield_widget, field
314

  
315
    @classmethod
316
    def apply_field_prefills(cls, data, form, displayed_fields):
317
        req = get_request()
318
        had_prefill = False
319
        for field, field_key, widget, block in cls.iter_with_block_fields(form, displayed_fields):
320
            v = None
321
            prefilled = False
322
            locked = False
323

  
324
            if field.prefill:
325
                prefill_user = get_request().user
326
                if get_request().is_in_backoffice():
327
                    prefill_user = get_publisher().substitutions.get_context_variables(
328
                            ).get('form_user')
329
                v, locked = field.get_prefill_value(user=prefill_user)
330

  
331
                # always set additional attributes as they will be used for
332
                # "live prefill", regardless of existing data.
333
                widget.prefill_attributes = field.get_prefill_attributes()
334

  
335
            should_prefill = bool(field.prefill)
336

  
337
            has_current_value = False
338
            if block:
339
                try:
340
                    current_value = data[block.id]['data'][0][field.id]
341
                    has_current_value = True
342
                except (IndexError, KeyError, ValueError):
343
                    pass
344
            else:
345
                try:
346
                    current_value = data[field_key]
347
                    has_current_value = True
348
                except KeyError:
349
                    pass
350

  
351
            if has_current_value:
352
                # existing value, update it with the new computed value
353
                # if it's the same that was previously computed.
354
                prefill_value = v
355
                v = current_value
356
                if data.get('prefilling_data', {}).get(field_key) == current_value:
357
                    # replace value with new value computed for prefill
358
                    v = prefill_value
359
                else:
360
                    should_prefill = False
361

  
362
            if should_prefill:
363
                if get_request().is_in_backoffice() and (
364
                        field.prefill and field.prefill.get('type') == 'geoloc'):
365
                    # turn off prefilling from geolocation attributes if
366
                    # the form is filled from the backoffice
367
                    v = None
368
                if v:
369
                    prefilled = True
370
                    widget.prefilled = True
371

  
372
            if not prefilled and widget:
373
                widget.clear_error()
374
                widget._parsed = False
375

  
376
            if v is not None:
377
                # store computed value, it will be used to compare with
378
                # submitted value if page is visited again.
379
                if should_prefill:
380
                    if 'prefilling_data' not in data:
381
                        data['prefilling_data'] = {}
382
                    data['prefilling_data'][field_key] = v
383
                if not isinstance(v, str) and field.convert_value_to_str:
384
                    v = field.convert_value_to_str(v)
385
                widget.set_value(v)
386
                widget.transfer_form_value(req)
387
                if field.type == 'item' and v and widget.value != v:
388
                    # mark field as invalid if the value was not accepted
389
                    # (this is required by quixote>=3 as the value would
390
                    # not be evaluated in the initial GET request of the
391
                    # page).
392
                    widget.set_error(get_selection_error_text())
393
                if locked:
394
                    widget.readonly = 'readonly'
395
                    widget.attrs['readonly'] = 'readonly'
396
                had_prefill = True
397
        return had_prefill
398

  
301 399
    def page(self, page, page_change=True, page_error_messages=None, submit_button=None):
302 400
        displayed_fields = []
303 401

  
......
360 458
            # visited, we restore values; otherwise we set req.form as empty.
361 459
            req = get_request()
362 460
            req.environ['REQUEST_METHOD'] = 'GET'
363
            for field in displayed_fields:
364
                k = field.id
365
                v = None
366
                prefilled = False
367
                locked = False
368

  
369
                if field.prefill:
370
                    prefill_user = get_request().user
371
                    if get_request().is_in_backoffice():
372
                        prefill_user = get_publisher().substitutions.get_context_variables(
373
                                ).get('form_user')
374
                    v, locked = field.get_prefill_value(user=prefill_user)
375

  
376
                    # always set additional attributes as they will be used for
377
                    # "live prefill", regardless of existing data.
378
                    form.get_widget('f%s' % k).prefill_attributes = field.get_prefill_attributes()
379

  
380
                should_prefill = bool(field.prefill)
381

  
382
                if k in data:
383
                    # existing value, update it with the new computed value
384
                    # if it's the same that was previously computed.
385
                    prefill_value = v
386
                    v = data[k]
387
                    if data.get('prefilling_data', {}).get(k) == data[k]:
388
                        # replace value with new value computed for prefill
389
                        v = prefill_value
390
                    else:
391
                        should_prefill = False
392 461

  
393
                if should_prefill:
394
                    if get_request().is_in_backoffice() and (
395
                            field.prefill and field.prefill.get('type') == 'geoloc'):
396
                        # turn off prefilling from geolocation attributes if
397
                        # the form is filled from the backoffice
398
                        v = None
399
                    if v:
400
                        prefilled = True
401
                        form.get_widget('f%s' % k).prefilled = True
402

  
403
                if not prefilled and form.get_widget('f%s' % k):
404
                    form.get_widget('f%s' % k).clear_error()
405

  
406
                if v is not None:
407
                    # store computed value, it will be used to compare with
408
                    # submitted value if page is visited again.
409
                    if should_prefill:
410
                        if 'prefilling_data' not in data:
411
                            data['prefilling_data'] = {}
412
                        data['prefilling_data'][k] = v
413
                    if not isinstance(v, str) and field.convert_value_to_str:
414
                        v = field.convert_value_to_str(v)
415
                    form.get_widget('f%s' % k).set_value(v)
416
                    form.get_widget('f%s' % k).transfer_form_value(req)
417
                    if field.type == 'item' and v and form.get_widget('f%s' % k).value != v:
418
                        # mark field as invalid if the value was not accepted
419
                        # (this is required by quixote>=3 as the value would
420
                        # not be evaluated in the initial GET request of the
421
                        # page).
422
                        form.get_widget('f%s' % k).set_error(get_selection_error_text())
423
                    if locked:
424
                        form.get_widget('f%s' % k).readonly = 'readonly'
425
                        form.get_widget('f%s' % k).attrs['readonly'] = 'readonly'
426
                    had_prefill = True
462
            had_prefill = self.apply_field_prefills(data, form, displayed_fields)
427 463

  
428 464
            if had_prefill:
429 465
                # include prefilled data
......
438 474
        else:
439 475
            # not a page change, reset_locked_data() will have been called
440 476
            # earlier, we use that to set appropriate fields as readonly.
441
            for field in displayed_fields:
442
                if get_request().form.get('__locked_f%s' % field.id):
443
                    form.get_widget('f%s' % field.id).readonly = 'readonly'
444
                    form.get_widget('f%s' % field.id).attrs['readonly'] = 'readonly'
477
            for field, field_key, widget, block in self.iter_with_block_fields(form, displayed_fields):
478
                if get_request().form.get('__locked_f%s' % field_key):
479
                    widget.readonly = 'readonly'
480
                    widget.attrs['readonly'] = 'readonly'
445 481

  
446
        for field in displayed_fields:
482
        for field, field_key, widget, block in self.iter_with_block_fields(form, displayed_fields):
447 483
            if field.prefill:
448 484
                # always set additional attributes as they will be used for
449 485
                # "live prefill", regardless of existing data.
450
                form.get_widget('f%s' % field.id).prefill_attributes = field.get_prefill_attributes()
486
                widget.prefill_attributes = field.get_prefill_attributes()
451 487

  
452 488
        self.formdef.set_live_condition_sources(form, displayed_fields)
453 489

  
......
455 491
            # pass over prefilled fields that are used as live source of item
456 492
            # fields
457 493
            fields_to_update = set()
458
            for field in displayed_fields:
459
                widget = form.get_widget('f%s' % field.id)
494
            for field, field_key, widget, block in self.iter_with_block_fields(form, displayed_fields):
460 495
                if getattr(widget, 'prefilled', False) and getattr(widget, 'live_condition_source', False):
461 496
                    fields_to_update.update(widget.live_condition_fields)
462 497
                elif field in fields_to_update and field.type == 'item':
......
857 892
                    transient_formdata, force_mode='lazy'):
858 893
                # reset locked data with newly submitted values, this allows
859 894
                # for templates referencing fields from the sampe page.
860
                self.reset_locked_data()
895
                self.reset_locked_data(form)
861 896
                data = self.formdef.get_data(form)
862 897

  
863 898
            form_data.update(data)
......
969 1004
            else:
970 1005
                return self.page(self.pages[page_no])
971 1006

  
972
        self.reset_locked_data()
1007
        self.reset_locked_data(form)
973 1008
        if step == 1:
974 1009
            form.add_submit('previous')
975 1010
            magictoken = form.get_widget('magictoken').parse()
......
1020 1055

  
1021 1056
            return self.submitted(form, existing_formdata)
1022 1057

  
1023
    def reset_locked_data(self):
1058
    def reset_locked_data(self, form):
1024 1059
        # reset locked fields, making sure the user cannot alter them.
1025 1060
        prefill_user = get_request().user
1026 1061
        if get_request().is_in_backoffice():
1027 1062
            prefill_user = get_publisher().substitutions.get_context_variables().get('form_user')
1028
        for field in self.formdef.fields:
1063
        for field, field_key, widget, block in self.iter_with_block_fields(form, self.formdef.fields):
1029 1064
            if not field.prefill:
1030 1065
                continue
1031
            if not 'f%s' % field.id in get_request().form:
1066
            post_key = 'f%s' % field_key
1067
            if block:
1068
                post_key = 'f%s$element0$f%s' % (block.id, field.id)
1069
            if post_key not in get_request().form:
1032 1070
                continue
1033 1071
            v, locked = field.get_prefill_value(user=prefill_user)
1034 1072
            if locked:
......
1036 1074
                    # convert structured data to strings as if they were
1037 1075
                    # submitted by the browser.
1038 1076
                    v = field.convert_value_to_str(v)
1039
                get_request().form['f%s' % field.id] = v
1077
                get_request().form[post_key] = v
1078
                if widget:
1079
                    widget.set_value(v)
1080
                    if block:
1081
                        # child widget value was changed, mark parent widgets
1082
                        # as unparsed
1083
                        block_widget = form.get_widget('f%s' % block.id)
1084
                        block_widget._parsed = False
1085
                        block_widget.widgets[0]._parsed = False
1086

  
1040 1087
                # keep track of locked field, this will be used when
1041 1088
                # redisplaying the same page in case of errors.
1042
                get_request().form['__locked_f%s' % field.id] = True
1089
                get_request().form['__locked_f%s' % field_key] = True
1043 1090

  
1044 1091
    def previous_page(self, page_no, magictoken):
1045 1092
        session = get_session()
wcs/wf/form.py
26 26
from wcs.formdata import get_dict_with_varnames
27 27

  
28 28
from wcs.forms.common import FileDirectory
29
from wcs.forms.root import FormPage
29 30

  
30 31

  
31 32
def lookup_wf_form_file(self, filename):
......
177 178

  
178 179
        formdata.feed_session()
179 180

  
180
        req = get_request()
181 181
        self.formdef.set_live_condition_sources(form, self.formdef.fields)
182 182

  
183 183
        if form.is_submitted():
......
188 188
        if displayed_fields is not None:
189 189
            fields = displayed_fields
190 190

  
191
        for field in fields:
192
            if ('f%s' % field.id) in req.form:
193
                continue
194
            if not field.prefill or field.prefill.get('type') == 'none':
195
                continue
196
            # FIXME: this code duplicates code from wcs/forms/root.py :/
197
            prefill_user = get_request().user
198
            if get_request().is_in_backoffice():
199
                prefill_user = get_publisher().substitutions.get_context_variables(
200
                        ).get('form_user')
201
            v, verified = field.get_prefill_value(user=prefill_user)
202
            if get_request().is_in_backoffice() and (
203
                    field.prefill and field.prefill.get('type') == 'geoloc'):
204
                # turn off prefilling from geolocation attributes if
205
                # the form is filled from the backoffice
206
                v = None
207
            if v:
208
                form.get_widget('f%s' % field.id).set_value(v)
209
                req.form['f%s' % field.id] = v
191
        FormPage.apply_field_prefills({}, form, fields)
210 192

  
211 193
    def evaluate_live_form(self, form, formdata, user):
212 194
        workflow_data = {}
213
-