I have the following models:
class Message(models.Model):
date = models.DateTimeField()
user = models.ForeignKey(User)
thread = models.ForeignKey
Django admin nicely support many-to-many intermediary models that using the through
argument .
For example you have these Person
and Group
models with intermediate Membership
model:
models.py
from django.db import models
class Person(models.Model):
name = models.CharField(max_length=128)
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Membership')
class Membership(models.Model):
person = models.ForeignKey(Person, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
Now in admin.py
file ,
Define an inline class for the intermediate Membership
model:
@admin.register(Membership)
class MembershipInline(admin.TabularInline):
model = Membership
extra = 1
And use them in admin views of models:
@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
inlines = (MembershipInline,)
@admin.register(Group)
class GroupAdmin(admin.ModelAdmin):
inlines = (MembershipInline,)
More info in official docs:
Models, Admin
Documentation says:
When you specify an intermediary model using the through argument to a ManyToManyField, the admin will not display a widget by default.
But it's probably possible to display M2M fields in the admin change view even if the through
attribute is defined.
class ForumAdminForm(forms.ModelForm):
mm = forms.ModelMultipleChoiceField(
queryset=models.Message.objects.all(),
widget=FilteredSelectMultiple(_('ss'), False, attrs={'rows':'10'}))
def __init__(self, *args, **kwargs):
if 'instance' in kwargs:
initial = kwargs.setdefault('initial', {})
initial['mm'] = [t.service.pk for t in kwargs['instance'].message_forum_set.all()]
forms.ModelForm.__init__(self, *args, **kwargs)
def save(self, commit=True):
instance = forms.ModelForm.save(self, commit)
old_save_m2m = self.save_m2m
def save_m2m():
old_save_m2m()
messages = [s for s in self.cleaned_data['ss']]
for mf in instance.message_forum_set.all():
if mf.service not in messages:
mf.delete()
else:
messages.remove(mf.service)
for message in messages:
Message_forum.objects.create(message=message, forum=instance)
self.save_m2m = save_m2m
return instance
class Meta:
model = models.Forum
class ForumAdmin(admin.ModelAdmin):
form = ForumAdminForm
Take a look at the official documentation:
I learned a lot from @Fedor's answer, but some comments and cleanup may be still beneficial.
class ForumAdminForm(forms.ModelForm):
messages = forms.ModelMultipleChoiceField(
queryset=Message.objects.all(),
widget=FilteredSelectMultiple('Message', False))
# Technically, you don't need to manually set initial here for ForumAdminForm
# However, you NEED to do the following for MessageAdminForm
def __init__(self, *args, **kwargs):
if 'instance' in kwargs:
# a record is being changed. building initial
initial = kwargs.setdefault('initial', {})
initial['messages'] = [t.message.pk for t in kwargs['instance'].message_forum_set.all()]
super(ForumAdminForm, self).__init__(*args, **kwargs)
def save(self, commit=True):
if not self.is_valid():
raise HttpResponseForbidden
instance = super(ForumAdminForm, self).save(self, commit)
def save_m2m_with_through():
messages = [t for t in self.cleaned_data['messages']
old_memberships = instance.message_forum_set.all()
for old in old_memberships:
if old.message not in messages:
# and old membership is cleaned by the user
old.delete()
for message in [x for x in messages not in map(lambda x: x.message, old_memberships)]:
membership = Member_forum(message=messsage, forum=instance)
# You may have to initialize status, position and tag for your need
membership.save()
if commit:
save_m2m_with_through()
else:
self.save_m2m = save_m2m_with_through
return instance
class Meta:
model = Forum
fields = {'name', 'messages')
There's one caveat: if you have another many-to-many relationship in the models (that is without through), super(ForumAdminForm, self).save(self, commit)
will set self.save_m2m in case commit
is False. However, calling this would cause an error, because this function also tries to save the many-to-many with through as well. You may need to save all other many-to-many relationship manually, or catch the exception, or else.