Commit 4ae746b5 authored by Jacob Kaplan-Moss's avatar Jacob Kaplan-Moss
Browse files

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
parent 3e71a684
Loading
Loading
Loading
Loading
+29 −0
Original line number Diff line number Diff line
@@ -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
+27 −0
Original line number Diff line number Diff line
@@ -363,6 +363,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``
~~~~~~~~~~~~~

+47 −0
Original line number Diff line number Diff line
@@ -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()