【Flask】 WTForm表单编程
WTForm表单编程
在网页中,为了和用户进行信息交互总是不得不出现一些表单。flask设计了WTForm表单库来使flask可以更加简便地管理操作表单数据。WTForm中最重要的几个概念如下:
Form类,开发者自定义的表单必须继承自Form类或者其子类。Form类最主要的功能是通过其所包含的Field类提供对表单内数据的快捷访问方式。
各种Field类,即字段。一般而言每个Field类都对应一个input的HTML标签。比如WTForm自带的一些Field类比如BooleanField就对应<input type="checkbox">,SubmitField就对应<input type="submit">等等。
Validator类。这个类用于验证用户输入的数据的合法性。比如Length验证器可以用于验证输入数据的长度,FileAllowed验证上传文件的类型等等。
另外,flask为了防范csfr(cross-site request forgery)攻击,默认在使用flask-wtf之前要求app一定要设置过secret_key。最简单地可以通过app.config['SECRET_KEY'] = 'xxxx'来配置。app的配置涉及到如何架构整个项目目录,在以后再讲,这里默认这个SECRET_KEY已经配置完成。
■ 表单类
一个自定义表单类的例子如下:
from flask.ext.wtf import Form
#新版本的flask中都会提示现在这种import方法已经过时,最新的import应该是from flask_wtf import Form
from wtforms import StringField,BooleanField,HiddenField,TextAreaField,DateTimeField
from wtforms.validators import FileAllowed,Required
class BaseForm(Form):
id = HiddenField("id")
class BulletinForm(BaseForm):
dt = DateTimeForm("发布时间",format="%Y-%m-%d %H:%M:%S")
title = StringField("标题",validators=[Required()])
content = TextAreaField("内容")
valid = BooleanField("是否有效")
source = StringField("来源")
author = StringField("作者")
image = FileField("图片上传",validators = [FileAllowed(['jpg','png'],'Images Only!')])
从上面这个例子中可以看到,BaseForm类其实是一些类的基类,它具有一些很多类都会具有的特征,所以被构造成基类让其他类型的表单再去继承它。
● 各种Field字段
除了上面提到的这些常用的Field之外,在wtforms中还有以下这些Field可供使用:
PasswordField 密码字段,自动将输入转化为小黑点
DateField 文本字段,格式要求为datetime.date一样
IntergerField 文本字段,格式要求是整数
DecimalField 文本字段,格式要求和decimal.Decimal一样
FloatField 文本字段,值是浮点数
BooleanField 复选框,值为True或者False
RadioField 一组单选框
SelectField 下拉列表,需要注意一下的是choices参数确定了下拉选项,但是和HTML中的<select> 标签一样,其是一个tuple组成的列表,可以认为每个tuple的第一项是选项的真正的值,而第二项是alias。
MultipleSelectField 可选多个值的下拉列表
● 各种Validator
Validator是验证函数,把一个字段绑定某个验证函数之后,flask会在接收表单中的数据之前对数据做一个验证,如果验证成功才会接收数据。验证函数Validator如下,具体的validator可能需要的参数不太一样,这里只给出一些常用的,更多详细的用法可以参见wtforms/validators.py文件的源码,参看每一个validator类需要哪些参数:
*基本上每一个validator都有message参数,指出当输入数据不符合validator要求时显示什么信息。
Email 验证电子邮件地址的合法性,要求正则模式是^.+@([^.@][^@]+)$
EqualTo 比较两个字段的值,通常用于输入两次密码等场景,可写参数fieldname,不过注意其是一个字符串变量,指向同表单中的另一个字段的字段名
IPAddress 验证IPv4地址,参数默认ipv4=True,ipv6=False。如果想要验证ipv6可以设置这两个参数反过来。
Length 验证输入的字符串的长度,可以有min,max两个参数指出要设置的长度下限和上限,注意参数类型是字符串,不是INT!!
NumberRange 验证输入数字是否在范围内,可以有min和max两个参数指出数字上限下限,注意参数类型是字符串,不是INT!!然后在这个validator的message参数里可以设置%(min)s和%(max)s两个格式化部分,来告诉前端这个范围到底是多少。其他validator也有这种类似的小技巧,可以参看源码。
Optional 无输入值时跳过同字段的其他验证函数
Required 必填字段
Regexp 用正则表达式验证值,参数regex='正则模式'
URL 验证URL,要求正则模式是^[a-z]+://(?P<host>[^/:]+)(?P<port>:[0-9]+)?(?P<path>\/.*)?$
AnyOf 确保值在可选值列表中。参数是values(一个可选值的列表)。特别提下,和SelectField进行配合使用时,不知道为什么SelectField的choices中项的值不能是数字。。否则AnyOf的values参数中即使有相关数字也无法识别出当前选项是合法选项。我怀疑NoneOf可能也是一样的套路。
NoneOf 确保值不在可选值列表中
此外再多说一句,在教材上和很多其他地方,基本上的路由模式都是把第一次进入表单的GET和提交表单数据时的POST的路径指向同一个。但是在我下面的那个实践当中,把POST单独分离于GET,放到了另一个路由下。这么做可以更加清晰的管理URL,但是会让validator失效。因为validator验证时验证的是POST路由下的表单对象,所以给出的反映也是给出到这个表单对象的,但是按照POST/GET分离的模式,必然要让POST的响应函数最后重定向到GET路由下,而在GET的响应函数中必然会重新创建一个表单对象,使得之前的validator失效了。
有意思的是flash消息并不受这种模式的影响。因为flash消息是通过后端把消息推入一个消息队列,然后前端拿去渲染,在POST上去后后端推入队列的flash消息并不立刻反映到POST路由下的表单对象,而当路由回到GET下,重新建立了表单对象然后渲染表单时,正好把flash消息给渲染出来了。
■ 显示表单
在建立了表单类之后,可以在响应函数的合适位置实例化表单对象,然后把表单对象作为render_template的参数传递给前端的模板。
在模板中通常可以调用一些已经定义好的宏来一键生成表单或者表单元素。当然也可以把表单的一个个元素按照一定顺序写到模板中手动地渲染模板。
● Bootstrap表单和flask-bootstrap
默认样式的表单总是很丑的,如果想要美化表单界面,同时也引进一些宏来方便前端的渲染的话,那么可以考虑一些flask扩展。比如flask-bootstrap就是整合了flask和有名的前段框架bootstrap的扩展,经过pip install flask-bootstrap之后,我们可以到$PYTHON_HOME/Lib/site-package/flask_bootstrap/template/bootstrap目录下找到一些文件。这里我们要用的是base.html和wtf.html。
base.html通过Jinja2模板语言给我们搭建了一个HTML文件的基本框架。我们可以让项目中所有HTML文件都{% extends "base.html" %}的话就可以少写很多代码。只要在合适的地方重写某个特定的block就可以了。需要提醒的一点是运用前端的这些扩展有时候会因为依赖或者其他神奇的原因【已经查明,神奇原因就是忘记写Bootstrap(app)了,如果写上这个的话,所有JS导入什么的引擎都会帮你做好的】而无法做到成功渲染,需要我们适当地修改源码。比如base.html中定义了一些神奇的自动寻找bootstrap.css以及jquery.js等的宏,但是用的时候总是报错。我的做法是把从网上下来的相关前端库文件放到项目的特定static目录下,然后修改base.html,去掉自动寻找的宏,手动指定引用路径。
比如把base.html改造成下面这样:
{% block doc -%} <!DOCTYPE html> <html{% block html_attribs %}{% endblock html_attribs %}> {%- block html %} <head> {%- block head %} <title>{% block title %}{{title|default}}{% endblock title %}</title> {%- block metas %} <meta name="viewport" content="width=device-width, initial-scale=1.0"> {%- endblock metas %} {%- block styles %} <!-- 把自动寻找宏改成静态路径 --> <link rel="stylesheet" href="../static/bootstrap/css/bootstrap.min.css"> {%- endblock styles %} <script type="text/javascript" src="../static/bootstrap/js/jquery.min.js"></script> <script type="text/javascript" src="../static/bootstrap/js/bootstrap.min.js"></script> <!-- 原先的block scripts放在body里面,按照我的习惯放到head里,并且在它之前把两个js文件引用好。需要额外提醒的是一定要先引用jquery再引用bootstrap.js --> {% block scripts %} {% endblock scripts %} {%- endblock head %} </head> <body{% block body_attribs %}{% endblock body_attribs %}> {% block body -%} {% block navbar %} {%- endblock navbar %} {% block content -%} {%- endblock content %} {%- endblock body %} </body> {%- endblock html %} </html> {% endblock doc -%}
wtf.html是bootstrap为了支持wtforms组件而特别的一个存在。里面有一键生成表单的宏。一般而言在响应函数(或者说视图函数)中我们实例化了一个表单对象,然后把它作为参数传递给render_template之后:
from flask_bootstrap import Bootstrap Bootstrap(app)"""这一步很重要!看似没有什么实际用处,但是指出了这个flask项目前端和bootstrap的关系,如果没有这句话,\
base.html和wtf.html必须要复制到项目目录下来,还要改动下源码。
渲染模板的时候也可能会报错如找不到bootstrap_is_hidden_field方法,import bootstrap/wtf.html出错等等 """ @app.route('/form') def form_test(): form = BulletinForm(request.form) return render_template("form.html",form=form)
在我们的模板文件form.html中可以写这样:
{% extends "base.html" %}
{# extends一定要写第一行 #}
{% import "bootstrap/wtf.html" as wtf %}
{% block content %}
{{ wtf.quick_form(form,form_type="horizontal",horizontal_columns=('lg',5,2))}}
{% endblock content %}
这里只是为了演示,就把表单直接渲染在block content中。wtf定义的quick_form可以一键生成表单,而且这个表单是带bootstrap的CSS样式的,比如按钮是白色的,输入框会发出蓝光等等。不带参数的情况下,表单默认宽度是页面宽度,个人感觉有点丑,阅读了一下wtf.html的源码之后发现可以加入form_type以及horizontal_columns两个参数来控制渲染的宽度。更多的参数也可以从源码中去发掘,这里不多说了。看了源码的另一个发现就是其实quick_form是对表单中的所有表单元素做了一个循环,每个循环都会调用form_field这个方法,所以我们也可以有选择地进行表单元素的渲染比如:
{% block content %}
<form method='post'>
{{ wtf.form_field(form.title) }}
{{ wtf.form_field(form.author) }}
</form>
{% endblock %}
上面就是只渲染了两个表单元素。和quick_form不同的是,quick_form会自带地渲染一个<form>标签,而form_field需要手动加上一个form标签的。另外quick_form会对一些自带的隐藏字段比如CSRFToken(防止跨站攻击的一种防范手段)自动隐藏好,如果运用form_field可能会在表单的某些地方出现CSRF Token字样,非常难看。
渲染完成之后的表单,每一个field都自带id和name,id和name都是表单类中指定的那个field对象的变量名。
■ 获取表单数据
一般而言,表单在flask的处理函数中表示为flask.request.form。如果表单成功递交,这个对象中有数据的话,这个对象会是一个ImmutableMultiDict对象而不是我们所熟知的字典类型。ImmutableMultiDict类型有它自己的好处,以及考虑到要和其他flask组件兼容,尽量不要改变其类型。如果实在是不习惯或者只需要进行读操作的话,那么可以调用ImmutableMultiDict的to_dict()方法来把这种类型的对象转换成一个经典格式的字典副本。在实际使用过程中,我们通常会增加逻辑判断form.validate_on_submit来判断本次request是不是一个带POST数据,且数据都符合表单定义是给出要求的请求。如果是那么就可以对表单数据做出一些处理,否则还是返回GET请求返回的页面。
一般默认情况下,对于用wtf渲染出来的表单,action属性是action=''。也就是说,单击这个表单的submit默认是把表单数据POST到当前的URL相对路径下。如果我们有需要把表单数据POST到不同的地方那么就需要在wtf.quick_form中添加action="xxx"的参数。由于之前不熟悉action这个属性,就多说一句,action可以是相对路径的某个文件,也可以是某个绝对的URL。
除了通过request这种比较通用的方法来获取表单数据之外,我们还可以用form本身自带的属性来访问表单数据。比如在validate_on_submit之后,可以直接通过<表单对象名>.<Field名称>.data来直接访问表单中某个具体字段提交上来的数据。
*有关换行符的一个小坑:在进行POST请求的时候,表单可以经过Submit来进行POST;另外如果给提交按钮加一个AJAX的话,也可以通过AJAX来进行POST。比较了一下两者后发现在换行符的处理上有一点微妙的区别。同样是取比如说一个文本框中的几行文字,通过POST到后端的数据其换行符默认是DOS中的\r\n,所以如果后端是在Unix系统上的话那么最好对换行符进行统一的处理。如果换做是通过AJAX,然后POST到后端的数据,同样的几行文字其换行符是\n。具体原理是什么还没来得及深究,怀疑跟表单取数据以及jQuery里的val方法不同有关。
还有一个用ajax传送数组到后台的坑。首先送到后台的form中,数组内容的key不是ajax中取名的key,而是key[],这一点可以通过在ajax请求中加上traditional:true来避免。第二点,一般的request.form.get方法是无法得到完整的数组列表的。正确的做法应该是request.form.getlist("list_name")。这个list_name要结合前面的考虑,即有没有后面的那个[]。
下面是一个简单的,异URL的表单POST和ajax的POST的比较的简单例子,部分后端代码:
####view.py#### @app.route('/form',methods=['GET']) def form(): content_form = ContentForm() return render_template("form.html",form=content_form) @app.route('/form_recv',methods=['POST']) def form_recv(): content_form = ContentForm() if content_form.validate_on_submit(): res_dict = request.form.to_dict() content = res_dict.get(u'content').replace('\r\n','\n') with open("static/demofile_form","wb+") as res_file: res_file.write(content) return redirect(url_for('form')) @app.route('/ajaxtest',methods=['POST']) def ajax_test(): content = request.form.get('content') with open("static/demofile_ajax","wb+") as res_file: res_file.write(content) return json.dumps({'code':200})
这里稍微提一下,我一开始想把接受POST请求的ajaxtest这个路由放到另外一个ajax.py的文件中,这样整理起来方便一些。但是事实证明,这些路由函数似乎不能放在两个不同的文件中,也就是说整个flask项目的所有@app.route必须都放在view.py里面?这个我是不太相信的。。不知道正确答案是什么。
部分前端代码:
<!--form.html--> {% block content %} <div class="container"> <h1 style="text-align:center;">Welcome to Form.</h1> <hr> {{ wtf.quick_form(form,form_type='horizontal',horizontal_columns=('lg',3,6),action="/form_recv") }} </div> <div class="container"> <button class="btn btn-default" id="an_btn">ajax按钮</button> </div> {% endblock %} {% block scripts %} {{ super() }} <!--不要忘了这个super,否则会报没有JQ库的错误--> <script> $(document).ready(function(){ $("#an_btn").click(function(){ $.ajax({ type:'POST', url:'/ajaxtest', data:{content:$("#content").val()}, datatype:'json', success:function(data) { var obj = JSON.parse(data); if (obj.code != 200) { alert(obj.msg); } else { alert('成功创建文件'); } } }); }); }); </script> {% endblock %}
上面这个例子中content_form中有一个文本输入框,点击确认按钮可以通过表单提交文本框中内容,在后台生成demofile_form这个文件。文件打开模式用wb+时因为只用w时python会自动对换行符做转换处理。如果后台是windows系统,那么就会变成\r\n。写入模式改成wb之后,就变成写入二进制文件,就不会做换行处理了。
点击ajax按钮则可以通过ajax把文本框中的内容提交给后台,后台创建文件demofile_ajax之后返回一个json串来告诉前端处理结果。
■ 关于flash消息的使用
在flask中包装了flash消息,所谓flash消息就是一种前端风格上和Bootstrap相匹配的动态提示消息框。虽然flash消息是独立属于flask的一种组件,但是常常用于表单提交后的信息提示,所以放在了表单编程这一篇里面。
flash的使用方法很简单,如下:
from flask import flash #其余import省略 @app.route('/form',method=['GET','PSOT']) def form(): form = content_form if form.validate_on_submit(): content = request.form.get(u'content') if len(content.split('\r\n')) > 10: flash(u'输入最多不能超过十行') else: with open('static/demofile','wb+') as f: f.write(content) return render_template('form.html',form=form)
可以看到,在后端合适的位置直接写flash('message')就可以发送flash消息到前端。那么前端的什么地方才会显示flash消息呢?这个就需要前端代码来进行控制了:
<!-- 比如在一个.container里面渲染flash消息的话--> <div class="container"> {% for message in get_flashed_messages() %} <div class="alert alert-danger alert-dismissable"> <button type="button" class="close" data-dismiss="alert">×</button> {{ message }} </div> {% endfor %}
上面的前端代码就是用bootstrap的alert消息提示框来承载了flash消息。如果愿意也可以用其他方式来呈现flash消息的。
● 更加灵活的flash消息
看了两本书上关于flash消息全部都是这么一块写死,打印出来的消息也全部都是同一个分类的。为了实现更加灵活的flash消息渲染,看了下flask/helpers.py的源码中的flash函数和get_flashed_messages函数,下面提出一些我自己的改良手段。首先在views.py中调用flash函数的时候,除了默认的message参数还可以增加一个category参数来指定这条消息的分级,这时flash出去的对象是一个tuple,其tuple[0]是分级信息,tuple[1]是渲染出来的消息。然后在前端jinja2模板中的get_flashed_message函数中可以添加with_categories=True参数来说明传递的是带分级的消息tuple。如此便可把消息的分级传递给前端了。比如下面这个例子中的实践:
##后端## if content.split('\n') <= 2: flash(message=u'输入行数过少',category='warning') elif content.split('\n') >= 5: flash(message=u'输入行数过大',category='danger') ##前端## {% block content %} {% for category,message in get_flashed_message(with_category=True ) %} <div class="alert alert-{{ category }} alert-dismissable"> <button type="button" class="close" data-dismiss="alert">&tims;</button> {{ message }} </div> {% endfor %} {% endblock %}
content可以是一个对应某个输入框的表单对象,其输入行数如果小于两行就flash出一个黄色的警告级别的flash消息,如果大于五行就flash出一个红色的危险级别的消息。