Django 表单处理:ModelForm 与自定义验证

春秋大王2025-10-1057 次阅读
Django 表单处理:ModelForm 与自定义验证


Django 表单是处理用户输入的核心工具,ModelForm能自动关联数据库模型生成表单,配合自定义验证逻辑,可大幅减少重复代码,同时确保数据合法性。以下是完整实战案例:​

(1)基础 ModelForm 实现(关联模型)

# 1. 定义模型(models.py)
from django.db import models

class Article(models.Model):
    """文章模型"""
    title = models.CharField(max_length=200, verbose_name="标题")
    content = models.TextField(verbose_name="内容")
    author = models.ForeignKey(
        'auth.User',  # 关联Django内置User模型
        on_delete=models.CASCADE,
        verbose_name="作者"
    )
    publish_date = models.DateTimeField(auto_now_add=True, verbose_name="发布时间")
    update_date = models.DateTimeField(auto_now=True, verbose_name="更新时间")

    class Meta:
        verbose_name = "文章"
        verbose_name_plural = "文章列表"
        ordering = ["-publish_date"]

# 2. 定义ModelForm(forms.py)
from django import forms
from .models import Article

class ArticleForm(forms.ModelForm):
    """文章表单:自动关联Article模型"""
    # 可选:自定义额外字段(非模型字段)
    confirm_title = forms.CharField(
        label="确认标题",
        max_length=200,
        widget=forms.TextInput(attrs={"class": "form-control"})  # 设置HTML属性
    )

    class Meta:
        model = Article  # 关联的模型
        # 指定需要生成的字段(或使用exclude排除不需要的字段)
        fields = ["title", "content", "confirm_title"]
        # 自定义字段的widget和属性
        widgets = {
            "title": forms.TextInput(attrs={"class": "form-control", "placeholder": "请输入文章标题"}),
            "content": forms.Textarea(attrs={"class": "form-control", "rows": 6, "placeholder": "请输入文章内容"}),
        }
        # 自定义字段标签
        labels = {
            "title": "文章标题",
            "content": "文章内容",
        }
        # 自定义错误信息
        error_messages = {
            "title": {
                "required": "标题不能为空",
                "max_length": "标题长度不能超过200个字符"
            },
            "content": {
                "required": "内容不能为空"
            }
        }

    # 3. 自定义字段验证:方法名格式为clean_字段名
    def clean_title(self):
        """验证标题:不能包含敏感词"""
        title = self.cleaned_data.get("title")
        sensitive_words = ["敏感词1", "敏感词2"]
        if any(word in title for word in sensitive_words):
            raise forms.ValidationError("标题包含敏感词,请修改")
        return title

    # 4. 自定义跨字段验证:验证标题与确认标题一致性
    def clean(self):
        cleaned_data = super().clean()  # 调用父类clean方法
        title = cleaned_data.get("title")
        confirm_title = cleaned_data.get("confirm_title")
        
        if title and confirm_title and title != confirm_title:
            # 添加错误信息到指定字段(或使用non_field_errors)
            self.add_error("confirm_title", "两次输入的标题不一致")
        
        return cleaned_data

(2)在视图中使用 ModelForm

# views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required  # 登录装饰器
from .models import Article
from .forms import ArticleForm

# 发布文章(登录后才能访问)
@login_required
def article_create(request):
    if request.method == "POST":
        # 1. 绑定POST数据到表单(request.FILES用于处理文件上传)
        form = ArticleForm(request.POST)
        # 2. 验证表单数据
        if form.is_valid():
            # 3. 获取验证后的干净数据(已排除无效字段)
            article = form.save(commit=False)  # commit=False:不立即保存到数据库
            # 4. 补充模型中未在表单中的字段(如作者)
            article.author = request.user
            # 5. 保存到数据库
            article.save()
            # 6. 跳转至文章详情页
            return redirect("article_detail", pk=article.pk)
    else:
        # GET请求:显示空表单
        form = ArticleForm()
    
    # 渲染模板(传递表单对象,用于前端显示)
    return render(request, "article/create.html", {"form": form})

# 编辑文章(仅作者可编辑)
@login_required
def article_edit(request, pk):
    # 获取要编辑的文章(不存在则返回404)
    article = get_object_or_404(Article, pk=pk)
    
    # 校验权限:仅文章作者可编辑
    if article.author != request.user:
        return redirect("article_detail", pk=article.pk)  # 无权限则跳转详情页
    
    if request.method == "POST":
        # 绑定POST数据和已有文章对象(实现更新逻辑)
        form = ArticleForm(request.POST, instance=article)
        if form.is_valid():
            article = form.save(commit=False)
            article.author = request.user
            article.save()
            return redirect("article_detail", pk=article.pk)
    else:
        # GET请求:显示带有初始数据的表单(instance=article)
        form = ArticleForm(instance=article)
    
    return render(request, "article/edit.html", {"form": form, "article": article})

(3)前端模板渲染表单

<!-- article/create.html -->
<!DOCTYPE html>
<html>
<head>
    <title>发布文章</title>
    <!-- 引入Bootstrap样式(美化表单) -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-5">
        <h2>发布新文章</h2>
        <!-- 表单提交:POST方法,action为空表示提交到当前URL -->
        <form method="post">
            {% csrf_token %}  <!-- 必须添加CSRF令牌,防御CSRF攻击 -->
            
            <!-- 渲染标题字段 -->
            <div class="mb-3">
                {{ form.title.label_tag }}  <!-- 显示字段标签 -->
                {{ form.title }}  <!-- 显示输入控件 -->
                {% if form.title.errors %}  <!-- 显示字段错误信息 -->
                    <div class="text-danger mt-1">
                        {% for error in form.title.errors %}
                            <p>{{ error }}</p>
                        {% endfor %}
                    </div>
                {% endif %}
            </div>
            
            <!-- 渲染确认标题字段 -->
            <div class="mb-3">
                {{ form.confirm_title.label_tag }}
                {{ form.confirm_title }}
                {% if form.confirm_title.errors %}
                    <div class="text-danger mt-1">
                        {% for error in form.confirm_title.errors %}
                            <p>{{ error }}</p>
                        {% endfor %}
                    </div>
                {% endif %}
            </div>
            
            <!-- 渲染内容字段 -->
            <div class="mb-3">
                {{ form.content.label_tag }}
                {{ form.content }}
                {% if form.content.errors %}
                    <div class="text-danger mt-1">
                        {% for error in form.content.errors %}
                            <p>{{ error }}</p>
                        {% endfor %}
                    </div>
                {% endif %}
            </div>
            
            <!-- 渲染全局错误信息(跨字段验证错误) -->
            {% if form.non_field_errors %}
                <div class="text-danger mb-3">
                    {% for error in form.non_field_errors %}
                        <p>{{ error }}</p>
                    {% endfor %}
                </div>
            {% endif %}
            
            <button type="submit" class="btn btn-primary">发布</button>
        </form>
    </div>
</body>
</html>