Fixed #13181 -- Added support for callable choices to forms.ChoiceField

Thanks vanschelven and expleo for the initial patch.
This commit is contained in:
Peter Inglesby 2014-10-27 20:21:59 +00:00 committed by Tim Graham
parent e0685368c6
commit 74e1980cf9
4 changed files with 50 additions and 5 deletions

View File

@ -798,6 +798,15 @@ class NullBooleanField(BooleanField):
return initial != data return initial != data
class CallableChoiceIterator(object):
def __init__(self, choices_func):
self.choices_func = choices_func
def __iter__(self):
for e in self.choices_func():
yield e
class ChoiceField(Field): class ChoiceField(Field):
widget = Select widget = Select
default_error_messages = { default_error_messages = {
@ -822,7 +831,12 @@ class ChoiceField(Field):
# Setting choices also sets the choices on the widget. # Setting choices also sets the choices on the widget.
# choices can be any iterable, but we call list() on it because # choices can be any iterable, but we call list() on it because
# it will be consumed more than once. # it will be consumed more than once.
self._choices = self.widget.choices = list(value) if callable(value):
value = CallableChoiceIterator(value)
else:
value = list(value)
self._choices = self.widget.choices = value
choices = property(_get_choices, _set_choices) choices = property(_get_choices, _set_choices)

View File

@ -387,10 +387,16 @@ For each field, we describe the default widget used if you don't specify
.. attribute:: choices .. attribute:: choices
An iterable (e.g., a list or tuple) of 2-tuples to use as choices for this Either an iterable (e.g., a list or tuple) of 2-tuples to use as
field. This argument accepts the same formats as the ``choices`` argument choices for this field, or a callable that returns such an iterable.
to a model field. See the :ref:`model field reference documentation on This argument accepts the same formats as the ``choices`` argument to a
choices <field-choices>` for more details. model field. See the :ref:`model field reference documentation on
choices <field-choices>` for more details. If the argument is a
callable, it is evaluated each time the field's form is initialized.
.. versionchanged:: 1.8
The ability to pass a callable to ``choices`` was added.
``TypedChoiceField`` ``TypedChoiceField``
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~

View File

@ -240,6 +240,9 @@ Forms
will also update ``UploadedFile.content_type`` with the image's content type will also update ``UploadedFile.content_type`` with the image's content type
as determined by Pillow. as determined by Pillow.
* You can now pass a callable that returns an iterable of choices when
instantiating a :class:`~django.forms.ChoiceField`.
Generic Views Generic Views
^^^^^^^^^^^^^ ^^^^^^^^^^^^^

View File

@ -961,6 +961,28 @@ class FieldsTests(SimpleTestCase):
self.assertEqual('5', f.clean('5')) self.assertEqual('5', f.clean('5'))
self.assertRaisesMessage(ValidationError, "'Select a valid choice. 6 is not one of the available choices.'", f.clean, '6') self.assertRaisesMessage(ValidationError, "'Select a valid choice. 6 is not one of the available choices.'", f.clean, '6')
def test_choicefield_callable(self):
choices = lambda: [('J', 'John'), ('P', 'Paul')]
f = ChoiceField(choices=choices)
self.assertEqual('J', f.clean('J'))
def test_choicefield_callable_may_evaluate_to_different_values(self):
choices = []
def choices_as_callable():
return choices
class ChoiceFieldForm(Form):
choicefield = ChoiceField(choices=choices_as_callable)
choices = [('J', 'John')]
form = ChoiceFieldForm()
self.assertEqual([('J', 'John')], list(form.fields['choicefield'].choices))
choices = [('P', 'Paul')]
form = ChoiceFieldForm()
self.assertEqual([('P', 'Paul')], list(form.fields['choicefield'].choices))
# TypedChoiceField ############################################################ # TypedChoiceField ############################################################
# TypedChoiceField is just like ChoiceField, except that coerced types will # TypedChoiceField is just like ChoiceField, except that coerced types will
# be returned: # be returned: