Skip to content

Commit

Permalink
Fixed #21397 -- Re-added flexibility to TypedChoiceField coercion
Browse files Browse the repository at this point in the history
Thanks Elec for the report and Simon Charette for the review.
  • Loading branch information
claudep committed Nov 18, 2013
1 parent 4a00f13 commit a0f3eec
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 7 deletions.
17 changes: 11 additions & 6 deletions django/forms/fields.py
Expand Up @@ -822,12 +822,10 @@ def __init__(self, *args, **kwargs):
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:
Expand All @@ -840,6 +838,10 @@ def to_python(self, value):
) )
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
Expand Down Expand Up @@ -889,12 +891,11 @@ def __init__(self, *args, **kwargs):
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 = []
Expand All @@ -909,6 +910,10 @@ def to_python(self, value):
) )
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)
Expand Down
4 changes: 3 additions & 1 deletion docs/ref/forms/fields.txt
Expand Up @@ -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


Expand Down
4 changes: 4 additions & 0 deletions docs/releases/1.7.txt
Expand Up @@ -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.
Expand Down
33 changes: 33 additions & 0 deletions tests/forms_tests/tests/test_fields.py
Expand Up @@ -956,6 +956,22 @@ def test_typedchoicefield_has_changed(self):
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):
Expand Down Expand Up @@ -1110,6 +1126,23 @@ def test_typedmultiplechoicefield_has_changed(self):
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):
Expand Down

0 comments on commit a0f3eec

Please sign in to comment.