How can a ChoiceField.choices callable know what choices to return?

5.3k views Asked by At

In Django 1.8, the ChoiceField's choices argument can accept a callable:

def get_choices():
    return [(1, "one"), (2, "two")]

class MyForm(forms.Form):
    my_choice_field = forms.ChoiceField(choices=get_choices)

In the above example, get_choices() always returns the same choices. However, being able to assign a callable to choices does not make much sense unless that callable knows something like, say, an object id, each time it is called. How can I pass such a thing to it?

2

There are 2 answers

4
Anas On BEST ANSWER

You can't do it in the form declaration because the CallableChoiceIterator calls the function without arguments that he gets from here. Doing in the __init__ Form method is easier than creating your own ChoiceField I guess. Here is what I suggest:

class MyForm(forms.Form):
    my_choice_field = forms.ChoiceField(choices=())

    def __init__(self, *args, **kwargs):
        # Let's pass the object id as a form kwarg
        self.object_id = kwargs.pop('object_id') 

        # django metaclass magic to construct fields
        super().__init__(*args, **kwargs)

        # Now you can get your choices based on that object id            
        self.fields['my_choice_field'].choices = your_get_choices_function(self.object_id)

That supposes that you have some Class Based View that looks that has a method like this :

class MyFormView(FormView):
   # ...

   def get_form_kwargs(self):
       kwargs = super().get_form_kwargs()
       kwargs['object_id'] = 'YOUR_OBJECT_ID_HERE'
       return kwargs

   # ...

P.S : The super() function call supposes you are using python 3

0
Adam Barnes On

The reason it's possible to set a callable like that is to avoid situations where you're using models before they're ready.

forms.py

class Foo(ModelForm):
    choice_field = ChoiceField(choices=[
        user.username.lower() for user in User.objects.all()
    ])

Were forms.py imported before models were ready, (which it probably is because views.py generally likes to import it, and urls.py generally likes to import that, and urls.py is imported by the startup machinery), it will raise an exception due to trying to do ORM stuff before all the apps are imported.

The correct way is to use a callable like so:

def lower_case_usernames():
    return [user.username.lower() for user in User.objects.all()]


class Foo(ModelForm):
    choice_field = ChoiceField(choices=lower_case_usernames)

This also has the benefit of being able to change without restarting the server.