发布帖子
发布帖子模块与注册模块有些类似,都是用来收集用户输入数据并发送到服务器。与注册模块不同的是,发布帖子需要选择帖子所属板块,帖子内容是富文本内容,下面分别进行讲解。
添加帖子相关模型
在实现发布帖子功能之前,先创建与帖子相关的模型,包括板块模型、帖子模型、评论模型。打开 models/post.py 文件,添加以下代码。
from exts import db
from datetime import datetime
# 板块模型
class BoardModel(db.Model):
__tablename__ = 'board'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(20), nullable=False)
create_time = db.Column(db.DateTime, default=datetime.now)
is_active = db.Column(db.Boolean, default=True)
# 帖子模型
class PostModel(db.Model):
__tablename__ = 'post'
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)
create_time = db.Column(db.DateTime, default=datetime.now)
read_count = db.Column(db.Integer, default=0)
is_active = db.Column(db.Boolean, default=True)
board_id = db.Column(db.Integer, db.ForeignKey("board.id"))
author_id = db.Column(db.String(100), db.ForeignKey("user.id"), nullable=False)
board = db.relationship("BoardModel", backref="posts")
author = db.relationship("UserModel", backref='posts')
# 评论模型
class CommentModel(db.Model):
__tablename__ = 'comment'
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)
is_active = db.Column(db.Boolean, default=True)
post_id = db.Column(db.Integer, db.ForeignKey("post.id"))
author_id = db.Column(db.String(100), db.ForeignKey("user.id"), nullable=False)
post = db.relationship("PostModel", backref='comments')
author = db.relationship("UserModel", backref='comments')
模型需要被直接或者间接地导入 app.py 文件才能被检索到,在 blueprints/front.py 文件中导入以上模型,然后在 PyCharm 的 Terminal 中执行以下两条命令,即可将模型同步到数据库中。
$ flask db migrate -m "add post comment board model"
$ flask db upgrade
初始化板块数据
在网站上线前,应该先在数据库中添加一些板块数据,而且板块的数据一旦确定了也不会轻易变化。我们通过命令初始化板块数据,在 pythonbbs/comments.py 文件中添加以下代码。
def create_board():
board_names = ['Python语法', 'web开发', '数据分析', '测试开发', '运维开发']
for board_name in board_names:
board = BoardModel(name=board_name)
db.session.add(board)
db.session.commit()
click.echo("板块添加成功!")
下面再回到 app.py 文件中将 create_board 添加到命令中,代码如下。
...
app.cli.command("create-board")(commands.create_board)
打开 PyCharm 的 Terminal,执行 flask create-board 命令,即可完成板块数据的初始化。
渲染发布帖子模板
在渲染模板之前,先来看下发布帖子页面的效果,如图 9-29 所示。

用户需要输入的数据包括标题、板块、内容,我们先来完成模板的渲染。在 blueprints/post.py 文件中,添加 public_post 视图函数,并且需要在模板中显示板块数据,所以在渲染模板时要把所有板块数据传给模板,代码如下。
@bp.route("/post/public", methods=['GET', 'POST'])
def public_post():
if request.method == 'GET':
boards = BoardModel.query.all()
return render_template("front/public_post.html", boards=boards)
else:
pass
然后在 front/public_post.html 文件中,将板块数据循环渲染到板块的 select 标签下,代码如下。
...
<div class="form-group">
<label>板块</label>
<select name="board_id" class="form-control">
{% for board in boards %}
<option value="{{ board.id }}">{{ board.name }}</option>
{% endfor %}
</select>
</div>
...
在浏览器中访问 http://127.0.0.1:5000/post/public ,可以看到如图 9-30 所示的效果图。

现在模板虽然渲染了,但是还没有加入内容输入框,内容输入框需要加入富文本编辑器。下面讲解如何加入富文本编辑器。
使用wangEditor富文本编辑器
内容部分的输入框,应该具有类似 Word 软件的功能,如可以设置字体大小、颜色等,还可以插入图片,并且所见即所得,也就是编辑的时候是什么效果,以后在网页中展示的也是什么效果,这类编辑器叫作富文本编辑器。目前互联网上有许多免费开源的富文本编辑器,如百度官方出品的 UEditor、国外的 CKEditor,以及国内以王福朋为首的前端团队开发的 wangEditor。笔者尝试使用过多款富文本编辑器发现,wangEditor 简单易用、功能稳定,且有详细的中文文档,本书使用 wangEditor 作为富文本编辑器。
wangEditor 富文本编辑器的官方文档地址为 https://www.wangeditor.com/doc/ 。 |
使用 wangEditor 分为 4 步,第 1 步在模板中引入 wangEditor.js 文件,第 2 步添加生成编辑器的占位标签,第 3 步初始化编辑器,第 4 步设置图片上传 URL,下面分别来实现。
-
引入wangEditor. js文件
打开 templates/front/public_post.js 文件,然后实现 head 这个 block,并添加以下代码。
... {% block head %} <script src="https://cdn.jsdelivr.net/npm/wangeditor@latest/dist/wangEditor.min.js"></script> {% endblock %} ...
上述代码中,我们使用 jsdelivr 服务器提供的 cdn 服务加载 wangEditor.min.js 脚本,读者如果要通过自己的服务器加载,可以把 wangEditor.min.js 保存下来,存放到自己的服务器加载即可。
-
添加生成编辑器的占位标签
在需要生成 wangEditor 富文本编辑器的地方,添加一个占位标签,一般用 div 即可,为了后期方便寻找,在 div 标签上添加一个 id 属性,代码如下。
... <div class="form-group"> <label>内容</label> <div id="editor"></div> </div> ...
-
初始化编辑器
接下来将编辑器占位标签初始化成真正的编辑器,这里需要用到 JavaScript 代码,我们在 static/front/js 下创建一个 public_post.js 文件,然后添加以下代码。
$(function () { var editor = new window.wangEditor("#editor"); editor.create(); });
下面在 templates/front/public_post.html 的 head block 中加载此 js 文件,代码如下。
... {% block head %} <script src="https://cdn.jsdelivr.net/npm/wangeditor@latest/dist/wangEditor.min.js"></script> <script src="{{ url_for('static', filename='front/js/public_post.js') }}"></script> {% endblock %} ...
执行以上代码后,访问 http://127.0.0.1:5000/post/public ,就可以看到 wangEditor 编辑器成功被渲染,效果如图 9-29 所示。
-
设置图片上传URL
如果不想在 wangEditor 中上传图片,这一步可以忽略,也不影响使用 wangEditor。这里添加图片上传的功能。首先在 blueprints/front.py 文件中添加上传图片的视图函数,代码如下。

上述代码中,通过在视图函数 upload_image 的 request.files 中获取 image 参数来获取图片,这也意味着前端在上传图片时需要用 image 作为参数名。通过判断文件的后缀名来判断文件是否是图片,如果不是就返回 400 错误。这里没有用 restful 模块中的函数返回 JSON 数据,而是用 jsonify,这是因为 wangEditor 期望返回的结果为如下格式。
{
"errno": 0,
"data": [
{
"url": "url",
"alt": "",
"href": ""
}
]
}
返回结果中的参数说明如下。
-
errorno:为 0 代表正常,非 0 代表异常。
-
data:图片上传后的信息数组,图片信息包括 URL、提示信息 alt 以及跳转链接 href。
为了防止黑客利用图片文件名攻击服务器,使用 werkzeug.utils.secure_filename 产生一个安全的文件名来保存图片,并且把文件存储在配置项 UPLOAD_IMAGE_PATH 指定的路径下,这个配置项可以自行设置。
在图片保存完成后,再使用 url_for 对 media.media_file 进行反转,用来获取图片的 URL。media.media_file 是新增的用于返回上传的文件的蓝图,为了后期方便,将上传的文件切换到 Nginx 服务器上部署。下面创建一个 URL(以 /media 开头的蓝图),在开发阶段都会使用这个蓝图返回图片文件。首先在 blueprints 下创建 media.py 文件,然后输入以下代码。
from flask import Blueprint, current_app
import os
bp = Blueprint("media", __name__, url_prefix="/media")
@bp.get("/<path:filename>")
def media_file(filename):
return os.path.join(current_app.config.get("UPLOAD_IMAGE_PATH"), filename)
接下来在 app.py 文件中对 media 蓝图进行注册,代码如下。
from blueprints.media import bp as media_bp
...
app.register_blueprint(media_bp)
...
图片上传的视图函数写完后,再来到 static/front/js/public_post.js 脚本中,添加设置上传图片 URL 的代码,代码如下。
var editor = new window.wangEditor("#editor");
editor.config.uploadImgServer = "/upload/image";
editor.config.uploadFileName = "image";
editor.create();
在浏览器中重新加载发布帖子的页面,然后在 wangEditor 富文本编辑器中单击 “图片” 按钮,可以看到上传图片的功能已经添加,如图 9-31 所示。

但是现在还不能使用上传图片功能,因为 wangEditor 使用 POST 方法上传图片,而我们的项目对非 GET 请求做了 CSRF 防御,必须要提交 csrf_token,这样上传图片才能保证安全。对此可以对 upload_image 视图函数关闭 CSRF 防御,在 exts.py 文件中添加以下代码。
from flask_wtf import CSRFProtect
...
csrf = CSRFProtect()
下面再回到 app.py 文件中,将原来 CSRFProtect(app) 的代码修改为以下代码。
...
from exts import csrf
...
csrf.init_app(app)
这样做是因为在 blueprints/post.py 文件中也需要使用 csrf 对象,如此可避免循环导入。然后在 upload_image 函数定义中加上 @csrf.exempt 装饰器,代码如下。
@bp.post("/upload/image")
@csrf.exempt
def upload_image():
...
这样就针对 upload_image 视图函数关闭了 CSRF 防御,我们在 wangEditor 中重新上传图片,可以看到上传图片功能已经可以正常使用了。
未登录限制
现在发布帖子的页面在用户未登录的情况下可以直接访问,这样是不严谨的。如果用户没有登录发布帖子页面,应该重定向到登录页面。在用户访问网站后,为了方便获取当前用户的信息,需要添加 before_request 钩子函数。在 pythonbbs 项目的根路径下创建一个 hooks.py 文件,添加以下代码。
from flask import session, g
from models.user import UserModel
def bbs_before_request():
if "user_id" in session:
user_id = session.get("user_id")
try:
user = UserModel.query.get(user_id)
setattr(g, "user", user)
except Exception:
pass
在 pythonbbs/app.py 文件中,将 bbs_before_request 钩子函数添加进去,代码如下。
import hooks
...
# 添加钩子函数
app.before_request(hooks.bbs_before_request)
这样用户在登录的情况下访问本网站,会在全局对象 g 上面添加一个 user 属性,以后通过 g.user 即可获取到当前登录用户的信息,如果 g 没有 user 属性,说明此用户没有登录。
我们把未登录限制做成装饰器,添加到需要登录才能访问的视图函数中。在 pythonbbs 根目录下创建一个 decorators.py 文件,然后添加以下代码。
from functools import wraps
from flask import redirect, url_for, g
def login_required(func):
@wraps(func)
def inner(*args, **kwargs):
if not hasattr(g, "user"):
return redirect(url_for("user.login"))
else:
return func(*args, **kwargs)
return inner
上述代码中,添加了一个 login_required 装饰器函数,login_required 接收一个函数作为参数,通过全局对象 g 有无 user 属性,判断用户是否登录,若未登录就重定向到登录页面,否则就按照正常流程执行被装饰的函数。然后在 blueprints/post.py 文件中的 public_post 和 upload_image 上添加 login_required 装饰器,修改后代码如下。
from decorators import login_required
...
@bp.route("/post/public", methods=['GET', 'POST'])
@login_required
def public_post():
...
@bp.post("/upload/image")
@csrf.exempt
@login_required
def upload_image():
...
上述代码中,视图函数上的装饰器是有顺序的,在有多个装饰器的情况下,执行顺序是从里到外,开发者要充分考虑请求到达服务器后执行的过程,合理分配装饰器的位置。
此后用户在未登录的情况下重新访问发布帖子页面时,即会被重定向到登录页面了。
服务端实现发帖功能
在 blueprints/post.py 文件的 public_post 视图函数中,GET 请求是返回模板,POST 请求则是发布帖子。先添加发布帖子的表单,用来验证客户端上传的数据是否正确。在 pythonbbs/forms 下创建一个 post.py 文件,然后添加以下代码。
from .baseform import BaseForm
from wtforms import StringField, IntegerField
from wtforms.validators import InputRequired, Length
class PublicPostForm(BaseForm):
title = StringField(validators=[Length(min=2, max=100, message='请输入正确长度的标题!')])
content = StringField(validators=[Length(min=2, message="请输入正确长度的内容!")])
board_id = IntegerField(validators=[InputRequired(message='请输入板块 id!')])
上述代码中,添加了 PublicPostForm 类,在其中定义了 title、content、board_id 这 3 个字段。然后把 PublicPostForm 导入 blueprints/front.py 文件,并在 public_post 视图函数中添加如下代码。
@bp.route("/post/public", methods=['GET', 'POST'])
@login_required
def public_post():
if request.method == 'GET':
boards = BoardModel.query.all()
return render_template("front/public_post.html", boards=boards)
else:
form = PublicPostForm(request.form)
if form.validate():
title = form.title.data
content = form.content.data
board_id = form.board_id.data
post = PostModel(title=title, content=content, board_id=board_id, author=g.user)
db.session.add(post)
db.session.commit()
return restful.ok()
else:
message = form.messages[0]
return restful.params_error(message=message)
上述代码中,因为在 before_request 钩子函数中已经把 user 绑定到 g 对象上,所以可以直接通过 g.user 获取用户信息,并在初始化 PostModel 时赋值给 author 属性。
使用AJAX发布帖子
现在的帖子内容部分是通过 wangEditor 进行编辑的,只有通过 wangEditor 提供的 JavaScript 接口才能获取用户输入的内容,因此发布帖子的请求需要用 AJAX 来实现。wangEditor 是通过 editor.txt.html() 方法获取内容的,在 static/front/js/public_post.js 脚本中添加以下代码。
$(function () {
...
// 提交按钮单击事件
$("#submit-btn").click(function (event) {
event.preventDefault();
var title = $("input[name='title']").val();
var board_id = $("select[name='board_id']").val();
var content = editor.txt.html();
zlajax.post({
url: "/post/public",
data: { title, board_id, content }
}).done(function (data) {
setTimeout(function () {
window.location = "/";
}, 2000);
}).fail(function (error) {
alert(error.message);
});
});
});
上述代码中,我们绑定了提交按钮事件,然后分别从 HTML 标签中获取用户输入的 title、board_id 以及 content 数据,再用 zlajax.post 方法把数据发送到服务器上。在请求成功后跳转到首页,请求失败则弹出提示对话框。