PAYATHON 2026

Django admin Inline 动态设置 ForeignKey 默认值

支付老李

结论

可以实现,但要分清”界面默认值”和”服务端强制赋值”:

  1. 在 Customer 的 PaymentInline 中,建议隐藏 added_by 字段,并在保存新建的 Payment 时将它设为当前登录的 Agent。
  2. 普通新增页面可以通过 URL 参数设置初始值,但这些参数并不可信。如果字段不允许编辑,还要禁用字段,并在服务端重新校验和赋值。
  3. 模型中的字段名是 added_by,不是 agent。代码和 URL 参数都应使用实际字段名,除非你另外实现参数映射。

在 Inline 中设置当前登录的 Agent

customer 在 inline 中会自动隐藏,因为它是连接 PaymentInline 和当前 Customer 的外键。Django 的 inline formset 会自动设置这个字段。

added_by 与父对象没有这种关联,需要单独处理。比较可靠的做法是隐藏该字段,并在 save_formset() 中为新记录赋值:

from django.contrib import admin

from .models import Agent, Customer, Payment


def get_current_agent(request):
    if isinstance(request.user, Agent):
        return request.user

    # Agent 继承 User 时通常与 User 使用相同的主键。
    # 如果项目使用了其他用户模型设计,应按实际关系调整。
    return Agent.objects.get(pk=request.user.pk)


class PaymentInline(admin.TabularInline):
    model = Payment
    extra = 0
    exclude = ("added_by",)


@admin.register(Customer)
class CustomerAdmin(admin.ModelAdmin):
    inlines = (PaymentInline,)

    def save_formset(self, request, form, formset, change):
        if formset.model is not Payment:
            return super().save_formset(
                request, form, formset, change
            )

        agent = get_current_agent(request)
        instances = formset.save(commit=False)

        for deleted_object in formset.deleted_objects:
            deleted_object.delete()

        for payment in instances:
            # 只为新建记录设置创建人,避免编辑旧记录时覆盖原值。
            if payment.pk is None:
                payment.added_by = agent

            payment.save()

        formset.save_m2m()

如果 CustomerAdmin 只有这一个 inline,可以省略 formset.model is not Payment 这项判断。

不要对所有 payment 都直接执行:

payment.added_by = agent

这样做会导致管理员编辑已有付款记录时,原来的 added_by 被当前操作人覆盖。

只需要显示默认值时

如果允许用户修改 added_by,可以通过 formfield_for_foreignkey() 设置初始值:

class PaymentInline(admin.TabularInline):
    model = Payment
    extra = 0

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == "added_by":
            kwargs["initial"] = get_current_agent(request).pk

        return super().formfield_for_foreignkey(
            db_field, request, **kwargs
        )

这种方式只会设置表单的初始值。用户仍可修改该字段,所以它不能用于权限控制。如果 added_by 记录的是真实创建人,更合适的处理方式通常是隐藏字段,并在保存时强制赋值。

通过 URL 设置普通新增页面的初始值

Django admin 可以根据查询参数设置新增表单的初始值。参数通常使用模型字段名:

/admin/my_app/payment/add/?customer=1&added_by=1

不要使用数据库列名:

?customer_id=1&agent_id=1

查询参数只能提供初始值,字段仍然可以编辑,而且用户可以修改 URL。因此,最终保存的数据不能只由这些参数决定。

如果需要兼容题目中的 customer_id 和 agent_id,同时禁止用户修改这两个字段,可以自定义 PaymentAdmin:

from django.contrib import admin
from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.shortcuts import get_object_or_404

from .models import Agent, Customer, Payment


@admin.register(Payment)
class PaymentAdmin(admin.ModelAdmin):
    fields = ("customer", "added_by", "date", "amount")

    def get_fixed_relations(self, request):
        customer_id = request.GET.get("customer_id")
        agent_id = request.GET.get("agent_id")

        # 没有参数时保留普通新增页面的行为。
        if customer_id is None and agent_id is None:
            return None

        # 固定赋值时要求两个参数同时存在。
        if not customer_id or not agent_id:
            raise SuspiciousOperation(
                "customer_id 和 agent_id 必须同时提供"
            )

        customer = get_object_or_404(Customer, pk=customer_id)
        agent = get_object_or_404(Agent, pk=agent_id)

        current_agent = get_current_agent(request)

        # 示例策略:普通用户只能把 added_by 设置成自己。
        # 如果业务允许代其他 Agent 录入,应替换成对应的权限判断。
        if agent.pk != current_agent.pk and not request.user.is_superuser:
            raise PermissionDenied

        # 还可以在此检查当前用户是否有权访问该 Customer。
        return customer, agent

    def get_form(self, request, obj=None, change=False, **kwargs):
        form_class = super().get_form(
            request, obj=obj, change=change, **kwargs
        )

        # 编辑已有记录时不使用新增链接中的固定值。
        if obj is not None:
            return form_class

        relations = self.get_fixed_relations(request)
        if relations is None:
            return form_class

        customer, agent = relations

        class FixedPaymentForm(form_class):
            def __init__(self, *args, **form_kwargs):
                initial = form_kwargs.setdefault("initial", {})
                initial["customer"] = customer.pk
                initial["added_by"] = agent.pk

                super().__init__(*args, **form_kwargs)

                self.fields["customer"].disabled = True
                self.fields["added_by"].disabled = True

        return FixedPaymentForm

    def save_model(self, request, obj, form, change):
        if not change:
            relations = self.get_fixed_relations(request)

            if relations is not None:
                customer, agent = relations

                # 服务端再次赋值,不依赖浏览器提交的数据。
                obj.customer = customer
                obj.added_by = agent

        super().save_model(request, obj, form, change)

对应的链接是:

http://localhost:8000/admin/my_app/payment/add/?customer_id=1&agent_id=1

这里的 disabled = True 并非只是在 HTML 中添加 disabled 属性。Django 表单清理数据时,也会忽略客户端为这些字段提交的篡改值。save_model() 会在服务端再次赋值,作为第二层保障。

如果 added_by 必须始终等于当前登录用户,就不必通过 URL 接收 agent_id。链接可以简化为:

/admin/my_app/payment/add/?customer_id=1

然后直接在服务端设置:

obj.added_by = get_current_agent(request)

完全隐藏两个字段

如果页面不需要展示固定值,可以将这两个字段从表单中排除:

@admin.register(Payment)
class PaymentAdmin(admin.ModelAdmin):
    exclude = ("customer", "added_by")

    def save_model(self, request, obj, form, change):
        if not change:
            customer_id = request.GET.get("customer_id")

            if not customer_id:
                raise SuspiciousOperation("缺少 customer_id")

            obj.customer = get_object_or_404(
                Customer,
                pk=customer_id,
            )
            obj.added_by = get_current_agent(request)

        super().save_model(request, obj, form, change)

这种写法适用于 customer 来自链接,而 added_by 始终是当前用户的场景。服务端仍需检查当前用户是否有权为指定的 Customer 创建付款记录。

其他注意事项

DateField 的默认值应传入函数本身,不要调用函数:

date = models.DateField(default=datetime.date.today)

如果写成:

date = models.DateField(default=datetime.date.today())

日期会在模块加载时计算,之后创建的记录可能一直使用同一个旧日期。

在要求显式声明删除行为的 Django 版本中,外键还需要设置 on_delete:

class Payment(models.Model):
    added_by = models.ForeignKey(
        Agent,
        on_delete=models.PROTECT,
    )
    customer = models.ForeignKey(
        Customer,
        on_delete=models.PROTECT,
    )
    date = models.DateField(default=datetime.date.today)
    amount = models.IntegerField(default=0)

PROTECT 是常见选择,具体设置要看删除 Agent 或 Customer 时,业务上是否允许连带删除付款记录。

备注:内容仅供参考。