Author: russellm
Date: 2010-12-04 22:47:19 -0600 (Sat, 04 Dec 2010)
New Revision: 14829

Modified:
   django/trunk/django/forms/fields.py
   django/trunk/docs/ref/forms/fields.txt
   django/trunk/tests/regressiontests/forms/tests/fields.py
Log:
Fixed #12398 -- Added a TypedMultipleChoiceField. Thanks to Tai Lee.

Modified: django/trunk/django/forms/fields.py
===================================================================
--- django/trunk/django/forms/fields.py 2010-12-05 04:32:36 UTC (rev 14828)
+++ django/trunk/django/forms/fields.py 2010-12-05 04:47:19 UTC (rev 14829)
@@ -40,7 +40,7 @@
     'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField',
     'ComboField', 'MultiValueField', 'FloatField', 'DecimalField',
     'SplitDateTimeField', 'IPAddressField', 'FilePathField', 'SlugField',
-    'TypedChoiceField'
+    'TypedChoiceField', 'TypedMultipleChoiceField'
 )
 
 def en_format(name):
@@ -700,7 +700,7 @@
 
     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 @@
             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.

Modified: django/trunk/docs/ref/forms/fields.txt
===================================================================
--- django/trunk/docs/ref/forms/fields.txt      2010-12-05 04:32:36 UTC (rev 
14828)
+++ django/trunk/docs/ref/forms/fields.txt      2010-12-05 04:47:19 UTC (rev 
14829)
@@ -361,13 +361,14 @@
 
 .. 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,8 +636,26 @@
       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``
 ~~~~~~~~~~~~~~~~~~~~
 

Modified: django/trunk/tests/regressiontests/forms/tests/fields.py
===================================================================
--- django/trunk/tests/regressiontests/forms/tests/fields.py    2010-12-05 
04:32:36 UTC (rev 14828)
+++ django/trunk/tests/regressiontests/forms/tests/fields.py    2010-12-05 
04:47:19 UTC (rev 14829)
@@ -750,8 +750,50 @@
         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()])
         self.assertEqual(u'[email protected]', f.clean('[email protected]'))

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to 
[email protected].
For more options, visit this group at 
http://groups.google.com/group/django-updates?hl=en.

Reply via email to