跳到主要内容

表单基础

表单是 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'