Django Admin: Many-to-Many listbox doesn't show up with a through parameter

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.