Flask学习笔记_异步论坛(四)
flask 系列的代码笔记都放在了 仓库。
生成本环境的requirements.txt文件
pip freeze > requirements.txt
1.配置和数据库链接
1.exts.py里面实例化sqlalchemy数据库
from flask_sqlalchemy import SQLAlchemy
db=SQLAlchemy()
2.config.py配置app和数据库信息
#1.app配置
DEBUG=True
#2.数据库配置
DB_USERNAME="root"
DB_PASSWORD="1xxxx"
DB_HOST="127.0.0.1"
DB_PORT="3306"
DB_NAME="aforum"
DB_URI="mysql+pymysql://%s:%s@%s:%s/%s?charset=utf8mb4" % (DB_USERNAME,DB_PASSWORD,DB_HOST,DB_PORT,DB_NAME)
SQLALCHEMY_DATABASE_URI=DB_URI
SQLALCHEMY_TRACK_MODIFIER=False
3.app.py导入exts和config并初始化到app上
from flask import Flask
import config
from exts import db
app=Flask(__name__)#1.实例化app
app.config.from_object(config)#2.config配置文件绑定到app
db.init_app(app)#3.数据库绑定到app
@app.route('/')
def index():
return "hello"
if __name__=="__main__":
app.run()
2.创建用户模型并映射到数据库
1.models/auth.py创建用户模型
from exts import db
import shortuuid
from datetime import datetime
from werkzeug.security import generate_password_hash,check_password_hash
class UserModel(db.Model):
__tablename__ = "user"
id = db.Column(db.String(100), primary_key=True, default=shortuuid.uuid)
email = db.Column(db.String(50), unique=True, nullable=False)
username = db.Column(db.String(50), nullable=False)
_password = db.Column(db.String(200), nullable=False)
avatar = db.Column(db.String(100))
signature = db.Column(db.String(100))
join_time = db.Column(db.DateTime, default=datetime.now)
is_staff = db.Column(db.Boolean, default=False)
is_active = db.Column(db.Boolean, default=True)
def __init__(self, *args, **kwargs):
if "password" in kwargs:
self.password = kwargs.get('password')
kwargs.pop("password")
super(UserModel, self).__init__(*args, **kwargs)
@property
def password(self):
return self._password
@password.setter
def password(self, newpwd):
self._password = generate_password_hash(newpwd)
def check_password(self,rawpwd):
return check_password_hash(self.password, rawpwd)
2.app.py导入模型并用flask-migrate管理数据库
from flask_migrate import Migrate
from models import auth
migrate=Migrate(app,db)
3.命令行migrate三部曲将模型映射到数据库
在app.py文件的目录下
flask db init
flask db migrate
flask db upgrade
3.登录与注册页面的get请求
1.首先写登录和注册的前端页面
#1。首先抽出base.html文件
<html>
<head>
<meta charset="utf-8">
<script src="http://cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
<link href="http://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<script src="http://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="{{ url_for('static', filename='front/js/zlajax.js') }}"></script>
<script src="{{ url_for('static', filename='front/js/zlparam.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='front/css/front_base.css') }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{% endblock %}</title>
{% block head %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-default">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">论坛</a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active"><a href="/">首页<span class="sr-only">(current)</span></a></li>
</ul>
<form class="navbar-form navbar-left">
<div class="form-group">
<input type="text" class="form-control" placeholder="请输入关键字">
</div>
<button type="submit" class="btn btn-default">搜索</button>
</form>
<ul class="nav navbar-nav navbar-right">
{% if user %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
{{ user.username }}
<span class="caret"></span>
</a>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li><a href="{{ url_for('front.cms') }}">后台管理</a></li>
<li><a href="{{ url_for('front.setting') }}">设置</a></li>
<li><a href="{{ url_for('front.logout') }}">注销</a></li>
</ul>
</li>
{% else %}
<li><a href="{{ url_for('front.login') }}">登录</a></li>
<li><a href="{{ url_for('front.register') }}">注册</a></li>
{% endif %}
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
<div class="main-container">
{% block body %}{% endblock %}
</div>
</body>
</html>
#2.login.html文件
{% extends "front/base.html" %}
{% block title %}
登录
{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='front/css/signbase.css') }}">
<script src="{{ url_for('static', filename='front/js/login.js') }}"></script>
{% endblock %}
{% block body %}
<div class="outer-box">
<div class="logo-box">
<a href="/">
<img src="{{ url_for('static', filename='front/images/logo.png') }}" alt="">
</a>
</div>
<h2 class="page-title">
登录
</h2>
<div class="sign-box">
<div class="form-group">
<input type="text" class="form-control" name="email" placeholder="邮箱">
</div>
<div class="form-group">
<input type="password" class="form-control" name="password" placeholder="密码">
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="remember" value="1">记住我
</label>
</div>
<div class="form-group">
<button class="btn btn-warning btn-block" id="submit-btn">立即登录</button>
</div>
<div class="form-group">
<a href="#" class="signup-link">没有账号?立即注册</a>
<a href="#" class="resetpwd-link" style="float:right;">找回密码</a>
</div>
</div>
</div>
{% endblock %}
#3.register.html文件
{% extends "front/base.html" %}
{% block title %}
注册
{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='front/css/signbase.css') }}">
<script src="{{ url_for('static', filename='front/js/register.js') }}"></script>
{% endblock %}
{% block body %}
<div class="outer-box">
<div class="logo-box">
<a href="/">
<img src="{{ url_for('static', filename='front/images/logo.png') }}" alt="">
</a>
</div>
<h2 class="page-title">
注册
</h2>
<div class="sign-box">
<div class="form-group">
<div class="input-group">
<input type="email" class="form-control" name="email" placeholder="邮箱">
<span class="input-group-btn">
<button id="email-captcha-btn" class="btn btn-default">发送验证码</button>
</span>
</div>
</div>
<div class="form-group">
<input type="text" class="form-control" name="email-captcha" placeholder="邮箱验证码">
</div>
<div class="form-group">
<input type="text" class="form-control" name="username" placeholder="用户名">
</div>
<div class="form-group">
<input type="password" class="form-control" name="password" placeholder="密码">
</div>
<div class="form-group">
<input type="password" class="form-control" name="repeat-password" placeholder="确认密码">
</div>
<div class="form-group">
<div class="input-group">
<input type="text" class="form-control" name="graph-captcha" placeholder="图形验证码">
<span class="input-group-addon captcha-addon">
<img id="captcha-img" class="captcha-img" src="#" alt="">
</span>
</div>
</div>
<div class="form-group">
<button class="btn btn-warning btn-block" id="submit-btn">立即注册</button>
</div>
</div>
</div>
{% endblock %}
2.写它们的view蓝图并导入到__init__中
#1.在apps/front/views.py里面写蓝图的视图函数
from flask import Blueprint,request,render_template
bp=Blueprint("front",__name__,url_prefix="/")
@bp.route('/login/', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template('front/login.html')
@bp.route('/register/', methods=['GET', 'POST'])
def register():
if request.method == 'GET':
return render_template('front/register.html')
#2.在apps/front/__init__.py里面导入蓝图
from .views import bp as front_bp
3.蓝图注册到app
#在app.py里面导入蓝图并注册到app上
from apps.front import front_bp
app.register_blueprint(front_bp)
4.邮箱验证功能
1.邮箱验证
#1.在config里面配置邮箱第三方服务商来发送邮件
MAIL_SERVER="smtp.qq.com"#发送验证码的邮箱服务器,这里是自己公司的邮箱服务器
MAIL_PORT='587'#587是tls协议,465是ssl协议
MAIL_USE_TLS=True
#MAIL_USE_SSL
MAIL_USERNAME="1xxxc9@qq.com"
MAIL_PASSWORD="wxxbe"
MAIL_DEFAULT_SENDER="11xx@qq.com"
#2.exts里面导入mail
from flask_mail import Mail
mail=Mail()
#3.在app里面把exts里面的mail导入进来并绑定到app
from exts import db,mail
mail.init_app(app)
#4.开始在views里面写发送邮箱验证码的视图函数
from exts import mail
from flask_mail import Message
from flask importjsonify
import string,random
@bp.get("/email/captcha/")
def email_captcha():
email=request.args.get('email')
if not email:
return jsonify({"code":400,"message":"请先传入邮箱"})
source=list(string.digits)
captcha="".join(random.sample(source,6))
message=Message(subject="注册验证码",recipients=[email],body="您的注册验证码是:%s" % captcha)
try:
mail.send(message)
except Exception as e:
print("邮件发送失败")
print(e)
return jsonify({"code":500,"message":"邮件发送失败"})
return jsonify({"code":200,"message":"邮件发送成功"})
2.使用celery异步发送邮箱验证网络请求
celery(分布式任务队列/任务调度器)和redis(内存数据库)的教程和安装步骤可以参考学习。Broker和Backend都用redis存储。
pip install gevent
pip install redis
pip install hiredis
启动celery
redis-cli
#1.在config中设置reids的相关信息
CELERY_BROKER_URL="redis://127.0.0.1:6379/0"#broker
CELERY_RESULT_BACKEND="redis://127.0.0.1:6379/0"#backend
#2.mycelery.py里面定义并添加任务
from flask_mail import Message
from exts import mail
from celery import Celery
# 定义任务函数
def send_mail(recipient,subject,body):
message = Message(subject=subject,recipients=[recipient],body=body)
try:
mail.send(message)
return {"status": "SUCCESS"}
except Exception :
return {"status": "FAILURE"}
# 创建celery对象
def make_celery(app):
celery = Celery(app.import_name, backend=app.config['CELERY_RESULT_BACKEND'],broker=app.config['CELERY_BROKER_URL'])
TaskBase = celery.Task
class ContextTask(TaskBase):
abstract = True
def __call__(self, *args, **kwargs):
with app.app_context():
return TaskBase.__call__(self, *args, **kwargs)
celery.Task = ContextTask
app.celery = celery
# 添加任务
celery.task(name="send_mail")(send_mail)
return celery
#3.在app.py里面将celery绑定到app
from mycelery import make_celery
mycelery=make_celery(app)
#4.在views.py里面利用current_app调用celery里面的task任务
from flask import current_app
@bp.get("/email/captcha/")
def email_captcha():
email=request.args.get('email')
if not email:
return jsonify({"code":400,"message":"请先传入邮箱"})
source=list(string.digits)
captcha="".join(random.sample(source,6))
subject="注册验证码"
body="您的注册验证码是:%s"%captcha
current_app.celery.send_task("send_mail",(email,subject,body))
return jsonify({"code":200,"message":"邮件发送成功"})
#5.在工程目录下运行这个celery
celery -A app.mycelery worker --loglevel=info -P gevent
#6.访问这个视图函数就可以成功利用celery进行异步任务调取
3.使用flask-caching缓存验证码并验证
flask-caching的相关教程可以查看博文。
#1.安装:pip install flask-caching
#2.在config里面写flask-caching相关的配置
CACHE_TYPE="RedisCache"
CACHE_DEFAULT_TIMEOUT=300
CACHE_REDIS_HOST="127.0.0.1"
CACHE_REDIS_PORT=6379
#3.在exts里面引入caching
from flask_caching import Cache
cache=Cache()
#4.在app.py里面init
from exts import cache
cache.init_app(app)
#5.在view的视图函数里面缓存验证码
from exts import cache
cache.set(email,captcha)#cache缓存是键值对的形式
4.重构restful API
#1.在utils/restful.py里面
# Restful API
from flask import jsonify
class HttpCode(object):
# 响应正常
ok = 200
# 没有登陆错误
unloginerror = 401
# 没有权限错误
permissionerror = 403
# 客户端参数错误
paramserror = 400
# 服务器错误
servererror = 500
def _restful_result(code, message, data):
return jsonify({ "code": code,"message": message or "", "data": data or {}})
def ok(message=None, data=None):
return _restful_result(code=HttpCode.ok, message=message, data=data)
def unlogin_error(message="没有登录!"):
return _restful_result(code=HttpCode.unloginerror, message=message, data=None)
def permission_error(message="没有权限访问!"):
return _restful_result(code=HttpCode.paramserror, message=message, data=None)
def params_error(message="参数错误!"):
return _restful_result(code=HttpCode.paramserror, message=message, data=None)
def server_error(message="服务器开小差啦!"):
return _restful_result(code=HttpCode.servererror, message=message or '服务器内部错误', data=None)
#2.在view视图函数里
from utils import restful
return restful.params_error(message="请先传入邮箱")
return restful.ok(message="邮件发送成功")
5.注册页面的post请求
5.1注册页面邮箱验证码的ajax请求
#1.在register.html里面引入js文件
<script src="{{ url_for('static', filename='front/js/register.js') }}"></script>
#2.在register.js里面监听(4步),这里引用zlajax是因为它自动给了csrf-token
var RegisterHandler = function (){\\1.定义了一个JavaScript对象
}
RegisterHandler.prototype.listenSendCaptchaEvent = function (){\\2.包含一个方法
var callback = function (event){
// 原生的JS对象:this => jQuery对象
var $this = $(this);
// 阻止默认的点击事件
event.preventDefault();
var email = $("input[name='email']").val();
var reg = /^\w+((.\w+)|(-\w+))@[A-Za-z0-9]+((.|-)[A-Za-z0-9]+).[A-Za-z0-9]+$/;
if(!email || !reg.test(email)){
alert("请输入正确格式的邮箱!");
return;
}
zlajax.get({
url: "/email/captcha?email=" + email,
success: function (result){
if(result['code'] == 200){
console.log("邮件发送成功!");
// 取消按钮的点击事件
$this.off("click");
// 添加禁用状态
$this.attr("disabled", "disabled");
// 开始倒计时
var countdown = 60;
var interval = setInterval(function (){
if(countdown > 0){
$this.text(countdown);
}else{
$this.text("发送验证码");
$this.attr("disabled", false);
$this.on("click", callback);
// 清理定时器
clearInterval(interval);
}
countdown--;
}, 1000);
}else{
var message = result['message'];
alert(message);
}
}
})
}
$("#email-captcha-btn").on("click", callback);
}
RegisterHandler.prototype.run = function (){\\3.方法在run函数里调用
this.listenSendCaptchaEvent();
}
// $(function(){})
$(function (){\\4.实例化并运行
var handler = new RegisterHandler();
handler.run();
})
#3.post请求要用csrf-token,所以在base.html里面引入
<meta name="csrf-token" content="{{csrf_token()}}">
#4.csrf-token需要先安装:
pip install flask-wtf
#5.在config里面设置secretkey
SECRET_KEY="FASDFNMLKSDF"
#6.在exts里面引入
from flask_wtf import CSRFProtect
csrf=CSRFProtect()
#6.在app上绑定init
from exts import csrf
csrf.init_app(app)
5.2注册页面的图形验证码功能
#1.在config里面获取工程的base目录
import os
BASE_DIR=os.path.dirname(__file__)
#2.在utils目录下的captcha的init文件里生成图形验证码
import random
import string
# Image:一个画布
# ImageDraw:一个画笔
# ImageFont:画笔的字体
from PIL import Image,ImageDraw,ImageFont
from flask import current_app
import os
# pip install pillow
# Captcha验证码
class Captcha(object):
# 生成几位数的验证码
number = 4
# 验证码图片的宽度和高度
size = (100,30)
# 验证码字体大小
fontsize = 25
# 加入干扰线的条数
line_number = 2
# 构建一个验证码源文本
SOURCE = list(string.ascii_letters)
for index in range(0, 10):
SOURCE.append(str(index))
#用来绘制干扰线
@classmethod
def __gene_line(cls,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 = cls.__gene_random_color(),width=2)
# 用来绘制干扰点
@classmethod
def __gene_points(cls,draw,point_chance,width,height):
chance = min(100, max(0, int(point_chance))) # 大小限制在[0, 100]
for w in range(width):
for h in range(height):
tmp = random.randint(0, 100)
if tmp > 100 - chance:
draw.point((w, h), fill=cls.__gene_random_color())
# 生成随机的颜色
@classmethod
def __gene_random_color(cls,start=0,end=255):
random.seed()
return (random.randint(start,end),random.randint(start,end),random.randint(start,end))
# 随机选择一个字体
@classmethod
def __gene_random_font(cls):
fonts = [
'Courgette-Regular.ttf',
'LHANDW.TTF',
'Lobster-Regular.ttf',
'verdana.ttf'
]
font = random.choice(fonts)
fontpath = os.path.join(current_app.config['BASE_DIR'],'utils','captcha',font)
# return 'utils/captcha/'+font
return fontpath
# 用来随机生成一个字符串(包括英文和数字)
@classmethod
def gene_text(cls, number):
# number是生成验证码的位数
return ''.join(random.sample(cls.SOURCE, number))
#生成验证码
@classmethod
def gene_graph_captcha(cls):
# 验证码图片的宽和高
width,height = cls.size
# 创建图片
# R:Red(红色)0-255
# G:G(绿色)0-255
# B:B(蓝色)0-255
# A:Alpha(透明度)
image = Image.new('RGBA',(width,height),cls.__gene_random_color(0,100))
# 验证码的字体
font = ImageFont.truetype(cls.__gene_random_font(),cls.fontsize)
# 创建画笔
draw = ImageDraw.Draw(image)
# 生成字符串
text = cls.gene_text(cls.number)
# 获取字体的尺寸
font_width, font_height = font.getsize(text)
# 填充字符串
draw.text(((width - font_width) / 2, (height - font_height) / 2),text,font= font,fill=cls.__gene_random_color(150,255))
# 绘制干扰线
for x in range(0, cls.line_number):
cls.__gene_line(draw, width, height)
# 绘制噪点
cls.__gene_points(draw, 10, width, height)
return (text,image)
#3.在views里面写视图函数
from utils.captcha import Captcha
import time
from hashlib import md5
from io import BytesIO
from flask import make_response
@bp.route("/graph/captcha/")
def graph_captcha():
captcha,image=Captcha.gene_graph_captcha()
key=md5((captcha+str(time.time())).encode('utf-8')).hexdigest()
cache.set(key,captcha)#cache里面缓存这个captcha
buffer=BytesIO()
image.save(buffer,"png")
buffer.seek(0)#buffer文件指针指向最开始的位置
resp=make_response(buffer.read())
resp.content_type="image/png"
resp.set_cookie("_graph_captcha_key",key,max_age=3600)#将key值保存到cookie1个小时
return resp
#4.在register.html里面写图片验证码的src
<img id="captcha-img" class="captcha-img" src="{{url_for('front.graph_captcha')}}" alt="">
#5.实现点击图片重新生成,所以在regist.js里面监听
RegisterHandler.prototype.listenGraphCaptchaEvent = function (){
$("#captcha-img").on("click", function (){
console.log("点击了图形验证码");
var $this = $(this);
var src = $this.attr("src");
// /graph/captcha
// /graph/captcha?sign=Math.random()
// 防止一些老的浏览器,在两次url相同的情况下,不会重新发送请求,导致图形验证码不会更新
let new_src = zlparam.setParam(src, "sign", Math.random())
$this.attr("src",new_src);
});
}
RegisterHandler.prototype.run = function (){
this.listenSendCaptchaEvent();
this.listenGraphCaptchaEvent();
}
5.3注册页面的post提交
#1.在front/forms.py里面进行表单验证
from wtforms import Form,ValidationError
from wtforms.fields import StringField
from wtforms.validators import Email,Length,EqualTo
from models.auth import UserModel#对表单进行二次验证
from exts import cache
from flask import request
class BaseForm(Form):
@property
def messages(self):
message_list = []
if self.errors:
for error in self.errors.values():
message_list.extend(error)
return message_list
class RegisterForm(BaseForm):
email=StringField(validators=[Email(message="请输入正确的邮箱")])
email_captcha=StringField(validators=[Length(6,6,message="请输入6位验证码")])
username=StringField(validators=[Length(3,20,message="请输入3-20位的用户名")])
password=StringField(validators=[Length(6,20,message="请输入6-20位的密码")])
repeat_password=StringField(validators=[EqualTo("password",message="两次密码不一致")])
graph_captcha=StringField(validators=[Length(4,4,message="请输入4位图形验证码")])
def validate_email(self,field):
email=field.data
user=UserModel.query.filter_by(email=email).first()
if user:
raise ValidationError(message="邮箱已经被注册")
def validate_email_captcha(self,field):
email_captcha=field.data
email=self.email.data
cache_captcha=cache.get(email)
if not cache_captcha or cache_captcha!=email_captcha:
raise ValidationError(message="邮箱验证码错误")
def validate_graph_captcha(self,field):
graph_captcha=field.data
key=request.cookies.get("_graph_captcha_key")
cache_captcha=cache.get(key)
if not cache_captcha or cache_captcha.lower()!=graph_captcha.lower():
raise ValidationError(message="图形验证码错误")
#2.在front/views.py里面写post视图函数
from .forms import RegisterForm
from models.auth import UserModel
from exts import db
@bp.route('/register/', methods=['GET', 'POST'])
def register():
if request.method == 'GET':
return render_template('front/register.html')
else:
form=RegisterForm(request.form)
if form.validate():
email=form.email.data
username=form.username.data
password=form.password.data
user=UserModel(email=email,username=username,password=password)
db.session.add(user)
db.session.commit()
return restful.ok()
else:
message=form.messages[0]
return restful.params_error(message=message)
#3.在js里面绑定点击事件,跳到上面的视图函数
RegisterHandler.prototype.listenSubmitEvent = function (){
$("#submit-btn").on("click", function (event){
event.preventDefault();
var email = $("input[name='email']").val();
var email_captcha = $("input[name='email-captcha']").val();
var username = $("input[name='username']").val();
var password = $("input[name='password']").val();
var repeat_password = $("input[name='repeat-password']").val();
var graph_captcha = $("input[name='graph-captcha']").val();
// 如果是商业项目,一定要先验证这些数据是否正确
zlajax.post({
url: "/register",
data: {
"email": email,
"email_captcha": email_captcha,
"username": username,
password, // "password": password
repeat_password,
graph_captcha
},
success: function (result){
if(result['code'] == 200){
window.location = "/login";
}else{
alert(result['message']);
}
}
})
});
}
RegisterHandler.prototype.run = function (){
this.listenSendCaptchaEvent();
this.listenGraphCaptchaEvent();
this.listenSubmitEvent();
}
6.登录页面的post请求
#1.首先表单验证
from wtforms.fields import IntegerField
class LoginForm(BaseForm):
email=StringField(validators=[Email(message="请输入正确的邮箱")])
password=StringField(validators=[Length(6,20,message="请输入6-20位的密码")])
remember=IntegerField()
#2.视图函数
from flask import session
from .forms import LoginForm
@bp.route('/login/', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template('front/login.html')
else:
form=LoginForm(request.form)
if form.validate():
email=form.email.data
password=form.password.data
remember=form.remember.data
user=UserModel.query.filter_by(email=email).first()
if not user:
return restful.params_error("此邮箱没有注册")
if not user.check_password(password):
return restful.params_error("邮箱或密码错误")
session['user_id']=user.id
if remember ==1:
session.permanent=True
return restful.ok()
else:
return restful.params_error(message=form.messages[0])
#3.在config里面设置permanent时间
from datetime import timedelta
PERMANENT_SESSION_LIFETIME=timedelta(days=7)
#4.登录的post提交的前端监听,在login.js中,并加载到html中
var LoginHandler = function (){}
LoginHandler.prototype.listenSubmitEvent = function (){
$("#submit-btn").on("click", function (event){
event.preventDefault();
var email = $("input[name='email']").val();
var password = $("input[name='password']").val();
var remember = $("input[name='remember']").prop("checked");
zlajax.post({
url: "/login",
data: {
email,
password,
remember: remember?1:0
},
success: function (result){
if(result['code'] == 200){
var token = result['data']['token'];
var user = result['data']['user'];
localStorage.setItem("JWT_TOKEN_KEY", token);
localStorage.setItem("USER_KEY", JSON.stringify(user));
window.location = "/"
}else{
alert(result['message']);
}
}
})
});
}
LoginHandler.prototype.run = function (){
this.listenSubmitEvent();
}
$(function (){
var handler = new LoginHandler();
handler.run();
});
7.首页
7.1首页状态切换功能
1.get请求:写index视图函数和html
2.状态切换功能
#1.写退出登录的视图函数
@bp.route("/logout/")
def logout():
session.clear()
return redirect("/")
#2.利用钩子函数(用户发送请求前的操作)和上下文处理器函数(视图函数返回给用户数据前的操作)将user绑定到g上
bp=Blueprint("front",__name__,url_prefix="/")
@bp.before_request#钩子函数,在用户访问视图函数前在session里拿到用户绑到g上
def front_before_request():
if 'user_id' in session:
user_id=session.get("user_id")
user=UserModel.query.get(user_id)
setattr(g,"user",user)
@bp.context_processor#上下文处理器函数,在视图函数里,服务器返回给用户数据前将这里的参数返回给模板进行渲染
def front_after_request():
if hasattr(g,"user"):
return {"user":g.user}
else:
return {}
#3.修改html里面的变量
{% if user %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
{{ user.username }}
<span class="caret"></span>
</a>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li><a href="#">后台管理</a></li>
<li><a href="{{url_for('front.setting')}}">设置</a></li>
<li><a href="{{ url_for('front.logout') }}">注销</a></li>
</ul>
</li>
{% else %}
<li><a href="{{ url_for('front.login') }}">登录</a></li>
<li><a href="{{ url_for('front.register') }}">注册</a></li>
{% endif %}
7.2首页设置功能
#1.在front/decorates.py里面写登录装饰器
from flask import g,redirect,url_for
from functools import wraps
def login_required(func):
@wraps(func)
def inner(*args, **kwargs):
if hasattr(g,"user"):
return func(*args, **kwargs)
else:
return redirect(url_for('front.login'))
return inner
#2.写视图函数和html,以及设置按钮的跳转链接,并要有登录装饰器
@bp.route("/setting/")
@login_required
def setting():
email_hash=md5(g.user.email.encode("utf-8")).hexdigest()
return render_template("front/setting.html",email_hash=email_hash)
7.3首页头像功能
#1.pip install flask-avatars
#2.在exts里面导入,在app里面初始化
from flask_avatars import Avatars
avatars=Avatars()
from exts import avatars
avatars.init_app(app)
#3.使用Gravatar头像
<img src="{{ avatars.gravatar(email_hash) }}" alt="..." class="img-circle" id="avatar-img">
#4.使用标识生成头像,并把L尺寸的头像地址保存到数据库
AVATARS_SAVE_PATH=os.path.join(BASE_DIR,"media","avatars")#在config里面设置图像的保存地址
#在register视图函数里面添加avatar的保存数据
from flask_avatars import Identicon
import os
identicon=Identicon()
filenames=identicon.generate(text=md5(email.encode("utf-8")).hexdigest())
avatar=filenames[2]
user=UserModel(email=email,username=username,password=password,avatar=avatar)
#在media/view里面写访问头像的视图函数,并绑定到app上,然后修改html的链接
from flask import Blueprint,send_from_directory,current_app
bp=Blueprint("media",__name__,url_prefix="/media")
@bp.route('/avatar/<filename>')
def get_avatar(filename):
return send_from_directory(current_app.config['AVATARS_SAVE_PATH'],filename)
from apps.media import media_bp
app.register_blueprint(media_bp)
<img src="{{ url_for('media.get_avatar',filename=user.avatar) }}" alt="..." class="img-circle" id="avatar-img">
#4.用户自定义图像:用户头像上传就是表单提交的方式,所以要进行表单验证,然后写视图函数,
class UploadAvatarForm(BaseForm):
image=FileField(validators=[FileAllowed(['png', 'jpg', 'jpeg',],message='图片格式不符合要求'),FileSize(max_size=1024*1024*5,message='图片大小不超过5MB')])
@bp.post("/avatar/upload/")
@login_required
def upload_avatar():
form=UploadAvatarForm(request.files)
if form.validate():
image=form.image.data
filename=image.filename
_,ext=os.path.splitext(filename)
filename=md5((g.user.email+str(time.time())).encode('utf-8')).hexdigest()+ext
image_path=os.path.join(current_app.config['AVATARS_SAVE_PATH'],filename)
image.save(image_path)
g.user.avatar=filename
db.session.commit()
return restful.ok(data={'avatar':filename})
else:
message=form.messages[0]
return restful.params_error(message=message)
#写图像上传的js,并将js导入到html,
7.4个性签名功能
#1.写form表单验证,view视图进行post请求,写js,导入到html
8.帖子相关设置
8.1帖子板块
1.命令行实现板块初始化
#1.在models/post.py里面创建帖子板块模型,并导入到app文件,然后migrate到数据库
from exts import db
from datetime import datetime
class BoardModel(db.Model):
__tablename__ = 'board'
id=db.Column(db.Integer, primary_key=True,autoincrement=True)
name=db.Column(db.String(20),unique=True)
priority=db.Column(db.Integer, default=1)
create_time=db.Column(db.DateTime,default=datetime.now)
from models import post#app.py里面导入一下
flask db migrate
flask db upgrade
#2.在commands.py里面写初始化板块的命令函数(给板块数据到db数据库)
from models.post import BoardModel
from exts import db
def init_boards():
board_names=['flask','fast','ai','爬虫']
for index,board_name in enumerate(board_names):
board=BoardModel(name=board_name,priority=len(board_names)-index)
db.session.add(board)
db.session.commit()
print("板块初始化成功")
#3.在app.py里面导入并注册命令
import commands
app.cli.command("inbo")(commands.init_boards)
#4.在cmd里面调用命令
flask inbo
#5.板块的后端已经实现,现在要将这个信息传给前端并显示,所以在首页(/)视图函数下传参并在html中循环显示
from models.post import BoardModel
boards=BoardModel.query.order_by(BoardModel.priority.desc()).all()
return render_template("front/index.html",boards=boards)
{% for board in boards %}
<a href="#" class="list-group-item">{{board.name}}</a>
{% endfor %}
2.创建帖子相关的模型,包括PostModel,BannerModel,CommentModel,并migrate到数据库
8.2发布帖子
1.get
#1.首先修改首页的发布帖子的跳转链接,然后写视图函数和html文件
<a href="{{url_for('front.public_post')}}" class="btn btn-warning btn-block">发布帖子</a>
@bp.route("post/public/",methods=["POST", "GET"])
def public_post():
if request.method == "GET":
boards=BoardModel.query.all()
return render_template("front/public_post.html",boards=boards)
2.富文本编辑器wangEditor,这里首先需要导入它的js文件,可以按官网的示例线上引用也可以下载到本地再导入。
#1.在public_post.html里面导入富文本编辑器的js
<script type="text/javascript" src="{{url_for('static',filename='lib/wangEditor/wangEditor.min.js')}}"></script>
#2.写自己的初始化编辑器和文本内容提交的js并引入到html中
<script type="text/javascript" src="{{url_for('static',filename='front/js/public_post.js')}}"></script>
#3.上传图片到本地,首先在wangeditor里面初始化图片上传的路径等信息,然后写图片上传的视图函数
@bp.post("/post/image/upload/")
@login_required
def upload_post_image():
form=UploadAvatarForm(request.files)
if form.validate():
image=form.image.data
filename=image.filename
_,ext=os.path.splitext(filename)
filename=md5((g.user.email+str(time.time())).encode('utf-8')).hexdigest()+ext
image_path=os.path.join(current_app.config['POST_IMAGE_SAVE_PATH'],filename)
image.save(image_path)
return jsonify({
"errno": 0, # 注意:值是数字,不能是字符串
"data": [{
"url": url_for('media.get_post_image',filename=filename), # 图片 src ,必须
"alt": "filename", # 图片描述文字,非必须
"href": "" # 图片的链接,非必须
}]
})
else:
message=form.messages[0]
return jsonify({
"errno": 1, #只要不等于 0 就行
"message": message
})
3.发布帖子的提交
#1.首先表单验证,然后写帖子内容提交的视图函数,js提交前端
@bp.route("post/public/",methods=["POST", "GET"])
@login_required
def public_post():
if request.method == "GET":
boards=BoardModel.query.all()
return render_template("front/public_post.html",boards=boards)
else:
form=PublicPostForm(request.form)
if form.validate():
title=form.title.data
content=form.content.data
board_id=form.board_id.data
try:
board=BoardModel.query.get(board_id)
except Exception as e:
return restful.error(message='板块不存在')
post_model=PostModel(title=title,content=content,board=board,author=g.user)
db.session.add(post_model)
db.session.commit()
return restful.ok(data={"id":post_model.id})
else:
return restful.params_error(message=form.messages[0])
var PublicPostHandler = function (){
var csrf_token = $("meta[name='csrf-token']").attr("content");
var editor = new window.wangEditor("#editor");
editor.config.uploadImgServer = "/post/image/upload";
editor.config.uploadFileName = "image";
// 1. 放到请求体中
// 2. 放到请求头中X-CSRFToken
// 再和cookie中的csrf_token进行对比
editor.config.uploadImgHeaders = {
"X-CSRFToken": csrf_token
}
editor.config.uploadImgMaxSize = 1024*1024*5;
editor.create();
this.editor = editor;
}
PublicPostHandler.prototype.listenSubmitEvent = function (){
var that = this;
$("#submit-btn").on("click", function (event){
event.preventDefault();
var title = $("input[name='title']").val();
var board_id = $("select[name='board_id']").val();
var content = that.editor.txt.html();
zlajax.post({
url: "/post/public/",
data: {title,board_id,content},
success: function (result){
if(result['code'] == 200){
let data = result['data'];
let post_id = data['id'];
window.location = "/post/detail/" + post_id;
}else{
alert(result['message']);
}
}
});
});
}
PublicPostHandler.prototype.run = function(){
this.listenSubmitEvent();
}
$(function(){
var handler = new PublicPostHandler();
handler.run();
});
8.3帖子详情页
#1.帖子详情页的get请求视图函数,html
#2.帖子详情的代码高亮功能,使用highlight.js
<link rel="stylesheet" href="{{ url_for('static', filename='lib/highlight/styles/github-dark.min.css') }}">
<script src="{{ url_for('static', filename='lib/highlight/highlight.min.js') }}"></script>
<script src="{{ url_for('static', filename='front/js/post_detail.js') }}"></script>
hljs.highlightAll();#post_detail.js里面初始化
#3.帖子详情的评论功能:表单验证,post视图函数,在html中获取填写的信息通过js提交,html中导入js
8.4首页帖子列表
1.在首页的视图函数中拿到数据库的帖子数据,通过参数传递给前端html,前端通过for循环展示帖子相关信息
2.使用flask-paginate实现帖子分页
pip install flask-paginate
#1.首先在config中配置每页展示帖子的数量
PER_PAGE_COUNT=7
#2.在首页的视图函数中查询并进行分页,然后返回参数给html渲染
from flask_paginate import get_page_parameter,Pagination
@bp.route("/")
def index():
boards=BoardModel.query.order_by(BoardModel.priority.desc()).all()
post_query =PostModel.query.order_by(PostModel.create_time.desc())
total=post_query.count()
page= request.args.get(get_page_parameter(), type=int, default=1)
per_page_count = current_app.config['PER_PAGE_COUNT']
start = (page - 1) * per_page_count
posts = post_query.offset(start).limit(per_page_count).all()
pagination = Pagination(bs_version=3,page=page,total=total,per_page=per_page_count, force_parameter=True)
context={"boards":boards, "posts":posts,"pagination":pagination}
return render_template("front/index.html",**context)
#3.在index的html的帖子列表末尾添加翻页按钮
<div style="text-align:center;">
{{pagination.links}}
</div>
3.帖子按评论顺序和时间排列
#1.首先通过html传递st参数,即按什么方式排序
{% if st==1 %}
<li class="active">
{% else %}
<li>
{% endif %}
<a href="{{url_for('front.index',st=1)}}">最新</a></li>
{% if st==2 %}
<li class="active">
{% else %}
<li>
{% endif %}
<a href="{{url_for('front.index',st=2)}}">评论最多</a></li>
#2.在view里面按照st参数进行排序,
from sqlalchemy.sql import func
@bp.route("/")
def index():
sort=request.args.get('st',type=int,default=1)
post_query=None
if sort==1:
post_query=PostModel.query.order_by(PostModel.create_time.desc())
else:
post_query=db.session.query(PostModel).outerjoin(CommentModel).group_by(PostModel.id).order_by(func.count(CommentModel.id).desc(),PostModel.create_time.desc())
...
context={"boards":boards, "posts":posts,"pagination":pagination,"st":sort}
return render_template("front/index.html",**context)
4.帖子按板块过滤
#1.html传递板块bd参数
{% if not bd %}
<a href="/" class="list-group-item active">所有板块</a>
{% else %}
<a href="/" class="list-group-item">所有板块</a>
{% endif %}
{% for board in boards %}
{% if board.id==bd %}
<a href="{{url_for('front.index',bd=board.id,page=1)}}" class="list-group-item active">{{board.name}}</a>
{% else %}
<a href="{{url_for('front.index',bd=board.id,page=1)}}" class="list-group-item">{{board.name}}</a>
{% endif %}
{% endfor %}
#2.view里面拿到bd参数,对post进行过滤
@bp.route("/")
def index():
sort=request.args.get('st',type=int,default=1)
board_id=request.args.get('bd',type=int,default=None)
boards=BoardModel.query.order_by(BoardModel.priority.desc()).all()
post_query=None
if sort==1:
post_query=PostModel.query.order_by(PostModel.create_time.desc())
else:
post_query = db.session.query(PostModel).outerjoin(CommentModel).group_by(PostModel.id).order_by(func.count(CommentModel.id).desc(), PostModel.create_time.desc())
page= request.args.get(get_page_parameter(), type=int, default=1)
per_page_count = current_app.config['PER_PAGE_COUNT']
start = (page - 1) * per_page_count
end=start+current_app.config['PER_PAGE_COUNT']
if board_id:
post_query=post_query.filter(PostModel.board_id==board_id)
total = post_query.count()
posts=post_query.slice(start,end)
pagination = Pagination(bs_version=3,page=page,total=total,per_page=per_page_count, force_parameter=True)
context={"boards":boards, "posts":posts,"pagination":pagination,"st":sort,'bd':board_id}
return render_template("front/index.html",**context)