Django技术问询:结合表单与模型生成安全表单的实现疑问
解决Django工单表单安全验证问题:结合Form.is_valid()与cleaned_data
我来帮你梳理如何用Django原生表单机制安全实现这个需求,完全不用跳过自带的安全防护。核心思路是用Django表单类封装字段和验证逻辑,而不是手动在模板生成表单后自己处理POST数据。
场景1:单个工单选择+数量提交
如果用户只需要选择一个工单并填写对应数量,用普通表单类即可:
1. 定义表单类(包含验证逻辑)
from django import forms from .models import Ticket class TicketSelectionForm(forms.Form): # 自动从数据库拉取工单选项,安全生成下拉框/单选按钮 ticket = forms.ModelChoiceField( queryset=Ticket.objects.all(), empty_label="请选择工单", label="选择工单" ) # 内置基础验证:数量必须≥1 quantity = forms.IntegerField( min_value=1, label="申请数量", widget=forms.NumberInput(attrs={"placeholder": "请输入数量"}) ) # 自定义业务规则验证:数量不超过工单可用库存 def clean(self): cleaned_data = super().clean() selected_ticket = cleaned_data.get("ticket") requested_quantity = cleaned_data.get("quantity") if selected_ticket and requested_quantity: if requested_quantity > selected_ticket.available_quantity: raise forms.ValidationError( f"工单「{selected_ticket.name}」可用数量不足,当前剩余{selected_ticket.available_quantity}份" ) return cleaned_data
2. 视图处理请求
from django.shortcuts import render, redirect from .forms import TicketSelectionForm def ticket_request(request): if request.method == "POST": form = TicketSelectionForm(request.POST) if form.is_valid(): # 从cleaned_data获取安全的验证后数据 ticket = form.cleaned_data["ticket"] quantity = form.cleaned_data["quantity"] # 这里写你的业务逻辑:比如创建申请记录、扣减库存等 # ... return redirect("request_success") else: # GET请求初始化空表单 form = TicketSelectionForm() return render(request, "ticket_request.html", {"form": form})
3. 模板渲染(支持手动遍历工单选项)
如果不想用默认的下拉框,想手动渲染每个工单为单选按钮,模板可以这么写:
<form method="post"> {% csrf_token %} <!-- 必须加,Django自动CSRF防护 --> <!-- 显示全局验证错误 --> {% if form.non_field_errors %} <div class="error">{{ form.non_field_errors }}</div> {% endif %} <!-- 手动遍历工单选项 --> <div> <label>选择工单:</label> {% for radio in form.ticket %} <div class="ticket-option"> {{ radio.tag }} {{ radio.choice_label }} </div> {% endfor %} {% if form.ticket.errors %} <div class="error">{{ form.ticket.errors }}</div> {% endif %} </div> <!-- 数量输入框 --> <div> {{ form.quantity.label_tag }} {{ form.quantity }} {% if form.quantity.errors %} <div class="error">{{ form.quantity.errors }}</div> {% endif %} </div> <button type="submit">提交申请</button> </form>
场景2:批量提交多个工单的数量
如果需要同时为多个工单填写数量并提交,用**表单集(FormSet)**实现:
1. 定义单个工单的表单单元
from django import forms from .models import Ticket class TicketQuantityForm(forms.Form): # 隐藏字段传递工单ID,避免手动输入的风险 ticket = forms.ModelChoiceField( queryset=Ticket.objects.all(), widget=forms.HiddenInput() ) # 允许不填数量(表示不申请该工单),但填了必须≥1 quantity = forms.IntegerField( min_value=1, required=False, label="申请数量", widget=forms.NumberInput(attrs={"placeholder": "0表示不申请"}) ) def clean(self): cleaned_data = super().clean() ticket = cleaned_data.get("ticket") quantity = cleaned_data.get("quantity") if quantity is not None and quantity > 0: if quantity > ticket.available_quantity: raise forms.ValidationError( f"工单「{ticket.name}」可用数量不足,剩余{ticket.available_quantity}份" ) return cleaned_data
2. 视图中初始化表单集
from django.forms import formset_factory from django.shortcuts import render, redirect from .forms import TicketQuantityForm from .models import Ticket def bulk_ticket_request(request): # 创建表单集,extra=0表示不生成额外空表单 TicketQuantityFormSet = formset_factory(TicketQuantityForm, extra=0) all_tickets = Ticket.objects.all() if request.method == "POST": formset = TicketQuantityFormSet(request.POST) if formset.is_valid(): # 遍历每个表单处理数据 for form in formset: ticket = form.cleaned_data.get("ticket") quantity = form.cleaned_data.get("quantity") if quantity and quantity > 0: # 处理业务逻辑:创建申请记录等 # ... return redirect("bulk_request_success") else: # 用所有工单初始化表单集 initial_data = [{"ticket": ticket} for ticket in all_tickets] formset = TicketQuantityFormSet(initial=initial_data) # 把工单和对应表单配对,方便模板渲染 ticket_form_pairs = zip(all_tickets, formset) return render(request, "bulk_ticket_request.html", { "ticket_form_pairs": ticket_form_pairs, "formset": formset })
3. 批量提交模板
<form method="post"> {% csrf_token %} {{ formset.management_form }} <!-- 表单集必须的管理字段,不可省略 --> {% for ticket, form in ticket_form_pairs %} <div class="ticket-item"> <span>{{ ticket.name }} (可用数量: {{ ticket.available_quantity }})</span> {{ form.ticket }} <!-- 隐藏字段,传递工单ID --> {{ form.quantity.label_tag }} {{ form.quantity }} {% if form.errors %} <div class="error">{{ form.errors }}</div> {% endif %} </div> {% endfor %} <button type="submit">批量提交</button> </form>
关键安全保障要点
- CSRF防护:模板中必须添加
{% csrf_token %},Django自动拦截跨站伪造请求 - 字段验证:利用Django表单字段的内置验证(如
min_value)和自定义clean方法,避免非法数据进入业务逻辑 - cleaned_data:验证通过后仅从
cleaned_data获取数据,这些数据已被清洗(比如转成正确的Python类型),避免直接操作request.POST的风险 - ModelChoiceField:自动从数据库拉取工单选项,避免手动生成HTML选项导致的注入风险
- 表单集管理字段:批量提交时必须渲染
{{ formset.management_form }},确保表单集能正确识别POST数据
内容的提问来源于stack exchange,提问作者user9252255




