Flask Vue.js全栈开发|第9章:用户评论
Synopsis: 用户在浏览你的博客文章后,可能会在底下评论,增加互动性。一篇文章允许有多条评论,一个用户可以在多篇文章下面发表评论,所以都是一对多关系。同时,评论支持多级回复,支持点赞功能
代码已上传到 https://github.com/wangy8961/flask-vuejs-madblog/tree/v0.9 ,欢迎star
1. 数据库
1.1 声明模型
Post 与 Comment 是一对多关系,同时 User 与 Comment 也是一对多关系。登录的用户除了可以在文章下面发表评论以外,还可以 回复
别人的评论,所以评论模型是 自引用的一对多
关系,同时支持 级联删除
修改 back-end/app/models.py
,增加 Comment 数据模型:
class User(PaginatedAPIMixin, db.Model): ... comments = db.relationship('Comment', backref='author', lazy='dynamic', cascade='all, delete-orphan') class Post(PaginatedAPIMixin, db.Model): ... comments = db.relationship('Comment', backref='post', lazy='dynamic', cascade='all, delete-orphan') class Comment(PaginatedAPIMixin, db.Model): __tablename__ = 'comments' id = db.Column(db.Integer, primary_key=True) body = db.Column(db.Text) timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) mark_read = db.Column(db.Boolean, default=False) # 文章作者会收到评论提醒,可以标为已读 disabled = db.Column(db.Boolean, default=False) # 屏蔽显示 # 外键,评论作者的 id author_id = db.Column(db.Integer, db.ForeignKey('users.id')) # 外键,评论所属文章的 id post_id = db.Column(db.Integer, db.ForeignKey('posts.id')) # 自引用的多级评论实现 parent_id = db.Column(db.Integer, db.ForeignKey('comments.id', ondelete='CASCADE')) # 级联删除的 cascade 必须定义在 "多" 的那一侧,所以不能这样定义: parent = db.relationship('Comment', backref='children', remote_side=[id], cascade='all, delete-orphan') parent = db.relationship('Comment', backref=db.backref('children', cascade='all, delete-orphan'), remote_side=[id]) def __repr__(self): return '<Comment {}>'.format(self.id) def get_descendants(self): '''获取一级评论的所有子孙''' data = set() def descendants(comment): if comment.children: data.update(comment.children) for child in comment.children: descendants(child) descendants(self) return data
1.2 迁移脚本
(venv) D:\python-code\flask-vuejs-madblog\back-end>flask db migrate -m "add comments table" (venv) D:\python-code\flask-vuejs-madblog\back-end>flask db upgrade
2. RESTful API设计
没什么好说的,跟 Post 非常类似,创建 app/api/comments.py
:
from flask import request, jsonify, url_for, g, current_app from app.api import bp from app.api.auth import token_auth from app.api.errors import error_response, bad_request from app.extensions import db from app.models import Post, Comment @bp.route('/comments/', methods=['POST']) @token_auth.login_required def create_comment(): '''在某篇博客文章下面发表新评论''' data = request.get_json() if not data: return bad_request('You must post JSON data.') if 'body' not in data or not data.get('body').strip(): return bad_request('Body is required.') if 'post_id' not in data or not data.get('post_id'): return bad_request('Post id is required.') post = Post.query.get_or_404(int(data.get('post_id'))) comment = Comment() comment.from_dict(data) comment.author = g.current_user comment.post = post db.session.add(comment) db.session.commit() response = jsonify(comment.to_dict()) response.status_code = 201 # HTTP协议要求201响应包含一个值为新资源URL的Location头部 response.headers['Location'] = url_for('api.get_comment', id=comment.id) return response @bp.route('/comments/', methods=['GET']) @token_auth.login_required def get_comments(): '''返回评论集合,分页''' page = request.args.get('page', 1, type=int) per_page = min( request.args.get( 'per_page', current_app.config['COMMENTS_PER_PAGE'], type=int), 100) data = Comment.to_collection_dict( Comment.query.order_by(Comment.timestamp.desc()), page, per_page, 'api.get_comments') return jsonify(data) @bp.route('/comments/<int:id>', methods=['GET']) @token_auth.login_required def get_comment(id): '''返回单个评论''' comment = Comment.query.get_or_404(id) return jsonify(comment.to_dict()) @bp.route('/comments/<int:id>', methods=['PUT']) @token_auth.login_required def update_comment(id): '''修改单个评论''' comment = Comment.query.get_or_404(id) if g.current_user != comment.author and g.current_user != comment.post.author: return error_response(403) data = request.get_json() if not data: return bad_request('You must post JSON data.') # if 'body' not in data or not data.get('body'): # return bad_request('Body is required.') comment.from_dict(data) db.session.commit() return jsonify(comment.to_dict()) @bp.route('/comments/<int:id>', methods=['DELETE']) @token_auth.login_required def delete_comment(id): '''删除单个评论''' comment = Comment.query.get_or_404(id) if g.current_user != comment.author and g.current_user != comment.post.author: return error_response(403) db.session.delete(comment) db.session.commit() return '', 204
然后,修改 app/api/__init__.py
导入 comments.py
:
from flask import Blueprint bp = Blueprint('api', __name__) # 写在最后是为了防止循环导入,ping.py文件也会导入 bp from app.api import ping, tokens, errors, users, posts, comments
3. 用户评论 CURD
3.1 多级评论展示
后端需要增加一个 API,只返回该文章下面的 顶层评论
,每个 顶层评论
下面如果有子孙评论,则附加到 descendants
属性上,这是为了前端展示多级评论时,只显示两层,免得缩进太多就不好看了
修改back-end/api/posts.py
:
@bp.route('/posts/<int:id>/comments/', methods=['GET']) def get_post_comments(id): '''返回当前文章下面的一级评论''' post = Post.query.get_or_404(id) page = request.args.get('page', 1, type=int) per_page = min( request.args.get( 'per_page', current_app.config['COMMENTS_PER_PAGE'], type=
8 条评论
评论者的用户名
评论时间杨凯
2019-07-07T13:30:51Z试一试
shishijia 杨凯
2020-04-11T08:29:01Z试一试
杨凯 杨凯
2019-07-07T13:31:15Z二层楼
杨凯 杨凯
2019-07-07T13:31:55Z自己跟自己回复
杨凯 杨凯
2019-07-07T13:32:27Z继续回复
LongchuanYu 杨凯
2020-01-06T12:45:38Z杨凯
2019-07-07T13:30:52Z试一试
LLLeo Li
2022-05-16T15:11:22Z我们是
ASOUL!