Flask开发系列之Web表单
简单示例
from flask import Flask, request, render_template app = Flask(__name__) @app.route('/', methods=['GET', 'POST']) def home(): return render_template('home.html') @app.route('/signin', methods=['GET']) def signin_form(): return render_template('form.html') @app.route('/signin', methods=['POST']) def signin(): # receive the data from submit username = request.form['username'] password = request.form['password'] if username == 'admin' and password == 'password': return render_template('signin-ok.html', username=username) return render_template('form.html', message='Bad username or password', username=username) if __name__ == '__main__': app.run()
<html> <head> <title>Please Sign Intitle> head> <body> {% if message %} <p style="color:red">{{ message }}p> {% endif %} <form action="/signin" method="post"> <p>Please sign in:p> <p><input name="username" placeholder="Username" value="{{ username }}">p> <p><input name="password" placeholder="Password" type="password" value="{{ password }}">p> <p><button type="submit">Sign Inbutton>p> form> body> html>
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1>hello,{{ username }}h1>
body>
html>
跨站请求伪造保护
默认情况下,Flask-WTF 能保护所有表单免受跨站请求伪造(Cross-Site Request Forgery,
CSRF)的攻击。恶意网站把请求发送到被攻击者已登录的其他网站时就会引发 CSRF 攻击。
为了实现 CSRF 保护,Flask-WTF 需要程序设置一个密钥。Flask-WTF 使用这个密钥生成
加密令牌,再用令牌验证请求中表单数据的真伪。设置密钥的方法如示例:
app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'
app.config 字典可用来存储框架、扩展和程序本身的配置变量。使用标准的字典句法就能
把配置值添加到 app.config 对象中。这个对象还提供了一些方法,可以从文件或环境中导
入配置值。
SECRET_KEY 配置变量是通用密钥,可在 Flask 和多个第三方扩展中使用。如其名所示,加
密的强度取决于变量值的机密程度。不同的程序要使用不同的密钥,而且要保证其他人不
知道你所用的字符串。
表单类
使用 Flask-WTF 时,每个 Web 表单都由一个继承自 Form 的类表示。这个类定义表单中的
一组字段,每个字段都用对象表示。字段对象可附属一个或多个验证函数。验证函数用来
验证用户提交的输入值是否符合要求。
from flask_wtf import FlaskForm from wtforms import StringField, SubmitField, PasswordField,TextAreaField,HiddenFieldfrom wtforms.validators import DataRequired, Length, Email class NameForm(FlaskForm): title = StringField('Title', validators=[DataRequired()]) category = StringField('Category') content = TextAreaField('Content') post_id = HiddenField('post_id') submit = SubmitField('submit')
StringField 构造函数中的可选参数 validators 指定一个由验证函数组成的列表,在接受
用户提交的数据之前验证数据。验证函数 Required() 确保提交的字段不为空。
WTForms支持的HTML标准字段
字段类型 说 明
StringField 文本字段
TextAreaField 多行文本字段
PasswordField 密码文本字段
HiddenField 隐藏文本字段
DateField 文本字段,值为 datetime.date 格式
DateTimeField 文本字段,值为 datetime.datetime 格式
IntegerField 文本字段,值为整数
DecimalField 文本字段,值为 decimal.Decimal
FloatField 文本字段,值为浮点数
BooleanField 复选框,值为 True 和 False
RadioField 一组单选框
SelectField 下拉列表
SelectMultipleField 下拉列表,可选择多个值
FileField 文件上传字段
SubmitField 表单提交按钮
FormField 把表单作为字段嵌入另一个表单
FieldList 一组指定类型的字段
WTForms验证函数
验证函数 说 明
Email 验证电子邮件地址
EqualTo 比较两个字段的值;常用于要求输入两次密码进行确认的情况
IPAddress 验证 IPv4 网络地址
Length 验证输入字符串的长度
NumberRange 验证输入的值在数字范围内
Optional 无输入值时跳过其他验证函数
Required 确保字段中有数据
Regexp 使用正则表达式验证输入值
URL 验证 URL
AnyOf 确保输入值在可选值列表中
NoneOf 确保输入值不在可选值列表中
表单渲染
两种方式:
-
flask-bootstrap渲染
-
一般渲染(常用)
flask-bootstrap渲染
flask-boostrap是bootstrap的flask扩张,它可以提供wtf.html文件中的form_field函数来进行渲染:
{% import "bootstrap/wtf.html" as wtf %} <form class="form" method="POST"> {{ form.hidden_tag() }} {{ wtf.form_field(form.title) }} {{ wtf.form_field(form.content) }} ... {{ wtf.form_field(form.submit) }} form>
注意,如果有多个隐藏字段,可以使用form.hidden_tag()渲染所以隐藏字段。
另外Flask-WTF支持跨站请求伪造保护,表单类创建时会自动创建一个CSRF字段,你需要在表单里渲染这个字段:{{ form.csrf_token }}。
还有一种快速渲染方式:
{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}
一般渲染(常用)
直接在对应的html模版中引入字段:
{% extends "base.html" %} {% block title %}Flask{% endblock %} {% block body %}{% endblock %}
<html> <head> {% block head %} <title>{% block title %}{% endblock %} - My Applicationtitle> {% endblock %} head> <body> {% block body %} {% endblock %} body> html>
另外还可以在字段中加入有些属性,比如要加入class:
form.title(class_="form-control")
转化为html的效果:
<input type="text" name="title" value="title" class="form-control" id="title" />
上面的渲染如果有很多字段的话,一个个写出来会觉得很繁琐,在实际开发中,可以创建一个_form.html文件来处理。
_form.html
:
{% macro render(form) -%} {% for field in form %} {% if field.type in ['CSRFTokenField', 'HiddenField'] %} {{ field() }} {% elif field.type == "BooleanField" %} <div class="checkbox"> <label> {{ field() }} {{ field.label }} label> div> {% elif field.type == "RadioField" %} {{ field.label }} {% for subfield in field %} <div class="radio"> <label>{{ subfield }} {{ subfield.label }}label> div> {% endfor %} {% else %} <div class="clearfix {% if field.errors %}has-error{% endif %} form-group"> {{ field.label }} {% if field.type == "TextAreaField" %} {{ field(class_="form-control", rows=10) }} {% else %} {{ field(class_="form-control") }} {% endif %} {% if field.errors or field.help_text %} <span class="help-block"> {% if field.errors %} {{ field.errors|join(' ') }} {% else %} {{ field.help_text }} {% endif %} span> {% endif %} div> {% endif %} {% endfor %} {% endmacro %}
接着在对应的html模版中进行引用 :
{% import "_form.html" as forms %} <form method="POST" action=""> {{ forms.render(form) }}
一个简单的例子
from flask import Flask,render_template from flask_wtf import FlaskForm from wtforms import StringField, SubmitField from flask_bootstrap import Bootstrap from wtforms.validators import Required app = Flask(__name__) app.config["SECRET_KEY"] = "12345678" bootstrap = Bootstrap(app) class NameForm(FlaskForm): name = StringField('What is your name?', validators=[Required()]) submit = SubmitField('Submit') @app.route('/', methods=['GET', 'POST']) def index(): name = None #实例化表单类 form = NameForm() # 如果提交的数据验证通过,则返回True if form.validate_on_submit(): name = form.name.data form.name.data = '' return render_template('index.html', form=form, name=name) if __name__ == '__main__': app.run(debug=True) # Flask提供的render_template函数把Jinja2模板引擎集成到了程序中。 # render_template函数的第一个参数是模板的文件名。 # 随后的参数都是键值对,表示模板中变量对应的真实值。
{% extends "base.html" %} {% import "bootstrap/wtf.html" as wtf %} {% block title %}Flasky{% endblock %} {% block page_content %} <div class="page-header"> <h1>Hello, {% if name %}{{ name }}{% else %}Stranger{% endif %}!h1> div> {{ wtf.quick_form(form) }} {% endblock %}
{% extends "bootstrap/base.html" %} {% block title %}Flasky{% endblock %} {% block navbar %} <div class="navbar navbar-inverse" role="navigation"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <span class="sr-only">Toggle navigationspan> <span class="icon-bar">span> <span class="icon-bar">span> <span class="icon-bar">span> button> <a class="navbar-brand" href="/">Flaskya> div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li><a href="/">Homea>li> ul> div> div> div> {% endblock %} {% block content %} <div class="container"> {% block page_content %}{% endblock %} div> {% endblock %}
重定向和用户会话
from flask import Flask, render_template, session, redirect, url_for @app.route('/', methods=['GET', 'POST']) def index(): form = NameForm() if form.validate_on_submit(): session['name'] = form.name.data return redirect(url_for('index')) return render_template('index.html', form=form, name=session.get('name'))
前面到的代码存在一个可用性问题。用户输入名字后提交表单,然后点击浏览器的刷 新按钮,会看到一个莫名其妙的警告,要求在再次提交表单之前进行确认。之所以出现这 种情况,是因为刷新页面时浏览器会重新发送之前已经发送过的最后一个请求。如果这个 请求是一个包含表单数据的 POST 请求,刷新页面后会再次提交表单。大多数情况下,这并 不是理想的处理方式。 很多用户都不理解浏览器发出的这个警告。基于这个原因,最好别让 Web 程序把 POST 请 求作为浏览器发送的最后一个请求。 这种需求的实现方式是,使用重定向作为 POST 请求的响应,而不是使用常规响应。重定 向是一种特殊的响应,响应内容是 URL,而不是包含 HTML 代码的字符串。浏览器收到 这种响应时,会向重定向的 URL 发起 GET 请求,显示页面的内容。这个页面的加载可能 要多花几微秒,因为要先把第二个请求发给服务器。除此之外,用户不会察觉到有什么不 同。现在,最后一个请求是 GET 请求,所以刷新命令能像预期的那样正常使用了。这个技 巧称为 Post/ 重定向 /Get 模式。 但这种方法会带来另一个问题。程序处理 POST 请求时,使用 form.name.data 获取用户输 入的名字,可是一旦这个请求结束,数据也就丢失了。因为这个 POST 请求使用重定向处 理,所以程序需要保存输入的名字,这样重定向后的请求才能获得并使用这个名字,从而构建真正的响应。 程序可以把数据存储在用户会话中,在请求之间“记住”数据。用户会话是一种私有存 储,存在于每个连接到服务器的客户端中。用户会话,它是请求上下文中的变量,名为 session ,像标准的 Python 字典一样操作。
注意:默认情况下,用户会话保存在客户端 cookie 中,使用设置的 SECRET_KEY 进
行加密签名。如果篡改了 cookie 中的内容,签名就会失效,会话也会随之
失效。
代码分析
局部变量 name 被用于存储用户在表单中输入的名字。这个变量现 在保存在用户会话中,即 session['name'] ,所以在两次请求之间也能记住输入的值。 现在,包含合法表单数据的请求最后会调用 redirect() 函数。 redirect() 是个辅助函数, 用来生成 HTTP 重定向响应。 redirect() 函数的参数是重定向的 URL,这里使用的重定向 URL 是程序的根地址,因此重定向响应本可以写得更简单一些,写成 redirect('/') ,但 却会使用 Flask 提供的 URL 生成函数 url_for() 。推荐使用 url_for() 生成 URL,因为这 个函数使用 URL 映射生成 URL,从而保证 URL 和定义的路由兼容,而且修改路由名字后 依然可用。 url_for() 函数的第一个且唯一必须指定的参数是端点名,即路由的内部名字。默认情 况下,路由的端点是相应视图函数的名字。在这个示例中,处理根地址的视图函数是 index() ,因此传给 url_for() 函数的名字是 index 。 最后一处改动位于 render_function() 函数中,使用 session.get('name') 直接从会话中读 取 name 参数的值。和普通的字典一样,这里使用 get() 获取字典中键对应的值以避免未找 到键的异常情况,因为对于不存在的键, get() 会返回默认值 None 。
1. https://greyli.com/flask-form-create-and-render/
2. WTForms官方文档
3. http://flask.pocoo.org/docs/1.0/patterns/wtforms/
4. https://stackoverflow.com/questions/20905188/flask-wtforms-validation-always-false