Browse Source

Fixed #20199 -- Allow ModelForm fields to override error_messages from model fields

Loic Bistuer 11 years ago
parent
commit
ee77d4b253

+ 3 - 6
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

+ 5 - 5
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()

+ 24 - 9
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):

+ 12 - 4
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()

+ 4 - 1
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

+ 3 - 3
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
 

+ 3 - 1
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):

+ 1 - 3
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
 

+ 2 - 6
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):

+ 89 - 47
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)
-
-        msg = self.error_messages['invalid'] % value
-        raise exceptions.ValidationError(msg)
+            raise exceptions.ValidationError(
+                self.error_messages['invalid_date'],
+                code='invalid_date',
+                params={'value': value},
+            )
+
+        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)
-
-        msg = self.error_messages['invalid'] % value
-        raise exceptions.ValidationError(msg)
+            raise exceptions.ValidationError(
+                self.error_messages['invalid_date'],
+                code='invalid_date',
+                params={'value': value},
+            )
+
+        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)
-
-        msg = self.error_messages['invalid'] % value
-        raise exceptions.ValidationError(msg)
+            raise exceptions.ValidationError(
+                self.error_messages['invalid_time'],
+                code='invalid_time',
+                params={'value': value},
+            )
+
+        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):

+ 5 - 2
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

+ 62 - 34
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

+ 7 - 2
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:

+ 25 - 7
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)

+ 9 - 4
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):

+ 3 - 2
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

+ 101 - 9
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 <raising-validation-error>`
+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 <modifying-field-errors>`.
 
   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
 ------------------------------------------

+ 10 - 4
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.

+ 7 - 0
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 <raising-validation-error>` for raising
+  a ``ValidationError``.
+
 Backwards incompatible changes in 1.6
 =====================================
 

+ 10 - 0
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.'})

+ 19 - 1
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):
         </select> <span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span></p>"""
             % {'blue_pk': colour.pk})
 
+    def test_custom_error_messages(self) :
+        data = {'name1': '@#$!!**@#$', 'name2': '@#$!!**@#$'}
+        errors = CustomErrorMessageForm(data).errors
+        self.assertHTMLEqual(
+            str(errors['name1']),
+            '<ul class="errorlist"><li>Form custom error message.</li></ul>'
+        )
+        self.assertHTMLEqual(
+            str(errors['name2']),
+            '<ul class="errorlist"><li>Model custom error message.</li></ul>'
+        )
+
 
 class M2mHelpTextTest(TestCase):
     """Tests for ticket #9321."""

+ 2 - 2
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: