Commit a0f3eecc authored by Claude Paroz's avatar Claude Paroz
Browse files

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

Thanks Elec for the report and Simon Charette for the review.
parent 4a00f132
Loading
Loading
Loading
Loading
+11 −6
Original line number Diff line number Diff line
@@ -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)
+3 −1
Original line number Diff line number Diff line
@@ -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

+4 −0
Original line number Diff line number Diff line
@@ -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
  <django.forms.extras.widgets.SelectDateWidget.months>` can be used to
  customize the wording of the months displayed in the select widget.
+33 −0
Original line number Diff line number Diff line
@@ -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):