Reputation: 60649
Let's say I have a simple blog app in Django 1.4:
class Post(models.Model):
title = …
published_on = …
tags = models.ManyToManyField('Tag')
class Tag(models.Model):
name = …
i.e. a post has many tags. On the Django admin, I get a nice little <select multi>
if I include tags
in the fields
for the PostAdmin
. Is there an easy way to include the list of the posts (as a simple <select multi>
) in the TagAdmin
? I tried putting fields = ['name', 'posts']
in the TagAdmin
and got an ImproperlyConfigured
error. (same result for post_set
).
I'm alright with Django, so could whip up a proper AdminForm and Admin object, but I'm hoping there a Right Way™ to do it.
Upvotes: 37
Views: 8668
Reputation: 8890
You can add a symmetrical many to many filter this way.
Credit goes to https://gist.github.com/Grokzen/a64321dd69339c42a184
from django.db import models
class Pizza(models.Model):
name = models.CharField(max_length=50)
toppings = models.ManyToManyField(Topping, related_name='pizzas')
class Topping(models.Model):
name = models.CharField(max_length=50)
### pizza/admin.py ###
from django import forms
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from django.contrib.admin.widgets import FilteredSelectMultiple
from .models import Pizza, Topping
class PizzaAdmin(admin.ModelAdmin):
filter_horizonal = ('toppings',)
class ToppingAdminForm(forms.ModelForm):
pizzas = forms.ModelMultipleChoiceField(
queryset=Pizza.objects.all(),
required=False,
widget=FilteredSelectMultiple(
verbose_name=_('Pizzas'),
is_stacked=False
)
)
class Meta:
model = Topping
def __init__(self, *args, **kwargs):
super(ToppingAdminForm, self).__init__(*args, **kwargs)
if self.instance and self.instance.pk:
self.fields['pizzas'].initial = self.instance.pizzas.all()
def save(self, commit=True):
topping = super(ToppingAdminForm, self).save(commit=False)
if commit:
topping.save()
if topping.pk:
topping.pizzas = self.cleaned_data['pizzas']
self.save_m2m()
return topping
class ToppingAdmin(admin.ModelAdmin):
form = ToppingAdminForm
admin.site.register(Pizza, PizzaAdmin)
admin.site.register(Topping, ToppingAdmin)
Upvotes: 4
Reputation: 1215
Modify your models to add reverse field:
# models.py
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=100)
published_on = models.DateTimeField()
tags = models.ManyToManyField('Tag')
class Tag(models.Model):
name = models.CharField(max_length=10)
posts = models.ManyToManyField('blog.Post', through='blog.post_tags')
Then in standard way add field to ModelAdmin:
#admin.py
from django.contrib import admin
class TagAdmin(admin.ModelAdmin):
list_filter = ('posts', )
admin.site.register(Tag, TagAdmin)
Upvotes: 8
Reputation: 13479
Matthew's solution didn't work for me (Django 1.7) when creating a new entry, so I had to change it a bit. I hope it's useful for someone :)
class PortfolioCategoriesForm(forms.ModelForm):
items = forms.ModelMultipleChoiceField(
PortfolioItem.objects.all(),
widget=admin.widgets.FilteredSelectMultiple('Portfolio items', False),
required=False
)
def __init__(self, *args, **kwargs):
super(PortfolioCategoriesForm, self).__init__(*args, **kwargs)
if self.instance.pk:
initial_items = self.instance.items.values_list('pk', flat=True)
self.initial['items'] = initial_items
def save(self, *args, **kwargs):
kwargs['commit'] = True
return super(PortfolioCategoriesForm, self).save(*args, **kwargs)
def save_m2m(self):
self.instance.items.clear()
self.instance.items.add(*self.cleaned_data['items'])
Upvotes: 4
Reputation: 35599
This is possible to do with a custom form.
from django.contrib import admin
from django import forms
from models import Post, Tag
class PostAdminForm(forms.ModelForm):
tags = forms.ModelMultipleChoiceField(
Tag.objects.all(),
widget=admin.widgets.FilteredSelectMultiple('Tags', False),
required=False,
)
def __init__(self, *args, **kwargs):
super(PostAdminForm, self).__init__(*args, **kwargs)
if self.instance.pk:
self.initial['tags'] = self.instance.tags.values_list('pk', flat=True)
def save(self, *args, **kwargs):
instance = super(PostAdminForm, self).save(*args, **kwargs)
if instance.pk:
instance.tags.clear()
instance.tags.add(*self.cleaned_data['tags'])
return instance
class PostAdmin(admin.ModelAdmin):
form = PostAdminForm
admin.site.register(Post, PostAdmin)
That False
in there can be replaced with a True
if you want vertically stacked widget.
Upvotes: 33
Reputation: 6013
A bit late to the party, but this is the solution that works for me (no magic):
# admin.py
from django.contrib import admin
from models import Post
class TagPostInline(admin.TabularInline):
model = Post.tags.through
extra = 1
class PostAdmin(admin.ModelAdmin):
inlines = [TagPostInline]
admin.site.register(Post, PostAdmin)
Reference: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#working-with-many-to-many-models
Upvotes: 14