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:
parent
65131911db
commit
8847a0c601
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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'"""
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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``
|
||||||
-------------
|
-------------
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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``
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue