BBS-文章详情页面
点击文章标题跳转到具体的文章中去
重点功能:
- 点赞点踩数前端实时显示
- 后端要对is_up做反序列化
- 评论临时渲染
- 对于子评论,要获取parent_id的值
一、添加路由
urls.py
1、详情页路由
# 文章详情页路由:/站点名/article/文章的id
url(r'^(?P<username>\w+)/article/(?P<article_id>\d+)$', views.article_detail),
这个路由放到位置在这三个中是最下的
2、点赞点踩路由
# 点赞点踩路由
url(r'^up_and_down/$', views.up_and_down),
3、评论功能路由
# 评论功能路由
url(r'^comment/$', views.comment),
二、文章详情功能前端
文章详情页前端步骤
1. 继承base.html页面
2. 模板变量显示文章标题和正文内容
3. 点赞点踩样式。复制博客园的样式。
4. 点赞点踩绑定事件
5. 评论功能
6. 评论按钮绑定点击事件
7. 评论列表
8. 回复功能绑定事件
9. 第6步中加多行子评论中的parent_id参数
点赞点踩js步骤
1. html标签搭建好。复制博客园的样。
2. 复制html,css样式(所有标签的样式都要复制),对有图片防盗链的图片,复制到我们的项目中,更改图片路径。
3. 数据用文章表中点赞数点踩数展示
4. 点赞点踩js,绑定点击事件。给点赞点踩的div定义相同的类,给这个类绑定事件
4.1 区分开到底点的是赞还是踩,判断某个元素有没有这个类:hasClass()方法,返回布尔值
4.2 获取数据,获取is_up字段数据,文章id数据
4.3 发起ajax请求,把数据提交到django 的后端
4.4 接收后端的数据
4.5 点赞成功显示的文字信息
4.6 不成功显示文字信息
4.7 点赞数实时展示,用js。逻辑:要先拿到原来的数量,然后+1,再写进去
博客园点赞点踩绑定事件思路分析:
<div id="div_digg">
<div class="diggit" onlick="votePost(17391134, 'Digg')"><!--绑定事件的另一种写法-->
<span class="diggnum" id="digg_count">{{ article_detail.up_num }}</span><!--点赞数-->
</div>
<div class="buryit" onlick="votePost(17391134, 'Bury')">
<span class="burynum" id="bury_count">{{ article_detail.down_num }}</span><!--点踩数-->
</div>
function votePost(id, s) {
if (s == 'Digg') {
// 点赞逻辑
} else {
// 点踩逻辑
}
}
评论功能
前端逻辑:
# 评论功能
样式,发表评论文字,一个大段文本框,一个按钮 # 可以使用富文本编辑器
# 评论按钮绑定点击事件
1. 获取参数,直接去comment表中查看有哪些字段必须从前端获取
2. 发起ajax请求,把数据提交到后端
3. 临时渲染评论的内容,需要评论用户和文本内容,参数中增加一个获取用户(sessionu获取)
3.1 es6中的模板语法,反引号
3.2 把html追加到指定元素之后,$(A).append(B)
3.3 清空输入框里面的评论内容
4. 加参数数据:父id。根评论,parent_id为空。子评论,parent_id有值
# 在事件之前定义全局变量,parent_id
评论列表
1. 复制一个列表标签
2. 数据展示
几楼,用forloop.counter计数,从1开始
评论时间,格式化
当前评论的用户,评论表找用户表,正向
回复a链接,href="javascript:;",增加两个属性,当前评论用户名,当前评论用户id。回复事件中使用
评论内容
# 回复功能绑定事件
1. 点击回复后文本框中显示@用户名加换行符,获取焦点事件
2. 给回复标签加两个属性当前用户当前id,js中直接获取标签属性
3. parent_id的值获取,是回复的评论id
前端代码
文章详情页面,在templates文件夹中新建article_detial.html,继承的基础模板base.html
<!-- 文章详情页面 -->
{% extends 'base.html' %}
{% block css %}
{# 点赞点踩css样式开始 #}
<style>
#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('/static/img/upup.png') 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('/static/img/downdown.png') no-repeat; /*更改成自己静态文件的路径*/
text-align: center;
cursor: pointer;
margin-top: 2px;
padding-top: 5px;
}
.clear {
clear: both;
}
.diggword {
margin-top: 5px;
margin-left: 0;
font-size: 12px;
color: #808080;
}
</style>
{# 点赞点踩css样式结束 #}
{% endblock %}
{% block content %}
{# 文章内容展示开始 #}
<h1 class="article_title">{{ article_obj.title }}</h1><!-- 文章标题 -->
<div class="article_content"><!-- 文章内容 -->
{{ article_obj.content|safe }}<!-- safe能显示标签样式 -->
</div>
{# 文章内容展示结束 #}
{# 点赞点踩样式开始 #}
<div class="clearfix"><!-- 解决父标签塌陷,加个类clearfix -->
<div id="div_digg">
<div class="diggit action">
<span class="diggnum" id="digg_count">{{ article_obj.up_num }}</span><!--点赞数,从文章表中查询出来的-->
</div>
<div class="buryit action">
<span class="burynum" id="bury_count">{{ article_obj.down_num }}</span><!--点踩数,从文章表中查询出来的-->
</div>
<div class="clear"></div>
<div class="diggword" id="digg_tips" style="color: red"></div><!--一个空div,用来添加点赞之后成功或者失败显示的文本-->
</div>
</div>
{# 点赞点踩样式结束 #}
{# 评论列表展示开始 #}
<div>
<p><span>评论列表</span></p>
<ul class="list-group">
{% for comment in comment_list %}
<li class="list-group-item">
{# #1楼 2023-05-11 17:32 哈哈哈哼#}
<span># {{ forloop.counter }}楼 </span><!-- 几楼,用forloop计数,从1开始 -->
<span>{{ comment.comment_time|date:'Y-m-d H:i' }}</span><!-- 评论时间,格式化 -->
<!-- 通过当前评论找用户,正向 -->
<span>{{ comment.user.username }}</span><!-- 评论的用户 -->
<span class="pull-right"><!-- 回复链接 -->
<!-- href为空,点击就会刷新页面,解决方法1:"#",解决方法2:href="javascript:;"-->
<!-- javascript:,冒号后面可以写js代码,执行了a标签后就会直接执行冒号后的js代码 -->
<a href="javascript:;" comment_username="{{ comment.user.username }}"
comment_id="{{ comment.pk }}" class="reply">回复</a>
</span>
<p>
{{ comment.content }}
</p><!-- 评论内容 -->
</li>
{% endfor %}
</ul>
</div>
{# 评论列表展示结束 #}
{# 评论功能开始 #}
<div>
<p><span class="glyphicon glyphicon-comment">发表评论</span></p>
<p>
<textarea name="" id="content" cols="30" rows="10"></textarea><!-- 大段文本框 -->
</p>
<button class="btn btn-success id_comment">提交评论</button>
</div>
{# 评论功能结束 #}
<div style="height: 500px"></div>
{% endblock %}
{% block js %}
<script>
{# 点赞点踩js开始 #}
$(".action").click(function () { // 给点赞点踩的div定义相同的类,给这个类绑定事件
var _this = $(this);
// 1.区分开到底点的是赞还是踩,判断某个元素有没有这个类:hasClass()方法,返回布尔值
// $(this).hasClass('diggit'); // 如果返回的是true,说明点的是赞,否则是踩
// 2.获取数据
// 2.1 获取is_up字段的数据
var is_up = $(this).hasClass('diggit');
// console.log(is_up, typeof is_up) // true 'boolean'
// 2.2 获取文章id,文章详情对象点pk
var article_id = '{{ article_obj.pk }}'
// 2.3 用户id,后端从可以session里获取
// 3.发起ajax请求,把数据提交到django 的后端
$.ajax({
url: '/up_and_down/', // 点赞点踩路由
type: 'post',
data: {is_up: is_up, article_id: article_id, csrfmiddlewaretoken: '{{ csrf_token }}'},
success: function (res) {
// 4.接收后端的数据
if (res.code == 200) {
// 5. 点赞成功显示的文字信息
$("#digg_tips").text(res.msg); // 文本操作
// 7.点赞数实时展示,用js。逻辑:要先拿到原来的数量,然后+1,再写进去
// 7.1 在外面先用一个变量把当前的action的jquery对象保存下来:_this
{# 这里用this指的是function函数 #}
// 7.2 先拿到原来的值
{# _this:表示外面的$(".action")标签,这样点赞点踩都能拿到;.children():是拿到儿子元素的div,就是具体的点赞或点踩的div;.text() #}
var old_num = _this.children().text();
// console.log(old_num, typeof old_num); // 3 string
// 7.3 值加一后添加到原来的位置
// 字符串需要转格式才能相加减
_this.children().text(parseInt(old_num) + 1);
} else {
// 6. 不成功,给div加个文本
// 识别标签的用html(value)设置值
$("#digg_tips").html(res.msg);
}
}
})
})
{# 点赞点踩js结束 #}
{# 评论按钮绑定点击事件开始 #}
// 根评论和子评论都是使用这个按钮
// 定义全局变量,直接在两个事件之前就定义parent_id
var parent_id = null;
$(".id_comment").click(function () {
// 1. 获取参数,直接去comment表中查看有哪些字段必须从前端获取
// 1.1 评论内容
var content = $("#content").val();
// 1.2 获取文章id
var article_id = '{{ article_obj.pk }}';
// 用户从哪里来?
var current_user = '{{ request.session.username }}'
// 2. 发起ajax请求,把数据提交到后端
$.ajax({
url: '/comment/', // 评论路由
type: 'post',
// 4. 加参数数据:父id。根评论,parent_id为空。子评论,parent_id有值
data: {content: content, article_id: article_id, csrfmiddlewaretoken: '{{ csrf_token }}', parent_id:parent_id},
success: function () {
// 3.临时渲染
var html = '';
// 3.1 es6中的模板语法,反引号,多行文本, ${变量名占位}
html = `
<li class="list-group-item">
<span>${current_user}</span>
<p>
${content}
</p>
</li>
` // 变量有用户名和评论内容,需要提前获取到
// 3.2 把html追加到指定元素之后, $(A).append(B),把B追加到A标签之后
$('.list-group').append(html);
// 3.3 清空输入框里面的评论内容
$('#content').val('');
}
});
});
{# 评论按钮绑定点击事件结束 #}
{# 回复功能绑定事件开始 #}
// 回复功能
$(".reply").click(function () {
// 2. 给回复标签加两个属性当前用户名、当前id,js中直接获取标签属性
// 获取回复标签中的用户名属性的值
var comment_username = $(this).attr('comment_username')
// 3. parent_id的值获取,是回复的评论id
// var parent_id = $(this).attr('comment_id'); 这样写parent_id是局部变量,想要按钮点击事件中也能使用,就把parent_id设置为全局变量
parent_id = $(this).attr('comment_id');
// 子评论需要哪些参数?
// 评论的内容
// 需要评论id
// 1. 点击回复后文本框中显示@用户名加换行符,获取焦点事件
$("#content").val('@' + comment_username + '/n').focus();
})
{# 回复功能绑定事件结束 #}
</script>
{% endblock %}
三、添加视图函数
文章详情页
步骤:
1. 首页、站点页中文章标题处添加路由
href="/{{ username }}/article/{{ article.pk }}"
2. 验证用户是否存在,因为只要跳转,网址是可以更改的
3. 获取网址中的username,查询用户信息,用户不存在返回404页面
4. 根据文章id查询文章对象
5. 读取当前文章的所有评论,按照当前文章id来过滤
``
代码:
```python
@login_auth
def article_detail(request, username, article_id):
"""文章详情页"""
# 1. 路由
# 2. 根据用户名查询用户信息
user_obj = models.UserInfo.objects.filter(username=username).first()
# 3. 验证用户是否存在,因为只要跳转,网址是可以更改的
if not user_obj:
return render(request, '404.html')
# 4. 根据文章id查询文章对象
article_obj = models.Article.objects.filter(pk=article_id).first()
# 5. 读取当前文章的所有评论,按照当前文章id来过滤
comment_list = models.Comment.objects.filter(article_id=article_id).all()
return render(request, 'article_detail.html', locals())
点赞点踩
后端步骤:
1. 定义返回给前端的数据格式
2. 接收参数
3. 验证参数
3.1 用户必须登录
3.2 自己的文章自己不能点赞,先通过当前文章去查询用户(谁写的),然后与当前登录用户比较
3.3 验证一篇文章一个人只能点一次。根据文章id和用户id两个条件,去表里面查询,如果查到了,说明已经点过了,不能再点了
4. 正常业务逻辑
4.1 对is_up做反序列化:json.loads
4.2 操作文章表,更新数据。点赞点踩要操作不同的字段,需要先判断
4.3 操作点赞点踩表,增加记录
5. 返回给前端数据
"""问题:"""
为什么ajax提交到后端的数据被序列化了,查看编码方式,查看前端打印的is_up的type类型
代码:
from django.db.models import F # 数据变动加一
import json # 序列化模块
@login_auth
# 单独开设一个方法,来处理点赞点踩逻辑
def up_and_down(request):
"""点赞点踩函数,不需要页面"""
"""
点赞点踩重要功能分析:
1. 必须登录
2. 如果没有登录,在页面上显示:请先登录,并且可以点击,跳转到登录页面
3. 自己的文章不能给自己点
4. 如果登录成功了,前端页面上点赞数据要+1,显示点赞成功
5. 如果点赞过,就不能再点了,也就是一篇文章一个用户只能点一次
"""
if request.method == 'POST':
# 1. 定义返回给前端的数据格式
back_dic = {'code': 200, 'msg': '点赞成功'}
# 2. 接收参数
is_up = request.POST.get('is_up') # true <class 'str'>
print(is_up, type(is_up)) # 为什么格式是序列化后的格式
article_id = request.POST.get('article_id')
# 3.验证参数
# 3.1 用户必须登录
if not request.session.get('username'):
back_dic['code'] = 1013
back_dic['msg'] = '<a href="/login/" style="color:red">请先登录</a>' # 后端直接返回a标签
return JsonResponse(back_dic)
# 3.2 自己的文章自己不能点
# 先通过当前文章去查询用户(谁写的),然后与当前登录用户比较
# 文章找作者:文章--> 站点 --> 用户表
# 先查询出当前的文章对象
article_obj = models.Article.objects.filter(pk=article_id).first()
# article_obj.blog.userinfo.username ----> request.session.get(username)
if article_obj.blog.userinfo.username == request.session.get('username'):
back_dic['code'] = 1014
back_dic['msg'] = '不能给自己点赞或点踩'
return JsonResponse(back_dic)
# 3.3 验证一篇文章一个人只能点一次
# 根据文章id和用户id两个条件,去表里面查询,如果查到了,说明已经点过了,不能再点了
is_click = models.UpAndDown.objects.filter(article_id=article_id, user_id=request.session.get('id')).first()
if is_click:
back_dic['code'] = 1015
back_dic['msg'] = '你已经点过了,不能再点了!!'
return JsonResponse(back_dic)
# 4.处理正常的业务逻辑
# 入库,操作哪些表? 1. 点赞点踩表 2. 文章表:点赞数,点踩数
# 4.1 这里需要对is_up做反序列化:json.loads
is_up = json.loads(is_up)
# 4.2 操作文章表,更新数据
if is_up: # 说明是点赞了
models.Article.objects.filter(pk=article_id).update(up_num=F('up_num') + 1) # 增加数据,用到F查询
else: # 说明是点踩了
models.Article.objects.filter(pk=article_id).update(down_num=F('down_num') + 1)
back_dic['msg'] = '我会更加努力的' # 点踩需要改一下消息
# 4.3 操作点赞点踩表,增加记录
models.UpAndDown.objects.create(is_up=is_up, article_id=article_id, user_id=request.session.get('id'))
# models.UpAndDown.objects.create(is_up=is_up, article(不加id)=article_obj(文章对象))
return JsonResponse(back_dic)
评论功能
后端步骤:
1. 定义返回前端的数据格式
2. 接收参数
3. 验证参数,内容不能为空
4. 处理正常业务逻辑,操作2张表,1 文章表(评论数),2 评论表
4.1 更新文章表中的评论数量
4.2 新增评论表中的一条记录
4.3 练习一下事务的使用
5.返回给前端数据
后端代码:
@login_auth
def comment(request):
"""
评论函数,不需要页面
"""
"""
评论功能分析:
1. 必须登录之后才能评论,前端的评论框必须是登录之后才显示
2. 先做根评论
3. 在做子评论
"""
if request.method == 'POST':
# 1. 定义返回前端的数据格式
back_dic = {'code': 200, 'msg': '评论成功'}
# 2. 接收参数
content = request.POST.get('content')
article_id = request.POST.get("article_id")
parent_id = request.POST.get("parent_id")
# 3. 验证参数
# 内容不能为空
if not content:
back_dic['code'] = 1016
back_dic['msg'] = '评论内容不能为空'
return JsonResponse(back_dic)
# 4.处理正常业务逻辑
# 操作2张表,1 文章表(评论数) 2 评论表
# 4.3 练习一下事务的使用
from django.db import transaction
try:
with transaction.atomic():
# 4.1 更新文章表中的评论数
models.Article.objects.filter(pk=article_id).update(comment_num=F('comment_num') + 1)
# 4.2 新增评论表中的一条记录
models.Comment.objects.create(content=content, article_id=article_id, user_id=request.session.get('id'),
parent_id=parent_id)
# user_id=request.session.get('id'),企业中这些经常使用的会直接封装成函数,我们直接调用函数获得值
except Exception as e:
print(e)
transaction.rollback()
# 5.返回给前端数据
return JsonResponse(back_dic)
复习知识点:
1.es6中的模板语法
可以定义大段文本,并且实现格式化字符操作
<!-- es6中的模板语法使用 -->
var name = 'kevin';
var age = 18;
console.log(`my name is ${name}, my age is ${age}`);
<!-- 输出结果是:my name is kevin, my age is 18 -->
2.forloop的用法:https://www.cnblogs.com/zjyao/p/17360259.html#for标签