Fixed #16192 -- Made unique error messages in ModelForm customizable.

Overriding the error messages now works for both unique fields, unique_together
and unique_for_date.

This patch changed the overriding logic to allow customizing NON_FIELD_ERRORS
since previously only fields' errors were customizable.

Refs #20199.

Thanks leahculver for the suggestion.
This commit is contained in:
Loic Bistuer 2014-02-04 01:31:27 +07:00 committed by Tim Graham
parent 65131911db
commit 8847a0c601
11 changed files with 142 additions and 34 deletions

View File

@ -941,36 +941,52 @@ class Model(six.with_metaclass(ModelBase)):
) )
return errors return errors
def date_error_message(self, lookup_type, field, unique_for): def date_error_message(self, lookup_type, field_name, unique_for):
opts = self._meta opts = self._meta
return _("%(field_name)s must be unique for %(date_field)s %(lookup)s.") % { field = opts.get_field(field_name)
'field_name': six.text_type(capfirst(opts.get_field(field).verbose_name)), return ValidationError(
'date_field': six.text_type(capfirst(opts.get_field(unique_for).verbose_name)), message=field.error_messages['unique_for_date'],
'lookup': lookup_type, code='unique_for_date',
} params={
'model': self,
'model_name': six.text_type(capfirst(opts.verbose_name)),
'lookup_type': lookup_type,
'field': field_name,
'field_label': six.text_type(capfirst(field.verbose_name)),
'date_field': unique_for,
'date_field_label': six.text_type(capfirst(opts.get_field(unique_for).verbose_name)),
}
)
def unique_error_message(self, model_class, unique_check): def unique_error_message(self, model_class, unique_check):
opts = model_class._meta opts = model_class._meta
model_name = capfirst(opts.verbose_name)
params = {
'model': self,
'model_class': model_class,
'model_name': six.text_type(capfirst(opts.verbose_name)),
'unique_check': unique_check,
}
# A unique field # A unique field
if len(unique_check) == 1: if len(unique_check) == 1:
field_name = unique_check[0] field = opts.get_field(unique_check[0])
field = opts.get_field(field_name) params['field_label'] = six.text_type(capfirst(field.verbose_name))
field_label = capfirst(field.verbose_name) return ValidationError(
# Insert the error into the error dict, very sneaky message=field.error_messages['unique'],
return field.error_messages['unique'] % { code='unique',
'model_name': six.text_type(model_name), params=params,
'field_label': six.text_type(field_label) )
}
# unique_together # unique_together
else: else:
field_labels = [capfirst(opts.get_field(f).verbose_name) for f in unique_check] field_labels = [capfirst(opts.get_field(f).verbose_name) for f in unique_check]
field_labels = get_text_list(field_labels, _('and')) params['field_labels'] = six.text_type(get_text_list(field_labels, _('and')))
return _("%(model_name)s with this %(field_label)s already exists.") % { return ValidationError(
'model_name': six.text_type(model_name), message=_("%(model_name)s with this %(field_labels)s already exists."),
'field_label': six.text_type(field_labels) code='unique_together',
} params=params,
)
def full_clean(self, exclude=None, validate_unique=True): def full_clean(self, exclude=None, validate_unique=True):
""" """

View File

@ -105,6 +105,8 @@ class Field(RegisterLookupMixin):
'blank': _('This field cannot be blank.'), 'blank': _('This field cannot be blank.'),
'unique': _('%(model_name)s with this %(field_label)s ' 'unique': _('%(model_name)s with this %(field_label)s '
'already exists.'), 'already exists.'),
'unique_for_date': _("%(field_label)s must be unique for "
"%(date_field_label)s %(lookup_type)s."),
} }
class_lookups = default_lookups.copy() class_lookups = default_lookups.copy()

View File

@ -8,7 +8,7 @@ from collections import OrderedDict
import copy import copy
import warnings import warnings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
from django.forms.fields import Field, FileField from django.forms.fields import Field, FileField
from django.forms.utils import flatatt, ErrorDict, ErrorList from django.forms.utils import flatatt, ErrorDict, ErrorList
from django.forms.widgets import Media, MediaDefiningClass, TextInput, Textarea from django.forms.widgets import Media, MediaDefiningClass, TextInput, Textarea
@ -21,8 +21,6 @@ from django.utils import six
__all__ = ('BaseForm', 'Form') __all__ = ('BaseForm', 'Form')
NON_FIELD_ERRORS = '__all__'
def pretty_name(name): def pretty_name(name):
"""Converts 'first_name' to 'First name'""" """Converts 'first_name' to 'First name'"""

View File

@ -373,15 +373,21 @@ class BaseModelForm(BaseForm):
def _update_errors(self, errors): def _update_errors(self, errors):
# Override any validation error messages defined at the model level # Override any validation error messages defined at the model level
# with those defined on the form fields. # with those defined at the form level.
opts = self._meta
for field, messages in errors.error_dict.items(): for field, messages in errors.error_dict.items():
if field not in self.fields: if (field == NON_FIELD_ERRORS and opts.error_messages and
NON_FIELD_ERRORS in opts.error_messages):
error_messages = opts.error_messages[NON_FIELD_ERRORS]
elif field in self.fields:
error_messages = self.fields[field].error_messages
else:
continue continue
field = self.fields[field]
for message in messages: for message in messages:
if (isinstance(message, ValidationError) and if (isinstance(message, ValidationError) and
message.code in field.error_messages): message.code in error_messages):
message.message = field.error_messages[message.code] message.message = error_messages[message.code]
self.add_error(None, errors) self.add_error(None, errors)

View File

@ -124,6 +124,15 @@ ValidationError
:ref:`Model Field Validation <validating-objects>` and the :ref:`Model Field Validation <validating-objects>` and the
:doc:`Validator Reference </ref/validators>`. :doc:`Validator Reference </ref/validators>`.
NON_FIELD_ERRORS
~~~~~~~~~~~~~~~~
.. data:: NON_FIELD_ERRORS
``ValidationError``\s that don't belong to a particular field in a form
or model are classified as ``NON_FIELD_ERRORS``. This constant is used
as a key in dictonaries that otherwise map fields to their respective
list of errors.
.. currentmodule:: django.core.urlresolvers .. currentmodule:: django.core.urlresolvers
URL Resolver exceptions URL Resolver exceptions

View File

@ -233,8 +233,12 @@ field will raise. Pass in a dictionary with keys matching the error messages you
want to override. want to override.
Error message keys include ``null``, ``blank``, ``invalid``, ``invalid_choice``, Error message keys include ``null``, ``blank``, ``invalid``, ``invalid_choice``,
and ``unique``. Additional error message keys are specified for each field in ``unique``, and ``unique_for_date``. Additional error message keys are
the `Field types`_ section below. specified for each field in the `Field types`_ section below.
.. versionadded:: 1.7
The ``unique_for_date`` error message key was added.
``help_text`` ``help_text``
------------- -------------

View File

@ -151,8 +151,8 @@ access to more than a single field::
Any :exc:`~django.core.exceptions.ValidationError` exceptions raised by Any :exc:`~django.core.exceptions.ValidationError` exceptions raised by
``Model.clean()`` will be stored in a special key error dictionary key, ``Model.clean()`` will be stored in a special key error dictionary key,
``NON_FIELD_ERRORS``, that is used for errors that are tied to the entire model :data:`~django.core.exceptions.NON_FIELD_ERRORS`, that is used for errors
instead of to a specific field:: that are tied to the entire model instead of to a specific field::
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
try: try:

View File

@ -321,6 +321,11 @@ Django quotes column and table names behind the scenes.
:class:`~django.db.models.ManyToManyField`, try using a signal or :class:`~django.db.models.ManyToManyField`, try using a signal or
an explicit :attr:`through <ManyToManyField.through>` model. an explicit :attr:`through <ManyToManyField.through>` model.
.. versionchanged:: 1.7
The ``ValidationError`` raised during model validation when the
constraint is violated has the ``unique_together`` error code.
``index_together`` ``index_together``
------------------ ------------------

View File

@ -484,6 +484,14 @@ Forms
that maps fields to their original errors, complete with all metadata that maps fields to their original errors, complete with all metadata
(error code and params), the latter returns the errors serialized as json. (error code and params), the latter returns the errors serialized as json.
* It's now possible to customize the error messages for ``ModelForm``s
``unique``, ``unique_for_date``, and ``unique_together`` constraints.
In order to support ``unique_together`` or any other ``NON_FIELD_ERROR``,
``ModelForm`` now looks for the ``NON_FIELD_ERROR`` key in the
``error_messages`` dictionary of the ``ModelForm``s inner ``Meta`` class.
See :ref:`considerations regarding model's error_messages
<considerations-regarding-model-errormessages>` for more details.
Internationalization Internationalization
^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^

View File

@ -259,7 +259,9 @@ The model's ``clean()`` method will be called before any uniqueness checks are
made. See :ref:`Validating objects <validating-objects>` for more information made. See :ref:`Validating objects <validating-objects>` for more information
on the model's ``clean()`` hook. on the model's ``clean()`` hook.
Considerations regarding fields' ``error_messages`` .. _considerations-regarding-model-errormessages:
Considerations regarding model's ``error_messages``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Error messages defined at the Error messages defined at the
@ -267,12 +269,27 @@ Error messages defined at the
:ref:`form Meta <modelforms-overriding-default-fields>` level always take :ref:`form Meta <modelforms-overriding-default-fields>` level always take
precedence over the error messages defined at the precedence over the error messages defined at the
:attr:`model field <django.db.models.Field.error_messages>` level. :attr:`model field <django.db.models.Field.error_messages>` level.
Error messages defined on :attr:`model fields Error messages defined on :attr:`model fields
<django.db.models.Field.error_messages>` are only used when the <django.db.models.Field.error_messages>` are only used when the
``ValidationError`` is raised during the :ref:`model validation ``ValidationError`` is raised during the :ref:`model validation
<validating-objects>` step and no corresponding error messages are defined at <validating-objects>` step and no corresponding error messages are defined at
the form level. the form level.
.. versionadded:: 1.7
You can override the error messages from ``NON_FIELD_ERRORS`` raised by model
validation by adding the :data:`~django.core.exceptions.NON_FIELD_ERRORS` key
to the ``error_messages`` dictionary of the ``ModelForm``s inner ``Meta`` class::
class ArticleForm(ModelForm):
class Meta:
error_messages = {
NON_FIELD_ERRORS: {
'unique_together': "%(model_name)s's %(field_labels)s are not unique.",
}
}
The ``save()`` method The ``save()`` method
--------------------- ---------------------

View File

@ -7,7 +7,7 @@ from unittest import skipUnless
import warnings import warnings
from django import forms from django import forms
from django.core.exceptions import FieldError from django.core.exceptions import FieldError, NON_FIELD_ERRORS
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.validators import ValidationError from django.core.validators import ValidationError
from django.db import connection from django.db import connection
@ -792,6 +792,49 @@ class UniqueTest(TestCase):
"slug": "Django 1.0"}, instance=p) "slug": "Django 1.0"}, instance=p)
self.assertTrue(form.is_valid()) self.assertTrue(form.is_valid())
def test_override_unique_message(self):
class CustomProductForm(ProductForm):
class Meta(ProductForm.Meta):
error_messages = {
'slug': {
'unique': "%(model_name)s's %(field_label)s not unique.",
}
}
Product.objects.create(slug='teddy-bear-blue')
form = CustomProductForm({'slug': 'teddy-bear-blue'})
self.assertEqual(len(form.errors), 1)
self.assertEqual(form.errors['slug'], ["Product's Slug not unique."])
def test_override_unique_together_message(self):
class CustomPriceForm(PriceForm):
class Meta(PriceForm.Meta):
error_messages = {
NON_FIELD_ERRORS: {
'unique_together': "%(model_name)s's %(field_labels)s not unique.",
}
}
Price.objects.create(price=6.00, quantity=1)
form = CustomPriceForm({'price': '6.00', 'quantity': '1'})
self.assertEqual(len(form.errors), 1)
self.assertEqual(form.errors[NON_FIELD_ERRORS], ["Price's Price and Quantity not unique."])
def test_override_unique_for_date_message(self):
class CustomPostForm(PostForm):
class Meta(PostForm.Meta):
error_messages = {
'title': {
'unique_for_date': "%(model_name)s's %(field_label)s not unique for %(date_field_label)s date.",
}
}
Post.objects.create(title="Django 1.0 is released",
slug="Django 1.0", subtitle="Finally", posted=datetime.date(2008, 9, 3))
form = CustomPostForm({'title': "Django 1.0 is released", 'posted': '2008-09-03'})
self.assertEqual(len(form.errors), 1)
self.assertEqual(form.errors['title'], ["Post's Title not unique for Posted date."])
class ModelToDictTests(TestCase): class ModelToDictTests(TestCase):
""" """