第 2 章 · 表单 ModelForm 与 CSRF
本章目标:掌握 Django 表单体系(Form vs ModelForm);在 shop-demo 中实现商品的创建与编辑页面;理解字段校验、错误展示与 clean_* 钩子;正确使用 {% csrf_token %} 防御跨站请求伪造;能用函数视图完成完整的「GET 展示表单 → POST 提交 → 重定向」流程。
学时建议:4~5 小时(含 2 小时跟练)
前置:完成 django-web ch01(shopdemo 项目、Product 模型、基础 URL 与模板)。
2.1 场景说明:运营录入商品
shop-demo 电商后台中,运营人员需要在 Admin 之外提供一个更友好的录入页面:
| 场景 | 需求 | 本章实现 |
|---|---|---|
| 新建商品 | 填写名称、SKU、价格、库存、分类 | ProductCreateView(函数版) |
| 编辑商品 | 修改已有记录,保留 SKU 唯一性 | product_edit 视图 |
| 数据校验 | 价格 > 0,库存 ≥ 0 | ModelForm clean_* |
| 安全 | 防止伪造 POST | CSRF Token |
本章代码均在本地虚构项目 shop-demo 中演练,API 联调域名仅作占位:https://api.example.com(ch07 详讲 DRF)。
2.2 Form 与 ModelForm 对比
| 类型 | 基类 | 字段来源 | 典型用途 |
|---|---|---|---|
forms.Form | 手动声明字段 | 开发者定义 | 搜索框、联系表单、与模型无关的输入 |
forms.ModelForm | 绑定 Model | 从 models.py 自动生成 | 商品 CRUD、用户资料编辑 |
Model (数据库表结构)
↓ Meta.model
ModelForm (表单字段 + 校验)
↓ as_p / 手动渲染
Template (HTML <form>)
↓ POST
View (is_valid → save)
2.3 定义 ProductForm
catalog/forms.py(新建文件):
from django import forms
from django.core.exceptions import ValidationError
from .models import Product
class ProductForm(forms.ModelForm):
class Meta:
model = Product
fields = ["name", "sku", "price", "stock", "category", "is_active"]
labels = {
"name": "商品名称",
"sku": "SKU 编码",
"price": "售价(元)",
"stock": "库存数量",
"category": "所属分类",
"is_active": "是否上架",
}
widgets = {
"name": forms.TextInput(attrs={"class": "form-input", "placeholder": "如:无线鼠标"}),
"sku": forms.TextInput(attrs={"class": "form-input", "placeholder": "如:SKU-001"}),
"price": forms.NumberInput(attrs={"class": "form-input", "step": "0.01"}),
"stock": forms.NumberInput(attrs={"class": "form-input", "min": "0"}),
"category": forms.Select(attrs={"class": "form-select"}),
}
error_messages = {
"sku": {"unique": "该 SKU 已存在,请更换编码。"},
"price": {"invalid": "请输入合法的价格数字。"},
}
def clean_price(self):
price = self.cleaned_data.get("price")
if price is not None and price <= 0:
raise ValidationError("售价必须大于 0。")
return price
def clean_stock(self):
stock = self.cleaned_data.get("stock")
if stock is not None and stock < 0:
raise ValidationError("库存不能为负数。")
return stock
def clean(self):
cleaned = super().clean()
sku = cleaned.get("sku")
if sku and not sku.upper().startswith("SKU-"):
# 业务规则:教学用 SKU 前缀
self.add_error("sku", "SKU 建议以 SKU- 开头,如 SKU-1001。")
return cleaned
| 机制 | 作用 |
|---|---|
clean_<field> | 单字段校验 |
clean() | 多字段联合校验 |
error_messages | 覆盖默认英文错误文案 |
widgets | 控制 HTML 属性(class、placeholder) |
2.4 创建商品视图
catalog/views.py 追加:
from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect, render
from .forms import ProductForm
from .models import Product
def product_create(request):
if request.method == "POST":
form = ProductForm(request.POST)
if form.is_valid():
product = form.save()
messages.success(request, f"商品「{product.name}」创建成功。")
return redirect("product_list")
else:
form = ProductForm()
return render(request, "catalog/product_form.html", {
"form": form,
"title": "新建商品",
"submit_label": "创建",
})
def product_edit(request, pk):
product = get_object_or_404(Product, pk=pk)
if request.method == "POST":
form = ProductForm(request.POST, instance=product)
if form.is_valid():
form.save()
messages.success(request, f"商品「{product.name}」已更新。")
return redirect("product_list")
else:
form = ProductForm(instance=product)
return render(request, "catalog/product_form.html", {
"form": form,
"title": "编辑商品",
"submit_label": "保存",
"product": product,
})
POST-Redirect-GET(PRG)模式:提交成功后 redirect,避免刷新页面重复提交。