You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Django多对多through模型,如何创建含双外键的Inline Formset

问题描述

我有继承自AbstractUser的User模型和Relationship模型,User模型的friends字段是通过Relationship实现的自关联非对称多对多字段。

模型代码

class User(AbstractUser):
    username = models.CharField(
        verbose_name="username", max_length=256, unique=True)
    first_name = models.CharField(verbose_name="first name", max_length=30)
    last_name = models.CharField(verbose_name="last name", max_length=30)
    slug = models.SlugField(max_length=255, unique=True, blank=True)
    date_joined = models.DateTimeField(
        verbose_name="joined date", auto_now_add=True)
    friends = models.ManyToManyField('self', through='Relationship', symmetrical=False,
                                     related_name='following')
    is_active = models.BooleanField(default=True)
    is_superuser = models.BooleanField(default=False)
    is_staff = models.BooleanField(default=False)

    def __str__(self):
        return str(self.username)

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.username)
        super().save(*args, **kwargs)


class Relationship(models.Model):
    from_person = models.ForeignKey(
        User, related_name='from_people', on_delete=models.CASCADE)
    to_person = models.ForeignKey(
        User, related_name='to_people', on_delete=models.CASCADE)

    def __str__(self):
        return f"from {self.from_person} to {self.to_person}"

我想创建一个包含from_person和to_person字段的inline formset,用来为用户建立关联,但尝试实现时,因为fk_name的限制没法同时显示这两个字段,求解决办法。

尝试的视图代码

def staff_profile_view(request):
    user = User.objects.get_by_natural_key(request.user)
    AssignFriendsFormSet = inlineformset_factory(
        User, Relationship, fk_name='from_person', fields=('to_person', 'from_person',))

    formset = AssignFriendsFormSet(
        queryset=Relationship.objects.none(), instance=user)

    if request.method == "POST":
        formset = AssignFriendsFormSet(request.POST, instance=user)
        if formset.is_valid():
            formset.save()

    return render(request, 'staff_profile.html',{"formset": formset})

解决方案

问题核心是inlineformset_factory的默认逻辑:指定fk_name后,该外键会自动绑定到传入的instance对象,默认不会在表单中展示。要同时显示两个字段,有两种可行方案:

方案一:自定义Inline表单,手动添加from_person字段

  1. 先定义自定义的Relationship表单,手动加入from_person字段:
from django import forms
from .models import Relationship, User

class RelationshipForm(forms.ModelForm):
    from_person = forms.ModelChoiceField(queryset=User.objects.all(), required=True)
    
    class Meta:
        model = Relationship
        fields = ('from_person', 'to_person')
  1. 在视图中使用这个自定义表单创建inline formset:
def staff_profile_view(request):
    user = User.objects.get_by_natural_key(request.user)
    # 关联自定义表单,同时保留fk_name指定
    AssignFriendsFormSet = inlineformset_factory(
        User, Relationship, form=RelationshipForm,
        fk_name='from_person', fields=('from_person', 'to_person')
    )

    if request.method == "POST":
        formset = AssignFriendsFormSet(request.POST, instance=user)
        if formset.is_valid():
            formset.save()
            return redirect('staff_profile')  # 保存后重定向刷新页面
    else:
        # 可选:给新表单默认填充当前用户为from_person
        formset = AssignFriendsFormSet(
            queryset=Relationship.objects.none(), 
            instance=user,
            initial=[{'from_person': user}]
        )

    return render(request, 'staff_profile.html', {"formset": formset})

如果需要限制from_person只能是当前用户,可以在表单中添加校验逻辑:

class RelationshipForm(forms.ModelForm):
    from_person = forms.ModelChoiceField(queryset=User.objects.all(), required=True)
    
    class Meta:
        model = Relationship
        fields = ('from_person', 'to_person')
    
    def clean_from_person(self):
        from_person = self.cleaned_data.get('from_person')
        if from_person != self.instance:
            raise forms.ValidationError("只能选择当前用户作为关联发起者")
        return from_person

方案二:使用ModelFormSet替代InlineFormSet

如果不需要和User实例强绑定的inline逻辑,直接用ModelFormSet更灵活:

from django.forms import modelformset_factory

def staff_profile_view(request):
    RelationshipFormSet = modelformset_factory(
        Relationship, fields=('from_person', 'to_person'), extra=1
    )

    if request.method == "POST":
        formset = RelationshipFormSet(request.POST)
        if formset.is_valid():
            formset.save()
            return redirect('staff_profile')
    else:
        formset = RelationshipFormSet(queryset=Relationship.objects.none())

    return render(request, 'staff_profile.html', {"formset": formset})

内容的提问来源于stack exchange,提问作者Menna Magdy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 15:35:40