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

This commit is contained in:
Loic Bistuer 2013-06-05 14:55:05 -04:00 committed by Tim Graham
parent f34cfec0fa
commit ee77d4b253
22 changed files with 403 additions and 149 deletions

View File

@ -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

View File

@ -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()

View File

@ -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):

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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
------------------------------------------

View File

@ -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.

View File

@ -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
=====================================

View File

@ -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.'})

View File

@ -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."""

View File

@ -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: