Flask 笔记四:用 WTForms 做新增、编辑和删除

1. 为什么不用原生 <form>

之前的写法:

title = request.form.get("title")

if not title:

flash("标题不能为空")

能工作,但页面一多就会重复很多类似代码。Flask-WTF 帮你集中处理:

能力 原生表单 Flask-WTF
必填校验 自己写 if DataRequired()
错误提示 自己 flash form.title.errors
CSRF 防伪造 没有 自动带 csrf_token
编辑页回填 手动赋值 form.title.data = row.title

入门项目用 WTForms,后面会省很多事。


2. 安装 Flask-WTF

pip install Flask-WTF

app/__init__.py 里已有 SECRET_KEY,WTForms 会用它生成 CSRF token,不用额外配置。


3. 定义表单:app/forms.py

新建文件,写 NoteForm

from flask_wtf import FlaskForm

from wtforms import StringField, TextAreaField, SubmitField

from wtforms.validators import DataRequired, Length, Optional

class NoteForm(FlaskForm):

title = StringField(

"标题",

validators=[

DataRequired("请输入标题"),

Length(max=100, message="标题最多 100 个字"),

],

)

content = TextAreaField(

"内容",

validators=Optional(),

)

submit = SubmitField("保存")

几个常用校验器:

  • DataRequired --- 不能为空
  • Length(max=100) --- 长度限制
  • Optional --- 可以为空

4. 改造「新增」页面

视图 app/home/views.py

from flask import render_template, redirect, url_for, flash

from app import db

from app.home import home

from app.forms import NoteForm

from app.models import Note

@home.route("/notes/add/", methods="GET", "POST")

def note_add():

form = NoteForm()

if form.validate_on_submit():

note = Note(

title=form.title.data.strip(),

content=(form.content.data or "").strip(),

)

db.session.add(note)

db.session.commit()

flash("保存成功")

return redirect(url_for("home.note_list"))

return render_template("home/note_form.html", form=form, title="新增备忘录")

和之前学习的form的区别:

  • form.validate_on_submit() 代替手写的 if not title
  • 校验失败时,表单会保留用户已填的内容

模板 app/templates/home/note_form.html

新增和编辑共用这一个模板:

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8">

<title>{{ title }}</title>

</head>

<body>

<h1>{{ title }}</h1>

{% with messages = get_flashed_messages() %}

{% for msg in messages %}

<p style="color:green;">{{ msg }}</p>

{% endfor %}

{% endwith %}

<form method="post">

{{ form.csrf_token }}

<p>

{{ form.title.label }}<br>

{{ form.title(size=40) }}

{% for err in form.title.errors %}

<span style="color:red;">{{ err }}</span>

{% endfor %}

</p>

<p>

{{ form.content.label }}<br>

{{ form.content(rows=5, cols=40) }}

{% for err in form.content.errors %}

<span style="color:red;">{{ err }}</span>

{% endfor %}

</p>

<p>{{ form.submit }}</p>

<a href="{{ url_for('home.note_list') }}">返回列表</a>

</form>

</body>

</html>

别忘了 {``{ form.csrf_token }},漏了 POST 会 400 报错。


5. 做「编辑」页面

编辑和新增逻辑很像:GET 时把数据库里的值填进表单,POST 时更新。

@home.route("/notes/edit/<int:note_id>/", methods="GET", "POST")

def note_edit(note_id):

row = Note.query.get_or_404(note_id)

form = NoteForm()

if request.method == "GET":

form.title.data = row.title

form.content.data = row.content

if form.validate_on_submit():

row.title = form.title.data.strip()

row.content = (form.content.data or "").strip()

db.session.commit()

flash("修改成功")

return redirect(url_for("home.note_list"))

return render_template(

"home/note_form.html",

form=form,

title="编辑备忘录",

)

get_or_404(note_id):找不到记录就返回 404,不用自己写 if。


6. 做「删除」

删除不要用 GET(链接一点就删,不安全)。用 POST + 单独的小表单。

表单 app/forms.py 里加一行

class DeleteForm(FlaskForm):

submit = SubmitField("确认删除")

也可以只放 csrf_token,不显示 submit 按钮,用 JS 提交;入门阶段这样写最直观。

视图

from app.forms import NoteForm, DeleteForm

@home.route("/notes/delete/<int:note_id>/", methods="POST")

def note_delete(note_id):

row = Note.query.get_or_404(note_id)

form = DeleteForm()

if form.validate_on_submit():

db.session.delete(row)

db.session.commit()

flash("已删除")

return redirect(url_for("home.note_list"))

列表页里加编辑 / 删除

更新 app/templates/home/note_list.html

{% for note in page_data.items %}

<div class="item">

<h3>{{ note.title }}</h3>

<p>{{ note.content or '(无内容)' }}</p>

<div class="time">{{ note.addtime.strftime('%Y-%m-%d %H:%M') }}</div>

<p>

<a href="{{ url_for('home.note_edit', note_id=note.id) }}">编辑</a>

<form method="post"

action="{{ url_for('home.note_delete', note_id=note.id) }}"

style="display:inline;">

{{ delete_form.csrf_token }}

<button type="submit" οnclick="return confirm('确定删除?')">删除</button>

</form>

</p>

</div>

{% endfor %}

列表视图里多传一个 delete_form

from app.forms import DeleteForm

@home.route("/notes/")

def note_list():

page = request.args.get("page", 1, type=int)

page_data = Note.query.order_by(Note.addtime.desc()).paginate(

page=page, per_page=10,

)

return render_template(

"home/note_list.html",

page_data=page_data,

delete_form=DeleteForm(),

)


7. 一张图串起来

列表页 /notes/

├─ 新增 → /notes/add/ → NoteForm → db.session.add

├─ 编辑 → /notes/edit/1/ → NoteForm → 改 row 字段 → commit

└─ 删除 → POST /notes/delete/1/ → DeleteForm → db.session.delete

到这一步,一个最小的 增删改查 就齐了。


8. 新手常踩的 4 个坑

坑 1:模板里漏了 csrf_token

报错类似 The CSRF token is missing

每个 POST 表单都要有 {``{ form.csrf_token }}

坑 2:只写 form.validate(),没写 validate_on_submit()

validate_on_submit() = 「这次是 POST 提交 并且 校验通过」。

GET 打开页面时不应触发写入逻辑。

坑 3:编辑页 GET 和 POST 混在一个 if 里

推荐顺序:

if request.method == "GET":

form.title.data = row.title # 先回填

if form.validate_on_submit(): # 再处理提交

...

坑 4:删除用 GET 链接

<!-- 不要这样 -->

<a href="/notes/delete/1/">删除</a>

爬虫、预加载都可能误触。删除务必 POST。


9. 和真实项目的关系(不写细节,只讲习惯)

实际项目里常见做法和这篇一致:

  • 一个 XxxForm 管新增和编辑
  • validate_on_submit()add 或改 row
  • 删除单独 POST,带 CSRF
  • 模板里循环 form.xxx.errors 显示错误

复杂表单会加 SelectFieldDateField、自定义 validate_xxx,但套路不变。


10. 小结

这一篇核心三件事:

  1. 表单类 --- 字段 + 校验规则写在一起
  2. validate_on_submit() --- 统一的「提交且合法」入口
  3. CSRF --- 每个 POST 表单都要 token

三篇连起来,你已经会:

内容
项目结构、Blueprint
数据库、列表、分页
WTForms、编辑、删除