浏览知识库目录

Python

表单验证与文件上传

表单验证与文件上传

Django Form 不只是生成 HTML,它更重要的职责是把不可信的用户输入转换为经过校验的 Python 数据。本篇将完成普通表单、ModelForm、自定义校验和文件上传。


一、Form 的处理流程

一个标准表单请求有两条路径:

  • GET:创建空表单并展示。
  • POST:绑定请求数据,校验成功后执行业务并重定向;失败则带错误重新渲染。
def message_create(request):
    if request.method == "POST":
        form = MessageForm(request.POST, request.FILES)
        if form.is_valid():
            message = form.save()
            return redirect("notes:detail", pk=message.pk)
    else:
        form = MessageForm()

    return render(request, "notes/message_form.html", {"form": form})

POST 成功后重定向是 PRG 模式(Post/Redirect/Get),可以避免用户刷新页面导致重复提交。


二、普通 Form

普通 Form 适合搜索、登录、筛选或不直接对应模型的数据。

from django import forms


class MessageSearchForm(forms.Form):
    q = forms.CharField(label="关键词", max_length=100, required=False)
    status = forms.ChoiceField(
        label="状态",
        required=False,
        choices=[
            ("", "全部"),
            ("draft", "草稿"),
            ("published", "已发布"),
            ("archived", "已归档"),
        ],
    )
form = MessageSearchForm(request.GET)
if form.is_valid():
    keyword = form.cleaned_data["q"]

只有 is_valid() 成功后才能可靠使用 cleaned_data


三、ModelForm

from django import forms

from .models import Message


class MessageForm(forms.ModelForm):
    class Meta:
        model = Message
        fields = ["name", "content", "status", "tags"]
        widgets = {
            "content": forms.Textarea(attrs={"rows": 6}),
        }
        help_texts = {
            "content": "请填写 10~1000 个字符。",
        }

应显式列出 fields,避免模型新增敏感字段后意外暴露在表单中。

编辑对象时传入 instance

form = MessageForm(request.POST or None, instance=message)

四、字段级校验

方法名格式为 clean_字段名

class MessageForm(forms.ModelForm):
    class Meta:
        model = Message
        fields = ["name", "content", "status"]

    def clean_content(self):
        content = self.cleaned_data["content"].strip()
        if len(content) < 10:
            raise forms.ValidationError("留言内容至少需要 10 个字符。")
        return content

返回值会写回 cleaned_data,因此可以在这里做安全的标准化,例如去除首尾空格。


五、跨字段校验

多个字段之间存在规则时重写 clean()

def clean(self):
    cleaned_data = super().clean()
    status = cleaned_data.get("status")
    content = cleaned_data.get("content", "")

    if status == "published" and len(content) < 30:
        raise forms.ValidationError("发布内容至少需要 30 个字符。")

    return cleaned_data

字段缺失时要使用 get(),因为此前的字段校验可能已经失败。


六、把当前用户写入模型

owner 不应该让普通用户在表单中选择,应由服务端决定:

if form.is_valid():
    message = form.save(commit=False)
    message.owner = request.user
    message.save()
    form.save_m2m()
    return redirect("notes:detail", pk=message.pk)

使用 commit=False 后,多对多数据需要在实例保存后执行 form.save_m2m()


七、模板中渲染表单

<form method="post" enctype="multipart/form-data" novalidate>
  {% csrf_token %}

  {{ form.non_field_errors }}

  {% for field in form %}
    <div>
      {{ field.label_tag }}
      {{ field }}
      {% if field.help_text %}<small>{{ field.help_text }}</small>{% endif %}
      {{ field.errors }}
    </div>
  {% endfor %}

  <button type="submit">保存</button>
</form>
  • 写操作表单必须带 {% csrf_token %}
  • 有文件字段时必须使用 enctype="multipart/form-data"
  • novalidate 可关闭浏览器原生提示,便于统一展示服务端错误;是否使用由项目体验决定。

八、文件上传

模型:

def attachment_path(instance, filename):
    return f"attachments/{instance.message_id}/{filename}"


class Attachment(models.Model):
    message = models.ForeignKey(Message, on_delete=models.CASCADE)
    file = models.FileField(upload_to=attachment_path)

表单校验大小和扩展名:

from pathlib import Path


class AttachmentForm(forms.ModelForm):
    class Meta:
        model = Attachment
        fields = ["file"]

    def clean_file(self):
        uploaded = self.cleaned_data["file"]
        if uploaded.size > 5 * 1024 * 1024:
            raise forms.ValidationError("文件不能超过 5 MB。")

        allowed = {".pdf", ".png", ".jpg", ".jpeg"}
        if Path(uploaded.name).suffix.lower() not in allowed:
            raise forms.ValidationError("不支持该文件类型。")
        return uploaded

只检查扩展名并不能证明文件内容安全。生产系统还应考虑 MIME 检查、随机文件名、病毒扫描、下载响应头和访问权限。


九、FormSet 简介

一次编辑多条同类数据时可使用 FormSet:

from django.forms import modelformset_factory

MessageFormSet = modelformset_factory(
    Message,
    fields=["name", "status"],
    extra=0,
)

父子模型一起编辑时可以使用 Inline FormSet。它们功能强但复杂,先掌握单表单再使用。


十、常见问题

  • form.is_valid() 总是失败:在模板中输出 form.errors 查看具体原因。
  • 文件字段一直为空:检查 request.FILES 和表单 enctype
  • 编辑时新增了一条记录:创建 ModelForm 时忘记传 instance
  • 多对多没有保存:使用 commit=False 后忘记 save_m2m()
  • 重复提交:保存成功后直接渲染模板,未采用重定向。

十一、本篇检查清单

  • 能区分 Form 与 ModelForm 的使用场景。
  • 能编写字段级和跨字段校验。
  • 能安全地从 cleaned_data 读取数据。
  • 能用 commit=False 写入当前用户。
  • 能正确接收文件并理解上传安全边界。

上一篇:模板、静态文件与媒体文件 | 下一篇:CRUD、分页、搜索与消息框架