1👍
I subclassed ChoiceField as suggested by GwynBleidD and it works sufficiently for now.
class ListModelChoiceField(forms.ChoiceField):
"""
special field using list instead of queryset as choices
"""
def __init__(self, model, *args, **kwargs):
self.model = model
super(ListModelChoiceField, self).__init__(*args, **kwargs)
def to_python(self, value):
if value in self.empty_values:
return None
try:
value = self.model.objects.get(id=value)
except self.model.DoesNotExist:
raise ValidationError(self.error_messages['invalid_choice'], code='invalid_choice')
return value
def valid_value(self, value):
"Check to see if the provided value is a valid choice"
if any(value.id == int(choice[0]) for choice in self.choices):
return True
return False
3👍
ModelChoiceField
is an subclass of ChoiceField
in which “normal” choices are replaced with iterator that will iterate through provided queryset. Also there is customized ‘to_python’ method that will return actual object instead of it’s pk. Unfortunately that iterator will reset queryset and hit database once again for each choice field, even if they are sharing queryset
What you need to do is subclass ChoiceField and mimic behaviour of ModelChoiceField
with one difference: it will take static choices list instead of queryset. That choices list you will build in your view once for all fields (or forms).
3👍
A maybe less invasive hack, using an overload of Django’s FormSets and keeping the base form untouched (i.e. keeping the ModelChoiceFields with their dynamic queryset):
from django import forms
class OptimFormSet( forms.BaseFormSet ):
"""
FormSet with minimized number of SQL queries for ModelChoiceFields
"""
def __init__( self, *args, modelchoicefields_qs=None, **kwargs ):
"""
Overload the ModelChoiceField querysets by a common queryset per
field, with dummy .all() and .iterator() methods to avoid multiple
queries when filling the (repeated) choices fields.
Parameters
----------
modelchoicefields_qs : dict
Dictionary of modelchoicefield querysets. If ``None``, the
modelchoicefields are identified internally
"""
# Init the formset
super( OptimFormSet, self ).__init__( *args, **kwargs )
if modelchoicefields_qs is None and len( self.forms ) > 0:
# Store querysets of modelchoicefields
modelchoicefields_qs = {}
first_form = self.forms[0]
for key in first_form.fields:
if isinstance( first_form.fields[key], forms.ModelChoiceField ):
modelchoicefields_qs[key] = first_form.fields[key].queryset
# Django calls .queryset.all() before iterating over the queried objects
# to render the select boxes. This clones the querysets and multiplies
# the queries for nothing.
# Hence, overload the querysets' .all() method to avoid cloning querysets
# in ModelChoiceField. Simply return the queryset itself with a lambda function.
# Django also calls .queryset.iterator() as an optimization which
# doesn't make sense for formsets. Hence, overload .iterator as well.
if modelchoicefields_qs:
for qs in modelchoicefields_qs.values():
qs.all = lambda local_qs=qs: local_qs # use a default value of qs to pass from late to immediate binding (so that the last qs is not used for all lambda's)
qs.iterator = qs.all
# Apply the common (non-cloning) querysets to all the forms
for form in self.forms:
for key in modelchoicefields_qs:
form.fields[key].queryset = modelchoicefields_qs[key]
In your view, you then call:
formset_class = forms.formset_factory( form=MyBaseForm, formset=OptimFormSet )
formset = formset_class()
And then render your template with the formset as described in Django’s doc.
Note that on form validation, you will still have 1 query per ModelChoiceField instance, but limited to a single primary key value each time. That is also the case with the accepted answer. To avoid that, the to_python
method should use the existing queryset, which would make the hack even hackier.
This works at least for Django 1.11.
- [Django]-Django – copy header from admin to all templates
- [Django]-Django REST framework nested serializer and POST nested JSON with files
- [Django]-Import parent module from submodule
- [Django]-Displaying a message as a popup/alert in Django?
- [Django]-How to use Apache to serve Django server and React client?