Fixed #27002 -- Prevented double query when rendering ModelChoiceField.

This commit is contained in:
Alex Hill 2016-08-03 14:18:48 +08:00 committed by Tim Graham
parent 29a3f8b4bb
commit 74105b2636
2 changed files with 28 additions and 12 deletions

View File

@ -8,6 +8,7 @@ from django.utils import six
from django.utils.encoding import ( from django.utils.encoding import (
force_text, python_2_unicode_compatible, smart_text, force_text, python_2_unicode_compatible, smart_text,
) )
from django.utils.functional import cached_property
from django.utils.html import conditional_escape, format_html, html_safe from django.utils.html import conditional_escape, format_html, html_safe
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -42,28 +43,32 @@ class BoundField(object):
return self.as_widget() + self.as_hidden(only_initial=True) return self.as_widget() + self.as_hidden(only_initial=True)
return self.as_widget() return self.as_widget()
def __iter__(self): @cached_property
def subwidgets(self):
""" """
Yields rendered strings that comprise all widgets in this BoundField. Most widgets yield a single subwidget, but others like RadioSelect and
CheckboxSelectMultiple produce one subwidget for each choice.
This really is only useful for RadioSelect widgets, so that you can This property is cached so that only one database query occurs when
iterate over individual radio buttons in a template. rendering ModelChoiceFields.
""" """
id_ = self.field.widget.attrs.get('id') or self.auto_id id_ = self.field.widget.attrs.get('id') or self.auto_id
attrs = {'id': id_} if id_ else {} attrs = {'id': id_} if id_ else {}
attrs = self.build_widget_attrs(attrs) attrs = self.build_widget_attrs(attrs)
for subwidget in self.field.widget.subwidgets(self.html_name, self.value(), attrs): return list(self.field.widget.subwidgets(self.html_name, self.value(), attrs))
yield subwidget
def __iter__(self):
return iter(self.subwidgets)
def __len__(self): def __len__(self):
return len(list(self.__iter__())) return len(self.subwidgets)
def __getitem__(self, idx): def __getitem__(self, idx):
# Prevent unnecessary reevaluation when accessing BoundField's attrs # Prevent unnecessary reevaluation when accessing BoundField's attrs
# from templates. # from templates.
if not isinstance(idx, six.integer_types + (slice,)): if not isinstance(idx, six.integer_types + (slice,)):
raise TypeError raise TypeError
return list(self.__iter__())[idx] return self.subwidgets[idx]
@property @property
def errors(self): def errors(self):

View File

@ -1576,11 +1576,22 @@ class ModelChoiceFieldTests(TestCase):
field = CustomModelChoiceField(Category.objects.all()) field = CustomModelChoiceField(Category.objects.all())
self.assertIsInstance(field.choices, CustomModelChoiceIterator) self.assertIsInstance(field.choices, CustomModelChoiceIterator)
def test_radioselect_num_queries(self): def test_modelchoicefield_num_queries(self):
class CategoriesForm(forms.Form): """
categories = forms.ModelChoiceField(Category.objects.all(), widget=forms.RadioSelect) Widgets that render multiple subwidgets shouldn't make more than one
database query.
"""
categories = Category.objects.all()
class CategoriesForm(forms.Form):
radio = forms.ModelChoiceField(queryset=categories, widget=forms.RadioSelect)
checkbox = forms.ModelMultipleChoiceField(queryset=categories, widget=forms.CheckboxSelectMultiple)
template = Template("""
{% for widget in form.checkbox %}{{ widget }}{% endfor %}
{% for widget in form.radio %}{{ widget }}{% endfor %}
""")
template = Template('{% for widget in form.categories %}{{ widget }}{% endfor %}')
with self.assertNumQueries(2): with self.assertNumQueries(2):
template.render(Context({'form': CategoriesForm()})) template.render(Context({'form': CategoriesForm()}))