code serveur pour la validation à la volée (#76632) #241
|
@ -2356,7 +2356,7 @@ def test_backoffice_wfedit_and_data_source_with_field_info(pub):
|
|||
resp = resp.follow()
|
||||
assert len(rsps.calls) == 1
|
||||
assert '?xxx=FOO%20BAR%2030' in rsps.calls[-1].request.url
|
||||
assert 'invalid value' not in resp
|
||||
assert len(resp.pyquery('.error:not(#form_error_fieldname)')) == 0
|
||||
|
||||
assert resp.form['f3'].value == 'EE'
|
||||
resp.form['f3'].value = 'DD'
|
||||
resp = resp.form.submit('submit')
|
||||
|
|
|
@ -537,7 +537,7 @@ def test_form_string_field_submit(pub):
|
|||
page = get_app(pub).get('/test/')
|
||||
formdef.data_class().wipe()
|
||||
next_page = page.forms[0].submit('submit') # but the field is required
|
||||
assert next_page.pyquery('div.error').text() == 'required field'
|
||||
assert next_page.pyquery('#form_error_f0').text() == 'required field'
|
||||
fpeters
commented
Ou pour la même raison, plutôt que viser le div.error, viser le div du champ précis. Ou pour la même raison, plutôt que viser le div.error, viser le div du champ précis.
|
||||
next_page.forms[0]['f0'] = 'foobar'
|
||||
next_page = next_page.forms[0].submit('submit')
|
||||
assert 'Check values then click submit.' in next_page.text
|
||||
|
@ -569,7 +569,7 @@ def test_form_items_submit(pub):
|
|||
page = get_app(pub).get('/test/')
|
||||
assert 'List of items' in page.text
|
||||
next_page = page.forms[0].submit('submit') # but the field is required
|
||||
assert next_page.pyquery('div.error').text() == 'required field'
|
||||
assert next_page.pyquery('#form_error_f0').text() == 'required field'
|
||||
next_page.forms[0]['f0$element0'].checked = True
|
||||
next_page.forms[0]['f0$element1'].checked = True
|
||||
next_page = next_page.forms[0].submit('submit')
|
||||
|
@ -591,14 +591,14 @@ def test_form_items_submit(pub):
|
|||
page = get_app(pub).get('/test/')
|
||||
page.forms[0]['f0$element0'].checked = True
|
||||
page = page.forms[0].submit('submit')
|
||||
assert page.pyquery('div.error').text() == 'You must select at least 2 answers.'
|
||||
assert page.pyquery('#form_error_f0').text() == 'You must select at least 2 answers.'
|
||||
page.forms[0]['f0$element1'].checked = True
|
||||
page.forms[0]['f0$element2'].checked = True
|
||||
page.forms[0]['f0$element3'].checked = True
|
||||
page.forms[0]['f0$element4'].checked = True
|
||||
page.forms[0]['f0$element5'].checked = True
|
||||
page = page.forms[0].submit('submit')
|
||||
assert page.pyquery('div.error').text() == 'You must select at most 5 answers.'
|
||||
assert page.pyquery('#form_error_f0').text() == 'You must select at most 5 answers.'
|
||||
page.forms[0]['f0$element5'].checked = False
|
||||
page = next_page.forms[0].submit('submit').follow()
|
||||
assert 'The form has been recorded' in page.text
|
||||
|
@ -623,7 +623,7 @@ def test_form_items_autocomplete(pub):
|
|||
resp = get_app(pub).get('/test/')
|
||||
assert 'select2.min.js' in resp.text
|
||||
resp = resp.forms[0].submit('submit') # but the field is required
|
||||
assert resp.pyquery('div.error').text() == 'required field'
|
||||
assert resp.pyquery('#form_error_f0').text() == 'required field'
|
||||
resp.forms[0]['f0[]'].select_multiple(['Foo', 'Bar'])
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert resp.pyquery('[name="f0[]"] option[selected]').text() == 'Foo Bar'
|
||||
|
@ -646,11 +646,11 @@ def test_form_items_autocomplete(pub):
|
|||
resp = get_app(pub).get('/test/')
|
||||
resp.forms[0]['f0[]'].select_multiple(['Foo', 'Bar'])
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert resp.pyquery('div.error').text() == 'You must select at least 3 choices.'
|
||||
assert resp.pyquery('#form_error_f0').text() == 'You must select at least 3 choices.'
|
||||
assert resp.forms[0]['f0[]'].value == ['Foo', 'Bar']
|
||||
resp.forms[0]['f0[]'].select_multiple(['Foo', 'Bar', 'Three', 'Four', 'Five'])
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert resp.pyquery('div.error').text() == 'You must select at most 4 choices.'
|
||||
assert resp.pyquery('#form_error_f0').text() == 'You must select at most 4 choices.'
|
||||
assert resp.forms[0]['f0[]'].value == ['Foo', 'Bar', 'Three', 'Four', 'Five']
|
||||
resp.forms[0]['f0[]'].select_multiple(['Foo', 'Bar', 'Three'])
|
||||
resp = resp.forms[0].submit('submit') # -> validation
|
||||
|
@ -2746,7 +2746,7 @@ def test_form_draft_save_on_error_page(pub):
|
|||
tracking_code = get_displayed_tracking_code(resp)
|
||||
resp.forms[0]['f1'] = 'plop'
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert resp.pyquery('div.error').text() == 'required field'
|
||||
assert resp.pyquery('#form_error_f2').text() == 'required field'
|
||||
|
||||
resp = login(get_app(pub), username='foo', password='foo').get('/')
|
||||
resp.forms[0]['code'] = tracking_code
|
||||
|
|
|
@ -98,7 +98,7 @@ def test_block_required(pub):
|
|||
resp = app.get(formdef.get_url())
|
||||
resp = resp.form.submit('submit') # -> error page
|
||||
assert 'There were errors processing the form' in resp
|
||||
assert resp.text.count('required field') == 1
|
||||
assert len(resp.pyquery('.error:not(#form_error_fieldname)')) == 1
|
||||
resp.form['f1$element0$f123'] = 'foo'
|
||||
resp.form['f1$element0$f234'] = 'bar'
|
||||
resp = resp.form.submit('submit') # -> validation page
|
||||
|
@ -108,7 +108,7 @@ def test_block_required(pub):
|
|||
resp.form['f1$element0$f123'] = 'foo'
|
||||
resp = resp.form.submit('submit') # -> error page
|
||||
assert 'There were errors processing the form' in resp
|
||||
assert resp.text.count('required field') == 1
|
||||
assert len(resp.pyquery('.error:not(#form_error_fieldname)')) == 1
|
||||
resp.form['f1$element0$f234'] = 'bar'
|
||||
resp = resp.form.submit('submit') # -> validation page
|
||||
assert 'Check values then click submit.' in resp.text
|
||||
|
@ -135,7 +135,7 @@ def test_block_required(pub):
|
|||
resp = app.get(formdef.get_url())
|
||||
resp = resp.form.submit('submit') # -> error page
|
||||
assert 'There were errors processing the form' in resp
|
||||
assert resp.text.count('required field') == 1
|
||||
assert len(resp.pyquery('.error:not(#form_error_fieldname)')) == 1
|
||||
resp.form['f1$element0$f234'] = 'bar'
|
||||
resp = resp.form.submit('submit') # -> validation page
|
||||
assert 'Check values then click submit.' in resp.text
|
||||
|
@ -174,7 +174,7 @@ def test_block_required_previous_page(pub):
|
|||
resp = app.get(formdef.get_url())
|
||||
resp = resp.form.submit('submit') # -> error page
|
||||
assert 'There were errors processing the form' in resp
|
||||
assert resp.text.count('required field') == 1
|
||||
assert len(resp.pyquery('.error:not(#form_error_fieldname)')) == 1
|
||||
resp.form['f1$element0$f123'] = 'foo'
|
||||
resp.form['f1$element0$f234'] = 'bar'
|
||||
if multipage:
|
||||
|
@ -185,7 +185,7 @@ def test_block_required_previous_page(pub):
|
|||
if multipage:
|
||||
resp = resp.form.submit('previous') # -> 2nd page
|
||||
resp = resp.form.submit('previous') # -> 1st page
|
||||
assert resp.text.count('required field') == 0
|
||||
assert len(resp.pyquery('.error:not(#form_error_fieldname)')) == 0
|
||||
assert resp.form['f1$element0$f123'].value == 'foo'
|
||||
assert resp.form['f1$element0$f234'].value == 'bar'
|
||||
|
||||
|
@ -222,7 +222,7 @@ def test_block_required_previous_page(pub):
|
|||
if multipage:
|
||||
resp = resp.form.submit('previous') # -> 2nd page
|
||||
resp = resp.form.submit('previous') # -> 1st page
|
||||
assert resp.text.count('required field') == 0
|
||||
assert len(resp.pyquery('.error:not(#form_error_fieldname)')) == 0
|
||||
assert resp.form['f1$element0$f123'].value == 'foo'
|
||||
assert resp.form['f1$element0$f234'].value == 'bar'
|
||||
assert resp.form['f1$element1$f123'].value == 'foo2'
|
||||
|
@ -429,7 +429,7 @@ def test_block_string_prefill(pub):
|
|||
resp = resp.form.submit('submit') # -> 2nd page
|
||||
assert not resp.pyquery('#form_error_f3') # not marked as error
|
||||
assert not resp.pyquery('#form_error_f4') # ...
|
||||
assert '>required field<' not in resp
|
||||
assert len(resp.pyquery('.error:not(#form_error_fieldname)')) == 0
|
||||
assert resp.form['f3$element0$f123'].value == 'Hello World'
|
||||
assert resp.form['f4$element0$f123'].value == ''
|
||||
resp.form['f4$element0$f123'] = 'plop'
|
||||
|
@ -506,11 +506,11 @@ def test_block_prefill_and_required(pub):
|
|||
|
||||
app = get_app(pub)
|
||||
resp = app.get(formdef.get_url())
|
||||
assert '>required field<' not in resp
|
||||
assert len(resp.pyquery('.error:not(#form_error_fieldname)')) == 0
|
||||
assert resp.form['f2$element0$f123'].value == 'World'
|
||||
resp.form['f3$element0$f123'] = 'Hello'
|
||||
resp = resp.form.submit('submit') # -> same page, error displyed
|
||||
assert resp.text.count('>required field<') == 1
|
||||
assert len(resp.pyquery('.error:not(#form_error_fieldname)')) == 1
|
||||
resp.form['f3$element0$f234'].checked = True
|
||||
resp = resp.form.submit('submit') # -> validation page
|
||||
resp = resp.form.submit('submit') # -> end page
|
||||
|
|
|
@ -426,9 +426,9 @@ def test_form_validation_message(pub):
|
|||
resp = get_app(pub).get(formdef.get_url(language='en'))
|
||||
resp.form['f1'] = 'test'
|
||||
resp = resp.form.submit('submit') # -> validation error
|
||||
assert resp.pyquery('.error').text() == 'validation error'
|
||||
assert resp.pyquery('#form_error_f1').text() == 'validation error'
|
||||
|
||||
resp = get_app(pub).get(formdef.get_url(language='fr'))
|
||||
resp.form['f1'] = 'test'
|
||||
resp = resp.form.submit('submit') # -> validation error
|
||||
assert resp.pyquery('.error').text() == 'erreur de validation'
|
||||
assert resp.pyquery('#form_error_f1').text() == 'erreur de validation'
|
||||
|
|
|
@ -220,7 +220,7 @@ def test_live_field_condition_on_required_field(pub):
|
|||
assert live_resp.json['result']['1']['visible']
|
||||
assert live_resp.json['result']['2']['visible']
|
||||
resp = resp.form.submit('submit')
|
||||
assert resp.pyquery('div.error').text() == 'required field'
|
||||
assert resp.pyquery('#form_error_f2').text() == 'required field'
|
||||
assert 'HELLO' not in resp.text
|
||||
|
||||
|
||||
|
@ -1972,3 +1972,81 @@ def test_comment_from_card_field(pub):
|
|||
resp.form['f1'] = '2'
|
||||
live_resp = app.post('/test/live', params=resp.form.submit_fields())
|
||||
assert live_resp.json['result']['3']['content'] == '<p>Xbar {{ form_var_foo }}Ybar plopZ</p>'
|
||||
|
||||
|
||||
def test_field_live_validation(pub):
|
||||
fpeters
commented
C'est ici les vrais nouveaux tests. C'est ici les vrais nouveaux tests.
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'Foo'
|
||||
formdef.fields = [
|
||||
fields.StringField(type='string', id='1', label='Bar', required=True, validation={'type': 'digits'}),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
app = get_app(pub)
|
||||
resp = app.get('/foo/')
|
||||
url = resp.pyquery('form[data-live-validation-url]').attr('data-live-validation-url')
|
||||
assert url
|
||||
|
||||
live_resp = get_app(pub).post(url, params=resp.form.submit_fields())
|
||||
assert live_resp.json == {'err': 2, 'msg': 'missing session'}
|
||||
|
||||
live_resp = app.post(url, params=resp.form.submit_fields())
|
||||
assert live_resp.json == {'err': 2, 'msg': 'missing ?field parameter'}
|
||||
|
||||
live_resp = app.post(url + '?field=xxx', params=resp.form.submit_fields())
|
||||
assert live_resp.json == {'err': 2, 'msg': 'unknown field'}
|
||||
|
||||
live_resp = app.post(url + '?field=xxx__yyy', params=resp.form.submit_fields())
|
||||
assert live_resp.json == {'err': 2, 'msg': 'invalid ?field parameter'}
|
||||
|
||||
resp.form['f1'] = '1234'
|
||||
live_resp = app.post(url + '?field=f1', params=resp.form.submit_fields())
|
||||
assert live_resp.json == {'err': 0}
|
||||
|
||||
resp.form['f1'] = ''
|
||||
live_resp = app.post(url + '?field=f1', params=resp.form.submit_fields())
|
||||
assert live_resp.json == {'err': 1, 'msg': 'required field', 'valueMissing': True}
|
||||
|
||||
resp.form['f1'] = 'abc'
|
||||
live_resp = app.post(url + '?field=f1', params=resp.form.submit_fields())
|
||||
assert live_resp.json == {'err': 1, 'msg': 'Only digits are allowed', 'badInput': True}
|
||||
|
||||
|
||||
def test_block_field_live_validation(pub):
|
||||
BlockDef.wipe()
|
||||
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [
|
||||
fields.StringField(type='string', id='1', label='Bar', required=True, validation={'type': 'digits'}),
|
||||
]
|
||||
block.store()
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'Foo'
|
||||
formdef.fields = [
|
||||
fields.BlockField(id='2', label='test', type='block:foobar'),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
app = get_app(pub)
|
||||
resp = app.get('/foo/')
|
||||
url = resp.pyquery('form[data-live-validation-url]').attr('data-live-validation-url')
|
||||
assert url
|
||||
|
||||
resp.form['f2$element0$f1'] = '1234'
|
||||
live_resp = app.post(url + '?field=f2__element0__f1', params=resp.form.submit_fields())
|
||||
assert live_resp.json == {'err': 0}
|
||||
|
||||
resp.form['f2$element0$f1'] = ''
|
||||
live_resp = app.post(url + '?field=f2__element0__f1', params=resp.form.submit_fields())
|
||||
assert live_resp.json == {'err': 1, 'msg': 'required field', 'valueMissing': True}
|
||||
|
||||
resp.form['f2$element0$f1'] = 'abc'
|
||||
live_resp = app.post(url + '?field=f2__element0__f1', params=resp.form.submit_fields())
|
||||
assert live_resp.json == {'err': 1, 'msg': 'Only digits are allowed', 'badInput': True}
|
||||
|
||||
live_resp = app.post(url + '?field=f2__element0__fX', params=resp.form.submit_fields())
|
||||
assert live_resp.json == {'err': 2, 'msg': 'unknown sub field'}
|
||||
|
|
|
@ -1433,3 +1433,15 @@ def test_emoji_button():
|
|||
assert PyQuery(str(form.render()))('button').attr.name == 'submit'
|
||||
assert not PyQuery(str(form.render()))('button').attr['aria-label']
|
||||
assert PyQuery(str(form.render()))('button').text() == '✅'
|
||||
|
||||
|
||||
def test_error_templates():
|
||||
widget = TextWidget('test', value='foo', maxlength=10)
|
||||
widget_html = str(widget.render())
|
||||
assert 'data-supports-live-validation=' not in widget_html
|
||||
|
||||
widget.supports_live_validation = True
|
||||
widget_html = str(widget.render())
|
||||
assert 'data-supports-live-validation=' in widget_html
|
||||
assert PyQuery(widget_html)('#error_test_valueMissing').text() == 'required field'
|
||||
assert PyQuery(widget_html)('#error_test_tooLong').text() == 'too many characters (limit is 10)'
|
||||
|
|
|
@ -876,6 +876,7 @@ class WidgetField(Field):
|
|||
prefill = {}
|
||||
|
||||
widget_class = None
|
||||
widget_supports_live_validation = True
|
||||
fpeters
commented
attribut pour déterminer qu'un champ gère la validation à la volée, ça permet derrière d'exclure les champs dépréciés type tableaux, exclus du périmètre. attribut pour déterminer qu'un champ gère la validation à la volée, ça permet derrière d'exclure les champs dépréciés type tableaux, exclus du périmètre.
|
||||
|
||||
def add_to_form(self, form, value=None):
|
||||
kwargs = {'required': self.required, 'render_br': False}
|
||||
|
@ -892,6 +893,7 @@ class WidgetField(Field):
|
|||
form.add(self.widget_class, 'f%s' % self.id, title=self.label, hint=hint, **kwargs)
|
||||
widget = form.get_widget('f%s' % self.id)
|
||||
widget.field = self
|
||||
widget.supports_live_validation = self.widget_supports_live_validation
|
||||
if self.extra_css_class:
|
||||
if hasattr(widget, 'extra_css_class') and widget.extra_css_class:
|
||||
widget.extra_css_class = '%s %s' % (widget.extra_css_class, self.extra_css_class)
|
||||
|
@ -1639,6 +1641,7 @@ class FileField(WidgetField):
|
|||
key = 'file'
|
||||
description = _('File Upload')
|
||||
allow_complex = True
|
||||
widget_supports_live_validation = False
|
||||
fpeters
commented
Les champs fichier aussi sont exclus mais c'est parce que la validation a déjà lieu lors du chargement du fichier. Les champs fichier aussi sont exclus mais c'est parce que la validation a déjà lieu lors du chargement du fichier.
|
||||
|
||||
document_type = None
|
||||
max_file_size = None
|
||||
|
@ -3150,6 +3153,7 @@ class TableField(WidgetField):
|
|||
columns = None
|
||||
|
||||
widget_class = TableWidget
|
||||
widget_supports_live_validation = False
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.rows = []
|
||||
|
@ -3371,6 +3375,7 @@ class TableRowsField(WidgetField):
|
|||
columns = None
|
||||
|
||||
widget_class = TableListRowsWidget
|
||||
widget_supports_live_validation = False
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.columns = []
|
||||
|
@ -3632,6 +3637,7 @@ class RankedItemsField(WidgetField):
|
|||
items = []
|
||||
randomize_items = False
|
||||
widget_class = RankedItemsWidget
|
||||
widget_supports_live_validation = False
|
||||
anonymise = False
|
||||
|
||||
def perform_more_widget_changes(self, form, kwargs, edit=True):
|
||||
|
@ -3904,7 +3910,9 @@ class BlockField(WidgetField):
|
|||
self.block
|
||||
except KeyError:
|
||||
raise MissingBlockFieldError(self.type[6:])
|
||||
return super().add_to_form(form, value=value)
|
||||
widget = super().add_to_form(form, value=value)
|
||||
widget.supports_live_validation = False
|
||||
return widget
|
||||
|
||||
def fill_admin_form(self, form):
|
||||
super().fill_admin_form(form)
|
||||
|
|
|
@ -46,7 +46,15 @@ from wcs.workflows import ContentSnapshotPart, WorkflowBackofficeFieldsFormDef,
|
|||
|
||||
from ..qommon import _, emails, errors, get_cfg, misc, ngettext, template
|
||||
from ..qommon.admin.emails import EmailsDirectory
|
||||
from ..qommon.form import CheckboxWidget, EmailWidget, Form, HiddenErrorWidget, HtmlWidget, StringWidget
|
||||
from ..qommon.form import (
|
||||
CheckboxWidget,
|
||||
EmailWidget,
|
||||
ErrorMessage,
|
||||
Form,
|
||||
HiddenErrorWidget,
|
||||
HtmlWidget,
|
||||
StringWidget,
|
||||
)
|
||||
from ..qommon.template import TemplateError
|
||||
from ..qommon.template_utils import render_block_to_string
|
||||
|
||||
|
@ -270,6 +278,7 @@ class FormPage(FormdefDirectoryBase, FormTemplateMixin):
|
|||
'code',
|
||||
'removedraft',
|
||||
'live',
|
||||
('live-validation', 'live_validation'),
|
||||
('go-to-backoffice', 'go_to_backoffice'),
|
||||
]
|
||||
|
||||
|
@ -941,6 +950,16 @@ class FormPage(FormdefDirectoryBase, FormTemplateMixin):
|
|||
def create_form(self, *args, **kwargs):
|
||||
form = self.formdef.create_form(*args, **kwargs)
|
||||
form.attrs['data-live-url'] = self.formdef.get_url(language=get_publisher().current_language) + 'live'
|
||||
fpeters
commented
C'est la présence de cet attribut qui détermine la prise en charge, ou pas, de la validation à la volée. (un moment j'ai hésité à partager data-live-url pour également y faire la validation, mais non). C'est la présence de cet attribut qui détermine la prise en charge, ou pas, de la validation à la volée. (un moment j'ai hésité à partager data-live-url pour également y faire la validation, mais non).
|
||||
form.attrs['data-live-validation-url'] = (
|
||||
self.formdef.get_url(language=get_publisher().current_language) + 'live-validation'
|
||||
)
|
||||
form.widgets.append(
|
||||
HtmlWidget(
|
||||
'''<template id="form_error_tpl">
|
||||
<div id="form_error_fieldname" class="error"></div>
|
||||
</template>'''
|
||||
)
|
||||
)
|
||||
return form
|
||||
|
||||
def create_view_form(self, *args, **kwargs):
|
||||
|
@ -1668,6 +1687,51 @@ class FormPage(FormdefDirectoryBase, FormTemplateMixin):
|
|||
return result_error('form deserialization failed: %s' % e)
|
||||
return FormStatusPage.live_process_fields(form, formdata, displayed_fields)
|
||||
|
||||
def live_validation(self):
|
||||
# live validation of field values
|
||||
get_request().ignore_session = True
|
||||
get_response().set_content_type('application/json')
|
||||
|
||||
def result_error(reason):
|
||||
return json.dumps({'err': 2, 'msg': reason})
|
||||
|
||||
session = get_session()
|
||||
if not (session and session.id):
|
||||
return result_error('missing session')
|
||||
|
||||
field_ref = get_request().form.get('field')
|
||||
if not field_ref:
|
||||
return result_error('missing ?field parameter')
|
||||
|
||||
fpeters
commented
En paramètre c'est ?field={{widget.get_name_for_id}} qui est une méthode qui fournit le nom avec les $ (invalides dans les id) remplacés par des En paramètre c'est ?field={{widget.get_name_for_id}} qui est une méthode qui fournit le nom avec les $ (invalides dans les id) remplacés par des `__`, ici on fait la conversion inverse.
|
||||
parts = field_ref.split('__')
|
||||
fpeters
commented
S'il y a un morceau, c'est un champ normal, s'il y a trois morceaux, c'est un champ d'un bloc (f0$element0$f1). S'il y a un morceau, c'est un champ normal, s'il y a trois morceaux, c'est un champ d'un bloc (f0$element0$f1).
|
||||
if len(parts) not in (1, 3):
|
||||
return result_error('invalid ?field parameter')
|
||||
for field in self.formdef.fields:
|
||||
if 'f%s' % field.id == parts[0]:
|
||||
break
|
||||
else:
|
||||
return result_error('unknown field')
|
||||
if len(parts) == 3: # block field
|
||||
for subfield in field.block.fields or []:
|
||||
if 'f%s' % subfield.id == parts[2]:
|
||||
break
|
||||
else:
|
||||
return result_error('unknown sub field')
|
||||
field = subfield
|
||||
field.id = field_ref[1:].replace('__', '$')
|
||||
|
||||
form = Form()
|
||||
widget = field.add_to_form(form)
|
||||
fpeters
commented
On crée un formulaire avec uniquement le champ qui nous intéresse, c'est pour ça que juste au-dessus pour le champ d'un bloc on modifie l'id, pour faire comme si c'était un champ direct. On gagne du temps ici à ne pas créer de formdata temporaire etc. c'est possible parce que les validations sur les champs sont limitées aux données de celui-ci. On crée un formulaire avec uniquement le champ qui nous intéresse, c'est pour ça que juste au-dessus pour le champ d'un bloc on modifie l'id, pour faire comme si c'était un champ direct.
On gagne du temps ici à ne pas créer de formdata temporaire etc. c'est possible parce que les validations sur les champs sont limitées aux données de celui-ci.
|
||||
error = widget.get_error()
|
||||
if error:
|
||||
resp = {'err': 1, 'msg': str(error)}
|
||||
if hasattr(widget, 'error_code'):
|
||||
error_message = ErrorMessage(widget.error_code, '')
|
||||
fpeters
commented
On répond ici avec un format qui mime ce que l'API de validation javascript propose. On répond ici avec un format qui mime ce que l'API de validation javascript propose.
|
||||
resp[error_message.camel_code()] = True
|
||||
return json.dumps(resp)
|
||||
else:
|
||||
return json.dumps({'err': 0})
|
||||
|
||||
def clean_submission_context(self):
|
||||
get_publisher().substitutions.unfeed(lambda x: x.__class__.__name__ == 'ConditionVars')
|
||||
get_publisher().substitutions.unfeed(lambda x: isinstance(x, FormData))
|
||||
|
|
|
@ -178,6 +178,41 @@ def widget_get_name_for_id(self):
|
|||
return self.name.replace('$', '__')
|
||||
|
||||
|
||||
class ErrorMessage:
|
||||
# map error code and readable message
|
||||
def __init__(self, code, message):
|
||||
self.code = code
|
||||
self.message = message
|
||||
|
||||
def camel_code(self):
|
||||
return ''.join(x.lower() if i == 0 else x.capitalize() for i, x in enumerate(self.code.split('_')))
|
||||
fpeters
commented
C'est pour faire value_missing -> valueMissing, pour correspondre à l'API javascript. C'est pour faire value_missing -> valueMissing, pour correspondre à l'API javascript.
|
||||
|
||||
|
||||
def widget_set_error_code(self, error_code):
|
||||
self.set_error(getattr(self, 'get_%s_message' % error_code)())
|
||||
self.error_code = error_code
|
||||
|
||||
|
||||
def widget_set_error(self, error):
|
||||
self.error = error
|
||||
if self.error == Widget.REQUIRED_ERROR:
|
||||
self.error_code = 'value_missing'
|
||||
else:
|
||||
self.error_code = 'bad_input'
|
||||
|
||||
|
||||
def widget_get_error_messages(self):
|
||||
for code in self.get_error_message_codes():
|
||||
yield ErrorMessage(code, getattr(self, 'get_%s_message' % code)())
|
||||
|
||||
|
||||
def widget_get_error_message_codes(self):
|
||||
# always add codes that may be generated by javascript validation API
|
||||
yield 'value_missing'
|
||||
yield 'bad_input'
|
||||
yield 'type_mismatch'
|
||||
fpeters
commented
Il y a typeMismatch qui est généré en sortie d'un champ date dont le contenu est partiel (type j'ai juste mis le jour) et en regardant https://developer.mozilla.org/en-US/docs/Web/API/ValidityState j'ai vu badInput et j'ai décidé de remplacer ce que j'avais nommé "invalid_value" par "bad_input". Il y a typeMismatch qui est généré en sortie d'un champ date dont le contenu est partiel (type j'ai juste mis le jour) et en regardant https://developer.mozilla.org/en-US/docs/Web/API/ValidityState j'ai vu badInput et j'ai décidé de remplacer ce que j'avais nommé "invalid_value" par "bad_input".
|
||||
|
||||
|
||||
Widget.render = render
|
||||
Widget.cleanup = None
|
||||
Widget.render_error = render_error
|
||||
|
@ -186,6 +221,13 @@ Widget.render_title = render_title
|
|||
Widget.is_prefilled = is_prefilled
|
||||
Widget.render_widget_content = render_widget_content
|
||||
Widget.get_name_for_id = widget_get_name_for_id
|
||||
Widget.get_error_messages = widget_get_error_messages
|
||||
Widget.get_error_message_codes = widget_get_error_message_codes
|
||||
Widget.get_value_missing_message = lambda x: Widget.REQUIRED_ERROR
|
||||
fpeters
commented
Introduction de cette série de méthodes sur Widget, on est toujours à monkeypatcher ainsi la classe de base dans quixote (il y aurait #5790 pour revoir ça). Introduction de cette série de méthodes sur Widget, on est toujours à monkeypatcher ainsi la classe de base dans quixote (il y aurait #5790 pour revoir ça).
|
||||
Widget.get_bad_input_message = lambda x: _('Invalid value')
|
||||
Widget.get_type_mismatch_message = lambda x: _('Invalid value')
|
||||
Widget.set_error = widget_set_error
|
||||
Widget.set_error_code = widget_set_error_code
|
||||
|
||||
|
||||
def file_render_content(self):
|
||||
|
@ -571,16 +613,25 @@ class StringWidget(QuixoteStringWidget):
|
|||
if self.value:
|
||||
self.value = self.value.strip()
|
||||
if self.maxlength and len(self.value) > self.maxlength:
|
||||
self.error = _('Too long, value must be at most %d characters.') % self.maxlength
|
||||
self.set_error_code('too_long')
|
||||
elif self.validation_function:
|
||||
try:
|
||||
self.validation_function(self.value)
|
||||
except ValueError as e:
|
||||
self.error = str(e)
|
||||
self.set_error(str(e))
|
||||
|
||||
def get_too_long_message(self):
|
||||
return _('Too long, value must be at most %d characters.') % self.maxlength
|
||||
|
||||
fpeters
commented
Pour les codes connus de l'API javascript on les fournit comme ça. Pour les codes connus de l'API javascript on les fournit comme ça.
|
||||
def get_error_message_codes(self):
|
||||
yield from super().get_error_message_codes()
|
||||
if self.maxlength:
|
||||
yield 'too_long'
|
||||
|
||||
def render_content(self):
|
||||
attrs = {'id': 'form_' + self.get_name_for_id()}
|
||||
if self.required:
|
||||
attrs['required'] = 'required'
|
||||
attrs['aria-required'] = 'true'
|
||||
if getattr(self, 'prefill_attributes', None) and 'autocomplete' in self.prefill_attributes:
|
||||
attrs['autocomplete'] = self.prefill_attributes['autocomplete']
|
||||
|
@ -627,12 +678,23 @@ class TextWidget(QuixoteTextWidget):
|
|||
maxlength = 0
|
||||
if maxlength:
|
||||
if len(self.value) > maxlength:
|
||||
self.error = _('too many characters (limit is %d)') % maxlength
|
||||
self.set_error_code('too_long')
|
||||
if use_validation_function and self.validation_function:
|
||||
try:
|
||||
self.validation_function(self.value)
|
||||
except ValueError as e:
|
||||
self.error = str(e)
|
||||
self.set_error(str(e))
|
||||
|
||||
def get_too_long_message(self):
|
||||
try:
|
||||
maxlength = int(self.attrs.get('maxlength', 0))
|
||||
except (TypeError, ValueError):
|
||||
maxlength = 0
|
||||
return _('too many characters (limit is %d)') % maxlength
|
||||
|
||||
def get_error_message_codes(self):
|
||||
yield from super().get_error_message_codes()
|
||||
yield 'too_long'
|
||||
|
||||
def render_content(self):
|
||||
attrs = {'id': 'form_' + self.get_name_for_id()}
|
||||
|
@ -867,7 +929,7 @@ class FileWithPreviewWidget(CompositeWidget):
|
|||
try:
|
||||
token = get_session().add_tempfile(self.get('file'), storage=self.storage)['token']
|
||||
except UploadStorageError:
|
||||
self.error = _('failed to store file (system error)')
|
||||
self.set_error(_('failed to store file (system error)'))
|
||||
return
|
||||
request.form[self.get_widget('token').get_name()] = token
|
||||
else:
|
||||
|
@ -886,7 +948,7 @@ class FileWithPreviewWidget(CompositeWidget):
|
|||
return
|
||||
|
||||
if self.storage and self.storage != self.storage:
|
||||
self.error = _('unknown storage system (system error)')
|
||||
self.set_error(_('unknown storage system (system error)'))
|
||||
return
|
||||
|
||||
# Don't trust the browser supplied MIME type, update the Upload object
|
||||
|
@ -922,7 +984,7 @@ class FileWithPreviewWidget(CompositeWidget):
|
|||
if self.max_file_size and hasattr(self.value, 'file_size'):
|
||||
# validate file size
|
||||
if self.value.file_size > self.max_file_size_bytes:
|
||||
self.error = _('over file size limit (%s)') % self.max_file_size
|
||||
self.set_error(_('over file size limit (%s)') % self.max_file_size)
|
||||
return
|
||||
|
||||
if self.file_type:
|
||||
|
@ -939,7 +1001,7 @@ class FileWithPreviewWidget(CompositeWidget):
|
|||
valid_file_type = True
|
||||
break
|
||||
if not valid_file_type:
|
||||
self.error = _('invalid file type')
|
||||
self.set_error(_('invalid file type'))
|
||||
|
||||
blacklisted_file_types = get_publisher().get_site_option('blacklisted-file-types')
|
||||
if blacklisted_file_types:
|
||||
|
@ -959,7 +1021,7 @@ class FileWithPreviewWidget(CompositeWidget):
|
|||
os.path.splitext(self.value.base_filename)[-1].lower() in blacklisted_file_types
|
||||
or filetype in blacklisted_file_types
|
||||
):
|
||||
self.error = _('forbidden file type')
|
||||
self.set_error(_('forbidden file type'))
|
||||
|
||||
|
||||
class EmailWidget(StringWidget):
|
||||
|
@ -988,42 +1050,45 @@ class EmailWidget(StringWidget):
|
|||
)
|
||||
)
|
||||
|
||||
def get_type_mismatch_message(self):
|
||||
return _('must be a valid email address')
|
||||
|
||||
def _parse(self, request):
|
||||
StringWidget._parse(self, request)
|
||||
if self.value is not None:
|
||||
# basic tests first
|
||||
if '@' not in self.value[1:-1]:
|
||||
self.error = _('must be a valid email address')
|
||||
self.set_error_code('type_mismatch')
|
||||
return
|
||||
if self.value[0] != '"' and ' ' in self.value:
|
||||
self.error = _('must be a valid email address')
|
||||
self.set_error_code('type_mismatch')
|
||||
return
|
||||
if self.value[0] != '"' and self.value.count('@') != 1:
|
||||
self.error = _('must be a valid email address')
|
||||
self.set_error_code('type_mismatch')
|
||||
return
|
||||
user_part, domain = self.value.rsplit('@', 1)
|
||||
if not self.user_part_re.match(user_part):
|
||||
self.error = _('must be a valid email address')
|
||||
self.set_error_code('type_mismatch')
|
||||
return
|
||||
if get_cfg('emails', {}).get('check_domain_with_dns', True):
|
||||
# testing for domain existence
|
||||
if [x for x in domain.split('.') if not x]:
|
||||
# empty parts in domain, ex: @example..net, or
|
||||
# @.example.net
|
||||
self.error = _('invalid address domain')
|
||||
self.set_error(_('invalid address domain'))
|
||||
return
|
||||
domain = force_str(domain, 'utf-8', errors='ignore')
|
||||
try:
|
||||
domain = force_str(domain.encode('idna'))
|
||||
except UnicodeError:
|
||||
self.error = _('invalid address domain')
|
||||
self.set_error(_('invalid address domain'))
|
||||
return
|
||||
if domain == 'localhost':
|
||||
return
|
||||
try:
|
||||
dns.resolver.query(force_str(domain), 'MX')
|
||||
except dns.exception.DNSException:
|
||||
self.error = _('invalid address domain')
|
||||
self.set_error(_('invalid address domain'))
|
||||
|
||||
|
||||
class OptGroup:
|
||||
|
@ -1373,13 +1438,18 @@ class WcsExtraStringWidget(StringWidget):
|
|||
self.inputmode = ValidationWidget.get_html_inputmode(self.field.validation)
|
||||
return super().render_content()
|
||||
|
||||
def get_bad_input_message(self):
|
||||
validation_function_error_message = self.validation_function_error_message
|
||||
if self.field and self.field.validation:
|
||||
validation_function_error_message = ValidationWidget.get_validation_error_message(
|
||||
self.field.validation
|
||||
)
|
||||
return validation_function_error_message or _('invalid value')
|
||||
|
||||
def _parse(self, request):
|
||||
StringWidget._parse(self, request)
|
||||
if self.field and self.field.validation and self.value is not None:
|
||||
self.validation_function = ValidationWidget.get_validation_function(self.field.validation)
|
||||
self.validation_function_error_message = ValidationWidget.get_validation_error_message(
|
||||
self.field.validation
|
||||
)
|
||||
|
||||
normalized_value = self.value
|
||||
if self.field and self.value and self.field.validation:
|
||||
|
@ -1387,7 +1457,7 @@ class WcsExtraStringWidget(StringWidget):
|
|||
normalized_value = normalize(self.value)
|
||||
|
||||
if self.value and self.validation_function and not self.validation_function(normalized_value):
|
||||
self.error = self.validation_function_error_message or _('invalid value')
|
||||
self.set_error_code('bad_input')
|
||||
|
||||
if self.field and self.value and not self.error and self.field.validation:
|
||||
self.value = normalized_value
|
||||
|
@ -1444,6 +1514,9 @@ class DateWidget(StringWidget):
|
|||
def get_format_string(cls):
|
||||
return misc.date_format()
|
||||
|
||||
def get_type_mismatch_message(self):
|
||||
return _('invalid date')
|
||||
|
||||
def _parse(self, request):
|
||||
StringWidget._parse(self, request)
|
||||
if self.value is not None:
|
||||
|
@ -1451,19 +1524,21 @@ class DateWidget(StringWidget):
|
|||
value = misc.get_as_datetime(self.value).timetuple()
|
||||
self.value = strftime(self.get_format_string(), value)
|
||||
except ValueError:
|
||||
self.error = _('invalid date')
|
||||
self.set_error_code('type_mismatch')
|
||||
self.value = None
|
||||
return
|
||||
if value[0] < 1500 or value[0] > 2099:
|
||||
self.error = _('invalid date')
|
||||
self.set_error_code('type_mismatch')
|
||||
self.value = None
|
||||
elif self.minimum_date and value[:3] < self.minimum_date.timetuple()[:3]:
|
||||
self.error = _('invalid date: date must be on or after %s') % strftime(
|
||||
misc.date_format(), self.minimum_date
|
||||
self.set_error(
|
||||
_('invalid date: date must be on or after %s')
|
||||
% strftime(misc.date_format(), self.minimum_date)
|
||||
)
|
||||
elif self.maximum_date and value[:3] > self.maximum_date.timetuple()[:3]:
|
||||
self.error = _('invalid date; date must be on or before %s') % strftime(
|
||||
misc.date_format(), self.maximum_date
|
||||
self.set_error(
|
||||
_('invalid date; date must be on or before %s')
|
||||
% strftime(misc.date_format(), self.maximum_date)
|
||||
)
|
||||
|
||||
def add_media(self):
|
||||
|
@ -1522,7 +1597,7 @@ class TimeWidget(DateWidget):
|
|||
value = datetime.datetime.strptime(self.value, self.get_format_string())
|
||||
self.value = strftime(self.get_format_string(), value)
|
||||
except ValueError:
|
||||
self.error = _('invalid time')
|
||||
self.set_error(_('invalid time'))
|
||||
self.value = None
|
||||
return
|
||||
|
||||
|
@ -1558,7 +1633,7 @@ class DateTimeWidget(CompositeWidget):
|
|||
try:
|
||||
misc.get_as_datetime('%s %s' % (date, time))
|
||||
except ValueError:
|
||||
self.error = _('invalid value')
|
||||
self.set_error(_('invalid value'))
|
||||
self.value = '%s %s' % (date, time)
|
||||
return self.value
|
||||
|
||||
|
@ -1572,7 +1647,7 @@ class RegexStringWidget(StringWidget):
|
|||
try:
|
||||
re.compile(self.value)
|
||||
except Exception:
|
||||
self.error = _('invalid regular expression')
|
||||
self.set_error(_('invalid regular expression'))
|
||||
self.value = None
|
||||
|
||||
|
||||
|
@ -1627,9 +1702,15 @@ class CheckboxesWidget(Widget):
|
|||
if self.required and not self.value:
|
||||
self.set_error(self.REQUIRED_ERROR)
|
||||
if self.value and self.min_choices and len(self.value) < self.min_choices:
|
||||
self.set_error(_('You must select at least %d answers.') % self.min_choices)
|
||||
self.set_error_code('too_short')
|
||||
if self.value and self.max_choices and len(self.value) > self.max_choices:
|
||||
self.set_error(_('You must select at most %d answers.') % self.max_choices)
|
||||
self.set_error_code('too_long')
|
||||
|
||||
def get_too_short_message(self):
|
||||
return _('You must select at least %d answers.') % self.min_choices
|
||||
|
||||
def get_too_long_message(self):
|
||||
return _('You must select at most %d answers.') % self.max_choices
|
||||
|
||||
def set_value(self, value):
|
||||
self.value = value
|
||||
|
@ -1657,7 +1738,7 @@ class ValidatedStringWidget(StringWidget):
|
|||
if self.regex and self.value is not None:
|
||||
match = re.match(self.regex, self.value)
|
||||
if not match or not match.group() == self.value:
|
||||
self.error = _('wrong format')
|
||||
self.set_error(_('wrong format'))
|
||||
|
||||
|
||||
class UrlWidget(ValidatedStringWidget):
|
||||
|
@ -1668,7 +1749,7 @@ class UrlWidget(ValidatedStringWidget):
|
|||
def _parse(self, request):
|
||||
ValidatedStringWidget._parse(self, request)
|
||||
if self.error:
|
||||
self.error = _('must start with http:// or https:// and have a domain name')
|
||||
self.set_error(_('must start with http:// or https:// and have a domain name'))
|
||||
|
||||
|
||||
class VarnameWidget(ValidatedStringWidget):
|
||||
|
@ -1680,7 +1761,7 @@ class VarnameWidget(ValidatedStringWidget):
|
|||
def _parse(self, request):
|
||||
ValidatedStringWidget._parse(self, request)
|
||||
if self.error:
|
||||
self.error = _('must only consist of letters, numbers, or underscore')
|
||||
self.set_error(_('must only consist of letters, numbers, or underscore'))
|
||||
# forbid id/text to be used as identifier, as they would clash against
|
||||
# "native" id/text keys in datasources; forbid "status" to avoid status
|
||||
# filtering being diverted to a form field.
|
||||
|
@ -1711,7 +1792,7 @@ class SlugWidget(ValidatedStringWidget):
|
|||
def _parse(self, request):
|
||||
super()._parse(request)
|
||||
if self.error:
|
||||
self.error = _('wrong format: must only consist of letters, numbers, dashes, or underscores')
|
||||
self.set_error(_('wrong format: must only consist of letters, numbers, dashes, or underscores'))
|
||||
|
||||
|
||||
class FileSizeWidget(ValidatedStringWidget):
|
||||
|
@ -1744,7 +1825,7 @@ class FileSizeWidget(ValidatedStringWidget):
|
|||
def _parse(self, request):
|
||||
ValidatedStringWidget._parse(self, request)
|
||||
if self.error:
|
||||
self.error = _('invalid file size')
|
||||
self.set_error(_('invalid file size'))
|
||||
|
||||
|
||||
class CaptchaWidget(CompositeWidget):
|
||||
|
@ -1794,7 +1875,7 @@ class CaptchaWidget(CompositeWidget):
|
|||
get_session().won_captcha = True
|
||||
self.value = v
|
||||
elif v['answer']:
|
||||
self.error = _('wrong answer')
|
||||
self.set_error(_('wrong answer'))
|
||||
|
||||
def get_title(self):
|
||||
return self.question
|
||||
|
@ -1885,7 +1966,14 @@ class WidgetList(quixote.form.widget.WidgetList):
|
|||
def _parse(self, request):
|
||||
super()._parse(request)
|
||||
if self.max_items and self.value and len(self.value) > self.max_items:
|
||||
self.set_error(_('Too many elements (maximum: %s)') % self.max_items)
|
||||
self.set_error_code('too_many')
|
||||
|
||||
def get_too_many_message(self):
|
||||
return _('Too many elements (maximum: %s)') % self.max_items
|
||||
|
||||
def get_error_message_codes(self):
|
||||
yield from super().get_error_message_codes()
|
||||
yield 'too_many'
|
||||
|
||||
def set_value(self, value):
|
||||
for dummy in range(len(value) - len(self.element_names)):
|
||||
|
@ -2375,7 +2463,7 @@ class WysiwygTextWidget(TextWidget):
|
|||
try:
|
||||
self.validation_function(self.value)
|
||||
except ValueError as e:
|
||||
self.error = str(e)
|
||||
self.set_error(str(e))
|
||||
if self.value == '':
|
||||
self.value = None
|
||||
|
||||
|
@ -2623,9 +2711,20 @@ class MultiSelectWidget(MultipleSelectWidget):
|
|||
finally:
|
||||
self.name = orig_name
|
||||
if self.value and self.min_choices and len(self.value) < self.min_choices:
|
||||
self.set_error(_('You must select at least %d choices.') % self.min_choices)
|
||||
self.set_error_code('too_short')
|
||||
if self.value and self.max_choices and len(self.value) > self.max_choices:
|
||||
self.set_error(_('You must select at most %d choices.') % self.max_choices)
|
||||
self.set_error_code('too_long')
|
||||
|
||||
def get_too_short_message(self):
|
||||
return _('You must select at least %d choices.') % self.min_choices
|
||||
|
||||
def get_too_long_message(self):
|
||||
return _('You must select at most %d choices.') % self.max_choices
|
||||
|
||||
def get_error_message_codes(self):
|
||||
yield from super().get_error_message_codes()
|
||||
yield 'too_short'
|
||||
yield 'too_long'
|
||||
|
||||
|
||||
class WidgetListAsTable(WidgetList):
|
||||
|
@ -2823,9 +2922,16 @@ class RankedItemsWidget(CompositeWidget):
|
|||
if value is not None:
|
||||
values[val] = value
|
||||
if value is not None and not isinstance(value, int):
|
||||
self.get_widget(key).set_error(IntWidget.TYPE_ERROR)
|
||||
self.get_widget(key).set_error_code('type_mismatch')
|
||||
self.value = values or None
|
||||
|
||||
def get_type_mismatch_message(self):
|
||||
return _('must be a number')
|
||||
|
||||
def get_error_message_codes(self):
|
||||
yield from super().get_error_message_codes()
|
||||
yield 'type_mismatch'
|
||||
|
||||
def set_value(self, value):
|
||||
self.value = value
|
||||
if value:
|
||||
|
@ -3282,7 +3388,7 @@ class MapWidget(CompositeWidget):
|
|||
try:
|
||||
lat, lon = self.value.split(';')
|
||||
except ValueError:
|
||||
self.set_error(_('Invalid value'))
|
||||
self.set_error_code('bad_input')
|
||||
else:
|
||||
lat_lon = misc.normalize_geolocation({'lat': lat, 'lon': lon})
|
||||
self.value = '%s;%s' % (lat_lon['lat'], lat_lon['lon']) if lat_lon else None
|
||||
|
|
|
@ -6,7 +6,12 @@
|
|||
{% block widget-attrs %}
|
||||
{% if widget.is_hidden %}style="display: none"{% endif %}
|
||||
{% if widget.field %}data-field-id="{{ widget.field.id }}"{% endif %}
|
||||
{% if not widget.readonly %}
|
||||
{% if widget.field.validation.type %}data-validation-type="{{ widget.field.validation.type }}"{% endif %}
|
||||
{% if widget.supports_live_validation %}data-supports-live-validation="true"{% endif %}
|
||||
{% endif %}
|
||||
data-widget-name="{{ widget.name }}"
|
||||
data-widget-name-for-id="{{ widget.get_name_for_id }}"
|
||||
{% if widget.div_id %}id="{{widget.div_id}}" data-valuecontainerid="form_{{widget.name}}"{% endif %}
|
||||
{% if widget.a11y_role %}role="{{ a11y_role }}"{% endif %}
|
||||
{% if widget.a11y_labelledby %}aria-labelledby="form_label_{{widget.name}}"{% endif %}
|
||||
|
@ -45,4 +50,11 @@
|
|||
{% if widget.render_br %}
|
||||
<br class="content {{widget.content.content_extra_css_class}}">
|
||||
{% endif %}
|
||||
{% if widget.supports_live_validation and not widget.readonly %}
|
||||
{% block widget-error-templates %}
|
||||
{% for error_message in widget.get_error_messages %}
|
||||
<template id="error_{{ widget.get_name_for_id }}_{{ error_message.camel_code }}"><p>{{ error_message.message }}</p></template>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
Il y a toute une série de changements de ce type dans les tests, c'est parce que l'HTML incorpore désormais un
<template>
avec dedans une classe "error", qui se trouvait donc matchée.