Flask之表单

news2024/11/17 15:43:46

前言:本博客仅作记录学习使用,部分图片出自网络,如有侵犯您的权益,请联系删除 

目录

一、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, 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')

每个字段属性通过实例化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 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保护,默认为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
methodget表单提交的HTTP请求方法,目前仅支持使用GET和POST方法
enctypeapplication/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+POST
    if 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+POST
    if 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+POST
    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)

表单类的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 LoginForm

app = 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 ValidationError

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

在现实中,通常让验证器支持传入参数来对验证过程进行设置。至少支持message参数来设置自定义错误消息。此时验证函数应该实现成工厂函数,即返回一个可调用对象的函数。

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

在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,FileAllowed

class 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.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)

表单验证通过后,我们通过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 uuid

def random_filename(filename):
    ext = os.path.splitext(filename)[1]
    new_filename = uuid.uuid4().hex + ext
    return 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 MultipleFileField

class 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'] = filenames
        return 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集成富文本编辑器

富文本编辑器即WYSIWYGWhat 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_LOCALFalse设为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, Length

class 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.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)

以登录表单(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.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 = SigninForm()
    register_form = RegisterForm()
    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属性:

...
<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中获取并迭代错误消息

致谢

在此,我要对所有为知识共享做出贡献的个人和机构表示最深切的感谢。同时也感谢每一位花时间阅读这篇文章的读者,如果文章中有任何错误,欢迎留言指正。 

学习永无止境,让我们共同进步!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1864430.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

武汉星起航:亚马逊全球化布局助力企业拓展国际市场

在当今全球化经济的大背景下&#xff0c;企业如何突破地域限制&#xff0c;将产品推向更广阔的市场&#xff0c;成为了摆在众多企业家面前的重要课题。武汉星起航相信&#xff0c;亚马逊&#xff0c;作为全球最大的在线零售平台之一&#xff0c;以其独特的全球化布局和强大的服…

nuget 包修改默认存放路径

平时使用 nuget packages 时&#xff0c;都是下载包文件到本地。 默认是在C盘&#xff0c;时间一久容量会高达几十个G&#xff0c;这样会拖慢系统运行效率。 这时需要修改包的下载位置。 打开nuget 包配置文件&#xff1a;Nuget.config 路径在 C:\Users\{UserName}\AppData…

一年Java|16K|同程艺龙面经

面经哥只做互联网社招面试经历分享&#xff0c;关注我&#xff0c;每日推送精选面经&#xff0c;面试前&#xff0c;先找面经哥 背景 公司&#xff1a;同程艺龙成都BU,现场部门老大面 之前的同程艺龙电话一面过了&#xff0c;然后通知到同程艺龙成都办公地点现场进行部门老大…

RK3588 Android13 TvSetting 中性能浮窗RAM显示bug

前言 电视产品,客户发现在设备偏好设置->高级设置->性能浮窗菜单里显示的 RAM 大小是错误的, 要求改成正确的,并且屏幕密度修改后,这个浮窗显示不全,也需要一起处理。 效果图 TvSetting 部分修改文件清单 bug 原因在于 Formatter.formatFileSize 这个 API,我们…

ATA-7025高压放大器的优势如何

高压放大器是一类在电子领域中具有重要作用的设备&#xff0c;其主要功能是将输入信号的电压放大到更高的水平。在许多应用中&#xff0c;高压放大器展现出独特的优势&#xff0c;下面将介绍高压放大器的优势以及它们在不同领域的应用。 高压放大器的优势 1.信号驱动能力强 高压…

探索AI世界系列:俗说AI智能体

AI agent&#xff0c;翻译为中文就是AI智能体。 什么是AI智能体呢&#xff1f; 一&#xff0c;GPT对AI智能体的定义 AI智能体&#xff0c;即人工智能体&#xff08;Artificial Intelligence Agent&#xff09;&#xff0c;是具有自主性、学习能力和推理能力的计算机程序。 …

常用的企业级快速传输大文件平台

在当今企业运营中&#xff0c;数据管理成了一项不可或缺的任务。企业每日需处理庞大的数据量&#xff0c;这包括高清视频、大量数据集和复杂的设计图纸等大型文件。然而&#xff0c;传统的文件传输手段&#xff0c;比如通过电子邮件发送附件或使用FTP服务&#xff0c;已经难以满…

【C++】关于虚函数的理解

深入探索C虚函数&#xff1a;原理、应用与实例分析 一、虚函数的原理二、虚函数的应用三、代码实例分析四、总结 在C面向对象编程的世界里&#xff0c;虚函数&#xff08;Virtual Function&#xff09;扮演着至关重要的角色。它不仅实现了多态性这一核心特性&#xff0c;还使得…

充电宝怎么选合适?买充电宝必看选购攻略!好用充电宝推荐

在这个科技飞速发展的时代&#xff0c;手机、平板等电子设备已经成为我们生活中不可或缺的一部分。然而&#xff0c;电池续航问题却常常困扰着我们&#xff0c;特别是在外出旅行、出差或者日常通勤中。这时候&#xff0c;一个靠谱的充电宝就显得尤为重要。但是&#xff0c;面对…

MySQL学习(3):SQL语句之DDL

1.SQL通用语法与分类 &#xff08;1&#xff09;通用语法 &#xff08;2&#xff09;分类 2.DDL 2.1数据库操作 show DATABASES; #查询所有数据库select DATABASE(); #查询当前数据库create DATABASE 数据库名称 [default charest 字符集] [collate 排列规则]; #default cha…

gMLP(NeurIPS 2021)原理与代码解析

paper&#xff1a;Pay Attention to MLPs third-party implementation&#xff1a;https://github.com/huggingface/pytorch-image-models/blob/main/timm/models/mlp_mixer.py 方法介绍 gMLP和MLP-Mixer以及ResMLP都是基于MLP的网络结构&#xff0c;非常简单&#xff0c;关…

太阳初升:born 诞生

在《long long ago》中&#xff0c;我们分析出了首字母l的形象&#xff0c;就是长长的脐带的形象&#xff0c;ong就是脐带冗余蔓连于婴儿肚子上的形象&#xff0c;整个场景为婴儿呱呱坠地脐带尚未剪掉时的情景&#xff0c;而且on通汉字“旦”&#xff0c;通“one”&#xff0c;…

红酒品鉴秘籍:一键解锁味觉宇宙,开启你的味觉探险新纪元

红酒&#xff0c;这种优雅的液体&#xff0c;蕴藏着丰富的口感和层次&#xff0c;每一次的品鉴都是一次味觉的探险。今天&#xff0c;就让我们一起探索红酒品鉴的奥秘&#xff0c;解锁味觉的新世界&#xff0c;而在这个过程中&#xff0c;雷盛红酒将成为我们的向导&#xff0c;…

GraphQL:简介

GraphQL 图片来源&#xff1a; 我们将探索GraphQL 的基础知识&#xff0c;并学习如何使用Apollo将其与 React 和 React Native 等前端框架连接起来。这将帮助您了解如何使用 GraphQL、React、React Native 和 Apollo 构建现代、高效的应用程序。 什么是 GraphQL&#xff1f;…

[深度学习] 生成对抗网络GAN

生成对抗网络&#xff08;Generative Adversarial Networks&#xff0c;GANs&#xff09;是一种由 Ian Goodfellow 等人在2014年提出的深度学习模型Generative Adversarial Networks。GANs的基本思想是通过两个神经网络&#xff08;生成器和判别器&#xff09;的对抗过程&#…

Nodejs使用mqtt库连接阿里云服务器

建项目 命令行输入&#xff1a; npm init 输入项目名&#xff0c;自动化生成项目列表。 6.3 编写代码 新建mqtt_demo_aliyun.js&#xff0c;代码如下&#xff1a; // mqtt_demo_aliyun.jsconst mqtt require("mqtt"); const connectUrl "ws://post-cn-nw**…

展厅设计中需要人性化的地方

1、预留参观空间 展厅空间的布局设计必须尽可能的宽敞&#xff0c;以避免参观人数较多时可能会发生的拥堵&#xff0c;重点展品需要预留较大的展示空间或四面通畅的中心位置&#xff0c;更方便观众从不同角度与方位参观。因为是展厅&#xff0c;不仅代表着企业形象&#xff0c;…

安科瑞光伏并网电表ADL400N-CT双向计量防逆流自带互感器电表-安科瑞 蒋静

1 概述 ADL 系列导轨式多功能电能表&#xff0c;是主要针对于光伏并网系统、微逆系统、储能系统、交流耦合系统等新能源发电系统而设计的一款智能仪表&#xff0c;产品具有精度高、体积小、响应速度快、安装方便等特点。具有对电力参数进行采样计量和监测&#xff0c;逆变器或…

flask与vue实现通过websocket通信

在一些情况下&#xff0c;我们需要实现前后端之间的时刻监听&#xff0c;本文是一篇工具文档&#xff0c;用于解决前后端之间使用websocket交互。 一. Flask的相关配置 1. 下载相关依赖库 如果还没有配置flask的话&#xff0c;需要先安装flask,同时为解决跨域问题&#xff0…

Topaz Gigapixel AI图片无损放大软件下载安装,Topaz Gigapixel AI 高精度的图片无损放大

Topaz Gigapixel AI无疑是一款革命性的图片无损放大软件&#xff0c;它在图像处理领域开创了一种全新的可能性。 Topaz Gigapixel AI的核心功能在于能够将图片进行高精度的无损放大。虽然经过软件处理的图片严格意义上并不能算是完全无损&#xff0c;但相较于传统方法&#xf…