Django admin interface: using horizontal_filter with inline ManyToMany field
There is an easier solution, just add filter_horizontal
, as explained here:
class YourAdmin(ModelAdmin)
filter_horizontal = ('your_many_to_many_field',)
Before:
After:
The problem isn't from having inlines; it's from the way ModelForm
s work, in general. They only build form fields for actual fields on the model, not related manager attributes. However, you can add this functionality to the form:
from django.contrib.admin.widgets import FilteredSelectMultiple
class ProjectAdminForm(forms.ModelForm):
class Meta:
model = Project
userprofiles = forms.ModelMultipleChoiceField(
queryset=UserProfile.objects.all(),
required=False,
widget=FilteredSelectMultiple(
verbose_name='User Profiles',
is_stacked=False
)
)
def __init__(self, *args, **kwargs):
super(ProjectAdminForm, self).__init__(*args, **kwargs)
if self.instance.pk:
self.fields['userprofiles'].initial = self.instance.userprofile_set.all()
def save(self, commit=True):
project = super(ProjectAdminForm, self).save(commit=False)
if commit:
project.save()
if project.pk:
project.userprofile_set = self.cleaned_data['userprofiles']
self.save_m2m()
return project
class ProjectAdmin(admin.ModelAdmin):
form = ProjectAdminForm
...
A little walkthrough is probably in order. First, we define a userprofiles
form field. It will use a ModelMultipleChoiceField
, which by default will result in a multiple select box. Since this isn't an actual field on the model, we can't just add it to filter_horizontal
, so we instead tell it to simply use the same widget, FilteredSelectMultiple
, that it would use if it were listed in filter_horizontal
.
We initially set the queryset as the entire UserProfile
set, you can't filter it here, yet, because at this stage of the class definition, the form hasn't been instantiated and thus doesn't have it's instance
set yet. As a result, we override __init__
so that we can set the filtered queryset as the field's initial value.
Finally, we override the save
method, so that we can set the related manager's contents to the same as what was in the form's POST data, and you're done.
A minor addition when dealing with a many to many relationship with itself. One might want to exclude itself from the choices:
if self.instance.pk:
self.fields['field_being_added'].queryset = self.fields['field_being_added'].queryset.exclude(pk=self.instance.pk)
self.fields['field_being_added'].initial = """Corresponding result queryset"""