模型表单
ModelForm 是 Django 提供的一个强大工具,它可以根据模型自动生成表单,大大简化了表单的创建和处理过程。
ModelForm 基础
创建 ModelForm
# models.py
from django.db import models
from django.contrib.auth.models import User
class Post(models.Model):
STATUS_CHOICES = [
('draft', '草稿'),
('published', '已发布'),
]
title = models.CharField('标题', max_length=200)
slug = models.SlugField('URL 别名', unique_for_date='publish')
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='posts',
verbose_name='作者'
)
content = models.TextField('内容')
status = models.CharField('状态', max_length=10, choices=STATUS_CHOICES, default='draft')
category = models.ForeignKey('Category', on_delete=models.SET_NULL, null=True, blank=True)
tags = models.ManyToManyField('Tag', blank=True)
publish = models.DateTimeField('发布时间', auto_now_add=True)
def __str__(self):
return self.title
# forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
"""文章表单"""
class Meta:
model = Post
fields = ['title', 'slug', 'content', 'status', 'category', 'tags']
labels = {
'title': '文章标题',
'slug': 'URL 别名',
'content': '文章内容',
}
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
'slug': forms.TextInput(attrs={'class': 'form-control'}),
'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
'status': forms.Select(attrs={'class': 'form-select'}),
}
Meta 类选项
class PostForm(forms.ModelForm):
class Meta:
model = Post
# 指定包含的字段
fields = ['title', 'content', 'status']
# 或排除某些字段
# exclude = ['author', 'publish']
# 使用所有字段
# fields = '__all__'
# 标签
labels = {
'title': '标题',
'content': '内容',
}
# 帮助文本
help_texts = {
'slug': '用于生成 URL,只能包含字母、数字和连字符',
}
# 错误消息
error_messages = {
'title': {
'required': '请输入标题',
'max_length': '标题不能超过 200 个字符',
},
}
# 小部件
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
'content': forms.Textarea(attrs={'class': 'form-control'}),
}
# 字段顺序
field_classes = {
'title': forms.CharField,
}
# 本地化
localized_fields = ['publish']
在视图中使用 ModelForm
# views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from .forms import PostForm
from .models import Post
@login_required
def post_create(request):
"""创建文章"""
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
# commit=False 不立即保存到数据库
post = form.save(commit=False)
post.author = request.user
post.save()
# 保存多对多关系
form.save_m2m()
return redirect('blog:post_detail', pk=post.pk)
else:
form = PostForm()
return render(request, 'blog/post_form.html', {'form': form})
@login_required
def post_update(request, pk):
"""更新文章"""
post = get_object_or_404(Post, pk=pk, author=request.user)
if request.method == 'POST':
form = PostForm(request.POST, instance=post)
if form.is_valid():
form.save()
return redirect('blog:post_detail', pk=post.pk)
else:
form = PostForm(instance=post)
return render(request, 'blog/post_form.html', {'form': form, 'post': post})
@login_required
def post_delete(request, pk):
"""删除文章"""
post = get_object_or_404(Post, pk=pk, author=request.user)
if request.method == 'POST':
post.delete()
return redirect('blog:post_list')
return render(request, 'blog/post_confirm_delete.html', {'post': post})
自定义 ModelForm
添加额外字段
class PostForm(forms.ModelForm):
"""带额外字段的文章表单"""
# 添加确认发布字段
confirm_publish = forms.BooleanField(
label='确认发布',
required=False,
help_text='勾选后将直接发布文章'
)
# 添加验证码字段
captcha = forms.CharField(label='验证码', max_length=6)
class Meta:
model = Post
fields = ['title', 'content', 'status', 'category', 'tags']
def clean_captcha(self):
captcha = self.cleaned_data.get('captcha')
# 验证验证码...
return captcha
覆盖字段
class PostForm(forms.ModelForm):
"""覆盖模型字段"""
# 覆盖 title 字段,添加更多验证
title = forms.CharField(
label='标题',
max_length=200,
min_length=5,
widget=forms.TextInput(attrs={'class': 'form-control'}),
error_messages={
'required': '请输入标题',
'min_length': '标题至少需要 5 个字符',
}
)
class Meta:
model = Post
fields = ['title', 'content', 'status']
自定义验证
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'slug', 'content', 'status']
def clean_slug(self):
"""验证 slug"""
slug = self.cleaned_data.get('slug')
# 检查是否已存在(排除当前实例)
if self.instance:
if Post.objects.exclude(pk=self.instance.pk).filter(slug=slug).exists():
raise forms.ValidationError('该 URL 别名已存在')
else:
if Post.objects.filter(slug=slug).exists():
raise forms.ValidationError('该 URL 别名已存在')
return slug
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
def clean_title(self):
"""验证标题"""
title = self.cleaned_data.get('title')
# 禁止敏感词
forbidden_words = ['广告', '推销']
for word in forbidden_words:
if word in title:
raise forms.ValidationError(f'标题不能包含敏感词:{word}')
return title
自定义保存逻辑
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content', 'status', 'category', 'tags']
def save(self, commit=True):
"""自定义保存逻辑"""
post = super().save(commit=False)
# 自动生成 slug
if not post.slug:
from django.utils.text import slugify
post.slug = slugify(post.title)
# 自动设置发布时间
if post.status == 'published' and not post.publish:
from django.utils import timezone
post.publish = timezone.now()
if commit:
post.save()
self.save_m2m()
return post
处理文件上传
# models.py
class Document(models.Model):
title = models.CharField(max_length=100)
file = models.FileField(upload_to='documents/')
image = models.ImageField(upload_to='images/', blank=True)
# forms.py
class DocumentForm(forms.ModelForm):
class Meta:
model = Document
fields = ['title', 'file', 'image']
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
}
def clean_file(self):
file = self.cleaned_data.get('file')
if file:
# 检查文件大小
if file.size > 10 * 1024 * 1024:
raise forms.ValidationError('文件大小不能超过 10MB')
return file
def clean_image(self):
image = self.cleaned_data.get('image')
if image:
# 检查图片大小
if image.size > 5 * 1024 * 1024:
raise forms.ValidationError('图片大小不能超过 5MB')
return image
# views.py
def upload_document(request):
if request.method == 'POST':
form = DocumentForm(request.POST, request.FILES)
if form.is_valid():
form.save()
return redirect('document_list')
else:
form = DocumentForm()
return render(request, 'upload.html', {'form': form})
模型表单集
ModelForm 可以创建表单集来处理多个模型实例:
from django.forms import modelformset_factory
from .models import Post
# 创建模型表单集
PostFormSet = modelformset_factory(
Post,
fields=['title', 'status'],
extra=3, # 额外空白表单数量
can_delete=True, # 允许删除
)
def manage_posts(request):
"""批量管理文章"""
if request.method == 'POST':
formset = PostFormSet(request.POST)
if formset.is_valid():
formset.save()
return redirect('post_list')
else:
formset = PostFormSet()
return render(request, 'manage_posts.html', {'formset': formset})
限制查询集
# 只显示当前用户的文章
PostFormSet = modelformset_factory(
Post,
fields=['title', 'status'],
extra=0,
)
def manage_user_posts(request):
formset = PostFormSet(
queryset=Post.objects.filter(author=request.user)
)
return render(request, 'manage_posts.html', {'formset': formset})
内联表单集
内联表单集用于处理相关模型的多个实例:
# models.py
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='books')
title = models.CharField(max_length=200)
isbn = models.CharField(max_length=13)
# forms.py
from django.forms import inlineformset_factory
class BookForm(forms.ModelForm):
class Meta:
model = Book
fields = ['title', 'isbn']
# 创建内联表单集
BookInlineFormSet = inlineformset_factory(
Author,
Book,
form=BookForm,
extra=2,
can_delete=True,
)
# views.py
def manage_author_books(request, author_id):
author = get_object_or_404(Author, pk=author_id)
if request.method == 'POST':
formset = BookInlineFormSet(request.POST, instance=author)
if formset.is_valid():
formset.save()
return redirect('author_detail', pk=author.pk)
else:
formset = BookInlineFormSet(instance=author)
return render(request, 'manage_books.html', {
'author': author,
'formset': formset,
})
完整示例:博客文章管理
# forms.py
from django import forms
from .models import Post, Category, Tag
class PostForm(forms.ModelForm):
"""文章表单"""
class Meta:
model = Post
fields = ['title', 'slug', 'content', 'status', 'category', 'tags']
widgets = {
'title': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '请输入文章标题'
}),
'slug': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '自动生成或手动输入'
}),
'content': forms.Textarea(attrs={
'class': 'form-control',
'rows': 15,
'placeholder': '支持 Markdown 格式'
}),
'status': forms.Select(attrs={'class': 'form-select'}),
'category': forms.Select(attrs={'class': 'form-select'}),
'tags': forms.SelectMultiple(attrs={'class': 'form-select'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 动态设置选项
self.fields['category'].queryset = Category.objects.all()
self.fields['category'].empty_label = '选择分类'
# 添加帮助文本
self.fields['slug'].help_text = '留空将自动根据标题生成'
def clean_title(self):
title = self.cleaned_data.get('title')
if len(title) < 5:
raise forms.ValidationError('标题至少需要 5 个字符')
return title
def save(self, commit=True, author=None):
post = super().save(commit=False)
if author:
post.author = author
# 自动生成 slug
if not post.slug:
from django.utils.text import slugify
base_slug = slugify(post.title)
slug = base_slug
counter = 1
while Post.objects.filter(slug=slug).exists():
slug = f'{base_slug}-{counter}'
counter += 1
post.slug = slug
if commit:
post.save()
self.save_m2m()
return post
# views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .forms import PostForm
from .models import Post
@login_required
def post_create(request):
"""创建文章"""
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=True, author=request.user)
messages.success(request, '文章创建成功!')
return redirect('blog:post_detail', pk=post.pk)
else:
form = PostForm()
return render(request, 'blog/post_form.html', {
'form': form,
'title': '创建文章',
})
@login_required
def post_update(request, pk):
"""更新文章"""
post = get_object_or_404(Post, pk=pk)
# 权限检查
if post.author != request.user and not request.user.is_staff:
messages.error(request, '您没有权限编辑此文章')
return redirect('blog:post_detail', pk=post.pk)
if request.method == 'POST':
form = PostForm(request.POST, instance=post)
if form.is_valid():
form.save()
messages.success(request, '文章更新成功!')
return redirect('blog:post_detail', pk=post.pk)
else:
form = PostForm(instance=post)
return render(request, 'blog/post_form.html', {
'form': form,
'post': post,
'title': '编辑文章',
})
模板:
<!-- templates/blog/post_form.html -->
{% extends 'base.html' %}
{% block content %}
<div class="container">
<h1>{{ title }}</h1>
<form method="post" novalidate>
{% csrf_token %}
{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors }}
</div>
{% endif %}
{% for field in form %}
<div class="mb-3">
<label for="{{ field.id_for_label }}" class="form-label">
{{ field.label }}
{% if field.field.required %}<span class="text-danger">*</span>{% endif %}
</label>
{{ field }}
{% if field.help_text %}
<div class="form-text">{{ field.help_text }}</div>
{% endif %}
{% if field.errors %}
<div class="invalid-feedback d-block">
{% for error in field.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
<div class="mb-3">
<button type="submit" class="btn btn-primary">保存</button>
<a href="{% url 'blog:post_list' %}" class="btn btn-secondary">取消</a>
</div>
</form>
</div>
{% endblock %}