基于用户权限的Django InlineFormSet自定义验证问题
订单及关联订单行预算审批表单的权限控制与验证修复方案
问题复盘
需要实现订单+关联订单行的预算审批流程:
- 表单展示全部订单行,但仅允许当前用户编辑有权限的行(支持新增行)
budgetholder_approved审批单选框为必填项
遇到的核心问题:
- 验证失败时错误字段高亮但值被清空
- 尝试重写
ModelForm.clean()无法判断字段是否被禁用;重写BaseInlineFormSet.clean()时,__init__里没法禁用字段,且验证失败直接返回空表单集 - 优化后验证失败能重载显示错误,但本该启用的
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
相关产品推荐
相关产品推荐

