第三篇:BBS侧边栏inclusion_tag、点赞和评论

第三篇:BBS侧边栏inclusion_tag、点赞和评论

一、侧边栏inclusion_tag

1、url冲突

由于url方法第一个参数是正则表达式,所有当路由特别多的时候,可能会出现被顶替的情况,针对这种情况有两种解决方式。【找到绝望,才找到bug,我贼】

1.修改正则表达式
2.调整url方法的位置

比如:

2、模板的继承

由于文章详情页和个人站点基本一致,所以我们可以使用模板的继承。将个人站点页进行修改,统一继承base.html中的样式即可。

3、侧边栏inclusion_tag

我们继承之后,会发现一个问题,就是在个人站点页面的分类、标签等内容可以正常显示,而在文章主页中的内容却不能正常显示,这是为什么?我们观看代码。

# 1 查询当前个人站点下所有的分类及分类下的文章数
category_list = models.Category.objects.filter(blog=blog).annotate(count_num=Count('article__pk')).values_list(
    'name', 'count_num', 'pk')
"""
    1、先过滤出个人站点下的所有分类
    2、将个人站点过滤出的分类进行分组
    3、统计出分组之后分类下的文章数【反向查询(直接表名小写+__, 就可以到达有外键的表)】
    4、拿出分类名和分类下的文章数
"""
# print(category_list)  # <QuerySet [('yangyi的分类一', 2), ('yangyi的分类二', 1), ('yangyi的分类三', 1)]>
# 2 查询当前个人站点下所有的标签及标签下的文章数
tag_list = models.Tag.objects.filter(blog=blog).annotate(count_num=Count('article__pk')).values_list('tag',
                                                                                                     'count_num',
                                                                                                     'pk')
# print(tag_list)  # <QuerySet [('yangyi的标签一', 1), ('yangyi的标签二', 2), ('yangyi的标签三', 1)]>
# 3 查询当前个人站点下按照年月统计所有的文章
date_list = models.Article.objects.filter(blog=blog).annotate(month=TruncMonth('create_time')).values(
    'month').annotate(count_num=Count('pk')).values_list('month', 'count_num')
# print(date_list)  # <QuerySet [(datetime.date(2020, 6, 1), 1), (datetime.date(2021, 5, 1), 1), (datetime.date(2021, 2, 1), 1), (datetime.date(2021, 7, 1), 1)]>

就是因为使用了模板的继承之后,我们发现文章页面并不能提供给base页面中的侧边栏中需要的参数,那么我们该如何解决这个问题呢?

# 第一种方式:直接在文章详情函数中赋值即可 【简单粗暴,但是代码重复】

# 第二种方式:将侧边栏做成inclusion_tag
侧边栏的渲染需要传输数据才能渲染,并且该侧边栏在很多页面都需要使用
"""
步骤:
	1.在应用下创建一个名字必须叫templatetags文件夹
	2.在该文件夹内创建一个任意名称的py文件
	3.在该py文件内先固定写两行代码
  		from django import template
  		register = template.Library()
  		# 自定义过滤器
  		# 自定义标签
  		# 自定义inclusion_tag
"""

代码如下所示。

"""将侧边栏做成inclusion_tag"""
from django import template
from app01 import models
from django.db.models import Count
from django.db.models.functions import TruncMonth

register = template.Library()

@register.inclusion_tag('left_menu.html')
def left_menu(username):
    # blog = models.Blog.objects.filter(userinfo__username=username).first()
    user_obj = models.UserInfo.objects.filter(username=username).first()
    blog = user_obj.blog
    # 1 查询当前个人站点下所有的分类及分类下的文章数
    category_list = models.Category.objects.filter(blog=blog).annotate(count_num=Count('article__pk')).values_list(
        'name', 'count_num', 'pk')
    """
        1、先过滤出个人站点下的所有分类
        2、将个人站点过滤出的分类进行分组
        3、统计出分组之后分类下的文章数【反向查询(直接表名小写+__, 就可以到达有外键的表)】
        4、拿出分类名和分类下的文章数
    """
    # print(category_list)  # <QuerySet [('yangyi的分类一', 2), ('yangyi的分类二', 1), ('yangyi的分类三', 1)]>
    # 2 查询当前个人站点下所有的标签及标签下的文章数
    tag_list = models.Tag.objects.filter(blog=blog).annotate(count_num=Count('article__pk')).values_list('tag',
                                                                                                         'count_num',
                                                                                                         'pk')
    # print(tag_list)  # <QuerySet [('yangyi的标签一', 1), ('yangyi的标签二', 2), ('yangyi的标签三', 1)]>
    # 3 查询当前个人站点下按照年月统计所有的文章
    date_list = models.Article.objects.filter(blog=blog).annotate(month=TruncMonth('create_time')).values(
        'month').annotate(count_num=Count('pk')).values_list('month', 'count_num')
    # print(date_list)  # <QuerySet [(datetime.date(2020, 6, 1), 1), (datetime.date(2021, 5, 1), 1), (datetime.date(2021, 2, 1), 1), (datetime.date(2021, 7, 1), 1)]>
    return locals()

使用方式,在base.html中直接加载就可以使用。

"""base.py""" # 之后,只要页面能够传username就可以加载侧边栏
<div class="col-md-3">
    {% load mytag %}
    {% left_menu username %}
</div>

同理,我们也可以将个人站点的样式,做成inclusion_tag的形式,而且还保证了个人站点的各个页面的样式都是一致的【以前的普通导入,可能会导致样式不渲染(如果不传递blog的话)】,代码如下。

"""mytag.py"""
from django import template
register = template.Library()

# 个人站点显示同样颜色
@register.inclusion_tag('user_theme.html')
def user_theme(username):
    blog = models.Blog.objects.filter(userinfo__username=username).first()
    return locals()

"""user_theme.html"""
<link rel="stylesheet" href="/media/css/{{ blog.site_theme }}">

"""base.html"""
{% load mytag %}
{% user_theme username %}
{#<link rel="stylesheet" href="/media/css/{{ blog.site_theme }}">#}

二、点赞点踩功能

1、文章素材准备

浏览器上你看到的花里胡哨的页面,内部都是HTML(前端)代码
为了显示效果,我们可以拷贝别人的html代码进行演示  >>>  copy outerhtml

使用admin后天管理进行添加,继承base.py的article_detail.py中进行直接渲染
"""
 {#文章标签和内容开始#}
<h1>{{ article_obj.title }}</h1>
<div>{{ article_obj.content|safe }}</div>
"""

2、点赞点踩图标展示

我们下载博客园的图标,渲染到自己的页面中。【由于有图片防盗链的问题,所以将图片直接下载到本地,static文件夹中】
<!--html代码-->
{#点赞点踩开始#}
<div id="div_digg" class="clearfix">
    <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>
{#点赞点踩结束#}


<!--css代码-->
{% block 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.gif') no-repeat;
    text-align: center;
    cursor: pointer;
    margin-top: 2px;
    padding-top: 5px;
}
#div_digg .diggnum {
    line-height: 1.5em !important;
}
.buryit {
    float: right;
    margin-left: 20px;
    width: 46px;
    height: 52px;
    background: url('/static/img/downdown.gif') no-repeat;
    text-align: center;
    cursor: pointer;
    margin-top: 2px;
    padding-top: 5px;
}
.clear {
    clear: both;
}
element.style {
    color: red;
}
</style>
{% endblock %}

渲染的样式如下。

3、点赞点踩功能实现

我们分为三个部分,前端点赞点踩按钮的渲染、js代码、后端代码

  • article_detail.html 按钮渲染
{#点赞点踩开始#}
<div id="div_digg" class="clearfix">
    <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>
{#点赞点踩结束#}
  • article_detail.html js代码
{#点赞点踩js代码#}
// 给action块绑定点击事件
$('.action').click(function () {
    // 定义一个变量记录是否点赞
    let isUp = $(this).hasClass('diggit');
    // 回调函数拿不到 $(this)了,必须在这里定义一下
    let $div = $(this);
    // alert(isUp)  // boolean
    // 发送ajax请求
    $.ajax({
        url: '/up_or_down/',
        type: 'post',
        data: {
            'article_id': '{{ article_id }}',
            'isUp': isUp,
            'csrfmiddlewaretoken': '{{ csrf_token }}'
        },
        success: function (args) {
            // 如果点赞点踩成功
            if(args.code === 1000) {
                // 临时的修改数据,因为ajax请求,不会直接刷新以前的数据,必须重新刷新页面,才会修改数据
                let old_num = $div.children().text();
                let new_num = Number(old_num) + 1;  // 将字符串转化成数字进行相加
                $div.children().text(new_num)  // 为什么不能立马渲染出来呢?
                $('#digg_tips').html(args.msg)
            }else{
                // 添加没有成功相关的消息
                $('#digg_tips').html(args.msg)
            }

        }
    })
})
  • views.py
# 点赞点踩
def up_or_down(request):
    """
    1 判断用户是否登录,
        1.1 用户不能给自己点赞
        1.2 点赞之后不能进行修改
    2 操作数据库
    """
    # 如果是ajax请求
    if request.is_ajax():
        if request.method == 'POST':
            # ajax返回字典
            back_dic = {'code': 1000, 'msg': ''}
            # 获取到前端传过来的数据
            article_id = request.POST.get('article_id')
            is_up = request.POST.get('isUp')
            is_up = json.loads(is_up)  # 反序列化
            # print(article_id, is_up)  # 1 true
            print(request.user)
            # 如果用户登录【正向判断】不能这样判断用户是否登录,即便没有登陆,request.user依然是一个AnonymousUser
            if request.user.is_authenticated():
                # 如果用户给自己点赞
                if request.user == models.UserInfo.objects.filter(blog__article__pk=article_id).first():
                    back_dic['code'] = 3000
                    back_dic['msg'] = '不能给自己点赞'
                else:
                    # 这里还要判断用户之前是否点击
                    is_click = models.UpAndDown.objects.filter(user=request.user, article_id=article_id).first()
                    # 如果用户之前点击过
                    if not is_click:
                        # 如果用户点了赞
                        if is_up:
                            # 修改数据库数据【哪个用户给哪篇文章点了什么】
                            models.UpAndDown.objects.create(user=request.user, article_id=article_id, is_up=is_up)
                            # 注意还要给文章表 同步更新
                            models.Article.objects.filter(pk=article_id).update(up_num=F('up_num') + 1)
                            # 修改back_dic中的值
                            back_dic['msg'] = '点赞成功'
                        else:
                            # 修改数据库数据【哪个用户给哪篇文章点了什么】
                            models.UpAndDown.objects.create(user=request.user, article_id=article_id, is_up=is_up)
                            # 注意还要给文章表 同步更新
                            models.Article.objects.filter(pk=article_id).update(down_num=F('down_num') + 1)
                            # 修改back_dic中的值
                            back_dic['msg'] = '点踩成功'
                    else:
                        back_dic['code'] = 4000
                        back_dic['msg'] = '你之前已经点过了'
            else:
                print('用户没有登录')
                back_dic['code'] = 2000
                back_dic['msg'] = '请先<a href="/login/">登录</a>'
            # 返回ajax数据
            return JsonResponse(back_dic)
    return HttpResponse('点赞点踩')

最终,可以实现一个点赞点踩功能,效果如下。

我给leichao的文章点赞。

如果没有登陆,进行点赞。还会直接跳转到登录界面。

4、点赞点踩总结

"""前端界面"""
1.拷贝博客园点赞点踩前端样式
  html代码 + css代码
2.如何判断用户到底点击了哪个图标?
  恰巧页面上只有两个图标,所以给两个图标标签添加一个公共的样式类,
  然后给这个样式类绑定点击事件,再利用this指代的就是当前被操作对象,
  利用hasClass判断是否有某个特定的类属性,从而判断出到底是两个图标中的哪一个
3.书写ajax代码朝后端提交数据
4.后端逻辑书写完毕之后,前端针对点赞点踩动作实现需要动态展示提示信息
5.前端点赞点踩数字自增1,【需要注意数据类型的问题】  Number(old_num) + 1
6.用户没有登陆,需要展示没有登陆提示,并且登陆可以点击跳转
  html()    |safe    mark_safe()
    
    
"""后端逻辑"""
1.先判断用户是否登陆
  request.user.authenticated()
2.再判断当前文字是否是当前用户自己写的
  通过文章主键值获取文章对象,之后利用orm查询获取文章对象对应的用户对象与request.user比对
3.判断当前用户是否已经给当前文章点了
  利用article_obj文章对象和request.user用户对象去点赞点踩表中筛选,数据如果有数据则点过,没有则没点
4.操作数据库,需要注意要同时操作两张表
# 前端发送过来的是否点赞是一个字符串 需要你自己转成布尔值或者用字符串判断
  is_up = json.loads(is_up)    F模块

   
"""
总结:在书写较为复杂的业务逻辑的时候,可以先按照一条线书写下去
 之后再去弥补其他线路情况。
"""

三、根评论子评论功能

1、根评论

我们可以实现下面的效果,比如。

根评论有两步渲染方式
1.DOM临时渲染
2.页面刷新render渲染

我们给雷超的文章进行评论,效果如下。

我们点击提交按钮。

然后,我们刷新界面,才是真正的评论楼效果。

2、子评论

子评论方式
1.点击回复自动聚焦到评论框
2.将回复按钮所在的那一行评论人的姓名
	@username
3.评论框内部自动换行

"""根评论子评论都是点击一个按钮朝后端提交数据的,即依靠parent_id进行辨识"""

我们换leichao进行登录,效果如下。

输入内容,显示效果如下。

刷新页面。

3、根评论子评论功能实现

那么该代码如何实现呢?

和上面一样,我们也是分为三个部分,前端渲染,js代码,后端代码。

  • article_detail.html 前端渲染
{#评论楼渲染开始#}
<br>
<br>
<br>
<br>
<div class="row">
    <div class="col-md-8">
        <ul class="list-group">
            {#循环每一篇文章的评论#}
            {% for comment_obj in article_obj.comment_set.all %}
                {#如果是子评论#}
                {% if comment_obj.parent_id %}
                     <li class="list-group-item">
                        <span>#{{ forloop.counter }}楼 {{ comment_obj.comment_time|date:'Y-m-d h:i:s' }} {{ comment_obj.user.username }} 回复 {{ comment_obj.parent.user.username }}</span>
                        <span class="pull-right reply" username="{{ article_obj.blog.userinfo.username }}" comment_id="{{ comment_obj.pk }}"><a>回复</a></span>
                        <div style="padding-left: 20px">
                            <p>@ {{ comment_obj.parent.user.username }}</p>
                            {{ comment_obj.content }}
                        </div>
                     </li>
                {% else %}
                     <li class="list-group-item">
                        <span>#{{ forloop.counter }}楼 {{ comment_obj.comment_time|date:'Y-m-d h:i:s' }} {{ comment_obj.user.username }}</span>
                        <span class="pull-right reply" username="{{ article_obj.blog.userinfo.username }}" comment_id="{{ comment_obj.pk }}"><a>回复</a></span>
                        <div style="padding-left: 20px">{{ comment_obj.content }}</div>
                     </li>
                {% endif %}
            {% endfor %}
{#                <li class="list-group-item" id="id_tem_comment">#}
{#                    <span></span>#}
{#                    <div></div>#}
{#                </li>#}
        </ul>
    </div>
</div>
{#评论楼渲染结束#}
{#评论功能开始#}
{% if request.user.is_authenticated %}
    <div>
        <p><span class="glyphicon glyphicon-comment"></span>发表评论</p>
        <div>
            <textarea name="comment" id="id_comment" cols="60" rows="10" ></textarea>
        </div>
        <button class="btn btn-primary" id="id_submit">提交评论</button>
        <span style="color: red" id="errors"></span>
    </div>
{% else %}
        <li><a href="{% url 'register' %}">注册</a></li>
        <li><a href="{% url 'login' %}">登陆</a></li>
{% endif %}
{#评论功能结束#}
  • article_detail.html js代码实现
// 设置一个全局的parentID字段
let parentId = null;
{#评论功能js代码#}
$('#id_submit').click(function (){
    // 像后端发送ajax请求[先不考虑子评论, 考虑 哪个用户给哪篇文章评论了哪些内容]
    let conTent = $('#id_comment').val();
    // 判断该评论是否有父评论[只有子评论才会走这条路]
    if(parentId){
        // 找到\n对应的索引 然后利用切片 但是前片顾头不顾尾 所以索引+1
        let indexNum = conTent.indexOf('\n') + 1;
        conTent = conTent.slice(indexNum);  // 将indexNum之前的所有数据切除 只保留后面的部分
    }
    // 发送ajax请求
    $.ajax({
        url: '/comment/',
        type: 'post',
        data: {
            'article_id': '{{ article_id }}',
            'comment': conTent,
            // 如果parentId没有值,那么就是null,后端存储null没有任何关系
            'parent_id': parentId,
            'csrfmiddlewaretoken': '{{ csrf_token }}'
        },
        success: function (args) {
            if(args.code === 1000){
                /*
                    // 动态将评论的内容,添加到页面中
                    $('#id_tem_comment').children().first().text('{{ request.user.username }}').next().text(conTent)
                    // 不管评论是否正确,评论完毕,之后内容都会消失
                    $('#id_comment').val('');
                */
                // 也可以采用下面的方法
                let userName = '{{ request.user.username }}';
                let template = `
                    <li class="list-group-item" id="id_tem_comment">
                        <span>${userName}</span>
                        <div>${conTent}</div>
                    </li>
                `;
                // 将生成好的标签添加到ul标签内
                $('.list-group').append(template);
                // 不管评论是否正确,评论完毕,之后内容都会消失
                $('#id_comment').val('');
                // 重置parentId
                parentId = null;
            }
        }
    })
})

{#回复评论功能#}
$('.reply').click(function () {
    // 1.将焦点放到textarea中,并将@添加到被回复名前面,跳转到下一行
    let username = $(this).attr('username');
    $('#id_comment').focus().val('@ ' + username + '\n');
    // 拿到当前评论的父评论id
    parentId = $(this).attr('comment_id');
})
  • views.py
from django.db import transaction

# 评论功能
def comment(request):
    if request.is_ajax():
        if request.method == 'POST':
            # 定义一个返回字典
            back_dic = {'code': 1000, 'msg': ''}
            # 拿到传过来的数据
            article_id = request.POST.get('article_id')
            comment_content = request.POST.get('comment')
            parent_id = request.POST.get('parent_id')
            print(comment_content)
            # 后端也判断用户是否登录
            if request.user.is_authenticated():
                # 开启事务
                with transaction.atomic():
                    # 直接将评论存到数据库中
                    models.Comment.objects.create(user=request.user, article_id=article_id, content=comment_content, parent_id=parent_id)
                    # 文章表中添加评论数+1
                    models.Article.objects.filter(pk=article_id).update(comment_num=F('comment_num')+1)
                back_dic['msg'] = '评论成功'
            else:
                back_dic['code'] = 2000
                back_dic['code'] = '评论失败'
            return JsonResponse(back_dic)
    return HttpResponse('评论功能')

4、评论实现总结

"""先写根评论"""
1.书写前端获取用户评论的标签
  可能点赞点踩有浮动带来的影响  clearfix
2.点击评论按钮发送ajax请求
3.后端针对评论单独开设url处理,后端逻辑其实非常的简单
4.针对根评论涉及到前端的两种渲染方式
  4.1 DOM操作临时渲染评论楼,需要用到模版字符串
  4.2 页面刷新永久(render)渲染
	  后端直接获取当前文章对应的所有评论,传递给html页面即可,
      前端利用for循环参考博客园评论楼样式渲染评论
  4.3 评论框里面的内容需要清空


"""再考虑子评论"""
从回复按钮入手,点击回复按钮发生了哪些事
1.评论框自动聚焦 .focus()
2.评论框里面自动添加对应评论的评论人姓名  @username\n
# 思考:
	   1.根评论和子评论点的是同一个按钮
	   2.根评论和子评论的区别
         其实之前的ajax代码只需要添加一个父评论id即可
3.点击回复按钮之后,我们应该获取到根评论对应的用户名和主键值
  针对主键值,多个函数都需要用,所以用全局变量的形式存储
4.针对子评论内容,需要切割出不是用户写的,@username\n
5.后端parent字段本来就可以为空,所以传不传值都可以直接存储数据
posted @   YangYi215  阅读(139)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
点击右上角即可分享
微信分享提示