diff --git a/django/forms/fields.py b/django/forms/fields.py
index c94a564..c6c6ca5 100644
a
|
b
|
class TypedChoiceField(ChoiceField):
|
812 | 812 | self.empty_value = kwargs.pop('empty_value', '') |
813 | 813 | super(TypedChoiceField, self).__init__(*args, **kwargs) |
814 | 814 | |
815 | | def to_python(self, value): |
| 815 | def _coerce(self, value): |
816 | 816 | """ |
817 | | Validates that the value is in self.choices and can be coerced to the |
818 | | right type. |
| 817 | Validate that the value can be coerced to the right type (if not empty). |
819 | 818 | """ |
820 | | value = super(TypedChoiceField, self).to_python(value) |
821 | 819 | if value == self.empty_value or value in self.empty_values: |
822 | 820 | return self.empty_value |
823 | 821 | try: |
… |
… |
class TypedChoiceField(ChoiceField):
|
830 | 828 | ) |
831 | 829 | return value |
832 | 830 | |
| 831 | def clean(self, value): |
| 832 | value = super(TypedChoiceField, self).clean(value) |
| 833 | return self._coerce(value) |
| 834 | |
833 | 835 | |
834 | 836 | class MultipleChoiceField(ChoiceField): |
835 | 837 | hidden_widget = MultipleHiddenInput |
… |
… |
class TypedMultipleChoiceField(MultipleChoiceField):
|
879 | 881 | self.empty_value = kwargs.pop('empty_value', []) |
880 | 882 | super(TypedMultipleChoiceField, self).__init__(*args, **kwargs) |
881 | 883 | |
882 | | def to_python(self, value): |
| 884 | def _coerce(self, value): |
883 | 885 | """ |
884 | 886 | Validates that the values are in self.choices and can be coerced to the |
885 | 887 | right type. |
886 | 888 | """ |
887 | | value = super(TypedMultipleChoiceField, self).to_python(value) |
888 | 889 | if value == self.empty_value or value in self.empty_values: |
889 | 890 | return self.empty_value |
890 | 891 | new_value = [] |
… |
… |
class TypedMultipleChoiceField(MultipleChoiceField):
|
899 | 900 | ) |
900 | 901 | return new_value |
901 | 902 | |
| 903 | def clean(self, value): |
| 904 | value = super(TypedMultipleChoiceField, self).clean(value) |
| 905 | return self._coerce(value) |
| 906 | |
902 | 907 | def validate(self, value): |
903 | 908 | if value != self.empty_value: |
904 | 909 | super(TypedMultipleChoiceField, self).validate(value) |
diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt
index ef4ed72..476149b 100644
a
|
b
|
For each field, we describe the default widget used if you don't specify
|
375 | 375 | |
376 | 376 | A function that takes one argument and returns a coerced value. Examples |
377 | 377 | include the built-in ``int``, ``float``, ``bool`` and other types. Defaults |
378 | | to an identity function. |
| 378 | to an identity function. Note that coercion happens after input |
| 379 | validation, so it is possible to coerce to a value not present in |
| 380 | ``choices``. |
379 | 381 | |
380 | 382 | .. attribute:: empty_value |
381 | 383 | |
diff --git a/tests/forms_tests/tests/test_fields.py b/tests/forms_tests/tests/test_fields.py
index f02593e..0b1826b 100644
a
|
b
|
class FieldsTests(SimpleTestCase):
|
950 | 950 | f = TypedChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=True) |
951 | 951 | self.assertFalse(f._has_changed(None, '')) |
952 | 952 | |
| 953 | def test_typedchoicefield_special_coerce(self): |
| 954 | """ |
| 955 | Test a coerce function which results in a value not present in choices. |
| 956 | Refs #21397. |
| 957 | """ |
| 958 | def coerce_func(val): |
| 959 | return Decimal('1.%s' % val) |
| 960 | |
| 961 | f = TypedChoiceField(choices=[(1, "1"), (2, "2")], coerce=coerce_func, required=True) |
| 962 | self.assertEqual(Decimal('1.2'), f.clean('2')) |
| 963 | self.assertRaisesMessage(ValidationError, |
| 964 | "'This field is required.'", f.clean, '') |
| 965 | self.assertRaisesMessage(ValidationError, |
| 966 | "'Select a valid choice. 3 is not one of the available choices.'", |
| 967 | f.clean, '3') |
| 968 | |
953 | 969 | # NullBooleanField ############################################################ |
954 | 970 | |
955 | 971 | def test_nullbooleanfield_1(self): |
… |
… |
class FieldsTests(SimpleTestCase):
|
1104 | 1120 | f = TypedMultipleChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=True) |
1105 | 1121 | self.assertFalse(f._has_changed(None, '')) |
1106 | 1122 | |
| 1123 | def test_typedmultiplechoicefield_special_coerce(self): |
| 1124 | """ |
| 1125 | Test a coerce function which results in a value not present in choices. |
| 1126 | Refs #21397. |
| 1127 | """ |
| 1128 | def coerce_func(val): |
| 1129 | return Decimal('1.%s' % val) |
| 1130 | |
| 1131 | f = TypedMultipleChoiceField( |
| 1132 | choices=[(1, "1"), (2, "2")], coerce=coerce_func, required=True) |
| 1133 | self.assertEqual([Decimal('1.2')], f.clean(['2'])) |
| 1134 | self.assertRaisesMessage(ValidationError, |
| 1135 | "'This field is required.'", f.clean, []) |
| 1136 | self.assertRaisesMessage(ValidationError, |
| 1137 | "'Select a valid choice. 3 is not one of the available choices.'", |
| 1138 | f.clean, ['3']) |
| 1139 | |
1107 | 1140 | # ComboField ################################################################## |
1108 | 1141 | |
1109 | 1142 | def test_combofield_1(self): |