第8章:Django表单(Forms)
8.3 高级表单特性
表单集(Formsets)
表单集是同一类型表单的集合,允许用户在单个页面上创建或编辑多个相关对象。
基本表单集使用:
python
# forms.py
from django import forms
class ArticleForm(forms.Form):
title = forms.CharField(max_length=100)
content = forms.CharField(widget=forms.Textarea)
# 使用表单集
from django.forms import formset_factory
# 创建表单集类
ArticleFormSet = formset_factory(ArticleForm, extra=2)
# 在视图中使用
def create_articles(request):
if request.method == 'POST':
formset = ArticleFormSet(request.POST)
if formset.is_valid():
for form in formset:
if form.cleaned_data:
# 处理每个表单的数据
title = form.cleaned_data['title']
content = form.cleaned_data['content']
# 保存到数据库
return redirect('success')
else:
formset = ArticleFormSet()
return render(request, 'create_articles.html', {'formset': formset})
<!-- create_articles.html -->
<form method="post">
{% csrf_token %}
{{ formset.management_form }}
{% for form in formset %}
<div class="article-form">
<h3>文章 {{ forloop.counter }}</h3>
{{ form.as_p }}
</div>
{% endfor %}
<button type="submit">提交</button>
</form>ModelForm表单集:
python
# models.py
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
email = models.EmailField()
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
publication_date = models.DateField()
# forms.py
from django import forms
from .models import Book
class BookForm(forms.ModelForm):
class Meta:
model = Book
fields = ['title', 'publication_date']
# 创建ModelForm表单集
from django.forms import modelformset_factory
BookFormSet = modelformset_factory(
Book,
form=BookForm,
extra=1,
can_delete=True # 允许删除
)
# 在视图中使用
def edit_books(request, author_id):
author = get_object_or_404(Author, id=author_id)
BookFormSet = modelformset_factory(
Book,
form=BookForm,
extra=1,
can_delete=True
)
if request.method == 'POST':
formset = BookFormSet(request.POST, queryset=author.book_set.all())
if formset.is_valid():
instances = formset.save(commit=False)
for instance in instances:
instance.author = author
instance.save()
formset.save_m2m()
return redirect('author_detail', author_id=author.id)
else:
formset = BookFormSet(queryset=author.book_set.all())
return render(request, 'edit_books.html', {
'formset': formset,
'author': author
})内联表单集
内联表单集用于处理主模型和相关模型之间的关系,特别适用于一对多关系。
python
# models.py
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
email = models.EmailField()
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
publication_date = models.DateField()
# forms.py
from django.forms import inlineformset_factory
# 创建内联表单集
BookInlineFormSet = inlineformset_factory(
Author,
Book,
fields=['title', 'publication_date'],
extra=1,
can_delete=True
)
# views.py
def edit_author_books(request, author_id):
author = get_object_or_404(Author, id=author_id)
if request.method == 'POST':
formset = BookInlineFormSet(request.POST, instance=author)
if formset.is_valid():
formset.save()
return redirect('author_detail', author_id=author.id)
else:
formset = BookInlineFormSet(instance=author)
return render(request, 'edit_author_books.html', {
'formset': formset,
'author': author
})
<!-- edit_author_books.html -->
<form method="post">
{% csrf_token %}
<h2>编辑作者:{{ author.name }}</h2>
{{ formset.management_form }}
{% for form in formset %}
<div class="book-form">
{% if form.non_field_errors %}
<div class="alert alert-danger">{{ form.non_field_errors }}</div>
{% endif %}
{% for field in form %}
<div class="mb-3">
{{ field.label_tag }}
{{ field }}
{% if field.errors %}
<div class="text-danger">{{ field.errors }}</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endfor %}
<button type="submit" class="btn btn-primary">保存</button>
<a href="{% url 'author_detail' author.id %}" class="btn btn-secondary">取消</a>
</form>文件上传处理
Django提供了强大的文件上传处理功能:
python
# models.py
from django.db import models
class Document(models.Model):
title = models.CharField(max_length=200)
file = models.FileField(upload_to='documents/')
uploaded_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.title
# forms.py
from django import forms
from .models import Document
class DocumentForm(forms.ModelForm):
class Meta:
model = Document
fields = ['title', 'file']
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
'file': forms.FileInput(attrs={'class': 'form-control'})
}
# 处理不同类型的文件上传
class ImageUploadForm(forms.Form):
title = forms.CharField(max_length=100)
image = forms.ImageField()
def clean_image(self):
image = self.cleaned_data['image']
if image:
# 验证文件大小
if image.size > 5 * 1024 * 1024: # 5MB
raise forms.ValidationError("图片大小不能超过5MB")
# 验证文件类型
if not image.content_type.startswith('image/'):
raise forms.ValidationError("请上传有效的图片文件")
return image
# views.py
from django.shortcuts import render, redirect
from django.conf import settings
import os
def upload_document(request):
if request.method == 'POST':
form = DocumentForm(request.POST, request.FILES)
if form.is_valid():
document = form.save()
return redirect('document_list')
else:
form = DocumentForm()
return render(request, 'upload_document.html', {'form': form})
# 处理多个文件上传
class MultipleFileField(forms.FileField):
def __init__(self, *args, **kwargs):
kwargs.setdefault("widget", forms.ClearableFileInput(attrs={"multiple": True}))
super().__init__(*args, **kwargs)
class MultipleFileForm(forms.Form):
files = MultipleFileField()
def clean_files(self):
files = self.files.getlist('files')
for file in files:
if file.size > 10 * 1024 * 1024: # 10MB
raise forms.ValidationError("单个文件大小不能超过10MB")
return files
def upload_multiple_files(request):
if request.method == 'POST':
form = MultipleFileForm(request.POST, request.FILES)
if form.is_valid():
files = request.FILES.getlist('files')
for file in files:
# 保存每个文件
Document.objects.create(
title=file.name,
file=file
)
return redirect('document_list')
else:
form = MultipleFileForm()
return render(request, 'upload_multiple.html', {'form': form})文件上传模板示例:
<!-- upload_document.html -->
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="mb-3">
{{ form.title.label_tag }}
{{ form.title }}
{% if form.title.errors %}
<div class="text-danger">{{ form.title.errors }}</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.file.label_tag }}
{{ form.file }}
{% if form.file.errors %}
<div class="text-danger">{{ form.file.errors }}</div>
{% endif %}
<div class="form-text">支持的文件类型:PDF, DOC, DOCX, TXT</div>
</div>
<button type="submit" class="btn btn-primary">上传</button>
</form>
<!-- 显示上传的文件 -->
{% for document in documents %}
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">{{ document.title }}</h5>
<p class="card-text">
上传时间:{{ document.uploaded_at|date:"Y-m-d H:i" }}
</p>
<a href="{{ document.file.url }}" class="btn btn-primary" target="_blank">
下载文件
</a>
</div>
</div>
{% endfor %}CSRF保护
Django内置了CSRF(跨站请求伪造)保护机制:
<!-- 在表单中使用CSRF令牌 -->
<form method="post">
{% csrf_token %}
<!-- 表单字段 -->
<input type="text" name="username">
<input type="password" name="password">
<button type="submit">登录</button>
</form>在AJAX请求中使用CSRF令牌:
// 方法1:使用jQuery
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!(/^http:.*/.test(settings.url) || /^https:.*/.test(settings.url))) {
// 只发送本地请求的CSRF令牌
xhr.setRequestHeader("X-CSRFToken", $('[name=csrfmiddlewaretoken]').val());
}
}
});
// 方法2:从cookie获取CSRF令牌
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
const csrftoken = getCookie('csrftoken');
$.ajax({
url: '/api/endpoint/',
type: 'POST',
headers: {
'X-CSRFToken': csrftoken
},
data: {
// 表单数据
},
success: function(data) {
// 处理成功响应
}
});在Django设置中配置CSRF:
# settings.py
MIDDLEWARE = [
# ... 其他中间件
'django.middleware.csrf.CsrfViewMiddleware',
# ... 其他中间件
]
# CSRF设置
CSRF_COOKIE_HTTPONLY = True # 防止JavaScript访问CSRF cookie
CSRF_COOKIE_SECURE = True # 仅在HTTPS下发送CSRF cookie
CSRF_TRUSTED_ORIGINS = [
'https://yourdomain.com',
'https://www.yourdomain.com',
]自定义CSRF失败处理:
# views.py
from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse
# 免除CSRF保护(谨慎使用)
@csrf_exempt
def api_view(request):
if request.method == 'POST':
# 处理API请求
return JsonResponse({'status': 'success'})
return JsonResponse({'status': 'error'})
# 自定义CSRF失败处理
from django.views.decorators.csrf import requires_csrf_token
@requires_csrf_token
def custom_csrf_failure(request, reason=""):
return render(request, 'csrf_failure.html', {
'reason': reason
})这些高级表单特性使Django能够处理复杂的表单场景,包括批量操作、文件上传和安全保护。
小结
Django高级表单特性提供了强大的功能:
- ✅ 表单集支持批量创建和编辑
- ✅ 内联表单集处理模型关系
- ✅ 文件上传处理和验证
- ✅ CSRF保护机制防止攻击
- ✅ 灵活的自定义验证和错误处理
掌握这些高级特性能够构建更复杂和安全的表单应用。
下一篇
我们将学习Django用户认证系统。