问答平台项目-flask
问答平台项目
1.项目的基础架构
把不同类型功能的代码, 放到不同的文件中,可以使项目架构更加的明确,下面是一种参考的方式
在项目文件夹下创建 config.py 用来存放 项目中的配置信息,创建 exts.py 放置一些扩展文件(例如一些第三方的模块,flask-sqlalchemy,等 创建 models.py 用来存放 sqlalchemy 中 ORM 中 数据表 对应的类。
在 app.py 中绑定config配置文件
# app.py
from flask import Flask
import config
app = Flask(__name__)
# 绑定配置文件
app.config.from_object(config)
@app.route('/')
def hello_world(): # put application's code here
return 'Hello World!'
if __name__ == '__main__':
app.run()
模块之间的循环引用问题
另外使用 exts.py 放置 扩展文件可以防止 循环引用 的问题
如果不使用 exts.py 放置 flask_sqlalchemy 模块, 那么
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy(app)
上面两行代码如果放在 app.py 中
models.py 中放置 ORM 中的模型类 UserModel(db.Model), 在 models.py 创建 模型类 需要继承 db.Model,
在 models.py 中 需要从 app.py 中引入 db 对象
from app import db
然后我们在 app.py 要用到 模型类来创建 相关的数据映射的数据表需要从 models.py 中导入 类模型
from models.py import UserModel
"""
为了避免循环引用的问题, 可以把 下面两行代码放到 exts.py 中,
这样的话, models.py 从 exts.py 中引入 db 对象, app.py 可以从models.py 中 引入模型类
"""
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy(app) # 如果这里 和 app 绑定的话,exts.py 需要引用 app 还是有循环引用的问题
# 所以 如下图没有直接和 app 进行绑定, 在 app.py 中使用 db.init_app(app) 和 app 进行绑定
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
# app.py
from flask import Flask
import config
from exts import db
from models import UserModel
app = Flask(__name__)
# 绑定配置文件
app.config.from_object(config)
# db 对象 和 app 对象进行绑定
db.init_app(app)
@app.route('/')
def hello_world(): # put application's code here
return 'Hello World!'
if __name__ == '__main__':
app.run()
# exts.py
# 放一些扩展文件,第三方模块
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
# models.py
from exts import db
class UserModel(db.Model):
pass
蓝图(blueprint)
将所有的视图函数都写在 app.py 中,当项目 较大的时候不容易维护,
将不同类型的视图函数模块化,一类视图函数放在一个 py 文件中。
可以在项目文件夹下创建一个存放蓝图相关的python package, 在其中 创建 不同的 视图函数模块
在不同模块中定义不同的蓝图对象, 在 app.py 中 导入, 并注册
# auth.py
from flask import Blueprint
bp = Blueprint("auth", __name__, url_prefix="/auth")
@bp.route("/login")
def login():
pass
# qa.py
from flask import Blueprint
bp = Blueprint("qa", __name__, url_prefix="/")
@bp.route("/")
def index():
pass
from flask import Flask
import config
from exts import db
from models import UserModel
# 在 app.py 模块中导入 bp
from blueprints.qa import bp as qa_bp
from blueprints.auth import bp as auth_bp
app = Flask(__name__)
# 绑定配置文件
app.config.from_object(config)
# db 对象 和 app 对象进行绑定
db.init_app(app)
# 注册蓝图
app.register_blueprint(qa_bp)
app.register_blueprint(auth_bp)
@app.route('/')
def hello_world(): # put application's code here
return 'Hello World!'
if __name__ == '__main__':
app.run()
2.User 表模型创建
创建使用的数据库
在 config.py 中 添加 数据库相关的配置信息
# config.py
# 数据库配置信息
HOSTNAME = '127.0.0.1'
PORT = 3306
DATABASE = 'q_a_platform'
USERNAME = 'root'
PASSWORD = 'root'
encoding = "utf8mb4"
DB_URI = 'mysql+pymysql://{}:{}@{}:{}/{}?charset={}'.format(USERNAME, PASSWORD, HOSTNAME, PORT, DATABASE, encoding)
SQLALCHEMY_DATABASE_URI = DB_URI
在 models.py 中 创建 相关的 User模型类
# models.py
from exts import db
from datetime import datetime
class UserModel(db.Model):
__tablename__ = "user"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
username = db.Column(db.String(100), nullable=False)
password = db.Column(db.Stirng(100), nullable=False)
email = db.Column(db.String(100), nullable=False, unique=True)
join_time = db.Column(db.DateTime, default=datetime.now)
在 app.py 中到导入 model.py 中 的 User 模型类(如果不在 app.py 中导入的话,使用 flask_migrate 进行数据库的映射时, 识别不到相关的模型)
from flask import Flask
import config
from exts import db
from models import UserModel
from flask_migrate import Migrate
# 在 app.py 模块中导入 bp
from blueprints.qa import bp as qa_bp
from blueprints.auth import bp as auth_bp
app = Flask(__name__)
# 绑定配置文件
app.config.from_object(config)
# db 对象 和 app 对象进行绑定
db.init_app(app)
migrate = Migrate(app, db)
# 注册蓝图
app.register_blueprint(qa_bp)
app.register_blueprint(auth_bp)
@app.route('/')
def hello_world(): # put application's code here
return 'Hello World!'
if __name__ == '__main__':
app.run()
使用 flask_migrate 进行 Python 类模型 和 数据表的映射
# 在项目 终端中执行
flask db init
flask db migrate
flask db upgrade
3.注册页面模版渲染
使用 写好的html 文件和 css 文件渲染注册 页面, 将 html 文件放到 templates 文件夹中, 将静态文件放入到static 文件夹中,在 auth 蓝图下 写 register 视图函数, 使用 render_template 指向 register.html, 渲染的页面
可能没有 css 样式和 相关的格式 ,需要将html 文件中加载 静态文件的方式 改成使用 url_for 的方式,
因为这里 加载 html 模块 使用的 jinjia2 , 要符合jinjia2 的语法。
<!--register-->
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap/bootstrap.4.6.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/init.css') }}">
<title>Q\A平台-注册</title>
</head>
# auth.py
from flask import Blueprint, render_template
bp = Blueprint("auth", __name__, url_prefix="/auth")
@bp.route("/login")
def login():
pass
@bp.route("/register")
def register():
return render_template("register.html")
模版文件中注册页面和 登录页面等的导航条和底部是一样的,可以写一个 父模版文件,使用模版继承来使模版文件更加简化
在 父模版中占位 用
{% block block名字 %} {% endblock %}
4.Flask 发送邮件功能实现
注册功能的第一步,就是给用户输入的邮箱发送验证码,我们使用 flask-mail 发送验证码
pip install flask-mail
想要发送邮件, 需要有一个邮箱服务器,可以自己搭建, 也可以使用第三方的邮件服务器, 例如 qq邮箱,网易邮箱,有各种企业邮箱, 这里用个人邮箱进行实验, 使用 flask_mail 发送邮件, 需要使用 SMTP协议(simple Mail Transfer Protocol)
首先登录 qq / 网易 等等邮箱,开启 SMTP 服务, QQ 邮箱在 邮箱设置 -> 账号->下,
网易邮箱在设置下, 开启 SMTP 服务后, 会有一个授权码, 邮箱配置中会用到
开启 邮箱 SMTP 服务以后, flask 可以通过 flask-mail 登录邮箱,发送邮件。
在 config.py 中添加 邮箱信息配置
# 邮箱配置
MAIL_SERVER = "smtp.qq.com"
# 这里不要打错,打错看了半天,是 mail_use_ssl 而不是 mail_user_ssl
MAIL_USE_SSL = True
MAIL_PORT = 465
# 发送 邮件的的邮箱账号
MAIL_USERNAME = "xxxxx@qq.com"
# 邮箱开启 STMP 后,生成的 授权码
MAIL_PASSWORD = "xxxxclxxskchifi"
# 发送 邮件的的邮箱账号
MAIL_DEFAULT_SENDER = "xxxx@qq.com"
在 exts.py 中创建 mail 对象, 在 app.py 中初始化
# ext.py
from flask_mail import Mail
mail = Mail()
# app.py
from exts import mail
mail.init(app)
# auth.py 视图函数模块
from exts import mail
from flask_mail import Message
bp = Blueprint("auth", __name__, url_prefix="/auth")
@bp.route("/mail/test")
def mail_test():
message = Message(subject="邮箱测试", recipients=["xxxxxx@163.com"], body="这是一条测试邮箱")
mail.send(message)
return "邮箱发送成功"
5.发送邮箱验证码功能实现
向邮箱发送验证码, 将邮箱的验证码存储在 数据库中, 在 models.py 中创建相应的 模型类
# models.py
from exts import db
# 存储 邮箱和 验证码的数据表映射的类
class EmailcodeModel(db.Model):
__table__ = "email_code"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
email = db.Column(db.String(100), nullable=False)
code = db.Column(db.String(100), nullable=False)
# 在项目 终端中使用 flask-migrate 命令, 更新数据表
flask db init (只需要执行一次)
flask db migrate 将ORM模型生成迁移脚步
flask db upgrade 将迁移脚步映射到数据库中
# auth.py
from flask import Blueprint, render_template
from exts import mail
from flask_mail import Message
from flask import request
import string
import random
@bp.route("/code/email")
def get_code():
# 1. 通过 /code/email/<email> 路径传参
# 2. /code/email?eamil=xxx@qq.com
email = request.args.get("email")
# 验证码, 随机的 4/6位 数字,字母组合
source = string.digits * 4
code = random.sample(source, 4)
code = "".join(code)
message = Message(subject="注册验证码", recipients=[email], body=f"您的验证码是{code}")
mail.send(message)
# 邮箱验证码需要存储起来,最后用户提交数据后,进行校验,因为验证码并不是特别重要的数据,
# 最好使用缓存来实现 例如 memcached/redis 可以指定多久将数据同步到 硬盘中
# 这里 先用数据库存储
email_code = EmailcodeModel(email=email, code=code)
db.session.add(email_code)
db.session.commit()
# 验证码使用 ajax 来发送请求, 返回内容要符合 RESTFUL API的规范
# {code: 200/400/500, message: "",data:{}}
return jsonify({"code": 200, "message": "", "data": None})
# 访问URL http://127.0.0.1:5000/auth/code/email?email=xxxxx@163.com
在 register.html 中 head block 占位的地方, 加载 js 文件, 通过 js 文件获取 register.html 页面 获取验证码的按钮, 给这个按钮绑定 向视图函数发送 获取验证码的事件
{% block head %}
<!-- src 属性在标签内-->
<script src="{{ url_for('static', filename='jquery/jquery.3.6.min.js') }}"> </script>
<script src="{{ url_for('static', filename='js/register.js') }}"> </script>
{% endblock %}
register.html 中 加载 绑定事件的 js 文件在 获取验证码按钮 的 html 内容的上方, html 文件是从上往下加载的
这导致 按钮还没有加载, 就要使用按钮的问题。
使用 jquery 的函数, jquery 的函数会在 整个网页的内容加载完成后, 再执行 函数
// register.js
// jquery 函数类型
$(function()){
}
// 整个网页加载完毕后, 再执行下面这个函数
$(function(){
// # 号, 根据 id 进行标签的选择,获取发送验证码的按钮
$("#captcha-btn").click(function(event){
// 阻止默认的事件提交整个表单, 获取验证码的按钮 在一个 form标签下,默认点击按钮,会
// 将整个表单的信息提交 给form 表单的 action 地址,这里不需要这样
event.preventDefault();
// 获取邮箱,找到邮箱的输入框,获取 text 属性
// $("#exampleInputEmail1") // 通过id 获取
// 通过 name 属性去获取输入框
let email = $("input[name='email']").val();
// alert(email);
//$ 就是一个 jquery 对象
$.ajax({
//http://127.0.0.1:5000 可以不写,默认向当前域名请求
url:"/auth/code/email?email=" + email,
method: "GET",
success: function(result){
// console.log(result);
let code = result['code'];
if(code == 200){
alert("邮箱验证码发送成功!");
}else{
alert(result['message']);
}
},
fail: function(error){
console.log(error);
}
})
});
})
// register.js
function bindEmailcodeClick(){
// # 号, 根据 id 进行标签的选择,获取发送验证码的按钮元素
$("#captcha-btn").click(function(event){
// this 代表 这个按钮对象, 加上 $ 符号, 将对象包装成jquery 对象
// 可以使用 text 来设置内容
//$(this).text()
var $this = $(this);
// 阻止默认的事件提交整个表单, 获取验证码的按钮 在一个 form标签下,默认点击按钮,会
// 将整个表单的信息提交 给form 表单的 action 地址,这里不需要这样
event.preventDefault();
// 获取邮箱,找到邮箱的输入框,获取 text 属性
// $("#exampleInputEmail1") // 通过id 获取
// 通过 name 属性去获取输入框
let email = $("input[name='email']").val();
// alert(email);
//$ 就是一个 jquery 对象
$.ajax({
//http://127.0.0.1:5000 可以不写,默认向当前域名请求
url:"/auth/code/email?email=" + email,
method: "GET",
success: function(result){
// console.log(result);
let code = result['code'];
if(code == 200){
// 点击验证码后,倒计时多少秒不能够点击
let countdown = 5;
// 开始倒计时之前, 取消按钮的点击事件
$this.off("click");
// 1000 ms
let timer = setInterval(function(){
$this.text(countdown);
countdown -= 1;
if(countdown <= 0){
// 清除定时器
clearInterval(timer);
// 将按钮的文字重新修改回来
$this.text("获取验证码");
// 倒计时结束的时候,重新绑定点击事件, 重新执行整个函数
bindEmailcodeClick();
}
}, 1000)
alert("邮箱验证码发送成功!");
}else{
alert(result['message']);
}
},
fail: function(error){
console.log(error);
}
})
});
}
// 整个网页加载完毕后, 再执行下面这个函数
$(function(){
bindEmailcodeClick();
});
点击 register页面 获取验证码速度较慢和 如下视图函数中发送邮件的的 I/ o 操作有关
视图函数默认的请求方法是 get 请求
@bp.route("/code/email")
def get_code():
# 1. 通过 /code/email/<email> 路径传参
# 2. /code/email?eamil=xxx@qq.com
email = request.args.get("email")
# 验证码, 随机的 4/6位 数字,字母组合
source = string.digits * 4
code = random.sample(source, 4)
code = "".join(code)
# I/O Input/Output 输入输出, 是一个比较耗时的操作,前端点击 发送验证码 按钮,执行完
# 发送验证码,ajax 请求 返回 success 才执行 后面的js 代码,使前端 发送验证码按钮点击后,反应较慢
# 解决方法:将发送邮件代码(也就是下面两行代码)放在队列里,另一个进程,不影响后续代码执行
message = Message(subject="注册验证码", recipients=[email], body=f"您的验证码是{code}")
mail.send(message)
# 邮箱验证码需要存储起来,最后用户提交数据后,进行校验,因为验证码并不是特别重要的数据,
# 最好使用缓存来实现 例如 memcached/redis 可以指定多久将数据同步到 硬盘中
# 这里 先用数据库存储
email_code = EmailcodeModel(email=email, code=code)
db.session.add(email_code)
db.session.commit()
# 验证码使用 ajax 来发送请求, 返回内容要符合 RESTFUL API的规范
# {code: 200/400/500, message: "",data:{}}
return jsonify({"code": 200, "message": "", "data": None})
6.后端注册表单验证器实现
注册信息验证,使用表单验证来做数据验证 flask-wtf, wtforms
在 blueprints 目录下创建 forms.py, 使用wtforms 和 自定义验证进行验证
# forms.py
import wtforms
# Email 中 依赖 email_validator 这个模块 , pip install email_validator
from wtforms.validators import Email, Length, EqualTo
from models import UserModel, EmailcodeModel
from exts import db
# Form: 主要用来验证前端提交的数据是否符合要求
class RegisterForm(wtforms.Form):
email = wtforms.StringField(validators=[Email(message="邮箱格式错误!")])
code = wtforms.StringField(validators=[Length(min=4, max=4, message="验证码格式错误!")])
username = wtforms.StringField(validators=[Length(min=3, max=20, message="用户名格式错误!")])
password = wtforms.StringField(validators=[Length(min=6, max=20, message="密码格式错误!")])
password_confirm = wtforms.StringField(validators=[EqualTo("password")])
# 自定义验证
# 1. 邮箱是否已经被注册
def validate_email(self, field):
email = field.data
user = UserModel.query.filter_by(email=email).first()
if user:
raise wtforms.ValidationError(message="该邮箱已经被注册!")
# 2. 验证码是否正确
def validate_code(self, field):
code = field.data
email = self.email.data
code_model = EmailcodeModel.query.filter_by(email=email, code=code).first()
if not code_model:
raise wtforms.ValidationError(message="邮箱或验证码错误!")
# else:
# # 优点: 验证码用过以后就被删除了,
# # 缺点: 数据库操作太频繁,影响性能
# db.session.delete(code_model)
# db.session.commit()
7.后端注册功能完成
完善register 视图函数, get 请求方式跳转到注册页面, post 请求方式, 提交数据, 首先使用表单验证器对
form 中的数据进行验证,数据验证成功后,将用户信息存储到user 数据库表中,
数据库用户密码不能存储明文,需要加密,可以使用 generate_password_hash
注册成功后,使用 redirect 重定向到 登录页面
注册失败,使用 redirect 重定向到 注册页面
重定向的时候,可以使用redircet(url_for(蓝图名.视图函数名))
from werkzeug.security import generate_password_hash
user = UserModel(email=email, username=username, password=generate_password_hash(password))
from flask import Blueprint, render_template, jsonify, redirect, url_for
from exts import mail, db
from flask_mail import Message
from flask import request
import string
import random
from models import EmailcodeModel, UserModel
from .forms import RegisterForm
from werkzeug.security import generate_password_hash
# GET: 从服务器获取数据
# POST:将客户端的数据提交给服务器
@bp.route("/register", methods=["GET", "POST"])
def register():
if request.method == "GET":
return render_template("register.html")
else:
# 验证 用户提交的邮箱和验证码是否对应且正确
# 表单验证, flask-wtf: 基于wtforms实现
# form 表单中 key: val 和 html 文件中输出框的name 属性和 填入的值对应
# 需要和 表单验证中的 验证 字段名一致
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=generate_password_hash(password))
db.session.add(user)
db.session.commit()
# return redirect("/auth/login")
return redirect(url_for("auth.login"))
else:
print(form.errors)
return redirect(url_for("auth.register"))
8.登录页面模版渲染完成
login.html 使用 extends 和 block 继承 父模版
将登录页面独有的内容放入 body block 占位中。
9.登录功能后端实现
和 注册功能一样,区分请求方式, get 请求和post 请求返回不同的内容,get 请求返回登录页面,post 请求如果登录成功 跳转到首页,登录失败也重定向到 登录页面。在数据库中存储的是加密后的密码, 如果进行密码 对比?
加密 时 使用 generate_password_hash(password),
对比时, 可以使用 check_password_hash(数据库存储的加密值, password)
from werkzeug.security import generate_password_hash, check_password_hash
user = UserModel(email=email, username=username, password=generate_password_hash(password))
result = check_password_hash(user.password, password)
验证登录forms 中的内容, 在 forms.py 中写一个login 验证类
#\auth\forms.py
import wtforms
from wtforms.validators import Email, Length, EqualTo
from models import UserModel, EmailcodeModel
class LoginForm(wtforms.Form):
email = wtforms.StringField(validators=[Email(message="邮箱格式错误!")])
password = wtforms.StringField(validators=[Length(min=6, max=20, message="密码格式错误!")])
登录成功后, 如何保持登录状态, 需要使用到 cookie 和session
@bp.route("/login", methods=["GET", "POST"])
def login():
if request.method == "GET":
return render_template("login.html")
else:
# 使用表单验证 登录表单
form = LoginForm(request.form)
if form.validate():
email = form.email.data
password = form.password.data
user = UserModel.query.filter_by(email=email).first()
if not user:
print("邮箱在数据库中不存在!")
return redirect(url_for("auth.login"))
if check_password_hash(user.password, password):
# 保持登录状态
# cookie 中不适合存储太多的数据, 只适合存储少量的数据
# cookie 一般用来存放登录授权的东西
# flask 中的session, 是经过加密后存储在 cookie 中的
# seesion 加密需要盐,在config.py 中 SECRET_KEY = "dsfsdfdsfsdytryrt123"
session["user_id"] = user.id
return redirect("/")
else:
print("密码错误!")
return redirect(url_for("auth.login"))
else:
print(form.errors)
return redirect(url_for("auth.login"))
10.两个钩子函数
一般情况下,客户端向服务端发送请求,是直接访问 视图函数的,现在我们需要在访问视图函数前做一些事情
flask 中有一些钩子函数 :例如 before_request/ before_first_request/after_request/ 等等
hook
# blueprints/auth.py 下、auth/login 视图函数中保存了用户的信息
user = UserModel.query.filter_by(email=email).first()
session["user_id"] = user.id
#app.py
@app.before_request
def my_before_request():
# flask 内部自己做了加密和解密
user_id = session.get("user_id")
if user_id:
user = UserModel.query.get(user_id)
setattr(g, 'user', user)
else:
setattr(g, 'user', None)
# 这里返回什么, 在所有的html模版中都会有相关的变量
# 上下文处理器
@app.context_processor
def my_context_processor():
return {"user": g.user}
11.登录和非登录状态切换
导航条的显示是在 base.html 中做的,需要更改 base.html
修改如上图,红框内的html 代码逻辑
# base.html
{% if user %}
<li class="nav-item">
<span class="nav-link">{{ user.username }}</span>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for("auth.logout") }}">退出登录</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for("auth.login") }}">登录</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for("auth.register") }}">注册</a>
</li>
{% endif %}
在 auth.py 中添加一个 logout 的视图函数,点击 退出登录的时候,执行 该视图函数
@bp.route("/logout")
def logout():
# 清除 session信息, 也就清除了登录状态
session.clear()
return redirect("/")
12.发布问答页面渲染
修改 public_question.html 为 Jinja2 的模版继承方式
13.发布问答后端功能实现
写一个视图函数来存储发布问答的内容,使用 wtforms 进行表单验证, 使用ORM 创建相关的数据表类,映射到数据库。
# models.py
class QuestionModel(db.Model):
__tablename__ = "question"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
title = db.Column(db.String(100), nullable=False)
content = db.Column(db.Text, nullable=False)
create_time = db.Column(db.DateTime, default=datetime.now)
# 外键
author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
author = db.relationship(UserModel, backref="questions")
# 在项目的 终端页面使用 flask-migrate 映射模型类到数据库表
"""
flask db init(只需要在第一次映射时执行)
flask db migrate
flask db upgrade
""""
# blueprints/forms.py
# 问答表单验证
import wtforms
from wtforms.validators import Email, Length, EqualTo
class QuestionForm(wtforms.Form):
title = wtforms.StringField(validators=[Length(min=3, max=100, message="标题格式错误!")])
content = wtforms.StringField(validators=[Length(min=3, message="内容格式错误!")])
# qa.py 发布问答视图函数
from flask import Blueprint, request, render_template, g, redirect, url_for
from .forms import QuestionForm
from models import QuestionModel
from exts import db
bp = Blueprint("qa", __name__, url_prefix="/")
@bp.route("/public", methods=["GET", "POST"])
def public_question():
if request.method == "GET":
return render_template("public_question.html")
else:
# 表单验证
form = QuestionForm(request.form)
# 如果表单验证成功
if form.validate():
title = form.title.data
content = form.content.data
# 这里有一个问题,没有登录的情况下,可以访问这个页面, 但是g.user 是None,不符合要求
# 后面要在登录状态下才可以访问 发布问答页面
question = QuestionModel(title=title, content=content, author=g.user)
db.session.add(question)
db.session.commit()
return redirect("/")
else:
print(form.errors)
return redirect(url_for("qa.public"))
14.登录装饰器的实现
可以在 public 视图函数这里 判断,如果g.user 为空,重定向到auth.login页面
@bp.route("/public", methods=["GET", "POST"])
def public_question():
if not g.user:
return redirect(url_for("auth.login"))
if request.method == "GET":
return render_template("public_question.html")
else:
# 表单验证
form = QuestionForm(request.form)
# 如果表单验证成功
if form.validate():
title = form.title.data
content = form.content.data
question = QuestionModel(title=title, content=content, author=g.user)
db.session.add(question)
db.session.commit()
return redirect("/")
else:
print(form.errors)
return redirect(url_for("qa.public"))
在一个页面可以这样写,但是如果 10 个页面都需要登录后,才能访问, 那就需要把判断 g.user 是否为空写 10 次,那么使用 装饰器就很方便了。
# 在项目目录下创建一个 decorators.py 存放写好的装饰器
# decorators.py
from functools import wraps
from flask import g, redirect, url_for
def login_required(func):
# 保留原函数的信息
@wraps(func)
def inner(*args, **kwargs):
if g.user:
return func(*args, **kwargs)
else:
return redirect(url_for("auth.login"))
return inner
# qa.py
# 在需要登录才能访问的页面加上这个装饰器 例如:
@bp.route("/public", methods=["GET", "POST"])
@login_required
def public_question():
pass
base.html 中
发布问答按钮点击时, 跳转到发布问答页面
<li class="nav-item">
<a class="nav-link" href="{{ url_for("qa.public_question") }}">发布问答</a>
</li>
15.首页问答列表渲染完成
继承 base.html , 填充 block 内容
首页视图函数指向 index.html, 从问答信息 数据表中倒序查询 所有的问答信息(问答信息过多的话, 需要分页)
# qa.py
@bp.route("/")
def index():
questions = QuestionModel.query.order_by(QuestionModel.create_time.desc()).all()
return render_template("index.html", questions=questions)
在index.html 中 使用 jinja2 for 循环 展示每条问答信息
16.问答列表页渲染
继承base.html, 填充 block 内容
将 detail 中的详情信息改成 根据 question 不同,展示不同的信息
通过点击 index.html 中的 问答信息 title , 跳转到问答详情页
<!--index.html-->
<!--通过 href 属性 和 url_for跳转到问答详情页,问答详情页需要接收 问答信息的 id 参数来获取 显示哪条信息,需要传递参数,具体如下 -->
<div class="question-title"><a href="{{ url_for("qa.qa_detail", qa_id=question.id) }}">{{ question.title }}</a></div>
#qa.py
@bp.route("/qa/detail/<qa_id>")
def qa_detail(qa_id):
question = QuestionModel.query.get(qa_id)
return render_template("detail.html", question=question)
17.答案模型创建
在 models.py 中 创建问答 答案的数据库表 映射模型
# 问答answer数据表模型
class AnswerModel(db.Model):
__tablename__ = "answer"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
content = db.Column(db.Text, nullable=False)
create_time = db.Column(db.DateTime, default=datetime.now)
# 外键
question_id = db.Column(db.Integer, db.ForeignKey("question.id"))
author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
# 关系, 反向引用
# 使用创建时间进行排序,时间越大(离现在越近)排在前面。
question = db.relationship(QuestionModel, backref=db.backref("answers", order_by=create_time.desc()))
author = db.relationship(UserModel, backref="answers")
"""
使用 flask-migrate 将 数据表模型 映射到数据库
flask db init (只需要在第一次映射的时候 执行)
flask db migrate
flask db upgrade
"""
18.发布答案功能完成
在 form.py 中定义 发布答案的表单验证类, 写一个视图函数来将发布的答案存储到数据库中
# form.py
class AnswerForm(wtforms.Form):
content = wtforms.StringField(validators=[Length(min=3, message="内容格式错误!")])
question_id = wtforms.IntegerField(validators=[InputRequired(message="必须要传入问题id!")])
# qa.py
# 给视图 函数指定请求方式的两种方法
# @bp.route("/answer/public", methods=["POST"])
@bp.post("/answer/public")
@login_required
def public_answer():
form = AnswerForm(request.form)
if form.validate():
content = form.content.data
question_id = form.question_id.data
answer = AnswerModel(content=content, question_id=question_id, author_id=g.user.id)
db.session.add(answer)
db.session.commit()
return redirect(url_for("qa.qa_detail", qa_id=question_id))
else:
print(form.errors)
return redirect(url_for("qa.qa.detail", qa_id=request.form.get("question_id")))
修改 问答 detail.html 中的 form 表单的 action, 即 表单的 提交 视图函数
通过 question.answers 反向引用, 获得一个问答问题的的所有评论答案, 并利用 jinja2 for 循环展示在detail.html 网页中 , 下图 42 行 answer.content 忘了 加 {{ }} 了, 正确应该是 {{ answer.content }}
19.搜索功能的实现
首先根据 前端传递的参数(即搜索内容), 写一个search视图函数, 将符合要求的问答从数据库中搜索出来,然后传递给 index.html 进行 循环展示
@bp.route("/search")
def search():
# /search?q=flask
# /search/<q>
# post请求, request.form
q = request.args.get("q")
questions = QuestionModel.query.filter(QuestionModel.title.contains(q).all)
return render_template("index.html", questions=questions)
搜索 按钮是在 base.html 中的, 点击 搜索按钮,调用 search 视图函数,input 框的输入值,即为 q
20.总结
# 通过这个项目学习了什么?
# url 传参
# 邮件发送
# ajax
# orm 和数据库
# Jinja2 模块
# cookie 和 session 原理
# flask 中的 hook 函数 ,全局变量 g
# wtforms 表单验证
# flask 蓝图