17370845950

如何在 Django Formset 中正确禁用字段以实现只读显示但保留提交值

本文介绍在 django 表单集(formset)中安全禁用非编辑字段的正确方法:使用 `form.fields['field'].disabled = true` 替代 html `disabled` 属性,确保字段既不可编辑、又参与验证与保存,避免因前端禁用导致数据丢失或 csrf 绕过风险。

在 Django 开发中,常需在表格界面中混合展示“可编辑字段”与“只读字段”(如 department 字段仅用于展示、不应被用户修改)。初学者易误用 HTML 的 disabled="True" 属性(如 widgets={'department': Select(attrs={'disabled': 'True'})}),但这会导致严重问题:

  • ✅ 前端视觉上禁用,用户无法修改;
  • ❌ 但 disabled 字段不会随 POST 请求提交,Django 表单接收不到该字段值,校验时会报 This field is required 错误(即使数据库中已有值);
  • ❌ 更危险的是,disabled 纯属前端限制,恶意用户可轻易移除属性并伪造请求篡改数据,存在安全漏洞。

✅ 正确做法:在表单类中设置 field.disabled = True

应将禁用逻辑移至 Python 层——在 ModelForm.__init__() 中显式设置字段 disabled=True。这不仅禁用前端交互,更关键的是:

  • 字段仍会包含在 POST 数据中(作为隐藏输入自动渲染);
  • Django 表单跳过对该字段的验证(不检查是否为空/格式等);
  • 保存时保留原始数据库值,且无法被 POST 数据覆盖;
  • 从根本上防止客户端篡改,保障数据一致性与安全性。
# forms.py
class OrderCloseForm(forms.ModelForm):
    class Meta:
        model = Order
        fields = ('type_car', 'department', 'car', ...)  # 明确列出所有需呈现的字段
        widgets = {
            'car': forms.Select(attrs={'style': 'width: 100%'}),
            'department': forms.Select(attrs={'style': 'width: 100%'}),  # 移除 disabled 属性!
            # 其他字段...
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # ✅ 关键:服务端禁用,安全且可靠
        self.fields['department'].disabled = True
        # 如需禁用多个字段,可链式设置:
        # self.fields['car'].disabled = True
        # self.fields['order_date'].disabled = True

✅ 视图层优化:简化逻辑 + 遵循 PRG 模式

原视图中存在重复初始化 formset、未处理 request.FILES、缺少重定向等问题。修正后如下:

# views.py
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)
            # ✅ PRG 模式:成功后重定向,防止重复提交
            return redirect('orders:orders_list', year=year, month=month, day=day)

    else:
        # ✅ GET 请求时才初始化空 formset
        formset = OrderCloseFormSet(queryset=orders, prefix='order')

    context = {'orders': orders, 'formset': formset}
    return render(request, 'orders/orders_list.html', context)

? 模板注意事项

  • 无需手动处理 disabled 字段的隐藏输入;Django 会自动为 disabled=True 字段生成 保留原始值;
  • 确保模板中正常渲染所有字段(包括被禁用的):
    {% for form in formset %}
      
        {% for field in form.visible_fields %}
          {{ field|addclass:'input-box input-select' }}
        {% endfor %}
      
    {% endfor %}
  • 移除所有前端 JavaScript “移除 disabled”的hack(如 $('id_order-0-department').submit(...)),它既无效又多余。

⚠️ 总结

:关键原则

方式 是否提交数据 是否校验 是否可篡改 推荐度
widgets={'attr':{'disabled':'True'}} ❌ 否 ❌(因无数据) ✅ 是(纯前端) ❌ 不推荐
self.fields['x'].disabled = True ✅ 是(自动隐藏域) ✅ 跳过验证 ❌ 否(服务端控制) ✅ 强烈推荐

通过服务端禁用字段,你既能实现清晰的 UI 分层(编辑/只读),又能保证数据完整性、安全性和表单逻辑的健壮性。这是 Django 表单集开发中的最佳实践。