Lazy choices in Django form
-
05-09-2019 - |
Question
I have a Django my_forms.py like this:
class CarSearchForm(forms.Form):
# lots of fields like this
bodystyle = forms.ChoiceField(choices=bodystyle_choices())
Each choice is e.g. ("Saloon", "Saloon (15 cars)"). So the choices are computed by this function.
def bodystyle_choices():
return [(bodystyle.bodystyle_name, '%s (%s cars)' %
(bodystyle.bodystyle_name, bodystyle.car_set.count()))
for bodystyle in Bodystyle.objects.all()]
My problem is the choices functions are getting executed every time I merely import my_forms.py. I think this is due to the way Django declares its fields: in the class but not in a class method. Which is fine but my views.py imports my_forms.py so the choices lookups are done on every request no matter which view is used.
I thought that maybe putting choices=bodystyle_choices with no bracket would work, but I get:
'function' object is not iterable
Obviously I can use caching and put the "import my_forms" just in the view functions required but that doesn't change the main point: my choices need to be lazy!
Solution
You can use the "lazy" function :)
from django.utils.functional import lazy
class CarSearchForm(forms.Form):
# lots of fields like this
bodystyle = forms.ChoiceField(choices=lazy(bodystyle_choices, tuple)())
very nice util function !
OTHER TIPS
Try using a ModelChoiceField instead of a simple ChoiceField. I think you will be able to achieve what you want by tweaking your models a bit. Take a look at the docs for more.
I would also add that ModelChoiceFields are lazy
by default :)
Expanding on what Baishampayan Ghose said, this should probably be considered the most direct approach:
from django.forms import ModelChoiceField
class BodystyleChoiceField(ModelChoiceField):
def label_from_instance(self, obj):
return '%s (%s cars)' % (obj.bodystyle_name, obj.car_set.count()))
class CarSearchForm(forms.Form):
bodystyle = BodystyleChoiceField(queryset=Bodystyle.objects.all())
Docs are here: https://docs.djangoproject.com/en/1.8/ref/forms/fields/#modelchoicefield
This has the benefit that form.cleaned_data['bodystyle']
is a Bodystyle
instance instead of a string.
You can now just use (since I think Django 1.8):
class CarSearchForm(forms.Form):
# lots of fields like this
bodystyle = forms.ChoiceField(choices=bodystyle_choices)
Note the missing parenthesis. If you need to pass arguments, I just make a special version of the function with them hardcoded just for that form.