Fixed #17413 -- Serialization of form errors along with all metadata.

This commit is contained in:
Loic Bistuer 2013-11-30 02:38:13 +07:00 committed by Tim Graham
parent e2f142030b
commit 3ce9829b61
8 changed files with 129 additions and 20 deletions

View File

@ -329,6 +329,8 @@ class AdminErrorList(forms.utils.ErrorList):
Stores all errors for the form/formsets in an add/change stage view. Stores all errors for the form/formsets in an add/change stage view.
""" """
def __init__(self, form, inline_formsets): def __init__(self, form, inline_formsets):
super(AdminErrorList, self).__init__()
if form.is_bound: if form.is_bound:
self.extend(list(six.itervalues(form.errors))) self.extend(list(six.itervalues(form.errors)))
for inline_formset in inline_formsets: for inline_formset in inline_formsets:

View File

@ -15,7 +15,7 @@ from io import BytesIO
from django.core import validators from django.core import validators
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.forms.utils import ErrorList, from_current_timezone, to_current_timezone from django.forms.utils import from_current_timezone, to_current_timezone
from django.forms.widgets import ( from django.forms.widgets import (
TextInput, NumberInput, EmailInput, URLInput, HiddenInput, TextInput, NumberInput, EmailInput, URLInput, HiddenInput,
MultipleHiddenInput, ClearableFileInput, CheckboxInput, Select, MultipleHiddenInput, ClearableFileInput, CheckboxInput, Select,
@ -998,7 +998,7 @@ class MultiValueField(Field):
DateField.clean(value[0]) and TimeField.clean(value[1]). DateField.clean(value[0]) and TimeField.clean(value[1]).
""" """
clean_data = [] clean_data = []
errors = ErrorList() errors = []
if not value or isinstance(value, (list, tuple)): if not value or isinstance(value, (list, tuple)):
if not value or not [v for v in value if v not in self.empty_values]: if not value or not [v for v in value if v not in self.empty_values]:
if self.required: if self.required:

View File

@ -321,9 +321,9 @@ class BaseForm(object):
"argument contains errors for multiple fields." "argument contains errors for multiple fields."
) )
else: else:
error = dict(error) error = error.error_dict
else: else:
error = {field or NON_FIELD_ERRORS: list(error)} error = {field or NON_FIELD_ERRORS: error.error_list}
for field, error_list in error.items(): for field, error_list in error.items():
if field not in self.errors: if field not in self.errors:

View File

@ -341,7 +341,7 @@ class BaseFormSet(object):
# Give self.clean() a chance to do cross-form validation. # Give self.clean() a chance to do cross-form validation.
self.clean() self.clean()
except ValidationError as e: except ValidationError as e:
self._non_form_errors = self.error_class(e.messages) self._non_form_errors = self.error_class(e.error_list)
def clean(self): def clean(self):
""" """

View File

@ -1,5 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import json
import sys
import warnings import warnings
from django.conf import settings from django.conf import settings
@ -8,12 +10,16 @@ from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils import six from django.utils import six
import sys
# Import ValidationError so that it can be imported from this # Import ValidationError so that it can be imported from this
# module to maintain backwards compatibility. # module to maintain backwards compatibility.
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
try:
from collections import UserList
except ImportError: # Python 2
from UserList import UserList
def flatatt(attrs): def flatatt(attrs):
""" """
@ -46,8 +52,12 @@ class ErrorDict(dict):
The dictionary keys are the field names, and the values are the errors. The dictionary keys are the field names, and the values are the errors.
""" """
def __str__(self): def as_data(self):
return self.as_ul() return {f: e.as_data() for f, e in self.items()}
def as_json(self):
errors = {f: json.loads(e.as_json()) for f, e in self.items()}
return json.dumps(errors)
def as_ul(self): def as_ul(self):
if not self: if not self:
@ -58,19 +68,35 @@ class ErrorDict(dict):
) )
def as_text(self): def as_text(self):
return '\n'.join('* %s\n%s' % (k, '\n'.join(' * %s' % force_text(i) for i in v)) for k, v in self.items()) output = []
for field, errors in self.items():
output.append('* %s' % field)
output.append('\n'.join(' * %s' % e for e in errors))
return '\n'.join(output)
@python_2_unicode_compatible
class ErrorList(list):
"""
A collection of errors that knows how to display itself in various formats.
"""
def __str__(self): def __str__(self):
return self.as_ul() return self.as_ul()
@python_2_unicode_compatible
class ErrorList(UserList):
"""
A collection of errors that knows how to display itself in various formats.
"""
def as_data(self):
return self.data
def as_json(self):
errors = []
for error in ValidationError(self.data).error_list:
errors.append({
'message': list(error)[0],
'code': error.code or '',
})
return json.dumps(errors)
def as_ul(self): def as_ul(self):
if not self: if not self.data:
return '' return ''
return format_html( return format_html(
'<ul class="errorlist">{0}</ul>', '<ul class="errorlist">{0}</ul>',
@ -78,12 +104,28 @@ class ErrorList(list):
) )
def as_text(self): def as_text(self):
if not self: return '\n'.join('* %s' % e for e in self)
return ''
return '\n'.join('* %s' % force_text(e) for e in self) def __str__(self):
return self.as_ul()
def __repr__(self): def __repr__(self):
return repr([force_text(e) for e in self]) return repr(list(self))
def __contains__(self, item):
return item in list(self)
def __eq__(self, other):
return list(self) == other
def __ne__(self, other):
return list(self) != other
def __getitem__(self, i):
error = self.data[i]
if isinstance(error, ValidationError):
return list(error)[0]
return force_text(error)
# Utilities for time zone support in DateTimeField et al. # Utilities for time zone support in DateTimeField et al.

View File

@ -117,6 +117,27 @@ The validation routines will only get called once, regardless of how many times
you access :attr:`~Form.errors` or call :meth:`~Form.is_valid`. This means that you access :attr:`~Form.errors` or call :meth:`~Form.is_valid`. This means that
if validation has side effects, those side effects will only be triggered once. if validation has side effects, those side effects will only be triggered once.
.. method:: Form.errors.as_data()
.. versionadded:: 1.7
Returns a ``dict`` that maps fields to their original ``ValidationError``
instances.
>>> f.errors.as_data()
{'sender': [ValidationError(['Enter a valid email address.'])],
'subject': [ValidationError(['This field is required.'])]}
.. method:: Form.errors.as_json()
.. versionadded:: 1.7
Returns the errors serialized as JSON.
>>> f.errors.as_json()
{"sender": [{"message": "Enter a valid email address.", "code": "invalid"}],
"subject": [{"message": "This field is required.", "code": "required"}]}
.. method:: Form.add_error(field, error) .. method:: Form.add_error(field, error)
.. versionadded:: 1.7 .. versionadded:: 1.7

View File

@ -353,6 +353,12 @@ Forms
* The new :meth:`~django.forms.Form.add_error()` method allows adding errors * The new :meth:`~django.forms.Form.add_error()` method allows adding errors
to specific form fields. to specific form fields.
* The dict-like attribute :attr:`~django.forms.Form.errors` now has two new
methods :meth:`~django.forms.Form.errors.as_data()` and
:meth:`~django.forms.Form.errors.as_json()`. The former returns a ``dict``
that maps fields to their original errors, complete with all metadata
(error code and params), the latter returns the errors serialized as json.
Internationalization Internationalization
^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
import copy import copy
import datetime import datetime
import json
import warnings import warnings
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
@ -2031,3 +2032,40 @@ class FormsTestCase(TestCase):
form = SomeForm() form = SomeForm()
self.assertHTMLEqual(form.as_p(), '<p id="p_some_field"></p>') self.assertHTMLEqual(form.as_p(), '<p id="p_some_field"></p>')
def test_error_dict(self):
class MyForm(Form):
foo = CharField()
bar = CharField()
def clean(self):
raise ValidationError('Non-field error.', code='secret', params={'a': 1, 'b': 2})
form = MyForm({})
self.assertEqual(form.is_valid(), False)
errors = form.errors.as_text()
control = [
'* foo\n * This field is required.',
'* bar\n * This field is required.',
'* __all__\n * Non-field error.',
]
for error in control:
self.assertIn(error, errors)
errors = form.errors.as_ul()
control = [
'<li>foo<ul class="errorlist"><li>This field is required.</li></ul></li>',
'<li>bar<ul class="errorlist"><li>This field is required.</li></ul></li>',
'<li>__all__<ul class="errorlist"><li>Non-field error.</li></ul></li>',
]
for error in control:
self.assertInHTML(error, errors)
errors = json.loads(form.errors.as_json())
control = {
'foo': [{'code': 'required', 'message': 'This field is required.'}],
'bar': [{'code': 'required', 'message': 'This field is required.'}],
'__all__': [{'code': 'secret', 'message': 'Non-field error.'}]
}
self.assertEqual(errors, control)