summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--django/forms/models.py41
-rw-r--r--django/forms/widgets.py6
-rw-r--r--tests/model_forms/test_modelchoicefield.py11
3 files changed, 48 insertions, 10 deletions
diff --git a/django/forms/models.py b/django/forms/models.py
index 9686baa6f2..ac56a4df78 100644
--- a/django/forms/models.py
+++ b/django/forms/models.py
@@ -3,6 +3,7 @@ Helper functions for creating Form classes from Django models
and database field objects.
"""
+from collections import deque
from itertools import chain
from django.core.exceptions import (
@@ -1438,16 +1439,30 @@ class ModelChoiceIterator(BaseChoiceIterator):
def __init__(self, field):
self.field = field
self.queryset = field.queryset
+ self._deque = deque()
+
+ def generator():
+ if self.field.empty_label is not None:
+ yield ("", self.field.empty_label)
+ queryset = self.queryset
+ # Can't use iterator() when queryset uses prefetch_related()
+ if not queryset._prefetch_related_lookups:
+ queryset = queryset.iterator()
+ for obj in iter(queryset):
+ yield self.choice(obj)
+ # Reset the generator after it's exhausted so that it can
+ # be used again.
+ self.generator = generator()
+
+ self.generator = generator()
def __iter__(self):
- if self.field.empty_label is not None:
- yield ("", self.field.empty_label)
- queryset = self.queryset
- # Can't use iterator() when queryset uses prefetch_related()
- if not queryset._prefetch_related_lookups:
- queryset = queryset.iterator()
- for obj in queryset:
- yield self.choice(obj)
+ return self
+
+ def __next__(self):
+ if self._deque:
+ return self._deque.popleft()
+ return next(self.generator)
def __len__(self):
# count() adds a query but uses less memory since the QuerySet results
@@ -1456,7 +1471,9 @@ class ModelChoiceIterator(BaseChoiceIterator):
return self.queryset.count() + (1 if self.field.empty_label is not None else 0)
def __bool__(self):
- return self.field.empty_label is not None or self.queryset.exists()
+ return (
+ self.field.empty_label is not None or self._deque or self.queryset.exists()
+ )
def choice(self, obj):
return (
@@ -1464,6 +1481,12 @@ class ModelChoiceIterator(BaseChoiceIterator):
self.field.label_from_instance(obj),
)
+ def peek(self):
+ value = next(self.generator, None)
+ if value is not None:
+ self._deque.append(value)
+ return value
+
class ModelChoiceField(ChoiceField):
"""A ChoiceField whose choices are a model QuerySet."""
diff --git a/django/forms/widgets.py b/django/forms/widgets.py
index 1bcfeba288..82685a3fea 100644
--- a/django/forms/widgets.py
+++ b/django/forms/widgets.py
@@ -851,7 +851,11 @@ class Select(ChoiceWidget):
if self.allow_multiple_selected:
return use_required_attribute
- first_choice = next(iter(self.choices), None)
+ first_choice = (
+ self.choices.peek()
+ if hasattr(self.choices, "peek")
+ else next(iter(self.choices), None)
+ )
return (
use_required_attribute
and first_choice is not None
diff --git a/tests/model_forms/test_modelchoicefield.py b/tests/model_forms/test_modelchoicefield.py
index 40c625da7c..f2f75829d1 100644
--- a/tests/model_forms/test_modelchoicefield.py
+++ b/tests/model_forms/test_modelchoicefield.py
@@ -169,6 +169,17 @@ class ModelChoiceFieldTests(TestCase):
Category.objects.all().delete()
self.assertIs(bool(f.choices), False)
+ def test_empty_label_one_query(self):
+ class MyForm(forms.Form):
+ f = forms.ModelChoiceField(Category.objects.all(), empty_label=None)
+
+ form = MyForm()
+ with self.assertNumQueries(1):
+ form.as_ul()
+ # Make sure the query re-runs when re-rendering the form.
+ with self.assertNumQueries(1):
+ form.as_ul()
+
def test_choices_bool_empty_label(self):
f = forms.ModelChoiceField(Category.objects.all(), empty_label="--------")
Category.objects.all().delete()