Grouping Choices in a Django Select

To make an ordinary Django ChoiceField you can do something like this:

choices = [[1, 'Apples'], [2, 'Oranges'], [3, 'Carrots'], [4, 'Beans']]
my_choices = forms.ChoiceField(choices=choices)

But what if you want to group the choices by using the HTML OPTGROUP tag? My first thought was to over-ride some on the methods in the Django Select widget code. But when I inspected the code, I found the code already supports this (Django 1.8). Here is how to do it:

choices = [
    ['Fruit', [[1, 'Apples'], [2, 'Oranges']]], 
    ['Veggies', [[3, 'Carrots'], [4, 'Beans']]]
]
my_choices = forms.ChoiceField(choices=choices)

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)