Chapter 06

表单与验证

表单系统是 Django 又一份「电池」:自动渲染 HTML、服务端校验、CSRF 防护、从模型一键生成,安全高效。

6.1 为什么用 Django 表单

手写 <form> + 手动取 request.POST + 手动校验,既繁琐又易出安全漏洞。Django 表单把「渲染、校验、清洗、错误提示」封装成类,一处声明处处受益。

Form
普通表单类,字段手动声明。适合与模型无关的输入(搜索框、联系表单)。
ModelForm
从模型自动生成表单,字段、校验、保存全打通。form.save() 直接写库。
Field 与 Widget
Field 是数据层(校验规则),Widget 是表现层(渲染成什么 HTML 控件)。如 CharField + Textarea
is_valid()
触发全部校验,通过后把清洗后的数据放进 form.cleaned_data,失败则填充 form.errors
cleaned_data
校验通过后类型转换、去空格后的干净数据字典,视图应只读它,绝不用原始 request.POST
CSRF(跨站请求伪造)
攻击者诱导用户在已登录站点执行非本意操作。Django 用 {% csrf_token %} 令牌防护,POST 缺令牌直接 403。
clean 方法
自定义校验钩子:clean_<field>() 校验单字段,clean() 做跨字段校验。

6.2 普通 Form

# blog/forms.py
from django import forms

class ContactForm(forms.Form):
    name = forms.CharField(max_length=50, label="姓名")
    email = forms.EmailField(label="邮箱")
    message = forms.CharField(widget=forms.Textarea, label="内容")

6.3 视图中处理表单

from django.shortcuts import render, redirect
from .forms import ContactForm

def contact(request):
    if request.method == "POST":
        form = ContactForm(request.POST)
        if form.is_valid():
            data = form.cleaned_data       # 干净数据
            # 发邮件 / 存库 ...
            return redirect("blog:thanks")
    else:
        form = ContactForm()
    return render(request, "blog/contact.html", {"form": form})

6.4 模板渲染表单与 CSRF

<form method="post">
  {% csrf_token %}                # 必须!否则 POST 被拒 403
  {{ form.as_p }}                 # 逐字段包 <p> 渲染
  <button type="submit">提交</button>
</form>
渲染方式输出
{{ form.as_p }}每字段一个 <p>
{{ form.as_div }}每字段一个 <div>(推荐)
{{ form.as_table }}表格行
{{ form.name }}手动逐字段控制样式
CSRF 令牌不可省

任何 POST 表单都必须在 <form> 内放 {% csrf_token %}。缺失将触发 403 Forbidden。这是 Django 默认开启的关键安全防护,切勿为图省事关掉 CsrfViewMiddleware

6.5 ModelForm:从模型生成表单

from django.forms import ModelForm
from .models import Post

class PostForm(ModelForm):
    class Meta:
        model = Post
        fields = ["title", "body", "category", "published"]
        widgets = {"body": forms.Textarea(attrs={"rows": 8})}

# 视图里保存
form = PostForm(request.POST)
if form.is_valid():
    post = form.save()          # 直接写库并返回实例

6.6 自定义校验:clean 方法

class PostForm(ModelForm):
    class Meta:
        model = Post
        fields = ["title", "body"]

    def clean_title(self):          # 单字段校验
        title = self.cleaned_data["title"]
        if "广告" in title:
            raise forms.ValidationError("标题不能含广告字样")
        return title

    def clean(self):                 # 跨字段校验
        cleaned = super().clean()
        if len(cleaned.get("body", "")) < 10:
            raise forms.ValidationError("正文太短")
        return cleaned

6.7 文件上传

# 模型加文件字段
class Post(models.Model):
    cover = models.ImageField(upload_to="covers/", blank=True)

# 表单必须传 request.FILES
form = PostForm(request.POST, request.FILES)

# 模板 form 加 enctype
<form method="post" enctype="multipart/form-data">
ImageField 依赖 Pillow

使用 ImageField 需先 pip install Pillow。同时在 settings 配置 MEDIA_URL/MEDIA_ROOT 并在开发路由挂载媒体文件服务。

6.8 小结与下一步