From 4ae746b574a84e30a8be4f207c9f386fa09c03f9 Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Sun, 31 Aug 2008 20:10:50 +0000 Subject: [PATCH] Added a `TypedChoiceField` which acts just like `ChoiceField`, except that it returns a value coerced by some provided function. Refs #6967. git-svn-id: http://code.djangoproject.com/svn/django/trunk@8771 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/forms/fields.py | 29 +++++++++++++++++ docs/ref/forms/fields.txt | 27 +++++++++++++++ tests/regressiontests/forms/fields.py | 47 +++++++++++++++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/django/forms/fields.py b/django/forms/fields.py index b5826590990..e2674988089 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -23,6 +23,7 @@ try: except NameError: from sets import Set as set +import django.core.exceptions from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import smart_unicode, smart_str @@ -39,6 +40,7 @@ __all__ = ( 'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField', 'ComboField', 'MultiValueField', 'FloatField', 'DecimalField', 'SplitDateTimeField', 'IPAddressField', 'FilePathField', 'SlugField', + 'TypedChoiceField' ) # These values, if given to to_python(), will trigger the self.required check. @@ -657,6 +659,33 @@ class ChoiceField(Field): return True return False +class TypedChoiceField(ChoiceField): + def __init__(self, *args, **kwargs): + self.coerce = kwargs.pop('coerce', lambda val: val) + self.empty_value = kwargs.pop('empty_value', '') + super(TypedChoiceField, self).__init__(*args, **kwargs) + + def clean(self, value): + """ + Validate that the value is in self.choices and can be coerced to the + right type. + """ + value = super(TypedChoiceField, self).clean(value) + if value == self.empty_value or value in EMPTY_VALUES: + return self.empty_value + + # Hack alert: This field is purpose-made to use with Field.to_python as + # a coercion function so that ModelForms with choices work. However, + # Django's Field.to_python raises django.core.exceptions.ValidationError, + # which is a *different* exception than + # django.forms.utils.ValidationError. So unfortunatly we need to catch + # both. + try: + value = self.coerce(value) + except (ValueError, TypeError, django.core.exceptions.ValidationError): + raise ValidationError(self.error_messages['invalid_choice'] % {'value': value}) + return value + class MultipleChoiceField(ChoiceField): hidden_widget = MultipleHiddenInput widget = SelectMultiple diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index 7654300617f..262538fec75 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -362,6 +362,33 @@ Takes one extra required argument: An iterable (e.g., a list or tuple) of 2-tuples to use as choices for this field. + +``TypedChoiceField`` +~~~~~~~~~~~~~~~~~~~~ + +.. class:: TypedChoiceField(**kwargs) + +Just like a :class:`ChoiceField`, except :class:`TypedChoiceField` takes an +extra ``coerce`` argument. + + * 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. + * Error message keys: ``required``, ``invalid_choice`` + +Takes extra arguments: + +.. attribute:: TypedChoiceField.coerce + + A function that takes one argument and returns a coerced value. Examples + include the built-in ``int``, ``float``, ``bool`` and other types. Defaults + to an identity function. + +.. attribute:: TypedChoiceField.empty_value + + The value to use to represent "empty." Defaults to the empty string; + ``None`` is another common choice here. ``DateField`` ~~~~~~~~~~~~~ diff --git a/tests/regressiontests/forms/fields.py b/tests/regressiontests/forms/fields.py index cbd59a40894..7f55ec4eadd 100644 --- a/tests/regressiontests/forms/fields.py +++ b/tests/regressiontests/forms/fields.py @@ -1077,6 +1077,53 @@ Traceback (most recent call last): ... ValidationError: [u'Select a valid choice. 6 is not one of the available choices.'] +# TypedChoiceField ############################################################ + +# TypedChoiceField is just like ChoiceField, except that coerced types will +# be returned: +>>> f = TypedChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int) +>>> f.clean('1') +1 +>>> f.clean('2') +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. 2 is not one of the available choices.'] + +# Different coercion, same validation. +>>> f.coerce = float +>>> f.clean('1') +1.0 + + +# This can also cause weirdness: be careful (bool(-1) == True, remember) +>>> f.coerce = bool +>>> f.clean('-1') +True + +# 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 = TypedChoiceField(choices=[('A', 'A'), ('B', 'B')], coerce=int) +>>> f.clean('B') +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. B is not one of the available choices.'] + +# Required fields require values +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +# Non-required fields aren't required +>>> f = TypedChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=False) +>>> f.clean('') +'' + +# If you want cleaning an empty value to return a different type, tell the field +>>> f = TypedChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=False, empty_value=None) +>>> print f.clean('') +None + # NullBooleanField ############################################################ >>> f = NullBooleanField()