Flask 教程 第三章:Web表单
本文翻译自 The Flask Mega-Tutorial Part III: Web Forms
这是Flask Mega-Tutorial系列的第三部分,我将告诉你如何使用Web表单。
在第二章中我为应用主页创建了一个简单的模板,并使用诸如用户和用户动态的模拟对象。在本章中,我将解决这个应用程序中仍然存在的众多遗漏之一,那就是如何通过Web表单接受用户的输入。
Web表单是所有Web应用程序中最基本的组成部分之一。 我将使用表单来为用户发表动态和登录认证提供途径。
在继续阅读本章之前,确保你的microblog应用程序状态和上一章完结时一致,并且运行时不会报任何错误。
本章的GitHub链接为:Browse, Zip, Diff.
Flask-WTF简介
我将使用Flask-WTF插件来处理本应用中的Web表单,它对WTForms进行了浅层次的封装以便和Flask完美结合。这是本应用引入的第一个Flask插件,但绝不是最后一个。插件是Flask生态中的举足轻重的一部分,Flask故意设计为只包含核心功能以保持代码的整洁,并暴露接口以对接解决不同问题的插件。
Flask插件都是常规的Python三方包,可以使用pip
安装。 那就继续在你的虚拟环境中安装Flask-WTF吧:
(venv) $ pip install flask-wtf
配置
到目前为止,这个应用程序都非常简单,因此我不需要考虑它的配置。 但是,除了最简单的应用,你会发现Flask(也可能是Flask插件)为使用者提供了一些可自由配置的选项。你需要决定传入什么样的配置变量列表到框架中。
有几种途径来为应用指定配置选项。最基本的解决方案是使用app.config
对象,它是一个类似字典的对象,可以将配置以键值的方式存储其中。例如,你可以这样做:
app = Flask(__name__)
app.config['SECRET_KEY'] = 'you-will-never-guess'
# ... add more variables here as needed
上面的代码虽然可以为应用创建配置,但是我有松耦合的癖好。因此,我不会让配置和应用代码处于同一个部分,而是使用稍微复杂点的结构,将配置保存到一个单独的文件中。
使用类来存储配置变量,才是我真正的风格。我会将这个配置类存储到单独的Python模块,以保持良好的组织结构。下面就让你见识一下这个存储在顶级目录下,名为config.py的模块的配置类吧:
import os class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
简单的不像话,有没有? 配置设置被定义为Config
类中的属性。 一旦应用程序需要更多配置选项,直接依样画葫芦,附加到这个类上即可,稍后如果我发现需要多个配置集,则可以创建它的子类。现在则不用操心。
SECRET_KEY
是我添加的唯一配置选项,对大多数Flask应用来说,它都是极其重要的。Flask及其一些扩展使用密钥的值作为加密密钥,用于生成签名或令牌。Flask-WTF插件使用它来保护网页表单免受名为Cross-Site Request Forgery或CSRF(发音为“seasurf”)的恶意攻击。顾名思义,密钥应该是隐密的,因为由它产生的令牌和签名的加密强度保证,取决于除了可信维护者之外,没有任何人能够获得它。
密钥被定义成由or
运算符连接两个项的表达式。第一个项查找环境变量SECRET_KEY
的值,第二个项是一个硬编码的字符串。这种首先检查环境变量中是否存在这个配置,找不到的情况下就使用硬编码字符串的配置变量的模式你将会反复看到。在开发阶段,安全性要求较低,因此可以直接使用硬编码字符串。但是,当应用部署到生产服务器上的时候,我将设置一个独一无二且难以揣摩的环境变量,这样,服务器就拥有了一个别人未知的安全密钥了。
拥有了这样一份配置文件,我还需要通知Flask读取并使用它。可以在生成Flask应用之后,利用app.config.from_object()
方法来完成这个操作:
from flask import Flask
from config import Config app = Flask(__name__)
app.config.from_object(Config) from app import routes
导入Config
类的方式,乍一看可能会让人感到困惑,不过如果你注意到从flask
包导入Flask
类的过程,就会发现这其实是类似的操作。 显而易见,小写的“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的模块中。就让我们来定义用户登录表单来做一个开始吧,它会要求用户输入username和password,并提供一个“remember me”的复选框和提交按钮:
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模块的顶部从flask_wtf
导入了名为FlaskForm
的基类。
由于Flask-WTF插件本身不提供字段类型,因此我直接从WTForms包中导入了四个表示表单字段的类。每个字段类都接受一个描述或别名作为第一个参数,并生成一个实例来作为LoginForm
的类属性。
你在一些字段中看到的可选参数validators
用于验证输入字段是否符合预期。DataRequired
验证器仅验证字段输入是否为空。更多的验证器将会在未来的表单中接触到。
表单模板
下一步是将表单添加到HTML模板以便渲染到网页上。 令人高兴的是在LoginForm类中定义的字段支持自渲染为HTML元素,所以这个任务相当简单。 我将把登录模板存储在文件app/templates/login.html 中,代码如下:
{% 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 %}
一如第二章,在这个模板中我再次使用了extends
来继承base.html
基础模板。事实上,我将会对所有的模板继承基础模板,以保持顶部导航栏风格统一。
这个模板需要一个form参数的传入到渲染模板的函数中,form来自于LoginForm
类的实例化,不过我现在还没有编写它。
HTML<form>
元素被用作Web表单的容器。 表单的action
属性告诉浏览器在提交用户在表单中输入的信息时应该请求的URL。 当action
设置为空字符串时,表单将被提交给当前地址栏中的URL,即当前页面。 method
属性指定了将表单提交给服务器时应该使用的HTTP请求方法。 默认情况下是用GET
请求发送,但几乎在所有情况下,使用POST
请求会提供更好的用户体验,因为这种类型的请求可以在请求的主体中提交表单数据, GET
请求将表单字段添加到URL,会使浏览器地址栏变得混乱。
form.hidden_tag()
模板参数生成了一个隐藏字段,其中包含一个用于保护表单免受CSRF攻击的token
。 对于保护表单,你需要做的所有事情就是在模板中包括这个隐藏的字段,并在Flask配置中定义SECRET_KEY
变量,Flask-WTF会完成剩下的工作。
如果你以前编写过HTML Web表单,那么你会发现一个奇怪的现象——在此模板中没有HTML表单元素,这是因为表单的字段对象的在渲染时会自动转化为HTML元素。 我只需在需要字段标签的地方加上{{ form.<field_name>.label }}
,需要这个字段的地方加上{{ form.<field_name>() }}
。 对于需要附加HTML属性的字段,可以作为关键字参数传递到函数中。 此模板中的username和password字段将size
作为参数,将其作为属性添加到<input>
HTML元素中。 你也可以通过这种手段为表单字段设置class和id属性
表单视图
完成这个表单的最后一步就是编写一个新的视图函数来渲染上面创建的模板。
函数的逻辑只需创建一个form实例,并将其传入渲染模板的函数中即可,然后用/login URL来关联它。这个视图函数也存储到app/routes.py模块中,代码如下:
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
的语法看起来奇怪,这是Python函数或方法传入关键字参数的方式,左边的form
代表在模板中引用的变量名称,右边则是传入的form实例。这就是获取表单字段渲染结果的所有代码了。
在基础模板templates/base.html的导航栏上添加登录的链接,以便访问:
<div>
Microblog:
<a href="/index">Home</a>
<a href="/login">Login</a>
</div>
此时,你可以验证结果了。运行该应用,在浏览器的地址栏中输入http://localhost:5000/
,然后点击顶部导航栏中的“Login”链接来查看新的登录表单。 是不是非常炫酷?
接收表单数据
点击提交按钮,浏览器将显示“Method Not Allowed”错误。为什么呢? 这是因为之前的登录视图功能到目前为止只完成了一半的工作。 它可以在网页上显示表单,但没有逻辑来处理用户提交的数据。Flask-WTF可以轻松完成这部分工作, 以下是视图函数的更新版本,它接受和验证用户提交的数据:
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)
这个版本中的第一个新东西是路由装饰器中的methods
参数。 它告诉Flask这个视图函数接受GET
和POST
请求,并覆盖了默认的GET
。 HTTP协议规定对GET
请求需要返回信息给客户端(本例中是浏览器)。 本应用的所有GET
请求都是如此。 当浏览器向服务器提交表单数据时,通常会使用POST
请求(实际上用GET
请求也可以,但这不是推荐的做法)。之前的“Method Not Allowed”错误正是由于视图函数还未配置允许POST
请求。 通过传入methods
参数,你就能告诉Flask哪些请求方法可以被接受。
form.validate_on_submit()
实例方法会执行form校验的工作。当浏览器发起GET
请求的时候,它返回False
,这样视图函数就会跳过if
块中的代码,直接转到视图函数的最后一句来渲染模板。
当用户在浏览器点击提交按钮后,浏览器会发送POST
请求。form.validate_on_submit()
就会获取到所有的数据,运行字段各自的验证器,全部通过之后就会返回True
,这表示数据有效。不过,一旦有任意一个字段未通过验证,这个实例方法就会返回False
,引发类似GET
请求那样的表单的渲染并返回给用户。稍后我会在添加代码以实现在验证失败的时候显示一条错误消息。
当form.validate_on_submit()
返回True
时,登录视图函数调用从Flask导入的两个新函数。 flash()
函数是向用户显示消息的有效途径。 许多应用使用这个技术来让用户知道某个动作是否成功。我将使用这种机制作为临时解决方案,因为我没有基础架构来真正地登录用户。 显示一条消息来确认应用已经收到登录认证凭据,我认为对当前来说已经足够了。
登录视图函数中使用的第二个新函数是redirect()
。这个函数指引浏览器自动重定向到它的参数所关联的URL。当前视图函数使用它将用户重定向到应用的主页。
当你调用flash()
函数后,Flask会存储这个消息,但是却不会奇迹般地直接出现在页面上。模板需要将消息渲染到基础模板中,才能让所有派生出来的模板都能显示出来。更新后的基础模板代码如下:
<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()
注册过的消息列表。接下来的条件结构用来检查变量messages
是否包含元素,如果有,则在<ul>
元素中,为每条消息用<li>
元素来包裹渲染。这种渲染的样式结果看起来不会美观,之后会有主题讲到Web应用的样式。
闪现消息的一个有趣的属性是,一旦通过get_flashed_messages
函数请求了一次,它们就会从消息列表中移除,所以在调用flash()
函数后它们只会出现一次。
时机成熟,再次测试表单吧,将username和password字段留空并点击提交按钮来观察DataRequired
验证器是如何中断提交处理流程的。
完善字段验证
表单字段的验证器可防止无效数据被接收到应用中。 应用处理无效表单输入的方式是重新显示表单,以便用户进行更正。
如果你尝试过提交无效的数据,相信你会注意到,虽然验证机制查无遗漏,却没有给出表单错误的具体线索。下一个任务是通过在验证失败的每个字段旁边添加有意义的错误消息来改善用户体验。
实际上,表单验证器已经生成了这些描述性错误消息,所缺少的不过是模板中的一些额外的逻辑来渲染它们。
这是给username和password字段添加了验证描述性错误消息渲染逻辑之后的登录模板:
{% 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 %}
我做的唯一的改变是,在username和password字段之后添加for循环以便用红色字体来渲染验证器添加的错误信息。通常情况下,拥有验证器的字段都会用form.<field_name>.errors
来渲染错误信息。 一个字段的验证错误信息结果是一个列表,因为字段可以附加多个验证器,并且多个验证器都可能会提供错误消息以显示给用户。
如果你尝试在未填写username和password字段的情况下提交表单,就可以看到显眼的红色错误信息了。
生成链接
现在的登录表单已经相当完整了,但在结束本章之前,我想讨论在模板和重定向中包含链接的妥当方法。 到目前为止,你已经看到了一些定义链接的例子。 例如,这是当前基础模板中的导航栏代码:
<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')
# ...
直接在模板和源文件中硬编码链接存在隐患,如果有一天你决定重新组织链接,那么你将不得不在整个应用中搜索并替换这些链接。
为了更好地管理这些链接,Flask提供了一个名为url_for()
的函数,它使用URL到视图函数的内部映射关系来生成URL。 例如,url_for('login')
返回/login
,url_for('index')
返回/index
。 url_for()
的参数是endpoint名称,也就是视图函数的名字。
你可能会问,为什么使用函数名称而不是URL? 事实是,URL比起视图函数名称变更的可能性更高。 稍后你会了解到的第二个原因是,一些URL中包含动态组件,手动生成这些URL需要连接多个元素,枯燥乏味且容易出错。 url_for()
生成这种复杂的URL就方便许多。
因此,从现在起,一旦我需要生成应用链接,我就会使用url_for()
。基础模板中的导航栏部分代码变更如下:
<div>
Microblog:
<a href="{{ url_for('index') }}">Home</a>
<a href="{{ url_for('login') }}">Login</a>
</div>
login()
视图函数也做了相应变更:
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'))
# ...
Flask 教程 第三章:Web表单的更多相关文章
- flask 利用flask_wtf扩展 创建web表单
在Flask中,为了处理web表单,我们一般使用Flask-WTF扩展,它封装了WTForms,并且它有验证表单数据的功能 创建语句格式: startTime = DateTimeField('计划开 ...
- HTML第三章:表单
第三章:表单 表单标签form:<form></form>//相当于一张记录用户信息的单子 常用属性:method:表单的提交方式,常用的值有两个 ...
- 第四章 Web表单
4.1 跨站请求伪造保护 安装flask-wtf app = Flask(__name__) app.config['SECRET_KEY'] = 'hard to guess string' 密钥不 ...
- GXT之旅:第三章:表单和窗口(4)——表单的提交和RPC
表单使用HTTP提交 表单有两种提交方式,第一种就是传统的HTTP提交. 最直接的步骤就是: 使用FormPanel的setAction()方法,去定义submit的URL 使用FormPanel的i ...
- 第三章 XHTML 表单
1.表单的主要作用在于在网页上提供一个图形用户界面,以采集和提交用户输入的数据. 2.HTML表单元素和属性可以分为两种类型:定义表单整体结构,使浏览器知道如何处理表单数据的元素:创建输入控件的元素. ...
- ↗☻【HTML5秘籍 #BOOK#】第4章 Web表单
from元素用于组织所有表单部件,负责告诉浏览器把数据提交到哪里,方法是在action属性中提供一个URL.假如你只想在客户端使用JavaScript操作表单,那么只要在action属性中指定一个#即 ...
- Flask之模板web表单
3.3 Web表单: web表单是web应用程序的基本功能. 它是HTML页面中负责数据采集的部件.表单有三个部分组成:表单标签.表单域.表单按钮.表单允许用户输入数据,负责HTML页面数据采集,通过 ...
- Flask教程 —— Web表单(上)
第二章中介绍的request对象公开了所有客户端发送的请求信息.特别是request.form可以访问POST请求提交的表单数据. 尽管Flask的request对象提供的支持足以处理web表单,但依 ...
- 第三章:Web表单
感谢作者 –> 原文链接 本文翻译自 The Flask Mega-Tutorial Part III: Web Forms 这是Flask Mega-Tutorial系列的第三部分,我将告诉你 ...
随机推荐
- [问题排查]通过调度系统远程执行脚本,报mysql command not found异常
今天在公司使用LS调度系统(百度内部的工具),执行远程脚本的时候,每次都失败. 脚本内容比较简单,其实就是将HDFS(AFS)中的数据插入到Palo(Doris)数据库中,脚本如下: mysql -h ...
- Redis 数据类型及应用场景
一. redis 特点 所有数据存储在内存中,高速读写 提供丰富多样的数据类型:string. hash. set. sorted set.bitmap.hyperloglog 提供了 AOF 和 R ...
- GO-结构体(类似python中的类)
一.结构体有名属性 package main import "fmt" func main(){ type Sb struct { name string //属性名,数据类型 a ...
- Pycharm自带Git实现版本管理
之前一直使用本地的git客户端,通过命令来上传.下载代码到Gitlab:每次都需要启动git客户端,敲git命令来完成,不够灵活,因为强大的Pycharm就自带git功能,可以直接在Pycharm完成 ...
- Java中的“浅复制”与“深复制”
复制 将一个对象的引用复制给另一个对象,一共有三种方式.第一种方式是直接赋值,第二种方式是浅复制,第三种方式是深复制. 1.直接赋值 在Java中,A a1 = a2,这实际上复制的是引用,也就是说 ...
- PHP mysqli_fetch_object MySQLi 函数
定义和用法 mysqli_fetch_object - 返回结果集的当前行作为对象 版本支持 PHP4 PHP5 PHP7 不支持 支持 支持 语法 mysqli_fetch_object ( mys ...
- [转]VB.net中 excel 的range方法
本文转自:https://blog.csdn.net/bigheadsheep/article/details/7711235 第五章 Range对象基本操作应用示例(1)Range对象可能是VBA代 ...
- JS&Jquery基础之窗口对象的关系总结
1.top 该变更永远指分割窗口最高层次的浏览器窗口.如果计划从分割窗口的最高层次开始执行命令,就可以用top变量.2.opener opener用于在window.open的页面引用执行该wi ...
- Transportable tablespace on standby (Doc ID 788176.1)
APPLIES TO: Oracle Database - Enterprise Edition - Version 10.2.0.1 to 10.2.0.4 [Release 10.2]Oracle ...
- SSH的本地、远程、动态端口转发实验笔记
SSH端口转发 SSH 会自动加密和解密所有 SSH 客户端与服务端之间的网络数据.但是,SSH 还能够将其他 TCP 端口的网络数据通过 SSH 链接来转发,并且自动提供了相应的加密及解密服务.这一 ...