Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.enter image description here

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)
like image 947
Dylan Klomparens Avatar asked Jul 25 '12 20:07

Dylan Klomparens


3 Answers

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.

like image 87
Chris Pratt Avatar answered Oct 23 '22 09:10

Chris Pratt


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"""
like image 29
Shishir Biyyala Avatar answered Oct 23 '22 09:10

Shishir Biyyala


There is an easier solution, just add filter_horizontal, as explained here:

class YourAdmin(ModelAdmin)
    filter_horizontal = ('your_many_to_many_field',)

Before:enter image description here

After:enter image description here

like image 2
tarasinf Avatar answered Oct 23 '22 08:10

tarasinf