You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Django中使用Partial Form保存草稿是否为最佳实践?新手实现方案咨询

嘿,作为一名Django开发者,我来帮你梳理下这个问题——首先明确说:用Partial Form保存草稿绝对是合理且常见的实践,但关键是要做好草稿和正式数据的隔离,不然会导致你的Candidate表出现不完整的无效数据。针对你的投票应用场景,我给你几个可行的实现思路,附代码示例:

一、核心原则:隔离草稿与正式数据

不管用哪种方案,核心都是别把未完成的草稿直接写入正式的Candidate模型——要么单独存草稿数据,要么给正式数据加状态标记,避免无效数据混入业务逻辑。

二、具体实现思路

1. 新增独立草稿模型(最推荐的最佳实践)

这个方案最清晰,完全隔离草稿和正式数据,后续维护也最省心。

步骤1:添加草稿模型

在models.py里新增一个和Candidate字段几乎一致的草稿模型,允许所有字段为空,并关联用户(或会话key)用于识别归属:

from django.contrib.auth.models import User
from django.db import models

class CandidateDraft(models.Model):
    # 关联登录用户,匿名用户用session_key识别
    user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
    session_key = models.CharField(max_length=40, null=True, blank=True)
    # 和Candidate一致的字段,全部允许为空
    name = models.CharField(max_length=50, blank=True, null=True)
    surname = models.CharField(max_length=50, blank=True, null=True)
    biography = models.TextField(blank=True, null=True)
    targets = models.CharField(max_length=1000, blank=True, null=True)
    # 记录时间,方便后续清理旧草稿
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

步骤2:创建草稿表单

在forms.py里新增草稿表单,不需要强制验证所有字段:

from django import forms
from .models import Candidate, CandidateDraft

class CandidateDraftForm(forms.ModelForm):
    class Meta:
        model = CandidateDraft
        fields = ['name', 'surname', 'biography', 'targets']
        widgets = {
            'biography': forms.Textarea(attrs={'rows': 3})
        }

步骤3:视图处理草稿保存与恢复

在视图里区分“保存草稿”和“正式提交”操作,自动填充用户的草稿数据:

from django.shortcuts import render, redirect
from .models import Candidate, CandidateDraft
from .forms import CandidateForm, CandidateDraftForm

def create_candidate(request):
    # 尝试获取用户的草稿数据
    draft = None
    if request.user.is_authenticated:
        # 登录用户取最新的草稿
        draft = CandidateDraft.objects.filter(user=request.user).last()
    else:
        # 匿名用户从session取草稿
        if 'candidate_draft' in request.session:
            draft_data = request.session['candidate_draft']
            draft = CandidateDraft(**draft_data)

    if request.method == 'POST':
        # 处理正式提交
        if 'submit' in request.POST:
            form = CandidateForm(request.POST)
            if form.is_valid():
                form.save()
                # 提交成功后删除草稿
                if draft:
                    draft.delete()
                if not request.user.is_authenticated and 'candidate_draft' in request.session:
                    del request.session['candidate_draft']
                return redirect('candidate_list')  # 替换成你的列表页路由
        # 处理保存草稿
        elif 'save_draft' in request.POST:
            if request.user.is_authenticated:
                draft_form = CandidateDraftForm(request.POST, instance=draft)
                if draft_form.is_valid():
                    draft = draft_form.save(commit=False)
                    draft.user = request.user
                    draft.save()
            else:
                # 匿名用户把草稿存在session里
                request.session['candidate_draft'] = {
                    'name': request.POST.get('name'),
                    'surname': request.POST.get('surname'),
                    'biography': request.POST.get('biography'),
                    'targets': request.POST.get('targets'),
                }
            return redirect('create_candidate')
    else:
        # 初始化表单时填充草稿数据
        if draft:
            form = CandidateForm(instance=draft)
        else:
            form = CandidateForm()

    return render(request, 'create_candidate.html', {'form': form})

步骤4:模板添加按钮

在模板里区分两个提交按钮:

<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit" name="save_draft">保存草稿</button>
    <button type="submit" name="submit">提交候选人</button>
</form>

2. 给原模型添加状态字段(轻量方案)

如果不想新增模型,可以给Candidate加一个状态字段,区分草稿和正式数据:

步骤1:修改原模型

class Candidate(models.Model):
    STATUS_CHOICES = [
        ('draft', '草稿'),
        ('published', '已发布')
    ]
    name = models.CharField(max_length=50)
    surname = models.CharField(max_length=50)
    biography = models.TextField(blank=True, null=True)  # 改为允许为空
    targets = models.CharField(max_length=1000)
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')

步骤2:修改表单验证

在CandidateForm里重写clean方法,只有正式提交时才验证必填字段:

class CandidateForm(forms.ModelForm):
    class Meta:
        model = Candidate
        fields = ['name', 'surname', 'biography', 'targets', 'status']
        widgets = {
            'biography': forms.Textarea(attrs={'rows': 3}),
            'status': forms.HiddenInput()  # 隐藏状态字段,由视图控制
        }

    def clean(self):
        cleaned_data = super().clean()
        # 只有正式提交时才验证biography必填
        if self.data.get('submit') and not cleaned_data.get('biography'):
            self.add_error('biography', '此字段为必填项')
        return cleaned_data

步骤3:视图处理逻辑

def create_candidate(request):
    # 取当前用户的草稿(如果有)
    draft = Candidate.objects.filter(user=request.user, status='draft').last() if request.user.is_authenticated else None
    form = CandidateForm(request.POST or None, instance=draft)
    
    if request.method == 'POST':
        if 'save_draft' in request.POST:
            form.instance.status = 'draft'
            form.save(commit=False)  # 跳过完整验证
            form.save()
            return redirect('create_candidate')
        elif 'submit' in request.POST:
            form.instance.status = 'published'
            if form.is_valid():
                form.save()
                return redirect('candidate_list')
    
    return render(request, 'create_candidate.html', {'form': form})

⚠️ 注意:用这个方案时,所有查询正式候选人的地方都要加上filter(status='published'),避免展示草稿数据。

3. 用Session存储草稿(适合匿名用户)

如果你的应用允许匿名用户创建候选人,可以把草稿数据存在session里,但要注意session有大小限制,且匿名用户关闭浏览器后可能丢失草稿。

视图里的核心逻辑:

def create_candidate(request):
    # 从session读取草稿
    draft_data = request.session.get('candidate_draft', {})
    form = CandidateForm(request.POST or draft_data)
    
    if request.method == 'POST':
        if 'save_draft' in request.POST:
            # 把表单数据存入session
            request.session['candidate_draft'] = {
                k: v for k, v in request.POST.items() if k in form.fields
            }
            return redirect('create_candidate')
        elif 'submit' in request.POST:
            if form.is_valid():
                form.save()
                del request.session['candidate_draft']
                return redirect('candidate_list')
    
    return render(request, 'create_candidate.html', {'form': form})
三、安全注意事项
  • 权限控制:草稿只能被创建它的用户访问,登录用户关联user字段,匿名用户用session隔离。
  • 数据验证:即使是草稿,也要做基础验证(比如字段长度),避免恶意垃圾数据。
  • 定期清理:用Django定时任务(如Celery)自动删除超过30天的草稿,避免数据库冗余。
  • XSS防护:如果biography允许富文本,要清理HTML标签,避免跨站脚本攻击。

内容的提问来源于stack exchange,提问作者AnoMorCH

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.01 02:12:37