Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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
  • Loading branch information...
commit 4a1f2129d09658e705fbe0660275c6efccf1474a 1 parent ee48da2
Russell Keith-Magee authored December 05, 2010
30  django/forms/fields.py
@@ -40,7 +40,7 @@
40 40
     'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField',
41 41
     'ComboField', 'MultiValueField', 'FloatField', 'DecimalField',
42 42
     'SplitDateTimeField', 'IPAddressField', 'FilePathField', 'SlugField',
43  
-    'TypedChoiceField'
  43
+    'TypedChoiceField', 'TypedMultipleChoiceField'
44 44
 )
45 45
 
46 46
 def en_format(name):
@@ -700,7 +700,7 @@ def __init__(self, *args, **kwargs):
700 700
 
701 701
     def to_python(self, value):
702 702
         """
703  
-        Validate that the value is in self.choices and can be coerced to the
  703
+        Validates that the value is in self.choices and can be coerced to the
704 704
         right type.
705 705
         """
706 706
         value = super(TypedChoiceField, self).to_python(value)
@@ -742,6 +742,32 @@ def validate(self, value):
742 742
             if not self.valid_value(val):
743 743
                 raise ValidationError(self.error_messages['invalid_choice'] % {'value': val})
744 744
 
  745
+class TypedMultipleChoiceField(MultipleChoiceField):
  746
+    def __init__(self, *args, **kwargs):
  747
+        self.coerce = kwargs.pop('coerce', lambda val: val)
  748
+        self.empty_value = kwargs.pop('empty_value', [])
  749
+        super(TypedMultipleChoiceField, self).__init__(*args, **kwargs)
  750
+
  751
+    def to_python(self, value):
  752
+        """
  753
+        Validates that the values are in self.choices and can be coerced to the
  754
+        right type.
  755
+        """
  756
+        value = super(TypedMultipleChoiceField, self).to_python(value)
  757
+        super(TypedMultipleChoiceField, self).validate(value)
  758
+        if value == self.empty_value or value in validators.EMPTY_VALUES:
  759
+            return self.empty_value
  760
+        new_value = []
  761
+        for choice in value:
  762
+            try:
  763
+                new_value.append(self.coerce(choice))
  764
+            except (ValueError, TypeError, ValidationError):
  765
+                raise ValidationError(self.error_messages['invalid_choice'] % {'value': choice})
  766
+        return new_value
  767
+
  768
+    def validate(self, value):
  769
+        pass
  770
+
745 771
 class ComboField(Field):
746 772
     """
747 773
     A Field whose clean() method calls multiple Field clean() methods.
29  docs/ref/forms/fields.txt
@@ -361,13 +361,14 @@ Takes one extra required argument:
361 361
 
362 362
 .. class:: TypedChoiceField(**kwargs)
363 363
 
364  
-Just like a :class:`ChoiceField`, except :class:`TypedChoiceField` takes an
365  
-extra ``coerce`` argument.
  364
+Just like a :class:`ChoiceField`, except :class:`TypedChoiceField` takes two
  365
+extra arguments, ``coerce`` and ``empty_value``.
366 366
 
367 367
     * Default widget: ``Select``
368 368
     * Empty value: Whatever you've given as ``empty_value``
369  
-    * Normalizes to: the value returned by the ``coerce`` argument.
370  
-    * Validates that the given value exists in the list of choices.
  369
+    * Normalizes to: A value of the type provided by the ``coerce`` argument.
  370
+    * Validates that the given value exists in the list of choices and can be
  371
+      coerced.
371 372
     * Error message keys: ``required``, ``invalid_choice``
372 373
 
373 374
 Takes extra arguments:
@@ -635,7 +636,25 @@ Takes two optional arguments for validation:
635 636
       of choices.
636 637
     * Error message keys: ``required``, ``invalid_choice``, ``invalid_list``
637 638
 
638  
-Takes one extra argument, ``choices``, as for ``ChoiceField``.
  639
+Takes one extra required argument, ``choices``, as for ``ChoiceField``.
  640
+
  641
+``TypedMultipleChoiceField``
  642
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  643
+
  644
+.. class:: TypedMultipleChoiceField(**kwargs)
  645
+
  646
+Just like a :class:`MultipleChoiceField`, except :class:`TypedMultipleChoiceField`
  647
+takes two extra arguments, ``coerce`` and ``empty_value``.
  648
+
  649
+    * Default widget: ``SelectMultiple``
  650
+    * Empty value: Whatever you've given as ``empty_value``
  651
+    * Normalizes to: A list of values of the type provided by the ``coerce``
  652
+      argument.
  653
+    * Validates that the given values exists in the list of choices and can be
  654
+      coerced.
  655
+    * Error message keys: ``required``, ``invalid_choice``
  656
+
  657
+Takes two extra arguments, ``coerce`` and ``empty_value``, as for ``TypedChoiceField``.
639 658
 
640 659
 ``NullBooleanField``
641 660
 ~~~~~~~~~~~~~~~~~~~~
44  tests/regressiontests/forms/tests/fields.py
@@ -750,7 +750,49 @@ def test_multiplechoicefield_3(self):
750 750
         self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 6 is not one of the available choices.']", f.clean, ['6'])
751 751
         self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 6 is not one of the available choices.']", f.clean, ['1','6'])
752 752
 
753  
-    # ComboField ##################################################################
  753
+    # TypedMultipleChoiceField ############################################################
  754
+    # TypedMultipleChoiceField is just like MultipleChoiceField, except that coerced types
  755
+    # will be returned:
  756
+
  757
+    def test_typedmultiplechoicefield_1(self):
  758
+        f = TypedMultipleChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int)
  759
+        self.assertEqual([1], f.clean(['1']))
  760
+        self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 2 is not one of the available choices.']", f.clean, ['2'])
  761
+
  762
+    def test_typedmultiplechoicefield_2(self):
  763
+        # Different coercion, same validation.
  764
+        f = TypedMultipleChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=float)
  765
+        self.assertEqual([1.0], f.clean(['1']))
  766
+
  767
+    def test_typedmultiplechoicefield_3(self):
  768
+        # This can also cause weirdness: be careful (bool(-1) == True, remember)
  769
+        f = TypedMultipleChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=bool)
  770
+        self.assertEqual([True], f.clean(['-1']))
  771
+
  772
+    def test_typedmultiplechoicefield_4(self):
  773
+        f = TypedMultipleChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int)
  774
+        self.assertEqual([1, -1], f.clean(['1','-1']))
  775
+        self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 2 is not one of the available choices.']", f.clean, ['1','2'])
  776
+
  777
+    def test_typedmultiplechoicefield_5(self):
  778
+        # Even more weirdness: if you have a valid choice but your coercion function
  779
+        # can't coerce, you'll still get a validation error. Don't do this!
  780
+        f = TypedMultipleChoiceField(choices=[('A', 'A'), ('B', 'B')], coerce=int)
  781
+        self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. B is not one of the available choices.']", f.clean, ['B'])
  782
+        # Required fields require values
  783
+        self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, [])
  784
+
  785
+    def test_typedmultiplechoicefield_6(self):
  786
+        # Non-required fields aren't required
  787
+        f = TypedMultipleChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=False)
  788
+        self.assertEqual([], f.clean([]))
  789
+
  790
+    def test_typedmultiplechoicefield_7(self):
  791
+        # If you want cleaning an empty value to return a different type, tell the field
  792
+        f = TypedMultipleChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=False, empty_value=None)
  793
+        self.assertEqual(None, f.clean([]))
  794
+
  795
+   # ComboField ##################################################################
754 796
 
755 797
     def test_combofield_1(self):
756 798
         f = ComboField(fields=[CharField(max_length=20), EmailField()])

0 notes on commit 4a1f212

Please sign in to comment.
Something went wrong with that request. Please try again.