<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>WTForms &#8211; 李辉 / Grey Li</title>
	<atom:link href="https://greyli.com/tag/wtforms/feed/" rel="self" type="application/rss+xml" />
	<link>https://greyli.com</link>
	<description>一个编程和写作爱好者的在线记事本</description>
	<lastBuildDate>Thu, 06 Nov 2025 11:36:11 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>https://wordpress.org/?v=4.9.26</generator>

<image>
	<url>https://greyli.com/wp-content/uploads/2025/03/avatar-500-compressed-144x144.jpg</url>
	<title>WTForms &#8211; 李辉 / Grey Li</title>
	<link>https://greyli.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>比修 Typo 还简单的开源贡献方式</title>
		<link>https://greyli.com/better-than-typo-fix/</link>
		<comments>https://greyli.com/better-than-typo-fix/#comments</comments>
		<pubDate>Sun, 21 Jun 2020 12:25:45 +0000</pubDate>
		<dc:creator><![CDATA[李辉]]></dc:creator>
				<category><![CDATA[思考与随感]]></category>
		<category><![CDATA[Flask]]></category>
		<category><![CDATA[WTForms]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[开源贡献]]></category>
		<category><![CDATA[开源项目]]></category>

		<guid isPermaLink="false">https://greyli.com/?p=3124</guid>
		<description><![CDATA[最近给 WTForms 提交了一个 PR，这个 PR 向源码、测试和本地化文件里添加了 537 个句号。听起来 [&#8230;]]]></description>
				<content:encoded><![CDATA[<p class="md-end-block md-p md-focus"><span class="md-plain">最近给 WTForms 提交了一个 <a href="https://github.com/wtforms/wtforms/pull/620">PR</a>，这个 PR 向源码、测试和本地化文件里添加了 537 个句号。听起来似乎很奇怪，为什么 WTForms 会需要增加 537 个句号？别着急，下面会慢慢解释（事实上我只完成了一部分的工作，还有大概 500 个句号需要添加）。我发现我似乎很喜欢提交这种 PR，这类开源贡献没有太多技术含量（基本就是体力活），但是能有效提高项目的整体完美度，让用户获得更一致和舒服的体验。下面是一些可以归到这一类开源贡献的 PR。</span></p>
<p class="md-end-block md-p"><span class="md-pair-s"><strong><span class="md-plain">1. 给 WTForms 添加 537 个句号</span></strong></span></p>
<p class="md-end-block md-p"><span class="md-link md-pair-s" spellcheck="false"><a href="https://github.com/wtforms/wtforms/pull/620">https://github.com/wtforms/wtforms/pull/620</a></span></p>
<p class="md-end-block md-p"><span class="md-plain">WTForms 从添加 CSRF 保护功能作为分界，之前定义的验证错误消息都包含结尾的句号，之后的错误消息都漏掉了句号。这会导致错误消息不一致（想象同一个表单显示两个输入框的错误消息，一个有句号，一个没有句号）。这个 PR 补齐了源码、测试、本地化文本（POT 和部分 PO 文件）中错误消息的句号，包括 506 个英文句号和 31 个中文句号。</span></p>
<p class="md-end-block md-p"><span class="md-plain">本地化文件在翻译时大都按照源文本决定是否添加句号，所以也存在错误消息不一致的问题。因为精力有限，在 32 个本地化文件里，我只更新了简体中文、繁体中文、日文、德文和俄文。大概还有</span><span class="md-meta-i-c md-link"><a spellcheck="false" href="https://github.com/wtforms/wtforms/tree/master/src/wtforms/locale"><span class="md-plain">二十几个本地化文件</span></a></span><span class="md-plain">需要进行确认和更新，如果感兴趣的话，你可以考虑去做这件事。</span></p>
<p class="md-end-block md-p"><span class="md-pair-s"><strong><span class="md-plain">2. 给 Flask 添加 96 个美元符号</span></strong></span></p>
<p class="md-end-block md-p"><span class="md-link md-pair-s" spellcheck="false"><a href="https://github.com/pallets/flask/pull/2877">https://github.com/pallets/flask/pull/2877</a></span></p>
<p class="md-end-block md-p"><span class="md-plain">Flask 文档、和各类 README.md 文件里对于命令行命令的标识很混乱，有时没有命令提示符，有时用「&gt;」，有时用「$」，有时用「#」。这个 PR 统一了所有命令行命令，统一添加美元符号作为提示符，仅在需要明确区分 Windows 命令的几处使用「&gt;」。</span></p>
<p class="md-end-block md-p"><span class="md-pair-s"><strong><span class="md-plain">3. 给 Flask 更换 13 个 URL</span></strong></span></p>
<p class="md-end-block md-p"><span class="md-link md-pair-s" spellcheck="false"><a href="https://github.com/pallets/flask/pull/3427">https://github.com/pallets/flask/pull/3427</a></span></p>
<p class="md-end-block md-p"><span class="md-plain">Flask 去年陆续把文档迁移到了 palletsprojects.com 域名下， 访问旧的 pocoo.org 会进行跳转，这个 PR 更新了所有文档和源码里的旧 URL。</span></p>
<p><span class="md-pair-s"><strong><span class="md-plain">4. 给 <span class="md-meta-i-c md-link">PyCon China</span> 和 <span class="md-meta-i-c md-link">FlaskCon</span> 压缩 119 张图片</span></strong></span></p>
<p class="md-end-block md-p"><span class="md-plain">除此之外，勉强能沾上边的还有给 </span><span class="md-meta-i-c md-link"><a spellcheck="false" href="https://cn.pycon.org"><span class="md-plain">PyCon China</span></a></span><span class="md-plain"> 和 </span><span class="md-meta-i-c md-link"><a spellcheck="false" href="https://flaskcon.com"><span class="md-plain">FlaskCon</span></a></span><span class="md-plain"> 的网站分别压缩了 105 和 14 张图片。因为缺乏规范，有些技术大会的网站图片在上传之前没有经过压缩和裁剪，这会让页面加载变得非常慢。这两个 PR（</span><span class="md-meta-i-c md-link"><a spellcheck="false" href="https://github.com/PyConChina/PyConChina2019/pull/3"><span class="md-plain">PyConChina #3</span></a></span><span class="md-plain">，</span><span class="md-meta-i-c md-link"><a spellcheck="false" href="https://github.com/FlaskCon/website/pull/7"><span class="md-plain">FlaskCon #7</span></a></span><span class="md-plain">）分别让两个网站的图片总大小从 25M 和 4M 降低到 5M 和 967KB。</span></p>
<p class="md-end-block md-p"><span class="md-plain">给文档修 typo 是很常见的开源贡献类型，这也是很多人一开始参与开源的方式。有人甚至会走火入魔最后变成专业的「开源 typo 修复专家」，不停的用英文语法检查工具去检查每一个流行的开源项目文档……相比之下，上面这一类 PR 要比修 typo 更简单（在智力上），有时也更有价值。</span></p>
<p class="md-end-block md-p"><span class="md-plain">我发现相对于技术实现，我其实更关注 API 设计和用户体验，总想要尽可能的追求设计的一致和美观。这也导致我会在写作和编程时花费大量时间在命名、文件组织、措辞、章节安排、排版、文案和彩蛋这些事情上。这大概就是我编程水平进步缓慢、写作速度缓慢的原因。</span></p>
<p class="md-end-block md-p"><span class="md-plain">P.S. 文中的数字都是估算，大概会有 1~5 左右的偏差。精确的数字是为了让措辞看起来更一致和美观瞎编的。</span></p>
]]></content:encoded>
			<wfw:commentRss>https://greyli.com/better-than-typo-fix/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>WTForms自定义验证方法（行内验证器）是如何被调用的？</title>
		<link>https://greyli.com/how-custom-validator-work-in-wtforms/</link>
		<comments>https://greyli.com/how-custom-validator-work-in-wtforms/#respond</comments>
		<pubDate>Thu, 12 Jul 2018 10:16:35 +0000</pubDate>
		<dc:creator><![CDATA[李辉]]></dc:creator>
				<category><![CDATA[计算机与编程]]></category>
		<category><![CDATA[Flask]]></category>
		<category><![CDATA[Flask-WTF]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[WTForms]]></category>
		<category><![CDATA[编程]]></category>

		<guid isPermaLink="false">http://greyli.com/?p=1754</guid>
		<description><![CDATA[这篇文章基于我在知乎上的这个回答，进行了相应的简化处理，放到这里做个备份。 万能的回答 答案在源码里。 简单的 [&#8230;]]]></description>
				<content:encoded><![CDATA[<div class="RichContent-inner">
<p>这篇文章基于我在知乎上的<a href="https://www.zhihu.com/question/280470162/answer/418393736">这个回答</a>，进行了相应的简化处理，放到这里做个备份。</p>
<h2>万能的回答</h2>
<p>答案在源码里。</p>
<h2>简单的回答</h2>
<p>WTForms会在你对表单实例调用Form.validate()方法时收集所有的行内验证方法，然后在对每个字段调用Field.validate()方法验证时将这些自定义行内验证方法一并和通过validators参数传入的验证器列表一起调用，进行验证。因为WTForms在调用时把对应的field作为参数传入了行内验证方法，所以你可以在自定义验证方法中通过field.data获取对应字段的数据。</p>
<h2>深入的回答</h2>
<p>WTForms会在你对表单实例调用Form.validate()方法时收集所有的行内验证方法。在Form类中的validate()方法定义中，你可以看到WTForms是如何收集这些行内验证方法的：</p>
<div>
<pre class="">class Form(with_metaclass(FormMeta, BaseForm)): 
   ....
   def validate(self):
        extra = {}
        for name in self._fields:
            inline = getattr(self.__class__, 'validate_%s' % name, None)
            if inline is not None:
                extra[name] = [inline]

        return super(Form, self).validate(extra)
</pre>
</div>
<blockquote><p>源码位置：<a class=" external" href="https://link.zhihu.com/?target=https%3A//github.com/wtforms/wtforms/blob/2.2.1/wtforms/form.py%23L305-L308" target="_blank" rel="nofollow noopener noreferrer" data-za-detail-view-id="1043"><span class="invisible">https://</span><span class="visible">github.com/wtforms/wtfo</span><span class="invisible">rms/blob/2.2.1/wtforms/form.py#L305-L308</span></a></p></blockquote>
<p>这里迭代所有的字段属性，然后表单类中是否包含<code>validate_字段名</code>形式的方法。如果有，那么就添加到extra字段里，这个字段被传递到BaseForm类的validate()方法中。在BaseForm类的validate()方法中，WTForms迭代所有字段，并对每个字段调用Field.validate()方法验证字段，继续传入自定义的行内验证方法：</p>
<div>
<pre class="">class BaseForm(object):
    ...
    def validate(self, extra_validators=None):
        self._errors = None
        success = True
        for name, field in iteritems(self._fields):  # 迭代字段名和字段对象
            if extra_validators is not None and name in extra_validators:  # 判断当前迭代字段是否存在自定义验证器
                extra = extra_validators[name]
            else:
                extra = tuple()
            if not field.validate(self, extra):  # 调用字段类的验证方法进行验证，传入自定义验证器
                success = False
        return success
</pre>
</div>
<blockquote><p>源码位置：<a class=" external" href="https://link.zhihu.com/?target=https%3A//github.com/wtforms/wtforms/blob/2.2.1/wtforms/form.py%23L134-L154" target="_blank" rel="nofollow noopener noreferrer" data-za-detail-view-id="1043"><span class="invisible">https://</span><span class="visible">github.com/wtforms/wtfo</span><span class="invisible">rms/blob/2.2.1/wtforms/form.py#L134-L154</span></a></p></blockquote>
<p>而在字段基类Field的validate()方法中，WTForms使用itertool模块提供的chain()函数把你实例化字段类时传入的验证器（<code>self.validators</code>）和自定义行内验证器（<code>extra_validators</code>）连接到一起：</p>
<div>
<pre class="">class Field(object):    
    ...
    def validate(self, form, extra_validators=tuple()):
        ...
        # Run validators
        if not stop_validation:
            chain = itertools.chain(self.validators, extra_validators)  # 合并两类验证器
            stop_validation = self._run_validation_chain(form, chain)  # 运行验证器
        ...
</pre>
</div>
<blockquote><p>源码位置：<a class=" external" href="https://link.zhihu.com/?target=https%3A//github.com/wtforms/wtforms/blob/2.2.1/wtforms/fields/core.py%23L204-L206" target="_blank" rel="nofollow noopener noreferrer" data-za-detail-view-id="1043"><span class="invisible">https://</span><span class="visible">github.com/wtforms/wtfo</span><span class="invisible">rms/blob/2.2.1/wtforms/fields/core.py#L204-L206</span></a></p></blockquote>
<p>连接起来的所有验证器赋值为chain变量，并传入到<code>self._run_validation_chain(form, chain)</code>进行进一步调用：</p>
<div>
<pre class="">class Field(object):    
    ...
    def _run_validation_chain(self, form, validators):
        for validator in validators:
            try:
                validator(form, self)  # 传入字段类本身作为第二个参数
</pre>
</div>
<blockquote><p>源码位置：<a class=" external" href="https://link.zhihu.com/?target=https%3A//github.com/wtforms/wtforms/blob/2.2.1/wtforms/fields/core.py%23L226" target="_blank" rel="nofollow noopener noreferrer" data-za-detail-view-id="1043"><span class="invisible">https://</span><span class="visible">github.com/wtforms/wtfo</span><span class="invisible">rms/blob/2.2.1/wtforms/fields/core.py#L226</span></a></p></blockquote>
<p>这个方法迭代所有的验证器对字段数据进行验证。关键在于validator(form, self)，可以看到这里传入了第二个参数self，即Field类本身，这也是为什么你可以在自定义验证方法中通过field.data获取当前字段数据。</p>
</div>
]]></content:encoded>
			<wfw:commentRss>https://greyli.com/how-custom-validator-work-in-wtforms/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Flask笔记（5）：表单</title>
		<link>https://greyli.com/flask-note-5-forms/</link>
		<comments>https://greyli.com/flask-note-5-forms/#respond</comments>
		<pubDate>Tue, 11 Oct 2016 00:21:57 +0000</pubDate>
		<dc:creator><![CDATA[李辉]]></dc:creator>
				<category><![CDATA[计算机与编程]]></category>
		<category><![CDATA[Flask]]></category>
		<category><![CDATA[WTForms]]></category>

		<guid isPermaLink="false">http://withlihui.com/?p=1113</guid>
		<description><![CDATA[Flask有众多的扩展来简化集成各种常用的库和框架，比如Flask-Bootstrap集成了Bootstrap [&#8230;]]]></description>
				<content:encoded><![CDATA[<p id="flask笔记表单" data-anchor-id="h4v1">Flask有众多的扩展来简化集成各种常用的库和框架，比如Flask-Bootstrap集成了Bootstrap，Flask-WTF集成了WTForms。这篇要介绍的就是Flask-WTF。</p>
<div class="md-section-divider"> </div>
<h2 id="安装和配置" data-anchor-id="rvd4">安装和配置</h2>
<p data-anchor-id="l9d0">安装很简单，使用pip：</p>
<p data-anchor-id="l9d0">pip install flask-wtf</p>
<p data-anchor-id="nrr2">Flask-WTF默认支持CSRF（跨站请求伪造）保护，只需要在程序中设置一个密钥。Flask-WTF使用这个密钥生成加密令牌，再用令牌验证表单中数据的真伪。（关于Flask项目的配置和初始化，后续文章会谈到）你可以使用app.config字典来存储配置变量：</p>
<pre data-anchor-id="bw8v"><code>app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'
</code></pre>
<p data-anchor-id="oe6k">或是从系统环境变量中获取</p>
<pre data-anchor-id="ttbo"><code>app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY')
</code></pre>
<p data-anchor-id="t7ag">很多重要的信息，比如邮箱账户和密码都不能直接下载程序里，而要设置系统环境变量，设置方法如下(Windows CMD)：</p>
<pre data-anchor-id="imvh"><code>set SECRET_KEY=very very hard to guess string
</code></pre>
<p data-anchor-id="ubur">后面的字符串不需要用任何符号括起来。</p>
<div class="md-section-divider"> </div>
<h2 id="表单" data-anchor-id="gxky">表单</h2>
<p data-anchor-id="ueuz">每个表单都用一个继承自Form的类表示，每个字段都用一个对象表示，每个对象可以附加多个验证函数。常见的验证函数有<code>Required()</code>，<code>Length()</code>，<code>Email()</code>等。 <br /> 一个登录表单示例：</p>
<pre data-anchor-id="40m3"><code>from flask_wtf import Form # 导入Form
from wtforms import StringField, SubmitField, RadioField, PasswordField, BooleanField # 导入字段
from wtforms.validators import Required, Length, Email # 导入验证函数
class LoginForm(Form):
    email = StringField(u'邮箱', validators=[Required(message= u'邮箱不能为空'), Length(1, 64), Email(message= u'请输入有效的邮箱地址，比如：username@domain.com')])
    password = PasswordField(u'密码', validators=[Required(message= u'密码不能为空')])
    remember_me = BooleanField(u'记住我')
    submit = SubmitField(u'登录')
</code></pre>
<ul data-anchor-id="u6qe">
<li>在验证函数中传入出错时的提示信息，覆盖默认的英文错误提示。</li>
<li>WTForms支持的<a href="http://wtforms.readthedocs.io/en/latest/fields.html#basic-fields" target="_blank">基本字段</a>。</li>
<li>WTForms支持的<a href="http://wtforms.readthedocs.io/en/latest/validators.html" target="_blank">验证函数</a>。</li>
</ul>
<div class="md-section-divider"> </div>
<h2 id="渲染表单" data-anchor-id="m6tx">渲染表单</h2>
<p data-anchor-id="digk">表单有很多种渲染方式。 <br /> 下面以一个撰写文章的表单为例。</p>
<div class="md-section-divider"> </div>
<h3 id="默认的渲染方式" data-anchor-id="x5xi">默认的渲染方式</h3>
<pre data-anchor-id="5cni"><code>&lt;form class="form" method="POST"&gt;
    {{ form.hidden_tag() }}
    {{ form.title.label }}{{ form.title() }}
    {{ form.body.label }}{{ form.body() }}
    {{ form.submit() }}
&lt;/form&gt;
</code></pre>
<p data-anchor-id="atar">这种方式渲染出来的表单没有样式，而且很繁琐（当然，你也可以使用for循环来遍历字段）。</p>
<div class="md-section-divider"> </div>
<h3 id="使用flask-bootstrap渲染" data-anchor-id="56a3">使用Flask-Bootstrap渲染</h3>
<p data-anchor-id="5rvq">Flask-Bootstrap提供了一个模板（wtf.html），可以让你快速生成样式良好的表单。</p>
<pre data-anchor-id="kn64"><code>{% import "bootstrap/wtf.html" as wtf %}
&lt;form class="form" method="POST"&gt;
    {{ form.hidden_tag() }}
    {{ wtf.form_field(form.title) }}
    {{ wtf.form_field(form.body) }}
    {{ wtf.form_field(form.submit) }}
&lt;/form&gt;
</code></pre>
<p data-anchor-id="d9oq">假如你不需要调整Boostrap的默认表单样式，只想要快速生成表单，那么Flask-Bootstrap还提供了一个强大的函数，只需要一行就可以快速生成表单）：</p>
<pre data-anchor-id="1yme"><code>{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}
</code></pre>
<div class="md-section-divider"> </div>
<h3 id="控制表单的样式" data-anchor-id="3gxa">控制表单的样式</h3>
<p data-anchor-id="it6d">对于样式，可以通过给表单增加id或class来实现。比如这样：</p>
<pre data-anchor-id="y867"><code>{{ form.body.label }}{{ form.body(class="post-body") }} # 默认渲染
{{ wtf.form_field(form.body, class="post-body") }} # 使用Flask-Bootstrap渲染
</code></pre>
<p data-anchor-id="gjhp">也可以在表单类里传入参数，像这样：</p>
<pre data-anchor-id="ap8p"><code>body = TextAreaField(u'正文', validators=[Required(u'内容不能为空！')], render_kw={'rows': 20, 'placeholder': u'你有什么想法？'})
</code></pre>
<p data-anchor-id="gh5b">在表单字段里传入一个render_kw字典，将HTML的参数和值以<strong>键值对</strong>的形式写入字典。<strong>WTForms2.1及以上版本</strong>适用。</p>
<p data-anchor-id="mbj8">这里需要注意的是，Flask-Bootstrap会给表单添加class来控制样式，这时你再<strong>通过render_kw传入已经被定义过的参数会失败</strong>。所以，如果要使用render_kw传入class，得确保表单字段使用默认的渲染方式。</p>
<div class="md-section-divider"> </div>
<h2 id="提交表单视图" data-anchor-id="idzp">提交表单视图</h2>
<p data-anchor-id="k048">这是一个添加文章的视图。</p>
<pre data-anchor-id="vddf"><code>@main.route('/new/post', methods=['GET', 'POST'])
@login_required
def new_post():
form = PostForm()
if form.validate_on_submit():
    post = Post(
    title = form.title.data,
    body = form.body.data)
    db.session.add(post)
    db.session.commit()
    return redirect(url_for('.post', id=post.id))
return render_template('edit/new_post.html', form=form)
</code></pre>
<div class="md-section-divider"> </div>
<h2 id="常见问题" data-anchor-id="jxtb">常见问题索引</h2>
<ol data-anchor-id="l9rd">
<li><a href="http://greyli.com/flask-set-a-single-page-two-multiple-forms/" target="_blank">一个页面里有多个表单</a></li>
<li>文件及多文件上传</li>
<li>文本编辑器支持</li>
</ol>
<h1 id="flask笔记文件及多文件上传" data-anchor-id="42ie"> </h1>
]]></content:encoded>
			<wfw:commentRss>https://greyli.com/flask-note-5-forms/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
