Filtering querysets in django.contrib.admin forms

I make extensive use of the django admin interface. It is the primary tool for our support team to look at user data for our product, and I have stretched it in many ways to suit my needs.

One problem I often come back to is a need to filter querysets in forms and formsets. Specifically, the objects that should be presented to the admin user in a relationship to the currently viewed object should be filtered. In most cases, this is something as simple as making sure the Person and the Units they work at are within the same company.

There is a simple bit of boilerplate that can do this. You need to create a custom form, and attach this to the ModelAdmin for the parent object:

 1from django.contrib import admin
 2from django import forms
 3from models import Person, Unit
 5class PersonAdminForm(forms.ModelForm):
 6    class Meta:
 7        model = Person
 9    def __init__(self, *args, **kwargs):
10        super(PersonAdminForm, self).__init__(*args, **kwargs)
11        # This is the bit that matters:
12        self.fields['units'].queryset =
14class PersonAdmin(admin.ModelAdmin):
15    form = PersonAdminForm

In actuality, it is a little more complicated than this: you need to test if the selected object has a company, and really, if the user has changed the company (or selected it on a new person), you should use that instead. So the code looks a bit more like:

 1company = None
 2if'company', None):
 3    try:
 4        company = Company.objects.get(['company'])
 5    except Company.DoesNotExist:
 6        pass
 8    try:
 9        company =
10    except Company.DoesNotExist:
11        pass
12if company:
13    self.fields['units'].queryset = company.units.all()

Now, having to write all of that every time you have to filter the choices available wears rather thin. And wait until you need to do it to a formset instead: you need to also do stuff to the empty_form, so that when you dynamically add an inline form, it has the correct choices.

Enter FilteringForm, and her niece FilteringFormSet:

 1from django import forms
 2from django.core.exceptions import ObjectDoesNotExist
 4class FilterMixin(object):
 5    filters = {}
 6    instance_filters = {}
 7    def apply_filters(self, forms=None):
 8        # If we didn't get a forms argument, we apply to ourself.
 9        if forms is None:
10            forms = [self]
11        # We need to apply instance filters first, as they allow us to
12        # select an attribute on our instance to be the queryset, and
13        # then apply a filter onto that with filters.
14        for field, attr in self.instance_filters.iteritems():
15            # It may be using a related attribute.
16            tokens = attr.split('.')
18            source = None
19            # See if there is any incoming data first.
20            if[0], ''):
21                try:
22                    source = self.instance._meta.get_field_by_name(tokens[0])[0][tokens[0]])
23                except ObjectDoesNotExist:
24                    pass
25            # Else, look for a match on the object we already have stored
26            if not source:
27                try:
28                    source = getattr(self.instance, tokens[0])
29                except ObjectDoesNotExist:
30                    pass
32            # Now, look for child attributes.
33            if source:
34                for segment in tokens[1:]:
35                    source = getattr(source, segment)
36                if forms:
37                    for form in forms:
38                        form.fields[field].queryset = source
40        # We can now apply any simple filters to the queryset.
41        for field, q_filter in self.filters.iteritems():
42            for form in forms:
43                form.fields[field].queryset = form.fields[field].queryset.filter(q_filter)
46class FilteringForm(forms.ModelForm, FilterMixin):
47    def __init__(self, *args, **kwargs):
48        super(FilteringForm, self).__init__(*args, **kwargs)
49        self.apply_filters()
51class FilteringFormSet(forms.models.BaseInlineFormSet, FilterMixin):
52    filters = {}
53    instance_filters = {}
55    def __init__(self, *args, **kwargs):
56        super(FilteringFormSet, self).__init__(*args, **kwargs)
57        self.apply_filters(self.forms)
59    def _get_empty_form(self, **kwargs):
60        form = super(FilteringFormSet, self)._get_empty_form(**kwargs)
61        self.apply_filters([form])
62        return form
63    empty_form = property(_get_empty_form)

Now, to use all of this, you still need to subclass, but you can declare the filters:

1class PersonAdminForm(FilteringForm):
2    class Meta:
3        model = Person
5    instance_filters = {
6        'units': 'company.units'
7    }

You can also have non-instance filters, and they will be applied after the instance_filters:

 1from django.db import models
 3class PersonAdminForm(FilteringForm):
 4    class Meta:
 5        model = Person
 7    instance_filters = {
 8        'units': 'company.units'
 9    }
10    filters = {
11        'units': models.Q(is_active=True)
12    }

I think it might be nice to be able to add an extra set of filtering for the empty form in a formset, so you could make it that only choices that hadn’t already been selected, for instance, were the only ones available. But that isn’t an issue for me right now.