diff --git a/django/forms/fields.py b/django/forms/fields.py index c95930a2d0..56d0e316f2 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -822,12 +822,10 @@ class TypedChoiceField(ChoiceField): self.empty_value = kwargs.pop('empty_value', '') super(TypedChoiceField, self).__init__(*args, **kwargs) - def to_python(self, value): + def _coerce(self, value): """ - Validates that the value is in self.choices and can be coerced to the - right type. + Validate that the value can be coerced to the right type (if not empty). """ - value = super(TypedChoiceField, self).to_python(value) if value == self.empty_value or value in self.empty_values: return self.empty_value try: @@ -840,6 +838,10 @@ class TypedChoiceField(ChoiceField): ) return value + def clean(self, value): + value = super(TypedChoiceField, self).clean(value) + return self._coerce(value) + class MultipleChoiceField(ChoiceField): hidden_widget = MultipleHiddenInput @@ -889,12 +891,11 @@ class TypedMultipleChoiceField(MultipleChoiceField): self.empty_value = kwargs.pop('empty_value', []) super(TypedMultipleChoiceField, self).__init__(*args, **kwargs) - def to_python(self, value): + def _coerce(self, value): """ Validates that the values are in self.choices and can be coerced to the right type. """ - value = super(TypedMultipleChoiceField, self).to_python(value) if value == self.empty_value or value in self.empty_values: return self.empty_value new_value = [] @@ -909,6 +910,10 @@ class TypedMultipleChoiceField(MultipleChoiceField): ) return new_value + def clean(self, value): + value = super(TypedMultipleChoiceField, self).clean(value) + return self._coerce(value) + def validate(self, value): if value != self.empty_value: super(TypedMultipleChoiceField, self).validate(value) diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index 81b65d04d4..f2cd122526 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -375,7 +375,9 @@ For each field, we describe the default widget used if you don't specify A function that takes one argument and returns a coerced value. Examples include the built-in ``int``, ``float``, ``bool`` and other types. Defaults - to an identity function. + to an identity function. Note that coercion happens after input + validation, so it is possible to coerce to a value not present in + ``choices``. .. attribute:: empty_value diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 2a2fd30363..8428c1853a 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -317,6 +317,10 @@ Forms return ``self.cleaned_data``. If it does return a changed dictionary then that will still be used. +* After a temporary regression in Django 1.6, it's now possible again to make + :class:`~django.forms.TypedChoiceField` ``coerce`` method return an arbitrary + value. + * :attr:`SelectDateWidget.months ` can be used to customize the wording of the months displayed in the select widget. diff --git a/tests/forms_tests/tests/test_fields.py b/tests/forms_tests/tests/test_fields.py index a7e124a50c..fe11370bde 100644 --- a/tests/forms_tests/tests/test_fields.py +++ b/tests/forms_tests/tests/test_fields.py @@ -956,6 +956,22 @@ class FieldsTests(SimpleTestCase): f = TypedChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=True) self.assertFalse(f._has_changed(None, '')) + def test_typedchoicefield_special_coerce(self): + """ + Test a coerce function which results in a value not present in choices. + Refs #21397. + """ + def coerce_func(val): + return Decimal('1.%s' % val) + + f = TypedChoiceField(choices=[(1, "1"), (2, "2")], coerce=coerce_func, required=True) + self.assertEqual(Decimal('1.2'), f.clean('2')) + self.assertRaisesMessage(ValidationError, + "'This field is required.'", f.clean, '') + self.assertRaisesMessage(ValidationError, + "'Select a valid choice. 3 is not one of the available choices.'", + f.clean, '3') + # NullBooleanField ############################################################ def test_nullbooleanfield_1(self): @@ -1110,6 +1126,23 @@ class FieldsTests(SimpleTestCase): f = TypedMultipleChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=True) self.assertFalse(f._has_changed(None, '')) + def test_typedmultiplechoicefield_special_coerce(self): + """ + Test a coerce function which results in a value not present in choices. + Refs #21397. + """ + def coerce_func(val): + return Decimal('1.%s' % val) + + f = TypedMultipleChoiceField( + choices=[(1, "1"), (2, "2")], coerce=coerce_func, required=True) + self.assertEqual([Decimal('1.2')], f.clean(['2'])) + self.assertRaisesMessage(ValidationError, + "'This field is required.'", f.clean, []) + self.assertRaisesMessage(ValidationError, + "'Select a valid choice. 3 is not one of the available choices.'", + f.clean, ['3']) + # ComboField ################################################################## def test_combofield_1(self):