[1.11.x] Fixed #28345 -- Applied limit_choices_to during ModelForm.__init__().

field_for_model() now has an additional keyword argument,
apply_limit_choices_to, allowing it to continue to be used to create
form fields dynamically after ModelForm.__init__() is called.

Thanks Tim Graham for the review.

Backport of a1be12fe193c8f3de8a0b0820f460a302472375f from master
This commit is contained in:
Jon Dufresne 2017-06-28 20:30:19 -07:00 committed by Tim Graham
parent c1621d8008
commit 8641489f4d
3 changed files with 37 additions and 12 deletions

View File

@ -97,10 +97,18 @@ def model_to_dict(instance, fields=None, exclude=None):
return data return data
def apply_limit_choices_to_to_formfield(formfield):
"""Apply limit_choices_to to the formfield's queryset if needed."""
if hasattr(formfield, 'queryset') and hasattr(formfield, 'get_limit_choices_to'):
limit_choices_to = formfield.get_limit_choices_to()
if limit_choices_to is not None:
formfield.queryset = formfield.queryset.complex_filter(limit_choices_to)
def fields_for_model(model, fields=None, exclude=None, widgets=None, def fields_for_model(model, fields=None, exclude=None, widgets=None,
formfield_callback=None, localized_fields=None, formfield_callback=None, localized_fields=None,
labels=None, help_texts=None, error_messages=None, labels=None, help_texts=None, error_messages=None,
field_classes=None): field_classes=None, apply_limit_choices_to=True):
""" """
Returns a ``OrderedDict`` containing form fields for the given model. Returns a ``OrderedDict`` containing form fields for the given model.
@ -127,6 +135,9 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None,
``field_classes`` is a dictionary of model field names mapped to a form ``field_classes`` is a dictionary of model field names mapped to a form
field class. field class.
``apply_limit_choices_to`` is a boolean indicating if limit_choices_to
should be applied to a field's queryset.
""" """
field_list = [] field_list = []
ignored = [] ignored = []
@ -170,11 +181,8 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None,
formfield = formfield_callback(f, **kwargs) formfield = formfield_callback(f, **kwargs)
if formfield: if formfield:
# Apply ``limit_choices_to``. if apply_limit_choices_to:
if hasattr(formfield, 'queryset') and hasattr(formfield, 'get_limit_choices_to'): apply_limit_choices_to_to_formfield(formfield)
limit_choices_to = formfield.get_limit_choices_to()
if limit_choices_to is not None:
formfield.queryset = formfield.queryset.complex_filter(limit_choices_to)
field_list.append((f.name, formfield)) field_list.append((f.name, formfield))
else: else:
ignored.append(f.name) ignored.append(f.name)
@ -245,11 +253,13 @@ class ModelFormMetaclass(DeclarativeFieldsMetaclass):
# fields from the model" # fields from the model"
opts.fields = None opts.fields = None
fields = fields_for_model(opts.model, opts.fields, opts.exclude, fields = fields_for_model(
opts.widgets, formfield_callback, opts.model, opts.fields, opts.exclude, opts.widgets,
opts.localized_fields, opts.labels, formfield_callback, opts.localized_fields, opts.labels,
opts.help_texts, opts.error_messages, opts.help_texts, opts.error_messages, opts.field_classes,
opts.field_classes) # limit_choices_to will be applied during ModelForm.__init__().
apply_limit_choices_to=False,
)
# make sure opts.fields doesn't specify an invalid field # make sure opts.fields doesn't specify an invalid field
none_model_fields = [k for k, v in six.iteritems(fields) if not v] none_model_fields = [k for k, v in six.iteritems(fields) if not v]
@ -296,6 +306,8 @@ class BaseModelForm(BaseForm):
data, files, auto_id, prefix, object_data, error_class, data, files, auto_id, prefix, object_data, error_class,
label_suffix, empty_permitted, use_required_attribute=use_required_attribute, label_suffix, empty_permitted, use_required_attribute=use_required_attribute,
) )
for formfield in self.fields.values():
apply_limit_choices_to_to_formfield(formfield)
def _get_validation_exclusions(self): def _get_validation_exclusions(self):
""" """

View File

@ -57,3 +57,6 @@ Bugfixes
* Fixed ``UnboundLocalError`` crash in ``RenameField`` with nonexistent field * Fixed ``UnboundLocalError`` crash in ``RenameField`` with nonexistent field
(:ticket:`28350`). (:ticket:`28350`).
* Fixed a regression preventing a model field's ``limit_choices_to`` from being
evaluated when a ``ModelForm`` is instantiated (:ticket:`28345`).

View File

@ -19,7 +19,7 @@ from django.forms.models import (
) )
from django.forms.widgets import CheckboxSelectMultiple from django.forms.widgets import CheckboxSelectMultiple
from django.template import Context, Template from django.template import Context, Template
from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature from django.test import SimpleTestCase, TestCase, mock, skipUnlessDBFeature
from django.utils import six from django.utils import six
from django.utils._os import upath from django.utils._os import upath
@ -2943,6 +2943,16 @@ class LimitChoicesToTests(TestCase):
fields = fields_for_model(StumpJoke, ['has_fooled_today']) fields = fields_for_model(StumpJoke, ['has_fooled_today'])
self.assertSequenceEqual(fields['has_fooled_today'].queryset, [self.threepwood]) self.assertSequenceEqual(fields['has_fooled_today'].queryset, [self.threepwood])
def test_callable_called_each_time_form_is_instantiated(self):
field = StumpJokeForm.base_fields['most_recently_fooled']
with mock.patch.object(field, 'limit_choices_to') as today_callable_dict:
StumpJokeForm()
self.assertEqual(today_callable_dict.call_count, 1)
StumpJokeForm()
self.assertEqual(today_callable_dict.call_count, 2)
StumpJokeForm()
self.assertEqual(today_callable_dict.call_count, 3)
class FormFieldCallbackTests(SimpleTestCase): class FormFieldCallbackTests(SimpleTestCase):