表单基础
表单是 Web 应用中用户输入数据的主要方式。Django 提供了强大的表单处理系统,可以自动生成 HTML 表单、验证用户输入并处理错误。
表单基础
创建表单类
Django 表单是 Python 类,继承自 forms.Form:
# forms.py
from django import forms
class ContactForm(forms.Form):
"""联系表单"""
name = forms.CharField(
label='姓名',
max_length=100,
widget=forms.TextInput(attrs={'class': 'form-control'})
)
email = forms.EmailField(
label='邮箱',
widget=forms.EmailInput(attrs={'class': 'form-control'})
)
subject = forms.CharField(
label='主题',
max_length=200,
widget=forms.TextInput(attrs={'class': 'form-control'})
)
message = forms.CharField(
label='消息内容',
widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 5})
)
在视图中使用表单
# views.py
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():
# 获取验证后的数据
name = form.cleaned_data['name']
email = form.cleaned_data['email']
subject = form.cleaned_data['subject']
message = form.cleaned_data['message']
# 处理数据(发送邮件等)
send_contact_email(name, email, subject, message)
return redirect('contact_success')
else:
form = ContactForm()
return render(request, 'contact.html', {'form': form})
在模板中渲染表单
<!-- contact.html -->
<form method="post">
{% csrf_token %}
<!-- 方式一:自动渲染整个表单 -->
{{ form.as_p }}
<!-- 方式二:手动渲染每个字段 -->
<div class="form-group">
{{ form.name.label_tag }}
{{ form.name }}
{% if form.name.errors %}
<div class="error">{{ form.name.errors }}</div>
{% endif %}
</div>
<!-- 方式三:循环渲染 -->
{% for field in form %}
<div class="form-group">
{{ field.label_tag }}
{{ field }}
{% if field.errors %}
<div class="error">{{ field.errors }}</div>
{% endif %}
{% if field.help_text %}
<small class="help">{{ field.help_text }}</small>
{% endif %}
</div>
{% endfor %}
<button type="submit">提交</button>
</form>
表单字段类型
Django 提供了丰富的内置字段类型:
from django import forms
class DemoForm(forms.Form):
"""演示各种字段类型"""
# 文本字段
char_field = forms.CharField(
label='文本字段',
max_length=100,
min_length=2,
strip=True, # 去除首尾空格
empty_value='', # 空值默认值
)
# 整数字段
integer_field = forms.IntegerField(
label='整数字段',
min_value=0,
max_value=100,
)
# 浮点数字段
float_field = forms.FloatField(
label='浮点数字段',
min_value=0.0,
max_value=100.0,
)
# 小数字段(高精度)
decimal_field = forms.DecimalField(
label='小数字段',
max_digits=10,
decimal_places=2,
)
# 布尔字段
boolean_field = forms.BooleanField(
label='布尔字段',
required=False, # 复选框通常不需要必填
)
# 邮箱字段
email_field = forms.EmailField(
label='邮箱字段',
)
# URL 字段
url_field = forms.URLField(
label='URL 字段',
)
# 日期字段
date_field = forms.DateField(
label='日期字段',
widget=forms.DateInput(attrs={'type': 'date'}),
)
# 时间字段
time_field = forms.TimeField(
label='时间字段',
widget=forms.TimeInput(attrs={'type': 'time'}),
)
# 日期时间字段
datetime_field = forms.DateTimeField(
label='日期时间字段',
widget=forms.DateTimeInput(attrs={'type': 'datetime-local'}),
)
# 选择字段(下拉框)
CHOICES = [
('option1', '选项一'),
('option2', '选项二'),
('option3', '选项三'),
]
choice_field = forms.ChoiceField(
label='选择字段',
choices=CHOICES,
)
# 多选字段
multiple_choice_field = forms.MultipleChoiceField(
label='多选字段',
choices=CHOICES,
widget=forms.CheckboxSelectMultiple,
)
# 文件上传字段
file_field = forms.FileField(
label='文件字段',
required=False,
)
# 图片上传字段
image_field = forms.ImageField(
label='图片字段',
required=False,
)
# 隐藏字段
hidden_field = forms.CharField(
widget=forms.HiddenInput(),
initial='default_value',
)
字段参数
通用参数
class FormExample(forms.Form):
field = forms.CharField(
# 基本参数
label='字段标签', # 显示标签
label_suffix=':', # 标签后缀
required=True, # 是否必填
initial='默认值', # 初始值
disabled=False, # 是否禁用
help_text='帮助文本', # 帮助提示
# 验证参数
min_length=2, # 最小长度
max_length=100, # 最大长度
# 错误消息
error_messages={
'required': '此字段必填',
'min_length': '最少需要 %(limit_value)d 个字符',
'max_length': '最多允许 %(limit_value)d 个字符',
},
# 小部件
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '请输入内容',
}),
# 本地化
localize=True,
# 是否显示
show_hidden_initial=False,
)
choices 参数
class ChoiceForm(forms.Form):
# 静态选项
STATUS_CHOICES = [
('draft', '草稿'),
('published', '已发布'),
]
status = forms.ChoiceField(choices=STATUS_CHOICES)
# 动态选项(函数)
def get_years():
return [(year, year) for year in range(2020, 2030)]
year = forms.ChoiceField(choices=get_years)
# 使用枚举
from django.db import models
class Priority(models.TextChoices):
LOW = 'low', '低'
MEDIUM = 'medium', '中'
HIGH = 'high', '高'
priority = forms.ChoiceField(choices=Priority.choices)
小部件(Widgets)
小部件控制表单字段在 HTML 中的渲染方式:
from django import forms
class WidgetForm(forms.Form):
# 文本输入
text = forms.CharField(
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '请输入文本',
})
)
# 密码输入
password = forms.CharField(
widget=forms.PasswordInput(attrs={
'class': 'form-control',
})
)
# 隐藏输入
hidden = forms.CharField(
widget=forms.HiddenInput()
)
# 多行文本
content = forms.CharField(
widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 5,
})
)
# 日期选择
date = forms.DateField(
widget=forms.DateInput(attrs={
'type': 'date',
'class': 'form-control',
})
)
# 复选框
agree = forms.BooleanField(
widget=forms.CheckboxInput(attrs={
'class': 'form-check-input',
})
)
# 下拉选择
category = forms.ChoiceField(
choices=[('tech', '技术'), ('life', '生活')],
widget=forms.Select(attrs={
'class': 'form-control',
})
)
# 单选按钮
gender = forms.ChoiceField(
choices=[('M', '男'), ('F', '女')],
widget=forms.RadioSelect(attrs={
'class': 'form-check-input',
})
)
# 多选复选框
tags = forms.MultipleChoiceField(
choices=[('python', 'Python'), ('django', 'Django')],
widget=forms.CheckboxSelectMultiple(attrs={
'class': 'form-check-input',
})
)
# 多选下拉
categories = forms.MultipleChoiceField(
choices=[('tech', '技术'), ('life', '生活')],
widget=forms.SelectMultiple(attrs={
'class': 'form-control',
})
)
# 文件上传
file = forms.FileField(
widget=forms.FileInput(attrs={
'class': 'form-control-file',
})
)
# 清除文件复选框
clear_file = forms.BooleanField(
required=False,
widget=forms.CheckboxInput(),
)
表单验证
内置验证器
from django import forms
from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.validators import RegexValidator, EmailValidator
class ValidationForm(forms.Form):
# 必填验证
required_field = forms.CharField(required=True)
# 长度验证
length_field = forms.CharField(
min_length=5,
max_length=20,
)
# 数值范围验证
number_field = forms.IntegerField(
validators=[
MinValueValidator(0),
MaxValueValidator(100),
]
)
# 正则表达式验证
phone_field = forms.CharField(
validators=[
RegexValidator(
regex=r'^1[3-9]\d{9}$',
message='请输入有效的手机号码',
)
]
)
# 邮箱验证
email_field = forms.EmailField(
validators=[EmailValidator()]
)
自定义验证方法
class CustomValidationForm(forms.Form):
password = forms.CharField()
confirm_password = forms.CharField()
def clean_password(self):
"""验证单个字段"""
password = self.cleaned_data.get('password')
# 密码长度检查
if len(password) < 8:
raise forms.ValidationError('密码至少需要 8 个字符')
# 密码复杂度检查
if not any(c.isupper() for c in password):
raise forms.ValidationError('密码必须包含大写字母')
if not any(c.isdigit() for c in password):
raise forms.ValidationError('密码必须包含数字')
return password
def clean(self):
"""验证多个字段"""
cleaned_data = super().clean()
password = cleaned_data.get('password')
confirm_password = cleaned_data.get('confirm_password')
if password and confirm_password and password != confirm_password:
raise forms.ValidationError('两次输入的密码不一致')
return cleaned_data
自定义验证器
from django.core.exceptions import ValidationError
def validate_even(value):
"""验证是否为偶数"""
if value % 2 != 0:
raise ValidationError('%(value)s 不是偶数', params={'value': value})
def validate_phone(value):
"""验证手机号"""
import re
if not re.match(r'^1[3-9]\d{9}$', value):
raise ValidationError('请输入有效的手机号码')
class CustomValidatorForm(forms.Form):
even_number = forms.IntegerField(validators=[validate_even])
phone = forms.CharField(validators=[validate_phone])
处理文件上传
# forms.py
class FileUploadForm(forms.Form):
title = forms.CharField(max_length=100)
file = forms.FileField()
image = forms.ImageField(required=False)
def clean_file(self):
"""验证文件"""
file = self.cleaned_data.get('file')
# 检查文件大小(最大 5MB)
if file.size > 5 * 1024 * 1024:
raise forms.ValidationError('文件大小不能超过 5MB')
# 检查文件类型
allowed_types = ['application/pdf', 'text/plain']
if file.content_type not in allowed_types:
raise forms.ValidationError('只允许上传 PDF 或文本文件')
return file
def clean_image(self):
"""验证图片"""
image = self.cleaned_data.get('image')
if image:
# 检查图片大小
if image.size > 2 * 1024 * 1024:
raise forms.ValidationError('图片大小不能超过 2MB')
# 检查图片尺寸
from PIL import Image
img = Image.open(image)
if img.width < 200 or img.height < 200:
raise forms.ValidationError('图片尺寸至少为 200x200 像素')
return image
# views.py
def upload_file(request):
if request.method == 'POST':
form = FileUploadForm(request.POST, request.FILES)
if form.is_valid():
# 保存文件
file = request.FILES['file']
# 方式一:保存到 MEDIA 目录
from django.core.files.storage import default_storage
path = default_storage.save(f'uploads/{file.name}', file)
# 方式二:保存到模型
# document = Document.objects.create(
# title=form.cleaned_data['title'],
# file=file
# )
return redirect('upload_success')
else:
form = FileUploadForm()
return render(request, 'upload.html', {'form': form})
模板中需要设置 enctype:
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form }}
<button type="submit">上传</button>
</form>
表单集(Formsets)
表单集用于处理多个相同类型的表单:
from django.forms import formset_factory
class ItemForm(forms.Form):
"""商品表单"""
name = forms.CharField(max_length=100)
quantity = forms.IntegerField(min_value=1)
price = forms.DecimalField(max_digits=10, decimal_places=2)
# 创建表单集
ItemFormSet = formset_factory(ItemForm, extra=3, max_num=10)
def order_form(request):
"""订单表单"""
if request.method == 'POST':
formset = ItemFormSet(request.POST)
if formset.is_valid():
for form in formset:
name = form.cleaned_data['name']
quantity = form.cleaned_data['quantity']
price = form.cleaned_data['price']
# 处理每个商品...
return redirect('order_success')
else:
formset = ItemFormSet()
return render(request, 'order.html', {'formset': formset})
模板:
<form method="post">
{% csrf_token %}
{{ formset.management_form }}
<table>
<thead>
<tr>
<th>商品名称</th>
<th>数量</th>
<th>价格</th>
</tr>
</thead>
<tbody>
{% for form in formset %}
<tr>
<td>{{ form.name }}</td>
<td>{{ form.quantity }}</td>
<td>{{ form.price }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<button type="submit">提交订单</button>
</form>
表单最佳实践
1. 使用表单类而不是手动处理
# 不好的做法
def bad_view(request):
if request.method == 'POST':
name = request.POST.get('name')
email = request.POST.get('email')
# 手动验证...
if not name:
errors = {'name': '姓名必填'}
# ...
# 好的做法
def good_view(request):
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid():
# 处理验证后的数据
pass
2. 使用 Crispy Forms 美化表单
安装:
pip install django-crispy-forms
pip install crispy-bootstrap5
配置:
# settings.py
INSTALLED_APPS = [
# ...
'crispy_forms',
'crispy_bootstrap5',
]
CRISPY_ALLOWED_TEMPLATE_PACKS = 'bootstrap5'
CRISPY_TEMPLATE_PACK = 'bootstrap5'
使用:
{% load crispy_forms_tags %}
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button type="submit">提交</button>
</form>
3. 统一表单样式
# forms.py
class StyledForm(forms.Form):
"""带统一样式的表单基类"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 为所有字段添加样式
for field_name, field in self.fields.items():
if isinstance(field.widget, forms.CheckboxInput):
field.widget.attrs['class'] = 'form-check-input'
elif isinstance(field.widget, forms.Select):
field.widget.attrs['class'] = 'form-select'
else:
field.widget.attrs['class'] = 'form-control'