diff --git a/django/forms/fields.py b/django/forms/fields.py index 6403509fbb..81b3e2f4f9 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -798,6 +798,15 @@ class NullBooleanField(BooleanField): 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): widget = Select default_error_messages = { @@ -822,7 +831,12 @@ class ChoiceField(Field): # Setting choices also sets the choices on the widget. # choices can be any iterable, but we call list() on it because # 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) diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index d7fcef2e75..070ccdf7a8 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -387,10 +387,16 @@ For each field, we describe the default widget used if you don't specify .. attribute:: choices - An iterable (e.g., a list or tuple) of 2-tuples to use as choices for this - field. This argument accepts the same formats as the ``choices`` argument - to a model field. See the :ref:`model field reference documentation on - choices ` for more details. + Either an iterable (e.g., a list or tuple) of 2-tuples to use as + choices for this field, or a callable that returns such an iterable. + This argument accepts the same formats as the ``choices`` argument to a + model field. See the :ref:`model field reference documentation on + 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`` ~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index da2e5ae466..d5bae559a7 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -240,6 +240,9 @@ Forms will also update ``UploadedFile.content_type`` with the image's content type 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 ^^^^^^^^^^^^^ diff --git a/tests/forms_tests/tests/test_fields.py b/tests/forms_tests/tests/test_fields.py index 22af2cf23d..3caf902349 100644 --- a/tests/forms_tests/tests/test_fields.py +++ b/tests/forms_tests/tests/test_fields.py @@ -961,6 +961,28 @@ class FieldsTests(SimpleTestCase): self.assertEqual('5', f.clean('5')) 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 is just like ChoiceField, except that coerced types will # be returned: