Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #17840 -- Generalized named placeholders in form error messages

Also fixed plural messages for DecimalField.
  • Loading branch information...
commit be9ae693c46021fd3a70c0ec21dd566960b29ffb 1 parent 9ac4dbd
Claude Paroz authored April 13, 2013
27  django/forms/fields.py
@@ -292,9 +292,18 @@ def widget_attrs(self, widget):
292 292
 class DecimalField(IntegerField):
293 293
     default_error_messages = {
294 294
         'invalid': _('Enter a number.'),
295  
-        'max_digits': _('Ensure that there are no more than %s digits in total.'),
296  
-        'max_decimal_places': _('Ensure that there are no more than %s decimal places.'),
297  
-        'max_whole_digits': _('Ensure that there are no more than %s digits before the decimal point.')
  295
+        'max_digits': ungettext_lazy(
  296
+            'Ensure that there are no more than %(max)s digit in total.',
  297
+            'Ensure that there are no more than %(max)s digits in total.',
  298
+            'max'),
  299
+        'max_decimal_places': ungettext_lazy(
  300
+            'Ensure that there are no more than %(max)s decimal place.',
  301
+            'Ensure that there are no more than %(max)s decimal places.',
  302
+            'max'),
  303
+        'max_whole_digits': ungettext_lazy(
  304
+            'Ensure that there are no more than %(max)s digit before the decimal point.',
  305
+            'Ensure that there are no more than %(max)s digits before the decimal point.',
  306
+            'max'),
298 307
     }
299 308
 
300 309
     def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs):
@@ -341,11 +350,15 @@ def validate(self, value):
341 350
         whole_digits = digits - decimals
342 351
 
343 352
         if self.max_digits is not None and digits > self.max_digits:
344  
-            raise ValidationError(self.error_messages['max_digits'] % self.max_digits)
  353
+            raise ValidationError(self.error_messages['max_digits'] % {
  354
+                                  'max': self.max_digits})
345 355
         if self.decimal_places is not None and decimals > self.decimal_places:
346  
-            raise ValidationError(self.error_messages['max_decimal_places'] % self.decimal_places)
347  
-        if self.max_digits is not None and self.decimal_places is not None and whole_digits > (self.max_digits - self.decimal_places):
348  
-            raise ValidationError(self.error_messages['max_whole_digits'] % (self.max_digits - self.decimal_places))
  356
+            raise ValidationError(self.error_messages['max_decimal_places'] % {
  357
+                                  'max': self.decimal_places})
  358
+        if (self.max_digits is not None and self.decimal_places is not None
  359
+                and whole_digits > (self.max_digits - self.decimal_places)):
  360
+            raise ValidationError(self.error_messages['max_whole_digits'] % {
  361
+                                  'max': (self.max_digits - self.decimal_places)})
349 362
         return value
350 363
 
351 364
     def widget_attrs(self, widget):
8  django/forms/models.py
@@ -1039,9 +1039,9 @@ class ModelMultipleChoiceField(ModelChoiceField):
1039 1039
     hidden_widget = MultipleHiddenInput
1040 1040
     default_error_messages = {
1041 1041
         'list': _('Enter a list of values.'),
1042  
-        'invalid_choice': _('Select a valid choice. %s is not one of the'
  1042
+        'invalid_choice': _('Select a valid choice. %(value)s is not one of the'
1043 1043
                             ' available choices.'),
1044  
-        'invalid_pk_value': _('"%s" is not a valid value for a primary key.')
  1044
+        'invalid_pk_value': _('"%(pk)s" is not a valid value for a primary key.')
1045 1045
     }
1046 1046
 
1047 1047
     def __init__(self, queryset, cache_choices=False, required=True,
@@ -1063,12 +1063,12 @@ def clean(self, value):
1063 1063
             try:
1064 1064
                 self.queryset.filter(**{key: pk})
1065 1065
             except ValueError:
1066  
-                raise ValidationError(self.error_messages['invalid_pk_value'] % pk)
  1066
+                raise ValidationError(self.error_messages['invalid_pk_value'] % {'pk': pk})
1067 1067
         qs = self.queryset.filter(**{'%s__in' % key: value})
1068 1068
         pks = set([force_text(getattr(o, key)) for o in qs])
1069 1069
         for val in value:
1070 1070
             if force_text(val) not in pks:
1071  
-                raise ValidationError(self.error_messages['invalid_choice'] % val)
  1071
+                raise ValidationError(self.error_messages['invalid_choice'] % {'value': val})
1072 1072
         # Since this overrides the inherited ModelChoiceField.clean
1073 1073
         # we run custom validators here
1074 1074
         self.run_validators(value)
8  tests/forms_tests/tests/test_error_messages.py
@@ -60,9 +60,9 @@ def test_decimalfield(self):
60 60
             'invalid': 'INVALID',
61 61
             'min_value': 'MIN VALUE IS %(limit_value)s',
62 62
             'max_value': 'MAX VALUE IS %(limit_value)s',
63  
-            'max_digits': 'MAX DIGITS IS %s',
64  
-            'max_decimal_places': 'MAX DP IS %s',
65  
-            'max_whole_digits': 'MAX DIGITS BEFORE DP IS %s',
  63
+            'max_digits': 'MAX DIGITS IS %(max)s',
  64
+            'max_decimal_places': 'MAX DP IS %(max)s',
  65
+            'max_whole_digits': 'MAX DIGITS BEFORE DP IS %(max)s',
66 66
         }
67 67
         f = DecimalField(min_value=5, max_value=10, error_messages=e)
68 68
         self.assertFormErrors(['REQUIRED'], f.clean, '')
@@ -254,7 +254,7 @@ def test_modelchoicefield(self):
254 254
         # ModelMultipleChoiceField
255 255
         e = {
256 256
             'required': 'REQUIRED',
257  
-            'invalid_choice': '%s IS INVALID CHOICE',
  257
+            'invalid_choice': '%(value)s IS INVALID CHOICE',
258 258
             'list': 'NOT A LIST OF VALUES',
259 259
         }
260 260
         f = ModelMultipleChoiceField(queryset=ChoiceModel.objects.all(), error_messages=e)

4 notes on commit be9ae69

Carl Meyer
Owner

@claudep Don't you think this deserves a mention in the backwards-incompatibility release notes? Overriding these error messages is definitely intended as public API, and if I'm not mistaken this change will break anyone who has done that and still has positional keys in their overridden message. (I agree it's still a change we need to make, just think it needs to be in the release notes.)

Carl Meyer
Owner

@claudep Looks pretty good, although it seems like the release note is focused on the wrong aspect. Generally users won't be "using a dictionary when formatting" these error messages, because Django is the one that provides the dictionary and does the formatting. So it seems like it would make more sense for the release note to focus on the construction of custom error message placeholders; for instance "You must use the appropriate named placeholders (e.g. %(max)s) rather than positional placeholders (e.g. %s) in custom error messages for DecimalField and ModelMultipleChoiceField; see the corresponding field documentation etc." Does that make sense, or am I missing something?

Claude Paroz
Owner

Of course, that makes sense!

Please sign in to comment.
Something went wrong with that request. Please try again.