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.