#Django Form表单处理详解
#课程目标
- 理解Django表单系统的工作原理
- 掌握表单定义和字段类型的使用
- 学会表单验证和错误处理
- 了解ModelForm的使用方法
#表单系统概述
Django表单系统提供了一套强大而灵活的机制来处理HTML表单。它不仅生成HTML表单,还能验证用户提交的数据,处理错误信息,并与Model无缝集成。
#表单的主要功能:
- 生成HTML表单
- 数据验证
- 错误处理
- 与Model集成
#基础表单定义
#简单表单示例
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(max_length=100, label='姓名')
email = forms.EmailField(label='邮箱')
subject = forms.CharField(max_length=200, label='主题')
message = forms.CharField(widget=forms.Textarea, label='消息')
subscribe = forms.BooleanField(required=False, label='订阅邮件')#在模板中使用表单
<!-- contact.html -->
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">提交</button>
</form>#在视图中处理表单
from django.shortcuts import render, redirect
from django.contrib import messages
from .forms import ContactForm
def contact_view(request):
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid():
# 处理表单数据
name = form.cleaned_data['name']
email = form.cleaned_data['email']
subject = form.cleaned_data['subject']
message = form.cleaned_data['message']
# 保存或处理数据
# send_email(name, email, subject, message)
messages.success(request, '消息发送成功!')
return redirect('contact')
else:
form = ContactForm()
return render(request, 'contact.html', {'form': form})#表单字段类型
#常用字段类型
class UserRegistrationForm(forms.Form):
# 文本字段
username = forms.CharField(
max_length=150,
min_length=3,
label='用户名',
help_text='3-150个字符'
)
# 邮箱字段
email = forms.EmailField(label='邮箱地址')
# 密码字段
password = forms.CharField(
widget=forms.PasswordInput,
min_length=8,
label='密码'
)
# 确认密码字段
password_confirm = forms.CharField(
widget=forms.PasswordInput,
label='确认密码'
)
# 选择字段
gender = forms.ChoiceField(
choices=[
('M', '男'),
('F', '女'),
('O', '其他')
],
label='性别'
)
# 多选字段
hobbies = forms.MultipleChoiceField(
choices=[
('reading', '阅读'),
('sports', '运动'),
('music', '音乐'),
('travel', '旅行')
],
widget=forms.CheckboxSelectMultiple,
label='兴趣爱好'
)
# 文件字段
avatar = forms.ImageField(required=False, label='头像')
# 日期字段
birth_date = forms.DateField(
widget=forms.DateInput(attrs={'type': 'date'}),
label='出生日期'
)
# 布尔字段
agree_terms = forms.BooleanField(label='同意服务条款')#表单验证
#字段验证
class ArticleForm(forms.Form):
title = forms.CharField(max_length=200, label='标题')
content = forms.CharField(widget=forms.Textarea, label='内容')
category = forms.CharField(max_length=50, label='分类')
tags = forms.CharField(max_length=200, required=False, label='标签')
def clean_title(self):
"""验证标题"""
title = self.cleaned_data['title']
if len(title) < 5:
raise forms.ValidationError('标题至少需要5个字符')
# 检查标题是否已存在
if Article.objects.filter(title=title).exists():
raise forms.ValidationError('该标题已存在')
return title
def clean_content(self):
"""验证内容"""
content = self.cleaned_data['content']
if len(content) < 10:
raise forms.ValidationError('内容至少需要10个字符')
return content
def clean(self):
"""整体验证"""
cleaned_data = super().clean()
title = cleaned_data.get('title')
content = cleaned_data.get('content')
# 检查标题和内容是否相似
if title and content and title in content:
raise forms.ValidationError('标题不应出现在内容中')
return cleaned_data#自定义验证器
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
def validate_even(value):
"""验证偶数"""
if value % 2 != 0:
raise ValidationError(
_('%(value)s 不是偶数'),
params={'value': value},
)
def validate_file_extension(value):
"""验证文件扩展名"""
import os
ext = os.path.splitext(value.name)[1]
valid_extensions = ['.pdf', '.doc', '.docx', '.txt']
if not ext.lower() in valid_extensions:
raise ValidationError('不支持的文件格式')
class DocumentForm(forms.Form):
title = forms.CharField(max_length=100)
document = forms.FileField(validators=[validate_file_extension])
number = forms.IntegerField(validators=[validate_even])#Widget自定义
#自定义Widget外观
class SearchForm(forms.Form):
query = forms.CharField(
max_length=200,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '搜索...',
'id': 'search-input'
}),
label='搜索'
)
category = forms.ChoiceField(
choices=[('all', '全部'), ('news', '新闻'), ('blog', '博客')],
widget=forms.Select(attrs={
'class': 'form-select'
}),
label='分类'
)
date_from = forms.DateField(
widget=forms.DateInput(attrs={
'type': 'date',
'class': 'form-control'
}),
required=False,
label='开始日期'
)
date_to = forms.DateField(
widget=forms.DateInput(attrs={
'type': 'date',
'class': 'form-control'
}),
required=False,
label='结束日期'
)#ModelForm使用
#基本ModelForm
from django import forms
from .models import Article
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ['title', 'content', 'category', 'tags', 'status']
exclude = ['author', 'created_at'] # 排除某些字段
widgets = {
'title': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '请输入文章标题'
}),
'content': forms.Textarea(attrs={
'class': 'form-control',
'rows': 10,
'placeholder': '请输入文章内容'
}),
'status': forms.Select(attrs={
'class': 'form-select'
})
}
labels = {
'title': '文章标题',
'content': '文章内容',
'category': '分类',
'tags': '标签',
'status': '状态'
}
help_texts = {
'title': '文章标题,最多200个字符',
'tags': '用逗号分隔多个标签'
}
error_messages = {
'title': {
'max_length': '标题太长了!',
},
}#ModelForm验证
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ['title', 'content', 'category', 'tags', 'status']
def clean_title(self):
"""验证标题唯一性"""
title = self.cleaned_data['title']
# 检查除当前实例外是否已存在相同标题
if Article.objects.filter(title=title).exclude(pk=self.instance.pk).exists():
raise forms.ValidationError('该标题已存在')
return title
def clean(self):
"""验证发布状态"""
cleaned_data = super().clean()
status = cleaned_data.get('status')
content = cleaned_data.get('content')
if status == 'published' and len(content) < 100:
raise forms.ValidationError('发布状态的文章内容至少需要100个字符')
return cleaned_data#表单集(Formsets)
#基础表单集
from django.forms import formset_factory
class AuthorForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
# 创建表单集
AuthorFormSet = formset_factory(AuthorForm, extra=2)
# 在视图中使用
def manage_authors(request):
if request.method == 'POST':
formset = AuthorFormSet(request.POST)
if formset.is_valid():
for form in formset:
if form.cleaned_data:
# 处理每个表单的数据
name = form.cleaned_data['name']
email = form.cleaned_data['email']
# 保存数据
else:
formset = AuthorFormSet()
return render(request, 'manage_authors.html', {'formset': formset})#ModelForm表单集
from django.forms import modelformset_factory
from .models import Article
# 创建Article的表单集
ArticleFormSet = modelformset_factory(
Article,
fields=['title', 'content', 'status'],
extra=1,
can_delete=True # 允许删除
)
def bulk_edit_articles(request):
ArticleFormSet = modelformset_factory(
Article,
fields=['title', 'content', 'status'],
extra=0,
can_delete=True
)
if request.method == 'POST':
formset = ArticleFormSet(request.POST)
if formset.is_valid():
instances = formset.save()
return redirect('article_list')
else:
queryset = Article.objects.filter(author=request.user)[:10]
formset = ArticleFormSet(queryset=queryset)
return render(request, 'bulk_edit.html', {'formset': formset})#表单处理最佳实践
#视图中的表单处理
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib import messages
from django.contrib.auth.decorators import login_required
@login_required
def create_article(request):
if request.method == 'POST':
form = ArticleForm(request.POST)
if form.is_valid():
article = form.save(commit=False) # 先不保存到数据库
article.author = request.user # 设置作者
article.save() # 保存到数据库
messages.success(request, '文章创建成功!')
return redirect('article_detail', pk=article.pk)
else:
messages.error(request, '请修正表单中的错误')
else:
form = ArticleForm()
return render(request, 'articles/create.html', {'form': form})
@login_required
def edit_article(request, pk):
article = get_object_or_404(Article, pk=pk)
# 检查权限
if article.author != request.user:
messages.error(request, '您没有权限编辑此文章')
return redirect('article_list')
if request.method == 'POST':
form = ArticleForm(request.POST, instance=article)
if form.is_valid():
form.save()
messages.success(request, '文章更新成功!')
return redirect('article_detail', pk=article.pk)
else:
form = ArticleForm(instance=article)
return render(request, 'articles/edit.html', {
'form': form,
'article': article
})#表单模板最佳实践
<!-- articles/form.html -->
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% if form.non_field_errors %}
<div class="alert alert-danger">
<ul>
{% for error in form.non_field_errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="mb-3">
{{ form.title.label_tag }}
{{ form.title }}
{% if form.title.help_text %}
<small class="form-text text-muted">{{ form.title.help_text }}</small>
{% endif %}
{% if form.title.errors %}
<div class="text-danger">{{ form.title.errors }}</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.content.label_tag }}
{{ form.content }}
{% if form.content.errors %}
<div class="text-danger">{{ form.content.errors }}</div>
{% endif %}
</div>
<button type="submit" class="btn btn-primary">提交</button>
<a href="{% url 'article_list' %}" class="btn btn-secondary">取消</a>
</form>#安全考虑
#CSRF保护
# 在表单中始终包含CSRF令牌
"""
<form method="post">
{% csrf_token %}
<!-- 表单字段 -->
</form>
"""
# 在AJAX请求中包含CSRF令牌
"""
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", $('[name=csrfmiddlewaretoken]').val());
}
}
});
"""#表单安全验证
class SecureContactForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
message = forms.CharField(widget=forms.Textarea)
def clean_message(self):
"""防止垃圾信息"""
message = self.cleaned_data['message']
# 检查是否包含垃圾信息关键词
spam_keywords = ['viagra', 'casino', 'lottery', 'money']
for keyword in spam_keywords:
if keyword.lower() in message.lower():
raise forms.ValidationError('消息内容包含敏感词汇')
# 检查链接数量(防止垃圾邮件)
import re
links = re.findall(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', message)
if len(links) > 2:
raise forms.ValidationError('消息中包含过多链接')
return message#课程总结
本节课我们学习了Django表单系统的各个方面,包括表单定义、字段类型、验证机制、ModelForm使用等。表单处理是Web应用开发中的重要环节,掌握Django的表单系统对于构建用户友好的应用至关重要。

