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

2 thoughts on “Django Dynamic ModelChoiceFields

    • Sorry, I do not have time to write a full example. Do you have a specific question?

      Maybe the best way to understand this is to ignore my code and write your own example using regular Django and AJAX to populate the select with a set of choices that is different from the ones you used when creating the form. When you try to validate the form, you will get an error. If you swap in my form field, the error will go away.

      I hope that helps.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s