问答平台项目

1.项目的基础架构

把不同类型功能的代码, 放到不同的文件中,可以使项目架构更加的明确,下面是一种参考的方式

在项目文件夹下创建 config.py 用来存放 项目中的配置信息,创建 exts.py 放置一些扩展文件(例如一些第三方的模块,flask-sqlalchemy,等 创建 models.py 用来存放 sqlalchemy 中 ORM 中 数据表 对应的类。

问答平台项目-flask_flask

在 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()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

模块之间的循环引用问题

另外使用 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
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

问答平台项目-flask_html_02

"""
为了避免循环引用的问题, 可以把 下面两行代码放到 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()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

问答平台项目-flask_html_03

# 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()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
# exts.py
# 放一些扩展文件,第三方模块
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
  • 1.
  • 2.
  • 3.
  • 4.
# models.py

from exts import db

class UserModel(db.Model):
    pass
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

蓝图(blueprint)

将所有的视图函数都写在 app.py 中,当项目 较大的时候不容易维护,

将不同类型的视图函数模块化,一类视图函数放在一个 py 文件中。

可以在项目文件夹下创建一个存放蓝图相关的python package, 在其中 创建 不同的 视图函数模块

在不同模块中定义不同的蓝图对象, 在 app.py 中 导入, 并注册

问答平台项目-flask_验证码_04

# auth.py
from flask import Blueprint

bp = Blueprint("auth", __name__, url_prefix="/auth")


@bp.route("/login")
def login():
    pass
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
# qa.py
from flask import Blueprint

bp = Blueprint("qa", __name__, url_prefix="/")


@bp.route("/")
def index():
    pass
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
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()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.

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
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

在 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)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

在 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()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.

使用 flask_migrate 进行 Python 类模型 和 数据表的映射

# 在项目 终端中执行
flask db init
flask db migrate
flask db upgrade
  • 1.
  • 2.
  • 3.
  • 4.

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>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
# 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")
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

模版文件中注册页面和 登录页面等的导航条和底部是一样的,可以写一个 父模版文件,使用模版继承来使模版文件更加简化

在 父模版中占位 用

{% block block名字 %} {% endblock %}
  • 1.

问答平台项目-flask_验证码_05

问答平台项目-flask_验证码_06

4.Flask 发送邮件功能实现

注册功能的第一步,就是给用户输入的邮箱发送验证码,我们使用 flask-mail 发送验证码

pip install flask-mail
  • 1.

想要发送邮件, 需要有一个邮箱服务器,可以自己搭建, 也可以使用第三方的邮件服务器, 例如 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"
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

在 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 "邮箱发送成功"
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

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  将迁移脚步映射到数据库中
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
# 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
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.

在 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 %}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

register.html 中 加载 绑定事件的 js 文件在 获取验证码按钮 的 html 内容的上方, html 文件是从上往下加载的

这导致 按钮还没有加载, 就要使用按钮的问题。

使用 jquery 的函数, jquery 的函数会在 整个网页的内容加载完成后, 再执行 函数

// register.js
// jquery 函数类型
$(function()){

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
// 整个网页加载完毕后, 再执行下面这个函数
$(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);
            }
        })
    });
})
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
// 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();
});
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.

点击 register页面 获取验证码速度较慢和 如下视图函数中发送邮件的的 I/ o 操作有关

问答平台项目-flask_验证码_07

视图函数默认的请求方法是 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})
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.

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()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.

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))
  • 1.
  • 2.
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"))
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.

8.登录页面模版渲染完成

login.html 使用 extends 和 block 继承 父模版

问答平台项目-flask_flask_08

将登录页面独有的内容放入 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)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

验证登录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="密码格式错误!")])
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

登录成功后, 如何保持登录状态, 需要使用到 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"))
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

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}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

11.登录和非登录状态切换

导航条的显示是在 base.html 中做的,需要更改 base.html

问答平台项目-flask_验证码_09

修改如上图,红框内的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 %}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

在 auth.py 中添加一个 logout 的视图函数,点击 退出登录的时候,执行 该视图函数

@bp.route("/logout")
def logout():
    # 清除 session信息, 也就清除了登录状态
    session.clear()
    return redirect("/")
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

12.发布问答页面渲染

修改 public_question.html 为 Jinja2 的模版继承方式

问答平台项目-flask_html_10

问答平台项目-flask_验证码_11

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
""""
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
# 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="内容格式错误!")])
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
# 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"))
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.

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"))
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

在一个页面可以这样写,但是如果 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
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.

base.html 中

发布问答按钮点击时, 跳转到发布问答页面

<li class="nav-item">
    <a class="nav-link" href="{{ url_for("qa.public_question") }}">发布问答</a>
</li>
  • 1.
  • 2.
  • 3.

15.首页问答列表渲染完成

继承 base.html , 填充 block 内容

问答平台项目-flask_html_12

首页视图函数指向 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)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

在index.html 中 使用 jinja2 for 循环 展示每条问答信息

问答平台项目-flask_验证码_13

16.问答列表页渲染

继承base.html, 填充 block 内容

问答平台项目-flask_验证码_14

将 detail 中的详情信息改成 根据 question 不同,展示不同的信息

问答平台项目-flask_html_15

通过点击 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>
  • 1.
  • 2.
  • 3.
  • 4.
#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)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

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
"""
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

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!")])
  • 1.
  • 2.
  • 3.
  • 4.
# 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")))
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

修改 问答 detail.html 中的 form 表单的 action, 即 表单的 提交 视图函数

问答平台项目-flask_验证码_16

通过 question.answers 反向引用, 获得一个问答问题的的所有评论答案, 并利用 jinja2 for 循环展示在detail.html 网页中 , 下图 42 行 answer.content 忘了 加 {{ }} 了, 正确应该是 {{ answer.content }}

问答平台项目-flask_flask_17

问答平台项目-flask_html_18

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)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

搜索 按钮是在 base.html 中的, 点击 搜索按钮,调用 search 视图函数,input 框的输入值,即为 q

问答平台项目-flask_html_19

20.总结

# 通过这个项目学习了什么?

# url 传参
# 邮件发送
# ajax
# orm 和数据库
# Jinja2 模块
# cookie 和 session 原理
# flask 中的 hook 函数 ,全局变量 g
# wtforms  表单验证
# flask 蓝图
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

项目代码地址:  https://github.com/Red-brief/Q-A-project-flask