mirror of https://github.com/django/django.git
Fixed #34899 -- Added blank choice to forms' callable choices lazily.
This commit is contained in:
parent
74afcee234
commit
171f91d9ef
|
@ -16,6 +16,7 @@ from django.db.models.constants import LOOKUP_SEP
|
||||||
from django.db.models.query_utils import DeferredAttribute, RegisterLookupMixin
|
from django.db.models.query_utils import DeferredAttribute, RegisterLookupMixin
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.choices import (
|
from django.utils.choices import (
|
||||||
|
BlankChoiceIterator,
|
||||||
CallableChoiceIterator,
|
CallableChoiceIterator,
|
||||||
flatten_choices,
|
flatten_choices,
|
||||||
normalize_choices,
|
normalize_choices,
|
||||||
|
@ -1055,14 +1056,9 @@ class Field(RegisterLookupMixin):
|
||||||
as <select> choices for this field.
|
as <select> choices for this field.
|
||||||
"""
|
"""
|
||||||
if self.choices is not None:
|
if self.choices is not None:
|
||||||
choices = list(self.choices)
|
|
||||||
if include_blank:
|
if include_blank:
|
||||||
blank_defined = any(
|
return BlankChoiceIterator(self.choices, blank_choice)
|
||||||
choice in ("", None) for choice, _ in self.flatchoices
|
return self.choices
|
||||||
)
|
|
||||||
if not blank_defined:
|
|
||||||
choices = blank_choice + choices
|
|
||||||
return choices
|
|
||||||
rel_model = self.remote_field.model
|
rel_model = self.remote_field.model
|
||||||
limit_choices_to = limit_choices_to or self.get_limit_choices_to()
|
limit_choices_to = limit_choices_to or self.get_limit_choices_to()
|
||||||
choice_func = operator.attrgetter(
|
choice_func = operator.attrgetter(
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
from collections.abc import Callable, Iterable, Iterator, Mapping
|
from collections.abc import Callable, Iterable, Iterator, Mapping
|
||||||
from itertools import islice, zip_longest
|
from itertools import islice, tee, zip_longest
|
||||||
|
|
||||||
from django.utils.functional import Promise
|
from django.utils.functional import Promise
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"BaseChoiceIterator",
|
"BaseChoiceIterator",
|
||||||
|
"BlankChoiceIterator",
|
||||||
"CallableChoiceIterator",
|
"CallableChoiceIterator",
|
||||||
"flatten_choices",
|
"flatten_choices",
|
||||||
"normalize_choices",
|
"normalize_choices",
|
||||||
|
@ -34,6 +35,20 @@ class BaseChoiceIterator:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BlankChoiceIterator(BaseChoiceIterator):
|
||||||
|
"""Iterator to lazily inject a blank choice."""
|
||||||
|
|
||||||
|
def __init__(self, choices, blank_choice):
|
||||||
|
self.choices = choices
|
||||||
|
self.blank_choice = blank_choice
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
choices, other = tee(self.choices)
|
||||||
|
if not any(value in ("", None) for value, _ in flatten_choices(other)):
|
||||||
|
yield from self.blank_choice
|
||||||
|
yield from choices
|
||||||
|
|
||||||
|
|
||||||
class CallableChoiceIterator(BaseChoiceIterator):
|
class CallableChoiceIterator(BaseChoiceIterator):
|
||||||
"""Iterator to lazily normalize choices generated by a callable."""
|
"""Iterator to lazily normalize choices generated by a callable."""
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ from django.forms.models import (
|
||||||
from django.template import Context, Template
|
from django.template import Context, Template
|
||||||
from django.test import SimpleTestCase, TestCase, ignore_warnings, skipUnlessDBFeature
|
from django.test import SimpleTestCase, TestCase, ignore_warnings, skipUnlessDBFeature
|
||||||
from django.test.utils import isolate_apps
|
from django.test.utils import isolate_apps
|
||||||
|
from django.utils.choices import BlankChoiceIterator
|
||||||
from django.utils.deprecation import RemovedInDjango60Warning
|
from django.utils.deprecation import RemovedInDjango60Warning
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
|
@ -2012,6 +2013,38 @@ class ModelFormBasicTests(TestCase):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@isolate_apps("model_forms")
|
||||||
|
def test_callable_choices_are_lazy(self):
|
||||||
|
call_count = 0
|
||||||
|
|
||||||
|
def get_animal_choices():
|
||||||
|
nonlocal call_count
|
||||||
|
call_count += 1
|
||||||
|
return [("LION", "Lion"), ("ZEBRA", "Zebra")]
|
||||||
|
|
||||||
|
class ZooKeeper(models.Model):
|
||||||
|
animal = models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=get_animal_choices,
|
||||||
|
max_length=5,
|
||||||
|
)
|
||||||
|
|
||||||
|
class ZooKeeperForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = ZooKeeper
|
||||||
|
fields = ["animal"]
|
||||||
|
|
||||||
|
self.assertEqual(call_count, 0)
|
||||||
|
form = ZooKeeperForm()
|
||||||
|
self.assertEqual(call_count, 0)
|
||||||
|
self.assertIsInstance(form.fields["animal"].choices, BlankChoiceIterator)
|
||||||
|
self.assertEqual(call_count, 0)
|
||||||
|
self.assertEqual(
|
||||||
|
form.fields["animal"].choices,
|
||||||
|
models.BLANK_CHOICE_DASH + [("LION", "Lion"), ("ZEBRA", "Zebra")],
|
||||||
|
)
|
||||||
|
self.assertEqual(call_count, 1)
|
||||||
|
|
||||||
def test_recleaning_model_form_instance(self):
|
def test_recleaning_model_form_instance(self):
|
||||||
"""
|
"""
|
||||||
Re-cleaning an instance that was added via a ModelForm shouldn't raise
|
Re-cleaning an instance that was added via a ModelForm shouldn't raise
|
||||||
|
|
Loading…
Reference in New Issue