Skip to content

第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高级表单特性提供了强大的功能:

  1. ✅ 表单集支持批量创建和编辑
  2. ✅ 内联表单集处理模型关系
  3. ✅ 文件上传处理和验证
  4. ✅ CSRF保护机制防止攻击
  5. ✅ 灵活的自定义验证和错误处理

掌握这些高级特性能够构建更复杂和安全的表单应用。

下一篇

我们将学习Django用户认证系统。

9.1 认证系统 →

目录

返回课程目录

Released under the Apache 2.0 License.