本文分前后两个部分,前半部分是跑通该项目的过程,后半部分是提炼其中的评论功能到其他项目的过程
带评论模板的项目:
博客https://github.com/goalong/flask-demo
https://github.com/13697165025/FlaskMyBlog
*新闻https://github.com/blue-harddisk/flask
一个新闻web,具体功能:登录注册,首页有分类,新闻详情页面有点赞,评论,回复评论功能,个人中心,后台管理
新建news_website的py3.7虚拟环境
activate
使用requirements安装:
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple |
pip install -r requirements.txt -i http://pypi.douban.com/simple |
pip install -r requirements.txt -i http://pypi.hustunique.com/simple |
pip install -r requirements.txt -i http://pypi.sdutlinux.org/simple |
pip install -r requirements.txt -i http://pypi.mirrors.ustc.edu.cn/simple |
MarkupSafe无法正常安装
报错:
Command errored out with exit status 1: python setup.py egg_info Check the logs for full command output. |
cannot import name 'Feature' from 'setuptools' |
setuptool版本的问题,python3源中的setuptools已经升级到46以上。所以导致pip安装失败.参考:https://blog.csdn.net/pengshengege/article/details/105113561
pip install --upgrade pip setuptools==45.2.0 |
报错:
Failed to build mysqlclient 无法打开包括文件: “mysql.h”: No such file or directory |
参考:
https://segmentfault.com/a/1190000016563585
https://blog.csdn.net/cn_1937/article/details/81533544?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
感觉都很麻烦,最后把它从requirement中删除了,运行时自动装上了最新版mysqlclient
使用migrations迁移数据库
https://www.cnblogs.com/senlinyang/p/8387007.html
为了导出数据库迁移命令,Flask-Migrate 提供了一个MigrateCommand 类,可附加到Flask-Script 的manager 对象上。在这个例子中,MigrateCommand 类使用db 命令附加。
manager.add_command('db', MigrateCommand) |
在维护数据库迁移之前,要使用init 子命令创建迁移仓库:
python hello.py db init |
python manage.py mysql init |
报错:Directory migrations already exists
删除文件后重新运行
migrate 子命令用来自动创建迁移脚本:
python hello.py db migrate -m "initial migration" |
python manage.py mysql migrate -m "initial migration" |
报错:
File "D:\ProgramData\Anaconda3\envs\news_website\lib\site-packages\sqlalchemy\dialects\mysql\mysqldb.py", line 102, in dbapi return __import__('MySQLdb') ModuleNotFoundError: No module named 'MySQLdb' |
尝试安装MySQLdb
pip install mysql-python -i https://pypi.tuna.tsinghua.edu.cn/simple |
报错:
_mysql.c(42): fatal error C1083: 无法打开包括文件: “config-win.h”: No such file or directory error: command 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\BuildTools\\VC\\Tools\\MSVC\\14.16.27023\\bin\\HostX86\\x64\\cl.exe' failed with exit status 2 |
参考:https://blog.csdn.net/weixin_42840933/article/details/85274313
https://www.cnblogs.com/SH170706/p/10082987.html
下载包,从dos命令行进入下载后的文件夹,执行下面命令:
pip install mysqlclient-1.3.14-cp37-cp37m-win_amd64.whl |
这个版本是对的,可以用MySQLdb模块
spec.loader.exec_module(module) File "<frozen importlib._bootstrap_external>", line 728, in exec_module File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed File "migrations\env.py", line 87, in <module> run_migrations_online() File "migrations\env.py", line 72, in run_migrations_online connection = engine.connect() |
Traceback (most recent call last): File "manage.py", line 30, in <module> manager.run() |
配置:
SQLALCHEMY_DATABASE_URI
mysql://username:password@hostname/database |
匹配数据库名与配置database名,再次运行,成功
使用db upgrade 命令把迁移应用到数据库中:
python hello.py db upgrade |
python manage.py mysql upgrade |
尝试启动项目报错:
redis.exceptions.ConnectionError: Error 10061 connecting to 127.0.0.1:6379. 由于目标计算机积极拒绝,无法连接。. |
参考:https://blog.csdn.net/qq_41192383/article/details/86559296
下载并安装Redis-x64-3.0.503.msi,地址:
https://github.com/MicrosoftArchive/redis/releases
安装完成后,启动服务(找到安装路径,双击redis-cli.exe文件即可)
requirement装的redis==2.10.6为python提供的模块redis-py
启动项目:
python manage.py runserver |
http://localhost:5000/ |
点击新闻列表时报错:
File "D:\anacondaProject\flask\info\user\views.py", line 138, in news_release category_list.pop(0) IndexError: pop from empty list |
判断是否为空:
users = session.query(user).filter(user.id == '234).all()#users为object列表 if(len(user) == 0): print "数据库中没有id为234的用户‘’ else: print "数据库中包含id为234的用户“ |
if(len(categorys)!=0): for category in categorys: category_list.append(category.to_dict()) # 删除第0个元素 category_list.pop(0) |
发布报错:
File "D:\anacondaProject\flask\info\user\views.py", line 151, in news_release index_image = request.files.get("index_image").read() AttributeError: 'NoneType' object has no attribute 'read' |
判断是否为空
成功发布新闻与评论:
其中评论部分的数据库实现:
Comment表:
Comment-like表:
Sql转储并导入
Info/models中News类有有关评论的数据库查询方法
class News(BaseModel, db.Model): """新闻""" __tablename__ = "info_news"
id = db.Column(db.Integer, primary_key=True) # 新闻编号 comments = db.relationship("Comment", lazy="dynamic") # dynamic: 不加载记录,但提供加载记录的查询,也就是生成query对象,只可以用在一对多和多对多关系中,不可以用在一对一和多对一中 即 lazy屬性的對向關系模型只能是多的一側 relationship的第一个参数表示这个关系的另一个数据库模型是哪个,这里是User,第二个参数backref表示给关联的数据库模型添加一个属性,这里是role。 也就是说,你可以通过User模型的role这个属性去访问Role模型,比如你在views.py中的查询结果,你可以通过user.role.name得到roles表中对应记录的name,user.role.id则得到roles表中对应记录的id |
class Comment(BaseModel, db.Model): """评论""" __tablename__ = "info_comment"
id = db.Column(db.Integer, primary_key=True) # 评论编号 user_id = db.Column(db.Integer, db.ForeignKey("info_user.id"), nullable=False) # 用户id news_id = db.Column(db.Integer, db.ForeignKey("info_news.id"), nullable=False) # 新闻id content = db.Column(db.Text, nullable=False) # 评论内容 parent_id = db.Column(db.Integer, db.ForeignKey("info_comment.id")) # 父评论id parent = db.relationship("Comment", remote_side=[id]) # 自关联 like_count = db.Column(db.Integer, default=0) # 点赞条数
def to_dict(self): resp_dict = { "id": self.id, "create_time": self.create_time.strftime("%Y-%m-%d %H:%M:%S"), "content": self.content, "parent": self.parent.to_dict() if self.parent else None, "user": User.query.get(self.user_id).to_dict(), "news_id": self.news_id, "like_count": self.like_count } return resp_dict |
class CommentLike(BaseModel, db.Model): """评论点赞""" __tablename__ = "info_comment_like" comment_id = db.Column("comment_id", db.Integer, db.ForeignKey("info_comment.id"), primary_key=True) # 评论编号 user_id = db.Column("user_id", db.Integer, db.ForeignKey("info_user.id"), primary_key=True) # 用户编号 |
添加:
注意user_id news_id等原表不统一的字段处理--user_id只去除外键,news_id去除外键并改名music_id,同时在数据库中修改字段名,删除两表外键
在基类时间报错:
module 'datetime' has no attribute 'now' |
我引入的是datetime,而非第二层,参照用户处使用datetime.datetime.now()
修改comment中todict方法下用户相关:
"user_id": self.user_id, "user": User.query.get(self.user_id).name, |
Info/news/views中有新闻详情展示所有评论,添加评论,与赞的方法
# 详情页 @news_blue.route("/<int:news_id>") @user_login_data #使用了装饰器,增加@functools.wraps(f), 可以保持当前装饰器去装饰的函数的 __name__ 的值不变 def news_detail(news_id): user = g.user #。。。省略部分代码 """ 获取到新闻评论列表 1 :我们需要查询新闻评论表,在查询的时候,直接通过新闻id就可以查询,因为所有的评论都是针对新闻产生的 """ comments = Comment.query.filter(Comment.news_id == news_id).order_by(Comment.create_time.desc()).all() comments_list = [] # 获取到新闻详情页面评论点赞的数据 # TODO commentLike_list = [] comment_like_ids = []
if user: # 根据user.id,查出用户点赞过的CommentLike对象 commentLike_list = CommentLike.query.filter(CommentLike.user_id == user.id).all() # 遍历CommentLike所有对象,查出点赞评论的id comment_like_ids = [comment_like.comment_id for comment_like in commentLike_list]
for comment in comments:
comment_dict = comment.to_dict() comment_dict["is_like"] = False # comment_like_ids:所有的评论点赞id if comment.id in comment_like_ids: comment_dict["is_like"] = True comments_list.append(comment_dict)
data = { "user_info": user.to_dict() if user else None, "click_news_list": news_list, "news": news.to_dict(), "is_collected": is_collected, "comments": comments_list, "is_followed": is_followed }
return render_template("news/detail.html", data=data) |
先再play中加,成功后移植到detail路由
原先的user一直使用session保存,但没有为他申明变量,多次调用有点繁琐:
如果没有设置session时,获取到的session就是None
尝试获取后再判user是否为None
判空:
`if x is not None`是最好的写法,清晰,不会出现错误 |
删除了data中对user\news的储存,因为该用法把user\news当作带to_dict方法的对象了,
"click_news_list": news_list, "is_followed": is_followed "is_collected": is_collected, |
也因为不需要删除了
click_news_list
登陆后报错:
name 'Comment' is not defined |
因为没有导入到view
# 点赞 @news_blue.route("/comment_like", methods=["POST"]) @user_login_data def comment_like(): user = g.user if not user: return jsonify(errno=RET.SESSIONERR, errmsg="请登陆")
comment_id = request.json.get("comment_id") news_id = request.json.get("news_id") # 判断当前用户的动作,到底是想点赞,还是想取消点赞 action = request.json.get("action")
comment = Comment.query.get(comment_id)
if action == "add": # 用户想点赞 comment_like = CommentLike.query.filter(CommentLike.comment_id == comment_id, CommentLike.user_id == user.id).first() # 查询出来之后,需要判断当前这条评论用户是否已经点赞,如果查询出来为空,说明之前没有点赞,那么就可以点赞 if not comment_like: comment_like = CommentLike() comment_like.comment_id = comment_id comment_like.user_id = user.id db.session.add(comment_like) # 因为点赞了,所以需要把当前的评论进行加1 comment.like_count += 1
else: # 取消点赞的动作 comment_like = CommentLike.query.filter(CommentLike.comment_id == comment_id, CommentLike.user_id == user.id).first() if comment_like: db.session.delete(comment_like) comment.like_count -= 1
db.session.commit() return jsonify(errno=RET.OK, errmsg="点赞成功")
# 评论 @news_blue.route("/news_comment", methods=["POST"]) @user_login_data def news_comment(): user = g.user if not user: return jsonify(errno=RET.SESSIONERR, errmsg="请登陆") news_id = request.json.get("news_id") # 评论的内容 comment_str = request.json.get("comment") # 评论的父id parent_id = request.json.get("parent_id") """ 用户评论: 用户如果在登录的情况下,可以进行评论,未登录,点击评论弹出登录框 用户可以直接评论当前新闻,也可以回复别人发的评论 1:用户必须先登陆才能进行评论,如果不登陆,直接返回 2:如果需要评论,那么就需要知道当前评论的是哪条新闻,如果想知道是哪条新闻,那么就可以通过news_id 查询出来新闻 3:如果评论成功之后,那么我们需要把用户的评论信息存储到数据库,为了方便下次用户在进来的时候可以看到评论 """ news = News.query.get(news_id) comment = Comment() comment.user_id = user.id comment.news_id = news.id comment.content = comment_str # 不是所有的新闻都有评论 if parent_id: comment.parent_id = parent_id
db.session.add(comment) db.session.commit() return jsonify(errno=RET.OK, errmsg="评论成功", data=comment.to_dict()) |
前端何处news_id传到后端request.json.get("news_id"),确定是Json
和表单中其他内容接收方式相同,可能是submit时js传过来的
去除news = News.query.get(news_id)查数据库部分
新增评论路由时报错:
@news_blue.route("/news_comment", methods=["POST"]) NameError: name 'news_blue' is not defined |
记得将路由名改为该蓝图名
报错:
line 521, in news_comment comment.user_id = user.id AttributeError: 'str' object has no attribute 'id' |
改为session取出的user_id
报错:
sqlalchemy.exc.OperationalError: (_mysql_exceptions.OperationalError) (1048, "Column 'music_id' cannot be null") [SQL: 'INSERT INTO info_comment (create_time, update_time, user_id, music_id, content, parent_id, like_count) VALUES (%s, %s, %s, %s, %s, %s, %s)'] [parameters: (datetime.datetime(2020, 4, 7, 16, 21, 17, 132015), datetime.datetime(2020, 4, 7, 16, 21, 17, 132015), 48, None, '1', None, 0)] (Background on this error at: http://sqlalche.me/e/e3q8) line 530, in news_comment db.session.commit() |
提交前为comment.music_id赋值
报错:
return jsonify(errno=RET.OK, errmsg="评论成功", data=comment.to_dict()) NameError: name 'jsonify' is not defined |
from flask import jsonify
报错:
File "D:\anacondaProject\music-website-flask\app\home\views.py", line 433, in play comment_dict = comment.to_dict() File "D:\anacondaProject\music-website-flask\app\models.py", line 153, in to_dict "user": User.query.get(self.user_id).to_dict(), AttributeError: 'User' object has no attribute 'to_dict' |
在comment model中to_dic方法调用了
"user": User.query.get(self.user_id).to_dict(), |
因此修改为:
"user": self.user_id, "news_id": self.music_id, |
报错:
return jsonify(errno=RET.SESSIONERR, errmsg="请登陆") return jsonify(errno=RET.OK, errmsg="评论成功", data=comment.to_dict()) NameError: name 'RET' is not defined |
原导入方法:
from info.utils.response_code import RET |
是一堆常量,OK为0,SESSIONERR为4101,修改:
return jsonify(errno="4101", errmsg="请登陆") return jsonify(errno="0", errmsg="评论成功", data=comment.to_dict()) |
点赞路由同上一样改
Html
{% if data.user_info %} <form action="" class="comment_form" data-newsid="{{ data.news.id }}"> <div class=" person_pic"> <img src="{% if data.user_info.avatar_url %} {{ data.user_info.avatar_url }} {% else %} ../../static/news/images/person01.png {% endif %}" alt="用户图标"> </div> <textarea placeholder="请发表您的评论" class="comment_input"></textarea> <input type="submit" name="" value="评 论" class="comment_sub"> </form> {% else %} <div class="comment_form_logout"> 登录发表你的评论 </div> {% endif %}
<div class="comment_count"> {{ data.news.comments_count }}条评论 </div>
<div class="comment_list_con"> {% for comment in data.comments %} <div class="comment_list"> <div class="person_pic fl"> <img src="{% if comment.user.avatar_url %} {{ comment.user.avatar_url }} {% else %} ../../static/news/images/person01.png {% endif %}" alt="用户图标"> </div> <div class="user_name fl">{{ comment.user.nick_name }}</div> <div class="comment_text fl">{{ comment.content }}</div> {% if comment.parent %} <div class="reply_text_con fl"> <div class="user_name2">{{ comment.parent.user.nick_name }}</div> <div class="reply_text"> {{ comment.parent.content }} </div> </div> {% endif %} <div class="comment_time fl">{{ comment.create_time }}</div>
<a href="javascript:;" class="comment_up {% if comment.is_like %} has_comment_up {% endif %} fr" data-commentid="{{ comment.id }}" data-likecount="{{ comment.like_count }}" data-newsid="{{ data.news.id }}"> {% if comment.like_count > 0 %} {{ comment.like_count }} {% else %} 赞 {% endif %}</a>
<a href="javascript:;" class="comment_reply fr">回复</a> <form class="reply_form fl" data-commentid="{{ comment.id }}" data-newsid="{{ data.news.id }}"> <textarea class="reply_input"></textarea> <input type="button" value="回复" class="reply_sub fr"> <input type="reset" name="" value="取消" class="reply_cancel fr"> </form> </div> {% endfor %} </div> |
新增form中:
data-newsid="{{ data.news.id }}" |
改为musicid-仿照收藏逻辑
加入评论列表后报错:
{{ data.news.comments_count }}条评论 {% for comment in data.comments %} jinja2.exceptions.UndefinedError: 'data' is undefined |
原view的news = News.query.get(news_id)取出后
由于在model news中有
comments = db.relationship("Comment", lazy="dynamic") data:{ "comments_count": self.comments.count(), |
所以可以取出
此处加comments_count性价比不高,删除
但data也无法识别,是由于渲染模板的时候没传过去
报错:
jinja2.exceptions.UndefinedError: 'dict object' has no attribute 'news' |
修改<a href="javascript:;" class="comment_up处的
data-newsid="{{ data.news.id }} |
为mus.music_id
CSS
comment_form等class
将news文件夹放到static下,在网页中引入对应文件
比照不同之处,逻辑确实未登录
其中main.css中部分class如form-group的样式可能影响原有样式
导致时间滑到上一行的是原布局中的
box-sizing: border-box |
解决参考:https://blog.csdn.net/qq_33644670/article/details/95071581
给不需要box-sizing:border-box的元素上加上 box-sizing:content-box;
限制浮动元素在div内,参考:
https://www.cnblogs.com/heqichang/archive/2011/08/01/2123887.html
我使用的是父div加fl
且Div宽度占满父div,参考:
https://segmentfault.com/q/1010000017319613?utm_source=tag-newest
style="width:100%" |
评论按钮错位:
评论输入框样式:
.comment_form .comment_input { float: left; width: 690px; height: 60px; margin-left: 20px; border-radius: 4px; padding: 10px; outline: none; border: 1px solid #2185ed; } |
在需要换行的浮动元素外包裹:
<div class="fl" style="width:750px"> |
JS
Detail.js使用jQuery,
添加评论后前端点击评论时url为:
http://localhost:5000/news/news_comment |
修改:
$.ajax({ url: "/news/news_comment", |
为
url: "/home/news_comment", |
http://localhost:5000/home/news_comment
依然404,参考收藏、链接等
收藏为:
{{ url_for('home.like',id=mus.music_id) }} |
相似链接:
<a href="http://localhost:5000/detail?key=SHANGHAI"> |
评论成功,但显示用户名不对,原列表中的用户名也要加上
评论提交的Ajax中
if (comment.user.avatar_url) { comment_html += '<img src="' + comment.user.avatar_url + '" alt="用户图标">' } else { comment_html += '<img src="../../static/news/images/person01.png" alt="用户图标">' } comment_html += '</div>' comment_html += '<div class="user_name fl">' + comment.user.nick_name + '</div>' |
改为:
comment_html += '<div class="user_name fl">' + comment.user + '</div>' |
回复评论报错:
"POST /news/news_comment HTTP/1.1" 404 |
在class="comment_reply"调用js,其ajax请求
$.ajax({ url: "/news/comment_like", |
修改为:
url: "/comment_like", |
详情页面无法搜索:
原本Home最下方处js
<script> $(document).ready(function () { $("img.lazy").lazyload({ effect: "fadeIn" }); $("#do_search").click(function () { var key = $("#key_music").val(); location.href = "{{ url_for('home.search')}}?key=" + key }); $("#do_search").keydown(function () { var key = $("#key_music").val(); location.href = "{{ url_for('home.search')}}?key=" + key }); });
</script> |
通过注释文件引入,可以定位到导致问题产生的文件是news/js/jquery-1.12.4.min.js
注释之后完全没有影响所需几个功能,所以就不用了