Flask之表单

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

目录

一、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, SubmitFieldfrom 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 FlaskFormfrom wtforms import StringField, PasswordField, BooleanField, SubmitFieldfrom 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+POSTif 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+POSTif 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+POSTif form.validate_on_submit():username = form.username.dataflash('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 LoginFormapp = 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 ValidationErrorclass 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 ValidationErrordef 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 ValidationErrordef 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_42class 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,FileAllowedclass 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.datafilename = 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 uuiddef random_filename(filename):ext = os.path.splitext(filename)[1]new_filename = uuid.uuid4().hex + extreturn 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 MultipleFileFieldclass 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'] = filenamesreturn 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, Lengthclass 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.dataflash('%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.dataflash('%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.dataflash('%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.dataflash('%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.mzph.cn/pingmian/35424.shtml

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

相关文章

geojson文件默认已有的style会导致webGL渲染错误处理办法

geojson文件默认已有的style会导致webGL渲染错误处理办法 相关链接&#xff1a; 功能示例(Vue版) | Mars3D三维可视化平台 | 火星科技 代码&#xff1a; export function showDraw(isFlyTo) {removeLayer()graphicLayer new mars3d.layer.GeoJsonLayer({data: {type: &quo…

有两个长方柱,其高、宽、长分别为12,20,25;10,14,20。求它们的体积。编写一个基于对象的程序,在类中用带参数的构造函数对数据成员初始化

在上一篇文章中的构造函数不带参数&#xff0c;在函数体中对数据成员赋初值。这种方式使该类的每一个对象的数据成员都得到同一组初值&#xff08;例中各个对象的数据成员的初值均为0)。但有时用户希望对不同的对象赋予不同的初值&#xff0c;这时就无法使用上面的办法来解决了…

Open AI 前 Superalignment部门研究员Leopold Aschenbrenner的关于Superintelligence担忧的真挚长文

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

理解MySQL数据库主键:从基础概念到实践

一、前言 在关系型数据库中&#xff0c;主键&#xff08;Primary Key&#xff09;是一个至关重要的概念。它不仅用于唯一标识表中的每一行数据&#xff0c;还在保证数据完整性和执行高效查询方面发挥着重要作用。本篇文章将深入探讨MySQL数据库主键的相关知识&#xff0c;包括…

基于ESP8266串口WIFI模块ESP-01S在Station模式(即接收无线信号( WiFi))下实现STC单片机与手机端网路串口助手相互通信功能

基于ESP8266串口WIFI模块ESP-01S在Station模式(即接收无线信号( WiFi))下实现STC单片机与手机端网路串口助手相互通信功能 ESP8266_01S引脚功能图ESP8266_01S原理图ESP8266_01S尺寸图检验工作1、USB-TTL串口工具(推荐使用搭载CP2102芯片的安信可USB-T1串口)与ESP8266_01S…

基于前馈神经网络的姓氏分类任务(基础)

1、认识前馈神经网络 What is it 图1-1 前馈神经网络结构 人们大多使用多层感知机&#xff08;英语&#xff1a;Multilayer Perceptron&#xff0c;缩写&#xff1a;MLP&#xff09;作为前馈神经网络的代名词&#xff0c;但是除了MLP之外&#xff0c;卷积神经网络&#xff08…

Orangepi Zero2使用外设驱动库wiringOP驱动蜂鸣器

目录 一、安装外设驱动库 1.1 wiringPi外设SDK安装&#xff1a; 二、使用wiringOP库驱动蜂鸣器 2.1 蜂鸣器的硬件连接&#xff1a; 2.2 使用wiringOP库实现蜂鸣器滴滴响&#xff1a; 2.3 设置vim代码显示格式&#xff1a; 一、安装外设驱动库 1.1 wiringPi外设SDK安装&a…

Free Pascal语言基础学习:定义变量、数据类型、循环语句、case语句、条件判断、with语句、运算符

Pascal是一种结构化编程语言&#xff0c;而Free Pascal作为其现代编译器&#xff0c;不仅支持跨多种操作系统和处理器架构&#xff0c;还提供了高效的内存使用和函数重载等先进功能。Free Pascal继承了Pascal语言的核心特性&#xff0c;同时进行了扩展和优化&#xff0c;使其成…

基于Java+MySQL停车场车位管理系统详细设计和实现(源码+LW+调试文档+讲解等)

&#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN作者、博客专家、全栈领域优质创作者&#xff0c;博客之星、平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌&#x1f497; &#x1f31f;文末获取源码数据库&#x1f31f; 感兴趣的可以先收藏起来&#xff0c;…

vrrp与mstp

简单实验步骤

Aiseesoft Screen Recorder v2解锁版安装教程 (屏幕录像工具)

前言 Aiseesoft Screen Recorder 是一个易于使用的屏幕捕捉工具&#xff0c;可以在您的计算机上录制任何视频或音频并以高质量保存它们。这款功能强大的视频屏幕录制应用程序可以帮助您在 Windows 11/10/8/7 上抓取在线电影和广播、捕捉游戏、制作视频教程、录制网络摄像头视频…

java类的加载 ,类加载器以及双亲委派机制详细介绍

1_类的加载 路径 类的加载过程类的加载时机 类的加载 当程序在运行后&#xff0c;第一次使用某个类的时候&#xff0c;会将此类的class文件读取到内存&#xff0c;并将此类的所有信息存储到一个Class对象中 说明&#xff1a;Class对象是指java.lang.Class类的对象&#xff0c…

【2024最新版】图解Mysql数据库配置、命令行及Workbench访问(Windows版本)

目录 1. 准备工作1.1 安装MySQL1.2 验证MySQL的环境变量 2. 环境变量配置3. 访问MySQL3.1 命令行访问MySQL3.2 Workbench访问MySQL 1. 准备工作 1.1 安装MySQL 如果您已经安装了MySQL&#xff0c;请从【2. Mysql 环境配置】开始&#xff1b;如果您没有安装MySQL&#xff0c;请…

【MySQL】数据库的索引机制

文章目录 前言1. 索引是什么2. 索引的优缺点2.1 优点2.2 缺点 3. 索引的操作3.1 创建索引3.2 查看索引3.3 删除索引 4. 索引的存储原理4.1 B树4.2 B树 结语 前言 在数据库中&#xff0c;我们经常使用到的操作就是查询&#xff0c;当数据量小的时候&#xff0c;查询的速度很快&…

COMSOL - 一个点光源是否总能照亮整个房间?

20 世纪 50 年代&#xff0c;数学家恩斯特施特劳斯&#xff08;Ernst Straus&#xff09;提出了一个有趣的问题&#xff1a;在一个侧壁由理想反射镜构成的任意形状的空房间里&#xff0c;一个点光源是否总能照亮整个房间&#xff1f;诺贝尔奖获得者罗杰彭罗斯&#xff08;Roger…

1S013485认证:企业腾飞的引擎

在全球化的今天&#xff0c;企业要想在激烈的市场竞争中立于不败之地&#xff0c;必须不断提升自身的综合实力。1S013485认证正是帮助企业实现这一目标的重要工具。获得这项认证&#xff0c;企业不仅能在市场竞争中占据优势&#xff0c;还能在多个层面实现质的飞跃&#xff0c;…

mac m芯片下安装nacos

背景&#xff1a;最近再研究 下载地址&#xff1a; https://nacos.io/download/nacos-server/ 解压zip包 unzip nacos-server-2.3.2.zip启动 进入到bin目录下 ./startup.sh -m standalone访问可视化界面 账号密码都是nacos&#xff0c;进行登录即可&#xff0c;nacos的端口为…

还在为职场烦恼?六西格玛黑带培训来帮忙!

为什么现在越来越多的学员都直奔六西格玛黑带培训呢&#xff1f;别急&#xff0c;张驰咨询这就来给你们揭秘&#xff01; 首先&#xff0c;六西格玛这个名头可不是盖的&#xff01;它就像是企业管理的“超级英雄”&#xff0c;专门解决各种流程中的“小怪兽”——也就是那些影…

Hadoop3:MapReduce中的Shuffle机制

一、流程图 Shuffle是Map方法之后&#xff0c;Reduce方法之前的数据处理过程称。 二、图解说明 1、数据流向 map方法中context.write(outK, outV);开始&#xff0c;写入环形缓冲区&#xff0c;再进行分区排序&#xff0c;写到磁盘 reduce方法拉取磁盘上的数据&#xff0c;…

Appium+python自动化(二十五)- 那些让人抓耳挠腮、揪头发和掉头发的事 - 获取控件ID(超详解)

简介 在前边的第二十二篇文章里&#xff0c;已经分享了通过获取控件的坐标点来获取点击事件的所需要的点击位置&#xff0c;那么还有没有其他方法来获取控件点击事件所需要的点击位置呢&#xff1f;答案是&#xff1a;Yes&#xff01;因为在不同的大小屏幕的手机上获取控件的坐…