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

基于用户权限的Django InlineFormSet自定义验证问题

订单及关联订单行预算审批表单的权限控制与验证修复方案

问题复盘

需要实现订单+关联订单行的预算审批流程:

  • 表单展示全部订单行,但仅允许当前用户编辑有权限的行(支持新增行)
  • budgetholder_approved审批单选框为必填项
    遇到的核心问题:
  1. 验证失败时错误字段高亮但值被清空
  2. 尝试重写ModelForm.clean()无法判断字段是否被禁用;重写BaseInlineFormSet.clean()时,__init__里没法禁用字段,且验证失败直接返回空表单集
  3. 优化后验证失败能重载显示错误,但本该启用的budgetholder_approved字段被错误禁用

解决方案

1. 自定义表单+表单集,结合权限控制字段状态

不要在表单集__init__里直接禁用字段,而是在单个表单的__init__中判断权限,同时处理禁用字段的提交值保留:

from django.forms import BaseInlineFormSet, ModelForm
from .models import OrderLine

class OrderLineApprovalForm(ModelForm):
    class Meta:
        model = OrderLine
        fields = ['description', 'amount', 'budgetholder_approved']

    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop('user')
        super().__init__(*args, **kwargs)
        # 根据权限设置字段禁用状态
        if self.instance.pk and not self.instance.has_approval_perm(self.user):
            self.fields['budgetholder_approved'].widget.attrs['disabled'] = True
            # 给禁用字段设置初始值,确保验证失败后不会被清空
            self.initial['budgetholder_approved'] = self.instance.budgetholder_approved

class OrderLineApprovalFormSet(BaseInlineFormSet):
    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop('user')
        super().__init__(*args, **kwargs)
        # 给每个子表单传入当前用户
        for form in self.forms:
            form.user = self.user

    def clean(self):
        super().clean()
        # 遍历表单,跳过无权限行的必填验证
        for form in self.forms:
            if form.instance.pk and not form.instance.has_approval_perm(self.user):
                # 移除无权限行的必填错误
                if 'budgetholder_approved' in form.errors:
                    del form.errors['budgetholder_approved']
                # 强制设置cleaned_data为实例原有值,避免验证不通过
                form.cleaned_data['budgetholder_approved'] = form.instance.budgetholder_approved
            # 新增行必须验证必填(默认用户有权限新增)
            elif not form.instance.pk:
                if form.cleaned_data.get('budgetholder_approved') is None:
                    form.add_error('budgetholder_approved', '此字段不能为空')

2. 视图层正确处理表单提交与重载

在视图中初始化表单集时必须传入当前用户,验证失败时要带上POST数据重新初始化,确保权限判断正确并保留已输入值:

from django.shortcuts import render, redirect
from .forms import OrderLineApprovalFormSet
from .models import Order

def order_approval(request, order_id):
    order = Order.objects.get(pk=order_id)
    if request.method == 'POST':
        # 提交时传入用户和POST数据
        formset = OrderLineApprovalFormSet(request.POST, instance=order, user=request.user)
        if formset.is_valid():
            formset.save()
            return redirect('order_list')
    else:
        formset = OrderLineApprovalFormSet(instance=order, user=request.user)
    return render(request, 'order_approval.html', {'formset': formset})

3. 模板层加固权限控制,避免数据丢失

模板中再次验证权限,同时给禁用字段添加隐藏输入,确保提交时能携带原有值:

{% for form in formset %}
    <div class="order-line-item">
        {{ form.id }}
        <div>{{ form.description }}</div>
        <div>{{ form.amount }}</div>
        <div class="approval-field">
            {% if form.instance.pk and form.instance.has_approval_perm user %}
                {{ form.budgetholder_approved }}
            {% else %}
                {{ form.budgetholder_approved }}
                <!-- 隐藏字段提交原有值,避免禁用字段不被浏览器提交 -->
                <input type="hidden" 
                       name="{{ form.prefix }}-budgetholder_approved" 
                       value="{{ form.instance.budgetholder_approved|default:'' }}">
            {% endif %}
            <!-- 显示错误信息 -->
            {% if form.errors.budgetholder_approved %}
                <span class="text-red-500">{{ form.errors.budgetholder_approved.0 }}</span>
            {% endif %}
        </div>
    </div>
{% endfor %}
{{ formset.management_form }}

核心要点说明

  • 禁用字段不会被浏览器提交,必须通过隐藏字段或表单初始值保留数据,避免验证失败后值被清空
  • 表单集的clean方法需要跳过无权限行的必填验证,强制设置cleaned_data确保表单集能通过验证
  • 视图层必须在验证失败时重新传入用户和POST数据,保证表单能正确判断权限并渲染已输入的内容

内容的提问来源于stack exchange,提问作者dj.bettega

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 03:50:23