Commit 4a1f2129 authored by Russell Keith-Magee's avatar Russell Keith-Magee
Browse files

Fixed #12398 -- Added a TypedMultipleChoiceField. Thanks to Tai Lee.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@14829 bcc190cf-cafb-0310-a4f2-bffc1f526a37
parent ee48da24
Loading
Loading
Loading
Loading
+28 −2
Original line number Diff line number Diff line
@@ -40,7 +40,7 @@ __all__ = (
    'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField',
    'ComboField', 'MultiValueField', 'FloatField', 'DecimalField',
    'SplitDateTimeField', 'IPAddressField', 'FilePathField', 'SlugField',
    'TypedChoiceField'
    'TypedChoiceField', 'TypedMultipleChoiceField'
)

def en_format(name):
@@ -700,7 +700,7 @@ class TypedChoiceField(ChoiceField):

    def to_python(self, value):
        """
        Validate that the value is in self.choices and can be coerced to the
        Validates that the value is in self.choices and can be coerced to the
        right type.
        """
        value = super(TypedChoiceField, self).to_python(value)
@@ -742,6 +742,32 @@ class MultipleChoiceField(ChoiceField):
            if not self.valid_value(val):
                raise ValidationError(self.error_messages['invalid_choice'] % {'value': val})

class TypedMultipleChoiceField(MultipleChoiceField):
    def __init__(self, *args, **kwargs):
        self.coerce = kwargs.pop('coerce', lambda val: val)
        self.empty_value = kwargs.pop('empty_value', [])
        super(TypedMultipleChoiceField, self).__init__(*args, **kwargs)

    def to_python(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)
        super(TypedMultipleChoiceField, self).validate(value)
        if value == self.empty_value or value in validators.EMPTY_VALUES:
            return self.empty_value
        new_value = []
        for choice in value:
            try:
                new_value.append(self.coerce(choice))
            except (ValueError, TypeError, ValidationError):
                raise ValidationError(self.error_messages['invalid_choice'] % {'value': choice})
        return new_value

    def validate(self, value):
        pass

class ComboField(Field):
    """
    A Field whose clean() method calls multiple Field clean() methods.
+24 −5
Original line number Diff line number Diff line
@@ -361,13 +361,14 @@ Takes one extra required argument:

.. class:: TypedChoiceField(**kwargs)

Just like a :class:`ChoiceField`, except :class:`TypedChoiceField` takes an
extra ``coerce`` argument.
Just like a :class:`ChoiceField`, except :class:`TypedChoiceField` takes two
extra arguments, ``coerce`` and ``empty_value``.

    * Default widget: ``Select``
    * Empty value: Whatever you've given as ``empty_value``
    * Normalizes to: the value returned by the ``coerce`` argument.
    * Validates that the given value exists in the list of choices.
    * Normalizes to: A value of the type provided by the ``coerce`` argument.
    * Validates that the given value exists in the list of choices and can be
      coerced.
    * Error message keys: ``required``, ``invalid_choice``

Takes extra arguments:
@@ -635,7 +636,25 @@ Takes two optional arguments for validation:
      of choices.
    * Error message keys: ``required``, ``invalid_choice``, ``invalid_list``

Takes one extra argument, ``choices``, as for ``ChoiceField``.
Takes one extra required argument, ``choices``, as for ``ChoiceField``.

``TypedMultipleChoiceField``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. class:: TypedMultipleChoiceField(**kwargs)

Just like a :class:`MultipleChoiceField`, except :class:`TypedMultipleChoiceField`
takes two extra arguments, ``coerce`` and ``empty_value``.

    * Default widget: ``SelectMultiple``
    * Empty value: Whatever you've given as ``empty_value``
    * Normalizes to: A list of values of the type provided by the ``coerce``
      argument.
    * Validates that the given values exists in the list of choices and can be
      coerced.
    * Error message keys: ``required``, ``invalid_choice``

Takes two extra arguments, ``coerce`` and ``empty_value``, as for ``TypedChoiceField``.

``NullBooleanField``
~~~~~~~~~~~~~~~~~~~~
+43 −1
Original line number Diff line number Diff line
@@ -750,6 +750,48 @@ class FieldsTests(TestCase):
        self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 6 is not one of the available choices.']", f.clean, ['6'])
        self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 6 is not one of the available choices.']", f.clean, ['1','6'])

    # TypedMultipleChoiceField ############################################################
    # TypedMultipleChoiceField is just like MultipleChoiceField, except that coerced types
    # will be returned:

    def test_typedmultiplechoicefield_1(self):
        f = TypedMultipleChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int)
        self.assertEqual([1], f.clean(['1']))
        self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 2 is not one of the available choices.']", f.clean, ['2'])

    def test_typedmultiplechoicefield_2(self):
        # Different coercion, same validation.
        f = TypedMultipleChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=float)
        self.assertEqual([1.0], f.clean(['1']))

    def test_typedmultiplechoicefield_3(self):
        # This can also cause weirdness: be careful (bool(-1) == True, remember)
        f = TypedMultipleChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=bool)
        self.assertEqual([True], f.clean(['-1']))

    def test_typedmultiplechoicefield_4(self):
        f = TypedMultipleChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int)
        self.assertEqual([1, -1], f.clean(['1','-1']))
        self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 2 is not one of the available choices.']", f.clean, ['1','2'])

    def test_typedmultiplechoicefield_5(self):
        # Even more weirdness: if you have a valid choice but your coercion function
        # can't coerce, you'll still get a validation error. Don't do this!
        f = TypedMultipleChoiceField(choices=[('A', 'A'), ('B', 'B')], coerce=int)
        self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. B is not one of the available choices.']", f.clean, ['B'])
        # Required fields require values
        self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, [])

    def test_typedmultiplechoicefield_6(self):
        # Non-required fields aren't required
        f = TypedMultipleChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=False)
        self.assertEqual([], f.clean([]))

    def test_typedmultiplechoicefield_7(self):
        # If you want cleaning an empty value to return a different type, tell the field
        f = TypedMultipleChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=False, empty_value=None)
        self.assertEqual(None, f.clean([]))

   # ComboField ##################################################################

    def test_combofield_1(self):