最近在Flask Web Development作者博客看到第二版Flask Mega-Tutorial已在2017年底更新,现翻译给大家参考,希望帮助大家学习flask。
这是Flask Mega-Tutorial系列的第三章,其中我将告诉您如何使用Web表单(flask-WTF)。
供您参考,以下是本系列文章的列表。
- 第1章:Hello, World!
- 第2章:模板
- 第3章:Web表单(本文)
- 第4章:数据库
- 第5章:用户登录
- 第6章:配置文件页面和头像
- 第7章:错误处理
- 第8章:关注与被关注
- 第9章:分页
- 第10章:电子邮件支持
- 第11章:整容
- 第12章:日期和时间
- 第13章:I18n和L10n
- 第14章:Ajax
- 第15章:大型应用程序结构
- 第16章:全文搜索
- 第17章:在Linux上部署
- 第18章:在Heroku上部署
- 第19章:Docker容器上的部署
- 第20章:一些JavaScript Magic
- 第21章:用户通知
- 第22章:后台工作
- 第23章:应用程序编程接口(API)
在第2章中我为应用程序的主页创建了一个简单的模板,并使用假对象作为我尚未拥有的东西的占位符,如用户或博客文章。在本章中,我将讨论我在此应用程序中仍然存在的众多漏洞之一,特别是如何通过Web表单接受用户的输入。
Web表单是任何Web应用程序中最基本的构建块之一。我将使用表单来允许用户提交博客帖子,以及登录应用程序。
在继续本章之前,请确保您已安装上一章中的微博应用程序,并且您可以毫无错误地运行它。
Flask-WTF简介
为了处理这个应用程序中的Web表单,我将使用Flask-WTF扩展,这是一个围绕WTForms包的薄包装,可以很好地与Flask集成。这是我向你呈现的第一个Flask扩展,但它不会是最后一个。扩展是Flask生态系统中非常重要的一部分,因为它们提供了Flask不考虑的问题的解决方案。
Flask扩展是与pip一起安装的常规Python包。您可以继续在虚拟环境中安装Flask-WTF:
(venv) $ pip install flask-wtf
配置
到目前为止,应用程序非常简单,因此我不需要担心它的配置。但对于除此之外的任何应用程序,您将发现Flask(还有您使用的Flask扩展)在如何执行操作方面提供了一些自由,并且您可以通过配置环境变量,将变量传递给框架,以此来配置扩展和应用。
应用程序有多种格式可指定配置选项。最基本的解决方案是将变量定义为app.config['key']
,使用字典样式来处理变量。例如,你可以这样做:
app = Flask(__name__)
app.config['SECRET_KEY'] = 'you-will-never-guess'
# ... add more variables here as needed
虽然上面的语法足以为Flask创建配置选项,但我喜欢强制执行关注点分离的原则,所以不要将配置放在创建应用程序的相同位置,而是使用一个稍微复杂一点的结构来允许我将我的配置保存在单独的文件中。
使用类来存储配置变量,是我非常喜欢的格式,因为它是非常可扩展的。为了保持组织良好,我将在一个单独的Python模块中创建配置类。您可以在下面看到此应用程序的新配置类,它存储在顶级目录的config.py模块中。
# config.py: Secret key configuration
import os
class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
很简单吧?配置设置被定义为类中的类变量Config
。由于应用程序需要更多配置项,因此可以将它们添加到此类中,稍后如果我发现需要有多个配置集,我可以创建它的子类。但是不要担心这一点。
作为唯一配置项添加的SECRET_KEY
配置变量是大多数Flask应用程序中的一个重要部分。Flask及其一些扩展使用密钥的值作为加密密钥,可用于生成签名或令牌。
Flask-WTF扩展使用它来保护Web表单不受一种称为跨站请求伪造或CSRF(发音为“seasurf”)的恶意攻击。顾名思义,密钥应该是保密的,因为使用它生成的令牌和签名的强度取决于除应用程序的维护者之外的任何人都不知道它。
密钥的值被设置为具有两个术语的表达式,由or
运算符连接。第一个查找环境变量的值,也称为SECRET_KEY
。第二个,只是一个硬编码的字符串。对于配置变量,您会看到我经常重复这种模式。我们的想法是首选来自环境变量的值,但如果环境没有定义变量,则使用硬编码字符串。在开发此应用程序时,安全性要求很低,因此您可以忽略此设置并使用硬编码字符串。但是当这个应用程序部署在生产服务器上时,我将在环境中设置一个独特且难以猜测的值,以便服务器具有其他人不知道的安全密钥。
现在我有一个配置文件,我需要告诉Flask读取并应用它。这可以在Flask应用程序实例创建后,使用app.config.from_object()方法
立即完成:
# app/__init__.py: Flask configuration
from flask import Flask
from config import Config
app = Flask(__name__)
app.config.from_object(Config)
from app import routes
我导入
Config
类的方式起初可能看起来很混乱,但是如果你看一下如何Flask
从flask
包中导入类(大写“F”)(小写“f”)你会注意到我在做同样的事情配置。小写的“config”是Python模块config.py的名称,显然具有大写“C”的那个是实际的类。
如上所述,可以使用字典语法访问配置项app.config
。在这里,您可以看到使用Python解释器的快速会话,我在其中检查密钥的值是什么:
>>> from microblog import app
>>> app.config['SECRET_KEY']
'you-will-never-guess'
用户登录表
Flask-WTF扩展使用Python类来表示Web表单。表单类只是将表单的字段定义为类变量。
再次考虑到关注点分离,我将使用新的app/forms.py模块来存储Web表单类。首先,让我们定义一个用户登录表单,要求用户输入用户名和密码。表单还将包含“记住我”复选框和提交按钮:
# app/forms.py: Login form
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired
class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Remember Me')
submit = SubmitField('Sign In')
大多数Flask扩展使用
flask_<name>作为
其顶级导入符号的命名约定。在这种情况下,Flask-WTF的所有符号都在下面flask_wtf
。这里从app/forms.py顶部导入FlaskForm
基类。
因为Flask-WTF扩展不提供自定义版本,我用于表示此表单的四个字段类型类是直接从WTForms包导入的。对于每个字段,在LoginForm
类中将被创建一个对象作为类变量。每个字段都有一个描述或标签作为第一个参数。
您在某些字段中看到的可选参数validators
用于将验证行为附加到字段。该DataRequired
只是简单地检查该字段不会为空。还有更多的验证器,其中有一些将会被用来作其他表单验证。
表单模板
下一步是将表单添加到HTML模板。好消息是,LoginForm
类中定义的字段知道如何将自己呈现为HTML,因此这项任务非常简单。您可以在下面看到登录模板,我将在文件app/templates/login.html中存储该模板:
app/templates/login.html: Login form template
{% extends "base.html" %}
{% block content %}
<h1>Sign In</h1>
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
{{ form.username(size=32) }}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=32) }}
</p>
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}
对于这个模板,我再次继承第2章中base.html
模板。实际上,我将使用所有模板继承base.html,这样可以确保布局的一致。
此模板需要将从LoginForm
类中实例化的表单对象form
作为参数提供。这个form
参数将由登录视图函数返回,暂未完成此功能。
HTML:
<form>
元素用作Web表单的容器。
action
属性,
用于告知浏览器用户在提交表单中输入的信息时应使用的URL。当操作设置为空时,表单将提交到当前位于地址栏中的URL。
method
属性,指定在将表单提交到服务器时应使用的HTTP请求方法。默认情况下是通过GET
请求发送它,但几乎在所有情况下,使用POST
请求都可以获得更好的用户体验,因为此类请求可以在当前页面URL中提交表单数据。而GET
请求会将表单字段添加到URL,使浏览器地址栏变得混乱。
novalidate
属性,用于告知Web浏览器不对此表单中的字段应用验证,验证将留给服务器处理。使用novalidate
完全是可选的,但是对于这个表单,设置它是很重要的,因为这将允许您在本章后面测试服务器端验证。
form.hidden_tag()
模板参数生成一个隐藏字段,其中包括用来防止CSRF攻击形式的令牌。要保护表单,您需要在表单中包含此隐藏字段并在Flask配置中定义变量SECRET_KEY
,Flask-WTF会为您完成剩下的工作。
如果您以前编写过HTML Web表单,您可能会发现此模板中没有HTML字段很奇怪。这是因为表单对象中的字段知道如何将自己呈现为HTML。我只需包括{{ form.<field_name>.label }}
,{{ form.<field_name>() }}即可
。对于需要其他HTML属性的字段,可以将这些属性作为参数传递。此模板中的username和password字段将size
作为参数添加到HTML<input>
元素作为属性。您还可以用这种方法将CSS类或ID附加到表单字段。
表单视图
在浏览器中看到此表单之前,还需要在应用程序中编写一个新的视图函数,该函数将渲染上一节中的模板。
因此,让我们编写一个URL 映射到 /login 的视图函数,并将其传递给模板进行渲染。此视图函数可以放在app/routes.py:
# app/routes.py: Login view function
from flask import render_template
from app import app
from app.forms import LoginForm
# ...
@app.route('/login')
def login():
form = LoginForm()
return render_template('login.html', title='Sign In', form=form)
首先
从forms.py导入LoginForm
类,从中实例化一个对象,然后将其发送到模板。form=form
可能看起来很奇怪,但是这样简单的将LoginForm()实例化的对象form("="右侧)传递给模板参数form
("="左侧)。这样就能获取表单字段所需的全部内容。
为了便于访问登录表单,可以在基本模板的导航栏中加入Login链接:
app/templates/base.html: Login link in navigation bar
<div>
Microblog:
<a href="/index">Home</a>
<a href="/login">Login</a>
</div>
此时,您可以运行该应用程序并在Web浏览器中查看该表单。运行应用程序后,键入http://localhost:5000/
浏览器的地址栏,然后单击顶部导航栏中的“Login”链接以查看新的登录表单。很酷,对吗?
接收表单数据
如果您尝试按下提交按钮,浏览器将显示“方法不允许”错误。这是因为到目前为止,上一节中的登录视图功能执行了一半的工作。它可以在网页上显示表单,但它没有处理用户提交的数据的逻辑。这是Flask-WTF使这项工作变得非常简单的另一个领域。以下是视图函数的更新版本,它接受并验证用户提交的数据
# app/routes.py: Receiving login credentials
from flask import render_template, flash, redirect
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
flash('Login requested for user {}, remember_me={}'.format(
form.username.data, form.remember_me.data))
return redirect('/index')
return render_template('login.html', title='Sign In', form=form)
这个版本的第一个新东西是@app.route装饰器中的参数
methods
。这告诉Flask视图函数接受'GET'和'
POST'
请求,覆盖默认值(即只接受GET
请求)。
HTTP协议声明GET
请求是将信息返回给客户端的请求(在本例中为Web浏览器)。到目前为止,应用程序中的所有请求都属于'GET'。POST
通常在浏览器向服务器提交表单数据时使用(实际上,GET
请求也可用于此目的,但不建议使用此方法)。
刚才浏览器显示的“Method Not Allowed”错误,因为浏览器尝试发送POST
请求,而@app.route装饰器未配置'POST'。通过提供methods
参数,您告诉Flask应该接受哪些请求方法。
form.validate_on_submit()
方法完成所有表单处理工作。当浏览器发送GET并接收
带有表单的网页请求时,此方法将返回False
,在这种情况下,会跳过if语句,并执行函数的最后一行来渲染模板。
当浏览器由于用户按下提交按钮而发送POST
请求时,form.validate_on_submit()
将收集所有数据,运行附加到字段的所有验证器,如果一切正常,它将返回True
,表明数据有效且可以由应用程序处理。只要有一个字段未通过验证,则该函数将返回False
,这将导致将表单渲染给用户,就像在GET
请求案例中一样。稍后我将在验证失败时添加错误消息。
当form.validate_on_submit()
返回 True
,登录视图函数将调用两个从flask导入的新函数。flash()的作用:
向用户显示消息。许多应用程序使用flash()
让用户知道某些操作是否成功。在这种情况下,我将使用此机制作为临时解决方案,因为我还没有为用户登录建立所需的所有基础结构。我现在能做的是显示一条消息,确认应用程序已收到凭据。
登录视图功能中使用的第二个新函数是redirect()
。此函数的作用是Web浏览器重定向其他页面。此视图函数使用它将用户重定向到'/index'。
当您调用flash()
函数时,Flask会存储该消息,但闪烁的消息不会出现在网页中。模板需要以适用于布局的方式呈现这些闪烁的消息。我将把这些消息添加到base.html,以便所有模板都继承此功能。这是更新的基本模板:
app/templates/base.html: Flashed messages in base template
<html>
<head>
{% if title %}
<title>{{ title }} - microblog</title>
{% else %}
<title>microblog</title>
{% endif %}
</head>
<body>
<div>
Microblog:
<a href="/index">Home</a>
<a href="/login">Login</a>
</div>
<hr>
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</body>
</html>
在模板上下文中,使用with
结构将调用get_flashed_messages()的结果分配给
messages
变量。get_flashed_messages()
函数来自Flask,并返回之前已在flash()
注册的所有消息的列表。if语句是用来检查messages
是否为空,如果非空,每个消息渲染为一个<ul>
元素中的<li>
列表项。这种渲染风格看起来不太好,稍后将介绍web的其它样式。
这些闪烁消息的一个有趣属性是,一旦它门通过
get_flashed_messages
函数被请求一次,它们就会从消息列表中删除,因此它们在flash()
调用函数后只出现一次。
这是再次测试表单如何工作的好时机。确保您尝试将username或password字段为空的表单提交,以查看DataRequired
验证程序如何暂停提交过程。
改进字段验证
表单字段的验证可防止无效数据被接受到应用程序中。处理无效表单的方式是重新显示表单,让用户进行必要的更正。
如果您尝试提交无效数据,我确信您注意到虽然验证机制运行良好,但没有指示用户表单有问题,用户只需返回表单。下一个任务是通过在验证失败的每个字段旁边添加有意义的错误消息来改善用户体验。
事实上,表单验证器已经生成了这些错误提示消息,只是没有在模板中渲染他们。
以下是在usename和password字段中添加了字段验证消息的登录模板:
app/templates/login.html: Validation errors in login form template
{% extends "base.html" %}
{% block content %}
<h1>Sign In</h1>
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
{{ form.username(size=32) }}<br>
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=32) }}<br>
{% for error in form.password.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}
这里唯一更改是在usename和password字段之后添加for循环,这些字段用于呈现验证器以红色添加的错误消息。一般来说,任何带有验证器的字段之后,都会添加form.<field_name>.errors
。这将是一个列表,因为字段可以附加多个验证器,并且可能有多个验证器向用户显示错误消息。
如果您尝试使用空用户名或密码提交表单,现在您将收到一条红色的错误消息。
生成URL
登录表单现在相当完整,但在结束本章之前,我想讨论在模板和重定向中包含URL的更好的方式。到目前为止,您已经看到了一些定义URL的实例。例如,这是当前基本模板中的导航栏:
<div>
Microblog:
<a href="/index">Home</a>
<a href="/login">Login</a>
</div>
登录视图函数还定义了传递给redirect()
函数的链接:
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
# ...
return redirect('/index')
# ...
直接在模板和源文件中编写URL的一个问题是,如果有一天您决定重新组织URL,那么您将不得不在整个应用程序中搜索并替换这些URL。
为了更好地控制这些URL,Flask提供了一个名为url_for()
的函数,它使用URL的内部映射来生成URL以查看函数。例如,url_for('login')
返回'/login',
url_for('index')
返回'/index'
。 url_for()的
参数是端点名称,它是视图函数的函数名。
您可能会问为什么使用函数名而不是URL。事实上,URL比视图函数名称更有可能发生变化,这些名称完全是内部的。第二个原因是,您将在后面了解到,某些URL中包含动态组件,因此手动生成这些URL需要连接多个元素,这很繁琐且容易出错。该url_for()
还能够产生这些复杂的URL。
所以从现在开始,每次我需要生成应用程序URL时,我都会使用url_for()
。然后基本模板中的导航栏变为:
app/templates/base.html: Use url\_for() function for links
<div>
Microblog:
<a href="{{ url_for('index') }}">Home</a>
<a href="{{ url_for('login') }}">Login</a>
</div>
这是更新的login()
视图功能:
app/routes.py: Use url\_for() function for links
from flask import render_template, flash, redirect, url_for
# ...
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
# ...
return redirect(url_for('index'))
# ...
原文链接:https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-iii-web-forms