Flask表单之WTForms和flask-wtf

2020-08-11 14:56:52 浏览数 (1)

Flask-WTF简介

Flask-WTF是简化了WTForms操作的一个第三方库。WTForms表单的两个主要功能是验证用户提交数据的合法性以及渲染模板。还有其它一些功能:CSRF保护,文件上传等。

代码语言:javascript复制
pip install flask-wtf

WTForms常用验证器和自定义验证器

常用的验证器

  • Email:验证上传的数据是否为邮箱格式
  • EqualTo:两个字段是否相等(密码和重复密码)
  • InputRequired:原始数据的需要验证
  • Length:长度限制,有mix和max两个值
  • NumberRange:数字的区间,有mix和max两个值,如果在两个值之间则满足
  • Regexp:自定义正则表达式
  • URL:必须url格式
  • UUID:uuid格式
代码语言:javascript复制
from wtforms import Form,StringField,IntegerField
from wtforms.validators import Length,EqualTo,Email,InputRequired,NumberRange
from wtforms.validators import Regexp,URL,ValidationError


class LoginForm(Form):
    email = StringField(validators=[Email(message='邮箱格式不正确')])
    username = StringField(validators=[InputRequired(message='这个字段必须要填')])
    age = IntegerField(validators=[NumberRange(min=18,max=100)])
    phone = StringField(validators=[Regexp(r'1[38745]d{9}')])
    homepage = StringField(validators=[URL()])
    captcha = StringField(validators=[Length(4,4)])

    # 自定义验证器
    def validate_captcha(self,field):
        if field.data != '1234':      #field.data:用户提交过来的数据
            raise ValidationError('验证码错误')          #如果验证失败,就抛出验证失败的异常

Flask-WTF是集成WTForms,并带有 csrf 令牌的安全表单和全局的 csrf 保护的功能。 每次我们在建立表单所创建的类都是继承与flask_wtf中的FlaskForm,而FlaskForm是继承WTForms中forms。

用法:

1.创建基础表单

例如,form.py:

代码语言:javascript复制
class LoginForm(FlaskForm):
    username = StringField()
    password = PasswordField()
    remember_me = BooleanField(label='Keep me logged in')

2.CSRF保护

任何使用FlaskForm创建的表单发送请求,都会有CSRF的全部保护,在对应的template中HTML渲染表单时,可以加入form.csrf_token:

代码语言:javascript复制
<form method="post">
    {{ form.csrf_token }}
</form>

但是如果模板中没有表单,则可以使用一个隐藏的input标签加入csrf_token。

代码语言:javascript复制
<form method="post">
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
</form>

3.验证表单

在视图处理程序中验证请求: view.py

代码语言:javascript复制
def login():
    form = LoginForm()
    if form.validate_on_submit():
        return redirect('/success')
    return render_template('login.html', form=form)

使用validate_on_submit 来检查是否是一个 POST 请求并且请求是否有效。

4.文件上传

Flask-WTF 提供 FileField 来处理文件上传,它在表单提交后,自动从 flask.request.files 中抽取数据。FileFielddata 属性是一个 Werkzeug FileStorage 实例。

代码语言:javascript复制
from werkzeug import secure_filename
from flask_wtf.file import FileField

class PhotoForm(Form):
    photo = FileField('Your photo')

@app.route('/upload/', methods=('GET', 'POST'))
def upload():
    form = PhotoForm()
    if form.validate_on_submit():
        filename = secure_filename(form.photo.data.filename)
        form.photo.data.save('uploads/'   filename)
    else:
        filename = None
    return render_template('upload.html', form=form, filename=filename)

注意:在 HTML 表单的 enctype 设置成 multipart/form-data,如下:

代码语言:javascript复制
<form action="/upload/" method="POST" enctype="multipart/form-data">
    ....
</form>

5.验证码

Flask-WTF 通过 RecaptchaField 也提供对验证码的支持:

代码语言:javascript复制
from flask_wtf import Form, RecaptchaField
from wtforms import TextField

class SignupForm(Form):
    username = TextField('Username')
    recaptcha = RecaptchaField()

还需要配置一下信息:

字段

配置

RECAPTCHA_PUBLIC_KEY

必须 公钥

RECAPTCHA_PRIVATE_KEY

必须 私钥

RECAPTCHA_API_SERVER

可选 验证码 API 服务器

RECAPTCHA_PARAMETERS

可选 一个 JavaScript(api.js)参数的字典

RECAPTCHA_DATA_ATTRS

可选 一个数据属性项列表 https://developers.google.com/recaptcha/docs/display

WTForms

基本了解

WTForms是一个Flask集成的框架,或者是说库。用于处理浏览器表单提交的数据。它在Flask-WTF 的基础上扩展并添加了一些随手即得的精巧的帮助函数,这些函数将会使在 Flask 里使用表单更加有趣。

用法:

1.field字段

WTForms支持HTML字段:

字段类型

说明

StringField

文本字段, 相当于type类型为text的input标签

TextAreaField

多行文本字段

PasswordField

密码文本字段

HiddenField

隐藏文本字段

DateField

文本字段, 值为datetime.date格式

DateTimeField

文本字段, 值为datetime.datetime格式

IntegerField

文本字段, 值为整数

DecimalField

文本字段, 值为decimal.Decimal

FloatField

文本字段, 值为浮点数

BooleanField

复选框, 值为True 和 False

RadioField

一组单选框

SelectField

下拉列表

SelectMultipleField

下拉列表, 可选择多个值

FileField

文件上传字段

SubmitField

表单提交按钮

FormFiled

把表单作为字段嵌入另一个表单

FieldList

子组指定类型的字段

2.Validators验证器

WTForms可以支持很多表单的验证函数:

验证函数

说明

Email

验证是电子邮件地址

EqualTo

比较两个字段的值; 常用于要求输入两次密钥进行确认的情况

IPAddress

验证IPv4网络地址

Length

验证输入字符串的长度

NumberRange

验证输入的值在数字范围内

Optional

无输入值时跳过其它验证函数

DataRequired

确保字段中有数据

Regexp

使用正则表达式验证输入值

URL

验证url

AnyOf

确保输入值在可选值列表中

NoneOf

确保输入值不在可选列表中

3.自定义Validators验证器

第一种: in-line validator(内联验证器) 也就是自定义一个验证函数,在定义表单类的时候,在对应的字段中加入该函数进行认证。下面的my_length_check函数就是用于判name字段长度不能超过50.

代码语言:javascript复制
def my_length_check(form, field):
    if len(field.data) > 50:
        raise ValidationError('Field must be less than 50 characters')

class MyForm(Form):
    name = StringField('Name', [InputRequired(), my_length_check])

第二种:通用且可重用的验证函数 一般是以validate开头,加上下划线再加上对应的field字段(validate_filed),浏览器在提交表单数据时,会自动识别对应字段所有的验证器,然后执行验证器进行判断。

代码语言:javascript复制
class RegistrationForm(FlaskForm):
    email = StringField('Email', validators=[DataRequired(), Length(1, 60), Email()])
    username = StringField('Username', validators=[DataRequired(), Length(1, 60),
        Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0, 'username must have only letters, numbers dots or underscores')])
    password = PasswordField('Password', validators=[DataRequired(), EqualTo('password2', message='password must match')])
    password2 = PasswordField('Confirm password', validators=[DataRequired()])

    def validate_email(self, field):
        if User.objects.filter(email=field.data).count() > 0:
            raise ValidationError('Email already registered')

    def validate_username(self, field):
        if User.objects.filter(username=field.data).count() > 0:
            raise ValidationError('Username has exist')

第三种:比较高级的validators

代码语言:javascript复制
class Length(object):
    def __init__(self, min=-1, max=-1, message=None):
        self.min = min
        self.max = max
        if not message:
            message = u'Field must be between %i and %i characters long.' % (min, max)
        self.message = message

    def __call__(self, form, field):
        l = field.data and len(field.data) or 0
        if l < self.min or self.max != -1 and l > self.max:
            raise ValidationError(self.message)

length = Length

4.Widget组件

下面可以以登录界面为实例: login.py

代码语言:javascript复制
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from flask import Flask, render_template, request, redirect
from wtforms import Form
from wtforms import validators
from wtforms import widgets

class LoginForm(Form):
    name = simple.StringField(
        label='用户名',
        validators=[
            validators.DataRequired(message='用户名不能为空.'),
        ],
        widget=widgets.TextInput(),
        render_kw={'class': 'form-control'}
    )
    pwd = simple.PasswordField(
        label='密码',
        validators=[
            validators.DataRequired(message='密码不能为空.'),
        ],
        widget=widgets.PasswordInput(),
        render_kw={'class': 'form-control'}
    )

用户登录表单

Flask-WTF插件使用Python类来表示Web表单。表单类只需将表单的字段定义为类属性即可。

为了再次践行我的松耦合原则,我会将表单类单独存储到名为app/forms.py的模块中。就让我们来定义用户登录表单来做一个开始吧,它会要求用户输入username和password,并提供一个“remember me”的复选框和提交按钮:

代码语言:javascript复制
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired

class LoginForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    password = PasswordField('Password', validators=[DataRequired()])
    remember_me = BooleanField('Remember Me')
    submit = SubmitField('Sign In')

大多数Flask插件使用flask_ <name>命名约定来导入,Flask-WTF的所有内容都在flask_wtf包中。在本例中,app/forms.py模块的顶部从flask_wtf导入了名为FlaskForm的基类。

由于Flask-WTF插件本身不提供字段类型,因此我直接从WTForms包中导入了四个表示表单字段的类。每个字段类都接受一个描述或别名作为第一个参数,并生成一个实例来作为LoginForm的类属性。

你在一些字段中看到的可选参数validators用于验证输入字段是否符合预期。DataRequired验证器仅验证字段输入是否为空。更多的验证器将会在未来的表单中接触到。

表单模板

下一步是将表单添加到HTML模板以便渲染到网页上。 令人高兴的是在LoginForm类中定义的字段支持自渲染为HTML元素,所以这个任务相当简单。 我将把登录模板存储在文件*app/templates/login.html *中,代码如下:

代码语言:javascript复制
{% extends "base.html" %}

{% block content %}
    <h1>Sign In</h1>
    <form action="" method="post" novalidate>
        {{ form.hidden_tag() }}
        <p>
            {{ form.username.label }}<br>
            {{ form.username(size=32) }}
        </p>
        <p>
            {{ form.password.label }}<br>
            {{ form.password(size=32) }}
        </p>
        <p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
        <p>{{ form.submit() }}</p>
    </form>
{% endblock %}

这个模板需要一个form参数的传入到渲染模板的函数中,form来自于LoginForm类的实例化,不过我现在还没有编写它。

HTML<form>元素被用作Web表单的容器。 表单的action属性告诉浏览器在提交用户在表单中输入的信息时应该请求的URL。 当action设置为空字符串时,表单将被提交给当前地址栏中的URL,即当前页面。 method属性指定了将表单提交给服务器时应该使用的HTTP请求方法。 默认情况下是用GET请求发送,但几乎在所有情况下,使用POST请求会提供更好的用户体验,因为这种类型的请求可以在请求的主体中提交表单数据, GET请求将表单字段添加到URL,会使浏览器地址栏变得混乱。

form.hidden_tag()模板参数生成了一个隐藏字段,其中包含一个用于保护表单免受CSRF攻击的token。 对于保护表单,你需要做的所有事情就是在模板中包括这个隐藏的字段,并在Flask配置中定义SECRET_KEY变量,Flask-WTF会完成剩下的工作。

如果你以前编写过HTML Web表单,那么你会发现一个奇怪的现象——在此模板中没有HTML表单元素,这是因为表单的字段对象的在渲染时会自动转化为HTML元素。 我只需在需要字段标签的地方加上{{ form.<field_name>.label }},需要这个字段的地方加上{{ form.<field_name>() }}。 对于需要附加HTML属性的字段,可以作为关键字参数传递到函数中。 此模板中的username和password字段将size作为参数,将其作为属性添加到<input> HTML元素中。 你也可以通过这种手段为表单字段设置class和id属性。

表单视图

完成这个表单的最后一步就是编写一个新的视图函数来渲染上面创建的模板。

函数的逻辑只需创建一个form实例,并将其传入渲染模板的函数中即可,然后用*/login* URL来关联它。这个视图函数也存储到app/routes.py模块中,代码如下:

代码语言:javascript复制
from flask import render_template
from app import app
from app.forms import LoginForm

# ...

@app.route('/login')
def login():
    form = LoginForm()
    return render_template('login.html', title='Sign In', form=form)

我从forms.py导入LoginForm类,并生成了一个实例传入模板。form=form的语法看起来奇怪,这是Python函数或方法传入关键字参数的方式,左边的form代表在模板中引用的变量名称,右边则是传入的form实例。这就是获取表单字段渲染结果的所有代码了。

在基础模板templates/base.html的导航栏上添加登录的链接,以便访问:

代码语言:javascript复制
<div>
    Microblog:
    <a href="/index">Home</a>
    <a href="/login">Login</a>
</div>

此时,你可以验证结果了。运行该应用,在浏览器的地址栏中输入http://localhost:5000/,然后点击顶部导航栏中的“Login”链接来查看新的登录表单。 是不是非常炫酷? 接收表单数据

点击提交按钮,浏览器将显示“Method Not Allowed”错误。为什么呢? 这是因为之前的登录视图功能到目前为止只完成了一半的工作。 它可以在网页上显示表单,但没有逻辑来处理用户提交的数据。Flask-WTF可以轻松完成这部分工作, 以下是视图函数的更新版本,它接受和验证用户提交的数据:

代码语言:javascript复制
from flask import render_template, flash, redirect

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        flash('Login requested for user {}, remember_me={}'.format(
            form.username.data, form.remember_me.data))
        return redirect('/index')
    return render_template('login.html', title='Sign In', form=form)

这个版本中的第一个新东西是路由装饰器中的methods参数。 它告诉Flask这个视图函数接受GETPOST请求,并覆盖了默认的GET。 HTTP协议规定对GET请求需要返回信息给客户端(本例中是浏览器)。 本应用的所有GET请求都是如此。 当浏览器向服务器提交表单数据时,通常会使用POST请求(实际上用GET请求也可以,但这不是推荐的做法)。之前的“Method Not Allowed”错误正是由于视图函数还未配置允许POST请求。 通过传入methods参数,你就能告诉Flask哪些请求方法可以被接受。

form.validate_on_submit()实例方法会执行form校验的工作。当浏览器发起GET请求的时候,它返回False,这样视图函数就会跳过if块中的代码,直接转到视图函数的最后一句来渲染模板。

当用户在浏览器点击提交按钮后,浏览器会发送POST请求。form.validate_on_submit()就会获取到所有的数据,运行字段各自的验证器,全部通过之后就会返回True,这表示数据有效。不过,一旦有任意一个字段未通过验证,这个实例方法就会返回False,引发类似GET请求那样的表单的渲染并返回给用户。稍后我会在添加代码以实现在验证失败的时候显示一条错误消息。

form.validate_on_submit()返回True时,登录视图函数调用从Flask导入的两个新函数。 flash()函数是向用户显示消息的有效途径。 许多应用使用这个技术来让用户知道某个动作是否成功。我将使用这种机制作为临时解决方案,因为我没有基础架构来真正地登录用户。 显示一条消息来确认应用已经收到登录认证凭据,我认为对当前来说已经足够了。

登录视图函数中使用的第二个新函数是redirect()。这个函数指引浏览器自动重定向到它的参数所关联的URL。当前视图函数使用它将用户重定向到应用的主页。

当你调用flash()函数后,Flask会存储这个消息,但是却不会奇迹般地直接出现在页面上。模板需要将消息渲染到基础模板中,才能让所有派生出来的模板都能显示出来。更新后的基础模板代码如下:

代码语言:javascript复制
<html>
    <head>
        {% if title %}
        <title>{{ title }} - microblog</title>
        {% else %}
        <title>microblog</title>
        {% endif %}
    </head>
    <body>
        <div>
            Microblog:
            <a href="/index">Home</a>
            <a href="/login">Login</a>
        </div>
        <hr>
        {% with messages = get_flashed_messages() %}
        {% if messages %}
        <ul>
            {% for message in messages %}
            <li>{{ message }}</li>
            {% endfor %}
        </ul>
        {% endif %}
        {% endwith %}
        {% block content %}{% endblock %}
    </body>
</html>

此处我用了with结构在当前模板的上下文中来将get_flashed_messages()的结果赋值给变量messagesget_flashed_messages()是Flask中的一个函数,它返回用flash()注册过的消息列表。接下来的条件结构用来检查变量messages是否包含元素,如果有,则在<ul>元素中,为每条消息用<li>元素来包裹渲染。这种渲染的样式结果看起来不会美观,之后会有主题讲到Web应用的样式。

闪现消息的一个有趣的属性是,一旦通过get_flashed_messages函数请求了一次,它们就会从消息列表中移除,所以在调用flash()函数后它们只会出现一次。

时机成熟,再次测试表单吧,将username和password字段留空并点击提交按钮来观察DataRequired验证器是如何中断提交处理流程的。

完善字段验证

表单字段的验证器可防止无效数据被接收到应用中。 应用处理无效表单输入的方式是重新显示表单,以便用户进行更正。

如果你尝试过提交无效的数据,相信你会注意到,虽然验证机制查无遗漏,却没有给出表单错误的具体线索。下一个任务是通过在验证失败的每个字段旁边添加有意义的错误消息来改善用户体验。

实际上,表单验证器已经生成了这些描述性错误消息,所缺少的不过是模板中的一些额外的逻辑来渲染它们。

这是给username和password字段添加了验证描述性错误消息渲染逻辑之后的登录模板:

代码语言:javascript复制
{% extends "base.html" %}

{% block content %}
    <h1>Sign In</h1>
    <form action="" method="post" novalidate>
        {{ form.hidden_tag() }}
        <p>
            {{ form.username.label }}<br>
            {{ form.username(size=32) }}<br>
            {% for error in form.username.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.password.label }}<br>
            {{ form.password(size=32) }}<br>
            {% for error in form.password.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
        <p>{{ form.submit() }}</p>
    </form>
{% endblock %}

我做的唯一的改变是,在username和password字段之后添加for循环以便用红色字体来渲染验证器添加的错误信息。通常情况下,拥有验证器的字段都会用form.<field_name>.errors来渲染错误信息。 一个字段的验证错误信息结果是一个列表,因为字段可以附加多个验证器,并且多个验证器都可能会提供错误消息以显示给用户。

生成链接

现在的登录表单已经相当完整了,但在结束本章之前,我想讨论在模板和重定向中包含链接的妥当方法。 到目前为止,你已经看到了一些定义链接的例子。 例如,这是当前基础模板中的导航栏代码:

代码语言:javascript复制
    <div>
        Microblog:
        <a href="/index">Home</a>
        <a href="/login">Login</a>
    </div>

登录视图函数同样定义了一个传入到redirect()函数作为参数的链接:

代码语言:javascript复制
@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        # ...
        return redirect('/index')
    # ...

直接在模板和源文件中硬编码链接存在隐患,如果有一天你决定重新组织链接,那么你将不得不在整个应用中搜索并替换这些链接。

为了更好地管理这些链接,Flask提供了一个名为url_for()的函数,它使用URL到视图函数的内部映射关系来生成URL。 例如,url_for('login')返回/loginurl_for('index')返回/indexurl_for()的参数是endpoint名称,也就是视图函数的名字。

你可能会问,为什么使用函数名称而不是URL? 事实是,URL比起视图函数名称变更的可能性更高。 稍后你会了解到的第二个原因是,一些URL中包含动态组件,手动生成这些URL需要连接多个元素,枯燥乏味且容易出错。 url_for()生成这种复杂的URL就方便许多。

因此,从现在起,一旦我需要生成应用链接,我就会使用url_for()。基础模板中的导航栏部分代码变更如下:

代码语言:javascript复制
    <div>
        Microblog:
        <a href="{{ url_for('index') }}">Home</a>
        <a href="{{ url_for('login') }}">Login</a>
    </div>

login()视图函数也做了相应变更:

代码语言:javascript复制
from flask import render_template, flash, redirect, url_for

# ...

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        # ...
        return redirect(url_for('index'))
    # ...

0 人点赞