Contents

Reusable Django Admin Filters

It’s fairly common to have a many-to-many connection to a User model. But sometimes, you have a lot of users in that table.

Problem 1: relations to large tables are unwieldy

If we let the Django Admin load every single user into the dropdown in the Django Admin, it’s unusable. The dropdowns are too long. The page takes too long to load. We need to filter it in advance.

Dig around a bit online, and you’ll find some code of this form:

def formfield_for_dbfield(self, db_field, request, **kwargs):
    field = super(MyModelAdmin, self).formfield_for_dbfield(
        db_field, request, **kwargs
    )
    if db_field.name == "author":
        field.queryset = field.queryset.filter(email__endswith="example.com")
    return field

As the admin page is rendered, each database field is run through that function before rendering. That’s your opportunity to modify the queryset.

Yes, database field. This covers both foreign key and many-to-many relationships.

Problem 2: the code isn’t DRY

What if you have a lot of models with those connections to the user table? I was pasting the same block around into several admins.

I need the same method override in multiple classes. This is a job for decorators. Cue a lot of reading about decorators (I’ve never written one before) and experimentation.

The solution

Here’s what I came up with for defining the decorator:

def add_field_filter(configs):
    def wrapper(cls):
        def formfield_for_dbfield(self, db_field, request, **kwargs):
            field = super(cls, self).formfield_for_dbfield(db_field, request, **kwargs)

            for (field_names, filters) in configs:
                if db_field.name in field_names:
                    field.queryset = field.queryset.filter(**filters)

            return field

        setattr(cls, "formfield_for_dbfield", formfield_for_dbfield)
        return cls

    return wrapper

That’s mostly standard usage of decorators (defining the wrapper() and then returning it), but since we’re trying to define formfield_for_dbfield on the wrapped model, we have to attach it using setattr().

Put that at the top of your admin.py. It’s more flexible than the original, too. It can operate on more than one field, and it can apply different filters to each named field.

Here’s the basic usage:

@admin.register(MyModel)
@add_field_filter([(["author"], {'email__endswith': "example.com"})])
Class MyModelAdmin(admin.ModelAdmin):
...

Advanced

Or, if you needed the same filter on two fields:

@admin.register(MyModel)
@add_field_filter([(["author", "editor"], {'email__endswith': "example.com"})])
Class MyModelAdmin(admin.ModelAdmin):
...

Two filters on one field:

@admin.register(MyModel)
@add_field_filter([(["author"], {'email__endswith': "example.com", 'active': True})])
Class MyModelAdmin(admin.ModelAdmin):
...

Different filters for different fields:

@admin.register(MyModel)
@add_field_filter([(["author"], {'email__endswith': "example.com"}),
                   (["editor"], {'active': True})])
Class MyModelAdmin(admin.ModelAdmin):
...

Frameworks can easily encourage duplicate code, but the decorator pattern helps. Hopefully this will help you keep your code tidy.