前言:本博客仅作记录学习使用,部分图片出自网络,如有侵犯您的权益,请联系删除
目录
一、HTML表单
二、使用Flask-WTF处理表单
2.1、定义WTForms表单类
2.2、输出HTML代码
2.3、在模板中渲染表单
三、处理表单数据
3.1、提交表单
3.2、验证表单数据
3.3、在模板中渲染错误信息
四、表单进阶实践
4.1、设置错误消息语言
4.2、使用宏渲染表单
4.3、自定义验证器
4.4、文件上传
4.5、使用Flask-CKEditor集成富文本编辑器
4.6、单个表单多个提交按钮
4.7、单个页面多个表单
WTForms是一个使用Python编写的表单库。
一、HTML表单
表单通过<form>标签创建,表单中的字段使用<input>标签定义。
<form method="post"><label for="username">Username</label><br><input type="text" name="username" placeholder="Hector Rivera"><br><label for="password">Password</label><br><input type="password" name="password" placeholder="123456789"><br><input id="remember" name="remember" type="checkbox" checked><label for="remember"><small>Remember me</small></label><br><input type="submit" name="submit" value="Log in"></form>
<input>标签表示各种输入字段,<label>标签用来定义字段的标签文字。我们可以在<form>和<input>标签中使用各种属性来对表单进行设置。
二、使用Flask-WTF处理表单
扩展Flask-WTF集成了WTForms,可以将表单数据解析、CSRF保护、文件上传等功能与Flask集成,另外附加reCAPTCHA支持(Google开发的免费验证码服务,在国内目前无法使用)
pip install flask-wtf
Flask-WTF默认为每个表单启动CSRF保护,会为我们自动生成和验证CSRF令牌。默认下,Flask-WTF使用程序密钥来对CSRF令牌进行签名,所以我们为程序设置密钥:
app.secret_key = 'secret string'
2.1、定义WTForms表单类
当使用WTForms创建表单时,表单由Python类表示,这个类继承从WTForms导入的Form基类。一个表单由若干个输入字段组成,这些字段分别用表单类属性表示(字段即Field,类似表单内的输入框,按钮等部件)。
from wtforms import Form, StringField, PasswordField, BooleanField, SubmitFieldfrom wtforms.validators import DataRequired, Lengthclass LoginForm(Form): username = StringField('Username', validators=[DataRequired()])password = PasswordField('Password', validators=[DataRequired(), Length(8, 128)])remember = BooleanField('Remember me')submit = SubmitField('Log in')
每个字段属性通过实例化WTForms提供的字段表示。字段属性的名称将作为对应HTML<input>元素的name属性及id属性值。(属性名称大小写敏感)
下面是常用的WTForms字段:
字段类 | 说明 | 对应的HTML表示 |
---|---|---|
BooleanField | 复选框,值会被处理为True或False | <input type="checkbox"> |
DateField | 文本字段,值会被处理为datetime.date对象 | <input type="text"> |
DateTimeField | 文本字段,值会被处理为datetime.datetime对象 | <input type="text"> |
FileField | 文件上传字段 | <input type="file"> |
FloatField | 浮点数字段,值会被处理为浮点型 | <input type="text"> |
IntegerField | 整数字段,值会被处理为整型 | <input type="text"> |
RadioField | 一组单选按钮 | <input type="radio"> |
SelectField | 下拉列表 | <select><option></option></select> |
SelectMultipleField | 多选下拉列表 | <select multiple><option></option></select> |
SubmitField | 提交按钮 | <input type="submit"> |
StringField | 文本字段 | <input type="text"> |
HiddenField | 隐藏文本字段 | <input type="hidden"> |
PasswordField | 密码文本字段 | <input type="password"> |
TextAreaField | 多行文本字段 | <textarea></textarea> |
实例化字段类常用参数
参数 | 说明 |
---|---|
label | 字段标签<label>的值,也就是渲染后显示在输入字段前的文字 |
render_kw | 一个字典,用来设置对应HTML<input>标签的属性 |
validators | 一个列表,包含一系列验证器,会在表单提交后被逐一调用验证表单数据 |
default | 字符串或可调用对象,用来为表单字段设置默认值 |
在WTForm中,验证器(validator)是一系列用于验证字段数据的类,我们在实例化字段类时使用validators关键字来指定附加的验证器列表。验证器从wtforms.valitators模块中导入,常用的有:
验证器 | 说明 |
---|---|
DataRequired(message=None) | 验证数据是否有效 |
Email(message=None) | 验证Email地址 |
EqualTo(filedname,message=None) | 验证两个字段值是否相同 |
InputRequired(message=None) | 验证是否有数据 |
LengthRange(min=-1,max=None,message=None) | 验证输入值长度是否在给定范围内 |
NumberRange(min=None,max=None,message=None) | 验证输入数字是否在给定范围内 |
Optional(strip_whitespace=True) | 允许输入值为空,并跳过其他验证 |
Regexp(regex,flags=0.message=None) | 使用正则表达式输入验证值 |
URL(require_tld=True,message=None) | 验证URL |
AnyOf(values,message=None,values_formatter=None) | 确保输入值在可选值列表中 |
NoneOf(values,message=None,value_formatter=None) | 确保输入值不在可选值列表中 |
验证器的第一个参数一般为错误提示信息,使用message关键字传递参数:
name = StringField('Your Name', validators=[DataRequired(message=u'名字不能为空!')])
使用Flask-WTF定义表单时,我们仍然使用WTForms提供的字段类和验证器,方式也相同,不过表单类要继承
Flask-WTF提供的FlaskForm类(继承自Form类,进行了一些设置,附加了一些辅助方法)
from flask_wtf import FlaskFormfrom wtforms import StringField, PasswordField, BooleanField, SubmitFieldfrom wtforms.validators import DataRequired, Lengthclass LoginForm(FlaskForm):username = StringField('Username',validators=[DataRequired()])password = PasswordField('Password',validators=[DataRequired(),Length(8,128)])remember = BooleanField('Remember me')submit = SubmitField('Log in')
配置键WTF_CSRF_ENABLED用来设置是否开启CSRF保护,默认为True。
2.2、输出HTML代码
以使用WTForms创建的LoginForm为例,实例化表单类,然后将实例属性转换为字符串或直接调用就可以获取表单字段对应的HTML代码:
>>> form = LoginForm()>>> form.username()u'<input id="username" name="username" type="text" value="">'>>> form.submit()u'<input id="submit" name="submit" type="submit" value="Submit">'
字段<label>元素的HTML代码则可以通过"form.字段名.label"的形式获取:
>>> form.username.label()u'<label for="username">Username</label>'>>> form.submit.label()u'<label for="submit">Submit</label>'
默认情况下,WTForms输出的字段HTML代码只会包含id和name属性,属性值均为表单类中对应的字段属性名称。如果需要添加额外的属性,有两种方法:
2.2.1、使用render_kw属性
为username字段使用render_kw设置了placeholderHTML属性
username = StringField('Username', render_kw={'placeholder':'Your Username'})
这个字段被调用后输出的HTML代码:
<input type="text" id="username" name="username" placeholder="Your Username">
2.2.2、在调用字段时传入
在调用字段属性时,通过添加括号使用关键字参数的形式也可以传入字段额外的HTML属性:
>>> form.username(style='width:200px;',class_='bar')
u'<input class="bar" id="username" name="username" style="width: 200px;" type="text">'
class是Python保留的关键字,在这里我们使用class_来替代class,渲染后的<input>会获得正确的class属性,在模板调用中时则可以直接使用class
2.3、在模板中渲染表单
首先把表单类实例传入模板。在视图函数里实例化表单类LoginForm,然后在render_template()函数中使用关键字参数form将表单实例传入模板。
@app.route('/basic')
def basic():form = LoginForm()return render_template('login.html',form=form)
在模板中渲染表单
<form method="post">{{ form.csrf_token }}{{ form.username.label }}{{ form.username }}<br>{{ form.password.label }}{{ form.password }}<br>{{ form.remember }}{{ form.remember.label }}<br>{{ form.submit }}<br>
</form>
form.csrf_token字段包含了自动生成的CSRF令牌值,在提交表单后自动被验证,我们必须在表单中手动渲染这个字段。
通过使用render_kw字典或是在调用字段时传入参数来定义额外HTML属性,通这种方式添加CSS类,我们可以编写一个Bootstrap风格的表单。
...
<form method="post">{{ form.csrf_token }}<div class="form-group">{{ form.username.label }}{{ form.username(class='form-control') }}</div><div class="form-group">{{ form.password.label }}{{ form.password(class='form-control') }}</div><div class="form-check">{{ form.remember(class='form-check-input') }}{{ form.remember.label }}</div>{{ form.submit(class='btn btn-primary') }}
</form>
...
三、处理表单数据
除去表单提交不说,从获取数据到保存数据经历以下步骤:
- 解析请求,获取表单数据
- 对数据进行必要的转换,比如将勾选框的值转换成为Python的布尔值
- 验证数据是否符合请求,同时验证CSRF令牌
- 如果验证未通过则生成错误信息,并在模板中显示错误信息
- 如果通过验证,就把数据保存到数据库或作进一步处理
3.1、提交表单
在HTML中,当<form>标签声明的表单中类型为submit的提交字段被单击时,就会创建一个提交表单的HTTP请求,请求中包含各个字段的数据。
属性 | 默认值 | 说明 |
---|---|---|
action | 当前URL,即页面对应的URL | 表单提交发送请求的目标URL |
method | get | 表单提交的HTTP请求方法,目前仅支持使用GET和POST方法 |
enctype | application/x-www-form-urlencoded | 表单数据的编码类型,当表单中包含文件上传字段时,需要设为multipart/form-data,还可以设为纯文本类型text/plain |
form标签的action属性用来指定表单提交的目标URL,默认为当前的URL,也就是渲染该模板的路由所在的URL。
Flask为路由设置默认监听的HTTP方法为GET。设置监听POST方法
@app.route('/',methods=['GET','POST'])
def basic():form = LoginForm()return render_template('login.html',form=form)
3.2、验证表单数据
3.2.1、客户端验证和服务器端验证
(1)客户端验证
客户端验证指在客户端(比如Web服务器)对用户的输入值进行验证。比如使用HTML5内置的验证属性即可实现基本的客户端验证(type、reqired、min、max、accept等)。例如:
<input type="text" name="username" required>
# 添加了required标志,若用户没有输入内容而按下提交按钮,会弹出浏览器内置的错误提示。
和其他附加HTML属性相同,我们可以在定义表单时通过render_kw传入这些属性,或是在渲染表单时传入。像required这类布尔值属性,可以是空或任意ASCII字符:
{{ form.username(required='')}}
通常还会使用JavaScript实现完善的验证机制,还有各种JavaScript表单验证库,比如jQuery Validation Plugin、Parsley js以及可与Bootstrap集成的Bootstrap Validator等。
(2)服务器端验证
服务器端验证指用户把输入的数据提交到服务器端,在服务器端对数据进行验证。这是必不可少的。
3.2.2、WTForms验证机制
WTForms验证表单字段的方式是在实例化表单类时传入表单数据,然后对表单实例调用validate()方法。这会逐个对字段调用实例化时定义的验证器,返回表示验证结果的布尔值。若验证失败,就把错误消息存储到表单实例的errors属性对应的字典中
3.2.3、在视图函数中验证表单
现在的basic_form视图同时接收两种类型的请求:GET请求和POST请求。不同请求不同处理:首先实例化表单,如果是GET请求,就渲染模板;如果是POST请求,就调用validate()方法验证表单数据。
请求的HTTP方法通过request.method属性获取
from flask import request@app.route('/basic',methods=['GET','POST'])
def basic():form = LoginForm() # GET+POSTif request.method == 'POST' and form.validate():... # 处理POST请求return render_template('forms/basic.html',form=form)
Flask-WTF提供的validate_on_submit()方法合并了上述操作,因此可简化为:
@app.route('/basic',methods=['GET','POST'])
def basic():form = LoginForm() # GET+POSTif form.validate_on_submit:... # 处理POST请求return render_template('forms/basic.html',form=form)# validate_on_submit()方法会验证表单数据
如果validate_on_submit()返回True,则表示用户提交了表单,且表单通过验证,我们就能在这个if语句中获取表单数据
@app.route('/basic',methods=['GET','POST'])
def basic():form = LoginForm() # GET+POSTif form.validate_on_submit():username = form.username.dataflash('Welcome home,%s!' %username)return redirect(url_for('index'))return render_template('basic.html',form=form)
表单类的data属性是一个匹配所有字段与之对应数据的字典,一般直接通过"form.字段属性名.data"的形式来获取对应字段的数据。
在浏览器中,当单击F5刷新/重载时的默认行为是发送上一个请求。如果上一个请求时POST请求,会弹出一个窗口询问用户是否再次提交表单。我们尽量不要让提交表单的POST请求作为最后一个请求。因此在处理表单后返回一个重定向响应,这会让浏览器重新发送一个新的GET请求到重定向的目标URL。
3.3、在模板中渲染错误信息
如果form.validate_on_submit()返回Flase,说明验证没有通过。WTForms会把错误信息添加到表单类的errors属性中(字典)。一般通过字段名来获取对应字段的错误消息列表:“form.字段名.errors”。
<form method="post">{{ form.csrf_token }}{{ form.username.label }}<br>{{ form.username() }}<br>{% for message in form.username.errors %}<small class="error">{{ message }}</small><br>{% endfor %}{{ form.password.label }}<br>{{ form.password }}<br>{% for message in form.password.errors %}<small class="error">{{ message }}</small><br>{% endfor %}{{ form.remember}}{{ form.remember.label }}<br>{{ form.submit }}<br>
</form>
四、表单进阶实践
4.1、设置错误消息语言
WTForms内置了多种语言的错误消息,通过自定义表单基类实现
设置内置错误消息语言为中文
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
from form import LoginFormapp = Flask(__name__)
app.config['WTF_I18N_ENABLED'] = False # 这会让Flask-WTF使用WTForms内置的错误消息翻译。class MybaseForm(FlaskForm):class Meta:locales = ['zh']class HelloForm(MybaseForm):name = StringField('Name',validators=[DataRequired()])submit = SubmitField()
在自定义基类中定义Meta类,并在locales列表中加入简体中文的地区字符串。在创建表单时,继承这个MybaseForm即可将错误消息语言设置为中文。另外也可在实例化表单类时通过meta关键字传入locales值:
form = Myform(meta=('locales': ['en_US','en']))
4.2、使用宏渲染表单
在模板中渲染表单,有大量工作:
- 调用字段属性,获取<input>定义
- 调用对应的label属性,获取<label>定义
- 渲染错误消息
为避免代码重复,创建一个宏来渲染表单字段:
{% macro form_field(field) %}{{ field.label }}<br>{{ field(**kwargs) }}<br>{% if field.errors %}{% for error in errors %}<small class="error">{{ error }}</small><br>{% endfor %}{% endif %}
{% endmacro %}
这个form_field()宏接收表单类实例的字段属性和附加的关键字参数作为输入,返回包含<label>标签、表单字段、错误消息列表的HTML表单字段代码。使用实例:
{% from 'macro.html' import form_field %}
...
<form method="post">{{ form.csrf_token }}{{ form_field(form.username) }}<br>{{ form_field(form.password) }}<br>
</form>
上述调用form_field()宏逐个渲染表单中的字段,只要把每一个类的属性传入form_field()宏,即可完成渲染。
4.3、自定义验证器
4.3.1、行内验证器
可在表单类中定义方法来验证特定字段:针对特定字段的验证器:
from wtforms import SubmitField, IntegerField
from wtforms.validators import ValidationErrorclass FortyTwoForm(FlaskForm):answer = IntegerField('The Number')submit = SubmitField()def validata_answer(form,field):if field.data != 42:raise ValidationError('Must be 42.')
在表单类中包含以"validate_字段属性名"形式命名的方法时,在验证字段数据时会同时调用这个方法来验证对应的字段。验证出错则抛出ValiddationError异常。仅用来验证特定的表单类字段,又称行内验证器。
4.3.2、全局验证器
若想要一个可重用的通用验证器,通过定义一个函数实现。简单示例:
from wtforms.validators import ValidationErrordef is_42(form,field):if field.data != 42:raise ValidationError('Must be 42')class FortyTwoForm(FlaskForm):answer = IntegerField('The Number',validators=[is_42])submit = SubmitField()
当使用函数定义全局的验证器时,我们需要在定义字段时在validators列表里传入这个验证器。因为在validators列表中传入的必须是可调用对象,所以这里传入函数对象,而不是函数调用。
在现实中,通常让验证器支持传入参数来对验证过程进行设置。至少支持message参数来设置自定义错误消息。此时验证函数应该实现成工厂函数,即返回一个可调用对象的函数。
from wtforms.validators import ValidationErrordef is_42(message=None):if message is None:message = 'Must be 42.'def _is_42(form, field):if field.data != 42:raise ValidationError(message)return _is_42class FortyTwoForm(FlaskForm):answer = IntegerField('The Number',validators=[is_42()])submit = SubmitField()
在is_42()函数中,我们创建了另一个_is_42()函数,这个函数作为可调用对象返回。is_42()函数接收的message参数用来传入自定义错误消息,默认为None。
4.4、文件上传
渲染一个文件上传字段只需要将<input>标签的type属性设为file:<input type="file">
在服务器端,可以和普通数据一样获取上传文件数据并保存。不过需要考虑安全问题,文件上传漏洞也是比较流行的攻击方式。除了常规的CSRF防范,还需要注意:
- 验证文件类型
- 验证文件大小
- 过滤文件名
4.4.1、定义上传表单
在Python表单类中创建文件字段时,我们使用扩展Flask-WTF提供的FileField类
from flask_wtf.file import FileField,FileRequired,FileAllowedclass UploadForm(FlaskForm):photo = FileField('Upload Image',validators=[FileRequired(),FileAllowed(['jpg','jpeg','png','gif'])])submit = SubmitField()
和其他字段类似,也需要对文件上传字段进行验证。Flask-WTF在flask_wtf.file模块下提供了两个文件相关的验证器:
验证器 | 说明 |
---|---|
FileRequired(message=None) | 验证是否包含文件对象 |
FileAllowed(upload_set,message=None) | 用来验证文件类型,upload_set参数用来传入包含允许的文件后缀名列表 |
使用FileRequired对上传的文件类型进行限制。(如果用户上传HTML文件,而我们同时提供了视图函数获取上传后的文件,那么容易导致XSS攻击)
FileAllowed是在服务器端验证上传文件,使用HTML5中的accept属性也可以在客户端实现简单的类型过滤。
<input type="file" id="profile_pic" name="profile_pic" accept=".jpg, .jpeg, .png, .gif">
通过设置Flask内置的配置变量MAX_CONTENT_LENGTH,我们限制请求报文的最大长度,单位为字节(byte):
# 限制最大长度为3M
app.config['MAX_CONTENT_LENGTH'] = 3 * 1024 * 1024
当请求数据(上传大小)超过这个限制后,会返回413错误响应(Request Entiy Too Large)
4.4.2、渲染上传表单
在新创建的upload视图里,我们实例化表单类UploadForm,然后传入模板:
@app.route('/upload',methods=['GET','POST'])
def upload():form = UploadForm()...return render_template('upload.html',form=form)
在模板中渲染表单
<form method="post" enctype="multipart/form-data">{{ form.csrf_token }}{{ form_field(form.photo) }}{{ form.submit }}</form>
当表单中包含文件上传字段时(即type属性为file的input标签),需要将表单的enctype属性设为“multipart/form-data”,这会告诉浏览器将上传数据发送到服务器,否则仅会把文件名作为表单数据提交。
4.4.3、处理上传文件
和普通的表单数据不同,当包含上传文件字段的表单提交后,上传的文件需要在请求对象的file属性(request.files)中获取。前面介绍过,这个属性是Werkzeug提供的ImmutableMultiDict字典对象,存储字段的name键值和文件对象的映射,比如:
ImmutableMultiDict([('photo',<FileStorage: u'0f913b0fddcds.JPG' ('image/jpeg')>)])
上传的文件会被Flask解析为Werkzeug中的FileStorage对象。当手动处理时,我们需要用文件上传字段的name属性值作为键获取对应的文件对象:
request.files.get('photo')
当使用Flask-WTF时,会自动帮我们获取对应的文件对象,这里我们仍使用表单类属性的data属性获取上传文件。
处理上传文件app.py:
@app.route('/upload',methods=['GET','POST'])
def upload():form = UploadForm()if form.validate_on_submit():f = form.photo.datafilename = random_filename(f.filename)f.save(os.path.join(app.config['UPLOAD_PATH'],filename))flash('Upload success.')session['filenames'] = [filename]return redirect(url_for('show_images'))return render_template('upload.html',form=form)
表单验证通过后,我们通过form.photo.data获取存储上传文件的FileStorage对象。接下来有三种方式处理文件名:
(1)使用原文件名
若能确定文件的来源安全,可直接使用原文件名,通过FileStorage对象的filename属性获取:
filename = f.filename
(2)使用过滤后的文件名
若支持用户上传文件,我们必须对文件名进行处理,因为攻击者可能会在文件名中加入恶意路径(比如加入上级目录的...)。我们可以使用Werkzeug提供的secure_filename()函数对文件名进行过滤,传递文件名作为参数,它会过滤掉所有危险字符,返回“安全的文件名”:
>>> form werkzeug import secure_filename
>>> secure_filename('avatar!@#//#\\%&$.jpg')
'avatar.jpg'
>>> secure_filename('avatar头像.jpg')
'avatar.jpg'
(3)统一文件名
secure_filename()函数非常方便,会过滤掉文件名中非ASCII字符。但如果文件名完全由非ASCII字符组成,那么会得到一个空文件名:
>>> secure_filename('头像.jpg')
'jpg'
更好的做法是使用统一的处理方式对所有上传的文件重新命名。随机文件名有很多种方式可以生成,下面是一个用Python内置的uuid模板生成随机文件名的random_filename()函数:
import uuiddef random_filename(filename):ext = os.path.splitext(filename)[1]new_filename = uuid.uuid4().hex + extreturn new_filename# 这个函数接受原文件名作为参数,使用内置的uuid模块中的uuid4()方法生成新的文件名,并使用hex属性获取十六进制字符串,最后返回包含后缀的新文件名
在upload视图就调用了这个方法来获取随机文件名。
处理完文件名后就是将文件保存到文件系统了。我们在forms目录下创建了一个uploads文件夹,用于保存上传后的文件。指向这个文件夹的绝对路径存储在自定义配置变量UPLOAD_PATH中:
app.config['UPLOAD_PATH'] = os.path.join(app.root_path,'uploads')
为了保存文件需提前创建。对FileStorage对象调用save()方法即可保存,传入包含目标文件夹绝对路径和文件名在内的完整保存路径:
f.save(os.apth.join(app.config['UPLOAD_PATH'],filename))
文件保存后,我们希望能够显示上传后的图片。为了让上传后的文件能够通过URL获取,我们创建一个视图函数来返回上传后的文件:
@app.route('/uploads/<path:filename>')
def get_file(filename):return send_from_directory(app.config['UPLOAD_PATH'],filename)
使用Flask提供的send_from_directory()函数来获取文件,传入文件的路径和文件名作为参数。
在upload视图保存文件后,使用flash()发送一个提示,将文件名保存到session中,最后重定向到show_images视图。show_images视图返回的upload.html模板将从session获取文件名,渲染出上传后的图片。
flash('Upload success.')
session['filenames'] = [filename]
return redirect(url_for('show_images'))
4.4.4、多文件上传
在客户端,通过在文件上传字段(type=file)加入multiple属性,就可以开启多选:
<input type="file" id="file" name="file" multiple>
创建表单类时,可以直接使用WTForms提供的MultipleFileField字段实现,添加一个DataRequired验证器来确保包含文件:
from wtforms import MultipleFileFieldclass MultiUploadForm(FlaskForm):photo = MultipleFileField('Upload Image', validators={DataRequired()})submit = SubmitField()
表单提交时,在服务器端的程序中,对request.files属性调用getlist()方法并传入字段的name属性值会返回包含所有上传文件对象的列表。在multi_upload视图中,我们迭代这个列表,然后逐一对文件进行处理:
@app.route('/multi_upload',methods=['GET','POST'])
def multi_upload():form = MultiUploadForm()if request.method == 'POST':filenames = []# 验证CSRF令牌try:validate_csrf(form.csrf_token.data)except ValidationError:flash('CSRF token error')return redirect(url_for('multi_upload'))# 检查文件是否存在if 'photo' not in request.files:flash('This field is required.')return redirect(url_for('multi_upload'))for f in request.files.getlist('photo'):# 检查文件类型if f and allowd_file(f.filename):filename = random_filename(f.filename)f.save(os.path.join(app.config['UPLOAD_PATH'],filename))filenames.append(filename)else:flash('Invalid file type.')return redirect(url_for('multi_upload'))flash('Upload success.')session['filenames'] = filenamesreturn redirect(url_for('show_images'))return render_template('upload.html',form=form)
当请求方法为POST时,我们对上传数据进行手动验证,主要包含以下几步:
- 手动调用flask_wtf.csrf.valitate_csrf验证CSRF令牌,传入表单中csrf_token隐藏字段的值。如果抛出wtforms.ValidationError异常则表明验证未通过。
- 其中if 'photo' not in request.files用来确保字段中包含文件数据(相当于FileRequired验证器),如果用户没有选择文件就提交表单则request.files将为空。
- if f用来确保文件对象存在,这里也可以检查f是否是FileStorage实例
- allowed_file(f.filename)调用了allowed_file()函数,传入文件名。这个函数相当于FileAllowed验证器,用来验证文件类型,返回布尔值:
app.config['ALLOWED_EXTENSIONS'] = ['png','jpg','jpeg','gif']def allowed_file(filename):return '.' in filename and \filename.rsplit('.',1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
在上面几个验证语句里,如果没有通过验证,则使用flash()函数显示错误信息,然后重定向到multi_upload视图。
4.5、使用Flask-CKEditor集成富文本编辑器
富文本编辑器即WYSIWYG(What You See Is What You Get,所见即所得)编辑器,类似我们经常使用的文本编辑软件。它提供一系列按钮和下拉列表来为文本设置格式,编辑状态的文本样式即最终呈现出来的样式。在Web程序中,这种编辑器也称为HTML富文本编辑器,因为它使用HTML标签来为文本定义样式。
CKEditor是一个开源的富文本编辑器,它包含丰富的配置选项,而且有大量第三方插件支持。扩展Flask-CKEditor简化了在Flask程序中使用CKEditor的过程,首先安装:
pip install flask-ckeditor
然后实例化Flask-CKEditor提供的CKEditor类,传入程序实例
from flask_ckeditor import CKEditor
ckeditor = CKEditor
4.5.1、配置富文本编辑器
Flask-CKEditor提供了许多配置变量来对编辑器进行设置,常用的配置:
配置键 | 默认值 | 说明 |
---|---|---|
CKEDITOR_SERVE_LOCAL | False | 设为True会使用内置的本地资源 |
CKEDITOR_PKG_TYPE | 'standard' | CKEditor包类型,可选值为basic。standard和full |
CKEDITOR_LANGUAGE | '' | 界面语言,传入ISO 639格式的语言码 |
CKEDITOR_HEIGHT | '' | 编辑器高度 |
CKEDITOR_WIDTH | '' | 编辑器宽度 |
在示例程序中,为方便开发,使用内置的本地资源
app.config['CKEDITOR_SERVE_LOCAL'] = True
配置变量CKEDITOR_LANGUAGE用来固定界面的显示语言(简体中文和繁体中文对应的配置分别为zh-cn和zh),如果不设置,默认自动匹配。
其他具体访问Flask-CKEditor文档的插件集成部分。
4.5.2、渲染富文本编辑器
富文本编辑器在HTML中通过文本区域字段表示,即<textarea></textarea>。Flask-CKEditor通过包装WTForms提供的TextAreaField字段类型实现一个CKEditorField字段类,我们使用它来构建富文本编辑框字段。
# 文章表单
from flask_wtf import FlaskForm
from flask_ckeditor import CKEditorField
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired, Lengthclass RichTextForm(FlaskForm):title = StringField('Title',validators=[DataRequired(),Length(1,50)])body = CKEditorField('Body',validators=[DataRequired()])submit = SubmitField('Publish')
文章正文字段(body)使用的CKEditorField字段类型从Flask-CKEditor导入。可像其他字段一样定义标签、验证器和默认值。同样使用data属性获取数据。
渲染模板上也一样:
{% extends 'base.html' %}
{% from 'macro.html' import form_field %}{% block content %}
<h1>Integrate CKEditor with Flask-CKEdtior</h1>
<form method="post">{{ form.csrf_token }}{{ form_field(form.title) }}{{ form_field(form.body) }}{{ form.submit }}
</form>
{% endblock %}{% block scripts %}{{ super() }}{{ ckeditor.load() }}
{% endblock %}
渲染CKEditor编辑器需要加载相应的JavaScript脚本。为方便开发,可以使用Flask-CKEditor在模板中提供的ckeditor.load()方法加载资源,它默认从CDN加载资源,将CKEDITOR_SERVE_LOCAL设为True会使用扩展内置的本地资源,内置的本地资源包含了几个常用的插件和语言包。ckeditor.load()方法支持通过pkg_type参数传入包类型,这会覆盖CKEDITOR_PKG_TYPE的值,额外的version参数可以设置从CDN加载的CKEditor版本。
作为替代,可访问CKEditor官网提供的构建工具构建自己的CKEditor包。
若使用配置变量设置了编辑器的高度、宽度和语言或是其他插件配置,需要使用ckeditor.config()方法加载配置,传入对应表单类属性名。这个方法需要在加载CKEditor资源后调用:
{{ ckeditor.config(name='body') }}
4.6、单个表单多个提交按钮
# 包含两个按钮的表单:
class NewPostForm(FlaskForm):title = StringField('Title',validators=[DataRequired(),Length(1,50)])body = TextAreaField('Body',validators=[DataRequired()])save = SubmitField('Save') # 保存按钮publish = SubmitField('Publish') # 发布按钮
机制:当表单数据通过POST请求提交时,Flask会把表单数据解析到request.form字典。如果表中有两个提交字段,那么只有被单击的提交字段才会出现在这个字典中。当我们对表单类实例或特定的字段属性调用data属性时,WTForms会对数据进行处理。对于提交字段的值,转换为布尔值;被单击的提交字段的值将是True,否则False。
# 判断被单击的提交按钮
@app.route('/two-submits',method=['GET','POST'])
def two_submits():form = NewPostForm()if form.validate_on_submit():if form.save.data:# save it ...flash('You click the "Save" button.')elif form.publish.data:# publish it...flash('You click the "Publish" button.')return redirect(url_for('index'))return render_template('2submit.html',form=form)
返回主页的按钮与表单提交无关,直接在HTML中手动添加即可。
4.7、单个页面多个表单
当在同一个页面上添加多个表单时,我们要解决的一个问题就是在视图函数中判断当前被提交的是哪个表单。
4.7.1、单视图处理
第一步:为两个表单的提交字段设置不同的名称
class SigninForm(FlaskForm):username = StringField('Username',validators=[DataRequired(),Length(1,20)])password = PasswordField('Password',validators=[DataRequired(),Length(8,128)])submit = SubmitField('Sign in.')class RegisterForm(FlaskForm):username = StringField('Username',validators=[DataRequired(),Length(1,20)])email = StringField('Email',validators=[DataRequired(),Email(),Length(1,254)])password = PasswordField('Password',validators=[DataRequired(),Length(8,128)])submit2 = SubmitField('Register')
第二步:在视图函数中处理多个表单
@app.route('/multi-form',method=['GET','POST'])
def multi_form():signin_form = SigninForm()register_form = RegisterForm()if signin_form.submit.data and signin_form.validate():username = signin_form.username.dataflash('%s, you just submit the Signin Form.' % username)return redirect(url_for('index'))if register_form.submit2.data and register_form.validate():username = register_form.username.dataflash('%s, you just submit the Register Form.' % username)return redirect(url_for('index'))return render_template('2form.html',signin_form=signin_form,register_form=register_form)
以登录表单(SigninForm)的if判断为例,如果signin_form.submit1.data的值为True,那就说明用户提交了登录表单,这时我们手动调用signin_form.validate()对这个表单进行验证。
这两个表单类实例通过不同的变量名称传入模板,以便在模板中相应渲染对应的表单字段:
<form method="post">{{ signin_form.csrf_token }}{{ form_filed(signin_form.username) }}{{ form_filed(signin_form.password) }}{{ signin_form.submit1 }}
</form>
<h2>Register Form</h2>
<form method="post">{{ register_form.csrf_token }}{{ form_field(register_form.username) }}{{ form_field(register_form.email) }}{{ form_field(register_form.password) }}
</form>
4.7.2、多视图处理
除了通过提交按钮判断,更简洁的方法是通过分离表单的渲染和验证实现。这时表单的提交字段可以使用同一个名称,在视图函数中处理表单时也只需要使用我们熟悉的form.validate_on_submit()方法。
当处理多个表单时,我们可以把表单的渲染在单独的视图函数中处理:
@app.route('/multi-form-multi-view')
def multi_form_multi_view():signin_form = SigninForm()register_form = RegisterForm()return render_template('2form2view.html',signin_form=signin_form,register_form=register_form)
这个视图只负责处理GET请求,实例化两个表单类并渲染模板。另外我们在为每一个表单单独创建一个视图函数来处理验证工作。处理表单提交请求的视图仅监听POST请求:
@app.route('/handle-signin',methods=['POST'])
def handle_signin():signin_form = SigninForm()register_form = RegisterForm()if signin_form.validate_on_submit():username = signin_form.username.dataflash('%s,you just submit the Signin Form.' % username)return redirect(url_for('index'))return render_template('2form2view.html',signin_form=signin_form,register_form=register_form)@app.route('/handle-register',methods=['POST'])
def handle_register():signin_form = SigninForm()register_form = RegisterForm()if register_form.validate_on_submit():username = register_form.username.dataflash('%s,you just submit the Register Form.'% username)return redirect(url_for('index'))return render_template('2form2view.html',signin_form=signin_form,register_form=register_form)
在HTML中,表单提交请求的模板URL通过action属性设置。为了让表单提交时将请求发送到对应的URL,我们需要设置action属性:
...
<h2>Login Form</h2>
<form method="post" action="{{ url_for('handle_signin') }}">...
</form>
<h2>Register Form</h2>
<form method="post" action="{{ url_for('handle_register') }}">...
</form>
...
这种方法有一个显著的缺点。如果验证未通过,要将错误消息的form.errors字典传入模板。在处理表单的视图中传入表单错误信息,意味着需要再次渲染模板,但如果视图函数中还涉及大量要传入模板的操作,那么这种方式会带来大量的重复。
对于这个问题,一般的解决方法是通过其他方式传递错误信息,然后统一重定向到渲染表单页面的视图。例如使用flash()函数迭代form.errors字典发送错误消息,然后重定向到用来渲染表单的multi_form_multi_view视图:
def flash_errors(form):for field,errors in form.errors.items():for error in errors:flash(u"Error in the %s field - %s" % (getattr(form,field).label.text,error))
若希望像往常一样在表单字段下渲染错误信息,可以直接将错误消息字典form.errors存储到session中,然后重定向到用来渲染表单的multi_form_multi_view视图。在模板中渲染表单字段错误时添加一个额外的判断,从session中获取并迭代错误消息。
致谢
在此,我要对所有为知识共享做出贡献的个人和机构表示最深切的感谢。同时也感谢每一位花时间阅读这篇文章的读者,如果文章中有任何错误,欢迎留言指正。
学习永无止境,让我们共同进步!!