Django Form表单处理详解

课程目标

  • 理解Django表单系统的工作原理
  • 掌握表单定义和字段类型的使用
  • 学会表单验证和错误处理
  • 了解ModelForm的使用方法

表单系统概述

Django表单系统提供了一套强大而灵活的机制来处理HTML表单。它不仅生成HTML表单,还能验证用户提交的数据,处理错误信息,并与Model无缝集成。

表单的主要功能:

  • 生成HTML表单
  • 数据验证
  • 错误处理
  • 与Model集成

基础表单定义

简单表单示例

from django import forms

class ContactForm(forms.Form):
    name = forms.CharField(max_length=100, label='姓名')
    email = forms.EmailField(label='邮箱')
    subject = forms.CharField(max_length=200, label='主题')
    message = forms.CharField(widget=forms.Textarea, label='消息')
    subscribe = forms.BooleanField(required=False, label='订阅邮件')

在模板中使用表单

<!-- contact.html -->
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">提交</button>
</form>

在视图中处理表单

from django.shortcuts import render, redirect
from django.contrib import messages
from .forms import ContactForm

def contact_view(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_email(name, email, subject, message)
            
            messages.success(request, '消息发送成功!')
            return redirect('contact')
    else:
        form = ContactForm()
    
    return render(request, 'contact.html', {'form': form})

表单字段类型

常用字段类型

class UserRegistrationForm(forms.Form):
    # 文本字段
    username = forms.CharField(
        max_length=150,
        min_length=3,
        label='用户名',
        help_text='3-150个字符'
    )
    
    # 邮箱字段
    email = forms.EmailField(label='邮箱地址')
    
    # 密码字段
    password = forms.CharField(
        widget=forms.PasswordInput,
        min_length=8,
        label='密码'
    )
    
    # 确认密码字段
    password_confirm = forms.CharField(
        widget=forms.PasswordInput,
        label='确认密码'
    )
    
    # 选择字段
    gender = forms.ChoiceField(
        choices=[
            ('M', '男'),
            ('F', '女'),
            ('O', '其他')
        ],
        label='性别'
    )
    
    # 多选字段
    hobbies = forms.MultipleChoiceField(
        choices=[
            ('reading', '阅读'),
            ('sports', '运动'),
            ('music', '音乐'),
            ('travel', '旅行')
        ],
        widget=forms.CheckboxSelectMultiple,
        label='兴趣爱好'
    )
    
    # 文件字段
    avatar = forms.ImageField(required=False, label='头像')
    
    # 日期字段
    birth_date = forms.DateField(
        widget=forms.DateInput(attrs={'type': 'date'}),
        label='出生日期'
    )
    
    # 布尔字段
    agree_terms = forms.BooleanField(label='同意服务条款')

表单验证

字段验证

class ArticleForm(forms.Form):
    title = forms.CharField(max_length=200, label='标题')
    content = forms.CharField(widget=forms.Textarea, label='内容')
    category = forms.CharField(max_length=50, label='分类')
    tags = forms.CharField(max_length=200, required=False, label='标签')
    
    def clean_title(self):
        """验证标题"""
        title = self.cleaned_data['title']
        if len(title) < 5:
            raise forms.ValidationError('标题至少需要5个字符')
        
        # 检查标题是否已存在
        if Article.objects.filter(title=title).exists():
            raise forms.ValidationError('该标题已存在')
        
        return title
    
    def clean_content(self):
        """验证内容"""
        content = self.cleaned_data['content']
        if len(content) < 10:
            raise forms.ValidationError('内容至少需要10个字符')
        return content
    
    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

自定义验证器

from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _

def validate_even(value):
    """验证偶数"""
    if value % 2 != 0:
        raise ValidationError(
            _('%(value)s 不是偶数'),
            params={'value': value},
        )

def validate_file_extension(value):
    """验证文件扩展名"""
    import os
    ext = os.path.splitext(value.name)[1]
    valid_extensions = ['.pdf', '.doc', '.docx', '.txt']
    if not ext.lower() in valid_extensions:
        raise ValidationError('不支持的文件格式')

class DocumentForm(forms.Form):
    title = forms.CharField(max_length=100)
    document = forms.FileField(validators=[validate_file_extension])
    number = forms.IntegerField(validators=[validate_even])

Widget自定义

自定义Widget外观

class SearchForm(forms.Form):
    query = forms.CharField(
        max_length=200,
        widget=forms.TextInput(attrs={
            'class': 'form-control',
            'placeholder': '搜索...',
            'id': 'search-input'
        }),
        label='搜索'
    )
    
    category = forms.ChoiceField(
        choices=[('all', '全部'), ('news', '新闻'), ('blog', '博客')],
        widget=forms.Select(attrs={
            'class': 'form-select'
        }),
        label='分类'
    )
    
    date_from = forms.DateField(
        widget=forms.DateInput(attrs={
            'type': 'date',
            'class': 'form-control'
        }),
        required=False,
        label='开始日期'
    )
    
    date_to = forms.DateField(
        widget=forms.DateInput(attrs={
            'type': 'date',
            'class': 'form-control'
        }),
        required=False,
        label='结束日期'
    )

ModelForm使用

基本ModelForm

from django import forms
from .models import Article

class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ['title', 'content', 'category', 'tags', 'status']
        exclude = ['author', 'created_at']  # 排除某些字段
        
        widgets = {
            'title': forms.TextInput(attrs={
                'class': 'form-control',
                'placeholder': '请输入文章标题'
            }),
            'content': forms.Textarea(attrs={
                'class': 'form-control',
                'rows': 10,
                'placeholder': '请输入文章内容'
            }),
            'status': forms.Select(attrs={
                'class': 'form-select'
            })
        }
        
        labels = {
            'title': '文章标题',
            'content': '文章内容',
            'category': '分类',
            'tags': '标签',
            'status': '状态'
        }
        
        help_texts = {
            'title': '文章标题,最多200个字符',
            'tags': '用逗号分隔多个标签'
        }
        
        error_messages = {
            'title': {
                'max_length': '标题太长了!',
            },
        }

ModelForm验证

class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ['title', 'content', 'category', 'tags', 'status']
    
    def clean_title(self):
        """验证标题唯一性"""
        title = self.cleaned_data['title']
        # 检查除当前实例外是否已存在相同标题
        if Article.objects.filter(title=title).exclude(pk=self.instance.pk).exists():
            raise forms.ValidationError('该标题已存在')
        return title
    
    def clean(self):
        """验证发布状态"""
        cleaned_data = super().clean()
        status = cleaned_data.get('status')
        content = cleaned_data.get('content')
        
        if status == 'published' and len(content) < 100:
            raise forms.ValidationError('发布状态的文章内容至少需要100个字符')
        
        return cleaned_data

表单集(Formsets)

基础表单集

from django.forms import formset_factory

class AuthorForm(forms.Form):
    name = forms.CharField(max_length=100)
    email = forms.EmailField()

# 创建表单集
AuthorFormSet = formset_factory(AuthorForm, extra=2)

# 在视图中使用
def manage_authors(request):
    if request.method == 'POST':
        formset = AuthorFormSet(request.POST)
        if formset.is_valid():
            for form in formset:
                if form.cleaned_data:
                    # 处理每个表单的数据
                    name = form.cleaned_data['name']
                    email = form.cleaned_data['email']
                    # 保存数据
    else:
        formset = AuthorFormSet()
    
    return render(request, 'manage_authors.html', {'formset': formset})

ModelForm表单集

from django.forms import modelformset_factory
from .models import Article

# 创建Article的表单集
ArticleFormSet = modelformset_factory(
    Article, 
    fields=['title', 'content', 'status'],
    extra=1,
    can_delete=True  # 允许删除
)

def bulk_edit_articles(request):
    ArticleFormSet = modelformset_factory(
        Article,
        fields=['title', 'content', 'status'],
        extra=0,
        can_delete=True
    )
    
    if request.method == 'POST':
        formset = ArticleFormSet(request.POST)
        if formset.is_valid():
            instances = formset.save()
            return redirect('article_list')
    else:
        queryset = Article.objects.filter(author=request.user)[:10]
        formset = ArticleFormSet(queryset=queryset)
    
    return render(request, 'bulk_edit.html', {'formset': formset})

表单处理最佳实践

视图中的表单处理

from django.shortcuts import render, get_object_or_404, redirect
from django.contrib import messages
from django.contrib.auth.decorators import login_required

@login_required
def create_article(request):
    if request.method == 'POST':
        form = ArticleForm(request.POST)
        if form.is_valid():
            article = form.save(commit=False)  # 先不保存到数据库
            article.author = request.user      # 设置作者
            article.save()                     # 保存到数据库
            messages.success(request, '文章创建成功!')
            return redirect('article_detail', pk=article.pk)
        else:
            messages.error(request, '请修正表单中的错误')
    else:
        form = ArticleForm()
    
    return render(request, 'articles/create.html', {'form': form})

@login_required
def edit_article(request, pk):
    article = get_object_or_404(Article, pk=pk)
    
    # 检查权限
    if article.author != request.user:
        messages.error(request, '您没有权限编辑此文章')
        return redirect('article_list')
    
    if request.method == 'POST':
        form = ArticleForm(request.POST, instance=article)
        if form.is_valid():
            form.save()
            messages.success(request, '文章更新成功!')
            return redirect('article_detail', pk=article.pk)
    else:
        form = ArticleForm(instance=article)
    
    return render(request, 'articles/edit.html', {
        'form': form,
        'article': article
    })

表单模板最佳实践

<!-- articles/form.html -->
<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    
    {% if form.non_field_errors %}
        <div class="alert alert-danger">
            <ul>
            {% for error in form.non_field_errors %}
                <li>{{ error }}</li>
            {% endfor %}
            </ul>
        </div>
    {% endif %}
    
    <div class="mb-3">
        {{ form.title.label_tag }}
        {{ form.title }}
        {% if form.title.help_text %}
            <small class="form-text text-muted">{{ form.title.help_text }}</small>
        {% endif %}
        {% if form.title.errors %}
            <div class="text-danger">{{ form.title.errors }}</div>
        {% endif %}
    </div>
    
    <div class="mb-3">
        {{ form.content.label_tag }}
        {{ form.content }}
        {% if form.content.errors %}
            <div class="text-danger">{{ form.content.errors }}</div>
        {% endif %}
    </div>
    
    <button type="submit" class="btn btn-primary">提交</button>
    <a href="{% url 'article_list' %}" class="btn btn-secondary">取消</a>
</form>

安全考虑

CSRF保护

# 在表单中始终包含CSRF令牌
"""
<form method="post">
    {% csrf_token %}
    <!-- 表单字段 -->
</form>
"""

# 在AJAX请求中包含CSRF令牌
"""
$.ajaxSetup({
    beforeSend: function(xhr, settings) {
        if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
            xhr.setRequestHeader("X-CSRFToken", $('[name=csrfmiddlewaretoken]').val());
        }
    }
});
"""

表单安全验证

class SecureContactForm(forms.Form):
    name = forms.CharField(max_length=100)
    email = forms.EmailField()
    message = forms.CharField(widget=forms.Textarea)
    
    def clean_message(self):
        """防止垃圾信息"""
        message = self.cleaned_data['message']
        
        # 检查是否包含垃圾信息关键词
        spam_keywords = ['viagra', 'casino', 'lottery', 'money']
        for keyword in spam_keywords:
            if keyword.lower() in message.lower():
                raise forms.ValidationError('消息内容包含敏感词汇')
        
        # 检查链接数量(防止垃圾邮件)
        import re
        links = re.findall(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', message)
        if len(links) > 2:
            raise forms.ValidationError('消息中包含过多链接')
        
        return message

课程总结

本节课我们学习了Django表单系统的各个方面,包括表单定义、字段类型、验证机制、ModelForm使用等。表单处理是Web应用开发中的重要环节,掌握Django的表单系统对于构建用户友好的应用至关重要。