Loading django/forms/fields.py +28 −2 Original line number Diff line number Diff line Loading @@ -40,7 +40,7 @@ __all__ = ( 'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField', 'ComboField', 'MultiValueField', 'FloatField', 'DecimalField', 'SplitDateTimeField', 'IPAddressField', 'FilePathField', 'SlugField', 'TypedChoiceField' 'TypedChoiceField', 'TypedMultipleChoiceField' ) def en_format(name): Loading Loading @@ -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) Loading Loading @@ -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. Loading docs/ref/forms/fields.txt +24 −5 Original line number Diff line number Diff line Loading @@ -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: Loading Loading @@ -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`` ~~~~~~~~~~~~~~~~~~~~ Loading tests/regressiontests/forms/tests/fields.py +43 −1 Original line number Diff line number Diff line Loading @@ -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): Loading Loading
django/forms/fields.py +28 −2 Original line number Diff line number Diff line Loading @@ -40,7 +40,7 @@ __all__ = ( 'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField', 'ComboField', 'MultiValueField', 'FloatField', 'DecimalField', 'SplitDateTimeField', 'IPAddressField', 'FilePathField', 'SlugField', 'TypedChoiceField' 'TypedChoiceField', 'TypedMultipleChoiceField' ) def en_format(name): Loading Loading @@ -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) Loading Loading @@ -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. Loading
docs/ref/forms/fields.txt +24 −5 Original line number Diff line number Diff line Loading @@ -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: Loading Loading @@ -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`` ~~~~~~~~~~~~~~~~~~~~ Loading
tests/regressiontests/forms/tests/fields.py +43 −1 Original line number Diff line number Diff line Loading @@ -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): Loading