Flask-WTF
1.什么是表单处理?
表单是允许用户跟你的web应用交互的基本元素。比如用户的注册、登录等,request对象包含客户端请求的信息(eg:request.remote_addr,用户的IP地址);request.form可以访问用户POST方法提交的表单数据。
2.什么是Flask-WTF?
Flask自己不会帮你处理表单,但Flask-WTF插件允许用户在Flask应用中使用脍炙人口的WTForms 包,这个包使得定义表单和处理表单功能变得轻松。
3.优势
- 在内部已经为我们等装好了前端的form表单
已经做好了校验工作
4.功能集成wtforms
- 带有csrf令牌的安全表单
- 全局csrf保护
- 支持验证码
- 与Flask-Uploads一起支持文件上传
- 国际化集成
安装Flask-WTF
pip install Flask-WTF
使用Flask-WTF
一个关于用户登录、注册、上传文件、登录日志的简易系统
用户登录:包括用户名,用户密码,验证码(验证);
用户注册:用户名,用户密码,用户邮箱,用户号码(与数据库进行交互);
上传文件:允许上传txt、pdf格式的文件,将上传的文件保存到服务器
登录日志:用户登录记录入数据库中的日志中
生成表单
#导入包
from flask_wtf import FlaskForm
from flask_wtf.file import FileRequired, FileAllowed
from wtforms import StringField, PasswordField, SubmitField, FileField
from wtforms.validators import DataRequired, Length, Email, Regexp, EqualTo,ValidationError
from models import User,Userlog
#定义一个用户登录的表单,继承导入的FlaskFrom
class LoginForm(FlaskForm):
#用户名:{
# StringField:字符串类型,
# label:输入信息表框标签,
#
# }
name=StringField(
label="用户名",
#表单的要求
validators=[
DataRequired("请输入用户名"), #表单要求不能为空,否则报错“请输入用户名”
Length(3,20), #表单允许输入字符串长度的范围为3~20
],
#租用前端html代码
render_kw={
'placeholder':"用输入用户名",
"class":"form-control"
}
)
#用户密码,PasswordField密码类,输入时加密
pwd=PasswordField(
label="用户密码",
validators=[
DataRequired("请输入密码!")
],
render_kw={
'placeholder': "用输入用户名",
"class": "form-control"
}
)
#验证码
verify_code=StringField(
label="验证码",
validators=[
DataRequired(),
]
)
#提交按钮
submit=SubmitField(
render_kw={
'value':"登录",
'class':'btn btn-success pull-right'
}
)
#定义一个用户注册的表单
class RegisterForm(FlaskForm):
name = StringField(
label="用户名",
# 表单的要求
validators=[
DataRequired("请输入用户名"), # 表单要求不能为空,否则报错“请输入用户名”
Length(3, 20), # 表单允许输入字符串长度的范围为3~20
],
# 租用前端html代码
render_kw={
'placeholder': "用输入用户名",
"class": "form-control"
}
)
#邮箱
email=StringField(
label="邮箱",
validators=[
DataRequired("请输入邮箱"),
#Email直接导入,用来检测邮箱格式
Email("邮箱格式不正确!")
],
render_kw={
'placeholder': "用输入邮箱",
}
)
#手机号码
phone=StringField(
label="电话号码",
validators=[
DataRequired("请输入电话"),
#Regexp匹配正则表达式(模板) message报错信息
Regexp('1\d{10}',message="手机号码格式不正确")
]
)
# 用户密码,PasswordField密码类,输入时加密
pwd = PasswordField(
label="用户密码",
validators=[
DataRequired("请输入密码!")
],
render_kw={
'placeholder': "用输入用户名",
"class": "form-control"
}
)
#确认密码
repwd=PasswordField(
label="确认密码",
validators=[
DataRequired("请再次输入密码"),
#确认是否与'pwd'相等
EqualTo('pwd',message="两次密码不一致")
],
render_kw={
'placeholder': "请再次输入密码",
"class": "form-control"
}
)
#注册按钮
submit = SubmitField(
render_kw={
'value': "注册",
'class': 'btn btn-success pull-right'
}
)
#自定义检查规则,表单类里面定义的validate_字段名开头的方法,
#Flask会在检查字段时对该函数一起进行调用
#判断要注册的用户是否已经在数据库中存在
def validate_name(self,field):
# field:<input id="name" name="name"
# placeholder="请输入用户名"
# required type="text" value="root">
name=field.data #表单里填的内容
#查询数据库中该用户名的数量,若为1则已经存在,若为0则不存在
count=User.query.filter_by(name=name).count()
if count==1:
raise ValidationError("昵称%s已经存在" %(name))
#判断要注册的邮箱是否已经在数据库中存在
def validate_email(self,field):
email=field.data #表单里填的内容
#查询数据库中该邮箱的数量,若为1则已经存在,若为0则不存在
count=User.query.filter_by(email=email).count()
if count==1:
raise ValidationError("邮箱%s已经存在" %(email))
#判断要注册的邮箱是否已经在数据库中存在
def validate_phone(self,field):
phone=field.data #表单里填的内容
#查询数据库中该手机号的数量,若为1则已经存在,若为0则不存在
count=User.query.filter_by(phone=phone).count()
if count==1:
raise ValidationError("电话号码%s已经存在" %(phone))
#上传的文件表单
class UploadForm(FlaskForm):
file=FileField(
label="简历",
validators=[
#文件必须选择
FileRequired(),
FileAllowed(['pdf','txt'])
]
)
submit=SubmitField(
render_kw={
'value':"上传",
'class':'btn btn-success pull-right'
}
)
使用表单
开始浏览器的请求为GET方法,经过路由找到视图函数,视图函数将定义的表单实例化,并且将空表单传递给前端代码,在前端页面显示表单。用户将信息填入表单,点击提交,浏览器的POST请求将用户填的信息经过路由提交到视图函数,再由视图函数与数据库进行交互,将信息保存到数据库。
数据库模板models.py
生成用户信息数据表和用户登录日志数据表,用来储存用户信息和用户登录记录。
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_bootstrap import Bootstrap
from datetime import datetime
app=Flask(__name__)
#配置数据库
app.config['SQLALCHEMY_DATABASE_URI']="mysql+pymysql://root:@127.0.0.1/Todoproject"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS']=True
app.config['SECRET_KEY']='westos'
Bootstrap(app)
#实例化对象
db=SQLAlchemy(app)
#定义一个用户的类数据表
class User(db.Model):
#用户id为主健且自增长
id=db.Column(db.Integer,autoincrement=True,primary_key=True) #用户编号
name=db.Column(db.String(50),unique=True) #用户名
pwd=db.Column(db.String(100)) #用户密码
email=db.Column(db.String(20),unique=True)
phone=db.Column(db.String(20),unique=True)
info=db.Column(db.Text) #个性简介
addtime=db.Column(db.DateTime,default=datetime.now()) #用户注册时间
userlogs=db.relationship('Userlog',backref="user") #外键关联用户日志
class Userlog(db.Model):
"""用户登录日志数据表"""
id=db.Column(db.Integer,autoincrement=True,primary_key=True)
user_id=db.Column(db.Integer,db.ForeignKey('user.id'))
ip=db.Column(db.String(100)) #登录的ip
addtime=db.Column(db.DateTime,default=datetime.now())
area=db.Column(db.String(100)) #登录的地区
if __name__=='__main__':
#生成数据表
db.create_all()
生成登录验证码
import random
import string
from PIL import Image, ImageDraw, ImageFont, ImageFilter
# **********************1. 可以单独为一个文件config.py文件 ***********************************
# 字体的位置,不同版本的系统会有不同
#在终端使用locate *.ttf 命令,查看字体位置
font_path ='/home/kiosk/Desktop/Python/WebStorm-172.3544.34/jre64/lib/fonts/DroidSerif-Regular.ttf'
# font_path = '/opt/WebStorm-172.3544.34/jre64/lib/fonts/DroidSansMonoSlashed.ttf'
# 生成几位数的验证码
number = 4
# 生成验证码图片的高度和宽度
size = (100, 30)
# 背景颜色,默认为白色
bgcolor = (255,250,240)
# 字体颜色,默认为蓝色
fontcolor = (190,190,190)
# 干扰线颜色。默认为红色
linecolor = (255, 0, 0)
# 是否要加入干扰线
draw_line = True
# 加入干扰线条数的上下限
line_number = (1, 5)
# **************************2. 详细信息 *********************************************************
# 用来随机生成一个字符串
def gene_text():
source = string.ascii_letters
return ''.join(random.sample(source, number)) # number是生成验证码的位数
# 用来绘制干扰线
def gene_line(draw, width, height):
begin = (random.randint(0, width), random.randint(0, height))
end = (random.randint(0, width), random.randint(0, height))
draw.line([begin, end], fill=linecolor)
# 生成验证码
def gene_code():
width, height = size # 宽和高
image = Image.new('RGBA', (width, height), bgcolor) # 创建图片
font = ImageFont.truetype(font_path, 25) # 验证码的字体
draw = ImageDraw.Draw(image) # 创建画笔
text = gene_text() # 生成字符串
font_width, font_height = font.getsize(text)
draw.text(((width - font_width) / number, (height - font_height) / number), text,
font=font, fill=fontcolor) # 填充字符串
if draw_line:
gene_line(draw, width, height)
# image = image.transform((width+30,height+10), Image.AFFINE, (1,-0.3,0,-0.1,1,0),Image.BILINEAR) #创建扭曲
# image = image.transform((width + 20, height + 10), Image.AFFINE, (1, -0.3, 0, -0.1, 1, 0), Image.BILINEAR) # 创建扭曲
image = image.filter(ImageFilter.EDGE_ENHANCE_MORE) # 滤镜,边界加强
image.save('idencode1.png') # 保存验证码图片
return image, text
if __name__ == "__main__":
gene_code()
视图函数views.py
import json
from io import BytesIO
from urllib.request import urlopen
from flask import Flask, redirect, render_template, request, session, flash, url_for, make_response
#由于在models中已经配置好app,所以直接导入
from werkzeug.security import check_password_hash,generate_password_hash
from models import app,db,User,Userlog
from forms import *
#通过ip获取该IP的所在城市和国家;
def get_ip_area(ip):
#构造url地址,使用淘宝的API接口
url = 'http://ip.taobao.com/service/getIpInfo.php?ip=%s' % (ip)
#获取页面返回的内容
json_data=urlopen(url).read().decode('utf-8')
#将json格式的数据转换为字典格式
s_data=json.loads(json_data)
country=s_data['data']['country']
if country=='XX':
country=''
city=s_data['data']['city']
if city=='XX':
city=''
return country+city
@app.route('/login/',methods=['POST','GET'])
def login():
#实例化定义的登录表单
form=LoginForm()
#判断用户的HTTP请求方法,如果为GET方法则返回前端
# 登录界面,如果为POST方法则获取提交的数据
if request.method=='POST':
data=form.data
name=form.name.data
pwd=form.pwd.data
#验证码获取
code=data['verify_code']
u=User.query.filter_by(name=name).first()
#判断用户是否存在
#check_password_hash((u.pwd,pwd)检查用户输入的密码
#是否等于数据库中加密的密码。
if u and check_password_hash(u.pwd,pwd):
if code==session['code']:
#将登录用户的用户名和id添加到会话中
session['user']=u.name
session['user_id']=u.id
#将登录信息写入登录日志
userlog=Userlog(
user_id=u.id,
ip=request.remote_addr,
area=get_ip_area(request.remote_addr)
)
db.session.add(userlog)
db.session.commit()
return "登录成功"
else:
flash("验证码错误",'error')
return redirect('/login/')
else:
#flash消息闪现,前端页面通过get_flash_messages()
#获取消息闪现,还可以分别闪现消息,消息后跟‘ok’和
#‘error’表示成功信息和报错信息
flash('用户名或密码不正确!','error')
#如果报错则返回登录界面
return redirect(url_for('login'))
return render_template('login.html',form=form)
#用户注册
@app.route('/register/',methods=['POST','GET'])
def register():
form=RegisterForm()
#form.validate_on_submit()会自动判断是否是一个POST请求
if form.validate_on_submit():
#获取表单提交的数据;(字典数据类型)
data=form.data
u=User(
name=data['name'],
#generate_password_hash对用户密码进行加密
pwd=generate_password_hash(data['pwd']),
email=data['email'],
phone=data['phone']
)
db.session.add(u)
db.session.commit()
flash('注册成功','ok')
return redirect('/register/')
return render_template('register.html',form=form)
@app.route('/upload/',methods=['POST','GET'])
def upload():
form=UploadForm()
if form.validate_on_submit():
filename=form.file.data.filename
#将上传的文件保存到服务器
form.file.data.save('/home/kiosk/Desktop/WTF练习/static/interview' +filename)
flash("上传文件成功",'ok')
return redirect('/upload/')
return render_template('upload.html',form=form)
#将验证码传给前端
@app.route('/code/')
def get_code():
#1.生成验证码
from static.doc.get_code import gene_code
image,code=gene_code()
#2.将验证码图片以二进制的方式写在内存中
buf=BytesIO()
image.save(buf,'png')
bug_str=buf.getvalue()
#3.将数据响应给前端页面
response=make_response(bug_str)
response.headers['Content-Type']='image/png'
#将验证码内容写入session中
session['code']=code
return response
if __name__=='__main__':
app.run(port='9000')
用户登录界面
login.html
{% extends 'bootstrap/base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block content %}
<div class="col-md-8 col-lg-offset-2">
{# 接受闪现消息 如果是报错信息则显示红色;成功则显示绿色#}
{% for msg in get_flashed_messages(category_filter='ok') %}
<div class="alert alert-success alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span>
</button>
<strong>Success!</strong> {{ msg }}.
</div>
{% endfor %}
{% for msg in get_flashed_messages(category_filter='error') %}
<div class="alert alert-warning alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span>
</button>
<strong>Warning!</strong> {{ msg }}.
</div>
{% endfor %}
{{ wtf.quick_form(form) }}
<img src="/code/" onclick="this.src='/code/'">
</div>
{% endblock %}
用户注册界面 register.html
{% extends 'bootstrap/base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block content %}
<div class="col-md-8 col-lg-offset-2">
{# 接受闪现消息 如果是报错信息则显示红色;成功则显示绿色#}
{% for msg in get_flashed_messages(category_filter='ok') %}
<div class="alert alert-success alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span>
</button>
<strong>Success!</strong> {{ msg }}.
</div>
{% endfor %}
{% for msg in get_flashed_messages(category_filter='error') %}
<div class="alert alert-warning alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span>
</button>
<strong>Warning!</strong> {{ msg }}.
</div>
{% endfor %}
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
上传文件的界面 upload.html
{% extends 'bootstrap/base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block content %}
<div class="col-md-8 col-lg-offset-2">
{# 接受闪现消息 如果是报错信息则显示红色;成功则显示绿色#}
{% for msg in get_flashed_messages(category_filter='ok') %}
<div class="alert alert-success alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span>
</button>
<strong>Success!</strong> {{ msg }}.
</div>
{% endfor %}
{% for msg in get_flashed_messages(category_filter='error') %}
<div class="alert alert-warning alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span>
</button>
<strong>Warning!</strong> {{ msg }}.
</div>
{% endfor %}
{{ wtf.quick_form(form) }}
</div>
{% endblock %}