From 26ea06b0ab423348e40652dd9e3f530a08b93582 Mon Sep 17 00:00:00 2001 From: Gary Wilson Jr Date: Sun, 28 Oct 2007 05:40:26 +0000 Subject: [PATCH] Fixed #3457 -- Allow overridding of error messages for newforms Fields. git-svn-id: http://code.djangoproject.com/svn/django/trunk@6625 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/newforms/fields.py | 200 ++++++++--- django/newforms/util.py | 10 +- django/test/testcases.py | 2 +- docs/newforms.txt | 51 ++- tests/regressiontests/forms/error_messages.py | 315 ++++++++++++++++++ tests/regressiontests/forms/tests.py | 2 + tests/regressiontests/forms/util.py | 7 + 7 files changed, 533 insertions(+), 54 deletions(-) create mode 100644 tests/regressiontests/forms/error_messages.py diff --git a/django/newforms/fields.py b/django/newforms/fields.py index 950307b15f..33f63b5aeb 100644 --- a/django/newforms/fields.py +++ b/django/newforms/fields.py @@ -16,7 +16,7 @@ try: except NameError: from sets import Set as set -from django.utils.translation import ugettext +from django.utils.translation import ugettext_lazy from django.utils.encoding import StrAndUnicode, smart_unicode from util import ErrorList, ValidationError @@ -41,11 +41,16 @@ EMPTY_VALUES = (None, '') class Field(object): widget = TextInput # Default widget to use when rendering this type of Field. hidden_widget = HiddenInput # Default widget to use when rendering this as "hidden". + default_error_messages = { + 'required': ugettext_lazy(u'This field is required.'), + 'invalid': ugettext_lazy(u'Enter a valid value.'), + } # Tracks each time a Field instance is created. Used to retain order. creation_counter = 0 - def __init__(self, required=True, widget=None, label=None, initial=None, help_text=None): + def __init__(self, required=True, widget=None, label=None, initial=None, + help_text=None, error_messages=None): # required -- Boolean that specifies whether the field is required. # True by default. # widget -- A Widget class, or instance of a Widget class, that should @@ -78,6 +83,20 @@ class Field(object): self.creation_counter = Field.creation_counter Field.creation_counter += 1 + self.error_messages = self._build_error_messages(error_messages) + + def _build_error_messages(self, extra_error_messages): + error_messages = {} + def get_default_error_messages(klass): + for base_class in klass.__bases__: + get_default_error_messages(base_class) + if hasattr(klass, 'default_error_messages'): + error_messages.update(klass.default_error_messages) + get_default_error_messages(self.__class__) + if extra_error_messages: + error_messages.update(extra_error_messages) + return error_messages + def clean(self, value): """ Validates the given value and returns its "cleaned" value as an @@ -86,7 +105,7 @@ class Field(object): Raises ValidationError for any errors. """ if self.required and value in EMPTY_VALUES: - raise ValidationError(ugettext(u'This field is required.')) + raise ValidationError(self.error_messages['required']) return value def widget_attrs(self, widget): @@ -104,6 +123,11 @@ class Field(object): return result class CharField(Field): + default_error_messages = { + 'max_length': ugettext_lazy(u'Ensure this value has at most %(max)d characters (it has %(length)d).'), + 'min_length': ugettext_lazy(u'Ensure this value has at least %(min)d characters (it has %(length)d).'), + } + def __init__(self, max_length=None, min_length=None, *args, **kwargs): self.max_length, self.min_length = max_length, min_length super(CharField, self).__init__(*args, **kwargs) @@ -116,9 +140,9 @@ class CharField(Field): value = smart_unicode(value) value_length = len(value) if self.max_length is not None and value_length > 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): @@ -127,6 +151,12 @@ class CharField(Field): 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) @@ -142,14 +172,20 @@ class IntegerField(Field): try: value = int(str(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) @@ -165,14 +201,23 @@ class FloatField(Field): 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 @@ -192,20 +237,20 @@ class DecimalField(Field): 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 = ( @@ -217,6 +262,10 @@ DEFAULT_DATE_INPUT_FORMATS = ( ) 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 @@ -238,7 +287,7 @@ class DateField(Field): 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' @@ -246,6 +295,10 @@ DEFAULT_TIME_INPUT_FORMATS = ( ) 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 @@ -265,7 +318,7 @@ class TimeField(Field): 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' @@ -281,6 +334,9 @@ DEFAULT_DATETIME_INPUT_FORMATS = ( class DateTimeField(Field): widget = DateTimeInput + 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) @@ -302,14 +358,14 @@ class DateTimeField(Field): # Input comes from a SplitDateTimeWidget, for example. So, it's two # components: date and time. if len(value) != 2: - raise ValidationError(ugettext(u'Enter a valid date/time.')) + raise ValidationError(self.error_messages['invalid']) value = '%s %s' % tuple(value) for format in self.input_formats: try: 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): @@ -318,11 +374,15 @@ class RegexField(CharField): 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): """ @@ -333,7 +393,7 @@ class RegexField(CharField): 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( @@ -342,9 +402,13 @@ email_re = re.compile( 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 @@ -368,6 +432,12 @@ class UploadedFile(StrAndUnicode): 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) @@ -378,14 +448,18 @@ class FileField(Field): 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, @@ -406,7 +480,7 @@ class ImageField(FileField): 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( @@ -418,9 +492,15 @@ url_re = re.compile( 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 @@ -445,9 +525,9 @@ class URLField(RegexField): 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): @@ -474,9 +554,14 @@ class NullBooleanField(BooleanField): 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): @@ -502,29 +587,33 @@ class ChoiceField(Field): 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): @@ -567,6 +656,10 @@ class MultiValueField(Field): 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 @@ -590,18 +683,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 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: @@ -625,8 +718,19 @@ class MultiValueField(Field): 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()) + errors = self.default_error_messages.copy() + if 'error_messages' in kwargs: + errors.update(kwargs['error_messages']) + fields = ( + DateField(error_messages={'invalid': errors['invalid_date']}), + TimeField(error_messages={'invalid': errors['invalid_time']}), + ) super(SplitDateTimeField, self).__init__(fields, *args, **kwargs) def compress(self, data_list): @@ -634,16 +738,18 @@ class SplitDateTimeField(MultiValueField): # 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 ipv4_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$') class IPAddressField(RegexField): + default_error_messages = { + 'invalid': ugettext_lazy(u'Enter a valid IPv4 address.'), + } + def __init__(self, *args, **kwargs): - RegexField.__init__(self, ipv4_re, - error_message=ugettext(u'Enter a valid IPv4 address.'), - *args, **kwargs) + super(IPAddressField, self).__init__(ipv4_re, *args, **kwargs) diff --git a/django/newforms/util.py b/django/newforms/util.py index e19d894397..490fbcaed0 100644 --- a/django/newforms/util.py +++ b/django/newforms/util.py @@ -42,13 +42,18 @@ class ErrorList(list, StrAndUnicode): if not self: return u'' return u'\n'.join([u'* %s' % force_unicode(e) for e in self]) + def __repr__(self): + return repr([force_unicode(e) for e in self]) + class ValidationError(Exception): def __init__(self, message): - "ValidationError can be passed a string or a list." + """ + ValidationError can be passed any object that can be printed (usually + a string) or a list of objects. + """ if isinstance(message, list): self.messages = ErrorList([smart_unicode(msg) for msg in message]) else: - assert isinstance(message, (basestring, Promise)), ("%s should be a basestring or lazy translation" % repr(message)) message = smart_unicode(message) self.messages = ErrorList([message]) @@ -58,4 +63,3 @@ class ValidationError(Exception): # AttributeError: ValidationError instance has no attribute 'args' # See http://www.python.org/doc/current/tut/node10.html#handling return repr(self.messages) - diff --git a/django/test/testcases.py b/django/test/testcases.py index 6b7714ec7b..732e713d4a 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -146,7 +146,7 @@ class TestCase(unittest.TestCase): " context %d does not contain the" " error '%s' (actual errors: %s)" % (field, form, i, err, - list(field_errors))) + repr(field_errors))) elif field in context[form].fields: self.fail("The field '%s' on form '%s' in context %d" " contains no errors" % (field, form, i)) diff --git a/docs/newforms.txt b/docs/newforms.txt index 5e33a478ee..593e9216d7 100644 --- a/docs/newforms.txt +++ b/docs/newforms.txt @@ -1078,6 +1078,30 @@ fields. We've specified ``auto_id=False`` to simplify the output::

Sender: A valid e-mail address, please.

Cc myself:

+``error_messages`` +~~~~~~~~~~~~~~~~~~ + +**New in Django development version** + +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 ---------------------- @@ -1143,6 +1167,7 @@ For each field, we describe the default widget used if you don't specify * Normalizes to: A Python ``True`` or ``False`` value. * Validates that the check box is checked (i.e. the value is ``True``) if the field has ``required=True``. + * Error message keys: ``required`` **New in Django development version:** The empty value for a ``CheckboxInput`` (and hence the standard ``BooleanField``) has changed to return ``False`` @@ -1162,6 +1187,7 @@ instead of ``None`` in the development version. * Normalizes to: A Unicode object. * Validates ``max_length`` or ``min_length``, if they are provided. Otherwise, all inputs are valid. + * 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 @@ -1174,6 +1200,7 @@ given length. * 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. @@ -1186,6 +1213,7 @@ tuple) of 2-tuples to use as choices for this field. * 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. @@ -1206,6 +1234,7 @@ If no ``input_formats`` argument is provided, the default input formats are:: * 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. @@ -1235,6 +1264,9 @@ If no ``input_formats`` argument is provided, the default input formats are:: * 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. @@ -1251,6 +1283,7 @@ decimal places permitted. * 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 @@ -1266,6 +1299,7 @@ given length. * 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: @@ -1296,6 +1330,8 @@ When you use a ``FileField`` on a form, you must also remember to 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. @@ -1312,6 +1348,8 @@ When you use a ``FileField`` on a form, you must also remember to * 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. @@ -1324,6 +1362,7 @@ These control the range of values permitted in the field. * Normalizes to: A Unicode object. * Validates that the given value is a valid IPv4 address, using a regular expression. + * Error message keys: ``required``, ``invalid`` ``MultipleChoiceField`` ~~~~~~~~~~~~~~~~~~~~~~~ @@ -1333,6 +1372,7 @@ These control the range of values permitted in the field. * 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. @@ -1353,6 +1393,7 @@ tuple) of 2-tuples to use as choices for this field. * 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. @@ -1364,11 +1405,13 @@ Also takes the following optional arguments: ====================== ===================================================== ``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`` ~~~~~~~~~~~~~ @@ -1377,6 +1420,7 @@ Also takes the following optional arguments: * 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. @@ -1393,6 +1437,7 @@ If no ``input_formats`` argument is provided, the default input formats are:: * 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: diff --git a/tests/regressiontests/forms/error_messages.py b/tests/regressiontests/forms/error_messages.py new file mode 100644 index 0000000000..ff7e110f6f --- /dev/null +++ b/tests/regressiontests/forms/error_messages.py @@ -0,0 +1,315 @@ +# -*- coding: utf-8 -*- +tests = r""" +>>> from django.newforms import * + +# CharField ################################################################### + +>>> e = {'required': 'REQUIRED'} +>>> e['min_length'] = 'LENGTH %(length)s, MIN LENGTH %(min)s' +>>> e['max_length'] = 'LENGTH %(length)s, MAX LENGTH %(max)s' +>>> f = CharField(min_length=5, max_length=10, error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('1234') +Traceback (most recent call last): +... +ValidationError: [u'LENGTH 4, MIN LENGTH 5'] +>>> f.clean('12345678901') +Traceback (most recent call last): +... +ValidationError: [u'LENGTH 11, MAX LENGTH 10'] + +# IntegerField ################################################################ + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid'] = 'INVALID' +>>> e['min_value'] = 'MIN VALUE IS %s' +>>> e['max_value'] = 'MAX VALUE IS %s' +>>> f = IntegerField(min_value=5, max_value=10, error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('abc') +Traceback (most recent call last): +... +ValidationError: [u'INVALID'] +>>> f.clean('4') +Traceback (most recent call last): +... +ValidationError: [u'MIN VALUE IS 5'] +>>> f.clean('11') +Traceback (most recent call last): +... +ValidationError: [u'MAX VALUE IS 10'] + +# FloatField ################################################################## + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid'] = 'INVALID' +>>> e['min_value'] = 'MIN VALUE IS %s' +>>> e['max_value'] = 'MAX VALUE IS %s' +>>> f = FloatField(min_value=5, max_value=10, error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('abc') +Traceback (most recent call last): +... +ValidationError: [u'INVALID'] +>>> f.clean('4') +Traceback (most recent call last): +... +ValidationError: [u'MIN VALUE IS 5'] +>>> f.clean('11') +Traceback (most recent call last): +... +ValidationError: [u'MAX VALUE IS 10'] + +# DecimalField ################################################################ + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid'] = 'INVALID' +>>> e['min_value'] = 'MIN VALUE IS %s' +>>> e['max_value'] = 'MAX VALUE IS %s' +>>> e['max_digits'] = 'MAX DIGITS IS %s' +>>> e['max_decimal_places'] = 'MAX DP IS %s' +>>> e['max_whole_digits'] = 'MAX DIGITS BEFORE DP IS %s' +>>> f = DecimalField(min_value=5, max_value=10, error_messages=e) +>>> f2 = DecimalField(max_digits=4, decimal_places=2, error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('abc') +Traceback (most recent call last): +... +ValidationError: [u'INVALID'] +>>> f.clean('4') +Traceback (most recent call last): +... +ValidationError: [u'MIN VALUE IS 5'] +>>> f.clean('11') +Traceback (most recent call last): +... +ValidationError: [u'MAX VALUE IS 10'] +>>> f2.clean('123.45') +Traceback (most recent call last): +... +ValidationError: [u'MAX DIGITS IS 4'] +>>> f2.clean('1.234') +Traceback (most recent call last): +... +ValidationError: [u'MAX DP IS 2'] +>>> f2.clean('123.4') +Traceback (most recent call last): +... +ValidationError: [u'MAX DIGITS BEFORE DP IS 2'] + +# DateField ################################################################### + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid'] = 'INVALID' +>>> f = DateField(error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('abc') +Traceback (most recent call last): +... +ValidationError: [u'INVALID'] + +# TimeField ################################################################### + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid'] = 'INVALID' +>>> f = TimeField(error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('abc') +Traceback (most recent call last): +... +ValidationError: [u'INVALID'] + +# DateTimeField ############################################################### + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid'] = 'INVALID' +>>> f = DateTimeField(error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('abc') +Traceback (most recent call last): +... +ValidationError: [u'INVALID'] + +# RegexField ################################################################## + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid'] = 'INVALID' +>>> e['min_length'] = 'LENGTH %(length)s, MIN LENGTH %(min)s' +>>> e['max_length'] = 'LENGTH %(length)s, MAX LENGTH %(max)s' +>>> f = RegexField(r'^\d+$', min_length=5, max_length=10, error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('abcde') +Traceback (most recent call last): +... +ValidationError: [u'INVALID'] +>>> f.clean('1234') +Traceback (most recent call last): +... +ValidationError: [u'LENGTH 4, MIN LENGTH 5'] +>>> f.clean('12345678901') +Traceback (most recent call last): +... +ValidationError: [u'LENGTH 11, MAX LENGTH 10'] + +# EmailField ################################################################## + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid'] = 'INVALID' +>>> e['min_length'] = 'LENGTH %(length)s, MIN LENGTH %(min)s' +>>> e['max_length'] = 'LENGTH %(length)s, MAX LENGTH %(max)s' +>>> f = EmailField(min_length=8, max_length=10, error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('abcdefgh') +Traceback (most recent call last): +... +ValidationError: [u'INVALID'] +>>> f.clean('a@b.com') +Traceback (most recent call last): +... +ValidationError: [u'LENGTH 7, MIN LENGTH 8'] +>>> f.clean('aye@bee.com') +Traceback (most recent call last): +... +ValidationError: [u'LENGTH 11, MAX LENGTH 10'] + +# FileField ################################################################## + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid'] = 'INVALID' +>>> e['missing'] = 'MISSING' +>>> e['empty'] = 'EMPTY FILE' +>>> f = FileField(error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('abc') +Traceback (most recent call last): +... +ValidationError: [u'INVALID'] +>>> f.clean({}) +Traceback (most recent call last): +... +ValidationError: [u'MISSING'] +>>> f.clean({'filename': 'name', 'content':''}) +Traceback (most recent call last): +... +ValidationError: [u'EMPTY FILE'] + +# URLField ################################################################## + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid'] = 'INVALID' +>>> e['invalid_link'] = 'INVALID LINK' +>>> f = URLField(verify_exists=True, error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('abc.c') +Traceback (most recent call last): +... +ValidationError: [u'INVALID'] +>>> f.clean('http://www.jfoiwjfoi23jfoijoaijfoiwjofiwjefewl.com') +Traceback (most recent call last): +... +ValidationError: [u'INVALID LINK'] + +# BooleanField ################################################################ + +>>> e = {'required': 'REQUIRED'} +>>> f = BooleanField(error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] + +# ChoiceField ################################################################# + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid_choice'] = '%(value)s IS INVALID CHOICE' +>>> f = ChoiceField(choices=[('a', 'aye')], error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('b') +Traceback (most recent call last): +... +ValidationError: [u'b IS INVALID CHOICE'] + +# MultipleChoiceField ######################################################### + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid_choice'] = '%(value)s IS INVALID CHOICE' +>>> e['invalid_list'] = 'NOT A LIST' +>>> f = MultipleChoiceField(choices=[('a', 'aye')], error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('b') +Traceback (most recent call last): +... +ValidationError: [u'NOT A LIST'] +>>> f.clean(['b']) +Traceback (most recent call last): +... +ValidationError: [u'b IS INVALID CHOICE'] + +# SplitDateTimeField ########################################################## + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid_date'] = 'INVALID DATE' +>>> e['invalid_time'] = 'INVALID TIME' +>>> f = SplitDateTimeField(error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean(['a', 'b']) +Traceback (most recent call last): +... +ValidationError: [u'INVALID DATE', u'INVALID TIME'] + +# IPAddressField ############################################################## + +>>> e = {'required': 'REQUIRED'} +>>> e['invalid'] = 'INVALID IP ADDRESS' +>>> f = IPAddressField(error_messages=e) +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'REQUIRED'] +>>> f.clean('127.0.0') +Traceback (most recent call last): +... +ValidationError: [u'INVALID IP ADDRESS'] +""" diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py index aa33386d09..1badfa6e8b 100644 --- a/tests/regressiontests/forms/tests.py +++ b/tests/regressiontests/forms/tests.py @@ -2,6 +2,7 @@ from extra import tests as extra_tests from fields import tests as fields_tests from forms import tests as form_tests +from error_messages import tests as custom_error_message_tests from localflavor.ar import tests as localflavor_ar_tests from localflavor.au import tests as localflavor_au_tests from localflavor.br import tests as localflavor_br_tests @@ -29,6 +30,7 @@ __test__ = { 'extra_tests': extra_tests, 'fields_tests': fields_tests, 'form_tests': form_tests, + 'custom_error_message_tests': custom_error_message_tests, 'localflavor_ar_tests': localflavor_ar_tests, 'localflavor_au_tests': localflavor_au_tests, 'localflavor_br_tests': localflavor_br_tests, diff --git a/tests/regressiontests/forms/util.py b/tests/regressiontests/forms/util.py index 4f81709082..bfaf73f6bc 100644 --- a/tests/regressiontests/forms/util.py +++ b/tests/regressiontests/forms/util.py @@ -42,4 +42,11 @@ u'' # Can take a mixture in a list. >>> print ValidationError(["First error.", u"Not \u03C0.", ugettext_lazy("Error.")]).messages + +>>> class VeryBadError: +... def __unicode__(self): return u"A very bad error." + +# Can take a non-string. +>>> print ValidationError(VeryBadError()).messages + """