Django admin Inline 动态设置 ForeignKey 默认值
结论
可以实现,但要分清”界面默认值”和”服务端强制赋值”:
- 在
Customer的PaymentInline中,建议隐藏added_by字段,并在保存新建的Payment时将它设为当前登录的Agent。 - 普通新增页面可以通过 URL 参数设置初始值,但这些参数并不可信。如果字段不允许编辑,还要禁用字段,并在服务端重新校验和赋值。
- 模型中的字段名是
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 时,业务上是否允许连带删除付款记录。
备注:内容仅供参考。
