Index: django/test/testcases.py =================================================================== --- django/test/testcases.py (revision 6193) +++ django/test/testcases.py (working copy) @@ -8,6 +8,7 @@ from django.core.management import call_command from django.test import _doctest as doctest from django.test.client import Client +from django.utils.encoding import force_unicode normalize_long_ints = lambda s: re.sub(r'(? self.max_length: - raise ValidationError(ugettext(u'Ensure this value has at most %(max)d characters (it has %(length)d).') % {'max': self.max_length, 'length': value_length}) + raise ValidationError(self.error_messages['max_length'] % {'max': self.max_length, 'length': value_length}) if self.min_length is not None and value_length < self.min_length: - raise ValidationError(ugettext(u'Ensure this value has at least %(min)d characters (it has %(length)d).') % {'min': self.min_length, 'length': value_length}) + raise ValidationError(self.error_messages['min_length'] % {'min': self.min_length, 'length': value_length}) return value def widget_attrs(self, widget): @@ -131,6 +155,12 @@ return {'maxlength': str(self.max_length)} class IntegerField(Field): + default_error_messages = { + 'invalid': ugettext_lazy(u'Enter a whole number.'), + 'max_value': ugettext_lazy(u'Ensure this value is less than or equal to %s.'), + 'min_value': ugettext_lazy(u'Ensure this value is greater than or equal to %s.'), + } + def __init__(self, max_value=None, min_value=None, *args, **kwargs): self.max_value, self.min_value = max_value, min_value super(IntegerField, self).__init__(*args, **kwargs) @@ -146,14 +176,20 @@ try: value = int(value) except (ValueError, TypeError): - raise ValidationError(ugettext(u'Enter a whole number.')) + raise ValidationError(self.error_messages['invalid']) if self.max_value is not None and value > self.max_value: - raise ValidationError(ugettext(u'Ensure this value is less than or equal to %s.') % self.max_value) + raise ValidationError(self.error_messages['max_value'] % self.max_value) if self.min_value is not None and value < self.min_value: - raise ValidationError(ugettext(u'Ensure this value is greater than or equal to %s.') % self.min_value) + raise ValidationError(self.error_messages['min_value'] % self.min_value) return value class FloatField(Field): + default_error_messages = { + 'invalid': ugettext_lazy(u'Enter a number.'), + 'max_value': ugettext_lazy(u'Ensure this value is less than or equal to %s.'), + 'min_value': ugettext_lazy(u'Ensure this value is greater than or equal to %s.'), + } + def __init__(self, max_value=None, min_value=None, *args, **kwargs): self.max_value, self.min_value = max_value, min_value Field.__init__(self, *args, **kwargs) @@ -169,14 +205,23 @@ try: value = float(value) except (ValueError, TypeError): - raise ValidationError(ugettext('Enter a number.')) + raise ValidationError(self.error_messages['invalid']) if self.max_value is not None and value > self.max_value: - raise ValidationError(ugettext('Ensure this value is less than or equal to %s.') % self.max_value) + raise ValidationError(self.error_messages['max_value'] % self.max_value) if self.min_value is not None and value < self.min_value: - raise ValidationError(ugettext('Ensure this value is greater than or equal to %s.') % self.min_value) + raise ValidationError(self.error_messages['min_value'] % self.min_value) return value class DecimalField(Field): + default_error_messages = { + 'invalid': ugettext_lazy(u'Enter a number.'), + 'max_value': ugettext_lazy(u'Ensure this value is less than or equal to %s.'), + 'min_value': ugettext_lazy(u'Ensure this value is greater than or equal to %s.'), + 'max_digits': ugettext_lazy('Ensure that there are no more than %s digits in total.'), + 'max_decimal_places': ugettext_lazy('Ensure that there are no more than %s decimal places.'), + 'max_whole_digits': ugettext_lazy('Ensure that there are no more than %s digits before the decimal point.') + } + def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs): self.max_value, self.min_value = max_value, min_value self.max_digits, self.decimal_places = max_digits, decimal_places @@ -196,20 +241,20 @@ try: value = Decimal(value) except DecimalException: - raise ValidationError(ugettext('Enter a number.')) + raise ValidationError(self.error_messages['invalid']) pieces = str(value).lstrip("-").split('.') decimals = (len(pieces) == 2) and len(pieces[1]) or 0 digits = len(pieces[0]) if self.max_value is not None and value > self.max_value: - raise ValidationError(ugettext('Ensure this value is less than or equal to %s.') % self.max_value) + raise ValidationError(self.error_messages['max_value'] % self.max_value) if self.min_value is not None and value < self.min_value: - raise ValidationError(ugettext('Ensure this value is greater than or equal to %s.') % self.min_value) + raise ValidationError(self.error_messages['min_value'] % self.min_value) if self.max_digits is not None and (digits + decimals) > self.max_digits: - raise ValidationError(ugettext('Ensure that there are no more than %s digits in total.') % self.max_digits) + raise ValidationError(self.error_messages['max_digits'] % self.max_digits) if self.decimal_places is not None and decimals > self.decimal_places: - raise ValidationError(ugettext('Ensure that there are no more than %s decimal places.') % self.decimal_places) + raise ValidationError(self.error_messages['max_decimal_places'] % self.decimal_places) if self.max_digits is not None and self.decimal_places is not None and digits > (self.max_digits - self.decimal_places): - raise ValidationError(ugettext('Ensure that there are no more than %s digits before the decimal point.') % (self.max_digits - self.decimal_places)) + raise ValidationError(self.error_messages['max_whole_digits'] % (self.max_digits - self.decimal_places)) return value DEFAULT_DATE_INPUT_FORMATS = ( @@ -221,6 +266,10 @@ ) class DateField(Field): + default_error_messages = { + 'invalid': ugettext_lazy(u'Enter a valid date.'), + } + def __init__(self, input_formats=None, *args, **kwargs): super(DateField, self).__init__(*args, **kwargs) self.input_formats = input_formats or DEFAULT_DATE_INPUT_FORMATS @@ -242,7 +291,7 @@ return datetime.date(*time.strptime(value, format)[:3]) except ValueError: continue - raise ValidationError(ugettext(u'Enter a valid date.')) + raise ValidationError(self.error_messages['invalid']) DEFAULT_TIME_INPUT_FORMATS = ( '%H:%M:%S', # '14:30:59' @@ -250,6 +299,10 @@ ) class TimeField(Field): + default_error_messages = { + 'invalid': ugettext_lazy(u'Enter a valid time.') + } + def __init__(self, input_formats=None, *args, **kwargs): super(TimeField, self).__init__(*args, **kwargs) self.input_formats = input_formats or DEFAULT_TIME_INPUT_FORMATS @@ -269,7 +322,7 @@ return datetime.time(*time.strptime(value, format)[3:6]) except ValueError: continue - raise ValidationError(ugettext(u'Enter a valid time.')) + raise ValidationError(self.error_messages['invalid']) DEFAULT_DATETIME_INPUT_FORMATS = ( '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59' @@ -284,6 +337,10 @@ ) class DateTimeField(Field): + default_error_messages = { + 'invalid': ugettext_lazy(u'Enter a valid date/time.'), + } + def __init__(self, input_formats=None, *args, **kwargs): super(DateTimeField, self).__init__(*args, **kwargs) self.input_formats = input_formats or DEFAULT_DATETIME_INPUT_FORMATS @@ -305,7 +362,7 @@ return datetime.datetime(*time.strptime(value, format)[:6]) except ValueError: continue - raise ValidationError(ugettext(u'Enter a valid date/time.')) + raise ValidationError(self.error_messages['invalid']) class RegexField(CharField): def __init__(self, regex, max_length=None, min_length=None, error_message=None, *args, **kwargs): @@ -314,11 +371,15 @@ error_message is an optional error message to use, if 'Enter a valid value' is too generic for you. """ + # error_message is just kept for backwards compatibility: + if error_message: + error_messages = kwargs.get('error_messages') or {} + error_messages['invalid'] = error_message + kwargs['error_messages'] = error_messages super(RegexField, self).__init__(max_length, min_length, *args, **kwargs) if isinstance(regex, basestring): regex = re.compile(regex) self.regex = regex - self.error_message = error_message or ugettext(u'Enter a valid value.') def clean(self, value): """ @@ -329,7 +390,7 @@ if value == u'': return value if not self.regex.search(value): - raise ValidationError(self.error_message) + raise ValidationError(self.error_messages['invalid']) return value email_re = re.compile( @@ -338,9 +399,13 @@ r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE) # domain class EmailField(RegexField): + default_error_messages = { + 'invalid': ugettext_lazy(u'Enter a valid e-mail address.'), + } + def __init__(self, max_length=None, min_length=None, *args, **kwargs): - RegexField.__init__(self, email_re, max_length, min_length, - ugettext(u'Enter a valid e-mail address.'), *args, **kwargs) + RegexField.__init__(self, email_re, max_length, min_length, *args, + **kwargs) try: from django.conf import settings @@ -364,6 +429,12 @@ class FileField(Field): widget = FileInput + default_error_messages = { + 'invalid': ugettext_lazy(u"No file was submitted. Check the encoding type on the form."), + 'missing': ugettext_lazy(u"No file was submitted."), + 'empty': ugettext_lazy(u"The submitted file is empty."), + } + def __init__(self, *args, **kwargs): super(FileField, self).__init__(*args, **kwargs) @@ -374,14 +445,18 @@ try: f = UploadedFile(data['filename'], data['content']) except TypeError: - raise ValidationError(ugettext(u"No file was submitted. Check the encoding type on the form.")) + raise ValidationError(self.error_messages['invalid']) except KeyError: - raise ValidationError(ugettext(u"No file was submitted.")) + raise ValidationError(self.error_messages['missing']) if not f.content: - raise ValidationError(ugettext(u"The submitted file is empty.")) + raise ValidationError(self.error_messages['empty']) return f class ImageField(FileField): + default_error_messages = { + 'invalid_image': ugettext_lazy(u"Upload a valid image. The file you uploaded was either not an image or a corrupted image."), + } + def clean(self, data): """ Checks that the file-upload field data contains a valid image (GIF, JPG, @@ -402,7 +477,7 @@ trial_image = Image.open(StringIO(f.content)) trial_image.verify() except Exception: # Python Imaging Library doesn't recognize it as an image - raise ValidationError(ugettext(u"Upload a valid image. The file you uploaded was either not an image or a corrupted image.")) + raise ValidationError(self.error_messages['invalid_image']) return f url_re = re.compile( @@ -414,9 +489,15 @@ r'(?:/?|/\S+)$', re.IGNORECASE) class URLField(RegexField): + default_error_messages = { + 'invalid': ugettext_lazy(u'Enter a valid URL.'), + 'invalid_link': ugettext_lazy(u'This URL appears to be a broken link.'), + } + def __init__(self, max_length=None, min_length=None, verify_exists=False, validator_user_agent=URL_VALIDATOR_USER_AGENT, *args, **kwargs): - super(URLField, self).__init__(url_re, max_length, min_length, ugettext(u'Enter a valid URL.'), *args, **kwargs) + super(URLField, self).__init__(url_re, max_length, min_length, *args, + **kwargs) self.verify_exists = verify_exists self.user_agent = validator_user_agent @@ -441,9 +522,9 @@ req = urllib2.Request(value, None, headers) u = urllib2.urlopen(req) except ValueError: - raise ValidationError(ugettext(u'Enter a valid URL.')) + raise ValidationError(self.error_messages['invalid']) except: # urllib2.URLError, httplib.InvalidURL, etc. - raise ValidationError(ugettext(u'This URL appears to be a broken link.')) + raise ValidationError(self.error_messages['invalid_link']) return value class BooleanField(Field): @@ -466,9 +547,14 @@ class ChoiceField(Field): widget = Select + default_error_messages = { + 'invalid_choice': ugettext_lazy(u'Select a valid choice. That choice is not one of the available choices.'), + } - def __init__(self, choices=(), required=True, widget=None, label=None, initial=None, help_text=None): - super(ChoiceField, self).__init__(required, widget, label, initial, help_text) + def __init__(self, choices=(), required=True, widget=None, label=None, + initial=None, help_text=None, *args, **kwargs): + super(ChoiceField, self).__init__(required, widget, label, initial, + help_text, *args, **kwargs) self.choices = choices def _get_choices(self): @@ -494,29 +580,33 @@ return value valid_values = set([smart_unicode(k) for k, v in self.choices]) if value not in valid_values: - raise ValidationError(ugettext(u'Select a valid choice. That choice is not one of the available choices.')) + raise ValidationError(self.error_messages['invalid_choice'] % {'value': value}) return value class MultipleChoiceField(ChoiceField): hidden_widget = MultipleHiddenInput widget = SelectMultiple + default_error_messages = { + 'invalid_choice': ugettext_lazy(u'Select a valid choice. %(value)s is not one of the available choices.'), + 'invalid_list': ugettext_lazy(u'Enter a list of values.'), + } def clean(self, value): """ Validates that the input is a list or tuple. """ if self.required and not value: - raise ValidationError(ugettext(u'This field is required.')) + raise ValidationError(self.error_messages['required']) elif not self.required and not value: return [] if not isinstance(value, (list, tuple)): - raise ValidationError(ugettext(u'Enter a list of values.')) + raise ValidationError(self.error_messages['invalid_list']) new_value = [smart_unicode(val) for val in value] # Validate that each value in the value list is in self.choices. valid_values = set([smart_unicode(k) for k, v in self.choices]) for val in new_value: if val not in valid_values: - raise ValidationError(ugettext(u'Select a valid choice. %s is not one of the available choices.') % val) + raise ValidationError(self.error_messages['invalid_choice'] % {'value': val}) return new_value class ComboField(Field): @@ -559,6 +649,9 @@ You'll probably want to use this with MultiWidget. """ + default_error_messages = { + 'invalid': ugettext_lazy(u'Enter a list of values.'), + } def __init__(self, fields=(), *args, **kwargs): super(MultiValueField, self).__init__(*args, **kwargs) # Set 'required' to False on the individual fields, because the @@ -582,18 +675,18 @@ if not value or isinstance(value, (list, tuple)): if not value or not [v for v in value if v not in EMPTY_VALUES]: if self.required: - raise ValidationError(ugettext(u'This field is required.')) + raise ValidationError(self.error_messages['required']) else: return self.compress([]) else: - raise ValidationError(ugettext(u'Enter a list of values.')) + raise ValidationError(self.error_messages['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 EMPTY_VALUES: - raise ValidationError(ugettext(u'This field is required.')) + raise ValidationError(self.error_messages['required']) try: clean_data.append(field.clean(field_value)) except ValidationError, e: @@ -617,8 +710,16 @@ raise NotImplementedError('Subclasses must implement this method.') class SplitDateTimeField(MultiValueField): + default_error_messages = { + 'invalid_date': ugettext_lazy(u'Enter a valid date.'), + 'invalid_time': ugettext_lazy(u'Enter a valid time.'), + } def __init__(self, *args, **kwargs): - fields = (DateField(), TimeField()) + e = self.default_error_messages.copy() + if 'error_messages' in kwargs: + e.update(kwargs['error_messages']) + fields = (DateField(error_messages={'invalid': e['invalid_date']}), + TimeField(error_messages={'invalid': e['invalid_time']})) super(SplitDateTimeField, self).__init__(fields, *args, **kwargs) def compress(self, data_list): @@ -626,8 +727,8 @@ # Raise a validation error if time or date is empty # (possible if SplitDateTimeField has required=False). if data_list[0] in EMPTY_VALUES: - raise ValidationError(ugettext(u'Enter a valid date.')) + raise ValidationError(self.error_messages['invalid_date']) if data_list[1] in EMPTY_VALUES: - raise ValidationError(ugettext(u'Enter a valid time.')) + raise ValidationError(self.error_messages['invalid_time']) return datetime.datetime.combine(*data_list) return None Index: tests/regressiontests/forms/util.py =================================================================== --- tests/regressiontests/forms/util.py (revision 6193) +++ tests/regressiontests/forms/util.py (working copy) @@ -42,4 +42,11 @@ # Can take a mixture in a list. >>> print ValidationError(["First error.", u"Not \u03C0.", ugettext_lazy("Error.")]).messages
Sender: A valid e-mail address, please.
Cc myself:
+``error_messages`` +~~~~~~~~~~~~~~~~~~ + +The ``error_messages`` argument lets you override the default messages which the +field will raise. Pass in a dictionary with keys matching the error messages you +want to override. For example:: + + >>> generic = forms.CharField() + >>> generic.clean('') + Traceback (most recent call last): + ... + ValidationError: [u'This field is required.'] + + >>> name = forms.CharField(error_messages={'required': 'Please enter your name'}) + >>> name.clean('') + Traceback (most recent call last): + ... + ValidationError: [u'Please enter your name'] + +In the `built-in Field classes`_ section below, each Field defines the error +message keys it uses. + Dynamic initial values ---------------------- @@ -1084,6 +1106,7 @@ * Empty value: ``None`` * Normalizes to: A Python ``True`` or ``False`` value. * Validates nothing (i.e., it never raises a ``ValidationError``). + * Error message keys: ``required`` ``CharField`` ~~~~~~~~~~~~~ @@ -1092,6 +1115,7 @@ * Empty value: ``''`` (an empty string) * Normalizes to: A Unicode object. * Validates nothing, unless ``max_length`` or ``min_length`` is provided. + * Error message keys: ``required``, ``max_length``, ``min_length`` Has two optional arguments for validation, ``max_length`` and ``min_length``. If provided, these arguments ensure that the string is at most or at least the @@ -1104,6 +1128,7 @@ * Empty value: ``''`` (an empty string) * Normalizes to: A Unicode object. * Validates that the given value exists in the list of choices. + * Error message keys: ``required``, ``invalid_choice`` Takes one extra argument, ``choices``, which is an iterable (e.g., a list or tuple) of 2-tuples to use as choices for this field. @@ -1116,6 +1141,7 @@ * Normalizes to: A Python ``datetime.date`` object. * Validates that the given value is either a ``datetime.date``, ``datetime.datetime`` or string formatted in a particular date format. + * Error message keys: ``required``, ``invalid`` Takes one optional argument, ``input_formats``, which is a list of formats used to attempt to convert a string to a valid ``datetime.date`` object. @@ -1136,6 +1162,7 @@ * Normalizes to: A Python ``datetime.datetime`` object. * Validates that the given value is either a ``datetime.datetime``, ``datetime.date`` or string formatted in a particular datetime format. + * Error message keys: ``required``, ``invalid`` Takes one optional argument, ``input_formats``, which is a list of formats used to attempt to convert a string to a valid ``datetime.datetime`` object. @@ -1162,6 +1189,9 @@ * Normalizes to: A Python ``decimal``. * Validates that the given value is a decimal. Leading and trailing whitespace is ignored. + * Error message keys: ``required``, ``invalid``, ``max_value``, + ``min_value``, ``max_digits``, ``max_decimal_places``, + ``max_whole_digits`` Takes four optional arguments: ``max_value``, ``min_value``, ``max_digits``, and ``decimal_places``. The first two define the limits for the fields value. @@ -1178,6 +1208,7 @@ * Normalizes to: A Unicode object. * Validates that the given value is a valid e-mail address, using a moderately complex regular expression. + * Error message keys: ``required``, ``invalid`` Has two optional arguments for validation, ``max_length`` and ``min_length``. If provided, these arguments ensure that the string is at most or at least the @@ -1193,6 +1224,7 @@ * Normalizes to: An ``UploadedFile`` object that wraps the file content and file name into a single object. * Validates that non-empty file data has been bound to the form. + * Error message keys: ``required``, ``invalid``, ``missing``, ``empty`` An ``UploadedFile`` object has two attributes: @@ -1223,6 +1255,8 @@ and file name into a single object. * Validates that file data has been bound to the form, and that the file is of an image format understood by PIL. + * Error message keys: ``required``, ``invalid``, ``missing``, ``empty``, + ``invalid_image`` Using an ImageField requires that the `Python Imaging Library`_ is installed. @@ -1239,6 +1273,8 @@ * Normalizes to: A Python integer or long integer. * Validates that the given value is an integer. Leading and trailing whitespace is allowed, as in Python's ``int()`` function. + * Error message keys: ``required``, ``invalid``, ``max_value``, + ``min_value`` Takes two optional arguments for validation, ``max_value`` and ``min_value``. These control the range of values permitted in the field. @@ -1251,6 +1287,7 @@ * Normalizes to: A list of Unicode objects. * Validates that every value in the given list of values exists in the list of choices. + * Error message keys: ``required``, ``invalid_choice``, ``invalid_list`` Takes one extra argument, ``choices``, which is an iterable (e.g., a list or tuple) of 2-tuples to use as choices for this field. @@ -1271,6 +1308,7 @@ * Normalizes to: A Unicode object. * Validates that the given value matches against a certain regular expression. + * Error message keys: ``required``, ``invalid`` Takes one required argument, ``regex``, which is a regular expression specified either as a string or a compiled regular expression object. @@ -1282,11 +1320,13 @@ ====================== ===================================================== ``max_length`` Ensures the string has at most this many characters. ``min_length`` Ensures the string has at least this many characters. - ``error_message`` Error message to return for failed validation. If no - message is provided, a generic error message will be - used. ====================== ===================================================== +The optional argument ``error_message`` is also accepted for backwards +compatibility. The preferred way to provide an error message is to use the +``error_messages`` argument, passing a dictionary with ``'invalid'`` as a key +and the error message as the value. + ``TimeField`` ~~~~~~~~~~~~~ @@ -1295,6 +1335,7 @@ * Normalizes to: A Python ``datetime.time`` object. * Validates that the given value is either a ``datetime.time`` or string formatted in a particular time format. + * Error message keys: ``required``, ``invalid`` Takes one optional argument, ``input_formats``, which is a list of formats used to attempt to convert a string to a valid ``datetime.time`` object. @@ -1311,6 +1352,7 @@ * Empty value: ``''`` (an empty string) * Normalizes to: A Unicode object. * Validates that the given value is a valid URL. + * Error message keys: ``required``, ``invalid``, ``invalid_link`` Takes the following optional arguments: