Python Flask框架基础(四)表单

生成表单

在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开发实战入门、进阶与原理解析》李辉 著

  • 17
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值