I’ve been dabbling with Django over the last couple of months. I’ve played around with it a couple of times previously but this time I’ve actually built something reasonably substantial, which has meant that I’ve had to delve a bit deeper. One of the minor problems I solved today was how to group items in the HTML <select>
element generated by a form’s ModelChoiceField
. HTML has the <optgroup>
tag for this purpose.
It’s not immediately obvious how you can get ModelChoiceField
to use optgroups without over-riding the render
method and re-implementing the HTML generation yourself, or bypassing ModelChoiceField
completely and building something based on the basic ChoiceField
(which does support optgroups). The only potential solutions I found from searching took the former approach (here and here). The reason I’m writing this post is because I think I’ve found a better, more concise solution that might be of use to future searchers.
ChoiceField
accepts a choices
parameter to its constructor. In the simple case this is just a list of items (each item is value/label pair). However, it can also accept a list of groups where each group is a tuple consisting of the group label and a list of items. The problem is that ModelChoiceField
is different in that it has a queryset
parameter instead, so there is no way to pass in the group information.
However, a comment in the source code says that we can set a property called choices
after constructing the ModelChoiceField
instance and the queryset will be ignored. The HTML <select>
will instead be populated from this data structure with <optgroup>
elements as required.
Assuming we want to group items by a field on the model, we can build the list of tuples from the queryset by sub-classing ModelChoiceField
and over-riding the constructor. In this example I’m assuming that the field is a list of countries grouped by continent, where the continent is just a text field on the country model.
from django.forms import ModelChoiceField from itertools import groupby from operator import attrgetter class CountryChoiceField(ModelChoiceField): def __init__(self, *args, **kwargs): super(CountryChoiceField, self).__init__(*args, **kwargs) groups = groupby(kwargs['queryset'], attrgetter('continent')) self.choices = [(continent, [(c.id, self.label_from_instance(c)) for c in countries]) for continent, countries in groups] |
In order for this to work, the queryset must be sorted by the field that it is to be grouped by so that items in the same group are adjacent. You can do this before you pass it to the constructor or you can change the code above to call order_by
on the queryset.