文章目录
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>
![在这里插入图片描述](https://img-blog.csdnimg.cn/66f9845d05f04b9cb325205180cc9b03.png)
过滤器
在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_filter
将datetime_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
控制语句
<!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>
![在这里插入图片描述](https://img-blog.csdnimg.cn/f7583e3d18ff4e3fa354a85f8b9fca25.png)
模板继承
一个网站中大部分网页的模块是重复的,比如顶部的导航栏,底部的备案信息。通过模板继承,可以把一些重复性的代码写在父模板中,子模板继承父模板后再分别实现自己页面的代码。
一个父模板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>
![在这里插入图片描述](https://img-blog.csdnimg.cn/e217f9d1bf6d4ef9b8bdac3438654709.png)
连接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.py
中import 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.py
和models.py
就不再存在循环引用的问题了,如下图。
在app.py
中import 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 "邮件发送成功!"
![在这里插入图片描述](https://img-blog.csdnimg.cn/23445fb045de4d28afbb8ca83ea1f57d.png)
发送邮箱验证码功能实现
@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();
});
![在这里插入图片描述](https://img-blog.csdnimg.cn/01fb3d7b9e1e4b52bbc539ff39e7fefa.png)
注册功能
@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);
}
}
})
})
});
内网穿透
![在这里插入图片描述](https://img-blog.csdnimg.cn/ab304ab2bfc1475b9c81a91467c301f4.png)
手机流量访问:
![在这里插入图片描述](https://img-blog.csdnimg.cn/7bd73ec937a247c59ad93de204446ae5.jpeg)