Django 表单处理与数据验证
Django 的表单系统(Forms)是处理用户输入的核心模块,从简单的文本输入到复杂的文件上传,Django forms 提供了完整的解决方案。本文深入讲解 Django 表单的使用方法。
Django Forms vs ModelForms
- Form:独立表单,不依赖模型,适合与模型无关的表单(如搜索框、联系表单)
- ModelForm:与模型绑定的表单,可自动生成字段并支持 save(),适合增删改查操作
创建表单
# blog/forms.py
from django import forms
from django.core.validators import MinLengthValidator, EmailValidator, URLValidator
from .models import Article, Comment
class SearchForm(forms.Form):
"""独立搜索表单(不依赖模型)"""
q = forms.CharField(
label="搜索",
max_length=100,
widget=forms.TextInput(attrs={
"placeholder": "输入关键词搜索...",
"class": "search-input"
})
)
category = forms.ChoiceField(
label="分类",
required=False,
choices=[("", "全部分类"), ("python", "Python"), ("django", "Django")]
)
sort = forms.ChoiceField(
label="排序",
required=False,
initial="latest",
choices=[("latest", "最新"), ("popular", "最热")]
)
class ArticleForm(forms.ModelForm):
"""模型表单:自动根据模型生成字段"""
class Meta:
model = Article
fields = ["title", "slug", "content", "category", "tags", "is_published"]
widgets = {
"title": forms.TextInput(attrs={"class": "form-control"}),
"content": forms.Textarea(attrs={"class": "form-control", "rows": 10}),
"slug": forms.TextInput(attrs={"class": "form-control"}),
}
def clean_slug(self):
"""自定义字段验证:slug 唯一性检查"""
slug = self.cleaned_data.get("slug")
if Article.objects.filter(slug=slug).exclude(pk=self.instance.pk).exists():
raise forms.ValidationError("该 slug 已存在")
return slug
def clean_title(self):
"""自定义字段验证:标题长度与内容检查"""
title = self.cleaned_data.get("title")
if len(title) < 5:
raise forms.ValidationError("标题长度不能少于 5 个字符")
if any(word in title for word in ["spam", "广告"]):
raise forms.ValidationError("标题包含敏感词")
return title
class CommentForm(forms.ModelForm):
"""评论表单"""
class Meta:
model = Comment
fields = ["author_name", "email", "content"]
widgets = {
"content": forms.Textarea(attrs={"rows": 4, "placeholder": "写下你的评论..."}),
}
def clean_email(self):
email = self.cleaned_data.get("email")
if email and not email.endswith("@example.com"):
raise forms.ValidationError("仅允许 example.com 域名的邮箱")
return email
视图中处理表单
# blog/views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from django.core.paginator import Paginator
from .forms import SearchForm, ArticleForm, CommentForm
from .models import Article, Comment
def article_create(request):
"""创建文章"""
if request.method == "POST":
form = ArticleForm(request.POST, request.FILES)
if form.is_valid():
article = form.save(commit=False)
article.author = request.user
article.save()
form.save_m2m() # 保存多对多关系
messages.success(request, "文章创建成功!")
return redirect("article_detail", slug=article.slug)
else:
messages.error(request, "表单验证失败,请检查输入。")
else:
form = ArticleForm()
return render(request, "blog/article_form.html", {"form": form, "action": "create"})
def article_edit(request, slug):
"""编辑文章"""
article = get_object_or_404(Article, slug=slug)
if request.method == "POST":
form = ArticleForm(request.POST, request.FILES, instance=article)
if form.is_valid():
form.save()
form.save_m2m()
messages.success(request, "文章更新成功!")
return redirect("article_detail", slug=article.slug)
else:
form = ArticleForm(instance=article)
return render(request, "blog/article_form.html", {
"form": form, "article": article, "action": "edit"
})
def add_comment(request, article_id):
"""添加评论"""
article = get_object_or_404(Article, id=article_id)
if request.method == "POST":
form = CommentForm(request.POST)
if form.is_valid():
comment = form.save(commit=False)
comment.article = article
comment.save()
messages.success(request, "评论已提交!")
else:
messages.error(request, str(form.errors))
return redirect("article_detail", slug=article.slug)
def search(request):
"""搜索视图"""
form = SearchForm(request.GET)
articles = Article.objects.filter(is_published=True)
if form.is_valid():
q = form.cleaned_data.get("q")
category = form.cleaned_data.get("category")
if q:
articles = articles.filter(title__icontains=q)
if category:
articles = articles.filter(category__slug=category)
if form.cleaned_data.get("sort") == "popular":
articles = articles.order_by("-views")
else:
articles = articles.order_by("-created_at")
paginator = Paginator(articles, 20)
page_obj = paginator.get_page(request.GET.get("page"))
return render(request, "blog/search.html", {
"form": form, "page_obj": page_obj
})
模板中使用表单
{# blog/article_form.html #}
{# 或者使用 as_p / as_table / as_ul 快速渲染 #}
表单字段与验证器
from django import forms
from django.core.validators import MinLengthValidator, RegexValidator
class ValidatedForm(forms.Form):
# CharField 验证
username = forms.CharField(
min_length=3,
max_length=20,
validators=[RegexValidator(r"^[a-zA-Z0-9_]+$", message="仅允许字母数字下划线")]
)
# EmailField
email = forms.EmailField(required=False)
# URLField
website = forms.URLField(required=False, validators=[URLValidator()])
# IntegerField / FloatField
age = forms.IntegerField(min_value=0, max_value=150)
price = forms.FloatField(min_value=0.01)
# DateField / DateTimeField
birth_date = forms.DateField(input_formats=["%Y-%m-%d", "%d/%m/%Y"])
# ChoiceField
gender = forms.ChoiceField(choices=[("M", "男"), ("F", "女")], widget=forms.RadioSelect)
# MultipleChoiceField
hobbies = forms.MultipleChoiceField(
choices=[("reading", "阅读"), ("coding", "编程"), ("gaming", "游戏")],
widget=forms.CheckboxSelectMultiple
)
# FileField / ImageField
avatar = forms.ImageField(required=False)
# RegexField
phone = forms.RegexField(regex=r"^1[3-9]d{9}$", error_message="手机号格式不正确")
常见问题
- Q: form.is_valid() 返回 True 但某些字段值为空?
A:is_valid() 会运行所有字段级验证,但未传入的字段不会进入 cleaned_data。对于可选字段,检查 cleaned_data 中是否存在该键:if "email" in form.cleaned_data。
- Q: 如何处理文件上传?
A:表单需设置 enctype="multipart/form-data",视图需接收 request.FILES,ImageField 会自动验证文件类型。 - Q: ModelForm 的 save(commit=False) 有什么用?
A:commit=False 返回模型实例但不保存到数据库,适合在保存前修改字段(如设置 author);之后需手动 save() 或 save_m2m()(有多对多时)。
- Q: 如何自定义表单错误消息?
A:在字段定义时通过 error_messages 参数或在 clean_xxx() 方法中 raise forms.ValidationError("自定义消息")。