Flask插件wtforms、Flask文件上传和Echarts柱状图

时间:2022-10-27 04:31:39

一、wtforms

类比Django的Form组件
Form组件的主要应用是帮助我们自动生成HTML代码和做一些表单数据的验证

flask的wtforms用法跟Form组件大同小异
参考文章:https://www.cnblogs.com/Zzbj/p/9966753.html

下载安装
pip install wtforms

1、wtforms使用介绍

1. wtforms支持的字段和验证函数
原文:https://blog.csdn.net/wuqing942274053/article/details/72510920

WTForms支持的HTML标准字段

字段类型 说明
StringField 文本字段
TextAreaField 多行文本字段
PasswordField 密码文本字段
HiddenField 隐藏文本字段
DateField 文本字段,值为datetime.date格式
DateTimeField 文本字段,值为datetime.datetime格式
IntegerField 文本字段,值为整数
DecimalField 文本字段,值为decimal.Decimal
FloatField 文本字段,值为浮点数
BooleanField 复选框,值为True和False
RadioField 一组单选框
SelectField 下拉列表
SelectMultipleField 下拉列表,可选择多个值
FileField 文件上传字段
SubmitField 表单提交按钮
FormField 把表单作为字段嵌入另一个表单
FieldList 一组指定类型的字段

WTForms验证函数

验证函数 说明
Email 验证电子邮件地址
EqualTo 比较两个字段的值,常用于要求输入两次密码进行确认的情况
IPAddress 验证IPv4网络地址
Length 验证输入字符串的长度
NumberRange 验证输入的值在数字范围内
Optional 无输入值时跳过其他验证函数
Required 确保字段中有数据
Regexp 使用正则表达式验证输入值
URL 验证URL
AnyOf 确保输入值在可选值列表中
NoneOf 确保输入值不在可选列表中

2. wtforms类的属性和方法
属性:
data
  包含每个字段的数据的字典

errors
  包含每个字段的错误列表的DECT。如果没有验证表单,或者没有错误,则为空。

meta
  这是一个包含各种配置选项以及自定义表单行为的能力的对象。有关可以使用类元选项自定义的内容的更多信息,请参见类元文档。

方法:
validate():通过在每个字段上调用Value来验证表单,将任何额外的Form.Value_<field name>验证器传递给字段验证器。
populate_obj(obj):使用表单字段中的数据填充传递的obj的属性。
__iter__():按创建顺序迭代表单字段。
__contains__(name):如果指定的字段是此表单的成员,则返回True。

例如:

form_obj = wtform(request.form)
if form_obj.validate():
# 包含每个字段的数据的字典
print(form_obj.data)
# 这是一个包含各种配置选项以及自定义表单行为的能力的对象
print(form_obj.meta)
return "注册成功"
# 包含每个字段的错误列表的DECT。如果没有验证表单,或者没有错误,则为空。
print(form_obj.errors)

字段的基类:

label 字段的标签
validators 验证器 -验证的序列时要调用验证被调用
default 如果未提供表单或对象输入,则分配给字段的默认值
widget 如果提供,则覆盖用于呈现字段的窗口小部件
render_kw 设置字段的额外参数,提供一个字典
   
filters 按进程在输入数据上运行的一系列过滤器。
description 字段的描述,通常用于帮助文本
id 用于字段的id。表单设置了合理的默认值,您不需要手动设置。
   
_form 包含此字段的表单。在施工期间,它由表格本身传递。你永远不应该自己传递这个值。
_name 此字段的名称,由封闭表单在构造期间传递。你永远不应该自己传递这个值
_prefix 前缀为此字段的表单名称的前缀,在构造期间由封闭表单传递。
_translations 提供消息翻译的翻译对象。通常在施工期间通过封闭的形式通过。
_meta 如果提供,这是表单中的'meta'实例。你通常不会自己通过

2、基本的使用

1. MyForms.py 定义Form表单的类

from wtforms import Form, widgets
from wtforms.fields import simple, core, html5 # 使用wtforms的类必须要继承它的Form类
# simple:普通字段 core:核心字段 html5:H5新增的字段 class RegisterForm(Form):
username = simple.StringField(
label='用户名',
# 给这个字段添加样式
render_kw={'class': 'form-control'},
# widget插件,可以把这个字段设置成其他type类型
widget=widgets.TextArea()
)
pwd = simple.PasswordField(
label='密码',
# 给这个字段添加样式
render_kw={'class': 'form-control'}
)
re_pwd = simple.PasswordField(
label='确认密码',
# 给这个字段添加样式
render_kw={'class': 'form-control'}, )

2. 视图

from flask import Blueprint, render_template
from FlaskPlug.utils.MyForms import RegisterForm userBlue = Blueprint("userBlue", __name__) @userBlue.route('/register')
def register():
# 实例化form
form_obj = RegisterForm()
return render_template('register.html', form_obj=form_obj)

3. HTML代码

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>注册</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-4 col-md-offset-4">
<h2 style="margin-top: 50px;margin-bottom: 30px;text-align: center">欢迎注册</h2>
<form action="" method="POST" novalidate class="form-horizontal">
{% for field in form_obj %}
<div class="form-group">
{{ field.label }}
{{ field }}
</div>
{% endfor %}
<button type="submit" class="btn btn-success">提交</button>
</form>
</div>
</div>
</div> </body>
</html>

3、验证

3-1、基本验证

步骤
  1. 在Form类中增加验证信息
  2. 在视图中做数据的校验 并且页面展示错误信息

1. MyForms.py 定义Form表单的类

from wtforms import Form, widgets, validators
from wtforms.fields import simple, core, html5 class RegisterForm(Form):
username = simple.StringField(
label='用户名',
# 给这个字段添加样式
render_kw={'class': 'form-control'},
# 可以定义多个校验规则
validators=[
# DataRequired字段必填
validators.DataRequired(message='用户名不能为空'),
# length字段的长度限制
# message:用户填写错误时的错误信息
validators.length(min=2, max=8, message='长度必须在2-8之间')
],
# widget=widgets.TextArea()
)
pwd = simple.PasswordField(
label='密码',
# 给这个字段添加样式
render_kw={'class': 'form-control'},
validators=[
validators.DataRequired(message='密码不能为空'),
validators.length(min=8, max=16, message='长度必须在8-16之间')
],
)
re_pwd = simple.PasswordField(
label='确认密码',
# 给这个字段添加样式
render_kw={'class': 'form-control'},
validators=[
validators.DataRequired(message='密码不能为空'),
validators.length(min=8, max=16, message='长度必须在8-16之间'),
# EqualTo:校验两个字段的值是否相等
validators.EqualTo('pwd', message='两次密码不一致')
],
)
phone = simple.StringField(
label='手机号码',
validators=[
validators.Regexp(regex="^1[3-9][0-9]{9}$",message='手机格式不正确')
] )

2. 视图

from flask import Blueprint, render_template, request
from FlaskPlug.utils.MyForms import RegisterForm
from FlaskPlug.models import User userBlue = Blueprint("userBlue", __name__) @userBlue.route('/register', methods=['GET', 'POST'])
def register():
# 实例化form
form_obj = RegisterForm()
if request.method == "POST":
# 把用户提交上来的数据放入Form表单中实例化
form_obj = RegisterForm(request.form)
# validate方法会去校验用户提交上来的数据
if form_obj.validate():
# 验证通过可以写入数据库,这里演示,不写入
# 验证通过的数据都保存在data这个大字典里面
# username = form_obj.data.get('username')
# password = form_obj.data.get('pwd')
# user_obj = User(username=username, password=password)
# db.session.add(user_obj)
# db.session.commit()
# db.session.close()
return "注册成功"
return render_template('register.html', form_obj=form_obj)

3. HTML代码

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>注册</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-4 col-md-offset-4">
<h2 style="margin-top: 50px;margin-bottom: 30px;text-align: center">欢迎注册</h2>
<form action="" method="POST" novalidate class="form-horizontal">
{% for field in form_obj %}
<div class="form-group">
{{ field.label }}
{{ field }} <span style="color: red">{{ field.errors[0] }}</span>
</div>
{% endfor %}
<button type="submit" class="btn btn-success">提交</button>
</form>
</div>
</div>
</div> </body>
</html>
3-2、自定义校验规则
from wtforms import Form, validators, ValidationError
from wtforms.fields import simple def check_username(form, field):
if len(field.data) < 2:
raise ValidationError('错了,嘿嘿') class TestForm(Form):
username = simple.StringField(
label='用户名',
validators=[check_username, ]
)
3-3、利用钩子函数进行校验
局部钩子函数: validate_字段名,接收两个参数(form, field),后端调用validate()校验函数的时候触发
form: wtforms类的实例
field: 字段对象,可以通过field.data获取前端传过来的该字段的数据,不是字典 全局钩子函数:validate, 接收self参数,self.data代表前端表单传过来的所有数据,是一个字典 from wtforms import Form, validators, ValidationError
from wtforms.fields import simple class TestForm(Form):
username = simple.StringField(
label='用户名',
)
password = simple.PasswordField(
label='密码',
validators=[
validators.DataRequired(message='密码不能为空'),
validators.length(min=8, max=16, message='长度必须在8-16之间')
]
) # 局部钩子函数
def validate_username(form, field):
print(field.data) # 张三
if len(field.data) < 2:
raise ValidationError('用户名至少两位字符') # 全局钩子函数
def validate(self):
print(self.data) # {'username': '张三', 'password': '12345678'}
for key, value in self.data.items():
print(value)

4、拓展字段core/html5

from wtforms import Form, widgets, validators
from wtforms.fields import simple, core, html5 class RegisterForm(Form):
username = simple.StringField(
label='用户名',
# 给这个字段添加样式
render_kw={'class': 'form-control'},
# 可以定义多个校验规则
validators=[
# DataRequired字段必填
validators.DataRequired(message='用户名不能为空'),
# length字段的长度限制
# message:用户填写错误时的错误信息
validators.length(min=2, max=8, message='长度必须在2-8之间')
],
# widget=widgets.TextArea()
)
pwd = simple.PasswordField(
label='密码',
# 给这个字段添加样式
render_kw={'class': 'form-control'},
validators=[
validators.DataRequired(message='密码不能为空'),
validators.length(min=8, max=16, message='长度必须在8-16之间')
],
)
re_pwd = simple.PasswordField(
label='确认密码',
# 给这个字段添加样式
render_kw={'class': 'form-control'},
validators=[
validators.DataRequired(message='密码不能为空'),
validators.length(min=8, max=16, message='长度必须在8-16之间'),
# EqualTo:校验两个字段的值是否相等
validators.EqualTo('pwd', message='两次密码不一致')
],
)
phone = simple.StringField(
label='手机号码',
validators=[
validators.Regexp(regex="^1[3-9][0-9]{9}$",message='手机格式不正确')
] )
# H5新增的标签email
email = html5.EmailField(
label='邮箱',
validators=[
validators.DataRequired(message='邮箱不能为空.'),
],
widget=widgets.TextInput(input_type='email'),
)
# 核心字段core,单选框
gender = core.RadioField(
label='性别',
choices=((1, '男'), (2, '女')),
# 前端传过来的数据是字符串类型,coerce可以把穿过来的数据转换类型
# 因为数据库存的1是int类型,前端选择"男",传过来的是字符串1
coerce=int,
default=1
)
# 单选下拉菜单
city = core.SelectField(
label='城市',
choices=(('sz', '深圳'), ('gz', '广州'), )
)
# 多选下拉菜单
hobby = core.SelectMultipleField(
label='爱好',
choices=(
(1, '美女'),
(2, 'xiong'),
),
)
favor = core.SelectMultipleField(
label='喜好',
choices=(
(1, '篮球'),
(2, '足球'),
),
# 把多选下拉菜单设置成列表
widget=widgets.ListWidget(prefix_label=False),
option_widget=widgets.CheckboxInput(),
coerce=int,
default=[1, 2]
) def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
# 从数据库获取数据 做到实时更新
# self.favor.choices = ORM操作
# 这里演示一下更改
self.favor.choices = ((1, '篮球'), (2, '足球'), (3, '羽毛球'))

5、实现csrf_token

1.Form类

from flask import request
from wtforms import Form
from wtforms.csrf.core import CSRF
from wtforms.fields import simple
from wtforms import validators
from wtforms import widgets
from hashlib import md5 # 自定义CSRF类,重写CSRF的三个方法
class MyCSRF(CSRF):
"""
Generate a CSRF token based on the user's IP. I am probably not very
secure, so don't use me.
""" def setup_form(self, form):
# 获取class Meta里设置的一些值
self.csrf_context = form.meta.csrf_context()
self.csrf_secret = form.meta.csrf_secret
# 调用父类的setup_form方法
return super(MyCSRF, self).setup_form(form) def generate_csrf_token(self, csrf_token):
# 使用md5加密,生成唯一token
gid = self.csrf_secret + self.csrf_context
token = md5(gid.encode('utf-8')).hexdigest()
return token def validate_csrf_token(self, form, field):
# 校验token
print(field.data, field.current_token)
if field.data != field.current_token:
raise ValueError('Invalid CSRF') class RegisterForm(Form):
username = simple.StringField(
label='用户名',
render_kw={'class': 'form-control'},
widget=widgets.TextInput(),
validators=[
validators.DataRequired(message='用户名不能为空'),
validators.length(min=2, max=8, message='长度必须在2-8之间')
]
)
password = simple.PasswordField(
label='密码',
render_kw={'class': 'form-control'},
validators=[
validators.DataRequired(message='密码不能为空'),
validators.length(min=8, max=16, message='长度必须在8-16之间')
]
)
re_pwd = simple.PasswordField(
label='确认密码',
# 给这个字段添加样式
render_kw={'class': 'form-control'},
validators=[
validators.DataRequired(message='密码不能为空'),
validators.length(min=8, max=16, message='长度必须在8-16之间'),
validators.EqualTo('password', message='两次密码不一致')
]
)
phone = simple.StringField(
label='手机号码',
validators=[
validators.Regexp(regex="^1[3-9][0-9]{9}$", message='手机格式不正确')
] ) class Meta:
# -- CSRF
# 是否自动生成CSRF标签
csrf = True
# 生成CSRF标签name
csrf_field_name = 'csrf_token' # 自动生成标签的值,加密用的csrf_secret
csrf_secret = '一库'
# 自动生成标签的值,加密用的csrf_context
csrf_context = lambda x: request.url
# 生成和比较csrf标签
csrf_class = MyCSRF # -- i18n
# 是否支持本地化
# locales = False
locales = ('zh', 'en')
# 是否对本地化进行缓存
cache_translations = True
# 保存本地化缓存信息的字段
translations_cache = {}

2.models

class User(db.Model):
__tablename__ = 'user' id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(32), nullable=False)
password = db.Column(db.String(32), nullable=False)
phone = db.Column(db.String(32))

3.视图

from flask import request, views, session, render_template
from flask_demo.home.models.home_model import *
from flask_demo import db
from flask_demo.home.forms.home_forms import RegisterForm class RegisterView(views.MethodView):
def get(self):
form_obj = RegisterForm()
return render_template('register.html', form_obj=form_obj) def post(self):
form_obj = RegisterForm(request.form)
if form_obj.validate():
user_dict = {}
user_dict['username'] = form_obj.data.get('username')
user_dict['password'] = form_obj.data.get('password')
user_dict['phone'] = form_obj.data.get('phone')
user_obj = User(**user_dict)
db.session.add(user_obj)
db.session.commit()
db.session.close()
return "注册成功"
return render_template('register.html', form_obj=form_obj)

4.HTML代码

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>注册</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-4 col-md-offset-4">
<h2 style="margin-top: 50px;margin-bottom: 30px;text-align: center">欢迎注册</h2>
<form action="" method="POST" novalidate class="form-horizontal">
{{ form_obj.csrf_token }}
<div class="form-group">
{{ form_obj.username.label }}
{{ form_obj.username }}
<span style="color: red">{{ form_obj.username.errors[0] }}</span>
</div>
<div class="form-group">
{{ form_obj.password.label }}
{{ form_obj.password }}
<span style="color: red">{{ form_obj.password.errors[0] }}</span>
</div>
<div class="form-group">
{{ form_obj.re_pwd.label }}
{{ form_obj.re_pwd }}
<span style="color: red">{{ form_obj.re_pwd.errors[0] }}</span>
</div>
<div class="form-group">
{{ form_obj.phone.label }}
{{ form_obj.phone }}
<span style="color: red">{{ form_obj.phone.errors[0] }}</span>
</div>
<button type="submit" class="btn btn-success">提交</button>
</form>
</div>
</div>
</div>
</body>
</html>

二、基于Flask的文件上传Demo

"""
文件上传完后,进行代码的统计
app.config.root_path: 项目的根路径
os.walk:
遍历你给的路径下的所有文件(会递归遍历)
每次循环的根文件夹的路径,文件夹的名字组成的列表,和文件组成的列表
dirpath, dirnames, filenames
zipfile: 压缩解压文件的模块
shutil: 也是压缩解压文件的模块,还能移动啥的
""" from flask import Blueprint, request, render_template
from flask import current_app as app
import shutil
from uploadCode.models import CodeRecord
from uploadCode import db
import os
import time uploadBlue = Blueprint('uploadBlue', __name__) # zip包上传
@uploadBlue.route('/upload', methods=['GET', 'POST'])
def upload():
if request.method == "GET":
return render_template("upload.html", error="")
# 先获取前端传过来的文件
file = request.files.get("zip_file")
# 判断是否是zip包
zip_file_type = file.filename.rsplit(".", 1)
if zip_file_type[-1] != "zip":
return render_template("upload.html", error="文件必须是zip包")
# 解压路径
upload_path = os.path.join(app.config.root_path, "files", zip_file_type[0]+str(time.time()))
print(upload_path)
# 解压前端传过来的文件file到upload_path这个路径
shutil._unpack_zipfile(file, upload_path)
# 遍历保存的文件夹得到所有.py文件
file_list = []
for (dirpath, dirnames, filenames) in os.walk(upload_path):
for filename in filenames:
file_type = filename.rsplit(".", 1)
if file_type[-1] != "py":
continue
file_path = os.path.join(dirpath, filename)
file_list.append(file_path)
# 打开每个文件读取行数
sum_num = 0
for path in file_list:
with open(path, mode="rb") as f:
for line in f:
if line.strip().startswith(b"#"):
continue
sum_num += 1
# 得到总行数去保存数据库
return str(sum_num)

1. upload.py

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="" method="post" enctype="multipart/form-data">
请上传你的代码: <input type="file" name="zip_file">
<button type="submit">提交</button>
{{error}}
</form> </body>
</html>

2. HTML代码

三、柱状图

参考文档Echarts:http://echarts.baidu.com/

1、使用Echarts步骤

1. 下载它需要的依赖包
2. 点击某个Demo-->Download
3. 参考着Demo去实现你的需求

2、注意

从后端传数据到前端的时候,因为展示的柱状图需要后端的数据,
而如果把数据直接在JS中使用,会出现一些问题,
因此,我们可以使用一个标签,给这个标签设置属性从而获取从后端传过来的数据,
然后在JS中获取这个标签的属性值,就可以拿到后端的数据了,
但是从属性获取的数据已经被转化成字符串了,我们这时可以使用eval()方法,
把数据类型重新转换回来。

3、Demo

from flask import Blueprint, request, render_template
from uploadCode.models import CodeRecord
from uploadCode import db # 柱状图
@uploadBlue.route("/")
def index():
# 展示用户提交代码柱状图
queryset = db.session.query(CodeRecord).all()
date_list = []
num_list = []
for obj in queryset:
date_list.append(str(obj.upload_date))
num_list.append(obj.code_nums)
return render_template("index.html", date_list=date_list, num_list=num_list)

1. Demo.py

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="/static/echarts.common.min.js"></script>
</head>
<body>
<div id="container" style="height: 400px"></div>
<!--用一个标签获取从后端传过来的数据-->
<div id="info" date_list="{{date_list}}" num_list="{{num_list}}"></div>
<script>
var dom = document.getElementById("container");
var myChart = echarts.init(dom);
var app = {};
let infoEle = document.getElementById("info");
let date_list = infoEle.getAttribute("date_list");
let num_list = infoEle.getAttribute("num_list");
option = null;
app.title = '坐标轴刻度与标签对齐'; option = {
color: ['#3398DB'],
tooltip : {
trigger: 'axis',
axisPointer : { // 坐标轴指示器,坐标轴触发有效
type : 'shadow' // 默认为直线,可选为:'line' | 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis : [
{
type : 'category',
data : eval(date_list),
axisTick: {
alignWithLabel: true
}
}
],
yAxis : [
{
type : 'value'
}
],
series : [
{
name:'直接访问',
type:'bar',
barWidth: '60%',
data: eval(num_list) // 重新计算一下这个字符串,转成原本的数据类型
}
]
};
;
if (option && typeof option === "object") {
myChart.setOption(option, true);
}
</script>
</body>
</html>

2. HTML代码(根据官方Demo修改一下)