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.
This commit is contained in:
Jon Dufresne 2017-06-28 20:30:19 -07:00
parent 5cbcb36839
commit a1be12fe19
3 changed files with 37 additions and 12 deletions

View File

@ -93,10 +93,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):
""" """
Return an ``OrderedDict`` containing form fields for the given model. Return an ``OrderedDict`` containing form fields for the given model.
@ -123,6 +131,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 = []
@ -166,11 +177,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)
@ -241,11 +249,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 fields.items() if not v] none_model_fields = [k for k, v in fields.items() if not v]
@ -291,6 +301,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

@ -1,7 +1,7 @@
import datetime import datetime
import os import os
from decimal import Decimal from decimal import Decimal
from unittest import skipUnless from unittest import mock, skipUnless
from django import forms from django import forms
from django.core.exceptions import ( from django.core.exceptions import (
@ -2906,6 +2906,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):