整体设计逻辑
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.blog
article_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 display
1. 评论入库时间;评论人即登录者;评论内容;
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.username
response["content"] = content
return 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`` objects
are allowed to be passed due to a security flaw before EcmaScript 5. See
the ``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. Defaults
to ``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 = 25
EMAIL_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_mail
from whereabouts import settings
import threading
t = 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, redirect
from django.http import JsonResponse
from django.contrib import auth
from django.db import transaction
from django.db.models import Count, F, Q
from django.db.models.functions import TruncMonth
import PIL, random, json
from blog.models import UserInfo
from blog.Myforms import UserForm
from blog.utils.validCode import get_valide_code_img
from blog import models
def login(request):
"""
功能设计:验证码和用户信息的校验
不区分验证码大小写,统一转换为大写 uppercase
auth.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.username
else:
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} # 用于前端交互,传递message
if form.is_valid():
response["user"] = form.cleaned_data.get("user") # 验证通过则会传递用户名
# 生成一张用户记录 UserInfo不仅是自己设计的用户表,也是用户验证组件的那一张表
# 该属性用于处理形成摘要的用户注册信息,不能用UserInfo.objects.create
user = 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_obj
UserInfo.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.errors
return JsonResponse(response)
# 实例化对象,
form = UserForm()
# form为提示信息
return render(request, "blog/registry.html", {"form": form})
def logout(request):
# from django.contrib import auth
auth.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")
# 查询当前站点对象以及id
blog = user.blog
userid = user.nid
nid = blog.nid # 用作原地跳转标签匹配
# 当前用户或者当前站点对应的所有文章
article_list = models.Article.objects.filter(user=userid)
if kwargs:
condition = kwargs.get("condition")
param = kwargs.get("param") # 2012-12
if condition == "category":
article_list = article_list.filter(category__title__icontains=param)
elif condition == 'tag': # 通过tags字段回到Tag
article_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.blog
userid = user.nid
nid = 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.blog
article_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"] = False
response["handled"] = obj.is_up
return 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.username
response["content"] = content
# 发送邮件
from django.core.mail import send_mail
from whereabouts import settings
import 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-finders
STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
]
# 邮件相关配置
EMAIL_HOST = "smtp.163.com"
EMAIL_PORT = 25
EMAIL_HOST_USER = '19970usde04@163.com'
EMAIL_HOST_PASSWORD = 'PGYSJZGIPNJWWV'
EMAIL_FROM = 'caesartylor<admin@caesartylor.com>'
EMAIL_USE_TLS = True