Fixed #21397 -- Re-added flexibility to TypedChoiceField coercion

Thanks Elec for the report and Simon Charette for the review.
This commit is contained in:
Claude Paroz 2013-11-18 18:24:56 +01:00
parent 4a00f132e0
commit a0f3eeccf3
4 changed files with 51 additions and 7 deletions

View File

@ -822,12 +822,10 @@ class TypedChoiceField(ChoiceField):
self.empty_value = kwargs.pop('empty_value', '') self.empty_value = kwargs.pop('empty_value', '')
super(TypedChoiceField, self).__init__(*args, **kwargs) 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 Validate that the value can be coerced to the right type (if not empty).
right type.
""" """
value = super(TypedChoiceField, self).to_python(value)
if value == self.empty_value or value in self.empty_values: if value == self.empty_value or value in self.empty_values:
return self.empty_value return self.empty_value
try: try:
@ -840,6 +838,10 @@ class TypedChoiceField(ChoiceField):
) )
return value return value
def clean(self, value):
value = super(TypedChoiceField, self).clean(value)
return self._coerce(value)
class MultipleChoiceField(ChoiceField): class MultipleChoiceField(ChoiceField):
hidden_widget = MultipleHiddenInput hidden_widget = MultipleHiddenInput
@ -889,12 +891,11 @@ class TypedMultipleChoiceField(MultipleChoiceField):
self.empty_value = kwargs.pop('empty_value', []) self.empty_value = kwargs.pop('empty_value', [])
super(TypedMultipleChoiceField, self).__init__(*args, **kwargs) 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 Validates that the values are in self.choices and can be coerced to the
right type. right type.
""" """
value = super(TypedMultipleChoiceField, self).to_python(value)
if value == self.empty_value or value in self.empty_values: if value == self.empty_value or value in self.empty_values:
return self.empty_value return self.empty_value
new_value = [] new_value = []
@ -909,6 +910,10 @@ class TypedMultipleChoiceField(MultipleChoiceField):
) )
return new_value return new_value
def clean(self, value):
value = super(TypedMultipleChoiceField, self).clean(value)
return self._coerce(value)
def validate(self, value): def validate(self, value):
if value != self.empty_value: if value != self.empty_value:
super(TypedMultipleChoiceField, self).validate(value) super(TypedMultipleChoiceField, self).validate(value)

View File

@ -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 A function that takes one argument and returns a coerced value. Examples
include the built-in ``int``, ``float``, ``bool`` and other types. Defaults 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 .. attribute:: empty_value

View File

@ -317,6 +317,10 @@ Forms
return ``self.cleaned_data``. If it does return a changed dictionary then return ``self.cleaned_data``. If it does return a changed dictionary then
that will still be used. 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 * :attr:`SelectDateWidget.months
<django.forms.extras.widgets.SelectDateWidget.months>` can be used to <django.forms.extras.widgets.SelectDateWidget.months>` can be used to
customize the wording of the months displayed in the select widget. customize the wording of the months displayed in the select widget.

View File

@ -956,6 +956,22 @@ class FieldsTests(SimpleTestCase):
f = TypedChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=True) f = TypedChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=True)
self.assertFalse(f._has_changed(None, '')) 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 ############################################################ # NullBooleanField ############################################################
def test_nullbooleanfield_1(self): def test_nullbooleanfield_1(self):
@ -1110,6 +1126,23 @@ class FieldsTests(SimpleTestCase):
f = TypedMultipleChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=True) f = TypedMultipleChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=True)
self.assertFalse(f._has_changed(None, '')) 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 ################################################################## # ComboField ##################################################################
def test_combofield_1(self): def test_combofield_1(self):