From 4a1f2129d09658e705fbe0660275c6efccf1474a Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 5 Dec 2010 04:47:19 +0000 Subject: [PATCH] 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 --- django/forms/fields.py | 30 +++++++++++++- docs/ref/forms/fields.txt | 29 +++++++++++--- tests/regressiontests/forms/tests/fields.py | 44 ++++++++++++++++++++- 3 files changed, 95 insertions(+), 8 deletions(-) diff --git a/django/forms/fields.py b/django/forms/fields.py index a0b559319b..d0a93cf0a6 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -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. diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index 7f18c673b6..b36c9cc749 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -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`` ~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/regressiontests/forms/tests/fields.py b/tests/regressiontests/forms/tests/fields.py index bc549a9848..93ca5c1762 100644 --- a/tests/regressiontests/forms/tests/fields.py +++ b/tests/regressiontests/forms/tests/fields.py @@ -750,7 +750,49 @@ 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']) - # ComboField ################################################################## + # 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): f = ComboField(fields=[CharField(max_length=20), EmailField()])