Django Dynamic ModelChoiceFields

In my projects, it is common to have select options that depend on other fields in the form. One seemingly obvious way to handle this is to attach an AJAX function to the onchange method of the independent fields and get the desired options for the select from the server. The problem with that approach is Django says the form is invalid if there are select options that were not there when the form was generated. What to do?

One solution is to include every possible select option in the Django form and then reduce those options as needed on the client side. When the form gets the POST data it tries to convert the result into a value by looking for the POST value as the first element of a choices tuple. The fact that some of the choices were not available on the client side does not matter.

Another solution is given here. Change your Django forms.ChoiceField into:

forms.CharField(widget=forms.Select())

A third option is to make a custom form field that solves this once and for all. It turns out the the problem is Django is trying to help you by trying to create a useful python object from the POST data. For the forms.ModelChoiceField, here is the Django source code:

class ModelChoiceField(ChoiceField):
    def to_python(self, value):
        if value in self.empty_values:
            return None
        try:
            key = self.to_field_name or 'pk'
            value = self.queryset.get(**{key: value})
        except (ValueError, self.queryset.model.DoesNotExist):
            raise ValidationError(self.error_messages['invalid_choice'], code='invalid_choice')
        return value

One way to get around that is to create a custom ModelChoiceField that overrides the to_python method. Here is one way to do that.

class AjaxModelChoiceField(forms.ModelChoiceField):
    def __init__(self, model_class, *args, **kwargs):
        queryset = model_class.objects.none()
        super(AjaxModelChoiceField, self).__init__(queryset, *args, **kwargs)
        self.model_class = model_class

    def to_python(self, value):
        if value in self.empty_values:
            return None
        try:
            key = self.to_field_name or 'pk'
            value = self.model_class.objects.get(**{key: value})
        except (ValueError, self.queryset.model.DoesNotExist):
            raise ValidationError(self.error_messages['invalid_choice'], code='invalid_choice')
        return value

# In your form
class MyForm(forms.Form):
    my_field = AjaxModelChoiceField(MyModel)

Django Forms, Choices and Caching

Some of the drop down menus on my forms require a bunch of database hits. To speed things up and reduce the load on my database, I use Django’s low level caching to hold those menus. To keep the cache fresh, I add code to clear the cache in the save method of the model. Nothing out of the ordinary here. So why weren’t my menus updating when the model changed???

My first thought was there was something wrong with my code to delete the cache. I was using the cache.delete(key) command. Some debug statements showed that was not the problem. They also showed that my method for generating the menu was not being called either. Odd…

Then I remembered I was setting the form choices when I was creating class variables. The class and those variables are created once when the server is started. Here was the offending code:

from django import forms

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

The problem was solved by moving the choices code to inside the __init__ method:

from django import forms

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

    def __init__(self, *args, **kwargs):
        super(MyForm, self).__init__(*args, **kwargs)
        self.fields['my_choice_field'].choices = my_model.make_choices()