Achieving Symmetrical ManyToMany Filtering in Django Admin
I recently tackled an interesting challenge that I think could benefit others in the community. The problem? Creating a symmetrical ManyToMany filter widget in the Django admin dashboard. You know, the kind where you can filter and select related objects from both sides of the relationship with the same neat horizontal filter interface.
The Problem
Let's say you have a CompanyStructure
model with a ManyToMany relationship to Django's CustomUser
. By default, Django admin gives you a nice horizontal filter widget on the CompanyStructure
side, but not on the CustomUser
side. This creates an inconsistent user experience when managing these relationships.
The Solution
After some digging into Django's internals, I found an elegant solution using Django's RelatedFieldWidgetWrapper
and a custom ModelForm. Here's how to implement it:
1. First, let's look at our models:
from django.contrib.auth.models import AbstractUser
from django.db import models
import treenode.models as treenode_models
class CustomUser(AbstractUser):
objects = CustomUserManager()
def __str__(self):
return self.username
class CompanyStructure(treenode_models.TreeNodeModel):
name = models.CharField(max_length=255)
users = models.ManyToManyField(
CustomUser,
related_name='company_structures',
blank=True
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
2. The Magic: Custom Form with RelatedFieldWidgetWrapper
Here's where the magic happens. We'll create a custom form that adds the horizontal filter widget:
from django import forms
from django.contrib import admin
from django.db import models
from django.contrib.admin import widgets
class CustomUserForm(forms.ModelForm):
company_structures = forms.ModelMultipleChoiceField(
queryset=CompanyStructure.objects.all(),
widget=widgets.RelatedFieldWidgetWrapper(
widget=widgets.FilteredSelectMultiple('company structures', False),
rel=models.ManyToManyRel(
field=CustomUser,
to=CompanyStructure,
through=CustomUser.company_structures.through
),
admin_site=admin.site
)
)
class Meta:
model = CustomUser
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Initialize with existing relations
if self.instance and self.instance.pk:
self.fields['company_structures'].initial = (
self.instance.company_structures.all()
)
def save(self, commit=True):
instance = super().save(commit=False)
if commit:
instance.save()
# Save the ManyToMany relations
instance.company_structures.set(self.cleaned_data['company_structures'])
return instance
3. Register with Admin
Finally, we hook it all up in the admin:
@admin.register(CustomUser)
class CustomUserAdmin(UserAdmin):
form = CustomUserForm
# ... other admin configurations ...
See how it looks
How It Works
Let's break down the key components that make this solution work:
RelatedFieldWidgetWrapper: This is the secret sauce. It's the same widget Django uses internally for related fields, but we're explicitly implementing it for our reverse relationship.
FilteredSelectMultiple: This creates the actual horizontal filter interface with search functionality.
ManyToManyRel: This tells Django about the relationship type and provides necessary metadata for the widget to function properly.
Key Benefits
Consistent UI: Users get the same filtering experience from both sides of the relationship
Better UX: The horizontal filter widget is more user-friendly than a basic select field
Search Functionality: Built-in search makes it easier to find specific items in large datasets
Potential Gotchas
Performance: With large datasets, you might want to optimize the queryset in the ModelMultipleChoiceField
Form Initialization: Make sure to handle the initial values correctly in the
__init__
methodSaving: Don't forget to implement the
save
method to handle the M2M relationship properly
Attention: Django 5.1+
Since we override the form class, we might want to bring back this beauty
Complete Code: https://gist.github.com/Kenan7/0a5169242f13b95acd362e5d7a841607
In Conclusion
This solution provides a clean, symmetrical interface for managing ManyToMany relationships in Django admin. While it requires a bit more code than the default setup, the improved user experience makes it worth the effort.
Remember to adapt the code to your specific needs and consider adding any necessary validation or customization to match your project's requirements.
Subscribe to my newsletter
Read articles from Mirkenan Kazımzade directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by