
本文详解如何在 django formset 中正确禁用只读字段(如外键下拉框),避免因 `disabled` 属性导致 post 数据丢失,同时防止恶意篡改——关键在于使用 `form.fields['field'].disabled = true` 而非 html `disabled` 属性。
在 Django 表单集中展示只读字段(例如 department)时,一个常见误区是直接在 Meta.widgets 中添加 {'disabled': 'True'}。这种写法仅在前端渲染时禁用输入控件,但会导致两个严重问题:
- POST 数据丢失:浏览器不会提交 disabled 字段的值,Django 表单校验时将其视为空,若该字段为必填(blank=False, null=False),则验证失败;
- 安全性风险:disabled 属性可被前端轻易绕过(如通过开发者工具移除属性),攻击者可伪造请求篡改本应只读的字段。
✅ 正确做法是:在表单初始化阶段,通过 Python 代码将字段设为 disabled=True。这会同时实现:
- 前端渲染为灰色不可编辑状态;
- 后端自动忽略该字段的提交值(不参与验证、不写入数据库);
- 即使客户端篡改 POST 数据,Django 也会静默丢弃该字段——真正保障数据一致性与安全性。
✅ 正确实现方式
修改你的 OrderCloseForm,移除 widgets 中的 disabled,改用 __init__ 动态设置:
class OrderCloseForm(forms.ModelForm):
class Meta:
model = Order
fields = (
'type_car',
'department', # 保持在此处(需参与模型绑定)
# ... 其他字段
)
widgets = {
'car': forms.Select(attrs={'style': 'width: 100%'}),
'department': forms.Select(attrs={'style': 'width: 100%'}), # ← 移除 'disabled'
# ... 其他 widget 配置
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# ✅ 关键:服务端禁用,安全且可靠
self.fields['department'].disabled = True⚠️ 注意:self.fields['department'].disabled = True 会自动保留原字段值(如数据库中已有的 department_id),并在保存时跳过该字段——无需手动处理 initial 或 instance。
✅ 视图层优化:简化逻辑 + PRG 模式
当前视图存在重复实例化 formset、未处理重定向等问题。推荐重构如下:
def orders_list(request, year, month, day):
orders = Order.objects.filter(
order_date__year=year,
order_date__month=month,
order_date__day=day
)
if request.method == 'POST':
formset = OrderCloseFormSet(
request.POST,
request.FILES,
queryset=orders,
prefix='order'
)
if formset.is_valid():
formset.save() # ✅ 直接 save(),等价于 save(commit=True)
return redirect('orders:orders_list', year=year, month=month, day=day) # PRG 模式
else:
formset = OrderCloseFormSet(queryset=orders, prefix='order')
context = {'orders': orders, 'formset': formset}
return render(request, 'orders/orders_list.html', context)- formset.save() 自动处理所有有效表单的 save() 调用,无需手动遍历 form.save();
- else 分支确保 GET 请求只创建一次 formset,避免覆盖 POST 失败后的错误表单;
- redirect() 实现 Post/Redirect/Get(PRG)模式,防止用户刷新导致重复提交。
❌ 模板中无需 JavaScript hack
你原先尝试用 jQuery 移除 disabled 属性再提交,不仅冗余,而且违背了 Django 的设计哲学——表单逻辑应在服务端定义和控制。删除以下无效脚本:
Django 表单已通过 disabled=True 安全地处理了值的保留与跳过,前端无需干预。
✅ 总结:最佳实践清单
| 项目 | 推荐做法 | 错误做法 |
|---|---|---|
| 禁用字段 | self.fields['xxx'].disabled = True(Python 层) | widgets={'xxx': ... 'disabled': 'True'}(HTML 层) |
| 保存逻辑 | formset.save() 一行调用 | 手动 save(commit=False) + 循环 form.save() |
| POST 成功后 | return redirect(...)(PRG) | 直接 render()(易导致重复提交) |
| 前端干预 | 完全不需要 JS 操作 disabled 状态 | 用 jQuery 移除 disabled 并提交 |
遵循以上方案,即可构建出既用户友好(清晰显示只读信息)、又安全可靠(服务端强制约束)、且符合 Django 最佳实践的表单集管理界面。










