整体设计逻辑
1. 评论的构成要件根评论:对文章的评论子评论: 对评论的评论区别:是否有父评论2. 评论的功能设计1. 构建评论样式2. 提交根评论3. 显示根评论---render显示---Ajax显示4. 提交子评论5. 显示子评论---render显示---Ajax显示6. 评论树的显示楼层结构 s树形结构 一目了然
现在版本CNBLOG评论区样式分析
样式展示

样式分析
1. 信息分类1. 动态展示的内容作者头像,关注数,粉丝数;点赞,反对按钮;提交日期 作者 阅读量和评论量;上一篇文章,快捷跳转;2. 静态展示的内容发表评论,点赞或者点踩,评论框;2. 权限区分1. 作者可编辑,点击转入文章后台页面;收藏和举报为登录用户功能,普通用户不予展示;2. 刷新评论和页面属于局部刷新,返回顶部也属于页面跳转,原地操作;3. 订阅评论和加关注属于钩子,用于建立数据表关系3. 技术探寻1. 如何嵌入markdown编辑器?2.
第一个基础功能—评论区样式构建
实现逻辑
注意点:1. 不要将用户名写死,username放在div盒子的value中,以模板标签的方式2. CSS样式引入图片仍然不能解耦,需在H5同文件中实现;3. 如何清除浮动效果? 用bs中的clearfix,将其设置在dive中;
实现效果

后端代码
<div class="comments"><p>发表评论</p><p>昵称:<input type="text" id="xyz" class="author" disabled="disabled" size="50"value="{{ user.username }}"></p><p>评论内容</p><textarea name="" id="" cols="45" rows="8"></textarea><button class="btn btn-default comment_btn"></button></div># CSS样式input.author {background-image: url('{% static 'font/icon_form.gif' %}');background-repeat: no-repeat;border: 1px solid #cccccc;padding: 4px 4px 4px 30px;width: 300px;font-size: 13px;background-position: 3px -3px;}
第二个功能—-为评论功能绑定Ajax事件
处理逻辑
1. 功能设计原理与点赞类似,首先为评论按钮绑定一个Ajax事件,行为触发之后将数据交予view视图处理;2. Ajax的数据结构首先需要绑定标签,然后分别指定URL,POST方式,以及data,最后控制台打印数据;3. 步骤先绑定基本的Ajax事件;然后创建URL以及处理视图;4. 注意事项1. Ajax中是否传递csrfmiddlewaretoken会影响客户端与服务器的连接5. 评论数据的构成要件1. article ID 必传,用于定位;content必须;parent comment用于定位;2. user即当前登录对象; create_time即为入库时间;
测试ajax基本功能

基础代码
# 处理逻辑代码# 用于提交评论path('comment/', views.comment, name='comment'),def comment(request):print(request.POST)return HttpResponse("comment")# 事件绑定代码$(".comment_btn").click(function (){$.ajax({url:"/blog/comment/", {# 指定URL #}type:"post", {# 请求方式 #}data:{"csrfmiddlewaretoken": $("[name='csrfmiddlewaretoken']").val(), {# 帮助进行安全性校验 #}},success:function (data){ {# 回调函数,成功处理请求之后 #}console.log(data)}})})
第三个功能—-数据传输
业务逻辑设计
1. 数据要件1. article ID 必传,用于定位;content必须;parent comment用于定位;2. user即当前登录对象; create_time即为入库时间;2. 所作修改1. uer.id无法从request中取得,使用模板标签从数据库抽取;2. URL转到子app下;3. 传输逻辑1. Ajax绑定特定字段,其中评论内容从标签中抽取;2. 包括CSRF_TOKEN在内的,模板标签抽取,标签抽取三类数据,通过post传递给view视图处理;3. view视图分别接收数据和使用ORM存储数据;
关键代码
def comment(request):print(request.POST)article_id = request.POST.get("article_id")pid = request.POST.get("pid")content = request.POST.get("content")user_id = request.POST.get("user_id")comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content, parent_comment_id=pid)return HttpResponse("comment")
$(".comment_btn").click(function (){var pid = ""var content = $("#comment_content").val();$.ajax({url:"/blog/comment/", {# 指定URL #}type:"post", {# 请求方式 #}data:{"csrfmiddlewaretoken": $("[name='csrfmiddlewaretoken']").val(), {# 帮助进行安全性校验 #}"article_id":"{{ article_obj.pk }}","content": content, {# 该数据从H5标签中取得 #}"user_id": "{{ user.nid }}","pid":pid, {# 默认为空,根评论本身就是父评论 #}},success:function (data){ {# 回调函数,成功处理请求之后 #}console.log(data){# 清空评论框内容 #}$("#comment_content").val("");}})})
效果


第三个功能—通过render显示评论
业务逻辑
1. 使用bootstrap搭建基本样式;2. 使用a标签嵌套,预留跳转接口,用于回复和访问评论人的个人资料;3. 从负责渲染页面的视图函数中,通过ORM查询数据库数据,从而实现H5页面的数据展示;4. 设置中修改 USE_TZ 的值为 False,使用当地时间;
bootstrap模板代码
<ul class="list-group"><li class="list-group-item">Cras justo odio</li><li class="list-group-item">Dapibus ac facilisis in</li><li class="list-group-item">Morbi leo risus</li><li class="list-group-item">Porta ac consectetur ac</li><li class="list-group-item">Vestibulum at eros</li></ul>
关键代码 [view & H5]
def article_detail(request, username, article_id):user = models.UserInfo.objects.filter(username=str(username)).first()blog = user.blogarticle_obj = models.Article.objects.filter(pk=article_id).first()comment_list = models.Comment.objects.filter(article_id=article_id)return render(request, "blog/article_detail.html",locals())
<p>评论列表</p><ul class="list-group comment_list">{% for comment in comment_list %}<li class="list-group-item"><div><a href=""># {{ forloop.counter }}</a> <span> {{ comment.create_time|date:"Y-m-d H:i" }}</span> <a><span>{{ comment.user.username }}</span></a> <a href="" class="pull-right">回复</a> </div><div class="comment-con"><p>{{ comment.content }}</p></div></li>{% endfor %}</ul>
.comment-con{margin-top: 10px;text-align: center;}
最终效果

第三个功能—-Ajax局部刷新,实时展现评论提交行为
整体设计
1. opportunity当提交完成评论之后,view视图向前端返回数据,此时应该返回提交的评论(相当于局部实时刷新)在Ajax的回调函数中设置插入评论的功能[分为数据抽取和插入标签数据];2. information for display1. 评论入库时间;评论人即登录者;评论内容;3. How to get data?1. 评论行为触发Ajax事件,view处理完数据,返回数据时取得数据;4. the way to get data?1. 在view中设置一个变量,类型为字典;将创建事件,用户,内容回传至Ajax的回调函数,由回调函数中的插入功能,一次性将三个数据导入进去;2. 特别注意,create_time存储时必须序列化,因为它是一个对象而非数据;
关键代码
views.py[9-16]
def comment(request):print(request.POST)article_id = request.POST.get("article_id")pid = request.POST.get("pid")content = request.POST.get("content")user_id = request.POST.get("user_id")comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content, parent_comment_id=pid)response = {}response["create_time"] = comment_obj.create_time.strftime("%Y-%m-%d %X")response["username"] = comment_obj.user.usernameresponse["content"] = contentreturn JsonResponse(response)
Ajax事件绑定
success: function (data) { {# 回调函数,成功处理请求之后 #}console.log(data);var create_time = data.create_time;var username = data.username;var content = data.content;var s = `<li class= "list-group-item" >< div ><span> ${create_time}</span> <a href=""><span>${username}</span></a> </div><div class="comment-con"><p>${content}</p></div></li>`;$("ul.comment_list").append(s);
最终效果

第四个功能—回复按钮事件
整体功能设计
1. 用于点击回复则跳转至评论区,重定位;2. 重定位的时候,自动填充每一个父评论的用户,这里首先需要定位每一个父评论,通过为回复标签建立唯一username,进行唯一定位,尔后填充数据;3. 自动填充数据后面加一个换行符用于光标自动换行;
给每层评论的回复下面增加一个用户名—用来子评论添加时定位父评论

点击后触发文本填充

关键代码
<a class="pull-right reply_btn" username="{{ comment.user.username }}">回复</a> $(".reply_btn").click(function (){$('#comment_content').focus();var val = "@" +$(this).attr("username")+"\n";$('#comment_content').val(val);})
第五个功能—提交子评论
业务逻辑
1. 调试前提1. 切换用户,便于实现提交子评论;2. 未完善的点1. 当前业务逻辑下,提交子评论会作为根评论被存储到数据库,出现的原因是PID默认为空,也就是所有的评论均为根评论;其次提交子评论时会将@username一起提交到数据库,因此需要对其进行拆分;提交成功后,除了删除评论内容,还需要重置PID;3. 处理逻辑1. 判断PID为空的条件? 如果点击回复,PID应该等于同层级评论的用户ID,如果未点击回复直接提交,则PID认为空,作为父评论进行提交;2. 提交成功才清空内容和重置PID;3. 将PID作为全局变量,提交之前就设置全局默认的PID;该逻辑绑定的是提交按钮;
默认状态下PID为空

为标签增加comment.pk属性便于给PID赋值

comment.pk的前端预期效果

绑定评论功能时对提交评论的标签所含的PID重新赋值

PID实时提交效果

需要清理的数据[@developer]

对content进行切片

实现效果

提交完成之后清空评论框和重置pid

提交后自动删除评论内容以及重置PID

第六个功能—显示父评论和子评论
整体功能设计
1. 预期实现的效果要求Ajax提交评论,显示局部刷新时就应该显示子评论和父评论;2. 实现逻辑1. 通过模板字符串设定条件,当评论的父评论ID存在时,才能继续显示一下子评论的对象[用户名名和评论内容];2. 父评论插入在评论内容样式之前,回复标签之后[展示页面];
关键代码
{% if comment.parent_comment_id %}<div class="pid_info well"><p>{{ comment.parent_comment.user.username }}:{{ comment.parent_comment.content }}</p></div>

最终实现效果

第七个功能—评论树显示层级关系
业务逻辑
1. 为什么使用树形结构?1. 层级分明,逻辑清晰;还有其他应用场景[权限:递归];2. 应用逻辑1. 新建一个区域,使用Ajax绑定事件,传递文章ID,view视图拿到文章ID,返回主键,内容,父评论ID;注意序列化时变量的数据类型;2. 该功能绑定的标签是 ```评论树```;获取到数据,也就是view视图传递过来的数据时,首先处理根评论;将数据填充在 ```comment_tree```标签中;3. 其他理解1. Ajax事件中指定的URL,仅仅是用于获取数据,充当中介[intermediary]的作用;
源码解析[19-20]
class JsonResponse(HttpResponse):"""An HTTP response class that consumes data to be serialized to JSON.:param data: Data to be dumped into json. By default only ``dict`` objectsare allowed to be passed due to a security flaw before EcmaScript 5. Seethe ``safe`` parameter for more information.:param encoder: Should be a json encoder class. Defaults to``django.core.serializers.json.DjangoJSONEncoder``.:param safe: Controls if only ``dict`` objects may be serialized. Defaultsto ``True``.:param json_dumps_params: A dictionary of kwargs passed to json.dumps()."""def __init__(self, data, encoder=DjangoJSONEncoder, safe=True,json_dumps_params=None, **kwargs):if safe and not isinstance(data, dict):raise TypeError('In order to allow non-dict objects to be serialized set the ''safe parameter to False.')if json_dumps_params is None:json_dumps_params = {}kwargs.setdefault('content_type', 'application/json')data = json.dumps(data, cls=encoder, **json_dumps_params)super().__init__(content=data, **kwargs)
关键代码
def get_comment_tree(request):article_id = request.GET.get("article_id")ret = list(models.Comment.objects.filter(article_id=article_id).values("pk", "content", "parent_comment_id"))return JsonResponse(ret, safe=False)
<script>$(".tree_btn").click(function (){$.ajax({url:"/blog/get_comment_tree/",type:"get",data:{article_id:"{{ article_obj.pk }}"},success:function (data){console.log(data);}})})</script>
点击 评论树实现的效果

第八个功能—展开评论树(根)
业务逻辑
1. 展现形式1. 与楼层式展现的评论一样,仍然需要展示父子评论,但区别在于梯度式;2. 实现方式1. 首先处理view视图传送的数据,分别重新用变量接收;2. 设置条件,父评论为空则将数据填充至 ```comment_tree```;3. 此外单独处理每一条评论时,首先有两个变量,索引值以及评论对象[自命名];4. 构建标签字符串,用于展示数据[此段代码放置于Ajax的回调函数中];5. 字符串拼接并且append填充即可;3. 技巧将目标样式首先放在目标位置,从前端测试显示效果,然后再放到Ajax的回调函数中;
关键代码
$.each(data, function (index, comment_object) {var pk = comment_object.pk;var content = comment_object.content;var parent_comment_id = comment_object.parent_comment_id;if (!parent_comment_id) {var s = '<div comment_id="+pk+"><span>'+content+'</span></div>'$(".comment_tree").append(s);}})
实现效果

第八个功能—展开评论树(根以及子评论)
业务逻辑
1. 通过comment_id找到父评论标签,然后插入信息;2. 插入数据时,通过标签指定,比如插入第一级的根评论,标签ID为comment_tree,而第二级子评论标签则为comment_id,因为第一级根评论定位仅仅需要找到标签,而子评论的定位需要找到对应的父评论ID,而这个ID已经定义在H5的标签中,用于唯一区别每一个标签,以及用于定位;
关键核心代码
success: function (comment_list) {console.log(comment_list);$.each(comment_list, function (index, comment_object) {var pk = comment_object.pk;var content = comment_object.content;var parent_comment_id = comment_object.parent_comment_id;var s = '<div class="comment_item" comment_id=' + pk + '><span>' + content + '</span></div>';if (!parent_comment_id) {$(".comment_tree").append(s);} else {$("[comment_id=" + parent_comment_id + "]").append(s);}})}
为实现阶梯分层

最终实现效果


第九个功能—-对评论树的数据优化
业务逻辑
子评论跟随根评论,因此子评论对于根评论的顺序无感;而根评论是按照创建顺序排列和传递的;通过数据库查询语句以主键排序,确保数据永远按照PK顺序排列;去掉Ajax绑定评论树标签的局部刷新,直接改为全局刷新,每一次render直接能够看到结果;
代码优化

第十个功能—评论行为与文章评论计数同步—数据库优化[原子性]
知识补充(事务[transaction])
1. 概念数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列;事务由事务开始与事务结束之间执行的全部数据库操作组成;2. 特性1、原子性(Atomicity):事务中的全部操作在数据库中是不可分割的,要么全部完成,要么全部不执行。 [1]2、一致性(Consistency):几个并行执行的事务,其执行结果必须与按某一顺序 串行执行的结果相一致。 [1]3、隔离性(Isolation):事务的执行不受其他事务的干扰,事务执行的中间结果对其他事务必须是透明的。 [1]4、持久性(Durability):对于任意已提交事务,系统必须保证该事务对数据库的改变不被丢失,即使数据库出现故障。 [1]3. 事务的ACID特性是由关系数据库系统(DBMS)来实现的;
关键代码
with transaction.atomic():comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content, parent_comment_id=pid)models.Article.objects.filter(pk=article_id).update(comment_count=F("comment_count")+1)
最终效果


第十一个功能—评论通知管理员
业务逻辑
1. 首先在settings文件导入邮件配置信息2. 然后在评论区导入配置信息,同时开始配置邮件发送函数;3. [优化]用异步多线程配置发送邮件的函数;4. 参考文档: [https://cloud.tencent.com/developer/article/1745008]5. 官方文档:[EIMIL配置] [https://docs.djangoproject.com/zh-hans/3.2/topics/email/]6. 官方文档:[SETTINGS配置] [https://docs.djangoproject.com/zh-hans/3.2/ref/settings/#email-host]
settings中的配置
# 邮件相关配置EMAIL_HOST = "smtp.163.com"EMAIL_PORT = 25EMAIL_HOST_USER = '19970266104@163.com'EMAIL_HOST_PASSWORD = 'PGYSJZGFGBJCFSWV'EMAIL_FROM = 'caesartylor<admin@caesartylor.com>'EMAIL_USE_TLS = True
view.py
def comment(request):"""from django.db import transaction : 用于数据库事务同步"""article_title = models.Article.objects.get(nid=article_id)# 发送邮件from django.core.mail import send_mailfrom whereabouts import settingsimport threadingt = threading.Thread(target=send_mail, args=("您的文章%s新增了一条评论内容"%article_title,content,settings.EMAIL_HOST_USER,["419997284@qq.com"]))t.start()return JsonResponse(response)
开启163邮箱的SMTP服务

最终效果

Solved Problems
1 jquery-3.6.0.min.js:2 GET http://127.0.0.1:8001/blog/get_comment_tree/?article_id=4 500 (Internal Server Error)

解决方法: 将HttpResponse 修改为 JsonResponse[函数名使用错误]

环境保存
$.ajax({url: "/blog/get_comment_tree/",type: "get",data: {article_id: "{{ article_obj.pk }}"},success: function (comment_list) {console.log(comment_list);$.each(comment_list, function (index, comment_object) {var pk = comment_object.pk;var content = comment_object.content;var parent_comment_id = comment_object.parent_comment_id;var s = '<div class="comment_item" comment_id=' + pk + '><span>' + content + '</span></div>';if (!parent_comment_id) {$(".comment_tree").append(s);} else {$("[comment_id=" + parent_comment_id + "]").append(s);}})}})
data backup
views.py
from django.shortcuts import render, HttpResponse, redirectfrom django.http import JsonResponsefrom django.contrib import authfrom django.db import transactionfrom django.db.models import Count, F, Qfrom django.db.models.functions import TruncMonthimport PIL, random, jsonfrom blog.models import UserInfofrom blog.Myforms import UserFormfrom blog.utils.validCode import get_valide_code_imgfrom blog import modelsdef login(request):"""功能设计:验证码和用户信息的校验不区分验证码大小写,统一转换为大写 uppercaseauth.login:在请求中保留用户id和后端。这样,用户就不必在每次请求时都重新验证。请注意,匿名会话期间的数据集在用户登录时保留。auth.authenticate: 从client请求中提取数据,将数据与数据库进行匹配response: 字典,作为message传递提示信息request.POST: 包含所有前端传递的信息auth.login:保存单个用户的单次登录信息JsonResponse:Json化后端生成的提示信息"""if request.method == "POST":response = {"user": None, "msg": None}user = request.POST.get("user")# print(user)pwd = request.POST.get("pwd")# 前端提交的验证码valid_code_one = request.POST.get("valid_code")valid_code = str(valid_code_one)# 后端生成的验证码,由get_validCode_img负责生成valid_code_str = request.session.get("valid_code_str")print(valid_code) # 测试后端在提交前端显示之前保存的验证码print(valid_code_str) # 测试前端POST请求提交时给出的验证码if valid_code.upper() == valid_code_str.upper():user = auth.authenticate(username=user, password=pwd) # 将前端提交的密码与后端MySQL存储的用户名与密码匹配if user:auth.login(request, user) # 匹配成功后则将其注册request.user==当前登录对象,存储当前登录对象response["user"] = user.usernameelse:response["msg"] = "username or password error!"else:response["msg"] = "vali de code error!"return JsonResponse(response)return render(request, "blog/login.html")def get_validCode_img(request):"""调用blog/utils/valid_code程序生成代码用request.session传递后端生成验证码"""data = get_valide_code_img(request)# print(type(data))return HttpResponse(data)def index(request):"""需要导入整个models模块,然后导出所有的文章文章数据从models提取出来,然后由views视图将数据渲染的时候传递给首页index,首页index再进行相关的渲染"""article_list = models.Article.objects.all()return render(request, "blog/index.html", {"article_list": article_list})def registry(request):"""UserForm验证提交的用户名,密码,邮箱等数据用settings中的media处理头像文件如果提交的数据错误,则由一个字典在原页面上显示提示信息"""if request.is_ajax():# print(request.POST) # 输出结果 <QueryDict: {'csrfmiddlewaretoken': [# '1DRQx9q2UOwhlL3gRDMwhiGsxOvEmjrt6RgrnJVW4O1zhA6E2IjPAiAofmcfoXxl'], 'avatar': ['undefined']}>form = UserForm(request.POST) # 由UserForm做验证# print(form)response = {"user": None, "msg": None} # 用于前端交互,传递messageif form.is_valid():response["user"] = form.cleaned_data.get("user") # 验证通过则会传递用户名# 生成一张用户记录 UserInfo不仅是自己设计的用户表,也是用户验证组件的那一张表# 该属性用于处理形成摘要的用户注册信息,不能用UserInfo.objects.createuser = form.cleaned_data.get("user")pwd = form.cleaned_data.get("pwd")email = form.cleaned_data.get("email")avata_obj = request.FILES.get("avatar") # 指定前端提交时的字段名字,隶属于formdata对象extra = {}if avata_obj:extra["avatar"] = avata_objUserInfo.objects.create_user(username=user, password=pwd, email=email,**extra) # avatar是UserInfo的field, avatar_obj是前端传递的文件else:# print(form.cleaned_data)# print(form.errors)response["msg"] = form.errorsreturn JsonResponse(response)# 实例化对象,form = UserForm()# form为提示信息return render(request, "blog/registry.html", {"form": form})def logout(request):# from django.contrib import authauth.logout(request) # 等同于request.session.flush# return redirect("templates/blog/login.html")return redirect("/login/")def home_site(request, username, **kwargs):"""个人站点视图函数"""print("kwargs", kwargs)print("username",username)user = models.UserInfo.objects.filter(username=str(username)).first()if not user:return render(request, "blog/not_found.html")# 查询当前站点对象以及idblog = user.bloguserid = user.nidnid = blog.nid # 用作原地跳转标签匹配# 当前用户或者当前站点对应的所有文章article_list = models.Article.objects.filter(user=userid)if kwargs:condition = kwargs.get("condition")param = kwargs.get("param") # 2012-12if condition == "category":article_list = article_list.filter(category__title__icontains=param)elif condition == 'tag': # 通过tags字段回到Tagarticle_list = article_list.filter(tags__title__icontains=param)else:year, month = param.split("-")article_list = article_list.filter(create_time__year=year,create_time__month=month)# 查询当前站点的每一个分类名称以及对应的文章数目; 能用Article_category是因为article包含了外键category# cate_list = models.Category.objects.filter(blog__nid=nid).values_list("title").annotate(c=Count("Article_category"))# 查询当前站点的每一个标签名称以及对应的文章数# tag_list = models.Tag.objects.values('pk').annotate(c=Count("article")).values_list("title", "c").filter(# blog_id=nid)# 查询当前站点每一个年月的名称以及对应的文章数---单表分组查询# 引入函数专门处理日期分组:from django.db.models.functions import TruncMonth# year_month = models.Article.objects.filter(user=nid).extra(# select={"y_m_date": "date_format(create_time,'%%Y-%%m')"}).values(# 'y_m_date').annotate(c=Count("nid")).values_list('y_m_date', 'c')return render(request, "blog/home_site.html", {"username":username,"blog":blog, "article_list":article_list})def get_classification_data(username):user = models.UserInfo.objects.filter(username=str(username)).first()blog = user.bloguserid = user.nidnid = blog.nid # 用作原地跳转标签匹配cate_list = models.Category.objects.filter(blog__nid=nid).values_list("title").annotate(c=Count("Article_category"))tag_list = models.Tag.objects.values('pk').annotate(c=Count("article")).values_list("title", "c").filter(blog_id=nid)year_month = models.Article.objects.filter(user=nid).extra(select={"y_m_date": "date_format(create_time,'%%Y-%%m')"}).values('y_m_date').annotate(c=Count("nid")).values_list('y_m_date', 'c')return {"username":username,"blog": blog, "cate_list": cate_list, "tag_list": tag_list,"year_month": year_month}def article_detail(request, username, article_id):user = models.UserInfo.objects.filter(username=str(username)).first()blog = user.blogarticle_obj = models.Article.objects.filter(pk=article_id).first()comment_list = models.Comment.objects.filter(article_id=article_id)return render(request, "blog/article_detail.html",locals())def updown(request):"""用于处理点赞行为执行后前端通过POST请求发送过来的数据json用于反序列化from django.db.models import F 用于自加一from django.http import JsonResponse 用于返回字典"""print(request.POST)article_id = request.POST.get("article_id")is_up = json.loads(request.POST.get("is_up")) # 'true'print(is_up)print(type(is_up))user_id = request.POST.get("user_id") # 由session提供print(user_id)obj = models.ArticleUpDown.objects.filter(user_id=user_id, article_id=article_id).first()response = {"state":True}if not obj:articleupdown = models.ArticleUpDown.objects.create(user_id=user_id, article_id=article_id, is_up=is_up)queryset = models.Article.objects.filter(pk=article_id)if is_up:queryset.update(up_count=F("up_count") + 1)else:queryset.update(down_count=F("down_count") + 1)else:response["state"] = Falseresponse["handled"] = obj.is_upreturn JsonResponse(response)def comment(request):"""from django.db import transaction : 用于数据库事务同步"""print(request.POST)article_id = request.POST.get("article_id")pid = request.POST.get("pid")content = request.POST.get("content")user_id = request.POST.get("user_id")article_title = models.Article.objects.get(nid=article_id)with transaction.atomic():comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content, parent_comment_id=pid)models.Article.objects.filter(pk=article_id).update(comment_count=F("comment_count")+1)response = {}response["create_time"] = comment_obj.create_time.strftime("%Y-%m-%d %X")response["username"] = comment_obj.user.usernameresponse["content"] = content# 发送邮件from django.core.mail import send_mailfrom whereabouts import settingsimport threading## send_mail(# "您的文章%s新增了一条评论内容"%article_title,# content,# settings.EMAIL_HOST_USER,# ["419997284@qq.com"]# )t = threading.Thread(target=send_mail, args=("您的文章%s新增了一条评论内容"%article_title,content,settings.EMAIL_HOST_USER,["419997284@qq.com"]))t.start()return JsonResponse(response)def get_comment_tree(request):article_id = request.GET.get("article_id")ret = list(models.Comment.objects.filter(article_id=article_id).order_by("pk").values("pk", "content", "parent_comment_id"))return JsonResponse(ret, safe=False)
article_detail.html
<!DOCTYPE html>{% extends 'base.html' %}{% load static %}<html lang="en">{% block content %}<head><meta charset="UTF-8"><title>文章详情页</title></head>{% csrf_token %}<style>.diggit {background: url('{% static 'font/upup.gif' %}') no-repeat;/*background: url("data:image/gif;base64,R0lGODlhLgA0AMQAAP/00P/22vfqt/b29v/11f/45P/56P/34PjtwfXlqf/55v/11//00v/44vnwyfn5+f/22Pbnrf/21vny0Pn00//33vDw8P/12KioqP/10ufm61B1uv/33f/33P////H4+iH5BAAAAAAALAAAAAAuADQAAAX/oJeMZGmeaJqKQOu+cCzPcxLJmZvlcM4DPlosE4kwMsfdLrlEIo8MZvRJjTKbxaZyy+16v94iYUwum8/odDlDjgjU8Lh8LBAs7mQ8YTHmS/aAfHyBeHeCg3l1dxAQFwsXkBeMjJILEJaNko2bk4+Pl4x3knUBpaanqKmqq6sCCAEdsR2ws6mytbSwprGns766HQgIHByxxR3FxMi3y8nIycTO0MqywtTH0Mu30s/PytLRt9bYxt3Hstzf4eDUsePlzc7bzebr8e3Bw+Tx2uj06t/unct3rVzAeeC8pQtXTR+8bAP/1Qs4beA7bRCZLVT4j6E7hxjNaezIkZ1FkOQO//pLGG0hvosp5a3k17LjSwQVcurcybOnz58/ETg4QLSo0aNIkypV6sBBg6dPC0CdWkBqg6pTs1q1qpWrgwlVw4odS7as2bMTJihYy7at27dw48adQMGA3bt48+rdy5cv3b6AAwu2S8EDhcOIEytezLhxYw+QI0ueTLmy5cuYM2veDPmD58+gQ4seTbq0htIPBqguzbq159OjH2jQgEHDANe4RcMWPUCDBwsYHuQe/pr0bA8PMNwmnnt36N6/lYve8Jk69enVdYdO3ds38OWgr4sPv6G8+fOfndPGwB5DdPAf0J8vjz2+ds8eMFigPEB/aOvjVTfffOl91t96+0X2HYB58REYHmu7JWfZgp7RZ9+FDw4I2m79teebgrVVeGGAD5ZYoGcSVjbBBvCJ52CDDb4Y4Xq1SQYcBSWSaN91Ioa222+0WSDkkBiwmGOPJmJ4ooHsbVDkkw4I6CJ62cV4H2gPFHDABVsWAB9zEIIppnFjlrnhbGimqeaabLbp5mwhAAA7") no-repeat;*/float: left;width: 46px;height: 52px;text-align: center;cursor: pointer;margin-top: 2px;padding-top: 5px;}.buryit {float: right;margin-left: 20px;width: 46px;height: 52px;background: url('{% static 'font/downdown.gif' %}') no-repeat;/*background:url("data:image/gif;base64,R0lGODlhLgA0AMQAAP////39/fn5+fj4+Pb29vX19fPz8/Ly8vHx8fDw8O/v7+7u7u3t7ezs7Ovr6+rq6ujo6Ofn5+bm5uXl5ePj4+Li4uHh4dzc3NfX19HR0c/Pz6qqqqioqKenp4WFhYODgyH5BAQUAP8ALAAAAAAuADQAAAX/IKCNZGmeaJqKUeu+cCzP80jfeN5mGeT/wKBwSCTyisik0sd7OJ/QqHRKpWYw1ax268RgHGDuFkwuO6Res3rNbre9jbh8Tq/b73fMpcHo+xl2f3iDdRcXC4iJiol/jY2LkIh/hpGQjpcMlYuTh5qMmI+ekn6UogugoaKcpqeofqyrpq6vsqSdqrOZtX2luLOwtsC/uwy9nrm6vsW3x7nCy6zR0ogXFgrX2Nna29zd3RYWCeLj5OXm5+jo4Ajs7e7v8PHy8hQUB/f4+fr7/P399QYCChxIsKDBgwcBIlzIsKGBCQAmSJxIsaLFixgxAtjIsaPHjyBDihxJsqTJjQVS8KpcybKly5cwIcCcSbOmSpk2c+pMiZOlAHYOFDhAQGBnzp4rN2z4wIEp0wpGayIFkOABB3MdPBh4+aHrB5pfb64UYJUDBwgdFXCYsNJr2LZu265EKoBDSAoftqp8O5MvX6QFzJpFyzHBWbhuu+5NmVgxz7EcAoA0XHQxS78FMItdedZsgo4HOFRmnNmy3NKoH6sEwAHBxwEcFlxOzZc06b8sIwg2S5WD3tNfa5cOG3xuSwEECPjoPdq2c8uaVb9ES3l25uKmoxcAPBdt3ebB/Tpu6dV4TAChBUSVClOADw4Smq93yX1scvnzWy7Zzz9ICAA7") no-repeat;*/text-align: center;cursor: pointer;margin-top: 2px;padding-top: 5px;}.clear {clear: both;color: orangered;font-size: 14px;}.diggword {clear: both;color: palevioletred;font-size: 14px;}input.author {background-image: url('{% static 'font/icon_form.gif' %}');background-repeat: no-repeat;border: 1px solid #cccccc;padding: 4px 4px 4px 30px;width: 300px;font-size: 16px;background-position: 3px -3px;}</style><div class="article_info"><h3 class="text-center">{{ article_obj.title }}</h3><div class="content">{{ article_obj.content | safe }}</div><div class="clearfix"><div id="div_digg"><div class="diggit action"><span class="diggnum" id="digg_count">{{ article_obj.up_count }}</span></div><div class="buryit action"><span class="burynum" id="bury_count">{{ article_obj.down_count }}</span></div><div class="clear"></div><div class="diggword" id="digg_tips"></div></div></div><div class="comments list-group"><p class="tree_btn">评论树</p><div class="comment_tree"></div><script>{# 用于评论树的局部刷新,与view中的get_comment_tree函数关联 #}$.ajax({url: "/blog/get_comment_tree/",type: "get",data: {article_id: "{{ article_obj.pk }}"},success: function (comment_list) {console.log(comment_list);$.each(comment_list, function (index, comment_object) {var pk = comment_object.pk;var content = comment_object.content;var parent_comment_id = comment_object.parent_comment_id;var s = '<div class="comment_item" comment_id=' + pk + '><span>' + content + '</span></div>';if (!parent_comment_id) {$(".comment_tree").append(s);} else {$("[comment_id=" + parent_comment_id + "]").append(s);}})}})</script></div><p>评论列表</p><ul class="list-group comment_list">{% for comment in comment_list %}<li class="list-group-item"><div><a href=""># {{ forloop.counter }}</a> <span> {{ comment.create_time|date:"Y-m-d H:i" }}</span> <a><span>{{ comment.user.username }}</span></a> <a class="pull-right reply_btn" username="{{ comment.user.username }}"comment_pk="{{ comment.pk }}">回复</a> </div>{% if comment.parent_comment_id %}<div class="pid_info well"><p>{{ comment.parent_comment.user.username }}:{{ comment.parent_comment.content }}</p></div>{% endif %}<div class="comment-con"><p>{{ comment.content }}</p></div></li>{% endfor %}</ul><p>发表评论</p><p>昵称:<input type="text" id="xyz" class="author" disabled="disabled" size="50"value="{{ user.username }}"></p><p>评论内容</p><textarea name="" id="comment_content" cols="45" rows="8"></textarea><br><button class="btn btn-default comment_btn"></button></div><script>{# 用于提交评论,数据从标签中取得,交给views.comment存储,提交成功后触发对多次点赞的阻止和提示 #}$("#div_digg .action").click(function () {var is_up = $(this).hasClass("diggit");$obj = $(this).children("span");$.ajax({url: "/blog/updown/", {# 需要自行实现该路径,也就是实现点赞行为记录之后返回提示的页面#}type: "post", {# 提交数据,使用PIOST请求#}data: {"csrfmiddlewaretoken": $("[name='csrfmiddlewaretoken']").val(), {# 帮助进行安全性校验 #}"is_up": is_up,"user_id": "{{ user.nid }}","article_id": "{{ article_obj.pk }}", {# 仅需要传递文章ID,点赞人与评论人即为当前文章的登陆者 #}}, {# 哪一个用户,对哪一篇文章,进行什么行为 #}success: function (data) {console.log(data);if (data.state) {var val = parseInt($obj.text());$obj.text(val + 1);} {# 如果状态码为true,说明是第一次操作 #}else {var val = data.handled ? "您已经推荐过!" : "您已经反对过!"$("#digg_tips").html(val);setTimeout(function () {$("#digg_tips").html("")}, 1000)}}})});{# 从评论中获取数据,完成点击回复后跳转,输入内容作为子评论存入数据库的功能#}var pid = "";$(".comment_btn").click(function () {var content = $("#comment_content").val();if (pid) {var index = content.indexOf("\n");content = content.slice(index + 1)}$.ajax({url: "/blog/comment/", {# 指定URL #}type: "post", {# 请求方式 #}data: {"csrfmiddlewaretoken": $("[name='csrfmiddlewaretoken']").val(), {# 帮助进行安全性校验 #}"article_id": "{{ article_obj.pk }}","content": content, {# 该数据从H5标签中取得 #}"user_id": "{{ user.nid }}","pid": pid, {# 默认为空,根评论本身就是父评论 #}},success: function (data) { {# 回调函数,成功处理请求之后 #}console.log(data);var create_time = data.create_time;var username = data.username;var content = data.content;var s = `<li class= "list-group-item" >< div ><span> ${create_time}</span> <a href=""><span>${username}</span></a> </div><div class="comment-con"><p>${content}</p></div></li>`;$("ul.comment_list").append(s);{# 清空评论框内容 #}pid = "",$("#comment_content").val("");}})});{# 回复按钮事件 #}$(".reply_btn").click(function () {$('#comment_content').focus();var val = "@" + $(this).attr("username") + "\n";$('#comment_content').val(val);pid = $(this).attr("comment_pk");})</script>{% endblock %}</html>
article_detail.css
{% load static %}.article_info .title{margin-bottom: 20px;}#div_digg {float: right;margin-bottom: 10px;margin-right: 30px;font-size: 12px;width: 125px;text-align: center;margin-top: 10px;}.diggit {float: left;width: 46px;height: 52px;/*background: url("data:image/gif;base64,R0lGODlhLgA0AMQAAP/00P/22vfqt/b29v/11f/45P/56P/34PjtwfXlqf/55v/11//00v/44vnwyfn5+f/22Pbnrf/21vny0Pn00//33vDw8P/12KioqP/10ufm61B1uv/33f/33P////H4+iH5BAAAAAAALAAAAAAuADQAAAX/oJeMZGmeaJqKQOu+cCzPcxLJmZvlcM4DPlosE4kwMsfdLrlEIo8MZvRJjTKbxaZyy+16v94iYUwum8/odDlDjgjU8Lh8LBAs7mQ8YTHmS/aAfHyBeHeCg3l1dxAQFwsXkBeMjJILEJaNko2bk4+Pl4x3knUBpaanqKmqq6sCCAEdsR2ws6mytbSwprGns766HQgIHByxxR3FxMi3y8nIycTO0MqywtTH0Mu30s/PytLRt9bYxt3Hstzf4eDUsePlzc7bzebr8e3Bw+Tx2uj06t/unct3rVzAeeC8pQtXTR+8bAP/1Qs4beA7bRCZLVT4j6E7hxjNaezIkZ1FkOQO//pLGG0hvosp5a3k17LjSwQVcurcybOnz58/ETg4QLSo0aNIkypV6sBBg6dPC0CdWkBqg6pTs1q1qpWrgwlVw4odS7as2bMTJihYy7at27dw48adQMGA3bt48+rdy5cv3b6AAwu2S8EDhcOIEytezLhxYw+QI0ueTLmy5cuYM2veDPmD58+gQ4seTbq0htIPBqguzbq159OjH2jQgEHDANe4RcMWPUCDBwsYHuQe/pr0bA8PMNwmnnt36N6/lYve8Jk69enVdYdO3ds38OWgr4sPv6G8+fOfndPGwB5DdPAf0J8vjz2+ds8eMFigPEB/aOvjVTfffOl91t96+0X2HYB58REYHmu7JWfZgp7RZ9+FDw4I2m79teebgrVVeGGAD5ZYoGcSVjbBBvCJ52CDDb4Y4Xq1SQYcBSWSaN91Ioa222+0WSDkkBiwmGOPJmJ4ooHsbVDkkw4I6CJ62cV4H2gPFHDABVsWAB9zEIIppnFjlrnhbGimqeaabLbp5mwhAAA7") no-repeat;*/text-align: center;cursor: pointer;margin-top: 2px;padding-top: 5px;}.buryit {float: right;margin-left: 20px;width: 46px;height: 52px;/*background:url("data:image/gif;base64,R0lGODlhLgA0AMQAAP////39/fn5+fj4+Pb29vX19fPz8/Ly8vHx8fDw8O/v7+7u7u3t7ezs7Ovr6+rq6ujo6Ofn5+bm5uXl5ePj4+Li4uHh4dzc3NfX19HR0c/Pz6qqqqioqKenp4WFhYODgyH5BAQUAP8ALAAAAAAuADQAAAX/IKCNZGmeaJqKUeu+cCzP80jfeN5mGeT/wKBwSCTyisik0sd7OJ/QqHRKpWYw1ax268RgHGDuFkwuO6Res3rNbre9jbh8Tq/b73fMpcHo+xl2f3iDdRcXC4iJiol/jY2LkIh/hpGQjpcMlYuTh5qMmI+ekn6UogugoaKcpqeofqyrpq6vsqSdqrOZtX2luLOwtsC/uwy9nrm6vsW3x7nCy6zR0ogXFgrX2Nna29zd3RYWCeLj5OXm5+jo4Ajs7e7v8PHy8hQUB/f4+fr7/P399QYCChxIsKDBgwcBIlzIsKGBCQAmSJxIsaLFixgxAtjIsaPHjyBDihxJsqTJjQVS8KpcybKly5cwIcCcSbOmSpk2c+pMiZOlAHYOFDhAQGBnzp4rN2z4wIEp0wpGayIFkOABB3MdPBh4+aHrB5pfb64UYJUDBwgdFXCYsNJr2LZu265EKoBDSAoftqp8O5MvX6QFzJpFyzHBWbhuu+5NmVgxz7EcAoA0XHQxS78FMItdedZsgo4HOFRmnNmy3NKoH6sEwAHBxwEcFlxOzZc06b8sIwg2S5WD3tNfa5cOG3xuSwEECPjoPdq2c8uaVb9ES3l25uKmoxcAPBdt3ebB/Tpu6dV4TAChBUSVClOADw4Smq93yX1scvnzWy7Zzz9ICAA7") no-repeat;*/text-align: center;cursor: pointer;margin-top: 2px;padding-top: 5px;}.clear {clear: both;}.comment-con{margin-top: 10px;text-align: center;}.comment_item{margin-left: 20px;}
settings.py
# 用于部署的静态文件(绝对路径),可以通过python manage.py collectstatic收集# simpleui依赖的配置项# STATIC_ROOT = os.path.join(BASE_DIR, "static")# Static files (CSS, JavaScript, Images)# https://docs.djangoproject.com/en/3.2/howto/static-files/# 引用位于 STATIC_ROOT 中的静态文件时要使用的 URL。STATIC_URL = '/fly/'# FileSystemFinder 查找器时将穿越的额外位置,使用 Unix 风格的斜线# https://docs.djangoproject.com/zh-hans/3.2/ref/settings/#std:setting-STATICFILES_DIRS# file path style : "C:/Users/user/mysite/extra_static_content"STATICFILES_DIRS = ["static",]# 文件系统默认配置# https://docs.djangoproject.com/zh-hans/3.2/ref/settings/#staticfiles-findersSTATICFILES_FINDERS = ['django.contrib.staticfiles.finders.FileSystemFinder','django.contrib.staticfiles.finders.AppDirectoriesFinder',]# 邮件相关配置EMAIL_HOST = "smtp.163.com"EMAIL_PORT = 25EMAIL_HOST_USER = '19970usde04@163.com'EMAIL_HOST_PASSWORD = 'PGYSJZGIPNJWWV'EMAIL_FROM = 'caesartylor<admin@caesartylor.com>'EMAIL_USE_TLS = True
