From 3ce9829b615336b0f3ac39b080c27fc8cf5af483 Mon Sep 17 00:00:00 2001 From: Loic Bistuer Date: Sat, 30 Nov 2013 02:38:13 +0700 Subject: [PATCH] Fixed #17413 -- Serialization of form errors along with all metadata. --- django/contrib/admin/helpers.py | 2 + django/forms/fields.py | 4 +- django/forms/forms.py | 4 +- django/forms/formsets.py | 2 +- django/forms/utils.py | 72 +++++++++++++++++++++------ docs/ref/forms/api.txt | 21 ++++++++ docs/releases/1.7.txt | 6 +++ tests/forms_tests/tests/test_forms.py | 38 ++++++++++++++ 8 files changed, 129 insertions(+), 20 deletions(-) diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py index 318c393c09..86ab0f263e 100644 --- a/django/contrib/admin/helpers.py +++ b/django/contrib/admin/helpers.py @@ -329,6 +329,8 @@ class AdminErrorList(forms.utils.ErrorList): Stores all errors for the form/formsets in an add/change stage view. """ def __init__(self, form, inline_formsets): + super(AdminErrorList, self).__init__() + if form.is_bound: self.extend(list(six.itervalues(form.errors))) for inline_formset in inline_formsets: diff --git a/django/forms/fields.py b/django/forms/fields.py index 6f88fb2662..fe056e3271 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -15,7 +15,7 @@ from io import BytesIO from django.core import validators 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 ( TextInput, NumberInput, EmailInput, URLInput, HiddenInput, MultipleHiddenInput, ClearableFileInput, CheckboxInput, Select, @@ -998,7 +998,7 @@ class MultiValueField(Field): DateField.clean(value[0]) and TimeField.clean(value[1]). """ clean_data = [] - errors = ErrorList() + errors = [] 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 self.required: diff --git a/django/forms/forms.py b/django/forms/forms.py index 350b8b8993..880a74f49e 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -321,9 +321,9 @@ class BaseForm(object): "argument contains errors for multiple fields." ) else: - error = dict(error) + error = error.error_dict 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(): if field not in self.errors: diff --git a/django/forms/formsets.py b/django/forms/formsets.py index fd1fd92773..215ec1d876 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -341,7 +341,7 @@ class BaseFormSet(object): # Give self.clean() a chance to do cross-form validation. self.clean() 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): """ diff --git a/django/forms/utils.py b/django/forms/utils.py index 9402f2612e..3582384293 100644 --- a/django/forms/utils.py +++ b/django/forms/utils.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import json +import sys import warnings 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.translation import ugettext_lazy as _ from django.utils import six -import sys # Import ValidationError so that it can be imported from this # module to maintain backwards compatibility. from django.core.exceptions import ValidationError +try: + from collections import UserList +except ImportError: # Python 2 + from UserList import UserList + def flatatt(attrs): """ @@ -46,8 +52,12 @@ class ErrorDict(dict): The dictionary keys are the field names, and the values are the errors. """ - def __str__(self): - return self.as_ul() + def as_data(self): + 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): if not self: @@ -58,19 +68,35 @@ class ErrorDict(dict): ) 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): 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): - if not self: + if not self.data: return '' return format_html( '', @@ -78,12 +104,28 @@ class ErrorList(list): ) def as_text(self): - if not self: - return '' - return '\n'.join('* %s' % force_text(e) for e in self) + return '\n'.join('* %s' % e for e in self) + + def __str__(self): + return self.as_ul() 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. diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index 69667643b4..ef4da9479e 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -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 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) .. versionadded:: 1.7 diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index c0be6860ac..e4ede9913e 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -353,6 +353,12 @@ Forms * The new :meth:`~django.forms.Form.add_error()` method allows adding errors 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 ^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py index 9fec9ddcaa..675565b026 100644 --- a/tests/forms_tests/tests/test_forms.py +++ b/tests/forms_tests/tests/test_forms.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import copy import datetime +import json import warnings from django.core.files.uploadedfile import SimpleUploadedFile @@ -2031,3 +2032,40 @@ class FormsTestCase(TestCase): form = SomeForm() self.assertHTMLEqual(form.as_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 = [ + '
  • foo
  • ', + '
  • bar
  • ', + '
  • __all__
  • ', + ] + 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)