Flask

项目创建及安装 Flask

创建一个 Flask

点击创建 Create 后,就得到 Flask 项目

requirements 文件

由于集成开发环境帮我们已经安装好了 Flask 包,所以可以使用pip freeze > ./requirements.txt命令,直接生成一个 requirements.txt 文件

运行

尝试运行一下这个 app.py

通过浏览器访问一下 http://127.0.0.1:5000/

之后的项目扩展 就在这个项目文件夹的基础上进行开发

路由管理

项目为了区分角色:如用户、管理员的权限,需要区分一下不同权限用户的访问路由,为了方便管理路由,在项目文件夹中创建一个/routes的 package 进行路由管理,并将原先的代码进行一下整理

1
2
3
4
5
6
7
8
9
10
11
12
from routes import app


@app.route('/')
@app.route('/index')
def home_page():
return 'Home Page!'


@app.route('/about')
def about_page():
return 'About Page!'
1
2
3
4
5
6
from routes import app


@app.route('/create_article')
def create_article():
return 'Create Article!'
1
2
3
4
5
6
from flask import Flask

app = Flask(__name__)

from routes import user_routes
from routes import admin_routes

然后再修改一下 app.py 就可以运行啦

1
2
3
4
5
from routes import app


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

Debug 模式

如果给user_routes新增一个路由,但是不重启项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from routes import app


@app.route('/')
@app.route('/index')
def home_page():
return 'Home Page!'


@app.route('/about')
def about_page():
return 'About Page!'


@app.route('/login')
def login_page():
return 'Login Page!'

这时候在浏览器上访问,无法找到

为了解决这个问题,可以重新启动一下项目;但是在开发过程中,需要逐次修改、增加和验证功能,总是频繁重启项目会显得很繁琐,那么 debug 模式就可以很方便地帮我们做到修改的同时,动态地根据代码是否修改来重启项目

  • 通过app.run(debug=True)实现
1
2
3
4
from routes import app

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

这时候如果,对代码进行调整 无需重启项目就可以看到效果

  • 修改运行文件配置


模板 templates

render_template

前一节我们通过浏览器访问路由 可以获得一些简单的文字页面

而模板可以给我们提供更多便捷的页面内容展示和继承使用

  • 结合 flask 的render_template()以及html文件进行页面内容的渲染

/templates目录下创建 index.html

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Home Page</title>
</head>
<body>
This is content of the home page
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# -*- coding:utf-8 -*-
# Author: Zachary
from flask import render_template

from routes import app


@app.route('/')
@app.route('/index')
def home_page():
return render_template('index.html')


@app.route('/about')
def about_page():
return 'About Page!'


@app.route('/login')
def login_page():
return 'Login Page!'

编写完这两个代码还不够,由于对 app 对象来说,它并不知道 index.html 的位置,所以这时候直接运行会报找不到模板

/routes/__init__.py添加template_folder='../templates

1
2
3
4
5
6
from flask import Flask

app = Flask(__name__, template_folder='../templates')

from routes import user_routes
from routes import admin_routes

刷新项目后可以看到

渲染传参

如果希望前端展示的内容,可以通过后端来传递的这时候就需要用到 render_template 的参数渲染

使用一对花括号 {{ }}将其抱起来的内容作为一个变量/参数,其具体的值,通过后端的 render_template 方法来传递

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>{{ my_title }}</title>
</head>
<body>
This is content of the home page
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from flask import render_template

from routes import app


@app.route('/')
@app.route('/index')
def home_page():
return render_template('index.html', my_title='Home Page')


@app.route('/about')
def about_page():
return 'About Page!'


@app.route('/login')
def login_page():
return 'Login Page!'

运行后就会通过my_title='Home Page'将内容传递给前端模板上的对应参数

base 模板

随着开发的持续,html 页面也会随之越来越多

不少页面之间肯定存在着相同内容,如标题,导航栏等;如果每个页面都重复编写这些相同的内容,后期若出现功能的修改或者内容的增加,将会是一项巨大的工程,因此引入base.html作为所有 html 模板的基础模板,并通过继承(extends){% extends 'base.html' %}来对 base 模板的内容进行共享

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Base Template</title>
</head>
<body>
this is base template content
</body>
</html>

创建两个 html 文件 index 和 about

1
{% extends 'base.html' %}

然后修改一下 user_routes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from flask import render_template

from routes import app


@app.route('/')
@app.route('/index')
def home_page():
return render_template('index.html')


@app.route('/about')
def about_page():
return render_template('about.html')


@app.route('/login')
def login_page():
return 'Login Page!'

通过两个路由 都可以访问到 base 模板

block 标签

但是有时候并不是全都需要一样的模板,有可能需要对 base 的模板内容进行修改

可以使用 block 标签,起到一个占位符的作用

  • % block 标签名 %标签起始
  • % endblock %标签结束
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>{% block title %} {% endblock %}</title>
</head>
<body>
{% block content %} {% endblock %}
</body>
</html>
1
2
{% extends 'base.html' %} {% block title %} index page {% endblock %} {% block
content %} this is a index content {% endblock %}
1
2
{% extends 'base.html' %} {% block title %} about page {% endblock %} {% block
content %} this is a about content {% endblock %}

Bootstrap&jQuery

https://getbootstrap.com/

https://jquery.com/download/

下载

Bootstrap 和 jQuery 提供了一系列好用的前端渲染效果,可以做出比较美观的页面内容

下载好后的整体项目目录结构

使用

前面有讲到 base 模板,所以可以很方便地用在 base 上 其他模板仅需 extends 就可以使用同样效果

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<!-- Bootstrap CSS -->
<link
rel="stylesheet"
href="/static/plugins/bootstrap-5.3.2/bootstrap.min.css"
/>
<script src="/static/plugins/bootstrap-5.3.2/bootstrap.bundle.min.js"></script>
<script src="/static/plugins/jquery-3.7.1/jquery.min.js"></script>

<title>{% block title %} {% endblock %}</title>
</head>
<body>
<nav
class="navbar navbar-expand-md navbar-dark bg-dark"
style="background-color: #1976d2; color: #ffffff;"
>
<a class="navbar-brand" href="#" style="font-size: 20px; padding: 10px;"
>Zachary的记事本</a
>
<button
class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarNav"
style="border: none; background-color: transparent; padding: 5px 10px;"
>
<span
class="navbar-toggler-icon"
style="background-color: #1976d2;"
></span>
</button>
<div
class="collapse navbar-collapse"
id="navbarNav"
style="width: 80%; margin: auto; border-radius: 10px;"
>
<ul class="navbar-nav me-auto" style="margin-top: 20px;">
<li class="nav-item active" style="border-bottom: 2px solid #4caf50;">
<a
class="nav-link"
href="{{ url_for('home_page') }}"
style="color: #ffffff; padding: 10px;"
>主页</a
>
</li>
<li class="nav-item" style="border-bottom: 2px solid #4caf50;">
<a
class="nav-link"
href="{{ url_for('about_page') }}"
style="color: #ffffff; padding: 10px;"
>关于</a
>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item">
<a
class="nav-link"
href="{{ url_for('login_page') }}"
style="color: #ffffff; padding: 10px;"
>登录</a
>
</li>
</ul>
</div>
</nav>

{% block content %} {% endblock %}
</body>
</html>
1
2
3
4
5
6
7
8
9
from flask import Flask

app = Flask(__name__,
template_folder='../templates',
static_folder='../static',
static_url_path='/static')

from routes import user_routes
from routes import admin_routes
1
2
{% extends 'base.html' %} {% block title %} login page {% endblock %} {% block
content %} this is a login content {% endblock %}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# -*- coding:utf-8 -*-
# Author: Zachary
from flask import render_template

from routes import app


@app.route('/')
@app.route('/index')
def home_page():
return render_template('index.html')


@app.route('/about')
def about_page():
return render_template('about.html')


@app.route('/login')
def login_page():
return render_template('login.html')

可以看到页面文字和内容些许不同了,同时在控制台上可以看到资源加载到了,上面的按钮也可以试着点按

调整

可以根据文档来进行一些间距的调整,比如 https://getbootstrap.com/docs/5.3/utilities/spacing/

连接 MySQL

安装包

之前已经有了 SQLAlchemy 的基础,那么这块可以通过对 Flask 与 SQLAlchemy 进行结合进行数据库的操作

需要的包有:

  • mysqlclient==2.1.1
  • SQLAlchemy==2.0.25
  • Flask-SQLAlchemy==3.0.0

可以直接添加到requirements.txt,点击一键安装

配置连接

需要给 app 添加一个

app.config['SQLAlchemy_DATABASE_URI'] = 'mysql+mysqldb://<user>:<password>@<host>[:<port>]/<dbname>'

1
2
3
4
5
6
7
8
9
10
11
12
13
# -*- coding:utf-8 -*-
# Author: Zachary
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__, template_folder='../templates', static_folder='../static', static_url_path='/static')

app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+mysqldb://root:980226@localhost/mynotebook_db'
db = SQLAlchemy(app)

from routes import user_routes
from routes import admin_routes

定义数据库表对应映射类

初始化数据库

现在 MySQL 中准备一个数据库mynotebook_db

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql> create database mynotebook_db;
Query OK, 1 row affected (0.01 sec)

mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mynotebook_db |
| mysql |
| performance_schema |
| sys |
| test-zach |
| testSql |
| userCenter |
+--------------------+
8 rows in set (0.00 sec)

初始化数据表

先在数据库里面准备一张 article 表

这块 title 记得做唯一性约束 上面图忘记了

然后准备一些数据插入进去

创建 models

在项目下创建一个/models的包,然后创建一个article.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# -*- coding:utf-8 -*-
# Author: Zachary
from datetime import datetime

from sqlalchemy import Integer, String, BLOB, TIMESTAMP
from sqlalchemy.orm import Mapped, mapped_column

from routes import db


class Article(db.Model):
__tablename__ = 'articles'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
title: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
__content: Mapped[bytes] = mapped_column(BLOB, name='content', nullable=False)
create_time: Mapped[datetime] = mapped_column(TIMESTAMP, nullable=False)
update_time: Mapped[datetime] = mapped_column(TIMESTAMP, nullable=True)

@property
def content(self):
return self.__content.decode('utf-8')

创建 services

在项目下创建一个/services的包,然后创建一个article_service.py

1
2
3
4
5
6
7
8
9
10
# -*- coding:utf-8 -*-
# Author: Zachary
from models.article import Article
from routes import db


class ArticleService:
def get_article_by_id(self, article_id):
return db.session.get(Article, article_id)

然后定义一个查询文章的路由

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
# -*- coding:utf-8 -*-
# Author: Zachary
from flask import render_template, abort

from routes import app
from services.article_service import ArticleService


@app.route('/')
@app.route('/index')
def home_page():
return render_template('index.html')


@app.route('/article/<article_id>')
def article_page(article_id):
article = ArticleService().get_article_by_id(article_id)
if article:
return render_template('article.html', article=article)
abort(404)


@app.route('/about')
def about_page():
return render_template('about.html')


@app.route('/login')
def login_page():
return render_template('login.html')

还需要新增一个article.html

1
2
3
4
5
6
{% extends 'base.html' %} {% block title %} zachary的记事本 - {{ article.title
}} {% endblock %} {% block content %}
<h1>{{ article.title }}</h1>
<p>发布于:{{ article.create_time }}</p>
<p>{{ article.content }}</p>
{% endblock %}

尝试通过浏览器通过 url 去获取一条数据

实现多结果查询

添加一个 service 查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# -*- coding:utf-8 -*-
# Author: Zachary
from sqlalchemy import Select

from models.article import Article
from routes import db


class ArticleService:
def get_article_by_id(self, article_id):
return db.session.get(Article, article_id)

def get_articles(self):
query = Select(Article)
return db.session.scalars(query).all()

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
# -*- coding:utf-8 -*-
# Author: Zachary
from flask import render_template, abort

from routes import app
from services.article_service import ArticleService


@app.route('/')
@app.route('/index')
def home_page():
articles = ArticleService().get_articles()
return render_template('index.html', articles=articles)


@app.route('/article/<article_id>')
def article_page(article_id):
article = ArticleService().get_article_by_id(article_id)
if article:
return render_template('article.html', article=article)
abort(404)


@app.route('/about')
def about_page():
return render_template('about.html')


@app.route('/login')
def login_page():
return render_template('login.html')

然后修改 index.html 显示所有文章即可

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
{% extends 'base.html' %} {% block title %} zachary的记事本 - 主页 {% endblock
%} {% block content %}
<table class="table">
<thead>
<tr>
<th scope="col">No.</th>
<th scope="col">记事标题</th>
<th scope="col">发布时间</th>
</tr>
</thead>
<tbody class="table-group-divider">
{% for article in articles %}
<tr>
<th scope="row">{{ article.id }}</th>
<td>
<a href="{{ url_for('article_page', article_id=article.id) }}"
>{{ article.title }}</a
>
</td>
<td>{{ article.create_time }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

用户登录

创建用户表

首先需要创建一个 users 表

然后适当地准备上数据

创建用户映射类

然后在/models创建user.py用于创建 users 表的映射类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column

from routes import db


class User(db.Model):
__tablename__ = 'users'

id: Mapped[int] = mapped_column(Integer, primary_key=True)
account: Mapped[str] = mapped_column(String(128), unique=True, nullable=False)
password: Mapped[str] = mapped_column(String(64), nullable=False)
username: Mapped[str] = mapped_column(String(128), nullable=False)
description: Mapped[str] = mapped_column(String(255), nullable=True)

准备与登录相关的包

1
2
Flask-WTF==1.1.1
flask-login==0.6.3

创建登录管理 login_manager 对象

在 routes 下的init中添加对 login_manager 的初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import Flask
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__, template_folder='../templates', static_folder='../static', static_url_path='/static')

app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+mysqldb://root:980226@localhost/mynotebook_db'
app.config['SECRET_KEY'] = 'zachary123456'

db = SQLAlchemy(app)
login_manager = LoginManager(app)

from routes import user_routes
from routes import admin_routes

给 User 类增加 user.load 支持并增加密码验证

给 User 类继承 UserMixin,为了实现一些方法如 is_active 等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from flask_login import UserMixin
from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column

from routes import db, login_manager


@login_manager.user_loader
def load_manager_user(user_id: int):
return db.session.get(User, user_id)


class User(db.Model, UserMixin):
__tablename__ = 'users'

id: Mapped[int] = mapped_column(Integer, primary_key=True)
account: Mapped[str] = mapped_column(String(128), unique=True, nullable=False)
password: Mapped[str] = mapped_column(String(64), nullable=False)
username: Mapped[str] = mapped_column(String(128), nullable=False)
description: Mapped[str] = mapped_column(String(255), nullable=True)

def password_check(self, password):
return self.password == password

创建表单类

为了便于管理表单,在项目目录下面创建一个/forms

然后创建一个login_form.py,用于实现登录的表单类(包括数据的验证,对应数据项必须输入数据)

1
2
3
4
5
6
7
8
9
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired


class LoginForm(FlaskForm):
account = StringField(label='账号', validators=[DataRequired()])
password = PasswordField(label='密码', validators=[DataRequired()])
submit = SubmitField(label='登录')

修改 login 的前端代码

修改在/templates下的login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{% extends 'base.html' %} {% block title %} zachary的记事本 - 登录 {% endblock
%} {% block content %}
<div class="container-xl">
<form method="POST" class="form-signin">
{{ form.hidden_tag() }}
<h1 class="h3 mb-3 font-weight-normal">记事本管理员登录</h1>
<br />
{{ form.account.label }} {{ form.account(class="form-control",
placeholder="请输入账号") }} {{ form.password.label }} {{
form.password(class="form-control", placeholder="请输入密码") }}
<br />
{{ form.submit(class="btn btn-lg btn-primary btn-block") }}
</form>
</div>
{% endblock %}

修改 login 的后端逻辑

先实现一下UserService,在/services下创建一个user_service.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from flask_login import login_user
from sqlalchemy import Select

from models.user import User
from routes import db


class UserService:

def do_login(self, account: str, password: str) -> bool:
query = Select(User).where(User.account == account)
user = db.session.scalar(query)
if user and user.password_check(password):
login_user(user)
return True
return False

修改/routes/user_routes.py中的login_page方法

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
# -*- coding:utf-8 -*-
# Author: Zachary
from flask import render_template, abort, flash, redirect, url_for

from forms.login_form import LoginForm
from routes import app
from services.article_service import ArticleService
from services.user_service import UserService


@app.route('/')
@app.route('/index')
def home_page():
articles = ArticleService().get_articles()
return render_template('index.html', articles=articles)


@app.route('/article/<article_id>')
def article_page(article_id):
article = ArticleService().get_article_by_id(article_id)
if article:
return render_template('article.html', article=article)
abort(404)


@app.route('/about')
def about_page():
return render_template('about.html')


@app.route('/login', methods=['GET', 'POST'])
def login_page():
form = LoginForm()
if form.validate_on_submit():
result = UserService().do_login(form.account.data, form.password.data)
if result:
flash(f'{form.account.data}登录成功, 正在跳转...', category='success')
return redirect(url_for('home_page'))
else:
flash(f'{form.account.data}登录失败, 请检查账号密码', category='danger')
return render_template('login.html', form=form)

启动一下项目,然后点击 登录 按钮,跳转到登录页面

登录成功后自动跳转至 homepage

修改标题栏登录按钮

在用户登录前后,登录按钮一直都是登录,这并不符合登录后的一些选项,为了实现这一点,base.html进行一点修改

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<!-- Bootstrap CSS -->
<link
rel="stylesheet"
href="/static/plugins/bootstrap-5.3.2/bootstrap.min.css"
/>
<script src="/static/plugins/bootstrap-5.3.2/bootstrap.bundle.min.js"></script>
<script src="/static/plugins/jquery-3.7.1/jquery.min.js"></script>

<title>{% block title %} {% endblock %}</title>
</head>
<body>
<nav
class="navbar navbar-expand-md navbar-dark bg-dark ps-4 pe-4"
style="background-color: #1976d2; color: #ffffff;"
>
<a class="navbar-brand" href="#" style="font-size: 20px; padding: 10px;"
>Zachary的记事本</a
>
<button
class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarNav"
style="border: none; background-color: transparent; padding: 5px 10px;"
>
<span
class="navbar-toggler-icon"
style="background-color: #1976d2;"
></span>
</button>
<div
class="collapse navbar-collapse"
id="navbarNav"
style="width: 80%; margin: auto; border-radius: 10px;"
>
<ul class="navbar-nav me-auto" style="margin-top: 20px;">
<li class="nav-item active" style="border-bottom: 2px solid #4caf50;">
<a
class="nav-link"
href="{{ url_for('home_page') }}"
style="color: #ffffff; padding: 10px;"
>主页</a
>
</li>
<li class="nav-item" style="border-bottom: 2px solid #4caf50;">
<a
class="nav-link"
href="{{ url_for('about_page') }}"
style="color: #ffffff; padding: 10px;"
>关于</a
>
</li>
</ul>
{% if current_user.is_authenticated %}
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="#" style="color: #ffffff; padding: 10px;"
>发布新文章</a
>
</li>
<li class="nav-item">
<a class="nav-link" href="#" style="color: #ffffff; padding: 10px;"
>退出</a
>
</li>
</ul>
{% else %}
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="#}" style="color: #ffffff; padding: 10px;"
>登录</a
>
</li>
</ul>
{% endif %}
</div>
</nav>

{% block content %} {% endblock %}
</body>
</html>

只是发布新文章和退出按钮的功能 暂时还没有实现

登录错误信息处理

在官网中去找到自己想要使用的信息提示样式

然后在相应部分进行编写

首先为了能让信息提示都能复用上,需要将信息提示的前端部分写到 base.html 里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages
%} {% for category, message in messages %}
<div
class="alert alert-{{ category }} alert-dismissible fade show"
role="alert"
>
{{ message }}
<button
type="button"
class="btn-close"
data-bs-dismiss="alert"
aria-label="Close"
></button>
</div>
{% endfor %} {% endif %} {% endwith %}
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<!-- Bootstrap CSS -->
<link
rel="stylesheet"
href="/static/plugins/bootstrap-5.3.2/bootstrap.min.css"
/>
<script src="/static/plugins/bootstrap-5.3.2/bootstrap.bundle.min.js"></script>
<script src="/static/plugins/jquery-3.7.1/jquery.min.js"></script>

<title>{% block title %} {% endblock %}</title>
</head>
<body>
<nav
class="navbar navbar-expand-md navbar-dark bg-dark ps-4 pe-4"
style="background-color: #1976d2; color: #ffffff;"
>
<a class="navbar-brand" href="#" style="font-size: 20px; padding: 10px;"
>Zachary的记事本</a
>
<button
class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarNav"
style="border: none; background-color: transparent; padding: 5px 10px;"
>
<span
class="navbar-toggler-icon"
style="background-color: #1976d2;"
></span>
</button>
<div
class="collapse navbar-collapse"
id="navbarNav"
style="width: 80%; margin: auto; border-radius: 10px;"
>
<ul class="navbar-nav me-auto" style="margin-top: 20px;">
<li class="nav-item active" style="border-bottom: 2px solid #4caf50;">
<a
class="nav-link"
href="{{ url_for('home_page') }}"
style="color: #ffffff; padding: 10px;"
>主页</a
>
</li>
<li class="nav-item" style="border-bottom: 2px solid #4caf50;">
<a
class="nav-link"
href="{{ url_for('about_page') }}"
style="color: #ffffff; padding: 10px;"
>关于</a
>
</li>
</ul>
{% if current_user.is_authenticated %}
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="#" style="color: #ffffff; padding: 10px;"
>发布新文章</a
>
</li>
<li class="nav-item">
<a class="nav-link" href="#" style="color: #ffffff; padding: 10px;"
>退出</a
>
</li>
</ul>
{% else %}
<ul class="navbar-nav">
<li class="nav-item">
<a
class="nav-link"
href="{{ url_for('login_page') }}"
style="color: #ffffff; padding: 10px;"
>登录</a
>
</li>
</ul>
{% endif %}
</div>
</nav>
{% with messages = get_flashed_messages(with_categories=true) %} {% if
messages %} {% for category, message in messages %}
<div
class="alert alert-{{ category }} alert-dismissible fade show"
role="alert"
>
{{ message }}
<button
type="button"
class="btn-close"
data-bs-dismiss="alert"
aria-label="Close"
></button>
</div>
{% endfor %} {% endif %} {% endwith %} {% block content %} {% endblock %}
</body>
</html>

退出登录

给退出按钮指定一下 url,这里给退出指定的是logout_page

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<!-- Bootstrap CSS -->
<link
rel="stylesheet"
href="/static/plugins/bootstrap-5.3.2/bootstrap.min.css"
/>
<script src="/static/plugins/bootstrap-5.3.2/bootstrap.bundle.min.js"></script>
<script src="/static/plugins/jquery-3.7.1/jquery.min.js"></script>

<title>{% block title %} {% endblock %}</title>
</head>
<body>
<nav
class="navbar navbar-expand-md navbar-dark bg-dark ps-4 pe-4"
style="background-color: #1976d2; color: #ffffff;"
>
<a class="navbar-brand" href="#" style="font-size: 20px; padding: 10px;"
>Zachary的记事本</a
>
<button
class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarNav"
style="border: none; background-color: transparent; padding: 5px 10px;"
>
<span
class="navbar-toggler-icon"
style="background-color: #1976d2;"
></span>
</button>
<div
class="collapse navbar-collapse"
id="navbarNav"
style="width: 80%; margin: auto; border-radius: 10px;"
>
<ul class="navbar-nav me-auto" style="margin-top: 20px;">
<li class="nav-item active" style="border-bottom: 2px solid #4caf50;">
<a
class="nav-link"
href="{{ url_for('home_page') }}"
style="color: #ffffff; padding: 10px;"
>主页</a
>
</li>
<li class="nav-item" style="border-bottom: 2px solid #4caf50;">
<a
class="nav-link"
href="{{ url_for('about_page') }}"
style="color: #ffffff; padding: 10px;"
>关于</a
>
</li>
</ul>
{% if current_user.is_authenticated %}
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="#" style="color: #ffffff; padding: 10px;"
>发布新文章</a
>
</li>
<li class="nav-item">
<a
class="nav-link"
href="{{ url_for('logout_page') }}"
style="color: #ffffff; padding: 10px;"
>退出</a
>
</li>
</ul>
{% else %}
<ul class="navbar-nav">
<li class="nav-item">
<a
class="nav-link"
href="{{ url_for('login_page') }}"
style="color: #ffffff; padding: 10px;"
>登录</a
>
</li>
</ul>
{% endif %}
</div>
</nav>
{% with messages = get_flashed_messages(with_categories=true) %} {% if
messages %} {% for category, message in messages %}
<div
class="alert alert-{{ category }} alert-dismissible fade show"
role="alert"
>
{{ message }}
<button
type="button"
class="btn-close"
data-bs-dismiss="alert"
aria-label="Close"
></button>
</div>
{% endfor %} {% endif %} {% endwith %} {% block content %} {% endblock %}
</body>
</html>

接着在 user_routes.py 中实现一下这个方法

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
# -*- coding:utf-8 -*-
# Author: Zachary
from flask import render_template, abort, flash, redirect, url_for
from flask_login import current_user, logout_user

from forms.login_form import LoginForm
from routes import app
from services.article_service import ArticleService
from services.user_service import UserService


@app.route('/')
@app.route('/index')
def home_page():
articles = ArticleService().get_articles()
return render_template('index.html', articles=articles)


@app.route('/article/<article_id>')
def article_page(article_id):
article = ArticleService().get_article_by_id(article_id)
if article:
return render_template('article.html', article=article)
abort(404)


@app.route('/about')
def about_page():
return render_template('about.html')


@app.route('/login', methods=['GET', 'POST'])
def login_page():
form = LoginForm()
if form.validate_on_submit():
result = UserService().do_login(form.account.data, form.password.data)
if result:
flash(f'{form.account.data}登录成功, 正在跳转...', category='success')
return redirect(url_for('home_page'))
else:
flash(f'{form.account.data}登录失败, 请检查账号密码', category='danger')
return render_template('login.html', form=form)


@app.route('/logout')
def logout_page():
logout_user()

return redirect(url_for('home_page'))

发布新文章

接下来开始实现 发布新文章 按钮的功能

创建表单类

首先在artilce_form.py中创建表单类ArticleForm

1
2
3
4
5
6
7
8
9
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import DataRequired


class ArticleForm(FlaskForm):
title = StringField(label='标题', validators=[DataRequired()])
content = TextAreaField(label='内容', validators=[DataRequired()])
submit = SubmitField(label='提交')

实现新增文章前端代码

/templates下面创建一个create_article.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{% extends 'base.html' %} {% block title %} zachary的记事本 - 发布新文章 {%
endblock %} {% block content %}
<style>
.content_height {
height: 550px;
}
</style>
<div class="container-fluid px-4 py-4">
<form method="POST" class="form-signin">
{{ form.hidden_tag() }}
<h1 class="h3 mb-3 font-weight-normal">发布新文章</h1>
<br />
{{ form.title.label }} {{ form.title(class="form-control",
placeholder="请输入标题") }} {{ form.content.label }} {{
form.content(class="form-control content_height", placeholder="请输入内容")
}}
<br />
{{ form.submit(class="btn btn-lg btn-primary btn-block") }}
</form>
</div>
{% endblock %}

发布新文章按钮指向一个 url {{ url_for('create_article_page') }}

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<!-- Bootstrap CSS -->
<link
rel="stylesheet"
href="/static/plugins/bootstrap-5.3.2/bootstrap.min.css"
/>
<script src="/static/plugins/bootstrap-5.3.2/bootstrap.bundle.min.js"></script>
<script src="/static/plugins/jquery-3.7.1/jquery.min.js"></script>

<title>{% block title %} {% endblock %}</title>
</head>
<body>
<nav
class="navbar navbar-expand-md navbar-dark bg-dark ps-4 pe-4"
style="background-color: #1976d2; color: #ffffff;"
>
<a class="navbar-brand" href="#" style="font-size: 20px; padding: 10px;"
>Zachary的记事本</a
>
<button
class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarNav"
style="border: none; background-color: transparent; padding: 5px 10px;"
>
<span
class="navbar-toggler-icon"
style="background-color: #1976d2;"
></span>
</button>
<div
class="collapse navbar-collapse"
id="navbarNav"
style="width: 80%; margin: auto; border-radius: 10px;"
>
<ul class="navbar-nav me-auto" style="margin-top: 20px;">
<li class="nav-item active" style="border-bottom: 2px solid #4caf50;">
<a
class="nav-link"
href="{{ url_for('home_page') }}"
style="color: #ffffff; padding: 10px;"
>主页</a
>
</li>
<li class="nav-item" style="border-bottom: 2px solid #4caf50;">
<a
class="nav-link"
href="{{ url_for('about_page') }}"
style="color: #ffffff; padding: 10px;"
>关于</a
>
</li>
</ul>
{% if current_user.is_authenticated %}
<ul class="navbar-nav">
<li class="nav-item">
<a
class="nav-link"
href="{{ url_for('create_article_page') }}"
style="color: #ffffff; padding: 10px;"
>发布新文章</a
>
</li>
<li class="nav-item">
<a
class="nav-link"
href="{{ url_for('logout_page') }}"
style="color: #ffffff; padding: 10px;"
>退出</a
>
</li>
</ul>
{% else %}
<ul class="navbar-nav">
<li class="nav-item">
<a
class="nav-link"
href="{{ url_for('login_page') }}"
style="color: #ffffff; padding: 10px;"
>登录</a
>
</li>
</ul>
{% endif %}
</div>
</nav>
{% with messages = get_flashed_messages(with_categories=true) %} {% if
messages %} {% for category, message in messages %}
<div
class="alert alert-{{ category }} alert-dismissible fade show"
role="alert"
>
{{ message }}
<button
type="button"
class="btn-close"
data-bs-dismiss="alert"
aria-label="Close"
></button>
</div>
{% endfor %} {% endif %} {% endwith %} {% block content %} {% endblock %}
</body>
</html>

实现新增文章后端逻辑

先编写一个新增文章的 service 方法

1
2
3
4
5
6
7
8
9
def add_article(self, article: Article):
# 先查询是否已经存在
query = Select(Article).where(Article.title == article.title)
if db.session.scalar(query):
# 抛出异常
raise Exception('该标题文章已经存在,请重新编辑')
db.session.add(article)
db.session.commit()
return article

然后在admin_routes.py新增一个create_article_page

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
from flask import flash, redirect, url_for, render_template
from flask_login import login_required

from forms.article_form import ArticleForm
from models.article import Article
from routes import app
from services.article_service import ArticleService


@app.route('/create_article', methods=['GET', 'POST'])
@login_required
def create_article_page():
form = ArticleForm()
if form.validate_on_submit():
article = Article()
article.title = form.title.data
article.content = form.content.data

try:
ArticleService().add_article(article)
flash(f'文章《{article.title}》创建成功', category='success')
return redirect(url_for('home_page'))
except Exception as e:
flash(f'文章《{article.title}》创建失败: {e}', category='danger')
return render_template('create_article.html', form=form)

需要给Article类 添加上对content的 setter 方法

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
# -*- coding:utf-8 -*-
# Author: Zachary
from datetime import datetime

from sqlalchemy import Integer, String, BLOB, TIMESTAMP
from sqlalchemy.orm import Mapped, mapped_column

from routes import db


class Article(db.Model):
__tablename__ = 'articles'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
title: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
__content: Mapped[bytes] = mapped_column(BLOB, name='content', nullable=False)
create_time: Mapped[datetime] = mapped_column(TIMESTAMP, nullable=False)
update_time: Mapped[datetime] = mapped_column(TIMESTAMP, nullable=True)

@property
def content(self):
return self.__content.decode('utf-8')

@content.setter
def content(self, content: str):
self.__content = content.encode()


运行效果

编辑文章

为了更方便地实现编辑这一功能,我们先修改一下index.html的显示效果

修改编辑文章前端效果

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
{% extends 'base.html' %} {% block title %} zachary的记事本 - 主页 {% endblock
%} {% block content %}
<div class="container-xl">
{% for article in articles %}
<div class="card m-4" style="background-color: #bcd0c7">
<div class="card-header">
<ul class="nav">
<li class="nav-item me-auto">
<a
class="btn fs-5 fw-bold"
href="{{ url_for('article_page', article_id=article.id) }}"
>{{ article.title }}</a
>
</li>
{% if current_user.is_authenticated %}
<li class="nav-item px-1">
<small class="text-body-secondary">
<a
class="btn"
href="{{ url_for('edit_article_page', article_id=article.id) }}"
>编辑</a
>
</small>
</li>
{% endif %}
</ul>
</div>
<div class="card-body">
<p class="card-text">
<a
class="btn fs-6"
href="{{ url_for('article_page', article_id=article.id) }}"
>{{ article.content }}</a
>
</p>
<ul class="nav">
<li class="nav-item me-auto">
<small class="text-body-secondary"
>发布时间:{{ article.create_time }}</small
>
</li>
</ul>
</div>
</div>
{% endfor %}
</div>
{% endblock %}

为了能先看一下 index 的内容样式,需要在admin_routes.py中添加一个新的路由

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
# -*- coding:utf-8 -*-
# Author: Zachary
from flask import flash, redirect, url_for, render_template

from forms.article_form import ArticleForm
from models.article import Article
from routes import app
from services.article_service import ArticleService


@app.route('/create_article', methods=['GET', 'POST'])
def create_article_page():
form = ArticleForm()
if form.validate_on_submit():
article = Article()
article.title = form.title.data
article.content = form.content.data

try:
ArticleService().add_article(article)
flash(f'文章《{article.title}》创建成功', category='success')
return redirect(url_for('home_page'))
except Exception as e:
flash(f'文章《{article.title}》创建失败: {e}', category='danger')
return render_template('create_article.html', form=form)


@app.route('/edit_article/<article_id>', methods=['GET', 'POST'])
def edit_article_page():
pass

同时为了复用当使用 发布新文章编辑的时候用同一个create_article.html页面编辑或者发布,做一个小修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{% extends 'base.html' %} {% block title %} zachary的记事本 - {% if isEdit %}
编辑文章 {% else %} 发布新文章 {% endif %} {% endblock %} {% block content %}
<style>
.content_height {
height: 550px;
}
</style>
<div class="container-fluid px-4 py-4">
<form method="POST" class="form-signin">
{{ form.hidden_tag() }}
<h1 class="h3 mb-3 font-weight-normal">
{% if isEdit %} 编辑文章 {% else %} 发布新文章 {% endif %}
</h1>
<br />
{{ form.title.label }} {{ form.title(class="form-control",
placeholder="请输入标题") }} {{ form.content.label }} {{
form.content(class="form-control content_height", placeholder="请输入内容")
}}
<br />
{{ form.submit(class="btn btn-lg btn-primary btn-block") }}
</form>
</div>
{% endblock %}

实现编辑文章后端逻辑

首先实现编辑的 service 代码

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
# -*- coding:utf-8 -*-
# Author: Zachary
from sqlalchemy import Select, func, and_

from models.article import Article
from routes import db


class ArticleService:
def get_article_by_id(self, article_id):
return db.session.get(Article, article_id)

def get_articles(self):
query = Select(Article)
return db.session.scalars(query).all()

def add_article(self, article: Article):
# 先查询是否已经存在
query = Select(Article).where(Article.title == article.title)
if db.session.scalar(query):
# 抛出异常
raise Exception('该标题文章已经存在,请重新编辑')
db.session.add(article)
db.session.commit()
return article

def update_article(self, article: Article):
# 先查询是否已经存在
existed_article = self.get_article_by_id(article.id)
if not existed_article:
raise Exception('该文章不存在')
# 查询要更新的文章标题是否已经存在
query = Select(Article).where(and_(Article.title == article.title, Article.id != article.id))
if db.session.scalar(query):
raise Exception('该标题文章已经存在,请重新编辑')
# 更新
existed_article.title = article.title
existed_article.content = article.content
existed_article.update_time = func.now()

db.session.commit()
return article

实现编辑的路由,其中需要实现两个

  • 点击编辑按钮后,跳转到当前该文章的信息页面
  • 修改信息之后,点击提交按钮进行更新
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
# -*- coding:utf-8 -*-
# Author: Zachary
from flask import flash, redirect, url_for, render_template, request
from flask_login import login_required

from forms.article_form import ArticleForm
from models.article import Article
from routes import app
from services.article_service import ArticleService


@app.route('/create_article', methods=['GET', 'POST'])
@login_required
def create_article_page():
form = ArticleForm()
if form.validate_on_submit():
article = Article()
article.title = form.title.data
article.content = form.content.data

try:
ArticleService().add_article(article)
flash(f'文章《{article.title}》创建成功', category='success')
return redirect(url_for('home_page'))
except Exception as e:
flash(f'文章《{article.title}》创建失败: {e}', category='danger')
return render_template('create_article.html', form=form, isEdit=False)


@app.route('/edit_article/<article_id>', methods=['GET', 'POST'])
@login_required
def edit_article_page(article_id: str):
form = ArticleForm()
# 点击编辑后 回显数据
if request.method == 'GET':
try:
article = ArticleService().get_article_by_id(article_id)
if not article:
flash("文章不存在", category='danger')
return redirect(url_for('home_page'))
else:
form.title.data = article.title
form.content.data = article.content
except Exception as e:
flash(f'文章获取失败: {e}', category='danger')
return redirect(url_for('home_page'))

# 更新数据后 保存
if form.validate_on_submit():
article = Article()
try:
article.id = int(article_id)
article.title = form.title.data
article.content = form.content.data

ArticleService().update_article(article)
flash(f'文章《{article.title}》更新成功', category='success')
return redirect(url_for('home_page'))
except Exception as e:
flash(f'文章《{article.title}》更新失败: {e}', category='danger')
return render_template('create_article.html', form=form, isEdit=True)

效果

修改这个 python 的输入语句 修改成 python 输入语句

删除文章

实现对页面中的文章进行删除

删除按钮的前端实现

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
{% extends 'base.html' %} {% block title %} zachary的记事本 - 主页 {% endblock
%} {% block content %}
<div class="container-xl">
{% for article in articles %}
<div class="card m-4" style="background-color: #bcd0c7">
<div class="card-header">
<ul class="nav">
<li class="nav-item me-auto">
<a
class="btn fs-5 fw-bold"
href="{{ url_for('article_page', article_id=article.id) }}"
>{{ article.title }}</a
>
</li>
{% if current_user.is_authenticated %}
<li class="nav-item px-1">
<small class="text-body-secondary">
<a
class="btn"
href="{{ url_for('edit_article_page', article_id=article.id) }}"
>编辑</a
>
</small>
</li>
<li class="nav-item px-1">
<small class="text-body-secondary">
<button
class="btn"
data-bs-toggle="modal"
data-bs-target="#Modal-DeleteConfirm-{{ article.id }}"
>
删除
</button>
</small>
</li>
{% endif %}
</ul>
</div>
<div class="card-body">
<p class="card-text">
<a
class="btn fs-6"
href="{{ url_for('article_page', article_id=article.id) }}"
>{{ article.content }}</a
>
</p>
<ul class="nav">
<li class="nav-item me-auto">
<small class="text-body-secondary"
>发布时间:{{ article.create_time }}</small
>
</li>
</ul>
</div>
</div>
{% endfor %}
</div>
{% endblock %}

实现删除文章后端逻辑

  • 创建 删除文章的表单类,article_delete_form.py
    • article_id并不需要用户输入,所以是一个隐藏字段
1
2
3
4
5
6
7
8
from flask_wtf import FlaskForm
from wtforms import HiddenField, SubmitField
from wtforms.validators import DataRequired


class ArticleDeleteForm(FlaskForm):
article_id = HiddenField(validators=[DataRequired()])
submit = SubmitField(label='删除')
  • 创建一个确认删除对话框,为了便捷地复用这个对话框,单独编写一个 html

/templates下创建一个/includes文件夹,然后创建一个article_modals.html用于存放与文章相关的 html

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
<!-- Delete Article Confirm -->
<div
class="modal fade"
id="Modal-DeleteConfirm-{{ article.id }}"
tabindex="-1"
aria-labelledby="deleteModalLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fs-5" id="deleteModalLabel">
{{ article.title }}
</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<form method="POST">
{{ article_delete_form.csrf_token }} {{
article_delete_form.article_id(value=article.id) }}
<div class="modal-body">
<h4>您确定要删除《{{ article.title }}》这篇文章吗?</h4>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
取消
</button>
<button type="submit" class="btn btn-primary">删除</button>
</div>
</form>
</div>
</div>
</div>
  • index.html中引入上面定义的对话框
1
2
{% if current_user.is_authenticated %} {% include 'includes/article_modals.html'
%} {% endif %}
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
{% extends 'base.html' %} {% block title %} zachary的记事本 - 主页 {% endblock
%} {% block content %}
<div class="container-xl">
{% for article in articles %} {% if current_user.is_authenticated %} {%
include 'includes/article_modals.html' %} {% endif %}
<div class="card m-4" style="background-color: #bcd0c7">
<div class="card-header">
<ul class="nav">
<li class="nav-item me-auto">
<a
class="btn fs-5 fw-bold"
href="{{ url_for('article_page', article_id=article.id) }}"
>{{ article.title }}</a
>
</li>
{% if current_user.is_authenticated %}
<li class="nav-item px-1">
<small class="text-body-secondary">
<a
class="btn"
href="{{ url_for('edit_article_page', article_id=article.id) }}"
>编辑</a
>
</small>
</li>
<li class="nav-item px-1">
<small class="text-body-secondary">
<button
class="btn"
data-bs-toggle="modal"
data-bs-target="#Modal-DeleteConfirm-{{ article.id }}"
>
删除
</button>
</small>
</li>
{% endif %}
</ul>
</div>
<div class="card-body">
<p class="card-text">
<a
class="btn fs-6"
href="{{ url_for('article_page', article_id=article.id) }}"
>{{ article.content }}</a
>
</p>
<ul class="nav">
<li class="nav-item me-auto">
<small class="text-body-secondary"
>发布时间:{{ article.create_time }}</small
>
</li>
</ul>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
  • user_routes.py中的 home_page 需要渲染一下表单,否则会出错
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
# -*- coding:utf-8 -*-
# Author: Zachary
from flask import render_template, abort, flash, redirect, url_for
from flask_login import current_user, logout_user

from forms.article_delete_form import ArticleDeleteForm
from forms.login_form import LoginForm
from routes import app
from services.article_service import ArticleService
from services.user_service import UserService


@app.route('/')
@app.route('/index')
def home_page():
articles = ArticleService().get_articles()
article_delete_form = ArticleDeleteForm()
return render_template('index.html', articles=articles, article_delete_form=article_delete_form)


@app.route('/article/<article_id>')
def article_page(article_id):
article = ArticleService().get_article_by_id(article_id)
if article:
return render_template('article.html', article=article)
abort(404)


@app.route('/about')
def about_page():
return render_template('about.html')


@app.route('/login', methods=['GET', 'POST'])
def login_page():
form = LoginForm()
if form.validate_on_submit():
result = UserService().do_login(form.account.data, form.password.data)
if result:
flash(f'{form.account.data}登录成功, 正在跳转...', category='success')
return redirect(url_for('home_page'))
else:
flash(f'{form.account.data}登录失败, 请检查账号密码', category='danger')
return render_template('login.html', form=form)


@app.route('/logout')
def logout_page():
logout_user()
return redirect(url_for('home_page'))

  • 新增一个delete_article的 service
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
# -*- coding:utf-8 -*-
# Author: Zachary
from sqlalchemy import Select, func, and_

from models.article import Article
from routes import db


class ArticleService:
def get_article_by_id(self, article_id):
return db.session.get(Article, article_id)

def get_articles(self):
query = Select(Article)
return db.session.scalars(query).all()

def add_article(self, article: Article):
# 先查询是否已经存在
query = Select(Article).where(Article.title == article.title)
if db.session.scalar(query):
# 抛出异常
raise Exception('该标题文章已经存在,请重新编辑')
db.session.add(article)
db.session.commit()
return article

def update_article(self, article: Article):
# 先查询是否已经存在
existed_article = self.get_article_by_id(article.id)
if not existed_article:
raise Exception('该文章不存在')
# 查询要更新的文章标题是否已经存在
query = Select(Article).where(and_(Article.title == article.title, Article.id != article.id))
if db.session.scalar(query):
raise Exception('该标题文章已经存在,请重新编辑')
# 更新
existed_article.title = article.title
existed_article.content = article.content
existed_article.update_time = func.now()

db.session.commit()
return article

def delete_article(self, article_id: int):
article = self.get_article_by_id(article_id)
if not article:
raise Exception('该文章不存在')
db.session.delete(article_id)
db.session.commit()
return article

  • 然后修改user_routes.py中的home_page的逻辑
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
# -*- coding:utf-8 -*-
# Author: Zachary
from flask import render_template, abort, flash, redirect, url_for
from flask_login import current_user, logout_user

from forms.article_delete_form import ArticleDeleteForm
from forms.login_form import LoginForm
from routes import app
from services.article_service import ArticleService
from services.user_service import UserService


@app.route('/', methods=['GET', 'POST'])
@app.route('/index', methods=['GET', 'POST'])
def home_page():
articles = ArticleService().get_articles()
if current_user.is_authenticated:
article_delete_form = ArticleDeleteForm()
if article_delete_form.validate_on_submit():
try:
ArticleService().delete_article(int(article_delete_form.article_id.data))
flash(f'文章删除成功', category='success')
return redirect(url_for('home_page'))
except Exception as e:
flash(f'文章删除失败: {e}', category='danger')
return render_template('index.html', articles=articles, article_delete_form=article_delete_form)
return render_template('index.html', articles=articles)


@app.route('/article/<article_id>')
def article_page(article_id):
article = ArticleService().get_article_by_id(article_id)
if article:
return render_template('article.html', article=article)
abort(404)


@app.route('/about')
def about_page():
return render_template('about.html')


@app.route('/login', methods=['GET', 'POST'])
def login_page():
form = LoginForm()
if form.validate_on_submit():
result = UserService().do_login(form.account.data, form.password.data)
if result:
flash(f'{form.account.data}登录成功, 正在跳转...', category='success')
return redirect(url_for('home_page'))
else:
flash(f'{form.account.data}登录失败, 请检查账号密码', category='danger')
return render_template('login.html', form=form)


@app.route('/logout')
def logout_page():
logout_user()
return redirect(url_for('home_page'))

效果

新建一个要删除的文章

点击删除按钮

#

引入 markdown 显示

为了让记事本的内容更加丰富,引入 markdown 语法,是的文本内容显示得更加美观些

markdown 资源下载

  • 将上述两个资源存储至 ☞/static/plugins/showdownjs-2.0.0

引入 markdown 资源

  • base.html中引入添加的 markdown 资源
    • <script src="/static/plugins/showdownjs-2.0.0/showdown.min.js"></script>
  • 并添加上 markdown 的预定义样式 <style>
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<!-- Bootstrap CSS -->
<link
rel="stylesheet"
href="/static/plugins/bootstrap-5.3.2/bootstrap.min.css"
/>
<script src="/static/plugins/bootstrap-5.3.2/bootstrap.bundle.min.js"></script>
<script src="/static/plugins/jquery-3.7.1/jquery.min.js"></script>
<script src="/static/plugins/showdownjs-2.0.0/showdown.min.js"></script>

<style>
pre {
white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
background-color: #f8f8f8;
border: 1px solid #e7e7e7;
margin-top: 1.5em;
margin-bottom: 1.5em;
padding: 0.125em 0.3125em 0.0625em;
}

pre code {
background-color: transparent;
border: 0;
padding: 0;
}
</style>

<title>{% block title %} {% endblock %}</title>
</head>
<body>
<nav
class="navbar navbar-expand-md navbar-dark bg-dark ps-4 pe-4"
style="background-color: #1976d2; color: #ffffff;"
>
<a class="navbar-brand" href="#" style="font-size: 20px; padding: 10px;"
>Zachary的记事本</a
>
<button
class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarNav"
style="border: none; background-color: transparent; padding: 5px 10px;"
>
<span
class="navbar-toggler-icon"
style="background-color: #1976d2;"
></span>
</button>
<div
class="collapse navbar-collapse"
id="navbarNav"
style="width: 80%; margin: auto; border-radius: 10px;"
>
<ul class="navbar-nav me-auto" style="margin-top: 20px;">
<li class="nav-item active" style="border-bottom: 2px solid #4caf50;">
<a
class="nav-link"
href="{{ url_for('home_page') }}"
style="color: #ffffff; padding: 10px;"
>主页</a
>
</li>
<li class="nav-item" style="border-bottom: 2px solid #4caf50;">
<a
class="nav-link"
href="{{ url_for('about_page') }}"
style="color: #ffffff; padding: 10px;"
>关于</a
>
</li>
</ul>
{% if current_user.is_authenticated %}
<ul class="navbar-nav">
<li class="nav-item">
<a
class="nav-link"
href="{{ url_for('create_article_page') }}"
style="color: #ffffff; padding: 10px;"
>发布新文章</a
>
</li>
<li class="nav-item">
<a
class="nav-link"
href="{{ url_for('logout_page') }}"
style="color: #ffffff; padding: 10px;"
>退出</a
>
</li>
</ul>
{% else %}
<ul class="navbar-nav">
<li class="nav-item">
<a
class="nav-link"
href="{{ url_for('login_page') }}"
style="color: #ffffff; padding: 10px;"
>登录</a
>
</li>
</ul>
{% endif %}
</div>
</nav>
{% with messages = get_flashed_messages(with_categories=true) %} {% if
messages %} {% for category, message in messages %}
<div
class="alert alert-{{ category }} alert-dismissible fade show"
role="alert"
>
{{ message }}
<button
type="button"
class="btn-close"
data-bs-dismiss="alert"
aria-label="Close"
></button>
</div>
{% endfor %} {% endif %} {% endwith %} {% block content %} {% endblock %}
</body>
</html>

修改文章内容显示

首先修改article.html,让显示的内容更加美观

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{% extends 'base.html' %} {% block title %} zachary的记事本 - {{ article.title
}} {% endblock %} {% block content %}

<textarea id="article_content" style="display: none">
{{ article.content }}</textarea
>
<div class="container-xl">
<h4>
<p class="text-center" style="margin-top: 20px;">{{ article.title }}</p>
</h4>
<p class="text-center" style="margin-top: 10px;">
发布于:{{ article.create_time }} 更新于:{{ article.update_time }}
</p>
<p id="article_viewer"></p>
</div>

<script src="/static/js/article.js"></script>
{% endblock %}

然后创建 js,用于使用资源渲染文章内容 作为 markdown 显示出来,在/static/js/下新增article.js

1
2
3
4
5
$(function () {
var converter = new showdown.Converter();
var article_html = converter.makeHtml($("#article_content").val());
$("#article_viewer").html(article_html);
});

编辑文章时可预览 markdown

首先修改一下编辑/新增时的界面

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
{% extends 'base.html' %} {% block title %} zachary的记事本 - {% if isEdit %}
编辑文章 {% else %} 发布新文章 {% endif %} {% endblock %} {% block content %}
<style>
.content_height {
height: 550px;
}
</style>
<div class="container-fluid px-4 py-4">
<form method="POST" class="form-signin">
{{ form.hidden_tag() }}
<h1 class="h3 mb-3 font-weight-normal">
{% if isEdit %} 编辑文章 {% else %} 发布新文章 {% endif %}
</h1>
<br />
{{ form.title.label }} {{ form.title(class="form-control",
placeholder="请输入标题") }}
<div class="row">
<div class="col">
{{ form.content.label }} {{ form.content(class="form-control
content_height", placeholder="请输入内容") }}
<br />
{{ form.submit(class="btn btn-lg btn-primary btn-block") }}
<a
href="#"
id="article_previewer_btn"
class="btn btn-lg btn-primary btn-block"
>预览</a
>
</div>
<div class="col">
内容预览
<div class="container-fluid border border-success">
<div
id="article_previewer"
class="content_height overflow-auto"
></div>
</div>
</div>
</div>
</form>
</div>

<script src="/static/js/create_article.js"></script>
{% endblock %}

然后给这个预览按钮实现 js 功能,点击后用 markdown 对文字内容渲染然后显示

/static/js/下创建一个create_article.js

1
2
3
4
5
6
7
$(function () {
$("#article_previewer_btn").click(function () {
var converter = new showdown.Converter();
var article_html = converter.makeHtml($("#content").val());
$("#article_previewer").html(article_html);
});
});

消除数据库中 password 的明文显示

需要使用到一个库 bcrypt 4.1.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
click==8.1.7
Flask==2.2.5
importlib-metadata==6.7.0
itsdangerous==2.1.2
Jinja2==3.1.3
MarkupSafe==2.1.4
typing_extensions==4.7.1
Werkzeug==2.2.3
zipp==3.15.0
mysqlclient==2.1.1
SQLAlchemy==2.0.25
Flask-SQLAlchemy==3.0.0
Flask-WTF==1.1.1
flask-login==0.6.3
bcrypt==4.1.1
  • bcrypt 的两种基础使用
    • hashed_pw = bcrypt.hashpw(password.encode(), bcrypt.gensalt()) 依据生成的盐值,生成 hash 后的密码值,结果是字节码
    • bcrypt.check(check_pw.encode(), hashed_pw)返回值结果即为密码是否匹配
  • 将数据库中的明文密码 进行一个手动替换(实际这个步骤应该是在注册的时候操作,这里省略)
    • 将这块你生成的密码值 (我的是 $2b$12$VSVvNnaGZbQLspsbKosC9e4cLdh/Rrq8uviwSKT7DlUagITHZsuMi)替换到数据库中的users表中
1
2
3
4
5
6
7
8
9
10
11
(venv) ➜  flaskProjectInitial python3
Python 3.7.7 (v3.7.7:d7c567b08f, Mar 10 2020, 02:56:16)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import bcrypt
>>> password = '123456'
>>> hashed_pw = bcrypt.hashpw(password.encode(),bcrypt.gensalt())
>>> print(hashed_pw.decode('utf-8'))
$2b$12$VSVvNnaGZbQLspsbKosC9e4cLdh/Rrq8uviwSKT7DlUagITHZsuMi
>>>

  • 修改user.py中实体类User的密码检查方法
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
# -*- coding:utf-8 -*-
# Author: Zachary
import crypt

import bcrypt
from flask_login import UserMixin
from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column

from routes import db, login_manager


@login_manager.user_loader
def load_manager_user(user_id: int):
return db.session.get(User, user_id)


class User(db.Model, UserMixin):
__tablename__ = 'users'

id: Mapped[int] = mapped_column(Integer, primary_key=True)
account: Mapped[str] = mapped_column(String(128), unique=True, nullable=False)
password: Mapped[str] = mapped_column(String(64), nullable=False)
username: Mapped[str] = mapped_column(String(128), nullable=False)
description: Mapped[str] = mapped_column(String(255), nullable=True)

def password_check(self, password):
return bcrypt.checkpw(password.encode(), self.password.encode())

错误密码的登录

正确密码的登录

实现图片功能

图片上传

图片上传表单

/forms下新增一个image_upload_form.py

1
2
3
4
5
6
7
8
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired
from wtforms import SubmitField


class ImageUploadForm(FlaskForm):
image = FileField(label='上传图片', validators=[FileRequired()])
submit = SubmitField(label='上传')

图片上传的前端

/templates下创建一个images.html

1
2
3
4
5
6
7
8
9
10
11
12
{% extends 'base.html' %} {% block title %} zachary的记事本 - 图片管理 {%
endblock %} {% block content %}
<div class="container-xl">
<form method="POST" class="form-signin" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<h1 class="h3 mb-3 font-weight-normal">上传图片</h1>
<br />
{{ form.image.label }} {{ form.image(class="form-control") }} {{
form.submit(class="btn btn-lg btn-primary btn-block") }}
</form>
</div>
{% endblock %}

其中 enctype="multipart/form-data"是用于需要上传文件的表单

图片上传目录

为了保存所上传的图片,需要准备一个目录用于存储上传的图片

  • 在项目目录下创建一个/data项目文件夹,其中上传的图片都保存在/data/image目录下
  • 同时,为了让代码中能方便地获取到这个路径,即项目部署后的一个绝对路径,做一个准备工作:在项目目录下创建一个/common文件夹,创建一个profile.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pathlib import Path


class Profile:
__image_path = None

@staticmethod
def get_image_path():
project_path = Path(__file__).parent.parent # 获取项目根目录
images_path = project_path.joinpath("data/images")
if not images_path.exists():
images_path.mkdir(parents=True)
Profile.__image_path = images_path
return images_path

图片上传的路由定义

admin_routes.py中新增images_page

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# -*- coding:utf-8 -*-
# Author: Zachary
from flask import flash, redirect, url_for, render_template, request
from flask_login import login_required
from werkzeug.utils import secure_filename

from common.profile import Profile
from forms.article_form import ArticleForm
from forms.image_upload_form import ImageUploadForm
from models.article import Article
from routes import app
from services.article_service import ArticleService


@app.route('/create_article', methods=['GET', 'POST'])
@login_required
def create_article_page():
form = ArticleForm()
if form.validate_on_submit():
article = Article()
article.title = form.title.data
article.content = form.content.data

try:
ArticleService().add_article(article)
flash(f'文章《{article.title}》创建成功', category='success')
return redirect(url_for('home_page'))
except Exception as e:
flash(f'文章《{article.title}》创建失败: {e}', category='danger')
return render_template('create_article.html', form=form, isEdit=False)


@app.route('/edit_article/<article_id>', methods=['GET', 'POST'])
@login_required
def edit_article_page(article_id: str):
form = ArticleForm()
# 点击编辑后 回显数据
if request.method == 'GET':
try:
article = ArticleService().get_article_by_id(article_id)
if not article:
flash("文章不存在", category='danger')
return redirect(url_for('home_page'))
else:
form.title.data = article.title
form.content.data = article.content
except Exception as e:
flash(f'文章获取失败: {e}', category='danger')
return redirect(url_for('home_page'))

# 更新数据后 保存
if form.validate_on_submit():
article = Article()
try:
article.id = int(article_id)
article.title = form.title.data
article.content = form.content.data

ArticleService().update_article(article)
flash(f'文章《{article.title}》更新成功', category='success')
return redirect(url_for('home_page'))
except Exception as e:
flash(f'文章《{article.title}》更新失败: {e}', category='danger')
return render_template('create_article.html', form=form, isEdit=True)


@app.route('/images', methods=['GET', 'POST'])
@login_required
def images_page():
form = ImageUploadForm()
if form.validate_on_submit():
image_file = form.image.data
image_path = Profile.get_image_path()
image_filename = secure_filename(image_file.filename)
image_full_path = image_path.joinpath(image_filename)
image_file.save(image_full_path)
flash(f'图片上传成功, 保存至 {image_full_path}', category='success')
return render_template('images.html', form=form)

尝试一下直接访问路由,是可以的

接下来还需要做两件事,一个是当上传重复名称图片时,是否要覆盖的问题;另一个是主页需要有一个按钮跳转至图片上传页面

给命名冲突图片重命名

/common下新建一个utils.py

现在需要实现的是,通过输入路径和文件名,先判断图片存储路径下是否有这个文件,如果没有直接返回;如果有这个文件,修改成一个文件名_1 这样的格式返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# -*- coding:utf-8 -*-
# Author: Zachary
from pathlib import Path


def get_file_name_patrs(filename):
pos = filename.rfind('.')
if pos == -1:
return filename, ''
return filename[:pos], filename[pos + 1:]


def get_save_filepath(filePath: Path, filename: str):
save_file = filePath.joinpath(filename)
if not save_file.exists():
return save_file

name, ext = get_file_name_patrs(filename)
index = 1
while True:
save_file = filePath.joinpath(f'{name}_{index}.{ext}')
if not save_file.exists():
return save_file
index += 1

然后修改一下admin_routes.py

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# -*- coding:utf-8 -*-
# Author: Zachary
from flask import flash, redirect, url_for, render_template, request
from flask_login import login_required
from werkzeug.utils import secure_filename

from common import utils
from common.profile import Profile
from forms.article_form import ArticleForm
from forms.image_upload_form import ImageUploadForm
from models.article import Article
from routes import app
from services.article_service import ArticleService


@app.route('/create_article', methods=['GET', 'POST'])
@login_required
def create_article_page():
form = ArticleForm()
if form.validate_on_submit():
article = Article()
article.title = form.title.data
article.content = form.content.data

try:
ArticleService().add_article(article)
flash(f'文章《{article.title}》创建成功', category='success')
return redirect(url_for('home_page'))
except Exception as e:
flash(f'文章《{article.title}》创建失败: {e}', category='danger')
return render_template('create_article.html', form=form, isEdit=False)


@app.route('/edit_article/<article_id>', methods=['GET', 'POST'])
@login_required
def edit_article_page(article_id: str):
form = ArticleForm()
# 点击编辑后 回显数据
if request.method == 'GET':
try:
article = ArticleService().get_article_by_id(article_id)
if not article:
flash("文章不存在", category='danger')
return redirect(url_for('home_page'))
else:
form.title.data = article.title
form.content.data = article.content
except Exception as e:
flash(f'文章获取失败: {e}', category='danger')
return redirect(url_for('home_page'))

# 更新数据后 保存
if form.validate_on_submit():
article = Article()
try:
article.id = int(article_id)
article.title = form.title.data
article.content = form.content.data

ArticleService().update_article(article)
flash(f'文章《{article.title}》更新成功', category='success')
return redirect(url_for('home_page'))
except Exception as e:
flash(f'文章《{article.title}》更新失败: {e}', category='danger')
return render_template('create_article.html', form=form, isEdit=True)


@app.route('/images', methods=['GET', 'POST'])
@login_required
def images_page():
form = ImageUploadForm()
if form.validate_on_submit():
image_file = form.image.data
image_path = Profile.get_image_path()
image_filename = secure_filename(image_file.filename)
image_full_path = utils.get_save_filepath(image_path, image_filename)
image_file.save(image_full_path)
flash(f'图片上传成功, 保存至 {image_full_path}', category='success')
return render_template('images.html', form=form)

上传图片跳转按钮

  • base.html处添加一下图片管理按钮
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<!-- Bootstrap CSS -->
<link
rel="stylesheet"
href="/static/plugins/bootstrap-5.3.2/bootstrap.min.css"
/>
<script src="/static/plugins/bootstrap-5.3.2/bootstrap.bundle.min.js"></script>
<script src="/static/plugins/jquery-3.7.1/jquery.min.js"></script>
<script src="/static/plugins/showdownjs-2.0.0/showdown.min.js"></script>

<style>
pre {
white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
background-color: #f8f8f8;
border: 1px solid #e7e7e7;
margin-top: 1.5em;
margin-bottom: 1.5em;
padding: 0.125em 0.3125em 0.0625em;
}

pre code {
background-color: transparent;
border: 0;
padding: 0;
}
</style>

<title>{% block title %} {% endblock %}</title>
</head>
<body>
<nav
class="navbar navbar-expand-md navbar-dark bg-dark ps-4 pe-4"
style="background-color: #1976d2; color: #ffffff;"
>
<a class="navbar-brand" href="#" style="font-size: 20px; padding: 10px;"
>Zachary的记事本</a
>
<button
class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarNav"
style="border: none; background-color: transparent; padding: 5px 10px;"
>
<span
class="navbar-toggler-icon"
style="background-color: #1976d2;"
></span>
</button>
<div
class="collapse navbar-collapse"
id="navbarNav"
style="width: 80%; margin: auto; border-radius: 10px;"
>
<ul class="navbar-nav me-auto" style="margin-top: 20px;">
<li class="nav-item active" style="border-bottom: 2px solid #4caf50;">
<a
class="nav-link"
href="{{ url_for('home_page') }}"
style="color: #ffffff; padding: 10px;"
>主页</a
>
</li>
<li class="nav-item" style="border-bottom: 2px solid #4caf50;">
<a
class="nav-link"
href="{{ url_for('about_page') }}"
style="color: #ffffff; padding: 10px;"
>关于</a
>
</li>
</ul>
{% if current_user.is_authenticated %}
<ul class="navbar-nav">
<li class="nav-item">
<a
class="nav-link"
href="{{ url_for('images_page') }}"
style="color: #ffffff; padding: 10px;"
>图片管理</a
>
</li>
<li class="nav-item">
<a
class="nav-link"
href="{{ url_for('create_article_page') }}"
style="color: #ffffff; padding: 10px;"
>发布新文章</a
>
</li>
<li class="nav-item">
<a
class="nav-link"
href="{{ url_for('logout_page') }}"
style="color: #ffffff; padding: 10px;"
>退出</a
>
</li>
</ul>
{% else %}
<ul class="navbar-nav">
<li class="nav-item">
<a
class="nav-link"
href="{{ url_for('login_page') }}"
style="color: #ffffff; padding: 10px;"
>登录</a
>
</li>
</ul>
{% endif %}
</div>
</nav>
{% with messages = get_flashed_messages(with_categories=true) %} {% if
messages %} {% for category, message in messages %}
<div
class="alert alert-{{ category }} alert-dismissible fade show"
role="alert"
>
{{ message }}
<button
type="button"
class="btn-close"
data-bs-dismiss="alert"
aria-label="Close"
></button>
</div>
{% endfor %} {% endif %} {% endwith %} {% block content %} {% endblock %}
</body>
</html>

图片下载

访问图片的路由定义

图片下载的权限是普通用户也可以 不需要登录

所以在/routes/user_routes.py下实现

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
65
66
67
68
69
# -*- coding:utf-8 -*-
# Author: Zachary
from flask import render_template, abort, flash, redirect, url_for, send_from_directory
from flask_login import current_user, logout_user

from common.profile import Profile
from forms.article_delete_form import ArticleDeleteForm
from forms.login_form import LoginForm
from routes import app
from services.article_service import ArticleService
from services.user_service import UserService


@app.route('/', methods=['GET', 'POST'])
@app.route('/index', methods=['GET', 'POST'])
def home_page():
articles = ArticleService().get_articles()
if current_user.is_authenticated:
article_delete_form = ArticleDeleteForm()
if article_delete_form.validate_on_submit():
try:
ArticleService().delete_article(int(article_delete_form.article_id.data))
flash(f'文章删除成功', category='success')
return redirect(url_for('home_page'))
except Exception as e:
flash(f'文章删除失败: {e}', category='danger')
return render_template('index.html', articles=articles, article_delete_form=article_delete_form)
return render_template('index.html', articles=articles)


@app.route('/article/<article_id>')
def article_page(article_id):
article = ArticleService().get_article_by_id(article_id)
if article:
return render_template('article.html', article=article)
abort(404)


@app.route('/about')
def about_page():
return render_template('about.html')


@app.route('/login', methods=['GET', 'POST'])
def login_page():
form = LoginForm()
if form.validate_on_submit():
result = UserService().do_login(form.account.data, form.password.data)
if result:
flash(f'{form.account.data}登录成功, 正在跳转...', category='success')
return redirect(url_for('home_page'))
else:
flash(f'{form.account.data}登录失败, 请检查账号密码', category='danger')
return render_template('login.html', form=form)


@app.route('/logout')
def logout_page():
logout_user()
return redirect(url_for('home_page'))


@app.route('/image/<image_filename>')
def download_image(image_filename: str):
image_path = Profile.get_image_path()
image_filepath = image_path.joinpath(image_filename)
if not image_filepath.exists():
abort(404)
return send_from_directory(directory=image_path, path=image_filename)

这样可以获取到对应文件名的图片

显示所有图片的前端

images.html处新增一个显示所有图片信息的功能

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
{% extends 'base.html' %} {% block title %} zachary的记事本 - 图片管理 {%
endblock %} {% block content %}
<div class="container-xl">
<form method="POST" class="form-signin" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<h1 class="h3 mb-3 font-weight-normal">上传图片</h1>
<br />
{{ form.image.label }} {{ form.image(class="form-control") }} {{
form.submit(class="btn btn-lg btn-primary btn-block") }}
</form>
<hr />

<div class="row">
{% if image_filenames %} {% for image_filename in image_filenames %}
<div class="col-md-3">
<b>/image/{{ image_filename }}</b>
<img
src="/image/{{ image_filename }}"
alt=""
class="img-thumbnail my-2"
style="width: 300px; height: 300px"
/>
</div>
{% endfor %} {% endif %}
</div>
</div>
{% endblock %}

获取所有图片列表

为了给前端返回所有图片的路径名称,需要一个 service,在/services下创建一个image_service.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# -*- coding:utf-8 -*-
# Author: Zachary
from common.profile import Profile


class ImageService:

def get_image_filename_list(self):
image_path = Profile.get_image_path()
filename_list = []
if image_path.exists():
for filename in image_path.iterdir():
if filename.is_file():
filename_list.append(filename.name)
return filename_list

然后再去修改一下admin_routes.pyimages_page的渲染结果

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# -*- coding:utf-8 -*-
# Author: Zachary
from flask import flash, redirect, url_for, render_template, request
from flask_login import login_required
from werkzeug.utils import secure_filename

from common import utils
from common.profile import Profile
from forms.article_form import ArticleForm
from forms.image_upload_form import ImageUploadForm
from models.article import Article
from routes import app
from services.article_service import ArticleService
from services.image_service import ImageService


@app.route('/create_article', methods=['GET', 'POST'])
@login_required
def create_article_page():
form = ArticleForm()
if form.validate_on_submit():
article = Article()
article.title = form.title.data
article.content = form.content.data

try:
ArticleService().add_article(article)
flash(f'文章《{article.title}》创建成功', category='success')
return redirect(url_for('home_page'))
except Exception as e:
flash(f'文章《{article.title}》创建失败: {e}', category='danger')
return render_template('create_article.html', form=form, isEdit=False)


@app.route('/edit_article/<article_id>', methods=['GET', 'POST'])
@login_required
def edit_article_page(article_id: str):
form = ArticleForm()
# 点击编辑后 回显数据
if request.method == 'GET':
try:
article = ArticleService().get_article_by_id(article_id)
if not article:
flash("文章不存在", category='danger')
return redirect(url_for('home_page'))
else:
form.title.data = article.title
form.content.data = article.content
except Exception as e:
flash(f'文章获取失败: {e}', category='danger')
return redirect(url_for('home_page'))

# 更新数据后 保存
if form.validate_on_submit():
article = Article()
try:
article.id = int(article_id)
article.title = form.title.data
article.content = form.content.data

ArticleService().update_article(article)
flash(f'文章《{article.title}》更新成功', category='success')
return redirect(url_for('home_page'))
except Exception as e:
flash(f'文章《{article.title}》更新失败: {e}', category='danger')
return render_template('create_article.html', form=form, isEdit=True)


@app.route('/images', methods=['GET', 'POST'])
@login_required
def images_page():
form = ImageUploadForm()
if form.validate_on_submit():
image_file = form.image.data
image_path = Profile.get_image_path()
image_filename = secure_filename(image_file.filename)
image_full_path = utils.get_save_filepath(image_path, image_filename)
image_file.save(image_full_path)
flash(f'图片上传成功, 保存至 {image_full_path}', category='success')

image_filenames = ImageService().get_image_filename_list()
return render_template('images.html', form=form, image_filenames=image_filenames)

在文章中使用图片

在图片管理的页面可以看到每一张图片上方的文字,在记事本中可以使用 markdown 语法显示该图片

这样上传的记事内容也可以看到图片了

同时也支持 html 语法格式,这样可以很方便地对图片做格式限定

docker 部署

准备工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# -*- coding:utf-8 -*-
# Author: Zachary
import os
from flask import Flask
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy

MYSQL_HOST = os.getenv('MYSQL_HOST', 'localhost')
MYSQL_PORT = os.getenv('MYSQL_PORT', 3306)
MYSQL_USER = os.getenv('MYSQL_USER', 'root')
MYSQL_PASSWORD = os.getenv('MYSQL_PASSWORD', '123456')
MYSQL_DB = os.getenv('MYSQL_DB', 'mynotebook_db')

app = Flask(__name__, template_folder='../templates', static_folder='../static', static_url_path='/static')

app.config[
'SQLALCHEMY_DATABASE_URI'] = f'mysql+mysqldb://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DB}'
app.config['SECRET_KEY'] = 'zachary123456'

db = SQLAlchemy(app)
login_manager = LoginManager(app)

from routes import user_routes
from routes import admin_routes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from sqlalchemy import inspect

from routes import app, db
import bcrypt


def init_db():
with app.app_context():
inspector = inspect(db.engine)
if not inspector.has_table('users'):
from models.user import User
from models.article import Article
db.create_all()
hashed_password = bcrypt.hashpw("123456".encode(), bcrypt.gensalt())
user = User(account="zachary", password=hashed_password.decode(), username="zachary")
db.session.add(user)
db.session.commit()


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FROM ubuntu

COPY . /opt/mynotebook/

WORKDIR /opt/mynotebook/

RUN apt update
RUN apt-get install -y python3 python3-pip
RUN apt-get install -y pkg-config
RUN apt-get install -y libmysqlclient-dev

RUN pip3 install --upgrade pip
RUN pip3 install -r requirements.txt

ENV PYTHONPATH=/opt/mynotebook/

ENTRYPOINT ["python3", "app.py"]
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
version: "3.8"
services:
mynotebook_server:
build: .
image: mynotebook
container_name: mynotebook_server
ports:
- "80:8080"
links:
- mysql_server
environment:
MYSQL_HOST: mysql_server
MYSQL_PORT: 3306
MYSQL_USER: root
MYSQL_PASSWORD: 123456
MYSQL_DB: mynotebook_db
volumes:
- /opt/mynotebook_data:/opt/mynotebook/data
depends_on:
mysql_server:
condition: service_healthy

mysql_server:
image: mysql
container_name: mysql_server
volumes:
- /opt/mysql:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: mynotebook_db
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 20s
retries: 10

服务器准备代码

完成上面四个部署准备文件后,将代码提交到 GitHub 上面

拿到地址: https://github.com/BlockZachary/mynotebook.git

然后去服务器上 克隆一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
root@ubuntu-server:~/pythonProject# ls
cmdDockerSample cmdEntrypointDockerSample entrypointDockerSample flaskDockerSample
root@ubuntu-server:~/pythonProject# git clone https://github.com/BlockZachary/mynotebook.git
Cloning into 'mynotebook'...
remote: Enumerating objects: 61, done.
remote: Counting objects: 100% (61/61), done.
remote: Compressing objects: 100% (54/54), done.
remote: Total 61 (delta 3), reused 61 (delta 3), pack-reused 0
Unpacking objects: 100% (61/61), 447.81 KiB | 16.00 KiB/s, done.
root@ubuntu-server:~/pythonProject# ls
cmdDockerSample cmdEntrypointDockerSample entrypointDockerSample flaskDockerSample mynotebook
root@ubuntu-server:~/pythonProject# cd mynotebook/
root@ubuntu-server:~/pythonProject/mynotebook# ls
app.py data Dockerfile models routes static
common docker-compose.yaml forms requirements.txt services templates
root@ubuntu-server:~/pythonProject/mynotebook#

然后 build 一下镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
root@ubuntu-server:~/pythonProject/mynotebook# docker compose build
[+] Building 50.1s (8/13) docker:default
=> [mynotebook_server internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 372B 0.0s
=> [mynotebook_server internal] load metadata for docker.io/library/ubuntu:latest 0.0s
=> [mynotebook_server internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [mynotebook_server internal] load build context 0.1s
=> => transferring context: 1.76MB 0.1s
=> CACHED [mynotebook_server 1/9] FROM docker.io/library/ubuntu:latest 0.0s
=> [mynotebook_server 2/9] COPY . /opt/mynotebook/ 0.1s
=> [mynotebook_server 3/9] WORKDIR /opt/mynotebook/ 0.0s
=> [mynotebook_server 4/9] RUN apt update 12.5s
=> [mynotebook_server 5/9] RUN apt-get install -y python3 python3-pip 37.3s
=> => # Get:15 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 perl-modules-5.34 all 5.34.0-3ubuntu1.3 [297
=> => # 6 kB]
=> => # Get:16 http://archive.ubuntu.com/ubuntu jammy/main amd64 libgdbm6 amd64 1.23-1 [33.9 kB]
=> => # Get:17 http://archive.ubuntu.com/ubuntu jammy/main amd64 libgdbm-compat4 amd64 1.23-1 [6606 B]
=> => # Get:18 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 libperl5.34 amd64 5.34.0-3ubuntu1.3 [4820 kB
=> => # ]
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
root@ubuntu-server:~/pythonProject/mynotebook# docker compose build
[+] Building 424.9s (14/14) FINISHED docker:default
=> [mynotebook_server internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 525B 0.0s
=> [mynotebook_server internal] load metadata for docker.io/library/ubuntu:latest 0.0s
=> [mynotebook_server internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [mynotebook_server internal] load build context 0.0s
=> => transferring context: 11.39kB 0.0s
=> CACHED [mynotebook_server 1/9] FROM docker.io/library/ubuntu:latest 0.0s
=> [mynotebook_server 2/9] COPY . /opt/mynotebook/ 0.1s
=> [mynotebook_server 3/9] WORKDIR /opt/mynotebook/ 0.0s
=> [mynotebook_server 4/9] RUN apt update 232.7s
=> [mynotebook_server 5/9] RUN apt-get install -y python3 python3-pip 128.8s
=> [mynotebook_server 6/9] RUN apt-get install -y pkg-config 22.6s
=> [mynotebook_server 7/9] RUN apt-get install -y libmysqlclient-dev 11.1s
=> [mynotebook_server 8/9] RUN pip3 install --upgrade pip -i http://mirrors.aliyun.com/pypi/simple/ --trusted-h 6.2s
=> [mynotebook_server 9/9] RUN pip3 install -r requirements.txt -i http://mirrors.aliyun.com/pypi/simple/ --tr 18.8s
=> [mynotebook_server] exporting to image 4.2s
=> => exporting layers 4.2s
=> => writing image sha256:6faeda711c94d58d72fdc430d0304bd8ebe60fef277f2186aa20375fb5bd0133 0.0s
=> => naming to docker.io/library/mynotebook 0.1s
root@ubuntu-server:~/pythonProject/mynotebook# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mynotebook latest 6faeda711c94 39 seconds ago 609MB
dbmanager latest 8cea6473ccdd 4 days ago 584MB
myos latest eb06c6d74ffa 5 days ago 127MB
entrypointdockersample latest d3e54cbe8477 6 days ago 155MB
cmddockersample latest 153eeb8273c4 6 days ago 155MB
cmdentrypointdockersample latest 01ab92cd6837 6 days ago 155MB
myflasksample latest e4e6142e4b41 6 days ago 484MB
zacharyblock/myflasksample latest e4e6142e4b41 6 days ago 484MB
mysql 8.0 b6188b3dc37c 12 days ago 603MB
mysql latest 56b21e040954 12 days ago 632MB
ubuntu latest e34e831650c1 2 weeks ago 77.9MB
wordpress latest 14425bb2eae9 7 weeks ago 739MB
nginx latest a8758716bb6a 3 months ago 187MB
hello-world latest d2c94e258dcb 9 months ago 13.3kB
nginx 1.24.0 f373f7a623e7 9 months ago 142MB

启动

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
root@ubuntu-server:~/pythonProject/mynotebook# docker compose up
[+] Running 3/2
✔ Network mynotebook_default Created 0.1s
✔ Container mysql_server Created 0.1s
✔ Container mynotebook_server Created 0.0s
Attaching to mynotebook_server, mysql_server
mysql_server | 2024-01-31 17:32:56+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.3.0-1.el8 started.
mysql_server | 2024-01-31 17:32:57+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'
mysql_server | 2024-01-31 17:32:57+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.3.0-1.el8 started.
mysql_server | '/var/lib/mysql/mysql.sock' -> '/var/run/mysqld/mysqld.sock'
mysql_server | 2024-01-31T17:32:57.518161Z 0 [System] [MY-015015] [Server] MySQL Server - start.
mysql_server | 2024-01-31T17:33:01.818805Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.3.0) starting as process 1
mysql_server | 2024-01-31T17:33:01.971261Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
mysql_server | 2024-01-31T17:33:04.004090Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
mysql_server | 2024-01-31T17:33:04.899718Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
mysql_server | 2024-01-31T17:33:04.899769Z 0 [System] [MY-013602] [Server] Channel mysql_main configured to support TLS. Encrypted connections are now supported for this channel.
mysql_server | 2024-01-31T17:33:04.902925Z 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.
mysql_server | 2024-01-31T17:33:04.947686Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.3.0' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server - GPL.
mysql_server | 2024-01-31T17:33:04.948714Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Bind-address: '::' port: 33060, socket: /var/run/mysqld/mysqlx.sock
mynotebook_server | * Serving Flask app 'routes'
mynotebook_server | * Debug mode: on
mynotebook_server | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
mynotebook_server | * Running on all addresses (0.0.0.0)
mynotebook_server | * Running on http://127.0.0.1:8080
mynotebook_server | * Running on http://172.21.0.3:8080
mynotebook_server | Press CTRL+C to quit
mynotebook_server | * Restarting with stat
mynotebook_server | * Debugger is active!
mynotebook_server | * Debugger PIN: 503-489-69

更新: 2024-02-01 01:39:09
原文: https://www.yuque.com/zacharyblock/cx2om6/fuixlxluoen5prgg