Django admin interface: using horizontal_filter with inline ManyToMany field
I have a Django model field that I'd like to inline. The field is a many-to-many relationship. So there are "Projects" and "User profiles". Each user profile can select any number of projects.
Currently, I've got the "tabular" inline view working. Is there a way to have a "horizontal filter" so that I can easily add and remove projects from a user profile?
Please see the attached picture for an example.
Here's the model code for the User Profile:
class UserProfile(models.Model): user = models.OneToOneField(User, unique=True) projects = models.ManyToManyField(Project, blank=True, help_text="Select the projects that this user is currently working on.")
And the model code for a Project:
class Project(models.Model): name = models.CharField(max_length=100, unique=True) application_identifier = models.CharField(max_length=100) type = models.IntegerField(choices=ProjectType) account = models.ForeignKey(Account) principle_investigator = models.ForeignKey(User) active = models.BooleanField()
And the admin code for the view:
class UserProfileInline(admin.TabularInline): model = UserProfile.projects.through extra = 0 verbose_name = 'user' verbose_name_plural = 'users' class ProjectAdmin(admin.ModelAdmin): list_display = ('name', 'application_identifier', 'type', 'account', 'active') search_fields = ('name', 'application_identifier', 'account__name') list_filter = ('type', 'active') inlines = [UserProfileInline,] admin.site.register(Project, ProjectAdmin)
The problem isn't from having inlines; it's from the way ModelForms 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"""