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 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"""
There is an easier solution, just add filter_horizontal
, as explained here:
class YourAdmin(ModelAdmin)
filter_horizontal = ('your_many_to_many_field',)
Before:
After:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With