Js: add live field validation (#75724) #240
|
@ -876,6 +876,7 @@ class WidgetField(Field):
|
||||||
prefill = {}
|
prefill = {}
|
||||||
|
|
||||||
widget_class = None
|
widget_class = None
|
||||||
|
widget_supports_check_condition = True
|
||||||
|
|
||||||
def add_to_form(self, form, value=None):
|
def add_to_form(self, form, value=None):
|
||||||
kwargs = {'required': self.required, 'render_br': False}
|
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)
|
form.add(self.widget_class, 'f%s' % self.id, title=self.label, hint=hint, **kwargs)
|
||||||
widget = form.get_widget('f%s' % self.id)
|
widget = form.get_widget('f%s' % self.id)
|
||||||
widget.field = self
|
widget.field = self
|
||||||
|
widget.supports_check_condition = self.widget_supports_check_condition
|
||||||
if self.extra_css_class:
|
if self.extra_css_class:
|
||||||
if hasattr(widget, 'extra_css_class') and widget.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)
|
widget.extra_css_class = '%s %s' % (widget.extra_css_class, self.extra_css_class)
|
||||||
|
@ -1641,6 +1643,7 @@ class FileField(WidgetField):
|
||||||
storage = 'default'
|
storage = 'default'
|
||||||
|
|
||||||
widget_class = FileWithPreviewWidget
|
widget_class = FileWithPreviewWidget
|
||||||
|
widget_supports_check_condition = True
|
||||||
extra_attributes = [
|
extra_attributes = [
|
||||||
'file_type',
|
'file_type',
|
||||||
'max_file_size',
|
'max_file_size',
|
||||||
|
@ -3144,6 +3147,7 @@ class TableField(WidgetField):
|
||||||
columns = None
|
columns = None
|
||||||
|
|
||||||
widget_class = TableWidget
|
widget_class = TableWidget
|
||||||
|
widget_supports_check_condition = True
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.rows = []
|
self.rows = []
|
||||||
|
@ -3365,6 +3369,7 @@ class TableRowsField(WidgetField):
|
||||||
columns = None
|
columns = None
|
||||||
|
|
||||||
widget_class = TableListRowsWidget
|
widget_class = TableListRowsWidget
|
||||||
|
widget_supports_check_condition = True
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.columns = []
|
self.columns = []
|
||||||
|
@ -3626,6 +3631,7 @@ class RankedItemsField(WidgetField):
|
||||||
items = []
|
items = []
|
||||||
randomize_items = False
|
randomize_items = False
|
||||||
widget_class = RankedItemsWidget
|
widget_class = RankedItemsWidget
|
||||||
|
widget_supports_check_condition = True
|
||||||
anonymise = False
|
anonymise = False
|
||||||
|
|
||||||
def perform_more_widget_changes(self, form, kwargs, edit=True):
|
def perform_more_widget_changes(self, form, kwargs, edit=True):
|
||||||
|
@ -3898,7 +3904,9 @@ class BlockField(WidgetField):
|
||||||
self.block
|
self.block
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise MissingBlockFieldError(self.type[6:])
|
raise MissingBlockFieldError(self.type[6:])
|
||||||
return super().add_to_form(form, value=value)
|
widget = super().add_to_form(form, value=value)
|
||||||
|
widget.supports_check_condition = False
|
||||||
|
return widget
|
||||||
|
|
||||||
def fill_admin_form(self, form):
|
def fill_admin_form(self, form):
|
||||||
super().fill_admin_form(form)
|
super().fill_admin_form(form)
|
||||||
|
|
|
@ -270,6 +270,7 @@ class FormPage(FormdefDirectoryBase, FormTemplateMixin):
|
||||||
'code',
|
'code',
|
||||||
'removedraft',
|
'removedraft',
|
||||||
'live',
|
'live',
|
||||||
|
'check_condition',
|
||||||
('go-to-backoffice', 'go_to_backoffice'),
|
('go-to-backoffice', 'go_to_backoffice'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -929,6 +930,16 @@ class FormPage(FormdefDirectoryBase, FormTemplateMixin):
|
||||||
def create_form(self, *args, **kwargs):
|
def create_form(self, *args, **kwargs):
|
||||||
form = self.formdef.create_form(*args, **kwargs)
|
form = self.formdef.create_form(*args, **kwargs)
|
||||||
form.attrs['data-live-url'] = self.formdef.get_url(language=get_publisher().current_language) + 'live'
|
form.attrs['data-live-url'] = self.formdef.get_url(language=get_publisher().current_language) + 'live'
|
||||||
|
form.attrs['data-check-condition-url'] = (
|
||||||
|
self.formdef.get_url(language=get_publisher().current_language) + 'check_condition'
|
||||||
|
)
|
||||||
|
form.widgets.append(
|
||||||
|
HtmlWidget(
|
||||||
|
'''<template id="form_error_tpl">
|
||||||
|
<div id="form_error_fieldname" class="error"></div>
|
||||||
|
</template>'''
|
||||||
|
)
|
||||||
|
)
|
||||||
return form
|
return form
|
||||||
|
|
||||||
def create_view_form(self, *args, **kwargs):
|
def create_view_form(self, *args, **kwargs):
|
||||||
|
@ -1656,6 +1667,47 @@ class FormPage(FormdefDirectoryBase, FormTemplateMixin):
|
||||||
return result_error('form deserialization failed: %s' % e)
|
return result_error('form deserialization failed: %s' % e)
|
||||||
return FormStatusPage.live_process_fields(form, formdata, displayed_fields)
|
return FormStatusPage.live_process_fields(form, formdata, displayed_fields)
|
||||||
|
|
||||||
|
def check_condition(self):
|
||||||
|
get_request().ignore_session = True
|
||||||
|
get_response().set_content_type('application/json')
|
||||||
|
|
||||||
|
def result_error(reason):
|
||||||
|
return json.dumps({'result': 'error', 'reason': reason})
|
||||||
|
|
||||||
|
session = get_session()
|
||||||
|
if not (session and session.id):
|
||||||
|
return result_error('missing session')
|
||||||
|
|
||||||
|
field_ref = get_request().form.get('field')
|
||||||
|
parts = field_ref.split('__')
|
||||||
|
for field in self.formdef.fields:
|
||||||
|
if 'f%s' % field.id == parts[0]:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return result_error('missing field')
|
||||||
|
if len(parts) == 3: # block field
|
||||||
|
for subfield in field.block.fields:
|
||||||
|
if 'f%s' % subfield.id == parts[2]:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return result_error('missing sub field')
|
||||||
|
field = subfield
|
||||||
|
field.id = field_ref[1:].replace('__', '$')
|
||||||
|
|
||||||
|
form = Form()
|
||||||
|
widget = field.add_to_form(form)
|
||||||
|
widget.parse()
|
||||||
|
if widget.has_error():
|
||||||
|
resp = {'err': 1, 'msg': str(widget.get_error())}
|
||||||
|
if hasattr(widget, 'error_code'):
|
||||||
|
from wcs.qommon.form import ErrorMessage
|
||||||
|
|
||||||
|
error_message = ErrorMessage(widget.error_code, '')
|
||||||
|
resp[error_message.camel_code()] = True
|
||||||
|
return json.dumps(resp)
|
||||||
|
else:
|
||||||
|
return json.dumps({'err': 0})
|
||||||
|
|
||||||
def clean_submission_context(self):
|
def clean_submission_context(self):
|
||||||
get_publisher().substitutions.unfeed(lambda x: x.__class__.__name__ == 'ConditionVars')
|
get_publisher().substitutions.unfeed(lambda x: x.__class__.__name__ == 'ConditionVars')
|
||||||
get_publisher().substitutions.unfeed(lambda x: isinstance(x, FormData))
|
get_publisher().substitutions.unfeed(lambda x: isinstance(x, FormData))
|
||||||
|
@ -2226,7 +2278,7 @@ class RootDirectory(AccessControlled, Directory):
|
||||||
|
|
||||||
|
|
||||||
class PublicFormStatusPage(FormStatusPage):
|
class PublicFormStatusPage(FormStatusPage):
|
||||||
_q_exports_orig = ['', 'download', 'status', 'live', 'tempfile']
|
_q_exports_orig = ['', 'download', 'status', 'live', 'tempfile', 'check_condition']
|
||||||
form_page_class = FormPage
|
form_page_class = FormPage
|
||||||
history_templates = ['wcs/front/formdata_history.html', 'wcs/formdata_history.html']
|
history_templates = ['wcs/front/formdata_history.html', 'wcs/formdata_history.html']
|
||||||
status_templates = ['wcs/front/formdata_status.html', 'wcs/formdata_status.html']
|
status_templates = ['wcs/front/formdata_status.html', 'wcs/formdata_status.html']
|
||||||
|
|
|
@ -176,6 +176,39 @@ def widget_get_name_for_id(self):
|
||||||
return self.name.replace('$', '__')
|
return self.name.replace('$', '__')
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorMessage:
|
||||||
|
def __init__(self, code, label):
|
||||||
|
self.code = code
|
||||||
|
self.label = label
|
||||||
|
|
||||||
|
def camel_code(self):
|
||||||
|
return ''.join(x.lower() if i == 0 else x.capitalize() for i, x in enumerate(self.code.split('_')))
|
||||||
|
|
||||||
|
|
||||||
|
def widget_set_error(self, error=None, error_code=None):
|
||||||
|
if error and error_code:
|
||||||
|
self.error = error
|
||||||
|
self.error_code = error_code
|
||||||
|
elif error_code:
|
||||||
|
self.error = getattr(self, 'get_%s_message' % error_code)()
|
||||||
|
self.error_code = error_code
|
||||||
|
else:
|
||||||
|
self.error = error
|
||||||
|
if self.error == Widget.REQUIRED_ERROR:
|
||||||
|
self.error_code = 'value_missing'
|
||||||
|
else:
|
||||||
|
self.error_code = 'invalid_value'
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
yield 'value_missing'
|
||||||
|
|
||||||
|
|
||||||
Widget.render = render
|
Widget.render = render
|
||||||
Widget.cleanup = None
|
Widget.cleanup = None
|
||||||
Widget.render_error = render_error
|
Widget.render_error = render_error
|
||||||
|
@ -184,6 +217,11 @@ Widget.render_title = render_title
|
||||||
Widget.is_prefilled = is_prefilled
|
Widget.is_prefilled = is_prefilled
|
||||||
Widget.render_widget_content = render_widget_content
|
Widget.render_widget_content = render_widget_content
|
||||||
Widget.get_name_for_id = widget_get_name_for_id
|
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
|
||||||
|
Widget.get_invalid_value_message = lambda x: _('Invalid value')
|
||||||
|
Widget.set_error = widget_set_error
|
||||||
|
|
||||||
|
|
||||||
def file_render_content(self):
|
def file_render_content(self):
|
||||||
|
@ -578,16 +616,25 @@ class StringWidget(QuixoteStringWidget):
|
||||||
if self.value:
|
if self.value:
|
||||||
self.value = self.value.strip()
|
self.value = self.value.strip()
|
||||||
if self.maxlength and len(self.value) > self.maxlength:
|
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(error_code='too_long')
|
||||||
elif self.validation_function:
|
elif self.validation_function:
|
||||||
try:
|
try:
|
||||||
self.validation_function(self.value)
|
self.validation_function(self.value)
|
||||||
except ValueError as e:
|
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
|
||||||
|
|
||||||
|
def get_error_message_codes(self):
|
||||||
|
yield from super().get_error_message_codes()
|
||||||
|
if self.maxlength:
|
||||||
|
yield 'too_long'
|
||||||
|
|
||||||
def render_content(self):
|
def render_content(self):
|
||||||
attrs = {'id': 'form_' + self.get_name_for_id()}
|
attrs = {'id': 'form_' + self.get_name_for_id()}
|
||||||
if self.required:
|
if self.required:
|
||||||
|
attrs['required'] = 'required'
|
||||||
attrs['aria-required'] = 'true'
|
attrs['aria-required'] = 'true'
|
||||||
if getattr(self, 'prefill_attributes', None) and 'autocomplete' in self.prefill_attributes:
|
if getattr(self, 'prefill_attributes', None) and 'autocomplete' in self.prefill_attributes:
|
||||||
attrs['autocomplete'] = self.prefill_attributes['autocomplete']
|
attrs['autocomplete'] = self.prefill_attributes['autocomplete']
|
||||||
|
@ -634,12 +681,23 @@ class TextWidget(QuixoteTextWidget):
|
||||||
maxlength = 0
|
maxlength = 0
|
||||||
if maxlength:
|
if maxlength:
|
||||||
if len(self.value) > maxlength:
|
if len(self.value) > maxlength:
|
||||||
self.error = _('too many characters (limit is %d)') % maxlength
|
self.set_error('too_long')
|
||||||
if use_validation_function and self.validation_function:
|
if use_validation_function and self.validation_function:
|
||||||
try:
|
try:
|
||||||
self.validation_function(self.value)
|
self.validation_function(self.value)
|
||||||
except ValueError as e:
|
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):
|
def render_content(self):
|
||||||
attrs = {'id': 'form_' + self.get_name_for_id()}
|
attrs = {'id': 'form_' + self.get_name_for_id()}
|
||||||
|
@ -671,7 +729,7 @@ class CheckboxWidget(QuixoteCheckboxWidget):
|
||||||
def _parse(self, request):
|
def _parse(self, request):
|
||||||
self.value = self.name in request.form and not request.form[self.name] in (False, '', 'False')
|
self.value = self.name in request.form and not request.form[self.name] in (False, '', 'False')
|
||||||
if self.required and not self.value:
|
if self.required and not self.value:
|
||||||
self.set_error(self.REQUIRED_ERROR)
|
self.set_error(error_code='value_missing')
|
||||||
|
|
||||||
def set_value(self, value):
|
def set_value(self, value):
|
||||||
if value in (None, False, '', 'False'):
|
if value in (None, False, '', 'False'):
|
||||||
|
@ -874,7 +932,7 @@ class FileWithPreviewWidget(CompositeWidget):
|
||||||
try:
|
try:
|
||||||
token = get_session().add_tempfile(self.get('file'), storage=self.storage)['token']
|
token = get_session().add_tempfile(self.get('file'), storage=self.storage)['token']
|
||||||
except UploadStorageError:
|
except UploadStorageError:
|
||||||
self.error = _('failed to store file (system error)')
|
self.set_error(_('failed to store file (system error)'))
|
||||||
return
|
return
|
||||||
request.form[self.get_widget('token').get_name()] = token
|
request.form[self.get_widget('token').get_name()] = token
|
||||||
else:
|
else:
|
||||||
|
@ -893,7 +951,7 @@ class FileWithPreviewWidget(CompositeWidget):
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.storage and self.storage != self.storage:
|
if self.storage and self.storage != self.storage:
|
||||||
self.error = _('unknown storage system (system error)')
|
self.set_error(_('unknown storage system (system error)'))
|
||||||
return
|
return
|
||||||
|
|
||||||
# Don't trust the browser supplied MIME type, update the Upload object
|
# Don't trust the browser supplied MIME type, update the Upload object
|
||||||
|
@ -929,7 +987,7 @@ class FileWithPreviewWidget(CompositeWidget):
|
||||||
if self.max_file_size and hasattr(self.value, 'file_size'):
|
if self.max_file_size and hasattr(self.value, 'file_size'):
|
||||||
# validate file size
|
# validate file size
|
||||||
if self.value.file_size > self.max_file_size_bytes:
|
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
|
return
|
||||||
|
|
||||||
if self.file_type:
|
if self.file_type:
|
||||||
|
@ -946,7 +1004,7 @@ class FileWithPreviewWidget(CompositeWidget):
|
||||||
valid_file_type = True
|
valid_file_type = True
|
||||||
break
|
break
|
||||||
if not valid_file_type:
|
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')
|
blacklisted_file_types = get_publisher().get_site_option('blacklisted-file-types')
|
||||||
if blacklisted_file_types:
|
if blacklisted_file_types:
|
||||||
|
@ -966,7 +1024,7 @@ class FileWithPreviewWidget(CompositeWidget):
|
||||||
os.path.splitext(self.value.base_filename)[-1].lower() in blacklisted_file_types
|
os.path.splitext(self.value.base_filename)[-1].lower() in blacklisted_file_types
|
||||||
or filetype in blacklisted_file_types
|
or filetype in blacklisted_file_types
|
||||||
):
|
):
|
||||||
self.error = _('forbidden file type')
|
self.set_error(_('forbidden file type'))
|
||||||
|
|
||||||
|
|
||||||
class EmailWidget(StringWidget):
|
class EmailWidget(StringWidget):
|
||||||
|
@ -1000,37 +1058,37 @@ class EmailWidget(StringWidget):
|
||||||
if self.value is not None:
|
if self.value is not None:
|
||||||
# basic tests first
|
# basic tests first
|
||||||
if '@' not in self.value[1:-1]:
|
if '@' not in self.value[1:-1]:
|
||||||
self.error = _('must be a valid email address')
|
self.set_error(_('must be a valid email address'))
|
||||||
return
|
return
|
||||||
if self.value[0] != '"' and ' ' in self.value:
|
if self.value[0] != '"' and ' ' in self.value:
|
||||||
self.error = _('must be a valid email address')
|
self.set_error(_('must be a valid email address'))
|
||||||
return
|
return
|
||||||
if self.value[0] != '"' and self.value.count('@') != 1:
|
if self.value[0] != '"' and self.value.count('@') != 1:
|
||||||
self.error = _('must be a valid email address')
|
self.set_error(_('must be a valid email address'))
|
||||||
return
|
return
|
||||||
user_part, domain = self.value.rsplit('@', 1)
|
user_part, domain = self.value.rsplit('@', 1)
|
||||||
if not self.user_part_re.match(user_part):
|
if not self.user_part_re.match(user_part):
|
||||||
self.error = _('must be a valid email address')
|
self.set_error(_('must be a valid email address'))
|
||||||
return
|
return
|
||||||
if get_cfg('emails', {}).get('check_domain_with_dns', True):
|
if get_cfg('emails', {}).get('check_domain_with_dns', True):
|
||||||
# testing for domain existence
|
# testing for domain existence
|
||||||
if [x for x in domain.split('.') if not x]:
|
if [x for x in domain.split('.') if not x]:
|
||||||
# empty parts in domain, ex: @example..net, or
|
# empty parts in domain, ex: @example..net, or
|
||||||
# @.example.net
|
# @.example.net
|
||||||
self.error = _('invalid address domain')
|
self.set_error(_('invalid address domain'))
|
||||||
return
|
return
|
||||||
domain = force_str(domain, 'utf-8', errors='ignore')
|
domain = force_str(domain, 'utf-8', errors='ignore')
|
||||||
try:
|
try:
|
||||||
domain = force_str(domain.encode('idna'))
|
domain = force_str(domain.encode('idna'))
|
||||||
except UnicodeError:
|
except UnicodeError:
|
||||||
self.error = _('invalid address domain')
|
self.set_error(_('invalid address domain'))
|
||||||
return
|
return
|
||||||
if domain == 'localhost':
|
if domain == 'localhost':
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
dns.resolver.query(force_str(domain), 'MX')
|
dns.resolver.query(force_str(domain), 'MX')
|
||||||
except dns.exception.DNSException:
|
except dns.exception.DNSException:
|
||||||
self.error = _('invalid address domain')
|
self.set_error(_('invalid address domain'))
|
||||||
|
|
||||||
|
|
||||||
class OptGroup:
|
class OptGroup:
|
||||||
|
@ -1368,7 +1426,6 @@ class WcsExtraStringWidget(StringWidget):
|
||||||
prefill = False
|
prefill = False
|
||||||
prefill_attributes = None
|
prefill_attributes = None
|
||||||
validation_function = None
|
validation_function = None
|
||||||
validation_function_error_message = None
|
|
||||||
|
|
||||||
def add_media(self):
|
def add_media(self):
|
||||||
if self.prefill_attributes and 'geolocation' in self.prefill_attributes:
|
if self.prefill_attributes and 'geolocation' in self.prefill_attributes:
|
||||||
|
@ -1380,13 +1437,22 @@ class WcsExtraStringWidget(StringWidget):
|
||||||
self.inputmode = ValidationWidget.get_html_inputmode(self.field.validation)
|
self.inputmode = ValidationWidget.get_html_inputmode(self.field.validation)
|
||||||
return super().render_content()
|
return super().render_content()
|
||||||
|
|
||||||
|
def get_error_message_codes(self):
|
||||||
|
yield from super().get_error_message_codes()
|
||||||
|
yield 'invalid_value'
|
||||||
|
|
||||||
|
def get_invalid_value_message(self):
|
||||||
|
validation_function_error_message = None
|
||||||
|
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):
|
def _parse(self, request):
|
||||||
StringWidget._parse(self, request)
|
StringWidget._parse(self, request)
|
||||||
if self.field and self.field.validation and self.value is not None:
|
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 = ValidationWidget.get_validation_function(self.field.validation)
|
||||||
self.validation_function_error_message = ValidationWidget.get_validation_error_message(
|
|
||||||
self.field.validation
|
|
||||||
)
|
|
||||||
|
|
||||||
normalized_value = self.value
|
normalized_value = self.value
|
||||||
if self.field and self.value and self.field.validation:
|
if self.field and self.value and self.field.validation:
|
||||||
|
@ -1394,7 +1460,7 @@ class WcsExtraStringWidget(StringWidget):
|
||||||
normalized_value = normalize(self.value)
|
normalized_value = normalize(self.value)
|
||||||
|
|
||||||
if self.value and self.validation_function and not self.validation_function(normalized_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(error_code='invalid_value')
|
||||||
|
|
||||||
if self.field and self.value and not self.error and self.field.validation:
|
if self.field and self.value and not self.error and self.field.validation:
|
||||||
self.value = normalized_value
|
self.value = normalized_value
|
||||||
|
@ -1458,19 +1524,21 @@ class DateWidget(StringWidget):
|
||||||
value = misc.get_as_datetime(self.value).timetuple()
|
value = misc.get_as_datetime(self.value).timetuple()
|
||||||
self.value = strftime(self.get_format_string(), value)
|
self.value = strftime(self.get_format_string(), value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.error = _('invalid date')
|
self.set_error(_('invalid date'))
|
||||||
self.value = None
|
self.value = None
|
||||||
return
|
return
|
||||||
if value[0] < 1500 or value[0] > 2099:
|
if value[0] < 1500 or value[0] > 2099:
|
||||||
self.error = _('invalid date')
|
self.set_error(_('invalid date'))
|
||||||
self.value = None
|
self.value = None
|
||||||
elif self.minimum_date and value[:3] < self.minimum_date.timetuple()[:3]:
|
elif self.minimum_date and value[:3] < self.minimum_date.timetuple()[:3]:
|
||||||
self.error = _('invalid date: date must be on or after %s') % strftime(
|
self.set_error(
|
||||||
misc.date_format(), self.minimum_date
|
_('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]:
|
elif self.maximum_date and value[:3] > self.maximum_date.timetuple()[:3]:
|
||||||
self.error = _('invalid date; date must be on or before %s') % strftime(
|
self.set_error(
|
||||||
misc.date_format(), self.maximum_date
|
_('invalid date; date must be on or before %s')
|
||||||
|
% strftime(misc.date_format(), self.maximum_date)
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_media(self):
|
def add_media(self):
|
||||||
|
@ -1529,7 +1597,7 @@ class TimeWidget(DateWidget):
|
||||||
value = datetime.datetime.strptime(self.value, self.get_format_string())
|
value = datetime.datetime.strptime(self.value, self.get_format_string())
|
||||||
self.value = strftime(self.get_format_string(), value)
|
self.value = strftime(self.get_format_string(), value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.error = _('invalid time')
|
self.set_error(_('invalid time'))
|
||||||
self.value = None
|
self.value = None
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -1565,7 +1633,7 @@ class DateTimeWidget(CompositeWidget):
|
||||||
try:
|
try:
|
||||||
misc.get_as_datetime('%s %s' % (date, time))
|
misc.get_as_datetime('%s %s' % (date, time))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.error = _('invalid value')
|
self.set_error(_('invalid value'))
|
||||||
self.value = '%s %s' % (date, time)
|
self.value = '%s %s' % (date, time)
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
|
@ -1579,7 +1647,7 @@ class RegexStringWidget(StringWidget):
|
||||||
try:
|
try:
|
||||||
re.compile(self.value)
|
re.compile(self.value)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.error = _('invalid regular expression')
|
self.set_error(_('invalid regular expression'))
|
||||||
self.value = None
|
self.value = None
|
||||||
|
|
||||||
|
|
||||||
|
@ -1632,11 +1700,17 @@ class CheckboxesWidget(Widget):
|
||||||
values.append(option['value'])
|
values.append(option['value'])
|
||||||
self.value = values
|
self.value = values
|
||||||
if self.required and not self.value:
|
if self.required and not self.value:
|
||||||
self.set_error(self.REQUIRED_ERROR)
|
self.set_error(error_code='value_missing')
|
||||||
if self.value and self.min_choices and len(self.value) < self.min_choices:
|
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(error_code='too_short')
|
||||||
if self.value and self.max_choices and len(self.value) > self.max_choices:
|
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(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):
|
def set_value(self, value):
|
||||||
self.value = value
|
self.value = value
|
||||||
|
@ -1664,7 +1738,7 @@ class ValidatedStringWidget(StringWidget):
|
||||||
if self.regex and self.value is not None:
|
if self.regex and self.value is not None:
|
||||||
match = re.match(self.regex, self.value)
|
match = re.match(self.regex, self.value)
|
||||||
if not match or not match.group() == self.value:
|
if not match or not match.group() == self.value:
|
||||||
self.error = _('wrong format')
|
self.set_error(_('wrong format'))
|
||||||
|
|
||||||
|
|
||||||
class UrlWidget(ValidatedStringWidget):
|
class UrlWidget(ValidatedStringWidget):
|
||||||
|
@ -1675,7 +1749,7 @@ class UrlWidget(ValidatedStringWidget):
|
||||||
def _parse(self, request):
|
def _parse(self, request):
|
||||||
ValidatedStringWidget._parse(self, request)
|
ValidatedStringWidget._parse(self, request)
|
||||||
if self.error:
|
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):
|
class VarnameWidget(ValidatedStringWidget):
|
||||||
|
@ -1687,7 +1761,7 @@ class VarnameWidget(ValidatedStringWidget):
|
||||||
def _parse(self, request):
|
def _parse(self, request):
|
||||||
ValidatedStringWidget._parse(self, request)
|
ValidatedStringWidget._parse(self, request)
|
||||||
if self.error:
|
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
|
# forbid id/text to be used as identifier, as they would clash against
|
||||||
# "native" id/text keys in datasources; forbid "status" to avoid status
|
# "native" id/text keys in datasources; forbid "status" to avoid status
|
||||||
# filtering being diverted to a form field.
|
# filtering being diverted to a form field.
|
||||||
|
@ -1733,7 +1807,7 @@ class VarnameWidget(ValidatedStringWidget):
|
||||||
'with',
|
'with',
|
||||||
'yield',
|
'yield',
|
||||||
):
|
):
|
||||||
self.error = _('this value is reserved for internal use.')
|
self.set_error(_('this value is reserved for internal use.'))
|
||||||
|
|
||||||
|
|
||||||
class SlugWidget(ValidatedStringWidget):
|
class SlugWidget(ValidatedStringWidget):
|
||||||
|
@ -1757,7 +1831,7 @@ class SlugWidget(ValidatedStringWidget):
|
||||||
def _parse(self, request):
|
def _parse(self, request):
|
||||||
super()._parse(request)
|
super()._parse(request)
|
||||||
if self.error:
|
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):
|
class FileSizeWidget(ValidatedStringWidget):
|
||||||
|
@ -1790,7 +1864,7 @@ class FileSizeWidget(ValidatedStringWidget):
|
||||||
def _parse(self, request):
|
def _parse(self, request):
|
||||||
ValidatedStringWidget._parse(self, request)
|
ValidatedStringWidget._parse(self, request)
|
||||||
if self.error:
|
if self.error:
|
||||||
self.error = _('invalid file size')
|
self.set_error(_('invalid file size'))
|
||||||
|
|
||||||
|
|
||||||
class CaptchaWidget(CompositeWidget):
|
class CaptchaWidget(CompositeWidget):
|
||||||
|
@ -1840,7 +1914,7 @@ class CaptchaWidget(CompositeWidget):
|
||||||
get_session().won_captcha = True
|
get_session().won_captcha = True
|
||||||
self.value = v
|
self.value = v
|
||||||
elif v['answer']:
|
elif v['answer']:
|
||||||
self.error = _('wrong answer')
|
self.set_error(_('wrong answer'))
|
||||||
|
|
||||||
def get_title(self):
|
def get_title(self):
|
||||||
return self.question
|
return self.question
|
||||||
|
@ -1931,7 +2005,14 @@ class WidgetList(quixote.form.widget.WidgetList):
|
||||||
def _parse(self, request):
|
def _parse(self, request):
|
||||||
super()._parse(request)
|
super()._parse(request)
|
||||||
if self.max_items and self.value and len(self.value) > self.max_items:
|
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(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):
|
def set_value(self, value):
|
||||||
for dummy in range(len(value) - len(self.element_names)):
|
for dummy in range(len(value) - len(self.element_names)):
|
||||||
|
@ -2421,7 +2502,7 @@ class WysiwygTextWidget(TextWidget):
|
||||||
try:
|
try:
|
||||||
self.validation_function(self.value)
|
self.validation_function(self.value)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
self.error = str(e)
|
self.set_error(str(e))
|
||||||
if self.value == '':
|
if self.value == '':
|
||||||
self.value = None
|
self.value = None
|
||||||
|
|
||||||
|
@ -2513,7 +2594,7 @@ class TableWidget(CompositeWidget):
|
||||||
request = get_request()
|
request = get_request()
|
||||||
if (request.form or request.get_method() == 'POST') and self.required:
|
if (request.form or request.get_method() == 'POST') and self.required:
|
||||||
if not self.value:
|
if not self.value:
|
||||||
self.set_error(self.REQUIRED_ERROR)
|
self.set_error(error_code='value_missing')
|
||||||
else:
|
else:
|
||||||
for row in self.value:
|
for row in self.value:
|
||||||
for column in row:
|
for column in row:
|
||||||
|
@ -2523,7 +2604,7 @@ class TableWidget(CompositeWidget):
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
self.set_error(self.REQUIRED_ERROR)
|
self.set_error(error_code='value_missing')
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
def _parse(self, request):
|
def _parse(self, request):
|
||||||
|
@ -2669,9 +2750,20 @@ class MultiSelectWidget(MultipleSelectWidget):
|
||||||
finally:
|
finally:
|
||||||
self.name = orig_name
|
self.name = orig_name
|
||||||
if self.value and self.min_choices and len(self.value) < self.min_choices:
|
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(error_code='too_short')
|
||||||
if self.value and self.max_choices and len(self.value) > self.max_choices:
|
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(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):
|
class WidgetListAsTable(WidgetList):
|
||||||
|
@ -2754,7 +2846,7 @@ class TableListRowsWidget(WidgetListAsTable):
|
||||||
add_element_pushed = self.get_widget('add_element').parse()
|
add_element_pushed = self.get_widget('add_element').parse()
|
||||||
if (request.form or request.get_method() == 'POST') and self.required:
|
if (request.form or request.get_method() == 'POST') and self.required:
|
||||||
if not self.value and not add_element_pushed:
|
if not self.value and not add_element_pushed:
|
||||||
self.set_error(self.REQUIRED_ERROR)
|
self.set_error(error_code='value_missing')
|
||||||
for row in self.value or []:
|
for row in self.value or []:
|
||||||
for column in row:
|
for column in row:
|
||||||
if column:
|
if column:
|
||||||
|
@ -2764,7 +2856,7 @@ class TableListRowsWidget(WidgetListAsTable):
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
if not add_element_pushed:
|
if not add_element_pushed:
|
||||||
self.set_error(self.REQUIRED_ERROR)
|
self.set_error(error_code='value_missing')
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
def _parse(self, request):
|
def _parse(self, request):
|
||||||
|
@ -2869,9 +2961,16 @@ class RankedItemsWidget(CompositeWidget):
|
||||||
if value is not None:
|
if value is not None:
|
||||||
values[val] = value
|
values[val] = value
|
||||||
if value is not None and not isinstance(value, int):
|
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(error_code='type_mismatch')
|
||||||
self.value = values or None
|
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):
|
def set_value(self, value):
|
||||||
self.value = value
|
self.value = value
|
||||||
if value:
|
if value:
|
||||||
|
@ -3328,11 +3427,15 @@ class MapWidget(CompositeWidget):
|
||||||
try:
|
try:
|
||||||
lat, lon = self.value.split(';')
|
lat, lon = self.value.split(';')
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.set_error(_('Invalid value'))
|
self.set_error(error_code='invalid_value')
|
||||||
else:
|
else:
|
||||||
lat_lon = misc.normalize_geolocation({'lat': lat, 'lon': lon})
|
lat_lon = misc.normalize_geolocation({'lat': lat, 'lon': lon})
|
||||||
self.value = '%s;%s' % (lat_lon['lat'], lat_lon['lon']) if lat_lon else None
|
self.value = '%s;%s' % (lat_lon['lat'], lat_lon['lon']) if lat_lon else None
|
||||||
|
|
||||||
|
def get_error_message_codes(self):
|
||||||
|
yield from super().get_error_message_codes()
|
||||||
|
yield 'invalid_value'
|
||||||
|
|
||||||
def set_value(self, value):
|
def set_value(self, value):
|
||||||
super().set_value(value)
|
super().set_value(value)
|
||||||
self.get_widget('latlng').set_value(value)
|
self.get_widget('latlng').set_value(value)
|
||||||
|
@ -3372,8 +3475,8 @@ class MapMarkerSelectionWidget(MapWidget):
|
||||||
|
|
||||||
|
|
||||||
class HiddenErrorWidget(HiddenWidget):
|
class HiddenErrorWidget(HiddenWidget):
|
||||||
def set_error(self, error):
|
def set_error(self, *args, **kwargs):
|
||||||
Widget.set_error(self, error)
|
Widget.set_error(self, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class SingleSelectWidgetWithOther(CompositeWidget):
|
class SingleSelectWidgetWithOther(CompositeWidget):
|
||||||
|
|
|
@ -756,3 +756,173 @@ $(function() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function check_condition(button, widget_id) {
|
||||||
|
var $form = $('form[data-check-condition-url]');
|
||||||
|
var form_content = $form.serialize();
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: $form.attr('data-check-condition-url') + '?field=' + widget_id,
|
||||||
|
|||||||
|
data: form_content,
|
||||||
|
success: function(json) {
|
||||||
|
if (json.err === 0) {
|
||||||
|
button.textContent = 'ok';
|
||||||
|
} else {
|
||||||
|
button.textContent = 'err: ' + json.msg;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Live Field Validation
|
||||||
|
*/
|
||||||
|
|
||||||
|
const LiveValidation = (function(){
|
||||||
|
|
||||||
|
const excludedField = function (field) {
|
||||||
|
if (field.disabled ) return true
|
||||||
|
const excludedType = [ 'button', 'reset', 'submit' ]
|
||||||
|
if (excludedType.includes(field.type)) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Check validy of field by HTML attributs
|
||||||
|
* cf JS constaint validation API
|
||||||
|
* return first error fouded
|
||||||
|
*/
|
||||||
|
const hasAttrError = function (field) {
|
||||||
|
const validityState = field.validity
|
||||||
|
if (validityState.valid) return
|
||||||
|
|
||||||
|
let errorType
|
||||||
|
for (const key in validityState) {
|
||||||
|
if (validityState[key]) {
|
||||||
|
errorType = key
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errorType
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Check Validy of field by request to server
|
||||||
|
*/
|
||||||
|
const hasServerError = function (field, form, url) {
|
||||||
|
return fetch( url+field.name, {
|
||||||
|
method: 'POST',
|
||||||
|
body: new FormData(form)
|
||||||
|
})
|
||||||
|
.then( (response) => response.json() )
|
||||||
|
.then( (json) => {
|
||||||
|
if (json.err === 0) {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
let errorType
|
||||||
|
for (const key in json) {
|
||||||
|
if (json[key] === true) {
|
||||||
|
errorType = key
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errorType
|
||||||
fpeters
commented
Il faudrait vraiment utiliser le message d'erreur fourni dans la réponse; comme je le notais il n'y a pas d'exhaustivité possible dans ce qui peut être ajouté à l'HTML dès le rendu. (anecdotique : le javascript sans point-virgules, c'est quelque chose qui vient avec notre choix de linter ?) Il faudrait vraiment utiliser le message d'erreur fourni dans la réponse; comme je le notais il n'y a pas d'exhaustivité possible dans ce qui peut être ajouté à l'HTML dès le rendu.
(anecdotique : le javascript sans point-virgules, c'est quelque chose qui vient avec notre choix de linter ?)
tjund
commented
oui, norme standardJS : https://standardjs.com/rules.html#semicolons
Ok > le javascript sans point-virgules
oui, norme standardJS : https://standardjs.com/rules.html#semicolons
> Il faudrait vraiment utiliser le message d'erreur fourni dans la réponse
Ok
tjund
commented
Dans le Json retourné par le serveur, est-ce qu'il n'y a qu' 1 seul message et 1 seul type d'erreur possible ? Dans le Json retourné par le serveur, est-ce qu'il n'y a qu' 1 seul message et 1 seul type d'erreur possible ?
fpeters
commented
oui. oui.
|
|||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
class FieldLiveValidation {
|
||||||
|
constructor (widget, formDatas) {
|
||||||
|
this.widget = widget
|
||||||
|
this.name = widget.dataset.widgetName
|
||||||
|
this.errorClass = "widget-with-error"
|
||||||
|
this.errorEl = this.setErrorEl(formDatas.errorTpl.content.children[0])
|
||||||
|
this.checkUrl = formDatas.checkUrl
|
||||||
|
this.hasError = false
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrorEl = function(errorTpl) {
|
||||||
|
const errorEl = document.importNode(errorTpl)
|
||||||
|
errorEl.id = errorEl.id.replace('fieldname', this.name)
|
||||||
|
return errorEl
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleStatus(field) {
|
||||||
|
if (excludedField(field)) return
|
||||||
|
const attrError = hasAttrError(field);
|
||||||
|
const serverError = this.widget.dataset.supportsCheckCondition
|
||||||
fpeters
commented
Dans la dernière mise à jour de #241 j'ai modifié le data-supports-check-condition en data-supports-live-validation (parce que globalement dans w.c.s. il y avait déjà une utilisation de check_condition et partager les mêmes mots rendait les choses confuses). Ça vaut aussi pour un peu plus bas,
l'attribut est désormais data-supports-live-validation="true". Dans la dernière mise à jour de https://git.entrouvert.org/entrouvert/wcs/pulls/241 j'ai modifié le data-supports-check-condition en data-supports-live-validation (parce que globalement dans w.c.s. il y avait déjà une utilisation de check_condition et partager les mêmes mots rendait les choses confuses).
Ça vaut aussi pour un peu plus bas,
```
checkUrl: form.dataset.checkConditionUrl + '?field=',
```
l'attribut est désormais data-supports-live-validation="true".
|
|||||||
|
? await hasServerError(field, field.form, this.checkUrl)
|
||||||
|
: false
|
||||||
|
const error = attrError ? attrError : serverError
|
||||||
|
if (error) {
|
||||||
|
this.showError(field, error)
|
||||||
|
} else {
|
||||||
|
this.removeError(field, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(field, error) {
|
||||||
|
if (this.hasError === error) return
|
||||||
|
this.widget.classList.add(this.errorClass)
|
||||||
|
const errorElMessage = document.getElementById(`error_${this.name}_${error}`).innerHTML
|
||||||
|
this.errorEl.innerHTML = errorElMessage
|
||||||
|
this.widget.appendChild(this.errorEl)
|
||||||
|
field.setAttribute("aria-invalid", "true")
|
||||||
|
field.setAttribute("aria-describedby", this.errorEl.id)
|
||||||
|
this.hasError = error
|
||||||
|
}
|
||||||
|
|
||||||
|
removeError(field, error) {
|
||||||
|
if (error) return
|
||||||
|
this.errorEl.remove()
|
||||||
|
field.setAttribute("aria-invalid", "false")
|
||||||
|
field.setAttribute("aria-describedby", this.errorEl.id)
|
||||||
|
this.widget.classList.remove(this.errorClass)
|
||||||
|
this.hasError = false
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Check if field is allready on error
|
||||||
|
if (this.widget.classList.contains(this.errorClass)) {
|
||||||
|
this.hasError = true;
|
||||||
|
// Check if error element exist allready
|
||||||
|
const existingErrorEl = document.getElementById(this.errorEl.id)
|
||||||
|
if (existingErrorEl)
|
||||||
|
this.errorEl = existingErrorEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events
|
||||||
|
this.widget.addEventListener('blur', (event) => {
|
||||||
|
this.toggleStatus(event.target)
|
||||||
|
}, true);
|
||||||
|
// If field has Error, check when it change with debounce
|
||||||
|
let timeout;
|
||||||
|
this.widget.addEventListener('input', (event) => {
|
||||||
|
if (this.hasError) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
this.toggleStatus(event.target)
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return FieldLiveValidation
|
||||||
|
})()
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function(){
|
||||||
|
const form = document.querySelector('form[data-check-condition-url]')
|
||||||
|
const formWidgets = form.querySelectorAll('.widget')
|
||||||
|
let formDatas = {
|
||||||
|
errorTpl: document.getElementById('form_error_tpl'),
|
||||||
|
checkUrl: form.dataset.checkConditionUrl + '?field=',
|
||||||
|
}
|
||||||
|
formWidgets.forEach((widget) => {
|
||||||
|
new LiveValidation(widget, formDatas)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
@ -6,6 +6,10 @@
|
||||||
{% block widget-attrs %}
|
{% block widget-attrs %}
|
||||||
{% if widget.is_hidden %}style="display: none"{% endif %}
|
{% if widget.is_hidden %}style="display: none"{% endif %}
|
||||||
{% if widget.field %}data-field-id="{{ widget.field.id }}"{% 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_check_condition %}data-supports-check-condition="true"{% endif %}
|
||||||
|
{% endif %}
|
||||||
data-widget-name="{{ widget.name }}"
|
data-widget-name="{{ widget.name }}"
|
||||||
{% if widget.div_id %}id="{{widget.div_id}}" data-valuecontainerid="form_{{widget.name}}"{% endif %}
|
{% 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_role %}role="{{ a11y_role }}"{% endif %}
|
||||||
|
@ -45,4 +49,14 @@
|
||||||
{% if widget.render_br %}
|
{% if widget.render_br %}
|
||||||
<br class="content {{widget.content.content_extra_css_class}}">
|
<br class="content {{widget.content.content_extra_css_class}}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if widget.supports_check_condition %}
|
||||||
|
{% block widget-error-templates %}
|
||||||
|
{% if not widget.readonly %}
|
||||||
|
{% for error_message in widget.get_error_messages %}
|
||||||
|
<template id="error_{{ widget.get_name_for_id }}_{{ error_message.camel_code }}">{{ error_message.label }}</template>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
<button type="button" onclick="check_condition(this, '{{ widget.get_name_for_id }}')">check condition</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
À tester davantage mainteannt je note ce passage, le code d'exemple, ici c'est "widget_id" qui était passé, il arrivait via
C'est différent de ce que le code proposé fait,
avec field.name qui va être différent de widget.get_name_for_id.
Ça n'a pas d'importance pour les champs ordinaires mais ça compte pour les blocs de champs, qui vont avoir un name de la forme f1$element0$fx, mais $ pas autorisé pour les id, donc conversion en f1__element0__fx dans get_name_for_id.
Il n'y avait pas d'accès direct à cette valeur disponible dans un attribut du champ, j'ai ajouté ça dans la branche à jour dans #76632,
(sur le
<div class="widget ...">
, pas l'input).