misc: check selected item field value against updated list of options (#73982) #727

Merged
fpeters merged 1 commits from wip/73982-dynamic-data-source-prefilled-value into main 2023-10-02 18:49:00 +02:00
3 changed files with 100 additions and 16 deletions

View File

@ -479,6 +479,89 @@ def test_form_page_profile_prefill_list(pub):
assert resp.forms[0]['f0'].value == ''
def test_form_page_item_with_variable_data_source_prefill(pub):
create_user(pub)
formdef = create_formdef()
formdef.data_class().wipe()
formdef.fields = [
fields.StringField(
id='1', label='string', varname='string', prefill={'type': 'string', 'value': 'foobar'}
),
fields.ItemField(
id='2',
label='item',
varname='item',
required=False,
data_source={'type': 'foobar'},
prefill={'type': 'string', 'value': '4'},
),
]
formdef.store()
NamedDataSource.wipe()
data_source = NamedDataSource(name='foobar')
data_source.data_source = {
'type': 'json',
'value': 'http://example.net/{{form_var_string}}',
}
data_source.store()
with responses.RequestsMock() as rsps:
rsps.get('http://example.net/None', json={'data': [{'id': '1', 'text': 'hello'}]})
rsps.get(
'http://example.net/foobar',
json={'data': [{'id': '1', 'text': 'hello'}, {'id': '4', 'text': 'world'}]},
)
resp = get_app(pub).get('/test/')
assert len(rsps.calls) == 2
assert rsps.calls[0].request.url == 'http://example.net/None'
assert rsps.calls[1].request.url == 'http://example.net/foobar'
assert [x.attrib['value'] for x in resp.pyquery('#form_f2 option')] == ['1', '4']
assert resp.form['f2'].value == '4'
assert not resp.pyquery('#form_error_f2').text()
def test_form_page_item_with_computed_field_variable_data_source_prefill(pub):
create_user(pub)
formdef = create_formdef()
formdef.data_class().wipe()
formdef.fields = [
fields.ComputedField(
id='1',
label='string',
varname='string',
value_template='foobar',
),
fields.ItemField(
id='2',
label='item',
varname='item',
required=False,
data_source={'type': 'foobar'},
prefill={'type': 'string', 'value': '4'},
),
]
formdef.store()
NamedDataSource.wipe()
data_source = NamedDataSource(name='foobar')
data_source.data_source = {
'type': 'json',
'value': 'http://example.net/{{form_var_string}}',
}
data_source.store()
with responses.RequestsMock() as rsps:
rsps.get(
'http://example.net/foobar',
json={'data': [{'id': '1', 'text': 'hello'}, {'id': '4', 'text': 'world'}]},
)
resp = get_app(pub).get('/test/')
assert [x.attrib['value'] for x in resp.pyquery('#form_f2 option')] == ['1', '4']
assert resp.form['f2'].value == '4'
assert not resp.pyquery('#form_error_f2').text()
def test_form_page_formula_prefill_items_field(pub):
create_user(pub)
formdef = create_formdef()

View File

@ -964,6 +964,9 @@ class FormDef(StorableObject):
if widget:
widget.live_condition_source = True
widget.live_condition_fields = live_condition_fields[field.varname]
elif field.key == 'computed':
field.live_condition_source = True
field.live_condition_fields = live_condition_fields[field.varname]
Review

Les champs calculés n'ont pas de widget associé mais on a besoin d'enregistrer quelque part l'info sur les champs qui en dépendent. (je pense qu'on ne peut pas faire ça de manière systématique parce que les blocs de champs peuvent donner plusieurs widgets pour un seul field, mais je n'ai pas creusé).

Les champs calculés n'ont pas de widget associé mais on a besoin d'enregistrer quelque part l'info sur les champs qui en dépendent. (je pense qu'on ne peut pas faire ça de manière systématique parce que les blocs de champs peuvent donner plusieurs widgets pour un seul field, mais je n'ai pas creusé).
@classmethod
def get_field_data(cls, field, widget, raise_on_error=False):

View File

@ -522,13 +522,6 @@ class FormPage(Directory, TempfileDirectoryMixin, FormTemplateMixin):
form.get_widget('f%s' % block.id).unparse()
form.get_widget('f%s' % block.id).clear_error()
if field.key == 'item' and v:
# mark field as invalid if the value is not allowed
# (this is required by quixote>=3 as the value would
# not be evaluated in the initial GET request of the
# page).
widget._parse(req)
Review

Ce code avait donc été ajouté pour quixote 3, c'est en forçant le passage dans _parse que l'erreur "choix invalide" était notée, parce qu'à ce stade du traitement les options possibles ne prennent pas encore en compte les données préremplies dans des champs précédents.

Ce code avait donc été ajouté pour quixote 3, c'est en forçant le passage dans _parse que l'erreur "choix invalide" était notée, parce qu'à ce stade du traitement les options possibles ne prennent pas encore en compte les données préremplies dans des champs précédents.
had_prefill = True
return had_prefill
@ -577,9 +570,8 @@ class FormPage(Directory, TempfileDirectoryMixin, FormTemplateMixin):
magictoken = randbytes(8)
has_new_magictoken = True
computed_data = self.handle_computed_fields(
magictoken, self.formdef.get_computed_fields_from_page(page)
)
computed_fields_on_page = list(self.formdef.get_computed_fields_from_page(page))
computed_data = self.handle_computed_fields(magictoken, computed_fields_on_page)
if computed_data:
form_data.update(computed_data)
self.feed_current_data(magictoken)
@ -687,18 +679,24 @@ class FormPage(Directory, TempfileDirectoryMixin, FormTemplateMixin):
if had_prefill:
# pass over prefilled fields that are used as live source of item
# fields
# fields, update matching list of options of matching fields,
# and mark fields as invalid if the selected value is not available.
Review

On passait déjà sur les champs pour actualiser les options présentées, on y ajoute ici l'appel à widget._parse() qui a été retiré plus haut.

On passait déjà sur les champs pour actualiser les options présentées, on y ajoute ici l'appel à widget._parse() qui a été retiré plus haut.
fields_to_update = set()
for field in computed_fields_on_page:
if getattr(field, 'live_condition_source', False):
fields_to_update.update(field.live_condition_fields)
Review

Mais on ne prenait pas en compte la dépendance aux champs calculés, donc voilà ici.

Mais on ne prenait pas en compte la dépendance aux champs calculés, donc voilà ici.
for field, field_key, widget, dummy, dummy in self.iter_with_block_fields(form, displayed_fields):
if getattr(widget, 'prefilled', False) and getattr(widget, 'live_condition_source', False):
fields_to_update.update(widget.live_condition_fields)
elif field in fields_to_update and field.key == 'item':
elif field.key == 'item':
kwargs = {}
with get_publisher().substitutions.temporary_feed(transient_formdata, force_mode='lazy'):
field.perform_more_widget_changes(form, kwargs)
if 'options' in kwargs and 'options_with_attributes' in kwargs:
widget.options = kwargs['options']
widget.options_with_attributes = kwargs['options_with_attributes']
if field in fields_to_update:
field.perform_more_widget_changes(form, kwargs)
if 'options' in kwargs:
widget.options = kwargs['options']
widget.options_with_attributes = kwargs.get('options_with_attributes')
widget._parse(req)
Review

Voilà l'appel au widget._parse.

Voilà l'appel au widget._parse.
self.set_page_title()