表单

WTForms是一个使用Python编写的表单库,它使得表单的定义、验证(服务器端)和处理变得非常轻松。

HTML表单

<form method="post">
    <label for="username">Username</label><br>
    <input type="text" name="username" placeholder="Kevin"><br>

    <label for="password">Password</label><br>
    <input type="text" name="username" placeholder="hi,kevin"><br>

     <input id="remember" name="remember" type="checkbox" checked>
     <label for="remember"><small>Remember me</small></label><br>

    <input type="submit" name="sumbit" value="Log in">
</form>

使用Flask-WTF处理表单

扩展Flask-WTF集成了WTForms,使用它可以在Flask中更方便地使用WTForms。Flask-WTF将表单数据解析、CSRF保护、文件上传等功能与Flask集成,另外还附加了re CAPTCHA支持。

先用Pipenv安装Flask-WTF及其依赖:

pip install -i https://pypi.douban.com/simple/ flask-wtf

Flask-WTF默认为每个表单启用CSRF保护,它会为我们自动生成和验证CSRF令牌。默认情况下,Flask-WTF使用程序密钥来对CSRF令牌进行签名。所以我们要设置密钥。

app.secret_key = 'hello,kevin'

定义WTForms表单类

当使用WTForms创建表单时,表单由Python类表示,这个类继承从WTForms导入的Form基类。下面定义了一个Login Form类,最终会生成我们在前面定义的HTML表单:

from wtforms import Form,StringField,PasswordField,BooleanField,SubmitField
from wtforms.validators import DataRequired,Length

class LoginForm(Form):
    username = StringField('Username',validators=[DataRequired()])
    password = StringField('Password', validators=[DataRequired(),Length(8,128)])

    remember = BooleanField('Remember me')
    submit = SubmitField('Log in')

每个字段属性通过实例化WTForms提供的字段类表示。字段属性的名称将作为对应HTML<input>元素的name属性及id属性值。

常用的WTForms字段:

实例化字段类常用参数:

在实例化字段类时使用validators关键字来指定附加的验证器列表。验证器从wtforms.validators模块中导入。

常用的WTForms验证器

我们可以使用message关键字传递参数,通过传入自定义错误信息来覆盖内置消息,比如:

name = StringField('Your Name',validators=[DataRequired(message=u'名字不能为空!')])

form/forms.py:定义表单类

from flask_wtf import FlaskForm
from wtforms import StringField,PasswordField,BooleanField,SubmitField
from wtforms.validators import DataRequired,Length

class 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保护,默认为TrueFlask-WTF会自动在实例化表单类时添加一个包含CSRF令牌值的隐藏字段,字段名为csrf_token

输出HTML代码

实例化表单类,然后将实例属性转换成字符串或直接调用就可以获取表单字段对应的HTML代码:

form = LoginForm()
form.username()

在模板中渲染表单

首先在视图函数里实例化表单类Login Form,然后在render_template()函数中使用关键字参数form将表单实例传入模板。

form/app.py:传入表单类实例

from flask import Flask,render_template
from forms import LoginForm

app = Flask(__name__)

@app.route('/basic')
def basic():
    form = LoginForm()
    return render_template('basic.html',form=form)

在模板中,只需要调用表单类的属性即可获取字段对应的HTML代码,如果需要传入参数,也可以添加括号。

form/templates/basic.html:在模板中渲染表单

<form method="post">
    {{ form.csrf_token }}<!--渲染CSRF令牌隐藏字段-->
    {{ form.username.label }}{{ form.username }}<br>
    {{ form.password.label }}{{ form.password }}<br>
    {{ form.remember }}{{ form.remember.label }}<br>
    {{ form.submit }}<br>
</form>

form/templates/bootstrap.html:渲染Bootstrap风格表单

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>

<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css')}}">

<body>
<form method="post">
    {{ form.csrf_token }}<!--渲染CSRF令牌隐藏字段-->

    <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="from-check">
    {{ form.remember(class='form-check-input')}}
    {{ form.remember.label }}
    </div>

    {{ form.submit(class='btn btn-primary')}}
</form>
</body>
</html>

处理表单数据

从获取数据到保存数据大致会经历以下步骤:
1)解析请求,获取表单数据。
2)对数据进行必要的转换,比如将勾选框的值转换成Python的布尔值。
3)验证数据是否符合要求,同时验证CSRF令牌。
4)如果验证未通过则需要生成错误消息,并在模板中显示错误消息。
5)如果通过验证,就把数据保存到数据库或做进一步处理。

提交表单

表单的提交行为主要由三个属性控制:

当使用GET方法提交表单数据时,表单的数据会以查询字符串的形式附加在请求的URL里,比如:http://127.0.0.1:5000/basic?username=kevin&password=123456

form/app.py:设置监听POST方法

@app.route('/', methods=['GET', 'POST'])
def index():
    return render_template('index.html')

验证表单数据

客户端验证和服务器端验证

客户端验证
客户端验证(client side validation)是指在客户端(比如Web浏览器)对用户的输入值进行验证。比如,使用HTML5内置的验证属性即可实现基本的客户端验证(type、required、min、max、accept等)。比如,下面的username字段添加了required标志:<input type="text" name="username" required>

服务器端验证
服务器端验证(server side validation)是指用户把输入的数据提交到服务器端,在服务器端对数据进行验证。如果验证出错,就在返回的响应中加入错误信息。用户修改后再次提交表单,直到通过验证。我们在Flask程序中使用WTForms实现的就是服务器端验证。

WTForms验证机制

WTForms验证表单字段的方式是在实例化表单类时传入表单数据,然后对表单实例调用validate()方法。这会逐个对字段调用字段实例化时定义的验证器,返回表示验证结果的布尔值。如果验证失败,就把错误消息存储到表单实例的errors属性对应的字典中。

在视图函数中验证表单

首先是实例化表单,如果是GET请求,那么就渲染模板;如果是POST请求,就调用validate()方法验证表单数据。

请求的HTTP方法可以通过request.method属性获取,我们可以使用下面的方式来组织视图函数:

from flask import request
@app.route('/basic', methods=['GET', 'POST'])
def basic():
    form = LoginForm()
    #if 用户提交表单 and 数据通过验证
    if request.method == 'POST' and form.validate():
        return render_template('basic.html', form=form)

如果form.validate_on_submit()返回True,则表示用户提交了表单,且表单通过验证,那么我们就可以在这个if语句内获取表单数据。

form/app.py:表单验证与获取数据

from flask import Flask,render_template,redirect,url_for,flash
from forms import LoginForm

app = Flask(__name__)

app.secret_key = 'kevin'

@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.validate_on_submit()返回False,那么说明验证没有通过。对于验证未通过的字段,WTForms会把错误消息添加到表单类的errors属性中,这是一个匹配作为表单字段的类属性到对应的错误消息列表的字典。我们一般会直接通过字段名来获取对应字段的错误消息列表,即“form.字段名.errors”。比如,form.name.errors返回name字段的错误消息列表。

form/templates/basic.html:渲染错误消息

<form method="post">
    {{ form.csrf_token }}<!--渲染CSRF令牌隐藏字段-->
    {{ form.username.label }}{{ form.username }}<br>

    {% for message in form.username.errors %}
        <small class="error"> {{message}}</small><br>
    {% endfor %}

    {{ form.password.label }}{{ 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>

表单进阶实践

设置错误消息语言

设置内置错误消息语言为中文

from flask import Flask
from flask_wtf import FlaskForm
from wtforms import StringField,SubmitField
from wtforms.validators import DataRequired

app = Flask(__name__)
app.config['WTF_I18N_ENABLED'] = False

class My_Base_Form(FlaskForm):
    class Meta:
        locales = ['zh']


class Hello_Form(My_Base_Form):
    name = StringField('Name',validators=[DataRequired()])
    submit = SubmitField()

需要将配置变量WTF_I18N_ENABLED设为False,这会让Flask-WTF使用WTForms内置的错误消息翻译。然后我们需要在自定义基类中定义Meta类,并在locales列表中加入简体中文的地区字符串。在创建表单时,继承这个My_BaseForm即可将错误消息语言设为中文,比如上面定义的Hello_Form

使用宏渲染表单

为了避免为每一个字段重复这些代码,我们可以创建一个宏来渲染表单字段
macros.html:表单渲染宏

{% macro form_field(field) %}
    {{ field.label }}<br>
    {{ field(**kwargs) }}<br>
    {% if field.errors -%}
        {% for error in field.errors -%}
            <small class="error">{{ error }}</small><br>
        {%- endfor %}
    {%- endif %}
{% endmacro %}

这个form_field()宏接收表单类实例的字段属性和附加的关键字参数作为输入,返回包含<label>标签、表单字段、错误消息列表的HTML表单字段代码。使用这个宏渲染表单的示例如下所示:

{% from 'macros.html' import form_field %}

<form method="post">
    {{ signin_form.csrf_token }}
    {{ form_field(signin_form.username) }}
    {{ form_field(signin_form.password) }}
    {{ signin_form.submit1 }}
</form>

自定义验证器

行内验证器

form/forms.py:针对特定字段的验证器

from flask import Flask
from flask_wtf import FlaskForm
from wtforms import SubmitField,IntegerField
from wtforms.validators import ValidationError

app = Flask(__name__)

class FortyTwoForm(FlaskForm):
    answer = IntegerField('The Number')
    submit = SubmitField()
    def validate_answer(form,field):
        if field.data != 42:
            raise ValidationError('Must be 42.')

当表单类中包含以“validate_字段属性名”形式命名的方法时,在验证字段数据时会同时调用这个方法来验证对应的字段,这也是为什么表单类的字段属性名不能以validate开头。验证出错时抛出从wtforms.validators模块导入的Validation Error异常,传入错误消息作为参数。因为这种方法仅用来验证特定的表单类字段,所以又称为行内验证器(in-line validator)。

全局验证器

全局验证器示例

from flask_wtf import FlaskForm
from wtforms import SubmitField,IntegerField
from wtforms.validators import ValidationError

def 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列表中传入的验证器必须是可调用对象,所以这里传入了函数对象,而不是函数调用

工厂函数形式的全局验证器示例

from flask_wtf import FlaskForm
from wtforms import SubmitField,IntegerField
from wtforms.validators import ValidationError

def 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_42



class FortyTwoForm(FlaskForm):
    answer = IntegerField('The Number',validators=[is_42()])
    submit = SubmitField()

文件上传

不过我们需要考虑安全问题,文件上传漏洞也是比较流行的攻击方式。除了常规的CSRF防范,我们还需要重点注意下面的问题:

  • 验证文件类型。
  • 验证文件大小。
  • 过滤文件名。

定义上传表单

在Python表单类中创建文件上传字段时,我们使用扩展Flask-WTF提供的FileField类,它继承WTForms提供的上传字段File Field,添加了对Flask的集成。

form/forms.py:创建上传表单

from flask_wtf.file import FileField,FileRequired,FileAllowed

class UploadForm(FlaskForm):
    photo = FileField('Upload Image', validators=[FileRequired(), FileAllowed(['jpg', 'jpeg', 'png', 'gif'])])
    submit = SubmitField()

Flask-WTF在flask_wtf.file模块下提供了两个文件相关的验证器:

HTML5中的accept属性也可以在客户端实现简单的类型过滤。这个属性接收MIME类型字符串或文件格式后缀,多个值之间使用逗号分隔,比如:

<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  # 3Mb

当请求数据(上传文件大小)超过这个限制后,会返回413错误响应(RequestEntity Too Large)。

渲染上传表单

在新创建的upload视图里,我们实例化表单类UploadForm,然后传入模板:

@app.route('/upload', methods=['GET', 'POST'])
def upload():
    form = UploadForm()
    return render_template('upload.html',form=form)

form/templates/upload.html:在模板中渲染上传表单

{% from 'macros.html' import form_field %}

<form method="post" enctype="multipart/form-data">
    {{ form.csrf_token }}
    {{ form_field(form.photo) }}
    {{ form.submit }}
</form>

处理上传文件

form/app.py:处理上传文件

from forms import UploadForm
import os,uuid
app.config['UPLOAD_PATH'] = os.path.join(app.root_path, 'uploads')


def random_filename(filename):
    ext = os.path.splitext(filename)[1]
    new_filename = uuid.uuid4().hex + ext
    return new_filename

@app.route('/upload', methods=['GET', 'POST'])
def upload():
    form = UploadForm()
    if form.validate_on_submit():
        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)

处理文件名,通常有三种处理方式:
(1)使用原文件名
如果能够确定文件的来源安全,可以直接使用原文件名,通过File Storage对象的filename属性获取:filename = f.filename

(2)使用过滤后的文件名
可以使用Werkzeug提供的secure_filename()函数对文件名进行过滤,传递文件名作为参数,它会过滤掉所有危险字符,返回“安全的文件名”。

(3)统一重命名
secure_filename()函数非常方便,它会过滤掉文件名中的非ASCII字符。

为了避免出现这种情况,更好的做法是使用统一的处理方式对所有上传的文件重新命名。随机文件名有很多种方式可以生成,下面是一个使用Python内置的uuid模块生成随机文件名的random_filename()函数:

def random_filename(filename):
    ext = os.path.splitext(filename)[1]
    new_filename = uuid.uuid4().hex + ext
    return new_filename

这个函数接收原文件名作为参数,使用内置的uuid模块中的uuid4()方法生成新的文件名,并使用hex属性获取十六进制字符串,最后返回包含后缀的新文件名。

指向这个文件夹的绝对路径存储在自定义配置变量UPLOAD_PATH中:app.config['UPLOAD_PATH'] = os.path.join(app.root_path, 'uploads'),相当于os.path.abspath(os.path.dirname(__file__)),为了保存文件,你需要提前手动创建这个文件夹。

文件保存后,我们希望能够显示上传后的图片。为了让上传后的文件能够通过URL获取,我们还需要创建一个视图函数来返回上传后的文件,如下所示:

@app.route('/uploads/<path:filename>')
def get_file(filename):
    return send_from_directory(app.config['UPLOAD_PATH'], filename)

这个视图的作用与Flask内置的static视图类似,通过传入的文件路径返回对应的静态文件。在这个uploads视图中,我们使用Flask提供的send_from_directory()函数来获取文件,传入文件的路径和文件名作为参数。

在upload视图里保存文件后,我们使用flash()发送一个提示,将文件名保存到session中,最后重定向到show_images视图。show_images视图返回的uploaded.html模板中将从session获取文件名,渲染出上传后的图片。

多文件上传

在客户端,通过在文件上传字段(type=file)加入multiple属性,就可以开启多选:

<input type="file" id="file" name="file" multiple>

创建表单类时,可以直接使用WTForms提供的Multiple File Field字段实现,添加一个Data Required验证器来确保包含文件:

class MultiUploadForm(FlaskForm):
    photo = MultipleFileField('Upload Image', validators=[DataRequired()])
    submit = SubmitField()

form/app.py:处理多文件上传

@app.route('/multi-upload', methods=['GET', 'POST'])
def multi_upload():
    form = MultiUploadForm()

    if request.method == 'POST':
        filenames = []

        # check csrf token
        try:
            validate_csrf(form.csrf_token.data)
        except ValidationError:
            flash('CSRF token error.')
            return redirect(url_for('multi_upload'))

        # check if the post request has the file part
        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 user does not select file, browser also
            # submit a empty part without filename
            # if f.filename == '':
            #     flash('No selected file.')
            #    return redirect(url_for('multi_upload'))
            # check the file extension
            if f and allowed_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'] = filenames
        return redirect(url_for('show_images'))
    return render_template('upload.html', form=form)

在请求方法为POST时,我们对上传数据进行手动验证,主要包含下面几步:
1)手动调用flask_wtf.csrf.validate_csrf验证CSRF令牌,传入表单中csrf_token隐藏字段的值。如果抛出wtforms.Validation Error异常则表明验证未通过。
2)其中if 'photo' not in request.files用来确保字段中包含文件数据(相当于FileRequired验证器),如果用户没有选择文件就提交表单则request.files将为空。
3)if f用来确保文件对象存在,这里也可以检查f是否是File Storage实例。
4)allowed_file(f.filename)调用了allowed_file()函数,传入文件名。这个函数相当于File Allowed验证器,用来验证文件类型,返回布尔值。

form/app.py:验证文件类型

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']

使用Flask-CKEditor集成富文本编辑器

扩展Flask-CKEditor简化了在Flask程序中使用CKEditor的过程,我们将使用它来集成CKEditor

pip install -i https://pypi.douban.com/simple/ flask-ckeditor

实例化Flask-CKEditor提供的CKEditor类,传入程序实例:

from flask import Flask
from flask_ckeditor import CKEditor

app = Flask(__name__)
ckeditor = CKEditor(app)

配置富文本编辑器

Flask-CKEditor提供了许多配置变量来对编辑器进行设置,常用的配置及其说明。

Flask-CKEditor内置了对常用第三方CKEditor插件的支持,你可以轻松地为编辑器添加图片上传与插入、插入语法高亮代码片段、Markdown编辑模式等功能。

渲染富文本编辑器

富文本编辑器在HTML中通过文本区域字段表示,即<textarea></textarea>。Flask-CKEditor通过包装WTForms提供的Text Area Field字段类型实现了一个CKEditor Field字段类,我们使用它来构建富文本编辑框字段。

form/forms.py:文章表单

# CKEditor Form
class RichTextForm(FlaskForm):
    title = StringField('Title', validators=[DataRequired(), Length(1, 50)])
    body = CKEditorField('Body', validators=[DataRequired()])
    submit = SubmitField('Publish')

form/templates/ckeditor.html:渲染包含CKEditor编辑器的表单

{% extends 'base.html' %}
{% from 'macros.html' import form_field %}

{% block content %}
    <h2>Integrate <a href="https://ckeditor.com">CKEditor</a> with <a href="https://github.com/greyli/flask-ckeditor">Flask-CKEditor</a>
    </h2>
    <form method="post">
        {{ form.csrf_token }}
        {{ form_field(form.title) }}
        {{ form_field(form.body) }}
        {{ form.submit }}
    </form>
{% endblock %}

{% block scripts %}
    {{ super() }}
    {{ ckeditor.load() }}
    {{ ckeditor.config(name='body') }}
{% endblock %}

渲染CKEditor编辑器需要加载相应的Java Script脚本。在开发时,为了方便开发,可以使用Flask-CKEditor在模板中提供的ckeditor.load()方法加载资源,它默认从CDN加载资源,将CKEDITOR_SERVE_LOCAL设为Ture会使用扩展内置的本地资源,内置的本地资源包含了几个常用的插件和语言包。ckeditor.load()方法支持通过pkg_type参数传入包类型,这会覆盖配置CKEDITOR_PKG_TYPE的值,额外的version参数可以设置从CDN加载的CKEditor版本。

单个表单多个提交按钮

创建了一个这样的表单,其中save表示保存草稿按钮, publish表示发布按钮,正文字段使用Text Area Field字段。

form/forms.py:包含两个提交按钮的表单

# 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')

当表单数据通过POST请求提交时,Flask会把表单数据解析到request.form字典。如果表单中有两个提交字段,那么只有被单击的提交字段才会出现在这个字典中。当我们对表单类实例或特定的字段属性调用data属性时,WTForms会对数据做进一步处理。对于提交字段的值,它会将其转换为布尔值:被单击的提交字段的值将是True,未被单击的值则是False。

基于这个机制,我们可以通过提交按钮字段的值来判断当前被单击的按钮,如代码

form/app.py:判断被单击的提交按钮

@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)

单个页面多个表单

单视图处理

form/forms.py:为两个表单设置不同的提交字段名称

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')

在视图函数中,我们分别实例化这两个表单,根据提交字段的值来区分被提交的表单。

form/app.py:在视图函数中处理多个表单

@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语句的内部,我们分别执行各自的代码逻辑。以登录表单(Signin Form)的if判断为例,如果signin_form.submit1.data的值为True,那就说明用户提交了登录表单,这时我们手动调用signin_form.validate()对这个表单进行验证。

这两个表单类实例通过不同的变量名称传入模板,以便在模板中相应渲染对应的表单字段:

<form method="post">
    {{ signin_form.csrf_token }}
    {{ form_field(signin_form.username) }}
    {{ form_field(signin_form.password) }}
    {{ signin_form.submit1 }}
</form>
<h3>Register Form</h3>
<form method="post">
    {{ register_form.csrf_token }}
    {{ form_field(register_form.username) }}
    {{ form_field(register_form.email) }}
    {{ form_field(register_form.password) }}
    {{ register_form.submit2 }}
</form>

多视图处理

在介绍表单处理时,我们在同一个视图函数内处理两类工作:渲染包含表单的模板(GET请求)、处理表单请求(POST请求)。

当处理多个表单时,我们可以把表单的渲染在单独的视图函数中处理,如下所示:

@app.route('/multi-form-multi-view')
def multi_form_multi_view():
    signin_form = SigninForm2()
    register_form = RegisterForm2()
    return render_template('2form2view.html', signin_form=signin_form, register_form=register_form)

这个视图只负责处理GET请求,实例化两个表单类并渲染模板。另外我们再为每一个表单单独创建一个视图函数来处理验证工作。处理表单提交请求的视图仅监听POST请求。

form/app.py:使用单独的视图函数处理表单提交的POST请求

@app.route('/handle-signin', methods=['POST'])
def handle_signin():
    signin_form = SigninForm2()
    register_form = RegisterForm2()

    if signin_form.validate_on_submit():
        username = signin_form.username.data
        flash('%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 = SigninForm2()
    register_form = RegisterForm2()

    if register_form.validate_on_submit():
        username = register_form.username.data
        flash('%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属性:

<h3>Login Form</h3>
<form method="post" action="{{ url_for('handle_signin') }}">
    {{ signin_form.csrf_token }}
    {{ form_field(signin_form.username) }}
    {{ form_field(signin_form.password) }}
    {{ signin_form.submit }}
</form>

<h3>Register Form</h3>
<form method="post" action="{{ url_for('handle_register') }}">
    {{ register_form.csrf_token }}
    {{ form_field(register_form.username) }}
    {{ form_field(register_form.email) }}
    {{ form_field(register_form.password) }}
    {{ register_form.submit }}
</form>

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!