Ticket #12398: 12398-TypedMultipleChoiceField-r11901.diff

File 12398-TypedMultipleChoiceField-r11901.diff, 6.8 KB (added by mrmachine, 6 years ago)
  • django/forms/fields.py

     
    3030    'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField',
    3131    'ComboField', 'MultiValueField', 'FloatField', 'DecimalField',
    3232    'SplitDateTimeField', 'IPAddressField', 'FilePathField', 'SlugField',
    33     'TypedChoiceField'
     33    'TypedChoiceField', 'TypedMultipleChoiceField'
    3434)
    3535
    3636# These values, if given to to_python(), will trigger the self.required check.
     
    711711                raise ValidationError(self.error_messages['invalid_choice'] % {'value': val})
    712712        return new_value
    713713
     714class TypedMultipleChoiceField(MultipleChoiceField):
     715    def __init__(self, *args, **kwargs):
     716        self.coerce = kwargs.pop('coerce', lambda val: val)
     717        self.empty_value = kwargs.pop('empty_value', [])
     718        super(TypedMultipleChoiceField, self).__init__(*args, **kwargs)
     719
     720    def clean(self, value):
     721        """
     722        Validate that the values are in self.choices and can be coerced to the
     723        right type.
     724        """
     725        value = super(TypedMultipleChoiceField, self).clean(value)
     726        if not value:
     727            return self.empty_value
     728
     729        # Hack alert: This field is purpose-made to use with Field.to_python as
     730        # a coercion function so that ModelForms with choices work. However,
     731        # Django's Field.to_python raises
     732        # django.core.exceptions.ValidationError, which is a *different*
     733        # exception than django.forms.util.ValidationError. So we need to catch
     734        # both.
     735        new_value = []
     736        for choice in value:
     737            try:
     738                new_value.append(self.coerce(choice))
     739            except (ValueError, TypeError, django.core.exceptions.ValidationError):
     740                raise ValidationError(self.error_messages['invalid_choice'] % {'value': choice})
     741        return new_value
     742
    714743class ComboField(Field):
    715744    """
    716745    A Field whose clean() method calls multiple Field clean() methods.
  • tests/regressiontests/forms/fields.py

     
    732732        self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 6 is not one of the available choices.']", f.clean, ['6'])
    733733        self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 6 is not one of the available choices.']", f.clean, ['1','6'])
    734734
     735    # TypedMultipleChoiceField ############################################################
     736    # TypedMultipleChoiceField is just like MultipleChoiceField, except that coerced types
     737    # will be returned:
     738
     739    def test_typedmultiplechoicefield_71(self):
     740        f = TypedMultipleChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int)
     741        self.assertEqual([1], f.clean(['1']))
     742        self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 2 is not one of the available choices.']", f.clean, ['2'])
     743
     744    def test_typedmultiplechoicefield_72(self):
     745        # Different coercion, same validation.
     746        f = TypedMultipleChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=float)
     747        self.assertEqual([1.0], f.clean(['1']))
     748
     749    def test_typedmultiplechoicefield_73(self):
     750        # This can also cause weirdness: be careful (bool(-1) == True, remember)
     751        f = TypedMultipleChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=bool)
     752        self.assertEqual([True], f.clean(['-1']))
     753
     754    def test_typedmultiplechoicefield_74(self):
     755        # Even more weirdness: if you have a valid choice but your coercion function
     756        # can't coerce, you'll still get a validation error. Don't do this!
     757        f = TypedMultipleChoiceField(choices=[('A', 'A'), ('B', 'B')], coerce=int)
     758        self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. B is not one of the available choices.']", f.clean, ['B'])
     759        # Required fields require values
     760        self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, [])
     761
     762    def test_typedmultiplechoicefield_75(self):
     763        # Non-required fields aren't required
     764        f = TypedMultipleChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=False)
     765        self.assertEqual([], f.clean([]))
     766
     767    def test_typedmultiplechoicefield_76(self):
     768        # If you want cleaning an empty value to return a different type, tell the field
     769        f = TypedMultipleChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=False, empty_value=None)
     770        self.assertEqual(None, f.clean([]))
     771
    735772    # ComboField ##################################################################
    736773
    737774    def test_combofield_63(self):
  • docs/ref/forms/fields.txt

     
    335335
    336336.. class:: TypedChoiceField(**kwargs)
    337337
    338 Just like a :class:`ChoiceField`, except :class:`TypedChoiceField` takes an
    339 extra ``coerce`` argument.
     338Just like a :class:`ChoiceField`, except :class:`TypedChoiceField` takes two
     339extra arguments, ``coerce`` and ``empty_value``.
    340340
    341341    * Default widget: ``Select``
    342342    * Empty value: Whatever you've given as ``empty_value``
    343     * Normalizes to: the value returned by the ``coerce`` argument.
    344     * Validates that the given value exists in the list of choices.
     343    * Normalizes to: The value returned by the ``coerce`` argument.
     344    * Validates that the given value exists in the list of choices and can be
     345      coerced.
    345346    * Error message keys: ``required``, ``invalid_choice``
    346347
    347348Takes extra arguments:
     
    604605      of choices.
    605606    * Error message keys: ``required``, ``invalid_choice``, ``invalid_list``
    606607
    607 Takes one extra argument, ``choices``, as for ``ChoiceField``.
     608Takes one extra required argument, ``choices``, as for ``ChoiceField``.
    608609
     610``TypedMultipleChoiceField``
     611~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     612
     613.. class:: TypedMultipleChoiceField(**kwargs)
     614
     615Just like a :class:`MultipleChoiceField`, except :class:`TypedMultipleChoiceField`
     616takes two extra arguments, ``coerce`` and ``empty_value``.
     617
     618    * Default widget: ``SelectMultiple``
     619    * Empty value: Whatever you've given as ``empty_value``
     620    * Normalizes to: A list of values, each returned by the ``coerce`` argument.
     621    * Validates that the given values exists in the list of choices and can be
     622      coerced.
     623    * Error message keys: ``required``, ``invalid_choice``
     624
     625Takes two extra arguments, ``coerce`` and ``empty_value``, as for ``TypedChoiceField``.
     626
    609627``NullBooleanField``
    610628~~~~~~~~~~~~~~~~~~~~
    611629
Back to Top