生成表单
在HTML中,表单通过<form>标签创建,表单中使用<input>标签表示各种输入字段,<label>标签用来定义标签文字。例如下面的HTML表单:
<form method="post">
<label for="username">Username</label><br>
<input type="text" name="username" placeholder="Héctor Rivera"><br>
<label for="password">Password</label><br>
<input type="password" name="password" placeholder="19001130"><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>
渲染后的效果如下:
WTForms是一个使用Python编写的表单库,它使得表单的定义、验证(服务器端)和处理变得非常轻松。
WTForms支持在Python中使用类定义表单,然后直接通过类定义生成对应的HTML代码,这种方式更方便且表单更易于重用。因此,除非是非常简单的程序,否则不会在模板中直接使用HTML编写表单。Flask-WTF集成了WTForms,使用它可以在Flask中更加方便地使用WTForms。
当使用WTForms创建表单时,表单由Python自定义类表示(例如下面的LoginForm类)这个类继承从WTForms导入的Form基类。一个表单由若干输入字段组成,这些字段分别用表单类的类属性(例如username、password)来表示。每个字段属性通过实例化WTForms提供的字段类表示。字段属性的名称将作为对应HTML<input>元素的name属性及id属性值。
例如下面的代码会生成前面一样的表单:
### 文件forms.py ###
from wtforms import Form, StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length
class LoginForm(Form):
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired(), Length(8, 128)])
remember = BooleanField('Remember me')
submit = SubmitField('Log in')
### 文件app.py ###
from flask import Flask, render_template
import os
from forms import LoginForm
app = Flask(__name__)
app.secret_key = "secret string"
app.secret_key = os.getenv('SECRET_KEY', 'secret string')
@app.route('/basic')
def basic():
form = LoginForm()
return render_template('basic.html', form=form)
### 文件basic.html ###
<!DOCTYPE html>
<form method="post">
{{ form.csrf_token }}
{{ form.username.label }}<br>
{{ form.username(placeholder='Your name') }}<br>
{{ form.password.label }}<br>
{{ form.password }}<br>
{{ form.remember }}{{ form.remember.label }}<br>
{{ form.submit }}<br>
</form>
</html>
渲染效果如下:
通过实例化字段类时传入的参数,我们可以对字段进行设置,字段类构造方法接收的常用参数如表:
参数 | 说明 |
---|---|
label | 字段标签<label>的值,也就是渲染后显示在输入字段前的文字 |
render_kw | 一个字典,用来设置对应的HTML<input>标签的属性,比如传入{‘placeholder’:‘Your Name’},渲染后的HTML代码会将<input>标签的placeholder属性设为Your Name |
validators | 一个列表,包含一系列验证器,会在表单提交后被逐一调用验证表单数据 |
default | 字符串或可调用对象,用来为表单字段设置默认值 |
验证器从wtforms.validators模块导入,常用的验证器如表所示:
验证器 | 说明 |
---|---|
DataRequired(message=None) | 验证数据是否有效 |
Email(message=None) | 验证Email地址 |
EqualTo(fieldname,message=None) | 验证两个字段值是否相同 |
InputRequired(message=None) | 验证是否有数据 |
Length(min=-1,max=-1,message=None) | 验证输入值长度是否在给定范围内 |
NumberRange(min=None,max=None,message=None) | 验证输入数字是否在给定范围内 |
Optional(strip_whitespace=True) | 允许输入值为空,并跳过其他验证 |
Regexp(regex,flag=0,message=None) | 使用正则表达式验证输入值 |
URL(require_tld=True,message=None) | 验证URL |
AnyOf(values,message=None,values_formatter=None) | 确保输入值在可选值列表中 |
NoneOf(values,message=None,values_formatter=None) | 确保输入值不在可选值列表中 |
以我们使用WTForms创建的LoginForm为例,实例化表单类,然后直接调用实例属性就可获得表单字段对应的HTML代码:
>>> form = LoginForm()
>>> form.username()
u'<input id='username' name="username" type='text' value="">'
>>> form.username.label()
u'<label for='username'>Username</label>'
WTForms输出的字段HTML代码只会包含id和name属性,属性值均为表单类中对应的字段属性名称。但是HTML<input>元素通常包括其他属性来对字段进行设置。要添加额外属性,通常有两种方法:
1、使用render_kw属性
username = StringField(‘Username’,render_kw={‘placeholder’: ‘Your Username’})
2、在调用字段时传入
form.username(style=‘width: 200px’, class_=‘bar’)
处理表单数据
在HTML中,当<form>标签声明的表单中类型为submit的提交字段被单击时,就会创建一个提交表单的HTTP请求,请求中包含表单各个字段的数据。为了提交表单数据,需要在app.route()装饰器中增加HTTP的post方法:
@app.route('/basic', methods=['GET', 'POST'])
表单数据的验证是Web表单最重要的主题之一,表单的验证通常分为客户端验证和服务器验证两种形式。
客户端验证是在客户端(比如Web浏览器)对用户输入值进行验证。客户端方式可以实时动态提示用户输入是否正确,只有用户输入正确后,才会将表单数据发送到服务器,降低了服务器负载。
服务器端验证是指用户把输入的数据提交到服务器端,在服务器端对数据进行验证。如果验证出错,就在返回的响应中加入错误信息。Flask程序中使用WTForms实现的就是服务器端验证。
WTForms验证表单字段的方式是在实例化表单类时传入表单数据,然后对表单实例调用validate()方法。这会逐个对字段调用字段实例化时定义的验证器,返回表示验证结果的布尔值。如果验证失败,就把错误消息存储到表单实例的errors属性对应的字典中。
因为现在的basic视图函数同时接收两种类型的请求:GET请求和POST请求。所以我们要根据请求方法的不同执行不同的代码。当请求方法是GET时,会跳过这个if语句,渲染basic.html模板;当请求的方法是POST时(说明用户提交了表单),则验证表单数据,这会逐个字段调用附加的验证器进行验证。
@app.route('/basic', methods=['GET', 'POST'])
def basic():
form = LoginForm()
if form.validate_on_submit(): #如果用户提交表单并且数据通过验证
... #处理post请求
return render_template('basic.html', form=form) #处理get请求
注意:使用validate_on_submit函数需要from flask_wtf import FlaskForm
来替换wtforms模块的Form。class LoginForm(FlaskForm)
语句的Form也要替换成FlaskForm。
1、验证通过,获取数据
表单类的data属性是一个匹配所有字段与对应数据的字典,我们一般直接通过“form.字段属性名.data”的形式来获取对应字段的数据。例如:
@app.route('/basic', methods=['GET', 'POST'])
def basic():
form = LoginForm()
if form.validate_on_submit():
username = form.username.data
flash('Welcome home, %s!' % username)
return redirect(url_for('index'))
return render_template('basic.html', form=form)
form.username.data返回username字段的值。上面的代码,获取到username字段的数据后,发送一条flash消息,最后将程序重定向到index视图。加重定向语句的原因就是执行最后的render_template函数时,会默认发送上一个请求,那么就会再次提交表单,出现再次提交表单弹窗。加了重定向语句后,上一个请求就是GET请求(获取index页面)。
2、验证失败,返回错误
如果form.validate_on_submit()返回false,验证没通过。WTForms会把错误消息添加到表单类的errors属性中,我们一般会通过字段名来获取对应字段的错误消息列表,即“form.字段名.errors”。比如,form.name.errors返回name字段的错误消息列表。
像之前渲染flash消息一样,可以在模板里使用for循环迭代错误消息列表。
...
{{ form.username.label }}<br>
{{ form.username }}<br>
{% if form.username.errors %}
{% for message in form.username.errors %}
<small class="error">{{ message }}</small><br>
{% endfor %}
{% endif %}
{{ form.password.label }}<br>
...
完整示例工程代码如下:
### 文件index.html ###
<!DOCTYPE html>
<main>
{% for message in get_flashed_messages() %}
<div class="alert">
{{ message }}
</div>
{% endfor %}
</main>
<h1>This is index page</h1>
</html>
### 文件basic.html ###
<!DOCTYPE html>
<form method="post">
{{ form.csrf_token }}
{{ form.username.label }}<br>
{{ form.username }}<br>
{% if form.username.errors %}
{% for message in form.username.errors %}
<small class="error">{{ message }}</small><br>
{% endfor %}
{% endif %}
{{ form.password.label }}<br>
{{ form.password }}<br>
{% if form.password.errors %}
{% for message in form.password.errors %}
<small class="error">{{ message }}</small><br>
{% endfor %}
{% endif %}
{{ form.remember }}{{ form.remember.label }}<br>
{{ form.submit }}<br>
</form>
</html>
### 文件forms.py ###
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length,ValidationError
class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired(), Length(8, 128)])
remember = BooleanField('Remember me')
submit = SubmitField('Log in')
### 文件app.py ###
from flask import Flask, render_template,flash,redirect,url_for
import os
from forms import LoginForm
app = Flask(__name__)
app.secret_key = "secret string"
app.secret_key = os.getenv('SECRET_KEY', 'secret string')
@app.route('/', methods=['GET', 'POST'])
def index():
return render_template('index.html')
@app.route('/basic', methods=['GET', 'POST'])
def basic():
form = LoginForm()
if form.validate_on_submit():
username = form.username.data
flash('Welcome home, %s!' % username)
return redirect(url_for('index'))
return render_template('basic.html', form=form)
初识表单
输入数据
提交表单,返回结果
文件上传
在HTML中,渲染一个文件上传字段只需要将<input>标签的type属性设为file,即<input type=“file”>。这会在浏览器中渲染成一个文件上传字段,单击文件选择按钮会打开文件选择窗口,选择对应的文件后,被选择的文件名会显示在文件选择按钮旁边。
在服务器端,可以和普通数据一样获取上传文件数据并保存。不过需要考虑安全问题,注意验证文件类型、验证文件大小、过滤文件名。
创建上传表单的代码如下:
from flask_wtf.file import FileField, FileRequired, FileAllowed
# upload form
class UploadForm(FlaskForm):
photo = FileField('Upload Image', validators=[FileRequired(), FileAllowed(['jpg', 'jpeg', 'png', 'gif'])])
submit = SubmitField()
代码中创建了一个用来上传图片的photo字段,和其他字段类似,我们也需要对文件上传字段进行验证。flask_wtf.file提供了两个文件相关的验证器。FileRequired(message=None)
验证是否包含文件对象。FileAllowed(upload_set, message=None)
验证文件类型,upload_set参数代表允许的文件后缀名列表。
除了验证文件类型,我们还需要对文件大小进行验证,过大的文件会拖垮服务器。通过设置Flask内置的配置变量MAX_CONTENT_LENGTH,可以限制请求报文的最大长度,单位是字节。比如下面限制最大长度为3M:
app.config['MAX_CONTENT_LENGTH'] = 3 * 1024 * 1024
当上传文件大小超出限制,会返回413错误响应。
在新创建的upload视图里,我们实例化表单类UploadForm,然后传入模板:
@app.route('/upload', methods=['GET', 'POST'])
def upload():
form = UploadForm()
# 文件处理 #
f = form.photo.data
filename = 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)
在模板(upload.html)中渲染表单
<form method="post" enctype="multipart/form-data">
{{ form.csrf_token }}
{{ form_field(form.photo) }}
{{ form.submit }}
</form>
需要注意的是,当表单中包含文件上传字段时(即type属性为file的input标签),需要将表单的enctype属性设为“multipart/form-data”,这会告诉浏览器将上传数据发送到服务器,否则仅会把文件名作为表单数据提交。
当表单验证通过后,我们通过form.photo.data获取存储上传文件的FileStorage对象。由于文件名中可能包含恶意代码,因此使用统一处理的方式(random_filename随机文件名对文件重命名)对所有上传的文件重新命名。
处理完文件名后,再将文件保存到文件系统中。我们在form目录下创建了一个uploads文件夹,用于保存上传后的文件。指向这个文件夹的绝对路径存储在自定义配置变量UPLOAD_PATH中:
app.config['UPLOAD_PATH'] = os.path.join(app.root_path, 'uploads')
app.root_path是程序实例所在脚本(app.py)的绝对路径。
对FileStorage对象调用save()方法即可保存,传入包含目标文件夹绝对路径和文件名作为参数。
在保存文件后,我们使用flash发送一个提示,将文件名保存到session中,最后重定向到show_images视图,show_images视图返回的uploaded.html模板将从session获取文件名,渲染出上传后的图。show_images视图函数如下:
@app.route('/uploaded-images')
def show_images():
return render_template('uploaded.html')
为了让上传后的文件能通过URL获取,我们还需要创建一个视图函数来返回上传后的文件,如下所示:
@app.route('/uploads/<path:filename>')
def get_file(filename):
return send_from_directory(app.config['UPLOAD_PATH'], filename)
这个视图函数通过传入的文件路径返回对应的静态文件,send_from_directory函数用来获取文件,传入文件的路径和文件名作为参数。
在uploaded.html模板中,我们将传入的文件名作为URL变量,通过上面的get_file视图获取文件URL,作为<img>标签的src属性值,如下所示:
<img src="{{ url_for('get_file', filename=filename) }}">
单个表单多个提交按钮
在某些情况下,我们可能需要为一个表单添加多个提交按钮。当用户提交表单时,我们需要在视图函数中根据按下的按钮来做出不同的处理。例如:
# multiple submit button
class NewPostForm(FlaskForm):
title = StringField('Title', validators=[DataRequired(), Length(1, 50)])
body = TextAreaField('Body', validators=[DataRequired()])
save = SubmitField('Save') # 保存按钮
publish = SubmitField('Publish') # 发布按钮
当我们对表单类实例或特定的字段属性调用data属性时,被点击的提交字段的值是True,未被点击的值则是False。基于这个机制,我们可以通过提交按钮字段的值来判断当前被点击的按钮。
@app.route('/two-submits', methods=['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)
单个页面多个表单
有时我们还需要在单个页面上创建多个表单。当同一个页面有多个表单时,我们要解决的就是在视图函数中判断当前被提交的是哪个表单。
解决问题的第一步就是为两个表单的提交字段设置不同的名称。
class SigninForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(1, 20)])
password = PasswordField('Password', validators=[DataRequired(), Length(8, 128)])
submit1 = 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', methods=['GET', 'POST'])
def multi_form():
signin_form = SigninForm()
register_form = RegisterForm()
if signin_form.submit1.data and signin_form.validate():
username = signin_form.username.data
flash('%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.data
flash('%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)
在视图函数中,我们为两个表单添加了各自的if判断,在这两个if语句的内部,我们分别执行各自的代码逻辑。
参考资料:《Flask Web开发实战入门、进阶与原理解析》李辉 著