Flask微框架

Flask速成

Flask是一个基于Python的Web开发微框架

pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple/
pip install flask

在这里插入图片描述

框架

Project
    ├─static
    ├─templates
    └─app.py

'''app.py'''
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello World!'

if __name__ == '__main__':
    app.run()

from flask import Flask

'''
使用Flask类创建一个对象
__name__: 代表当前app.py这个模块
          1.出现bug,可以帮助快速定位
          2.对于寻找模板文件,有一个相对路径
'''

app = Flask(__name__)



#创建一个路由和视图函数的映射
@app.route('/')
def hello_world():
    return 'Hello World!'


if __name__ == '__main__':
    app.run()

Debug、Host、Port的配置

Debug模式 :默认情况关闭,开启后可以实时更新代码修改后的效果,浏览器上可以看到错误信息。PyCharm configuration可以直接关闭,或修改代码:

if __name__=='__main__':
    app.run(debug=True)

修改host、port :设置监听的主机,让其他设备可以访问本机项目。比如本机在局域网中IP为192.168.10.187,运行时设置host为0.0.0.0,局域网其他设备可通过192.168.10.187访问。

无线局域网适配器 WLAN:
   连接特定的 DNS 后缀 . . . . . . . : lan
   IPv4 地址 . . . . . . . . . . . . : 192.168.10.187
   子网掩码  . . . . . . . . . . . . : 255.255.255.0
   默认网关. . . . . . . . . . . . . : 192.168.10.1
if __name__ == '__main__':
    app.run(debug=True, host="0.0.0.0", port=9000)
--host=0.0.0.0 --port=9000

在这里插入图片描述

URL与视图的映射

from flask import Flask, request


app = Flask(__name__)


@app.route('/')
def hello_world():
    return 'Hello World!'


@app.route("/profile")
def profile():
    return "个人中心"


@app.route("/blog/list")
def blog_list():
    return "博客列表"


# 带参数的url:将参数固定到了path中
@app.route("/blog/<int:blog_id>")
def blog_detail(blog_id):
    return "您访问的博客是:%s" % blog_id


# 查询字符串的方式传参,更加灵活,定义url时不用写参数
# /book/list:返回第一页的数据
# /book/list?page=2:获取第二页的数据
@app.route('/book/list')
def book_list():
    # args:参数
    # request.args:类似于字典的一种类型
    page = request.args.get("page", default=1, type=int)
    return f"您获取的是第{page}的图书列表!"  # Python f字符串可以用{变量}直接替换字符串中相应位置


if __name__ == '__main__':
    app.run(debug=True, host="0.0.0.0", port=9000)

Jinja2模板渲染

下面的演示代码:https://github.com/Dr-LeopoldFitz/Flask_Jinja2

模板渲染

渲染HTML通常会交给模板引擎来做,Flask中默认配套的模板引擎是Jinja2。在视图函数中,使用render_template("") 函数渲染html。

render_template默认会从当前项目的templates文件夹下寻找html文件,渲染后返回给浏览器。

HTML文件中,有些数据是需要动态加载的,不能直接在html中写死,称之为渲染变量
一般在视图函数中把数据先提取好,在使用render_template渲染的时候将渲染变量传给模板,模板再读取渲染出来。使用格式为{{渲染变量}} ,字典的键和对象的属性,在模板中都可以通过点.的形式访问,如下。

class User:
    def __init__(self, username, email):
        self.username = username
        self.email = email
     
@app.route('/')
def func():
    # 对象
    usr = User(username="Hive", email="hive@qq.com")
    # 字典
    person = {
        "username": "Hive",
        "email": "hive@qq.com"
    }
    return render_template("index.html", user=usr , person=psn)


[INDEX.HTML]
<div> {{user.username}} </div>
<div> {{person.username}}   {{person['username']}} </div>
在这里插入图片描述

过滤器

在Python中如果需要对某个变量进行处理可以通过函数来实现。在模板中如果需要对某个变量先进行处理再渲染,通过过滤器来实现。过滤器本质上也是函数,在模板中使用的方式是通过管道符号|来调用。
例如有个字符串类型变量name, 想要获取他的长度,则可以通过{{name|length}}来获取,Jinja2会把name当做第一个参数传给length过滤器底层对应的函数。length是Jinja2 内置好的过滤器,Jinja2中内置了许多过滤器,如果内置过滤器不满足需求,还可以自定义过滤器。

过滤器本质上是Python的函数,他会把被过滤的值当做第一个参数传给这个函数,函数经过一些处理后再返回新的值。在过滤器函数写好后,可以通过@app.template_filter装饰器或者是app.add_template_filter()函数来把函数注册成Jinja2能用的过滤器。
这里以注册一个时间格式化的过滤器为例,来说明自定义过滤器的方法:

from flask import Flask, render_template
from datetime import datetime
def datetime_format(value, format="%Y年%m月%d日 %H:%M"):
    return value.strftime(format)

app.add_template_filter(datetime_format, "dformat")

class User:
    def __init__(self, username, email):
        self.username = username
        self.email = email
        
@app.route("/filter")
def filter_demo():
    user = User(username="Hive", email="hive@qq.com")
    mytime = datetime.now()
    return render_template("filter.html", user=user, mytime=mytime)

[FILTER.HTML]
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>过滤器</title>
</head>
<body>
<div>{{ user.username }}-{{ user.username|length }}</div>
<div>{{ mytime|dformat }}</div>
<div>{{ mytime|dformat("%B-%Y") }}</div>
</body>
</html>

上面定义了一个datetime_format函数,第一个参数是需要被处理的值,第二个参数是时间的格式,并且指定了一个默认值。通过app.add_template_filterdatetime_format(datetime_format,"dformat")注册成了过滤器,并且这个过滤器的名字叫dformat。
在模板文件中,就可以这样使用了:

{{ mytime|dformat }}
{{ mytime|dformat("%B-%Y") }}

如果app.add_template_filter()没有传第二个参数,默认使用函数名作为过滤器的名称。
在这里插入图片描述

Jinja2内置过滤器

https://jinja.palletsprojects.com/en/3.0.x/templates/#builtin-filters

abs()float()lower()round()tojson()
attr()forceescape()map()safe()trim()
batch()format()max()select()truncate()
capitalize()groupby()min()selectattr()unique()
center()indent()pprint()slice()upper()
default()int()random()sort()urlencode()
dictsort()join()reject()string()urlize()
escape()last()rejectattr()striptags()wordcount()
filesizeformat()length()replace()sum()wordwrap()
first()list()reverse()title()xmlattr()

控制语句

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>控制</title>
</head>
<body>
{% if age>18 %}
    <div>您已经满18岁,可以进入!</div>
{% elif age==18 %}
    <div>您刚满18岁,需要父母陪同才能进入!</div>
{% else %}
    <div>您未满18岁,不能进入!</div>
{% endif %}


{% for book in books %}
    <div>图书名称:{{ book.name }},图书作者:{{ book.author }}</div>
{% endfor %}
</body>
</html>

在这里插入图片描述

模板继承

一个网站中大部分网页的模块是重复的,比如顶部的导航栏,底部的备案信息。通过模板继承,可以把一些重复性的代码写在父模板中,子模板继承父模板后再分别实现自己页面的代码。
一个父模板base.html的例子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}{% endblock %}</title>
</head>
<body>
<ul>
    <li><a href="/">返回首页</a></li>
</ul>
{% block body %}
{% endblock %}
<footer>====================这是底部栏====================</footer>
</body>
</html>

以上父模板中,编写好了网页的底部栏结构,针对子模板需要重写的地方,则定义成了block,子模板在继承了父模板后重写对应block的
代码,即可完成子模板的渲染。这里继承base.html实现一个新的html 文件,代码如下:

{% extends "base.html" %}

{% block title %}
    子模板
{% endblock %}

{% block body %}
    子模板的body
{% endblock %}

加载静态文件

静态文件是默认存放在当前项目static文件夹中的CSS、JavaScript和图片文件等静态资源,如果想要修改静态文件存放路
径,可以在创建Flask对象的时候,设置static_folder参数:
app=Flask(__name__,static_folder='D:\static')
在html文件中,可以通过url_for加载静态文件:
<link href="{{ url_for('static',filename='about.css') }}">
第一个参数static表示构建Flask内置的static视图这个URL,第二个filename可以传递文件名或者文件路径,路径是相对于static 或者static_ folder 参数自定义的路径。以上代码在被模板渲染后,会被解析成:<link href="/static/about.css">

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
    <script src="{{ url_for('static', filename='js/my.js') }}"></script>
</head>
<body>
<img src="{{ url_for('static', filename='images/xiaohei.jpg') }}" alt="" width="80%">
</body>
</html>
在这里插入图片描述

连接MySQL

Flask想要操作数据库,必须要先安装Python操作MySQL的驱动。Python目前有以下MySQL驱动包
(1) MySQL-python:也就是MySQLdb。是对C语言操作MySQL数据库的一个简单封装。只支持Python2。
(2) mysqlclient:MySQL_python的另外一个分支。是目前为止执行效率最高的驱动,但是安装的时候容易因为环境问题出错。
(3) pymysql:纯Python实现的一个驱动。执行效率不如mysqlclient。可以和Python代码无缝衔接。
(4) mysl-connector-python: MySQL官方推出的纯Python连接MySQL的驱动,执行效率最低。
这里选择用pymysql作为驱动程序。
pip install pymysql

在这里插入图片描述

Flask-SQLAlchemy基本使用

在Flask中,我们很少会使用pymysql直接写原生SQL语句去操作数据库,更多的是通过SQLAlchemy提供的ORM技术,类似于操作普通Python对象一样实现数据库的增删改查操作,Flask-SQLAlchemy是对SQLAlchemy的一个封装,使得在Flask中使用SQLAlchemy更加方便。
Flask-SQLAlchemy需要单独安装,只要安装了Flask SQLAlchemy, SQLAlchemy 会自动安装。
pip install flask-sqlalchemy
SQLAlchemy类似于Jinja2,可以独立于Flask使用。SQLAlchemy官方文档: https://www.sqlalchemy.org/

连接MySQL

使用Flask-SQLAlchemy操作数据库之前,要先创建一个由Flask-SQLAlchemy提供的SQLAlchemy类的对象。在创建这个类的时候,要传入当前的app,还需要在app.config中设置SQLALCHEMY_DATABASE_URI来配置数据库的连接,示例代码如下:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy


app = Flask(__name__)

# MySQL所在的主机名
HOSTNAME = "127.0.0.1"
# MySQL监听的端口号,默认3306
PORT = 3306
# 连接MySQL的用户名,读者用自己设置的
USERNAME = "root"
# 连接MySQL的密码,读者用自己的
PASSWORD = "123456"
# MySQL上创建的数据库名称
DATABASE = "flask_test"

#utf8mb4是对原视utf8的一种优化编码
app.config['SQLALCHEMY_DATABASE_URI'] = f"mysql+pymysql://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}?charset=utf8mb4"


if __name__ == '__main__':
    app.run()

# 在app.config中设置好连接数据库的信息,
# 然后使用SQLAlchemy(app)创建一个db对象
# SQLAlchemy会自动读取app.config中连接数据库的信息
db = SQLAlchemy(app)

# 测试是否连接成功
with db.engine.connect() as conn:
    rs = conn.execute("select 1")
    print(rs.fetchone()) # (1,)

ORM模型与表的映射

对象关系映射(Object Relationship Mapping),简称ORM,是一种可以用面向对象的方式来操作关系型数据库的技术,具有可以映射到数据库表能力的Python类称之为ORM模型。一个ORM模型与数据库中一个表相对应,ORM模型中的每个类属性分别对应表的每个字段,ORM模型的每个实例对象对应表中每条记录。

以下用Flask SQLAlchemy来创建一个User模型,示例代码如下:

class User(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.String(100), nullable=False)

#把所有ORM模型映射进数据库中,生成相应的表
with app.app_context():
    db.create_all()

类User继承自db.Model, 所有ORM模型必须是db.Model的直接或者间接子类。然后通过__tablename__属性, 指定User模型映射到数据库中表的名称。接着定义了三个db.Column类型的类属性,分别是id、usermame、password,只有使用db.Column定义的类属性,才会被映射到数据库表中成为字段。
在User模型中,id是db.Integer类型,并且传递primary_key=True参数来指定id作为主键,传递autoincrement=True来设置id为自增长。接下来分别指定的usermame和password类型为db.String(在数据库中表现为varchar类型),并且指定其最大长度为100,传递nullable=False设置不可为空

ORM模型的CRUD操作

使用ORM进行CRUD(Create、Read、Update、Delete) 操作,需要先把操作添加到会话中,通过db.session可以获取到会话对象。会话对象只是在内存中,如果想要把会话中的操作提交到数据库中,需要调用db.session.commit()操作,如果想要把会话中的操作回滚,则可以通过db.session.rollback()实现。

1.Create操作
先使用ORM模型创建一个对象,然后添加到会话中,再进行commit操作即可,示例代码如下:

@app.route("/user/add")
def add_user():
    # 1. 创建ORM对象
    user = User(username="Hive", password='111111')  # id是一个自增长的主键,可以不赋值
    # 2. 将ORM对象添加到db.session中
    db.session.add(user)
    # 3. 将db.session中的改变同步到数据库中
    db.session.commit()
    return "用户创建成功!"

@app.route("/article/add")
def article_add():
    article1 = Article(title="冰与火之歌", content="Winter is Coming...")
    article1.author = User.query.get(2)

    article2 = Article(title="血与火", content="House of the Dragon...")
    article2.author = User.query.get(2)

    db.session.add_all([article1, article2])
    db.session.commit()
    return "文章添加成功!"

2.Read操作
ORM模型都是继承自db.Model,db.Model内置的query属性有许多方法,可以分为两大类,分别是过滤方法提取方法

示例代码如下:

@app.route("/user/query")
def query_user():
    # 1. get查找:根据主键查找
    user = User.query.get(1)
    print(f"{user.id}: {user.username}-{user.password}")

    # 2. filter_by查找
    # Query:类似于列表的一种类型
    users = User.query.filter_by(username="Hive")
    for user in users:
        print(user.username)
    return "数据查找成功!"

3.Update操作

@app.route("/user/update")
def update_user():
    user = User.query.filter_by(username="Hive").first()
    user.password = "000000"
    # 不需要db.session.add
    db.session.commit()
    return "数据修改成功!"

4.Delete操作

@app.route('/user/delete')
def delete_user():
    # 1. 查找
    user = User.query.get(1)
    # 2. 从db.session中删除
    db.session.delete(user)
    # 3. 将db.session中的修改,同步到数据库中,不需要db.session.add
    db.session.commit()
    return "数据删除成功!"

外键

关系型数据库多个表之间可以建立关系。表关系建立的前提,是通过数据库层面的外键实现的。
外键是数据库层面的技术,在Flask-SQLAlchemy中支持创建ORM模型的时候指定外键,创建外键是通过db.ForeignKey实现的。
比如创建一个Article表,这个表有一个author_ id字段,通过外键引用user表的id字段,Article的模型代码如下:

class Article(db.Model):
    __tablename__ = "article"
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    title = db.Column(db.String(200), nullable=False)
    content = db.Column(db.Text, nullable=False)

    # 添加作者的外键
    author_id = db.Column(db.Integer, db.ForeignKey("user.id"))

建立关系
通过外键,实际上已经建立起一对多的关系。但是只建立外键,通过Article的对象,还是无法直接获取到author_id引用的那个User对象。为了达到操作ORM对象就跟操作普通Python对象一样, Flask-SQL Alchemy提供了db.relationship来引用外键所指向的那个ORM模型。
通过代码author=db.relationship("User")添加一个author属性,这个属性通过db.relationship与User模型建立了联系,以后通过Article的实例对象访问author的时候,比如article.author,那么Flask SQLAlchemy会自动根据外键author_id从user表中寻找数据,并形成User模型实例对象。

author = db.relationship("User")

建立双向关系

建立关系后的Article模型可以通过author属性访问到对应的User实例对象。但是User实例对象无法访问到和他关联的所有Article实例对象。为了实现双向关系绑定,还需要在User模型上添加db.relationship("Articles“),并且在User模型和Article模型双方的db.relationship上都添加一个back_populates参数,用于绑定对方访问自己的属性,示例代码如下:

class User(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.String(100), nullable=False)
    articles = db.relationship("Article", back_populates="author")  # 绑定到Article的author


class Article(db.Model):
    __tablename__ = "article"
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    title = db.Column(db.String(200), nullable=False)
    content = db.Column(db.Text, nullable=False)

    author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
    author = db.relationship("User", back_populates="articles")  # 绑定到User的articles

这种方式很复杂,下面推荐一种简单的方式:

class User(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.String(100), nullable=False)


class Article(db.Model):
    __tablename__ = "article"
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    title = db.Column(db.String(200), nullable=False)
    content = db.Column(db.Text, nullable=False)

    author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
    # backref会自动给User模型添加一个articles的属性,用来获取文章列表,即反向查找
    author = db.relationship("User", back_ref="articles")
    
@app.route("/article/query")
def query_article():
    user = User.query.get(2)
    for article in user.articles:
        print(article.title)
    return "该作者所有文章查找成功!"

flask-migrate迁移ORM模型

db.create_all()只会识别新增了哪些模型并映射,但如果某模型字段发生变化,db.create_all()无法更新映射。
使用flask-migrate迁移模型可以解决上述问题。

ORM模型映射成表的三步命令
1.flask db init:只需要执行一次,生成migrations文件夹,内含迁移脚本
2.flask db migrate:识别ORM模型的改变,生成迁移脚本
3.flask db upgrade:运行迁移脚本,同步到数据库中

pip install flask-migrate

在这里插入图片描述

db = SQLAlchemy(app)
migrate = Migrate(app, db)

项目根路径下在终端输入以下3条命令:

flask db init
flask db migrate
flask db upgrade

注意:若电脑中安装有Anaconda上的其他虚拟环境,可能会导致上述三条命令调用失败,原因是环境冲突调用到了Anaconda上的flask

PS D:\Center\Code\Flask_Jinja2> flask db init
Usage: flask [OPTIONS] COMMAND [ARGS]...
Try 'flask --help' for help.

Error: No such command 'db'.

解决方案如下:

D:\Software\Python\Scripts\flask.exe db init
D:\Software\Python\Scripts\flask.exe db migrate
D:\Software\Python\Scripts\flask.exe db upgrade

在这里插入图片描述

在这里插入图片描述

项目-问答平台

环境搭建

./pip.exe install flask_mail
./pip.exe install wtforms
./pip.exe install email_validator

项目结构

PS D:\> tree QAPlatform-Flask /f
D:\QAPLATFORM-FLASK
│  app.py
│  config.py
│  decorators.py
│  exts.py
│  models.py
│
├─blueprints
│      auth.py
│      forms.py
│      qa.py
│      __init__.py
│
├─static
│  ├─bootstrap
│  │      bootstrap.4.6.min.css
│  │
│  ├─css
│  │      detail.css
│  │      index.css
│  │      init.css
│  │
│  ├─images
│  │      avatar.jpg
│  │
│  ├─jquery
│  │      jquery.3.6.min.js
│  │
│  └─js
│          register.js
│
└─templates
        base.html
        detail.html
        index.html
        login.html
        public_question.html
        register.html

创建一个config.py用来存储配置相关的信息。在app.pyimport config后使用app.config.from_object(config)将配置文件config.py与flask项目进行绑定。auth.py用来管理用户授权相关。

创建一个exts.py用来管理扩展。比如用来存放db对象。exts.py存在的意义就是为了解决循环引用的问题。

循环引用问题:

ORM模型一般放在models.py里。如下图,app.py需要引用models.py中的用户自定义的继承自db.Model的模型,models.py又需要app.py中的实例化SQLAlchemy对象db,故你需要我,我需要你,造成了循环引用的问题。

为了解决这个问题,引入exts.py专门存放实例化对象db,这样app.pymodels.py就不再存在循环引用的问题了,如下图。
在这里插入图片描述

app.pyimport config后使用app.config.from_object(config)将配置文件config.py与flask项目进行绑定。
在这里插入图片描述

blueprints包(Python Package)用来做模块化,存放视图,避免所有url视图全部写在app.py中导致臃肿。blueprints包下创建用于用户相关的auth.py模块和问答相关的qa.py

数据库创建和连接

在这里插入图片描述

[config.py]

# 数据库的配置信息
HOSTNAME = '127.0.0.1'
PORT = '3306'
DATABASE = 'qaplatform'
USERNAME = 'root'
PASSWORD = '123456'
DB_URI = 'mysql+pymysql://{}:{}@{}:{}/{}?charset=utf8mb4'.format(USERNAME, PASSWORD, HOSTNAME, PORT, DATABASE)
SQLALCHEMY_DATABASE_URI = DB_URI



[app.py]

migrate = Migrate(app, db)

User模型创建

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.String(200), nullable=False)
    email = db.Column(db.String(100), nullable=False, unique=True)
    join_time = db.Column(db.DateTime, default=datetime.now)  
    # 这里注意datetime.now()后面的()要删掉,因为默认值是调用函数获取(即默认值为函数),而不是函数的值

注册页面模板渲染

@bp.route("/register")
def register():
    return render_template("register.html")

register.html中的相对路径改为Jinja2模板格式,即:

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="./static/bootstrap/bootstrap.4.6.min.css">
    <link rel="stylesheet" href="./static/css/init.css">
</head>

改为

<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') }}">
</head>

Flask发送邮件功能实现

发送邮件必须要有一个邮箱服务器,使用Flask-Mail发送邮件,需要使用SMTP协议。首先在邮箱中开启SMTP服务,登录邮箱后,选择"POP3/SMTP/IMAP"

在这里插入图片描述

得到授权码后写入config.py,添加如下配置

# 邮箱配置
MAIL_SERVER = "smtp.qq.com"
MAIL_USE_SSL = True
MAIL_PORT = 465
MAIL_USERNAME = "1214038972@qq.com"
MAIL_PASSWORD = "ihgencuesnjzcjbf"
MAIL_DEFAULT_SENDER = "1214038972@qq.com"

发送邮件要先创建一个Flask-Mail对象,在exts.py中创建一个mail变量,代码如下

from flask_sqlalchemy import SQLAlchemy
from flask_mail import Mail

db = SQLAlchemy()
mail = Mail()

在app.py中从exts.py导入mail变量并进行初始化,代码如下:

from exts import db, mail

mail.init_app(app)

后续就可以用mail变量发送邮件了。在blueprints/auth.py中加入以下代码测试是否能正常发出邮件:

@bp.route("/mail/test")
def mail_test():
    message = Message(subject="邮箱测试", recipients=["drfitz@163.com"], body="这是一条测试邮件")
    mail.send(message)
    return "邮件发送成功!"
在这里插入图片描述

发送邮箱验证码功能实现

@bp.route("/captcha/email")
def get_email_captcha():
    # 第一种方式 /captcha/email/<email>
    # 第二种方式 /captcha/email?email=xxx@qq.com
    
    # 这里用第二种
    email = request.args.get("email")
    # 4/6:随机数字、字母、数组和字母的组合
    source = string.digits*4
    captcha = random.sample(source, 4)  # 得到的是列表
    # 将列表变为字符串
    # captcha = "-".join(captcha)
    captcha = "".join(captcha)
    # I/O:Input/Output
    message = Message(subject="小黑问答注册验证码", recipients=[email], body=f"您的验证码是:{captcha},请勿告诉他人!")
    mail.send(message)
    # 将验证码存入缓存一份 如memcached、redis等
    # 这里用数据库表的方式存储
    email_captcha = EmailCaptchaModel(email=email,captcha=captcha)
    db.session.add(email_captcha)
    db.session.commit()
    # RESTful API
    # {code: 200/400/500, message: "", data: {}}
    return jsonify({"code": 200, "message": "", "data": None})

实现验证码模型

class EmailCaptchaModel(db.Model):
    __tablename__ = "email_captcha"
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    email = db.Column(db.String(100), nullable=False)
    captcha = db.Column(db.String(100), nullable=False)

在register.html中加载jQuery和register.js,以及用于实现bootstrap响应式状态栏的两个js,给button绑定事件

{% block head %}
  <script src="{{ url_for('static', filename='jquery/jquery.3.6.min.js') }}"></script>
  <script src="{{ url_for('static', filename='js/register.js') }}"></script>
  <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.min.js" integrity="sha384-+sLIOodYLS7CIrQpBjl+C7nPvqq+FbNUBDunl/OZv93DB7Ln/533i8e/mZXLi/P+" crossorigin="anonymous"></script>
{% endblock %}


<div class="input-group-append">
      <button class="btn btn-outline-secondary" type="button" id="captcha-btn">获取验证码</button>
</div>

也可以将在线bootstrap依赖下载到项目中后引用本地静态资源

<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') }}">
<script src="{{ url_for('static', filename='jquery/jquery.3.6.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/register.js') }}"></script>
<script src="{{ url_for('static', filename='bootstrap/bootstrap.min.js') }}"></script>
<script src="{{ url_for('static', filename='bootstrap/popper.min.js') }}"></script>

添加register.js

function bindEmailCaptchaClick(){
  $("#captcha-btn").click(function (event){
    // $this:代表的是当前按钮的jquery对象
    var $this = $(this);
    // 阻止默认的事件,防止点击按钮后触发默认事件将所有数据传给服务器
    event.preventDefault();

    var email = $("input[name='email']").val();
    $.ajax({
      // http://127.0.0.1:500
      // /auth/captcha/email?email=xx@qq.com
      url: "/auth/captcha/email?email="+email,
      method: "GET",
      success: function (result){
        var code = result['code'];
        if(code == 200){
          var countdown = 5;
          // 开始倒计时之前,就取消按钮的点击事件
          $this.off("click");
          var timer = setInterval(function (){
            $this.text(countdown);
            countdown -= 1;
            // 倒计时结束的时候执行
            if(countdown <= 0){
              // 清掉定时器
              clearInterval(timer);
              // 将按钮的文字重新修改回来
              $this.text("获取验证码");
              // 重新绑定点击事件
              bindEmailCaptchaClick();
            }
          }, 1000);
          // alert("邮箱验证码发送成功!");
        }else{
          alert(result['message']);
        }
      },
      fail: function (error){
        console.log(error);
      }
    })
  });
}


// 整个网页都加载完毕后再执行的
$(function (){
  bindEmailCaptchaClick();
});
在这里插入图片描述

注册功能

@bp.route("/register", methods=['GET', 'POST'])
def register():
    if request.method == 'GET':
        return render_template("register.html")
    else:
        # 验证用户提交的邮箱和验证码是否对应且正确
        # 表单验证:flask-wtf: wtforms
        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(url_for("auth.login"))
        else:
            print(form.errors)
            return redirect(url_for("auth.register"))

注册表单验证器实现

pip install flask-wtf

新建blueprints/forms.py

class RegisterForm(wtforms.Form):
    email = wtforms.StringField(validators=[Email(message="邮箱格式错误!")])
    captcha = wtforms.StringField(validators=[Length(min=4, max=4, message="验证码格式错误!")])
    username = wtforms.StringField(validators=[Length(min=3, max=20, message="用户名应为3-20位字符!")])
    password = wtforms.StringField(validators=[Length(min=6, max=20, message="密码应为6-20位字符!")])
    password_confirm = wtforms.StringField(validators=[EqualTo("password", message="两次密码不一致!")])

    # 自定义验证:
    # 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_captcha(self, field):
        captcha = field.data
        email = self.email.data
        captcha_model = EmailCaptchaModel.query.filter_by(email=email, captcha=captcha).first()
        if not captcha_model:
            raise wtforms.ValidationError(message="邮箱或验证码错误!")
        # 可以删掉captcha_model
        else:
            db.session.delete(captcha_model)
            db.session.commit()

登录功能

auth.py

# /auth/login
@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中不适合存储太多的数据,只适合存储少量的数据
                # cookie一般用来存放登录授权的东西
                # flask中的session,是经过加密后存储在cookie中的
                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"))

forms.py

class LoginForm(wtforms.Form):
    email = wtforms.StringField(validators=[Email(message="邮箱格式错误!")])
    password = wtforms.StringField(validators=[Length(min=6, max=20, message="密码格式错误!")])

在config.py中设置SECRET_KEY = "asdfasdfjasdfjasd;lf"用于session加密,字符串长度随意

两个钩子函数

在正常执行流程当中,插入一个函数并先执行此函数。Flask有专门的对象g用于存储全局变量

app.py

from flask import g
# before_request/ before_first_request/ after_request
# hook
@app.before_request
def my_before_request():
    user_id = session.get("user_id")
    if user_id:
        user = UserModel.query.get(user_id)
        setattr(g, "user", user)
    else:
        setattr(g, "user", None)

在app.py中添加上下文处理器,将用户信息放在上下文中,以后渲染任何页面的时候都会把上下文传给模板,任何页面都能得到上下文中的对象

@app.context_processor
def my_context_processor():
    return {"user": g.user}

登录状态切换

base.html

<ul class="navbar-nav">
    {% 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 %}
</ul>

auth.py

@bp.route("/logout")
def logout():
    session.clear()
    return redirect("/")

发布问答页面

新建blueprints/qa.py

@bp.route("/qa/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
            question = QuestionModel(title=title, content=content, author=g.user)
            db.session.add(question)
            db.session.commit()
            # todo: 跳转到这篇问答的详情页
            return redirect("/")
        else:
            print(form.errors)
            return redirect(url_for("qa.public_question"))

创建问题和答案的ORM模型

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")


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")

创建问答的表单验证器

forms.py

class QuestionForm(wtforms.Form):
    title = wtforms.StringField(validators=[Length(min=1, max=100, message="标题格式错误!")])
    content = wtforms.StringField(validators=[Length(min=3, message="内容过短!")])


class AnswerForm(wtforms.Form):
    content = wtforms.StringField(validators=[Length(min=3, message="内容过短!")])
    question_id = wtforms.IntegerField(validators=[InputRequired(message="必须要传入问题id!")])

登录装饰器的实现

如果用户没有登陆是不应该允许用户发布问答的,用登陆装饰器解决

新建decorators.py

from functools import wraps
from flask import g, redirect, url_for

def login_required(func):
    # 保留func的信息
    @wraps(func)
    # func(a,b,c)
    # func(1,2,c=3)
    def inner(*args, **kwargs):
        if g.user:
            return func(*args, **kwargs)
        else:
            return redirect(url_for("auth.login"))
    return inner

# @login_required
# def public_question(quesiton_id):
#     pass
#
# login_required(public_question)(question_id)

qa.py加入注解@login_required

@bp.route("/qa/public", methods=['GET', 'POST'])
@login_required

首页问答列表渲染

index.html

{% for question in questions %}
<li>
    <div class="side-question">
        <img class="side-question-avatar" src="{{ url_for('static', filename='images/profile.jpg') }}" alt="">
    </div>
    <div class="question-main">
        <div class="question-title"><a href="{{ url_for('qa.qa_detail', qa_id=question.id) }}">{{ question.title }}</a></div>
        <div class="question-content">{{ question.content }}</div>
        <div class="question-detail">
            <span class="question-author">{{ question.author.username }}</span>
            <span class="question-time">{{ question.create_time }}</span>
        </div>
    </div>
{% endfor %}

qa.py

@bp.route("/")
def index():
    questions = QuestionModel.query.order_by(QuestionModel.create_time.desc()).all()
    return render_template("index.html", questions=questions)

detail.html

{% block body %}
  <div class="row" style="margin-top: 20px;">
    <div class="col"></div>
    <div class="col-10" style="background-color: #fff;padding: 20px;">
      <h3 class="page-title">{{ question.title }}</h3>
      <p class="question-info">
        <span>作者:{{ question.author.username }}</span>
        <span>时间:{{ question.create_time }}</span>
      </p>
      <hr>
      <p class="question-content">{{ question.content }}</p>
      <hr>
      <h4 class="comment-group-title">评论({{ question.answers|length }}):</h4>
      <form action="{{ url_for('qa.public_answer') }}" method="post">
        <div class="form-group">
          <input type="text" placeholder="请填写评论" name="content" class="form-control">
          <input type="hidden" name="question_id" value="{{ question.id }}">
        </div>
        <div class="form-group" style="text-align: right;">
          <button class="btn btn-primary">评论</button>
        </div>
      </form>
      <ul class="comment-group">
        {% for answer in question.answers %}
        <li>
          <div class="user-info">
            <img class="avatar" src="{{ url_for('static', filename='images/profile.jpg') }}" alt="">
            <span class="username">{{ answer.author.username }}</span>
            <span class="create-time">{{ answer.create_time }}</span>
          </div>
          <p class="comment-content">{{ answer.content }}</p>
        </li>
        {% endfor %}
      </ul>
    </div>
    <div class="col"></div>
  </div>
{% endblock %}

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)


# @bp.route("/answer/public", methods=['POST'])
@bp.post("/answer/public")
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")))

搜索功能实现

@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)
<li class="nav-item ml-2">
    <form class="form-inline my-2 my-lg-0" method="GET" action="{{ url_for('qa.search') }}">
        <input class="form-control mr-sm-2" type="search" placeholder="请输入关键词" aria-label="Search" name="q">
        <button class="btn btn-outline-success my-2 my-sm-0" type="submit">搜索</button>
    </form>
</li>

用户头像

pip install Image

新建static/images/user_avatar目录用于存储用户头像,新建blueprints/user.py用于管理用户头像相关视图。新建用户修改头像的页面profile.hmtl,引入ajax需要的两个js

<script src="{{ url_for('static', filename='js/pic_info.js') }}"></script>
<script src="{{ url_for('static', filename='jquery/jquery.form.js') }}"></script>
{% extends "base.html" %}

{% block title %}小黑问答-个人信息{% endblock %}

{% block head %}
    <script src="{{ url_for('static', filename='js/pic_info.js') }}"></script>
    <script src="{{ url_for('static', filename='jquery/jquery.form.js') }}"></script>
{% endblock %}

{% block body %}
    <div class="row mt-4">
        <div class="col"></div>
        <div class="col">

            <form class="pic_info">
                <h4 style="text-align:center; margin-bottom: 20px;">头像设置</h4>
                <div class="form-group" style="text-align: center;">
{#                    <label class="label01">当前头像:</label>#}
                    <img src="/user/img_path" alt="用户图片" class="now_user_pic" style="width: 60%;">
                </div>
{#                <div class="col"></div>#}
                <div class="form-group" >
{#                    <label>上传图像:</label>#}
                    <input type="file" name="avatar" class="input_file"  style="font-size: 5px;width: 50%;display:block;margin:auto;">
                </div>


{#                <div class="col"></div>#}
                <div class="form-group">
                    <input type="submit" value="保存" class="btn btn-primary btn-block" style="border-radius: 10px;  text-align: center;display:block;margin:0 auto;width: 60%;">
                </div>
            </form>


        </div>
        <div class="col"></div>
    </div>
{% endblock %}

用户模型添加头像url列

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.String(200), nullable=False)
    email = db.Column(db.String(100), nullable=False, unique=True)
    join_time = db.Column(db.DateTime, default=datetime.now)
    avatar_url = db.Column(db.String(256))  # 用户头像路径

flask-migrate模型重新迁移并更新

D:\Software\Python\Scripts\flask.exe db migrate
D:\Software\Python\Scripts\flask.exe db upgrade

user.py内编写视图函数

import os
from PIL import Image

from flask import Blueprint, render_template, jsonify, redirect, url_for, session
from exts import mail, db
from flask_mail import Message
from flask import request
import string
import random
from models import EmailCaptchaModel
from .forms import RegisterForm, LoginForm
from models import UserModel
from werkzeug.security import generate_password_hash, check_password_hash



# /auth Blueprint(蓝图名, __name__代表当前模块, 当前模块(蓝图)下所有视图函数的url前缀)
# 得到一个蓝图对象bp 下面所有视图函数都使用@bp.route来创建
bp = Blueprint("user", __name__, url_prefix="/user")


@bp.route("/profile")
def show_profile():
    return render_template("profile.html")

# 修改用户头像功能
@bp.route("/avatar", methods=["POST"])
def user_avatar():
    # 1.提取新头像
    new_avatar = request.files.get("avatar")
    # 获取用户id
    user_id = session.get('user_id')
    # 判断是否接收到新图片
    if new_avatar:
        # 如果存在用Image打开新图片
        img = Image.open(new_avatar)
        # 根据session获取的用户id,从数据库匹配并获取用户
        user = UserModel.query.filter_by(id=user_id).first()
        print(user)
        # 添加图片路径文件夹
        img_path = './static/images/user_avatar/' + str(user.id)
        # 判断图片路径是否存在
        user_img = os.path.exists(img_path)
        # 如果不存在
        if not user_img:
            # 创建该路径文件夹
            os.mkdir(img_path)
        # 将该图片命名并存放到指定路径
        img.save(img_path + '/user_imgs.jpg')
        # 将该用户的头像路径更改为该图片路径
        user.avatar_url = img_path + '/user_imgs.jpg'
        # 提交到数据库
        db.session.commit()
        session['avatar_url'] = img_path
        return jsonify(errno=0, errmsg='登陆成功')
        # return redirect(url_for("user.show_profile"))


# 获取登录用户头像
@bp.route("/img_path", methods=["GET", "POST"])
def img_path():
    # 从session获取用户id
    user_id = session.get('user_id')
    print(user_id)
    # # 根据session获取的用户id,从数据库匹配并获取用户
    user = UserModel.query.filter_by(id=user_id).first()
    # 如果该用户已上传头像,打开用户头像,读取
    if user.avatar_url:
        with open(user.avatar_url, 'rb') as f:
            img = f.read()
    # 如果用户未上传头像信息,返回默认头像
    else:
        with open('./static/images/avatar.jpg', 'rb') as f:
            img = f.read()

    return img

# 获取各用户头像
# <div class="side-question">
# 	<img class="side-question-avatar" src="{{ url_for('user.author_img',authid=question.author_id) }}" >
# </div>
@bp.route("/author_img/<authid>", methods=["GET", "POST"])
def author_img(authid):
    # print(authid)
    # # 根据session获取的用户id,从数据库匹配并获取用户
    user = UserModel.query.filter_by(id=authid).first()
    # 如果该用户已上传头像,打开用户头像,读取
    if user.avatar_url:
        with open(user.avatar_url, 'rb') as f:
            img = f.read()
    # 如果用户未上传头像信息,返回默认头像
    else:
        with open('./static/images/avatar.jpg', 'rb') as f:
            img = f.read()

    return img

修改index.html和detail.html中各用户头像加载标签

<div class="side-question">
	<img class="side-question-avatar" src="{{ url_for('user.author_img',authid=question.author_id) }}" >
</div>

添加pic_info.js

// user_pic_info.js
$(function () {
    $(".pic_info").submit(function (e) {
        // 阻止表单默认提交行为
        e.preventDefault();

        //TODO 上传头像
        // 模拟表单的提交
        $(this).ajaxSubmit({
            url: '/user/avatar',
            type: 'post',


            success: function (resp) {
                if (resp.errno == '0') {
                    // `上传头像`成功
                    // 获取上传头像的完整的url地址
                    var avatar_url = resp.avatar_url;
                    // 设置页面上用户头像img的src属性
                    $(".now_user_pic").attr("src", avatar_url);
                    // 设置父窗口中用户头像img的src属性
                    // $(".user_center_pic>img", parent.document).attr("src", avatar_url);
                    // $(".user_login>img", parent.document).attr("src", avatar_url);
                    location.reload()
                }
                else {
                    // `上传头像`失败
                    alert("上传失败");
                    alert(resp.errmsg);
                }
            }
        })
    })
});

内网穿透

在这里插入图片描述

手机流量访问:

在这里插入图片描述

效果

在这里插入图片描述

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alveus

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值