下载工作台
Django Web 开发

表单 ModelForm 与 CSRF

试读上半部分 · 解锁后可读全文

第 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,库存 ≥ 0ModelForm clean_*
安全防止伪造 POSTCSRF Token
本章代码均在本地虚构项目 shop-demo 中演练,API 联调域名仅作占位:https://api.example.com(ch07 详讲 DRF)。

2.2 Form 与 ModelForm 对比

类型基类字段来源典型用途
forms.Form手动声明字段开发者定义搜索框、联系表单、与模型无关的输入
forms.ModelForm绑定 Modelmodels.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,避免刷新页面重复提交。


以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。