Flask插件wtforms、Flask文件上传和Echarts柱状图
一、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验证函数
验证函数 | 说明 |
---|---|
验证电子邮件地址 | |
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)
<!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>
三、柱状图
参考文档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)
<!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>