mirror of
https://github.com/django/django.git
synced 2025-01-08 17:37:20 +00:00
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.utils import timezone
|
||||
from django.utils.choices import (
|
||||
BlankChoiceIterator,
|
||||
CallableChoiceIterator,
|
||||
flatten_choices,
|
||||
normalize_choices,
|
||||
@ -1055,14 +1056,9 @@ class Field(RegisterLookupMixin):
|
||||
as <select> choices for this field.
|
||||
"""
|
||||
if self.choices is not None:
|
||||
choices = list(self.choices)
|
||||
if include_blank:
|
||||
blank_defined = any(
|
||||
choice in ("", None) for choice, _ in self.flatchoices
|
||||
)
|
||||
if not blank_defined:
|
||||
choices = blank_choice + choices
|
||||
return choices
|
||||
return BlankChoiceIterator(self.choices, blank_choice)
|
||||
return self.choices
|
||||
rel_model = self.remote_field.model
|
||||
limit_choices_to = limit_choices_to or self.get_limit_choices_to()
|
||||
choice_func = operator.attrgetter(
|
||||
|
@ -1,10 +1,11 @@
|
||||
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
|
||||
|
||||
__all__ = [
|
||||
"BaseChoiceIterator",
|
||||
"BlankChoiceIterator",
|
||||
"CallableChoiceIterator",
|
||||
"flatten_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):
|
||||
"""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.test import SimpleTestCase, TestCase, ignore_warnings, skipUnlessDBFeature
|
||||
from django.test.utils import isolate_apps
|
||||
from django.utils.choices import BlankChoiceIterator
|
||||
from django.utils.deprecation import RemovedInDjango60Warning
|
||||
|
||||
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):
|
||||
"""
|
||||
Re-cleaning an instance that was added via a ModelForm shouldn't raise
|
||||
|
Loading…
Reference in New Issue
Block a user