25

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!

Eli Courtwright
  • 186,300
  • 67
  • 213
  • 256
Tom Viner
  • 6,655
  • 7
  • 39
  • 40

4 Answers4

51

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 !

Sidi
  • 1,739
  • 14
  • 16
  • 2
    Definitely the superior solution, this should be the accepted answer imo. – Sverre Rabbelier Feb 27 '11 at 16:17
  • 2
    /agree is the cleanest solution I have seen so far and this lets you skip problems with validations, an important difference from the ModelChoiceField. – Hassek Aug 18 '11 at 21:59
  • 10
    This does not appear to work, at least with Django 1.6, because `ChoiceField._set_choices` does `self._choices = self.widget.choices = list(value)` – spookylukey Oct 23 '14 at 11:58
  • 2
    This does not work for me at all on Django 1.7, could anyone check if it works really? First, it raises Exception about the wrong type of returned value ("Lazy object returned unexpected type") when I use 'tuple' as 2nd parameter to 'lazy'. Second, when I use 'list' there, the function is called... once! Then it is not called any more and I have the same list of values as in the beginning. – dotz May 30 '15 at 17:33
18

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 :)

Baishampayan Ghose
  • 19,928
  • 10
  • 56
  • 60
2

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.

MrDBA
  • 420
  • 1
  • 6
  • 8
1

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.

John
  • 11
  • 1