(九)表单文本转成业务数据
1. 勾选框和标签需要解释
保存《Python Web》时,浏览器提交的不是 Python 数据类。书名是文本;勾选框勾选时提交 finished=1,没有勾选时通常根本没有这个字段;标签输入框提交一整段逗号分隔的文本。
本章规定书名去两端空白后为 1 至 100 个字符;标签用英文逗号分隔,去空白、去空项并合并重复项,最多 10 个不同标签,每项最多 30 个字符。所有者由登录身份决定。先把这些规则写成能独立调用的函数:
# 项目文件:shelf_project/src/shelfweb/validation.py
# 先清理书名和标签,再把表单的已读标记转换成数据库保存的整数。
import re
from dataclasses import dataclass
@dataclass(frozen=True)
class BookDraft:
title: str
finished: bool
tags: list[str]
def read_book_form(form):
values = {
"title": form.get("title", ""),
"finished": form.get("finished") == "1",
"tags": form.get("tags", ""),
}
errors = {}
for name in ["title", "finished", "tags"]:
if len(form.getlist(name)) > 1:
errors[name] = "同一字段请只提交一次"
if set(form) - {"title", "finished", "tags", "csrf_token"}:
errors["_form"] = "表单中有未识别的字段"
title = values["title"].strip()
if not 1 <= len(title) <= 100:
errors["title"] = "书名请填写 1 至 100 个字符"
if form.get("finished") not in {None, "1"}:
errors["finished"] = "已读状态请使用表单中的勾选框"
tags = list(
dict.fromkeys(
piece.strip() for piece in values["tags"].split(",") if piece.strip()
)
)
if len(tags) > 10 or any(len(tag) > 30 for tag in tags):
errors["tags"] = "最多填写 10 个标签,每个标签不超过 30 个字符"
draft = None if errors else BookDraft(title, values["finished"], tags)
return draft, values, errorsvalues 保留这次表单的显示值,失败时继续放回输入框;errors 按字段保存说明;只有检查通过,才构造 BookDraft。这份数据类和前一章的 Pydantic 输入模型承担相似的业务角色,校验动作在这里由函数显式执行。
form.getlist 检查同名字段,set(form) 取得字段名集合,再用集合差找出额外字段。伪造一个 owner_id 会得到表单错误,不会改变归属。form.get("finished") == "1" 则把一次明确的勾选转换为布尔值。
# 项目文件:demos/form_parse.py
# 表单字段按单值读取,保留用户填写内容供校验失败后重新渲染页面。
from werkzeug.datastructures import MultiDict
from shelfweb.validation import read_book_form
for data in [
MultiDict(
{"title": " Python Web ", "tags": " 网络, 网络, 基础 ", "finished": "1"}
),
MultiDict({"title": "A", "tags": ""}),
MultiDict([("title", "A"), ("title", "B")]),
]:
draft, values, errors = read_book_form(data)
print("表单值", values)
print("处理结果", draft if not errors else errors)第一份数据得到书名 Python Web、已读 True、标签 ["网络", "基础"];第二份没有勾选框字段,得到 False 和空标签;第三份有两个书名,保留错误信息,业务对象为 None。
2. HTML 检查与服务器检查连接
页面的 required、maxlength 会在浏览器提交前帮助用户检查。但测试客户端可以直接发送请求,所以服务端仍执行上面的规则。后面的表单片段把错误放在对应输入框旁边,不把失败输入写进数据库。