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

前端 未结 4 1305
灰色年华
灰色年华 2021-02-01 08:42

I have the following models:

class Message(models.Model):
    date = models.DateTimeField()
    user = models.ForeignKey(User)    
    thread = models.ForeignKey         


        
相关标签:
4条回答
  • 2021-02-01 09:01

    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

    0 讨论(0)
  • 2021-02-01 09:09

    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
    
    0 讨论(0)
  • 2021-02-01 09:11

    Take a look at the official documentation:

    0 讨论(0)
  • 2021-02-01 09:20

    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.

    0 讨论(0)
提交回复
热议问题