From ee77d4b25360a9fc050c32769a334fd69a011a63 Mon Sep 17 00:00:00 2001
From: Loic Bistuer
Date: Wed, 5 Jun 2013 14:55:05 -0400
Subject: [PATCH] Fixed #20199 -- Allow ModelForm fields to override
error_messages from model fields
---
django/contrib/admin/forms.py | 9 +-
django/contrib/admin/options.py | 10 +-
django/contrib/auth/forms.py | 33 ++++--
django/contrib/flatpages/forms.py | 16 ++-
django/contrib/formtools/wizard/views.py | 5 +-
django/contrib/gis/forms/fields.py | 6 +-
django/contrib/sites/models.py | 4 +-
django/core/exceptions.py | 4 +-
django/core/validators.py | 8 +-
django/db/models/fields/__init__.py | 130 +++++++++++++++--------
django/db/models/fields/related.py | 7 +-
django/forms/fields.py | 96 +++++++++++------
django/forms/formsets.py | 9 +-
django/forms/models.py | 32 ++++--
django/forms/util.py | 13 ++-
django/utils/ipv6.py | 5 +-
docs/ref/forms/validation.txt | 110 +++++++++++++++++--
docs/ref/models/instances.txt | 14 ++-
docs/releases/1.6.txt | 7 ++
tests/model_forms/models.py | 10 ++
tests/model_forms/tests.py | 20 +++-
tests/validators/tests.py | 4 +-
22 files changed, 403 insertions(+), 149 deletions(-)
diff --git a/django/contrib/admin/forms.py b/django/contrib/admin/forms.py
index 38c445f71a..6814fc9083 100644
--- a/django/contrib/admin/forms.py
+++ b/django/contrib/admin/forms.py
@@ -22,15 +22,12 @@ class AdminAuthenticationForm(AuthenticationForm):
username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password')
message = ERROR_MESSAGE
+ params = {'username': self.username_field.verbose_name}
if username and password:
self.user_cache = authenticate(username=username, password=password)
if self.user_cache is None:
- raise forms.ValidationError(message % {
- 'username': self.username_field.verbose_name
- })
+ raise forms.ValidationError(message, code='invalid', params=params)
elif not self.user_cache.is_active or not self.user_cache.is_staff:
- raise forms.ValidationError(message % {
- 'username': self.username_field.verbose_name
- })
+ raise forms.ValidationError(message, code='invalid', params=params)
return self.cleaned_data
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 34583ebf74..be9671a76e 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -1574,13 +1574,13 @@ class InlineModelAdmin(BaseModelAdmin):
'class_name': p._meta.verbose_name,
'instance': p}
)
- msg_dict = {'class_name': self._meta.model._meta.verbose_name,
- 'instance': self.instance,
- 'related_objects': get_text_list(objs, _('and'))}
+ params = {'class_name': self._meta.model._meta.verbose_name,
+ 'instance': self.instance,
+ 'related_objects': get_text_list(objs, _('and'))}
msg = _("Deleting %(class_name)s %(instance)s would require "
"deleting the following protected related objects: "
- "%(related_objects)s") % msg_dict
- raise ValidationError(msg)
+ "%(related_objects)s")
+ raise ValidationError(msg, code='deleting_protected', params=params)
def is_valid(self):
result = super(DeleteProtectedModelForm, self).is_valid()
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index edf2727b07..028ec8eafc 100644
--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -97,14 +97,19 @@ class UserCreationForm(forms.ModelForm):
User._default_manager.get(username=username)
except User.DoesNotExist:
return username
- raise forms.ValidationError(self.error_messages['duplicate_username'])
+ raise forms.ValidationError(
+ self.error_messages['duplicate_username'],
+ code='duplicate_username',
+ )
def clean_password2(self):
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError(
- self.error_messages['password_mismatch'])
+ self.error_messages['password_mismatch'],
+ code='password_mismatch',
+ )
return password2
def save(self, commit=True):
@@ -183,11 +188,15 @@ class AuthenticationForm(forms.Form):
password=password)
if self.user_cache is None:
raise forms.ValidationError(
- self.error_messages['invalid_login'] % {
- 'username': self.username_field.verbose_name
- })
+ self.error_messages['invalid_login'],
+ code='invalid_login',
+ params={'username': self.username_field.verbose_name},
+ )
elif not self.user_cache.is_active:
- raise forms.ValidationError(self.error_messages['inactive'])
+ raise forms.ValidationError(
+ self.error_messages['inactive'],
+ code='inactive',
+ )
return self.cleaned_data
def check_for_test_cookie(self):
@@ -269,7 +278,9 @@ class SetPasswordForm(forms.Form):
if password1 and password2:
if password1 != password2:
raise forms.ValidationError(
- self.error_messages['password_mismatch'])
+ self.error_messages['password_mismatch'],
+ code='password_mismatch',
+ )
return password2
def save(self, commit=True):
@@ -298,7 +309,9 @@ class PasswordChangeForm(SetPasswordForm):
old_password = self.cleaned_data["old_password"]
if not self.user.check_password(old_password):
raise forms.ValidationError(
- self.error_messages['password_incorrect'])
+ self.error_messages['password_incorrect'],
+ code='password_incorrect',
+ )
return old_password
PasswordChangeForm.base_fields = SortedDict([
@@ -329,7 +342,9 @@ class AdminPasswordChangeForm(forms.Form):
if password1 and password2:
if password1 != password2:
raise forms.ValidationError(
- self.error_messages['password_mismatch'])
+ self.error_messages['password_mismatch'],
+ code='password_mismatch',
+ )
return password2
def save(self, commit=True):
diff --git a/django/contrib/flatpages/forms.py b/django/contrib/flatpages/forms.py
index 80938116ad..a93a494096 100644
--- a/django/contrib/flatpages/forms.py
+++ b/django/contrib/flatpages/forms.py
@@ -17,11 +17,17 @@ class FlatpageForm(forms.ModelForm):
def clean_url(self):
url = self.cleaned_data['url']
if not url.startswith('/'):
- raise forms.ValidationError(ugettext("URL is missing a leading slash."))
+ raise forms.ValidationError(
+ ugettext("URL is missing a leading slash."),
+ code='missing_leading_slash',
+ )
if (settings.APPEND_SLASH and
'django.middleware.common.CommonMiddleware' in settings.MIDDLEWARE_CLASSES and
not url.endswith('/')):
- raise forms.ValidationError(ugettext("URL is missing a trailing slash."))
+ raise forms.ValidationError(
+ ugettext("URL is missing a trailing slash."),
+ code='missing_trailing_slash',
+ )
return url
def clean(self):
@@ -36,7 +42,9 @@ class FlatpageForm(forms.ModelForm):
for site in sites:
if same_url.filter(sites=site).exists():
raise forms.ValidationError(
- _('Flatpage with url %(url)s already exists for site %(site)s') %
- {'url': url, 'site': site})
+ _('Flatpage with url %(url)s already exists for site %(site)s'),
+ code='duplicate_url',
+ params={'url': url, 'site': site},
+ )
return super(FlatpageForm, self).clean()
diff --git a/django/contrib/formtools/wizard/views.py b/django/contrib/formtools/wizard/views.py
index 17cfa6baa7..c478f20854 100644
--- a/django/contrib/formtools/wizard/views.py
+++ b/django/contrib/formtools/wizard/views.py
@@ -7,6 +7,7 @@ from django.forms import formsets, ValidationError
from django.views.generic import TemplateView
from django.utils.datastructures import SortedDict
from django.utils.decorators import classonlymethod
+from django.utils.translation import ugettext as _
from django.utils import six
from django.contrib.formtools.wizard.storage import get_storage
@@ -271,7 +272,9 @@ class WizardView(TemplateView):
management_form = ManagementForm(self.request.POST, prefix=self.prefix)
if not management_form.is_valid():
raise ValidationError(
- 'ManagementForm data is missing or has been tampered.')
+ _('ManagementForm data is missing or has been tampered.'),
+ code='missing_management_form',
+ )
form_current_step = management_form.cleaned_data['current_step']
if (form_current_step != self.steps.current and
diff --git a/django/contrib/gis/forms/fields.py b/django/contrib/gis/forms/fields.py
index 6e2cbd59f5..59e725926c 100644
--- a/django/contrib/gis/forms/fields.py
+++ b/django/contrib/gis/forms/fields.py
@@ -50,7 +50,7 @@ class GeometryField(forms.Field):
try:
return GEOSGeometry(value)
except (GEOSException, ValueError, TypeError):
- raise forms.ValidationError(self.error_messages['invalid_geom'])
+ raise forms.ValidationError(self.error_messages['invalid_geom'], code='invalid_geom')
def clean(self, value):
"""
@@ -65,7 +65,7 @@ class GeometryField(forms.Field):
# Ensuring that the geometry is of the correct type (indicated
# using the OGC string label).
if str(geom.geom_type).upper() != self.geom_type and not self.geom_type == 'GEOMETRY':
- raise forms.ValidationError(self.error_messages['invalid_geom_type'])
+ raise forms.ValidationError(self.error_messages['invalid_geom_type'], code='invalid_geom_type')
# Transforming the geometry if the SRID was set.
if self.srid:
@@ -76,7 +76,7 @@ class GeometryField(forms.Field):
try:
geom.transform(self.srid)
except:
- raise forms.ValidationError(self.error_messages['transform_error'])
+ raise forms.ValidationError(self.error_messages['transform_error'], code='transform_error')
return geom
diff --git a/django/contrib/sites/models.py b/django/contrib/sites/models.py
index 879497deb3..bbd85ed3f6 100644
--- a/django/contrib/sites/models.py
+++ b/django/contrib/sites/models.py
@@ -22,7 +22,9 @@ def _simple_domain_name_validator(value):
checks = ((s in value) for s in string.whitespace)
if any(checks):
raise ValidationError(
- _("The domain name cannot contain any spaces or tabs."))
+ _("The domain name cannot contain any spaces or tabs."),
+ code='invalid',
+ )
class SiteManager(models.Manager):
diff --git a/django/core/exceptions.py b/django/core/exceptions.py
index 5975164d07..829d6e774e 100644
--- a/django/core/exceptions.py
+++ b/django/core/exceptions.py
@@ -117,9 +117,7 @@ class ValidationError(Exception):
message = message.message
if params:
message %= params
- message = force_text(message)
- else:
- message = force_text(message)
+ message = force_text(message)
messages.append(message)
return messages
diff --git a/django/core/validators.py b/django/core/validators.py
index d0b713be32..200d28fe02 100644
--- a/django/core/validators.py
+++ b/django/core/validators.py
@@ -76,7 +76,7 @@ def validate_integer(value):
try:
int(value)
except (ValueError, TypeError):
- raise ValidationError('')
+ raise ValidationError(_('Enter a valid integer.'), code='invalid')
class EmailValidator(object):
@@ -188,11 +188,7 @@ class BaseValidator(object):
cleaned = self.clean(value)
params = {'limit_value': self.limit_value, 'show_value': cleaned}
if self.compare(cleaned, self.limit_value):
- raise ValidationError(
- self.message % params,
- code=self.code,
- params=params,
- )
+ raise ValidationError(self.message, code=self.code, params=params)
class MaxValueValidator(BaseValidator):
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index 25ceb3f868..c0dce2e58e 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -77,7 +77,7 @@ class Field(object):
auto_creation_counter = -1
default_validators = [] # Default set of validators
default_error_messages = {
- 'invalid_choice': _('Value %r is not a valid choice.'),
+ 'invalid_choice': _('Value %(value)r is not a valid choice.'),
'null': _('This field cannot be null.'),
'blank': _('This field cannot be blank.'),
'unique': _('%(model_name)s with this %(field_label)s '
@@ -233,14 +233,17 @@ class Field(object):
return
elif value == option_key:
return
- msg = self.error_messages['invalid_choice'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid_choice'],
+ code='invalid_choice',
+ params={'value': value},
+ )
if value is None and not self.null:
- raise exceptions.ValidationError(self.error_messages['null'])
+ raise exceptions.ValidationError(self.error_messages['null'], code='null')
if not self.blank and value in self.empty_values:
- raise exceptions.ValidationError(self.error_messages['blank'])
+ raise exceptions.ValidationError(self.error_messages['blank'], code='blank')
def clean(self, value, model_instance):
"""
@@ -568,7 +571,7 @@ class AutoField(Field):
empty_strings_allowed = False
default_error_messages = {
- 'invalid': _("'%s' value must be an integer."),
+ 'invalid': _("'%(value)s' value must be an integer."),
}
def __init__(self, *args, **kwargs):
@@ -586,8 +589,11 @@ class AutoField(Field):
try:
return int(value)
except (TypeError, ValueError):
- msg = self.error_messages['invalid'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid'],
+ code='invalid',
+ params={'value': value},
+ )
def validate(self, value, model_instance):
pass
@@ -616,7 +622,7 @@ class AutoField(Field):
class BooleanField(Field):
empty_strings_allowed = False
default_error_messages = {
- 'invalid': _("'%s' value must be either True or False."),
+ 'invalid': _("'%(value)s' value must be either True or False."),
}
description = _("Boolean (Either True or False)")
@@ -636,8 +642,11 @@ class BooleanField(Field):
return True
if value in ('f', 'False', '0'):
return False
- msg = self.error_messages['invalid'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid'],
+ code='invalid',
+ params={'value': value},
+ )
def get_prep_lookup(self, lookup_type, value):
# Special-case handling for filters coming from a Web request (e.g. the
@@ -709,9 +718,9 @@ class CommaSeparatedIntegerField(CharField):
class DateField(Field):
empty_strings_allowed = False
default_error_messages = {
- 'invalid': _("'%s' value has an invalid date format. It must be "
+ 'invalid': _("'%(value)s' value has an invalid date format. It must be "
"in YYYY-MM-DD format."),
- 'invalid_date': _("'%s' value has the correct format (YYYY-MM-DD) "
+ 'invalid_date': _("'%(value)s' value has the correct format (YYYY-MM-DD) "
"but it is an invalid date."),
}
description = _("Date (without time)")
@@ -745,11 +754,17 @@ class DateField(Field):
if parsed is not None:
return parsed
except ValueError:
- msg = self.error_messages['invalid_date'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid_date'],
+ code='invalid_date',
+ params={'value': value},
+ )
- msg = self.error_messages['invalid'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid'],
+ code='invalid',
+ params={'value': value},
+ )
def pre_save(self, model_instance, add):
if self.auto_now or (self.auto_now_add and add):
@@ -797,11 +812,11 @@ class DateField(Field):
class DateTimeField(DateField):
empty_strings_allowed = False
default_error_messages = {
- 'invalid': _("'%s' value has an invalid format. It must be in "
+ 'invalid': _("'%(value)s' value has an invalid format. It must be in "
"YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] format."),
- 'invalid_date': _("'%s' value has the correct format "
+ 'invalid_date': _("'%(value)s' value has the correct format "
"(YYYY-MM-DD) but it is an invalid date."),
- 'invalid_datetime': _("'%s' value has the correct format "
+ 'invalid_datetime': _("'%(value)s' value has the correct format "
"(YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) "
"but it is an invalid date/time."),
}
@@ -836,19 +851,28 @@ class DateTimeField(DateField):
if parsed is not None:
return parsed
except ValueError:
- msg = self.error_messages['invalid_datetime'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid_datetime'],
+ code='invalid_datetime',
+ params={'value': value},
+ )
try:
parsed = parse_date(value)
if parsed is not None:
return datetime.datetime(parsed.year, parsed.month, parsed.day)
except ValueError:
- msg = self.error_messages['invalid_date'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid_date'],
+ code='invalid_date',
+ params={'value': value},
+ )
- msg = self.error_messages['invalid'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid'],
+ code='invalid',
+ params={'value': value},
+ )
def pre_save(self, model_instance, add):
if self.auto_now or (self.auto_now_add and add):
@@ -894,7 +918,7 @@ class DateTimeField(DateField):
class DecimalField(Field):
empty_strings_allowed = False
default_error_messages = {
- 'invalid': _("'%s' value must be a decimal number."),
+ 'invalid': _("'%(value)s' value must be a decimal number."),
}
description = _("Decimal number")
@@ -912,8 +936,11 @@ class DecimalField(Field):
try:
return decimal.Decimal(value)
except decimal.InvalidOperation:
- msg = self.error_messages['invalid'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid'],
+ code='invalid',
+ params={'value': value},
+ )
def _format(self, value):
if isinstance(value, six.string_types) or value is None:
@@ -999,7 +1026,7 @@ class FilePathField(Field):
class FloatField(Field):
empty_strings_allowed = False
default_error_messages = {
- 'invalid': _("'%s' value must be a float."),
+ 'invalid': _("'%(value)s' value must be a float."),
}
description = _("Floating point number")
@@ -1017,8 +1044,11 @@ class FloatField(Field):
try:
return float(value)
except (TypeError, ValueError):
- msg = self.error_messages['invalid'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid'],
+ code='invalid',
+ params={'value': value},
+ )
def formfield(self, **kwargs):
defaults = {'form_class': forms.FloatField}
@@ -1028,7 +1058,7 @@ class FloatField(Field):
class IntegerField(Field):
empty_strings_allowed = False
default_error_messages = {
- 'invalid': _("'%s' value must be an integer."),
+ 'invalid': _("'%(value)s' value must be an integer."),
}
description = _("Integer")
@@ -1052,8 +1082,11 @@ class IntegerField(Field):
try:
return int(value)
except (TypeError, ValueError):
- msg = self.error_messages['invalid'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid'],
+ code='invalid',
+ params={'value': value},
+ )
def formfield(self, **kwargs):
defaults = {'form_class': forms.IntegerField}
@@ -1135,7 +1168,7 @@ class GenericIPAddressField(Field):
class NullBooleanField(Field):
empty_strings_allowed = False
default_error_messages = {
- 'invalid': _("'%s' value must be either None, True or False."),
+ 'invalid': _("'%(value)s' value must be either None, True or False."),
}
description = _("Boolean (Either True, False or None)")
@@ -1158,8 +1191,11 @@ class NullBooleanField(Field):
return True
if value in ('f', 'False', '0'):
return False
- msg = self.error_messages['invalid'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid'],
+ code='invalid',
+ params={'value': value},
+ )
def get_prep_lookup(self, lookup_type, value):
# Special-case handling for filters coming from a Web request (e.g. the
@@ -1251,9 +1287,9 @@ class TextField(Field):
class TimeField(Field):
empty_strings_allowed = False
default_error_messages = {
- 'invalid': _("'%s' value has an invalid format. It must be in "
+ 'invalid': _("'%(value)s' value has an invalid format. It must be in "
"HH:MM[:ss[.uuuuuu]] format."),
- 'invalid_time': _("'%s' value has the correct format "
+ 'invalid_time': _("'%(value)s' value has the correct format "
"(HH:MM[:ss[.uuuuuu]]) but it is an invalid time."),
}
description = _("Time")
@@ -1285,11 +1321,17 @@ class TimeField(Field):
if parsed is not None:
return parsed
except ValueError:
- msg = self.error_messages['invalid_time'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid_time'],
+ code='invalid_time',
+ params={'value': value},
+ )
- msg = self.error_messages['invalid'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid'],
+ code='invalid',
+ params={'value': value},
+ )
def pre_save(self, model_instance, add):
if self.auto_now or (self.auto_now_add and add):
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index 1b79e5bade..80249817e2 100644
--- a/django/db/models/fields/related.py
+++ b/django/db/models/fields/related.py
@@ -1173,8 +1173,11 @@ class ForeignKey(ForeignObject):
)
qs = qs.complex_filter(self.rel.limit_choices_to)
if not qs.exists():
- raise exceptions.ValidationError(self.error_messages['invalid'] % {
- 'model': self.rel.to._meta.verbose_name, 'pk': value})
+ raise exceptions.ValidationError(
+ self.error_messages['invalid'],
+ code='invalid',
+ params={'model': self.rel.to._meta.verbose_name, 'pk': value},
+ )
def get_attname(self):
return '%s_id' % self.name
diff --git a/django/forms/fields.py b/django/forms/fields.py
index 219c82100e..52bcf9485c 100644
--- a/django/forms/fields.py
+++ b/django/forms/fields.py
@@ -125,7 +125,7 @@ class Field(object):
def validate(self, value):
if value in self.empty_values and self.required:
- raise ValidationError(self.error_messages['required'])
+ raise ValidationError(self.error_messages['required'], code='required')
def run_validators(self, value):
if value in self.empty_values:
@@ -246,7 +246,7 @@ class IntegerField(Field):
try:
value = int(str(value))
except (ValueError, TypeError):
- raise ValidationError(self.error_messages['invalid'])
+ raise ValidationError(self.error_messages['invalid'], code='invalid')
return value
def widget_attrs(self, widget):
@@ -277,7 +277,7 @@ class FloatField(IntegerField):
try:
value = float(value)
except (ValueError, TypeError):
- raise ValidationError(self.error_messages['invalid'])
+ raise ValidationError(self.error_messages['invalid'], code='invalid')
return value
def widget_attrs(self, widget):
@@ -323,7 +323,7 @@ class DecimalField(IntegerField):
try:
value = Decimal(value)
except DecimalException:
- raise ValidationError(self.error_messages['invalid'])
+ raise ValidationError(self.error_messages['invalid'], code='invalid')
return value
def validate(self, value):
@@ -334,7 +334,7 @@ class DecimalField(IntegerField):
# since it is never equal to itself. However, NaN is the only value that
# isn't equal to itself, so we can use this to identify NaN
if value != value or value == Decimal("Inf") or value == Decimal("-Inf"):
- raise ValidationError(self.error_messages['invalid'])
+ raise ValidationError(self.error_messages['invalid'], code='invalid')
sign, digittuple, exponent = value.as_tuple()
decimals = abs(exponent)
# digittuple doesn't include any leading zeros.
@@ -348,15 +348,24 @@ class DecimalField(IntegerField):
whole_digits = digits - decimals
if self.max_digits is not None and digits > self.max_digits:
- raise ValidationError(self.error_messages['max_digits'] % {
- 'max': self.max_digits})
+ raise ValidationError(
+ self.error_messages['max_digits'],
+ code='max_digits',
+ params={'max': self.max_digits},
+ )
if self.decimal_places is not None and decimals > self.decimal_places:
- raise ValidationError(self.error_messages['max_decimal_places'] % {
- 'max': self.decimal_places})
+ raise ValidationError(
+ self.error_messages['max_decimal_places'],
+ code='max_decimal_places',
+ params={'max': self.decimal_places},
+ )
if (self.max_digits is not None and self.decimal_places is not None
and whole_digits > (self.max_digits - self.decimal_places)):
- raise ValidationError(self.error_messages['max_whole_digits'] % {
- 'max': (self.max_digits - self.decimal_places)})
+ raise ValidationError(
+ self.error_messages['max_whole_digits'],
+ code='max_whole_digits',
+ params={'max': (self.max_digits - self.decimal_places)},
+ )
return value
def widget_attrs(self, widget):
@@ -391,7 +400,7 @@ class BaseTemporalField(Field):
return self.strptime(value, format)
except (ValueError, TypeError):
continue
- raise ValidationError(self.error_messages['invalid'])
+ raise ValidationError(self.error_messages['invalid'], code='invalid')
def strptime(self, value, format):
raise NotImplementedError('Subclasses must define this method.')
@@ -471,7 +480,7 @@ class DateTimeField(BaseTemporalField):
# Input comes from a SplitDateTimeWidget, for example. So, it's two
# components: date and time.
if len(value) != 2:
- raise ValidationError(self.error_messages['invalid'])
+ raise ValidationError(self.error_messages['invalid'], code='invalid')
if value[0] in self.empty_values and value[1] in self.empty_values:
return None
value = '%s %s' % tuple(value)
@@ -548,22 +557,22 @@ class FileField(Field):
file_name = data.name
file_size = data.size
except AttributeError:
- raise ValidationError(self.error_messages['invalid'])
+ raise ValidationError(self.error_messages['invalid'], code='invalid')
if self.max_length is not None and len(file_name) > self.max_length:
- error_values = {'max': self.max_length, 'length': len(file_name)}
- raise ValidationError(self.error_messages['max_length'] % error_values)
+ params = {'max': self.max_length, 'length': len(file_name)}
+ raise ValidationError(self.error_messages['max_length'], code='max_length', params=params)
if not file_name:
- raise ValidationError(self.error_messages['invalid'])
+ raise ValidationError(self.error_messages['invalid'], code='invalid')
if not self.allow_empty_file and not file_size:
- raise ValidationError(self.error_messages['empty'])
+ raise ValidationError(self.error_messages['empty'], code='empty')
return data
def clean(self, data, initial=None):
# If the widget got contradictory inputs, we raise a validation error
if data is FILE_INPUT_CONTRADICTION:
- raise ValidationError(self.error_messages['contradiction'])
+ raise ValidationError(self.error_messages['contradiction'], code='contradiction')
# False means the field value should be cleared; further validation is
# not needed.
if data is False:
@@ -623,7 +632,10 @@ class ImageField(FileField):
Image.open(file).verify()
except Exception:
# Pillow (or PIL) doesn't recognize it as an image.
- six.reraise(ValidationError, ValidationError(self.error_messages['invalid_image']), sys.exc_info()[2])
+ six.reraise(ValidationError, ValidationError(
+ self.error_messages['invalid_image'],
+ code='invalid_image',
+ ), sys.exc_info()[2])
if hasattr(f, 'seek') and callable(f.seek):
f.seek(0)
return f
@@ -648,7 +660,7 @@ class URLField(CharField):
except ValueError:
# urlparse.urlsplit can raise a ValueError with some
# misformatted URLs.
- raise ValidationError(self.error_messages['invalid'])
+ raise ValidationError(self.error_messages['invalid'], code='invalid')
value = super(URLField, self).to_python(value)
if value:
@@ -692,7 +704,7 @@ class BooleanField(Field):
def validate(self, value):
if not value and self.required:
- raise ValidationError(self.error_messages['required'])
+ raise ValidationError(self.error_messages['required'], code='required')
def _has_changed(self, initial, data):
# Sometimes data or initial could be None or '' which should be the
@@ -776,7 +788,11 @@ class ChoiceField(Field):
"""
super(ChoiceField, self).validate(value)
if value and not self.valid_value(value):
- raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
+ raise ValidationError(
+ self.error_messages['invalid_choice'],
+ code='invalid_choice',
+ params={'value': value},
+ )
def valid_value(self, value):
"Check to see if the provided value is a valid choice"
@@ -810,7 +826,11 @@ class TypedChoiceField(ChoiceField):
try:
value = self.coerce(value)
except (ValueError, TypeError, ValidationError):
- raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
+ raise ValidationError(
+ self.error_messages['invalid_choice'],
+ code='invalid_choice',
+ params={'value': value},
+ )
return value
@@ -826,7 +846,7 @@ class MultipleChoiceField(ChoiceField):
if not value:
return []
elif not isinstance(value, (list, tuple)):
- raise ValidationError(self.error_messages['invalid_list'])
+ raise ValidationError(self.error_messages['invalid_list'], code='invalid_list')
return [smart_text(val) for val in value]
def validate(self, value):
@@ -834,11 +854,15 @@ class MultipleChoiceField(ChoiceField):
Validates that the input is a list or tuple.
"""
if self.required and not value:
- raise ValidationError(self.error_messages['required'])
+ raise ValidationError(self.error_messages['required'], code='required')
# Validate that each value in the value list is in self.choices.
for val in value:
if not self.valid_value(val):
- raise ValidationError(self.error_messages['invalid_choice'] % {'value': val})
+ raise ValidationError(
+ self.error_messages['invalid_choice'],
+ code='invalid_choice',
+ params={'value': val},
+ )
def _has_changed(self, initial, data):
if initial is None:
@@ -871,14 +895,18 @@ class TypedMultipleChoiceField(MultipleChoiceField):
try:
new_value.append(self.coerce(choice))
except (ValueError, TypeError, ValidationError):
- raise ValidationError(self.error_messages['invalid_choice'] % {'value': choice})
+ raise ValidationError(
+ self.error_messages['invalid_choice'],
+ code='invalid_choice',
+ params={'value': choice},
+ )
return new_value
def validate(self, value):
if value != self.empty_value:
super(TypedMultipleChoiceField, self).validate(value)
elif self.required:
- raise ValidationError(self.error_messages['required'])
+ raise ValidationError(self.error_messages['required'], code='required')
class ComboField(Field):
@@ -952,18 +980,18 @@ class MultiValueField(Field):
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:
- raise ValidationError(self.error_messages['required'])
+ raise ValidationError(self.error_messages['required'], code='required')
else:
return self.compress([])
else:
- raise ValidationError(self.error_messages['invalid'])
+ raise ValidationError(self.error_messages['invalid'], code='invalid')
for i, field in enumerate(self.fields):
try:
field_value = value[i]
except IndexError:
field_value = None
if self.required and field_value in self.empty_values:
- raise ValidationError(self.error_messages['required'])
+ raise ValidationError(self.error_messages['required'], code='required')
try:
clean_data.append(field.clean(field_value))
except ValidationError as e:
@@ -1078,9 +1106,9 @@ class SplitDateTimeField(MultiValueField):
# Raise a validation error if time or date is empty
# (possible if SplitDateTimeField has required=False).
if data_list[0] in self.empty_values:
- raise ValidationError(self.error_messages['invalid_date'])
+ raise ValidationError(self.error_messages['invalid_date'], code='invalid_date')
if data_list[1] in self.empty_values:
- raise ValidationError(self.error_messages['invalid_time'])
+ raise ValidationError(self.error_messages['invalid_time'], code='invalid_time')
result = datetime.datetime.combine(*data_list)
return from_current_timezone(result)
return None
diff --git a/django/forms/formsets.py b/django/forms/formsets.py
index 79e1c2298a..edd362c595 100644
--- a/django/forms/formsets.py
+++ b/django/forms/formsets.py
@@ -85,7 +85,10 @@ class BaseFormSet(object):
if self.is_bound:
form = ManagementForm(self.data, auto_id=self.auto_id, prefix=self.prefix)
if not form.is_valid():
- raise ValidationError('ManagementForm data is missing or has been tampered with')
+ raise ValidationError(
+ _('ManagementForm data is missing or has been tampered with'),
+ code='missing_management_form',
+ )
else:
form = ManagementForm(auto_id=self.auto_id, prefix=self.prefix, initial={
TOTAL_FORM_COUNT: self.total_form_count(),
@@ -315,7 +318,9 @@ class BaseFormSet(object):
self.management_form.cleaned_data[TOTAL_FORM_COUNT] > self.absolute_max:
raise ValidationError(ungettext(
"Please submit %d or fewer forms.",
- "Please submit %d or fewer forms.", self.max_num) % self.max_num)
+ "Please submit %d or fewer forms.", self.max_num) % self.max_num,
+ code='too_many_forms',
+ )
# Give self.clean() a chance to do cross-form validation.
self.clean()
except ValidationError as e:
diff --git a/django/forms/models.py b/django/forms/models.py
index c4070b97fe..821f64199b 100644
--- a/django/forms/models.py
+++ b/django/forms/models.py
@@ -314,7 +314,17 @@ class BaseModelForm(BaseForm):
super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data,
error_class, label_suffix, empty_permitted)
- def _update_errors(self, message_dict):
+ def _update_errors(self, errors):
+ for field, messages in errors.error_dict.items():
+ if field not in self.fields:
+ continue
+ field = self.fields[field]
+ for message in messages:
+ if isinstance(message, ValidationError):
+ if message.code in field.error_messages:
+ message.message = field.error_messages[message.code]
+
+ message_dict = errors.message_dict
for k, v in message_dict.items():
if k != NON_FIELD_ERRORS:
self._errors.setdefault(k, self.error_class()).extend(v)
@@ -1000,7 +1010,7 @@ class InlineForeignKeyField(Field):
else:
orig = self.parent_instance.pk
if force_text(value) != force_text(orig):
- raise ValidationError(self.error_messages['invalid_choice'])
+ raise ValidationError(self.error_messages['invalid_choice'], code='invalid_choice')
return self.parent_instance
def _has_changed(self, initial, data):
@@ -1115,7 +1125,7 @@ class ModelChoiceField(ChoiceField):
key = self.to_field_name or 'pk'
value = self.queryset.get(**{key: value})
except (ValueError, self.queryset.model.DoesNotExist):
- raise ValidationError(self.error_messages['invalid_choice'])
+ raise ValidationError(self.error_messages['invalid_choice'], code='invalid_choice')
return value
def validate(self, value):
@@ -1150,22 +1160,30 @@ class ModelMultipleChoiceField(ModelChoiceField):
def clean(self, value):
if self.required and not value:
- raise ValidationError(self.error_messages['required'])
+ raise ValidationError(self.error_messages['required'], code='required')
elif not self.required and not value:
return self.queryset.none()
if not isinstance(value, (list, tuple)):
- raise ValidationError(self.error_messages['list'])
+ raise ValidationError(self.error_messages['list'], code='list')
key = self.to_field_name or 'pk'
for pk in value:
try:
self.queryset.filter(**{key: pk})
except ValueError:
- raise ValidationError(self.error_messages['invalid_pk_value'] % {'pk': pk})
+ raise ValidationError(
+ self.error_messages['invalid_pk_value'],
+ code='invalid_pk_value',
+ params={'pk': pk},
+ )
qs = self.queryset.filter(**{'%s__in' % key: value})
pks = set([force_text(getattr(o, key)) for o in qs])
for val in value:
if force_text(val) not in pks:
- raise ValidationError(self.error_messages['invalid_choice'] % {'value': val})
+ raise ValidationError(
+ self.error_messages['invalid_choice'],
+ code='invalid_choice',
+ params={'value': val},
+ )
# Since this overrides the inherited ModelChoiceField.clean
# we run custom validators here
self.run_validators(value)
diff --git a/django/forms/util.py b/django/forms/util.py
index f1b864e6b3..568cdd1086 100644
--- a/django/forms/util.py
+++ b/django/forms/util.py
@@ -80,12 +80,17 @@ def from_current_timezone(value):
try:
return timezone.make_aware(value, current_timezone)
except Exception:
- msg = _(
+ message = _(
'%(datetime)s couldn\'t be interpreted '
'in time zone %(current_timezone)s; it '
- 'may be ambiguous or it may not exist.') % {'datetime': value, 'current_timezone':
- current_timezone}
- six.reraise(ValidationError, ValidationError(msg), sys.exc_info()[2])
+ 'may be ambiguous or it may not exist.'
+ )
+ params = {'datetime': value, 'current_timezone': current_timezone}
+ six.reraise(ValidationError, ValidationError(
+ message,
+ code='ambiguous_timezone',
+ params=params,
+ ), sys.exc_info()[2])
return value
def to_current_timezone(value):
diff --git a/django/utils/ipv6.py b/django/utils/ipv6.py
index eaacfb4623..4d5352272b 100644
--- a/django/utils/ipv6.py
+++ b/django/utils/ipv6.py
@@ -2,10 +2,11 @@
# Copyright 2007 Google Inc. http://code.google.com/p/ipaddr-py/
# Licensed under the Apache License, Version 2.0 (the "License").
from django.core.exceptions import ValidationError
+from django.utils.translation import ugettext_lazy as _
from django.utils.six.moves import xrange
def clean_ipv6_address(ip_str, unpack_ipv4=False,
- error_message="This is not a valid IPv6 address."):
+ error_message=_("This is not a valid IPv6 address.")):
"""
Cleans a IPv6 address string.
@@ -31,7 +32,7 @@ def clean_ipv6_address(ip_str, unpack_ipv4=False,
doublecolon_len = 0
if not is_valid_ipv6_address(ip_str):
- raise ValidationError(error_message)
+ raise ValidationError(error_message, code='invalid')
# This algorithm can only handle fully exploded
# IP strings
diff --git a/docs/ref/forms/validation.txt b/docs/ref/forms/validation.txt
index 87c9764f64..8ab1c26831 100644
--- a/docs/ref/forms/validation.txt
+++ b/docs/ref/forms/validation.txt
@@ -12,13 +12,11 @@ validation (accessing the ``errors`` attribute or calling ``full_clean()``
directly), but normally they won't be needed.
In general, any cleaning method can raise ``ValidationError`` if there is a
-problem with the data it is processing, passing the relevant error message to
-the ``ValidationError`` constructor. If no ``ValidationError`` is raised, the
-method should return the cleaned (normalized) data as a Python object.
-
-If you detect multiple errors during a cleaning method and wish to signal all
-of them to the form submitter, it is possible to pass a list of errors to the
-``ValidationError`` constructor.
+problem with the data it is processing, passing the relevant information to
+the ``ValidationError`` constructor. :ref:`See below `
+for the best practice in raising ``ValidationError``. If no ``ValidationError``
+is raised, the method should return the cleaned (normalized) data as a Python
+object.
Most validation can be done using `validators`_ - simple helpers that can be
reused easily. Validators are simple functions (or callables) that take a single
@@ -87,7 +85,8 @@ overridden:
"field" (called ``__all__``), which you can access via the
``non_field_errors()`` method if you need to. If you want to attach
errors to a specific field in the form, you will need to access the
- ``_errors`` attribute on the form, which is `described later`_.
+ ``_errors`` attribute on the form, which is
+ :ref:`described later `.
Also note that there are special considerations when overriding
the ``clean()`` method of a ``ModelForm`` subclass. (see the
@@ -116,7 +115,100 @@ should iterate through ``self.cleaned_data.items()``, possibly considering the
``_errors`` dictionary attribute on the form as well. In this way, you will
already know which fields have passed their individual validation requirements.
-.. _described later:
+.. _raising-validation-error:
+
+Raising ``ValidationError``
+---------------------------
+
+.. versionchanged:: 1.6
+
+In order to make error messages flexible and easy to override, consider the
+following guidelines:
+
+* Provide a descriptive error ``code`` to the constructor::
+
+ # Good
+ ValidationError(_('Invalid value'), code='invalid')
+
+ # Bad
+ ValidationError(_('Invalid value'))
+
+* Don't coerce variables into the message; use placeholders and the ``params``
+ argument of the constructor::
+
+ # Good
+ ValidationError(
+ _('Invalid value: %(value)s'),
+ params={'value': '42'},
+ )
+
+ # Bad
+ ValidationError(_('Invalid value: %s') % value)
+
+* Use mapping keys instead of positional formatting. This enables putting
+ the variables in any order or omitting them altogether when rewriting the
+ message::
+
+ # Good
+ ValidationError(
+ _('Invalid value: %(value)s'),
+ params={'value': '42'},
+ )
+
+ # Bad
+ ValidationError(
+ _('Invalid value: %s'),
+ params=('42',),
+ )
+
+* Wrap the message with ``gettext`` to enable translation::
+
+ # Good
+ ValidationError(_('Invalid value'))
+
+ # Bad
+ ValidationError('Invalid value')
+
+Putting it all together::
+
+ raise ValidationErrror(
+ _('Invalid value: %(value)s'),
+ code='invalid',
+ params={'value': '42'},
+ )
+
+Following these guidelines is particularly necessary if you write reusable
+forms, form fields, and model fields.
+
+While not recommended, if you are at the end of the validation chain
+(i.e. your form ``clean()`` method) and you know you will *never* need
+to override your error message you can still opt for the less verbose::
+
+ ValidationError(_('Invalid value: %s') % value)
+
+Raising multiple errors
+~~~~~~~~~~~~~~~~~~~~~~~
+
+If you detect multiple errors during a cleaning method and wish to signal all
+of them to the form submitter, it is possible to pass a list of errors to the
+``ValidationError`` constructor.
+
+As above, it is recommended to pass a list of ``ValidationError`` instances
+with ``code``\s and ``params`` but a list of strings will also work::
+
+ # Good
+ raise ValidationError([
+ ValidationError(_('Error 1'), code='error1'),
+ ValidationError(_('Error 2'), code='error2'),
+ ])
+
+ # Bad
+ raise ValidationError([
+ _('Error 1'),
+ _('Error 2'),
+ ])
+
+.. _modifying-field-errors:
Form subclasses and modifying field errors
------------------------------------------
diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt
index f989ff1bec..cfc95db092 100644
--- a/docs/ref/models/instances.txt
+++ b/docs/ref/models/instances.txt
@@ -84,12 +84,18 @@ need to call a model's :meth:`~Model.full_clean()` method if you plan to handle
validation errors yourself, or if you have excluded fields from the
:class:`~django.forms.ModelForm` that require validation.
-.. method:: Model.full_clean(exclude=None)
+.. method:: Model.full_clean(exclude=None, validate_unique=True)
+
+.. versionchanged:: 1.6
+
+ The ``validate_unique`` parameter was added to allow skipping
+ :meth:`Model.validate_unique()`. Previously, :meth:`Model.validate_unique()`
+ was always called by ``full_clean``.
This method calls :meth:`Model.clean_fields()`, :meth:`Model.clean()`, and
-:meth:`Model.validate_unique()`, in that order and raises a
-:exc:`~django.core.exceptions.ValidationError` that has a ``message_dict``
-attribute containing errors from all three stages.
+:meth:`Model.validate_unique()` (if ``validate_unique`` is ``True``, in that
+order and raises a :exc:`~django.core.exceptions.ValidationError` that has a
+``message_dict`` attribute containing errors from all three stages.
The optional ``exclude`` argument can be used to provide a list of field names
that can be excluded from validation and cleaning.
diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt
index 7d449edbe5..f260a1eac0 100644
--- a/docs/releases/1.6.txt
+++ b/docs/releases/1.6.txt
@@ -318,6 +318,13 @@ Minor features
* Formsets now have a
:meth:`~django.forms.formsets.BaseFormSet.total_error_count` method.
+* :class:`~django.forms.ModelForm` fields can now override error messages
+ defined in model fields by using the
+ :attr:`~django.forms.Field.error_messages` argument of a ``Field``'s
+ constructor. To take advantage of this new feature with your custom fields,
+ :ref:`see the updated recommendation ` for raising
+ a ``ValidationError``.
+
Backwards incompatible changes in 1.6
=====================================
diff --git a/tests/model_forms/models.py b/tests/model_forms/models.py
index a798f9bf95..a4cf9471de 100644
--- a/tests/model_forms/models.py
+++ b/tests/model_forms/models.py
@@ -11,6 +11,7 @@ from __future__ import unicode_literals
import os
import tempfile
+from django.core import validators
from django.core.exceptions import ImproperlyConfigured
from django.core.files.storage import FileSystemStorage
from django.db import models
@@ -286,3 +287,12 @@ class ColourfulItem(models.Model):
class ArticleStatusNote(models.Model):
name = models.CharField(max_length=20)
status = models.ManyToManyField(ArticleStatus)
+
+class CustomErrorMessage(models.Model):
+ name1 = models.CharField(max_length=50,
+ validators=[validators.validate_slug],
+ error_messages={'invalid': 'Model custom error message.'})
+
+ name2 = models.CharField(max_length=50,
+ validators=[validators.validate_slug],
+ error_messages={'invalid': 'Model custom error message.'})
diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py
index eea1ef9b68..39be824798 100644
--- a/tests/model_forms/tests.py
+++ b/tests/model_forms/tests.py
@@ -22,7 +22,7 @@ from .models import (Article, ArticleStatus, BetterWriter, BigInt, Book,
DerivedPost, ExplicitPK, FlexibleDatePost, ImprovedArticle,
ImprovedArticleWithParentLink, Inventory, Post, Price,
Product, TextFile, Writer, WriterProfile, Colour, ColourfulItem,
- ArticleStatusNote, DateTimePost, test_images)
+ ArticleStatusNote, DateTimePost, CustomErrorMessage, test_images)
if test_images:
from .models import ImageFile, OptionalImageFile
@@ -252,6 +252,12 @@ class StatusNoteCBM2mForm(forms.ModelForm):
fields = '__all__'
widgets = {'status': forms.CheckboxSelectMultiple}
+class CustomErrorMessageForm(forms.ModelForm):
+ name1 = forms.CharField(error_messages={'invalid': 'Form custom error message.'})
+
+ class Meta:
+ model = CustomErrorMessage
+
class ModelFormBaseTest(TestCase):
def test_base_form(self):
@@ -1762,6 +1768,18 @@ class OldFormForXTests(TestCase):
Hold down "Control", or "Command" on a Mac, to select more than one.
"""
% {'blue_pk': colour.pk})
+ def test_custom_error_messages(self) :
+ data = {'name1': '@#$!!**@#$', 'name2': '@#$!!**@#$'}
+ errors = CustomErrorMessageForm(data).errors
+ self.assertHTMLEqual(
+ str(errors['name1']),
+ '- Form custom error message.
'
+ )
+ self.assertHTMLEqual(
+ str(errors['name2']),
+ '- Model custom error message.
'
+ )
+
class M2mHelpTextTest(TestCase):
"""Tests for ticket #9321."""
diff --git a/tests/validators/tests.py b/tests/validators/tests.py
index 49389ef663..a1555d8e91 100644
--- a/tests/validators/tests.py
+++ b/tests/validators/tests.py
@@ -214,8 +214,8 @@ class TestSimpleValidators(TestCase):
def test_message_dict(self):
v = ValidationError({'first': ['First Problem']})
- self.assertEqual(str(v), str_prefix("{%(_)s'first': %(_)s'First Problem'}"))
- self.assertEqual(repr(v), str_prefix("ValidationError({%(_)s'first': %(_)s'First Problem'})"))
+ self.assertEqual(str(v), str_prefix("{%(_)s'first': [%(_)s'First Problem']}"))
+ self.assertEqual(repr(v), str_prefix("ValidationError({%(_)s'first': [%(_)s'First Problem']})"))
test_counter = 0
for validator, value, expected in TEST_DATA: