Skip to content

Commit 8641489

Browse files
jdufresnetimgraham
authored andcommitted
[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 a1be12f from master
1 parent c1621d8 commit 8641489

File tree

3 files changed

+37
-12
lines changed

3 files changed

+37
-12
lines changed

django/forms/models.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,18 @@ def model_to_dict(instance, fields=None, exclude=None):
9797
return data
9898

9999

100+
def apply_limit_choices_to_to_formfield(formfield):
101+
"""Apply limit_choices_to to the formfield's queryset if needed."""
102+
if hasattr(formfield, 'queryset') and hasattr(formfield, 'get_limit_choices_to'):
103+
limit_choices_to = formfield.get_limit_choices_to()
104+
if limit_choices_to is not None:
105+
formfield.queryset = formfield.queryset.complex_filter(limit_choices_to)
106+
107+
100108
def fields_for_model(model, fields=None, exclude=None, widgets=None,
101109
formfield_callback=None, localized_fields=None,
102110
labels=None, help_texts=None, error_messages=None,
103-
field_classes=None):
111+
field_classes=None, apply_limit_choices_to=True):
104112
"""
105113
Returns a ``OrderedDict`` containing form fields for the given model.
106114
@@ -127,6 +135,9 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None,
127135
128136
``field_classes`` is a dictionary of model field names mapped to a form
129137
field class.
138+
139+
``apply_limit_choices_to`` is a boolean indicating if limit_choices_to
140+
should be applied to a field's queryset.
130141
"""
131142
field_list = []
132143
ignored = []
@@ -170,11 +181,8 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None,
170181
formfield = formfield_callback(f, **kwargs)
171182

172183
if formfield:
173-
# Apply ``limit_choices_to``.
174-
if hasattr(formfield, 'queryset') and hasattr(formfield, 'get_limit_choices_to'):
175-
limit_choices_to = formfield.get_limit_choices_to()
176-
if limit_choices_to is not None:
177-
formfield.queryset = formfield.queryset.complex_filter(limit_choices_to)
184+
if apply_limit_choices_to:
185+
apply_limit_choices_to_to_formfield(formfield)
178186
field_list.append((f.name, formfield))
179187
else:
180188
ignored.append(f.name)
@@ -245,11 +253,13 @@ def __new__(mcs, name, bases, attrs):
245253
# fields from the model"
246254
opts.fields = None
247255

248-
fields = fields_for_model(opts.model, opts.fields, opts.exclude,
249-
opts.widgets, formfield_callback,
250-
opts.localized_fields, opts.labels,
251-
opts.help_texts, opts.error_messages,
252-
opts.field_classes)
256+
fields = fields_for_model(
257+
opts.model, opts.fields, opts.exclude, opts.widgets,
258+
formfield_callback, opts.localized_fields, opts.labels,
259+
opts.help_texts, opts.error_messages, opts.field_classes,
260+
# limit_choices_to will be applied during ModelForm.__init__().
261+
apply_limit_choices_to=False,
262+
)
253263

254264
# make sure opts.fields doesn't specify an invalid field
255265
none_model_fields = [k for k, v in six.iteritems(fields) if not v]
@@ -296,6 +306,8 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
296306
data, files, auto_id, prefix, object_data, error_class,
297307
label_suffix, empty_permitted, use_required_attribute=use_required_attribute,
298308
)
309+
for formfield in self.fields.values():
310+
apply_limit_choices_to_to_formfield(formfield)
299311

300312
def _get_validation_exclusions(self):
301313
"""

docs/releases/1.11.3.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,6 @@ Bugfixes
5757

5858
* Fixed ``UnboundLocalError`` crash in ``RenameField`` with nonexistent field
5959
(:ticket:`28350`).
60+
61+
* Fixed a regression preventing a model field's ``limit_choices_to`` from being
62+
evaluated when a ``ModelForm`` is instantiated (:ticket:`28345`).

tests/model_forms/tests.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
)
2020
from django.forms.widgets import CheckboxSelectMultiple
2121
from django.template import Context, Template
22-
from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
22+
from django.test import SimpleTestCase, TestCase, mock, skipUnlessDBFeature
2323
from django.utils import six
2424
from django.utils._os import upath
2525

@@ -2943,6 +2943,16 @@ def test_fields_for_model_applies_limit_choices_to(self):
29432943
fields = fields_for_model(StumpJoke, ['has_fooled_today'])
29442944
self.assertSequenceEqual(fields['has_fooled_today'].queryset, [self.threepwood])
29452945

2946+
def test_callable_called_each_time_form_is_instantiated(self):
2947+
field = StumpJokeForm.base_fields['most_recently_fooled']
2948+
with mock.patch.object(field, 'limit_choices_to') as today_callable_dict:
2949+
StumpJokeForm()
2950+
self.assertEqual(today_callable_dict.call_count, 1)
2951+
StumpJokeForm()
2952+
self.assertEqual(today_callable_dict.call_count, 2)
2953+
StumpJokeForm()
2954+
self.assertEqual(today_callable_dict.call_count, 3)
2955+
29462956

29472957
class FormFieldCallbackTests(SimpleTestCase):
29482958

0 commit comments

Comments
 (0)