编辑
2022-11-11
🧰语言-python
00
请注意,本文编写于 951 天前,最后修改于 225 天前,其中某些信息可能已经过时。

目录

1. 使用Flask-WTF处理表单
1.1定义WTForms表单类
1.2 输出HTML代码
1.3 在模板中渲染表单
2.处理表单数据
1.提交表单
2.验证表单数据
2.1客户端验证
2.2服务器验证
2.3WTForms验证机制
2.4 在视图函数中验证表单
3.在模板中渲染错误消息
3.表单进阶实践
3.1设置错误消息语言
3.2 使用宏渲染表单
3.3 自定义验证器
3.4文件上传

1. 使用Flask-WTF处理表单

flask-wtf将表单数据解析、CSRF保护、文件上传等功能与Flask集成,另外还附加了reCAPTCHA(是google开发的免费验证码服务,在国内目前无法直接使用)支持。

默认情况下,Flask-WTF使用程序密钥来对CSRF令牌进行签名,所以我们需要为程序设置密钥:

app.secret_key = 'secret string'

1.1定义WTForms表单类

当使用WTForms创建表单时,表单由Python类表示,这个类继承从WTForms导入的Form基类。

一个表单由若干个输入字段组成,这些字典分别用表单类的类属性来表示。例如:

from wtforms import Form, StringField, PasswordField, BooleanField, SubmitField from wtforms.validators import DataRequired, Length class LoginForm(From): 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>标签的属性,比如传入{'placeholder':'Your name'},渲染后的HTML代码会将<input>标签的placeholder属性设为Your name
validators一个列表,包含一些列验证其,会在表单提交后被逐一调用验证表单数据
default字符串或可调用对象,用来为表单字段设置默认值

在WTforms种,验证器是一系列用于验证字段数据的类,我们在实例化字段类时使用validators关键字来指定附加的验证其列表。验证器从wtforms.validators模块中导入,常用的验证其如下所示:

验证器说明
DataRequired(message=None)验证数据是否有效
Email(message=None)验证Email地址
EqualTo(filename,message=None)验证两个字段是否相同
InputRequired(message=None)验证是否有数据
Length(min=-1,max=-1,message=None)验证输入值长度是否在给定范围内
NumberRange(min=None,max=None,message=None)验证数字是否在给定的范围内
Optional(strip_whitespace=True)允许输入值为空,并跳过其他验证
Regexp(regex,flags=0,message=None)使用正则表达式验证输入值
URL(require_tld=True,message=None)验证URL
Anyof(values,message=None,values_formatter=None)确保输入值在可选值列表中
Noneof(values,message=None,values_formatter=None)确保输入值不在可选值列表中

在实例化验证类时,message参数用来传入自定义错误消息,如果没有设置则使用内置的英文错误消息。

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

1.2 输出HTML代码

>>> form=LoginForm() >>> form.username() u'<input id="username" name="username" type="text" value="">'

字段的<label>元素的HTML代码则可以通过“form.字段名.label”的形式获取:

>>> form.username.label() u'<label for="username">username</label>'

在创建HTML表单时,我们经常会需要使用HTML<input>元素的其他属性来对字段进行设置。比如:

  • 添加class属性设置对应的css类为字段添加样式;
  • 添加placeholder属性设置占位文本

使用render_kw属性

username = StringField('Username',render_kw={'placeholder':'Yourname'})

在调用字段时传入(在html中)

form.username(style='width:200px;',class_='bar')

1.3 在模板中渲染表单

传入表单类实例

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

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

html
<form method='post'> {{ form.csrf_token }} {{ form.username.label }}{{ form.username(class='form-control') }}<br> {{ form.password.label }}{{ form.password(class='form-control') }}<br> {{ form.remember(class='form-check-input') }}{{ form.remember.label }}<br> {{ form.submit(class='btn btn-primary') }}<br> </form>

2.处理表单数据

从获取数据到保存数据大致会经历以下步骤:

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

1.提交表单

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

属性默认值说明
action当前URl,即页面对应的URL表单提交时发送请求的目标URL
methodget提交表单的HTTP请求方法,目前仅支持使用GET和POST方法
enctypeapplication/x-www-form-urlencoded表单数据的编码类型,当表单中包含文件上传字段时,需要设为multipart/form-data,还可设为纯文本类型text/plain

2.验证表单数据

2.1客户端验证

客户端验证是指在客户端对用户的输入值进行验证,比如,使用html5内置的验证属性即可实现基本的客户端验证(type、required、min、max、accept等)比如:

<input type='text' name='username' required>

同样,我们可以通过render_kw传入这些属性

{{ form.username(required='') }}

如果不想手动写,也可以使用各种Javascript表单验证库。

2.2服务器验证

服务端验证是指用户把输入的数据提交到服务器段,在服务器段对数据进行验证。

如果验证出错,就在返回的响应中加入错误的信息。

用户修改后再次提交表单,直到通过验证。

2.3WTForms验证机制

WTForms验证表单字段的方式是在实例化表单类时传入表单数据,然后对表单实例调用validate()方法。

from wtforms import Form, StringField, PasswordField, BooleanField, SubmitField from wtforms.validators import DataRequired, Length class LoginForm(From): username =StringField('Username',validators=[DataRequired()]) password = PasswordField('Password',validators=[DataRequired(),Length(8,128)]) remember = BooleanField('Remember me') submit = SubmitField('Log in')

2.4 在视图函数中验证表单

@app.route('/basic') def basic(): form = LoginForm() if request.method == 'POST' and form.validate():(等价于 if form.validate_on_submit()) …… 处理Post请求 username=form.username.data return render_template('forms/basic.html',form=form) #处理Get请求

除了POST方法,如果请求的方法是PUT、PATCH和DELTE,form.validate_on_submit()也会验证表单数据

3.在模板中渲染错误消息

<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.username.erros %} <small class="error">{{message}}</small><br> {% endfor %} {{ form.remember }}{{ form.remember.label }}<br> {{ form.submit }} </form>

3.表单进阶实践

3.1设置错误消息语言

示例程序创建了一个MyBaseForm基类,所有继承这个基类的表单类的内置错误消息语言都会设为简体中文。

from flask_wtf import FlaskForm app = Flask(__name__) app.config['WTF_I18N_ENABLEd'] = False class MyBaseForm(FlaskForm): class Meta: locales = ['zh'] class HelloForm(MyBaseForm): name = StringField('Name',validators = [DataRequired()]) submit = SubmitField()

首先,我们需要将配置变量WTF_I18N_ENABLED 设为False,这会让Flask-WTF使用WTForms内置的错误消息翻译。

然后我们需要在自定义基类中定义Meta类,并在locales列表中加入简体中文的地区字符串。

在创建表单时,继承这个MyBaseForm即可将错误消息语言设为中文。

另外,也有第二种方法:

form = MyForm(meta={'locales':['en_US','en']})

3.2 使用宏渲染表单

在模板中渲染表单时,我们由大量的工作要做:

  • 调用字段属性,获取<input>定义
  • 调用对应的label属性,获取<label>定义
  • 渲染错误消息
{% macro form_field(field) %} {{ field.label }}<br> {{ field(**kwargs) }}<br> {% if field.erros %} {% for error in field.errors%} {% endmacro %}

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

{% from 'macros.html' import form_field %} …… <form method='post'> {{ form.csrf_token }} {{ form_field(form.username) }}<br> {{ form_field(form.password) }}<br> </form>

3.3 自定义验证器

行内验证器

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

当表单类中包含以"validate_字段属性名"形式命名的方法时,在验证字段数据时会同时调用这个方法来验证对应的字段,这也是为什么表单类的字段属性名不能以validate开头。

验证方法接受2个位置参数,依次为form和field,前者为表单类实例,后者是字段对象,我们可以通过field.data获取字段数据,这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()

换成工厂函数

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 = IntergerField('The Number ',validators=[is_42()]) submit = SubmitField()

3.4文件上传

在HTML中,渲染一个文件上传字段只需要将<input>标签的type属性设为file。

定义上传表单

from flask_wtf.file import FileField, FileRequired,FileAllowed class UploadForm(FlaskFrom): photo = FileField('upload Image',validators=FileRequired(),fileAllowed(['jpg','jpeg','png','gif'])) submit = SubmitField()

Flask_WTF 提供的上传文件验证器

验证器说明
FileRequired(message=None)验证是否包含文件对象
FileAllowed(upload_set,message=None)用来验证文件类型,upload_set参数用来传入包含允许的文件后缀名列表

渲染上传表单

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

唯一需要注意的是,当表单包含文件上传字段时,需要将表单的enctype属性设为“multipart/form-data”,

这会告诉浏览器将上传数据发送到服务器,否则仅会把文件名作为表单数据提交。

处理上传文件

import os 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('/upoad',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)) session['filenames'] = [filename] return redirect(url_for('show_images')) return render_template('upload.html',form=form)

secure_filename()函数对文件名进行过滤,传递文件名作为参数,它会过滤掉所有危险字符,返回“安全的文件名”,会去除乱码和中文相关的内容。

这里将filename作为列表传入session知识为了兼容下面的多文件上传示例,这两个视图使用同一个模板,使用session可以在模板中统一从session获取文件名列表。

在uploaded.html模板里,我们将传入的文件名作为URL变量,通过上面的get_file视图获取文件URL,作为<img>标签的src属性值,如下所示:

<img src="{{url_for('get_file',filename=filename)}}"> @app.route('/uploads/<path:filename>') def get_file(filename): return send_from_drectory(app.config['UPLOAD_PATH'],filename)

使用Flask提供的send_from_directory()函数来获取文件,传入文件的路径和文件名作为参数。

多文件上传

在Html中这样表示

<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视图中,我们迭代这个列表,然后逐一对文件进行处理:

from flask import request, session, flash, redirect, url_for from flask_wtf.csrf import validate_csrf from wtforms import ValidationError @app.route('/multi-upload',method=['GET','POST']) def multi_upload(): form = MultiUploadForm() if reques.method == 'POST': filenames = [] # 验证CSRF令牌 try: validate_csrf(form.csrf_token.data) except ValidationError: return redirect(url_for('multi_upload')) # 检查文件是否存在 if 'photo' not in request.files: return redirect(url_for('multi_upload')) for f in request.files.getlist('photo'): # 检查文件类型 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: return redirect(url_for('multi_upload')) session['filenames'] = filenames return redirect(url_for('show_images')) return render_template('upload.html',form=form)

在请求方法为POST时,我们对上传数据进行手动验证,主要包含下面几步:

  • 手动调用 flask_wtf.csrf.validate_csrf验证CSRF令牌,传入表单中csrf_token隐藏字段的值。如果抛出wtforms.ValidationError异常则表明验证未通过。
  • 其中if 'photo'not in reqest.files用来确保字段中包含文件数据,如果用户没有选择文件就提交表单则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']

在新版本的Flask-WTF发布后,你就可以使用和单文件上传相同的方式处理表单。

比如,我们可以使用FLask-WTF提供的MultipleFileField来创建提供Flask支持的多文件上传字段,使用相应的验证器对文件进行验证。

在视图函数中,我们则可以继续使用form.validate_on_submit()来验证表单,并通过form.photo.data来获取字段的数据——包含所有上传文件对象的列表。

多文件上传处理通常会使用JavaScript库在客户端进行预验证,并添加进度条来优化用户体验

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

CKEditor是一个开源的富文本编辑器,它包含丰富的配置选项,而且有大量第三方插件支持。

扩展flask-CKEditor简化了在FLask程序中使用CKEditor的过程。 实例化flask-ckeditor提供的CKEditor类,传入程序实例:

from flask_ckeditor import CKEditor ckeditor = CKEditor(app)

1.配置富文本编辑器

flask-ckeditor常用配置

配置键默认值说明
CKEDITOR_SERVER_LOCALFalse设为True会使用内置的本地资源
CKEDITOR_PKG_TYPE'standard'CkEditor包类型,可选值为basic、standard和full
CKEDITOR_LANGUAGE''界面语言,传入ISO 639格式的语言码
CKEDITOR_HEIGHT''编辑器高度
CKEDITOR_WIDTH''编辑器宽度

使用内置的本地资源:

app.config['CKEDITOR_SERVER_LOCAL'] = True

2.渲染富文本编辑器

富文本编辑器在HTML中通过文本区域字段表示,即<textarea></textarea>。flask-ckeditor通过包装WTFforms提供的TextAreaField字段类型实现了一个CKEditor字段类,我们使用它来创建富文本编辑框字段。 文章表单:

from flask_wtf import FlaskForm from wtforms import StringField,SubmitField from wtforms.validators import DataRequired, Length from flask_ckeditor import CKEditorField class RichTextForm(FlaskForm): title = StringField('Title',validators=[DataRequired(),Length(1,50)]) body = CKEditorField('Body',validators=[DataRequired()]) submit = SubmitField('Publish')

文章正文字段使用的CkEditorField字段类型从flask-ckeditor导入。

我们可以像其他字段一样定义标签、验证器和默认值。

在使用时上,这个字段和WTForms内置的其他字段完全相同。比如,在提交表单时,同样使用data属性获取数据。

渲染的HTML

{% extends base.html %} {% from 'macros.html' import form_field %} {% block content %} <h1> Integrate CKEditor with FLask-CKEditor</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_SERVER_LOCAL设为True会使用扩展内置的本地资源,内置的本地资源包含了几个常用的插件和语言包。

ckeditor.load()方法支持通过pkg_type参数传入包类型;

{{ ckeditor.config(name='body') }}

单个表单多个提交按钮

包含两个提交按钮的表单:

class NewPostForm(FlaskForm): title = StringField('Title',validators = [DataRequired(),Length(1,50)]) body = TextAreaField('Body',validators = [DataRequired()]) save = SubmitField('Save') # 保存按钮 publish = SubmitField('Publish') # 发布按钮

判断被单机的提交按钮

@app.route('/two-submits',methods=['GET','POST']) def twp_submits(): form = NewPostForm() if form.validate_on_submit(): # 保存按钮被单击 if form.save.data: flash('You click the "save" button') elif form.publish.data: # 发布按钮被单击 flash('You click the "publish"button') return redirect(url_for('index')) return render_template('2submit.html',form=form)

单个页面多个表单

除了在单个表单实现多个提交按钮,有时我们还需要在单个页面上创建多个表单。

比如,在程序的主页上同时添加登录和注册表单。

当在同一个页面上添加多个表单时,我们要解决的一个问题就是在视图函数中判断当前被提交的是哪个表单。 1.单个视图的处理

2个表单设置不同的提交字段名称

class SignForm(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(),Lenght(1,20)]) email = StringFIeld('Email',validators = [DataRequired(),Length(1,254)]) password = PasswordField('Password',validators=[DataRequired(),Length(8,128)]) submit2 = SubmitField('Register ')

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

@app.route('/multi-form',methods=['GET','POSt']) def multi_form(): singin_form = SigninForm() register_form = RegisterForm() if signin_form.submit1.data and signin_form.validate(): username = signin_form.username.data return redirect(url_for('index')) if register_form.submit2.data and register_form.validate(): username = register_form.username.data return redirect(url_for('index')) return render_template('2form.html',signin_form=signin_form,register_form=register_form)

在视图函数中,我们为2个表单添加了各自的if判断,在这连个if语句的内部,我们分别执行各自的代码逻辑。

以登录表单的if判断为例,如果signin_form.submit1.data的值为True,那就说明用户提交了登录表单,这时我们手动调用signin_form.validate()对这个表单进行验证。

多视图处理

除了通过提交按钮判断,更简洁的方法是通过分离表单的渲染和验证实现。

这时表单的提交字段可以使用同一个名称,在视图函数中处理表单时也只需使用我们熟悉的form.validate_on_submit()方法。

GET请求统一,POST设置2个接口

本文作者:Eric

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!