Django项目实战:BBS+Blog项目开发之三评论功能与后台设计

上接:Django项目实战:BBS+Blog项目开发之二页面数据和渲染

五九. 博客系统之评论功能的实现流程

59.1 评论可以看成是BBS.  如果没有子评论, 则做法和点赞是差不多的.  难点在子评论的树型显示(有父子关系).    楼层显示方式比较简单.

  两种显示方法: rander 加载整个网页

        ajax-dom  局部刷新

 六零. 博客系统之评论样式  在文章详情页面构建发布评论的样式

60.1 最终效果

60.2 关键代码

        <div class="comment">
            <div id="comment_title">
                <p>发表评论:</p>
                昵称&ensp;<span class="glyphicon glyphicon-user"></span>
                <input disabled="disabled" value="{{ request.user.username }}">
            </div>
            <div>评论内容</div>
            <div><textarea id="comment-content" cols="60" rows="10"></textarea></div>
            <p>
                <button class="btn btn-default comment-btn">提交评论</button>
            </p>
        </div>
{# 继承 #}
{% extends 'base.html' %}

{% block content %}
    <div class="article_info">
        {% csrf_token %}
        <h3 class="text-center">{{ article_obj.title }}</h3>
        <div class="cont">
            {{ article_obj.content|safe }}
        </div>
        <div class="clearfix">
            <div id="div_digg">
                {#    diggit action   推荐       #}
                <div class="diggit action">
                    <span class="diggnum" id="digg_count">{{ article_obj.up_count }}</span>
                </div>
                {#    diggit action   反对       #}
                <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" style="color: red;"></div>
            </div>
        </div>


        <div class="comment">
            <div id="comment_title">
                <p>发表评论:</p>
                昵称&ensp;<span class="glyphicon glyphicon-user"></span>
                <input disabled="disabled" value="{{ request.user.username }}">
            </div>
            <div>评论内容</div>
            <div><textarea id="comment-content" cols="60" rows="10"></textarea></div>
            <p>
                <button class="btn btn-default comment-btn">提交评论</button>
            </p>
        </div>

        <script>
            // 注意 class属性是.action的有两个, 那么到底是点了哪一个,
            $('.action').click(function () {
                var is_up = $(this).hasClass('diggit');  // 有diggit属性,就是True, 否则就是False
                var $count_obj = $(this).children('span');  // 获取用于显示点赞数\或者是反对数的span标签对象
                $.ajax({
                    url: '/digg/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'is_up': is_up,
                    },
                    success: function (data) {
                        if (data.state) {
                            {#  state为True表示之前未点过  #}
                            var val = parseInt($count_obj.text());  // 获取点赞(反对)的数值
                            $count_obj.text(val + 1);  // 点赞(反对)数值加1
                        } else {
                            {# 三元运算符, True时,val=冒号前的内容, False时val=冒号后的内容 #}
                            var val = data.handled ? '您已经点过赞了!' : "您已经点过反对了!";
                            $(".diggword").html(val);
                            setTimeout(function () {
                                $(".diggword").html('')
                            }, 1000)
                        }
                    }
                })
            })
        </script>
    </div>

{% endblock %}
article_detail.html

 六一. 博客系统之提交根评论

61.1 分析数据库的comment评论表的字段, 搞清楚哪些数据需要ajax从前端获取,并传递给后端.

nid 自动生成
user 从视图中request直接获取
article_id 前端post传送
create_itme 自动生成
content 从前端post传送
parent_comment 前端post传送

61.2 前端绑定事件,获取数据,传递数据

        <div class="comment">
            <div id="comment_title">
                <p>发表评论:</p>
                昵称&ensp;<span class="glyphicon glyphicon-user"></span>
                <input disabled="disabled" value="{{ request.user.username }}">
            </div>
            <div>评论内容</div>
            <div><textarea id="comment-content" cols="60" rows="10"></textarea></div>
            <p>
                <button class="btn btn-default comment-btn">提交评论</button>
            </p>
        </div>
        <script>

            // 评论
            $('.comment-btn').click(function () {
                var pid = ''; // 父评论id, pid为空则表示当前为父评论.
                var content = $('#comment-content').val();  // 获取评论内容
                $.ajax({
                    url: '/comment/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'content': content,
                        'parent_comment':pid,
                    },
                    success: function (data) {
                        console.log(data);
                        $('#comment-content').val(''); // 将评论内容框清空
                    }
                })
            })

        </script>
{# 继承 #}
{% extends 'base.html' %}

{% block content %}
    <div class="article_info">
        {% csrf_token %}
        <h3 class="text-center">{{ article_obj.title }}</h3>
        <div class="cont">
            {{ article_obj.content|safe }}
        </div>
        <div class="clearfix">
            <div id="div_digg">
                {#    diggit action   推荐       #}
                <div class="diggit action">
                    <span class="diggnum" id="digg_count">{{ article_obj.up_count }}</span>
                </div>
                {#    diggit action   反对       #}
                <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" style="color: red;"></div>
            </div>
        </div>


        <div class="comment">
            <div id="comment_title">
                <p>发表评论:</p>
                昵称&ensp;<span class="glyphicon glyphicon-user"></span>
                <input disabled="disabled" value="{{ request.user.username }}">
            </div>
            <div>评论内容</div>
            <div><textarea id="comment-content" cols="60" rows="10"></textarea></div>
            <p>
                <button class="btn btn-default comment-btn">提交评论</button>
            </p>
        </div>

        <script>
            // 点赞
            $('.action').click(function () {
                var is_up = $(this).hasClass('diggit');  // 有diggit属性,就是True, 否则就是False
                var $count_obj = $(this).children('span');  // 获取用于显示点赞数\或者是反对数的span标签对象
                $.ajax({
                    url: '/digg/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'is_up': is_up,
                    },
                    success: function (data) {
                        if (data.state) {
                            {#  state为True表示之前未点过  #}
                            var val = parseInt($count_obj.text());  // 获取点赞(反对)的数值
                            $count_obj.text(val + 1);  // 点赞(反对)数值加1
                        } else {
                            {# 三元运算符, True时,val=冒号前的内容, False时val=冒号后的内容 #}
                            var val = data.handled ? '您已经点过赞了!' : "您已经点过反对了!";
                            $(".diggword").html(val);
                            setTimeout(function () {
                                $(".diggword").html('')
                            }, 1000)
                        }
                    }
                })
            })


            // 评论
            $('.comment-btn').click(function () {
                var pid = ''; // 父评论id, pid为空则表示当前为父评论.
                var content = $('#comment-content').val();  // 获取评论内容
                $.ajax({
                    url: '/comment/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'content': content,
                        'parent_comment':pid,
                    },
                    success: function (data) {
                        console.log(data);
                        $('#comment-content').val(''); // 将评论内容框清空
                    }
                })
            })


        </script>
    </div>

{% endblock %}
article_detail.html

61.2 后端视图,获取数据,写入数据库

def comment(request):
    """
    发布评论
    """
    print(request.POST)
    # 前端ajax发送数据, 这里予以获取
    article_id = request.POST.get('article_id')
    content = request.POST.get('content')
    pid = request.POST.get('pid')

    # 从当前用户登录信息中获取
    user_id = request.user.pk
    print("user_id:", user_id)

    # 写入到数据库comment表
    comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content, parent_comment_id=pid)

    return HttpResponse('comment')
from django.shortcuts import render, HttpResponse, redirect
from django.http import JsonResponse  # 传递json数据类型
from django.contrib import auth  # 导入用户组件,用于auth_user表的操作
from django.db.models import Avg, Max, Min, Count
from django.db.models.functions import TruncMonth
from blog import models

from blog.models import UserInfo
from blog.myforms import RegForm  # 导入自定义forms验证规则


def reg(request):
    """用户注册视图"""
    if request.is_ajax():
        form = RegForm(request.POST)
        respone = {'user': None, 'msg': None}
        if form.is_valid():
            respone['user'] = form.cleaned_data.get('user')
            user = form.cleaned_data.get('user')
            pwd = form.cleaned_data.get('pwd')
            email = form.cleaned_data.get('email')
            avatar_obj = request.FILES.get('avatar')
            extra = {}
            if avatar_obj:
                extra['avatar'] = avatar_obj
            user_obj = UserInfo.objects.create_user(username=user, password=pwd, email=email, **extra)
        else:
            respone['msg'] = form.errors
        return JsonResponse(respone)

    form = RegForm()  # 带入规则
    return render(request, 'reg.html', {"form": form})


def login(request):
    if request.method == "POST":
        response = {'user': None, 'msg': None}  # 构造登录提示信息
        user = request.POST.get('user')
        pwd = request.POST.get('pwd')
        valid_code = request.POST.get('valid_code')  # 获取用户输入的验证码
        valid_code_str = request.session.get('valid_code')  # 从session中提取自动生成的验证码字符串
        if valid_code_str.upper() == valid_code.upper():  # 全部转为大写字母
            user = auth.authenticate(username=user, password=pwd)
            if user:
                auth.login(request, user)  # yanz 成功在session中注册当前用户信息, 就有了request.user全局变量
                response['user'] = user.username  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.
            else:
                response['msg'] = '用户名或密码错误!'
        else:
            response['msg'] = '验证码错误!'
        return JsonResponse(response)  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.

    return render(request, 'login.html')


def get_validCode_img(request):
    """生成验证码图片"""
    from blog.utils.validCode import get_validCode_img  # 导入自定义的生成验证码功能函数
    data = get_validCode_img(request)
    return HttpResponse(data)


def index(request):
    article_list = models.Article.objects.all()
    return render(request, 'index.html', {'article_list': article_list})


def logout(request):
    auth.logout(request)
    return redirect('/login/')


def home_site(request, username, **kwargs):
    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')

    # 查询username用户的blog对象
    blog = user.blog
    article_list = models.Article.objects.filter(user=user)
    if kwargs:
        # 个人站点跳转
        condition = kwargs.get('condition')  # 使用**kwargs目的是可以接受任意多个参数
        param = kwargs.get('param')
        if condition == 'tag':
            article_list = article_list.filter(tags__title=param)
            # print(article_list)
        elif condition == 'category':
            article_list = article_list.filter(category__title=param)
        else:
            # 日期归档
            year, month = param.split('-')
            article_list = article_list.filter(create_time__year=year, create_time__month=month)

    return render(request, 'home_site.html', {'username': username, 'blog': blog, 'article_list': article_list})


def article_detail(request, username, article_id):
    """
    文章详情页视图
    http://127.0.0.1:8000/papa/article/2/
    """

    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')
    # 查询username用户的blog对象
    blog = user.blog
    article_obj = models.Article.objects.filter(pk=article_id).first()
    return render(request, 'article_detail.html', locals())  # locals()不能丢


def digg(request):
    print(request.user)
    import json
    article_id = request.POST.get('article_id')
    is_up = json.loads(request.POST.get('is_up'))  # 传递过来的是"True"字符串, 所以这里需要反序列化为布尔值
    user_id = request.user.pk  # 直接获取user_id,没有必要通过客户端传送

    # 判断是否已经点过赞
    response = {'state': True}  # 返回的状态,将传递给模板中的回调函数,默认是True成功, 表示为点过赞或者反对.
    ret_obj = models.ArticleUpDown.objects.filter(user_id=user_id, article_id=article_id).first()
    if not ret_obj:  # 没点过赞
        ard = models.ArticleUpDown.objects.create(user_id=user_id, article_id=article_id, is_up=is_up)
        # 将点赞写入数据库中
        from django.db.models import F
        article_obj = models.Article.objects.filter(pk=article_id)
        if is_up:
            article_obj.update(up_count=F("up_count")+1)
        else:
            article_obj.update(down_count=F("down_count")+1)
    else:  # 已经点过赞或反对了
        response['state'] = False  # 表示失败
        response['handled'] = ret_obj.is_up  # 获取当前数据库中是点赞过还是点反对过.

    return JsonResponse(response)  # 将状态传递给模板中的回调函数


def comment(request):
    """
    发布评论
    :param request:
    :return:
    """
    print(request.POST)
    # 前端ajax发送数据, 这里予以获取
    article_id = request.POST.get('article_id')
    content = request.POST.get('content')
    pid = request.POST.get('pid')

    # 从当前用户登录信息中获取
    user_id = request.user.pk
    print("user_id:", user_id)

    # 写入到数据库comment表
    comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content, parent_comment_id=pid)

    return HttpResponse('comment')
views.py

61.3 url加入评论的路由

path('comment/', views.comment),  # 评论
"""cnblog URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/3.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, re_path
from django.views.static import serve  # serve器

from blog import views
from cnblog import settings

urlpatterns = [
    path('admin/', admin.site.urls),
    path('login/', views.login),
    path('index/', views.index),
    path('logout/', views.logout),
    path('get_validCode_img/', views.get_validCode_img),
    path('reg/', views.reg),
    path('digg/', views.digg),  # 点赞
    path('comment/', views.comment),  # 评论

    # media配置, 外网直接访问用户上传文件的media文件夹的路由
    re_path(r'media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),  # 固定写法

    # 个人站点页面
    re_path(r'^(?P<username>\w+)/$', views.home_site),

    # 个人站点页面跳转
    re_path(r'^(?P<username>\w+)/(?P<condition>tag|category|archive)/(?P<param>.*)/$', views.home_site),

    # 个人站点文章详情页面
    re_path(r'^(?P<username>\w+)/article/(?P<article_id>\d+)/$', views.article_detail),

    #


]
urls.py

 六二. 博客系统之render显示评论  render方式就是从新加载网页

62.1 最终效果

 

 

62.2 前端样式

        <div class="comment">
            <div id="commnet-list">
                <p>评论列表</p>
                {#       复制bootstrap网站的组件中的列表组样式代码         #}
                <ul class="list-group">
                    {% for comment in comment_list %}
                        <li class="list-group-item">
                            <div>
                                <a href="">#{{ forloop.counter }}楼</a>&nbsp;&nbsp;
                                <span>{{ comment.create_time|date:"Y-m-d H:i" }}</span>&nbsp;&nbsp;
                                <a href="">{{ comment.user.username }}</a>
                                <div class="pull-right"><a href="">回复</a></div>
                            </div>
                            <div id="content-show" style="margin-top: 10px">
                                <p>{{ comment.content }}</p>
                            </div>
                        </li>
                    {% endfor %}
                </ul>
            </div>
{# 继承 #}
{% extends 'base.html' %}

{% block content %}
    <div class="article_info">
        {% csrf_token %}
        <h3 class="text-center">{{ article_obj.title }}</h3>
        <div class="cont">
            {{ article_obj.content|safe }}
        </div>
        <div class="clearfix">
            <div id="div_digg">
                {#    diggit action   推荐       #}
                <div class="diggit action">
                    <span class="diggnum" id="digg_count">{{ article_obj.up_count }}</span>
                </div>
                {#    diggit action   反对       #}
                <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" style="color: red;"></div>
            </div>
        </div>


        <div class="comment">
            <div id="commnet-list">
                <p>评论列表</p>
                {#       复制bootstrap网站的组件中的列表组样式代码         #}
                <ul class="list-group">
                    {% for comment in comment_list %}
                        <li class="list-group-item">
                            <div>
                                <a href="">#{{ forloop.counter }}楼</a>&nbsp;&nbsp;
                                <span>{{ comment.create_time|date:"Y-m-d H:i" }}</span>&nbsp;&nbsp;
                                <a href="">{{ comment.user.username }}</a>
                                <div class="pull-right"><a href="">回复</a></div>
                            </div>
                            <div id="content-show" style="margin-top: 10px">
                                <p>{{ comment.content }}</p>
                            </div>
                        </li>
                    {% endfor %}
                </ul>
            </div>

            <div id="comment_title">
                <p>发表评论:</p>
                昵称&ensp;<span class="glyphicon glyphicon-user"></span>
                <input disabled="disabled" value="{{ request.user.username }}">
            </div>
            <div>评论内容</div>
            <div><textarea id="comment-content" cols="60" rows="10"></textarea></div>
            <p>
                <button class="btn btn-default comment-btn">提交评论</button>
            </p>
        </div>

        <script>
            // 点赞
            $('.action').click(function () {
                var is_up = $(this).hasClass('diggit');  // 有diggit属性,就是True, 否则就是False
                var $count_obj = $(this).children('span');  // 获取用于显示点赞数\或者是反对数的span标签对象
                $.ajax({
                    url: '/digg/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'is_up': is_up,
                    },
                    success: function (data) {
                        if (data.state) {
                            {#  state为True表示之前未点过  #}
                            var val = parseInt($count_obj.text());  // 获取点赞(反对)的数值
                            $count_obj.text(val + 1);  // 点赞(反对)数值加1
                        } else {
                            {# 三元运算符, True时,val=冒号前的内容, False时val=冒号后的内容 #}
                            var val = data.handled ? '您已经点过赞了!' : "您已经点过反对了!";
                            $(".diggword").html(val);
                            setTimeout(function () {
                                $(".diggword").html('')
                            }, 1000)
                        }
                    }
                })
            })


            // 评论
            $('.comment-btn').click(function () {
                var pid = ''; // 父评论id, pid为空则表示当前为父评论.
                var content = $('#comment-content').val();  // 获取评论内容
                $.ajax({
                    url: '/comment/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'content': content,
                        'parent_comment':pid,
                    },
                    success: function (data) {
                        console.log(data);
                        $('#comment-content').val(''); // 将评论内容框清空
                    }
                })
            })


        </script>
    </div>

{% endblock %}
article_detail.html

62.3 后端从数据库获取评论数据

def article_detail(request, username, article_id):
    """
    文章详情页视图
    http://127.0.0.1:8000/papa/article/2/
    """

    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')
    # 查询username用户的blog对象
    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, 'article_detail.html', locals())  # locals()不能丢
from django.shortcuts import render, HttpResponse, redirect
from django.http import JsonResponse  # 传递json数据类型
from django.contrib import auth  # 导入用户组件,用于auth_user表的操作
from django.db.models import Avg, Max, Min, Count
from django.db.models.functions import TruncMonth
from blog import models

from blog.models import UserInfo
from blog.myforms import RegForm  # 导入自定义forms验证规则


def reg(request):
    """用户注册视图"""
    if request.is_ajax():
        form = RegForm(request.POST)
        respone = {'user': None, 'msg': None}
        if form.is_valid():
            respone['user'] = form.cleaned_data.get('user')
            user = form.cleaned_data.get('user')
            pwd = form.cleaned_data.get('pwd')
            email = form.cleaned_data.get('email')
            avatar_obj = request.FILES.get('avatar')
            extra = {}
            if avatar_obj:
                extra['avatar'] = avatar_obj
            user_obj = UserInfo.objects.create_user(username=user, password=pwd, email=email, **extra)
        else:
            respone['msg'] = form.errors
        return JsonResponse(respone)

    form = RegForm()  # 带入规则
    return render(request, 'reg.html', {"form": form})


def login(request):
    if request.method == "POST":
        response = {'user': None, 'msg': None}  # 构造登录提示信息
        user = request.POST.get('user')
        pwd = request.POST.get('pwd')
        valid_code = request.POST.get('valid_code')  # 获取用户输入的验证码
        valid_code_str = request.session.get('valid_code')  # 从session中提取自动生成的验证码字符串
        if valid_code_str.upper() == valid_code.upper():  # 全部转为大写字母
            user = auth.authenticate(username=user, password=pwd)
            if user:
                auth.login(request, user)  # yanz 成功在session中注册当前用户信息, 就有了request.user全局变量
                response['user'] = user.username  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.
            else:
                response['msg'] = '用户名或密码错误!'
        else:
            response['msg'] = '验证码错误!'
        return JsonResponse(response)  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.

    return render(request, 'login.html')


def get_validCode_img(request):
    """生成验证码图片"""
    from blog.utils.validCode import get_validCode_img  # 导入自定义的生成验证码功能函数
    data = get_validCode_img(request)
    return HttpResponse(data)


def index(request):
    article_list = models.Article.objects.all()
    return render(request, 'index.html', {'article_list': article_list})


def logout(request):
    auth.logout(request)
    return redirect('/login/')


def home_site(request, username, **kwargs):
    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')

    # 查询username用户的blog对象
    blog = user.blog
    article_list = models.Article.objects.filter(user=user)
    if kwargs:
        # 个人站点跳转
        condition = kwargs.get('condition')  # 使用**kwargs目的是可以接受任意多个参数
        param = kwargs.get('param')
        if condition == 'tag':
            article_list = article_list.filter(tags__title=param)
            # print(article_list)
        elif condition == 'category':
            article_list = article_list.filter(category__title=param)
        else:
            # 日期归档
            year, month = param.split('-')
            article_list = article_list.filter(create_time__year=year, create_time__month=month)

    return render(request, 'home_site.html', {'username': username, 'blog': blog, 'article_list': article_list})


def article_detail(request, username, article_id):
    """
    文章详情页视图
    http://127.0.0.1:8000/papa/article/2/
    """

    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')
    # 查询username用户的blog对象
    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, 'article_detail.html', locals())  # locals()不能丢


def digg(request):
    print(request.user)
    import json
    article_id = request.POST.get('article_id')
    is_up = json.loads(request.POST.get('is_up'))  # 传递过来的是"True"字符串, 所以这里需要反序列化为布尔值
    user_id = request.user.pk  # 直接获取user_id,没有必要通过客户端传送

    # 判断是否已经点过赞
    response = {'state': True}  # 返回的状态,将传递给模板中的回调函数,默认是True成功, 表示为点过赞或者反对.
    ret_obj = models.ArticleUpDown.objects.filter(user_id=user_id, article_id=article_id).first()
    if not ret_obj:  # 没点过赞
        ard = models.ArticleUpDown.objects.create(user_id=user_id, article_id=article_id, is_up=is_up)
        # 将点赞写入数据库中
        from django.db.models import F
        article_obj = models.Article.objects.filter(pk=article_id)
        if is_up:
            article_obj.update(up_count=F("up_count")+1)
        else:
            article_obj.update(down_count=F("down_count")+1)
    else:  # 已经点过赞或反对了
        response['state'] = False  # 表示失败
        response['handled'] = ret_obj.is_up  # 获取当前数据库中是点赞过还是点反对过.

    return JsonResponse(response)  # 将状态传递给模板中的回调函数


def comment(request):
    """
    发布评论
    :param request:
    :return:
    """
    print(request.POST)
    # 前端ajax发送数据, 这里予以获取
    article_id = request.POST.get('article_id')
    content = request.POST.get('content')
    pid = request.POST.get('pid')

    # 从当前用户登录信息中获取
    user_id = request.user.pk
    print("user_id:", user_id)

    # 写入到数据库comment表
    comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content, parent_comment_id=pid)

    return HttpResponse('comment')
views.py

 六三. 博客系统之Ajax显示根评论  从数据库提取刚刚存入的数据,再渲染在模板上

63.1 最终效果

 

 

 

63.2 视图函数提取数据

def comment(request):
    """
    发布评论
    :param request:
    :return:
    """
    print(request.POST)
    # 前端ajax发送数据, 这里予以获取
    article_id = request.POST.get('article_id')
    content = request.POST.get('content')
    pid = request.POST.get('pid')

    # 从当前用户登录信息中获取
    user_id = request.user.pk
    print("user_id:", user_id)

    # 写入到数据库comment表
    comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content,
                                                parent_comment_id=pid)

    # 从数据库提取刚刚存入的数据, 为啥非要从数据库中立即再次取出, 不能直接在前端提交时,在前端直接取数据吗?
    create_time = comment_obj.create_time.strftime('%Y-%m-%d %X')
    content = comment_obj.content
    username = request.user.username
    response = {}
    response["create_time"] = create_time
    response['content'] = content
    response['username'] = username

    return JsonResponse(response)
from django.shortcuts import render, HttpResponse, redirect
from django.http import JsonResponse  # 传递json数据类型
from django.contrib import auth  # 导入用户组件,用于auth_user表的操作
from django.db.models import Avg, Max, Min, Count
from django.db.models.functions import TruncMonth
from blog import models

from blog.models import UserInfo
from blog.myforms import RegForm  # 导入自定义forms验证规则


def reg(request):
    """用户注册视图"""
    if request.is_ajax():
        form = RegForm(request.POST)
        respone = {'user': None, 'msg': None}
        if form.is_valid():
            respone['user'] = form.cleaned_data.get('user')
            user = form.cleaned_data.get('user')
            pwd = form.cleaned_data.get('pwd')
            email = form.cleaned_data.get('email')
            avatar_obj = request.FILES.get('avatar')
            extra = {}
            if avatar_obj:
                extra['avatar'] = avatar_obj
            user_obj = UserInfo.objects.create_user(username=user, password=pwd, email=email, **extra)
        else:
            respone['msg'] = form.errors
        return JsonResponse(respone)

    form = RegForm()  # 带入规则
    return render(request, 'reg.html', {"form": form})


def login(request):
    if request.method == "POST":
        response = {'user': None, 'msg': None}  # 构造登录提示信息
        user = request.POST.get('user')
        pwd = request.POST.get('pwd')
        valid_code = request.POST.get('valid_code')  # 获取用户输入的验证码
        valid_code_str = request.session.get('valid_code')  # 从session中提取自动生成的验证码字符串
        if valid_code_str.upper() == valid_code.upper():  # 全部转为大写字母
            user = auth.authenticate(username=user, password=pwd)
            if user:
                auth.login(request, user)  # yanz 成功在session中注册当前用户信息, 就有了request.user全局变量
                response['user'] = user.username  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.
            else:
                response['msg'] = '用户名或密码错误!'
        else:
            response['msg'] = '验证码错误!'
        return JsonResponse(response)  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.

    return render(request, 'login.html')


def get_validCode_img(request):
    """生成验证码图片"""
    from blog.utils.validCode import get_validCode_img  # 导入自定义的生成验证码功能函数
    data = get_validCode_img(request)
    return HttpResponse(data)


def index(request):
    article_list = models.Article.objects.all()
    return render(request, 'index.html', {'article_list': article_list})


def logout(request):
    auth.logout(request)
    return redirect('/login/')


def home_site(request, username, **kwargs):
    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')

    # 查询username用户的blog对象
    blog = user.blog
    article_list = models.Article.objects.filter(user=user)
    if kwargs:
        # 个人站点跳转
        condition = kwargs.get('condition')  # 使用**kwargs目的是可以接受任意多个参数
        param = kwargs.get('param')
        if condition == 'tag':
            article_list = article_list.filter(tags__title=param)
            # print(article_list)
        elif condition == 'category':
            article_list = article_list.filter(category__title=param)
        else:
            # 日期归档
            year, month = param.split('-')
            article_list = article_list.filter(create_time__year=year, create_time__month=month)

    return render(request, 'home_site.html', {'username': username, 'blog': blog, 'article_list': article_list})


def article_detail(request, username, article_id):
    """
    文章详情页视图
    http://127.0.0.1:8000/papa/article/2/
    """

    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')
    # 查询username用户的blog对象
    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, 'article_detail.html', locals())  # locals()不能丢


def digg(request):
    print(request.user)
    import json
    article_id = request.POST.get('article_id')
    is_up = json.loads(request.POST.get('is_up'))  # 传递过来的是"True"字符串, 所以这里需要反序列化为布尔值
    user_id = request.user.pk  # 直接获取user_id,没有必要通过客户端传送

    # 判断是否已经点过赞
    response = {'state': True}  # 返回的状态,将传递给模板中的回调函数,默认是True成功, 表示为点过赞或者反对.
    ret_obj = models.ArticleUpDown.objects.filter(user_id=user_id, article_id=article_id).first()
    if not ret_obj:  # 没点过赞
        ard = models.ArticleUpDown.objects.create(user_id=user_id, article_id=article_id, is_up=is_up)
        # 将点赞写入数据库中
        from django.db.models import F
        article_obj = models.Article.objects.filter(pk=article_id)
        if is_up:
            article_obj.update(up_count=F("up_count") + 1)
        else:
            article_obj.update(down_count=F("down_count") + 1)
    else:  # 已经点过赞或反对了
        response['state'] = False  # 表示失败
        response['handled'] = ret_obj.is_up  # 获取当前数据库中是点赞过还是点反对过.

    return JsonResponse(response)  # 将状态传递给模板中的回调函数


def comment(request):
    """
    发布评论
    :param request:
    :return:
    """
    print(request.POST)
    # 前端ajax发送数据, 这里予以获取
    article_id = request.POST.get('article_id')
    content = request.POST.get('content')
    pid = request.POST.get('pid')

    # 从当前用户登录信息中获取
    user_id = request.user.pk
    print("user_id:", user_id)

    # 写入到数据库comment表
    comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content,
                                                parent_comment_id=pid)

    # 从数据库提取刚刚存入的数据, 为啥非要从数据库中立即再次取出, 不能直接在前端提交时,在前端直接取数据吗?
    create_time = comment_obj.create_time.strftime('%Y-%m-%d %X')
    content = comment_obj.content
    username = request.user.username
    response = {}
    response["create_time"] = create_time
    response['content'] = content
    response['username'] = username

    return JsonResponse(response)
views.py

63.3 前端渲染 

            // 评论
            $('.comment-btn').click(function () {
                var pid = ''; // 父评论id, pid为空则表示当前为父评论.
                var content = $('#comment-content').val();  // 获取评论内容
                $.ajax({
                    url: '/comment/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'content': content,
                        'parent_comment':pid,
                    },
                    success: function (data) {
                        // 展示刚刚提交的评论, 注意这里是从后台数据库中获取数据.
                        var create_time = data.create_time;
                        var content = data.content;
                        var username = data.username;
                        // 将变量嵌入到字符串中, es6语法使用反引号将字符串包住, 使用${}嵌入变量. 如果不用es6语法则需要用+拼接字符串.
                        var s = `
                            <li class="list-group-item">
                                <div>
                                    <span>${create_time}</span>&nbsp;&nbsp;
                                    <a href="">${username}</a>
                                </div>
                                <div id="content-show" style="margin-top: 10px">
                                    <p>${content}</p>
                                </div>
                            </li>
                            `;
                        // 将新构建的评论加入到评论列表后面
                        $('ul.commnet-list').append(s);
                        $('#comment-content').val(''); // 将评论内容框清空
                    }
                })
            })
{# 继承 #}
{% extends 'base.html' %}

{% block content %}
    <div class="article_info">
        {% csrf_token %}
        <h3 class="text-center">{{ article_obj.title }}</h3>
        <div class="cont">
            {{ article_obj.content|safe }}
        </div>
        <div class="clearfix">
            <div id="div_digg">
                {#    diggit action   推荐       #}
                <div class="diggit action">
                    <span class="diggnum" id="digg_count">{{ article_obj.up_count }}</span>
                </div>
                {#    diggit action   反对       #}
                <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" style="color: red;"></div>
            </div>
        </div>


        <div class="comment">
            <div id="">
                <p>评论列表</p>
                {#       复制bootstrap网站的组件中的列表组样式代码         #}
                <ul class="list-group commnet-list">
                    {% for comment in comment_list %}
                        <li class="list-group-item">
                            <div>
                                <a href="">#{{ forloop.counter }}楼</a>&nbsp;&nbsp;
                                <span>{{ comment.create_time|date:"Y-m-d H:i" }}</span>&nbsp;&nbsp;
                                <a href="">{{ comment.user.username }}</a>
                                <div class="pull-right"><a href="">回复</a></div>
                            </div>
                            <div id="content-show" style="margin-top: 10px">
                                <p>{{ comment.content }}</p>
                            </div>
                        </li>
                    {% endfor %}
                </ul>
            </div>

            <div id="comment_title">
                <p>发表评论:</p>
                昵称&ensp;<span class="glyphicon glyphicon-user"></span>
                <input disabled="disabled" value="{{ request.user.username }}">
            </div>
            <div>评论内容</div>
            <div><textarea id="comment-content" cols="60" rows="10"></textarea></div>
            <p>
                <button class="btn btn-default comment-btn">提交评论</button>
            </p>
        </div>

        <script>
            // 点赞
            $('.action').click(function () {
                var is_up = $(this).hasClass('diggit');  // 有diggit属性,就是True, 否则就是False
                var $count_obj = $(this).children('span');  // 获取用于显示点赞数\或者是反对数的span标签对象
                $.ajax({
                    url: '/digg/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'is_up': is_up,
                    },
                    success: function (data) {
                        if (data.state) {
                            {#  state为True表示之前未点过  #}
                            var val = parseInt($count_obj.text());  // 获取点赞(反对)的数值
                            $count_obj.text(val + 1);  // 点赞(反对)数值加1
                        } else {
                            {# 三元运算符, True时,val=冒号前的内容, False时val=冒号后的内容 #}
                            var val = data.handled ? '您已经点过赞了!' : "您已经点过反对了!";
                            $(".diggword").html(val);
                            setTimeout(function () {
                                $(".diggword").html('')
                            }, 1000)
                        }
                    }
                })
            })


            // 评论
            $('.comment-btn').click(function () {
                var pid = ''; // 父评论id, pid为空则表示当前为父评论.
                var content = $('#comment-content').val();  // 获取评论内容
                $.ajax({
                    url: '/comment/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'content': content,
                        'parent_comment':pid,
                    },
                    success: function (data) {
                        // 展示刚刚提交的评论, 注意这里是从后台数据库中获取数据.
                        var create_time = data.create_time;
                        var content = data.content;
                        var username = data.username;
                        // 将变量嵌入到字符串中, es6语法使用反引号将字符串包住, 使用${}嵌入变量. 如果不用es6语法则需要用+拼接字符串.
                        var s = `
                            <li class="list-group-item">
                                <div>
                                    <span>${create_time}</span>&nbsp;&nbsp;
                                    <a href="">${username}</a>
                                </div>
                                <div id="content-show" style="margin-top: 10px">
                                    <p>${content}</p>
                                </div>
                            </li>
                            `;
                        // 将新构建的评论加入到评论列表后面
                        $('ul.commnet-list').append(s);
                        $('#comment-content').val(''); // 将评论内容框清空
                    }
                })
            })


        </script>
    </div>

{% endblock %}
article_detail.html

 六四. 博客系统之回复按钮事件  子评论事件

64.1 做的工作

  1. 点击回复按钮, 光标转到发布评论的文本框中; 2. 增加@发文用户,另起一行

64.2 最终效果

 

 

64.3 关键代码article_detail.html

                <p>评论列表</p>
                {#       复制bootstrap网站的组件中的列表组样式代码         #}
                <ul class="list-group commnet-list">
                    {% for comment in comment_list %}
                        <li class="list-group-item">
                            <div>
                                <a href="">#{{ forloop.counter }}楼</a>&nbsp;&nbsp;
                                <span>{{ comment.create_time|date:"Y-m-d H:i" }}</span>&nbsp;&nbsp;
                                <a href="">{{ comment.user.username }}</a>
                                {#  为了方便回复,这里增加了自定义属性username,让它等于发文用户名   #}
                                <div class="pull-right"><a username="{{ comment.user.username }}" class="reply-btn">回复</a></div>
                            </div>
                            <div id="content-show" style="margin-top: 10px">
                                <p>{{ comment.content }}</p>
                            </div>
                        </li>
                    {% endfor %}
                </ul>
            // 回复评论
            $('.reply-btn').click(function () {
                // 点击回复,光标定位到下面的输入框中, 获取焦点
                $('#comment-content').focus();
                // 拼接加在回复框前的@用户,\n是换行.
                var val = '@' + $(this).attr('username') + '\n';
                $('#comment-content').val(val);
            })
{# 继承 #}
{% extends 'base.html' %}

{% block content %}
    <div class="article_info">
        {% csrf_token %}
        <h3 class="text-center">{{ article_obj.title }}</h3>
        <div class="cont">
            {{ article_obj.content|safe }}
        </div>
        <div class="clearfix">
            <div id="div_digg">
                {#    diggit action   推荐       #}
                <div class="diggit action">
                    <span class="diggnum" id="digg_count">{{ article_obj.up_count }}</span>
                </div>
                {#    diggit action   反对       #}
                <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" style="color: red;"></div>
            </div>
        </div>


        <div class="comment">
            <div id="">
                <p>评论列表</p>
                {#       复制bootstrap网站的组件中的列表组样式代码         #}
                <ul class="list-group commnet-list">
                    {% for comment in comment_list %}
                        <li class="list-group-item">
                            <div>
                                <a href="">#{{ forloop.counter }}楼</a>&nbsp;&nbsp;
                                <span>{{ comment.create_time|date:"Y-m-d H:i" }}</span>&nbsp;&nbsp;
                                <a href="">{{ comment.user.username }}</a>
                                {#  为了方便回复,这里增加了自定义属性username,让它等于发文用户名   #}
                                <div class="pull-right"><a username="{{ comment.user.username }}" class="reply-btn">回复</a></div>
                            </div>
                            <div id="content-show" style="margin-top: 10px">
                                <p>{{ comment.content }}</p>
                            </div>
                        </li>
                    {% endfor %}
                </ul>
            </div>

            <div id="comment_title">
                <p>发表评论:</p>
                昵称&ensp;<span class="glyphicon glyphicon-user"></span>
                <input disabled="disabled" value="{{ request.user.username }}">
            </div>
            <div>评论内容</div>
            <div><textarea class="form-control" id="comment-content" rows="10"></textarea></div>
            <p>
                <button class="btn btn-default comment-btn">提交评论</button>
            </p>
        </div>

        <script>
            // 点赞
            $('.action').click(function () {
                var is_up = $(this).hasClass('diggit');  // 有diggit属性,就是True, 否则就是False
                var $count_obj = $(this).children('span');  // 获取用于显示点赞数\或者是反对数的span标签对象
                $.ajax({
                    url: '/digg/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'is_up': is_up,
                    },
                    success: function (data) {
                        if (data.state) {
                            {#  state为True表示之前未点过  #}
                            var val = parseInt($count_obj.text());  // 获取点赞(反对)的数值
                            $count_obj.text(val + 1);  // 点赞(反对)数值加1
                        } else {
                            {# 三元运算符, True时,val=冒号前的内容, False时val=冒号后的内容 #}
                            var val = data.handled ? '您已经点过赞了!' : "您已经点过反对了!";
                            $(".diggword").html(val);
                            setTimeout(function () {
                                $(".diggword").html('')
                            }, 1000)
                        }
                    }
                })
            });


            // 评论
            $('.comment-btn').click(function () {
                var pid = ''; // 父评论id, pid为空则表示当前为父评论.
                var content = $('#comment-content').val();  // 获取评论内容
                $.ajax({
                    url: '/comment/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'content': content,
                        'parent_comment':pid,
                    },
                    success: function (data) {
                        // 展示刚刚提交的评论, 注意这里是从后台数据库中获取数据.
                        var create_time = data.create_time;
                        var content = data.content;
                        var username = data.username;
                        // 将变量嵌入到字符串中, es6语法使用反引号将字符串包住, 使用${}嵌入变量. 如果不用es6语法则需要用+拼接字符串.
                        var s = `
                            <li class="list-group-item">
                                <div>
                                    <span>${create_time}</span>&nbsp;&nbsp;
                                    <a href="">${username}</a>
                                </div>
                                <div id="content-show" style="margin-top: 10px">
                                    <p>${content}</p>
                                </div>
                            </li>
                            `;
                        // 将新构建的评论加入到评论列表后面
                        $('ul.commnet-list').append(s);
                        $('#comment-content').val(''); // 将评论内容框清空
                    }
                })
            });


            // 回复评论
            $('.reply-btn').click(function () {
                // 点击回复,光标定位到下面的输入框中, 获取焦点
                $('#comment-content').focus();
                // 拼接加在回复框前的@用户,\n是换行.
                var val = '@' + $(this).attr('username') + '\n';
                $('#comment-content').val(val);
            })


        </script>
    </div>

{% endblock %}
article_detail.html

 六五. 博客系统值提交子评论

65.1 实现功能

  1. 提交数据到数据库,但不能有@用户名,这个需要截断.  2. 注意这里的pid不能再是空了.回复谁,就要获取谁的pid.把pid变成全局变量.设定判断是否有值.

65.2 关键代码

            // 评论
            var pid = ''; // 父评论id, pid为空则表示当前为父评论, 默认为空.
            $('.comment-btn').click(function () {
                // 截断评框中@用户名
                var content = $('#comment-content').val();  // 获取评论内容
                // pid有值说明是子评论, 需要切断换行符之前的内容, 取其后的内容
                if (pid){
                    var index = content.indexOf('\n');
                    content = content.slice(index+1);
                };
                $.ajax({
                    url: '/comment/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'content': content,
                        'parent_comment':pid,
                    },
                    success: function (data) {
                        // 展示刚刚提交的评论, 注意这里是从后台数据库中获取数据.
                        var create_time = data.create_time;
                        var content = data.content;
                        var username = data.username;
                        // 将变量嵌入到字符串中, es6语法使用反引号将字符串包住, 使用${}嵌入变量. 如果不用es6语法则需要用+拼接字符串.
                        var s = `
                            <li class="list-group-item">
                                <div>
                                    <span>${create_time}</span>&nbsp;&nbsp;
                                    <a href="">${username}</a>
                                </div>
                                <div id="content-show" style="margin-top: 10px">
                                    <p>${content}</p>
                                </div>
                            </li>
                            `;
                        // 将新构建的评论加入到评论列表后面
                        $('ul.commnet-list').append(s);
                        $('#comment-content').val(''); // 将评论内容框清空
                        // 将pid设为空
                        pid = '';
                    }
                })
            });


            // 回复评论
            $('.reply-btn').click(function () {
                // 点击回复,光标定位到下面的输入框中, 获取焦点
                $('#comment-content').focus();
                // 拼接加在回复框前的@用户,\n是换行.
                var val = '@' + $(this).attr('username') + '\n';
                $('#comment-content').val(val);
                // 获取回复的pid值, 'comment_pk'是该标签的自定义属性
                pid = $(this).attr('comment_pk');
            })
{# 继承 #}
{% extends 'base.html' %}

{% block content %}
    <div class="article_info">
        {% csrf_token %}
        <h3 class="text-center">{{ article_obj.title }}</h3>
        <div class="cont">
            {{ article_obj.content|safe }}
        </div>
        <div class="clearfix">
            <div id="div_digg">
                {#    diggit action   推荐       #}
                <div class="diggit action">
                    <span class="diggnum" id="digg_count">{{ article_obj.up_count }}</span>
                </div>
                {#    diggit action   反对       #}
                <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" style="color: red;"></div>
            </div>
        </div>


        <div class="comment">
            <div id="">
                <p>评论列表</p>
                {#       复制bootstrap网站的组件中的列表组样式代码         #}
                <ul class="list-group commnet-list">
                    {% for comment in comment_list %}
                        <li class="list-group-item">
                            <div>
                                <a href="">#{{ forloop.counter }}楼</a>&nbsp;&nbsp;
                                <span>{{ comment.create_time|date:"Y-m-d H:i" }}</span>&nbsp;&nbsp;
                                <a href="">{{ comment.user.username }}</a>
                                {#  新增自定义comment_pk属性目的是方便获取回复的pid值   #}
                                <div class="pull-right"><a username="{{ comment.user.username }}" comment_pk = "{{ comment.pk }}" class="reply-btn">回复</a></div>
                            </div>
                            <div id="content-show" style="margin-top: 10px">
                                <p>{{ comment.content }}</p>
                            </div>
                        </li>
                    {% endfor %}
                </ul>
            </div>

            <div id="comment_title">
                <p>发表评论:</p>
                昵称&ensp;<span class="glyphicon glyphicon-user"></span>
                <input disabled="disabled" value="{{ request.user.username }}">
            </div>
            <div>评论内容</div>
            <div><textarea class="form-control" id="comment-content" rows="10"></textarea></div>
            <p>
                <button class="btn btn-default comment-btn">提交评论</button>
            </p>
        </div>

        <script>
            // 点赞
            $('.action').click(function () {
                var is_up = $(this).hasClass('diggit');  // 有diggit属性,就是True, 否则就是False
                var $count_obj = $(this).children('span');  // 获取用于显示点赞数\或者是反对数的span标签对象
                $.ajax({
                    url: '/digg/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'is_up': is_up,
                    },
                    success: function (data) {
                        if (data.state) {
                            {#  state为True表示之前未点过  #}
                            var val = parseInt($count_obj.text());  // 获取点赞(反对)的数值
                            $count_obj.text(val + 1);  // 点赞(反对)数值加1
                        } else {
                            {# 三元运算符, True时,val=冒号前的内容, False时val=冒号后的内容 #}
                            var val = data.handled ? '您已经点过赞了!' : "您已经点过反对了!";
                            $(".diggword").html(val);
                            setTimeout(function () {
                                $(".diggword").html('')
                            }, 1000)
                        }
                    }
                })
            });


            // 评论
            var pid = ''; // 父评论id, pid为空则表示当前为父评论, 默认为空.
            $('.comment-btn').click(function () {
                // 截断评框中@用户名
                var content = $('#comment-content').val();  // 获取评论内容
                // pid有值说明是子评论, 需要切断换行符之前的内容, 取其后的内容
                if (pid){
                    var index = content.indexOf('\n');
                    content = content.slice(index+1);
                };
                console.log(pid);
                $.ajax({
                    url: '/comment/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'content': content,
                        'parent_comment':pid,
                    },
                    success: function (data) {
                        // 展示刚刚提交的评论, 注意这里是从后台数据库中获取数据.
                        var create_time = data.create_time;
                        var content = data.content;
                        var username = data.username;
                        // 将变量嵌入到字符串中, es6语法使用反引号将字符串包住, 使用${}嵌入变量. 如果不用es6语法则需要用+拼接字符串.
                        var s = `
                            <li class="list-group-item">
                                <div>
                                    <span>${create_time}</span>&nbsp;&nbsp;
                                    <a href="">${username}</a>
                                </div>
                                <div id="content-show" style="margin-top: 10px">
                                    <p>${content}</p>
                                </div>
                            </li>
                            `;
                        // 将新构建的评论加入到评论列表后面
                        $('ul.commnet-list').append(s);
                        $('#comment-content').val(''); // 将评论内容框清空
                        // 将pid设为空
                        pid = '';
                    }
                })
            });


            // 回复评论
            $('.reply-btn').click(function () {
                // 点击回复,光标定位到下面的输入框中, 获取焦点
                $('#comment-content').focus();
                // 拼接加在回复框前的@用户,\n是换行.
                var val = '@' + $(this).attr('username') + '\n';
                $('#comment-content').val(val);
                // 获取回复的pid值, 'comment_pk'是该标签的自定义属性
                pid = $(this).attr('comment_pk');

            })


        </script>
    </div>

{% endblock %}
article_detail.html
from django.shortcuts import render, HttpResponse, redirect
from django.http import JsonResponse  # 传递json数据类型
from django.contrib import auth  # 导入用户组件,用于auth_user表的操作
from django.db.models import Avg, Max, Min, Count
from django.db.models.functions import TruncMonth
from blog import models

from blog.models import UserInfo
from blog.myforms import RegForm  # 导入自定义forms验证规则


def reg(request):
    """用户注册视图"""
    if request.is_ajax():
        form = RegForm(request.POST)
        respone = {'user': None, 'msg': None}
        if form.is_valid():
            respone['user'] = form.cleaned_data.get('user')
            user = form.cleaned_data.get('user')
            pwd = form.cleaned_data.get('pwd')
            email = form.cleaned_data.get('email')
            avatar_obj = request.FILES.get('avatar')
            extra = {}
            if avatar_obj:
                extra['avatar'] = avatar_obj
            user_obj = UserInfo.objects.create_user(username=user, password=pwd, email=email, **extra)
        else:
            respone['msg'] = form.errors
        return JsonResponse(respone)

    form = RegForm()  # 带入规则
    return render(request, 'reg.html', {"form": form})


def login(request):
    if request.method == "POST":
        response = {'user': None, 'msg': None}  # 构造登录提示信息
        user = request.POST.get('user')
        pwd = request.POST.get('pwd')
        valid_code = request.POST.get('valid_code')  # 获取用户输入的验证码
        valid_code_str = request.session.get('valid_code')  # 从session中提取自动生成的验证码字符串
        if valid_code_str.upper() == valid_code.upper():  # 全部转为大写字母
            user = auth.authenticate(username=user, password=pwd)
            if user:
                auth.login(request, user)  # yanz 成功在session中注册当前用户信息, 就有了request.user全局变量
                response['user'] = user.username  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.
            else:
                response['msg'] = '用户名或密码错误!'
        else:
            response['msg'] = '验证码错误!'
        return JsonResponse(response)  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.

    return render(request, 'login.html')


def get_validCode_img(request):
    """生成验证码图片"""
    from blog.utils.validCode import get_validCode_img  # 导入自定义的生成验证码功能函数
    data = get_validCode_img(request)
    return HttpResponse(data)


def index(request):
    article_list = models.Article.objects.all()
    return render(request, 'index.html', {'article_list': article_list})


def logout(request):
    auth.logout(request)
    return redirect('/login/')


def home_site(request, username, **kwargs):
    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')

    # 查询username用户的blog对象
    blog = user.blog
    article_list = models.Article.objects.filter(user=user)
    if kwargs:
        # 个人站点跳转
        condition = kwargs.get('condition')  # 使用**kwargs目的是可以接受任意多个参数
        param = kwargs.get('param')
        if condition == 'tag':
            article_list = article_list.filter(tags__title=param)
            # print(article_list)
        elif condition == 'category':
            article_list = article_list.filter(category__title=param)
        else:
            # 日期归档
            year, month = param.split('-')
            article_list = article_list.filter(create_time__year=year, create_time__month=month)

    return render(request, 'home_site.html', {'username': username, 'blog': blog, 'article_list': article_list})


def article_detail(request, username, article_id):
    """
    文章详情页视图
    http://127.0.0.1:8000/papa/article/2/
    """

    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')
    # 查询username用户的blog对象
    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, 'article_detail.html', locals())  # locals()不能丢


def digg(request):
    print(request.user)
    import json
    article_id = request.POST.get('article_id')
    is_up = json.loads(request.POST.get('is_up'))  # 传递过来的是"True"字符串, 所以这里需要反序列化为布尔值
    user_id = request.user.pk  # 直接获取user_id,没有必要通过客户端传送

    # 判断是否已经点过赞
    response = {'state': True}  # 返回的状态,将传递给模板中的回调函数,默认是True成功, 表示为点过赞或者反对.
    ret_obj = models.ArticleUpDown.objects.filter(user_id=user_id, article_id=article_id).first()
    if not ret_obj:  # 没点过赞
        ard = models.ArticleUpDown.objects.create(user_id=user_id, article_id=article_id, is_up=is_up)
        # 将点赞写入数据库中
        from django.db.models import F
        article_obj = models.Article.objects.filter(pk=article_id)
        if is_up:
            article_obj.update(up_count=F("up_count") + 1)
        else:
            article_obj.update(down_count=F("down_count") + 1)
    else:  # 已经点过赞或反对了
        response['state'] = False  # 表示失败
        response['handled'] = ret_obj.is_up  # 获取当前数据库中是点赞过还是点反对过.

    return JsonResponse(response)  # 将状态传递给模板中的回调函数


def comment(request):
    """
    发布评论
    :param request:
    :return:
    """
    # 前端ajax发送数据, 这里予以获取
    article_id = request.POST.get('article_id')
    content = request.POST.get('content')
    pid = request.POST.get('parent_comment')

    # 从当前用户登录信息中获取
    user_id = request.user.pk
    print("pid:", pid)

    # 写入到数据库comment表
    comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content,
                                                parent_comment_id=pid)

    # 从数据库提取刚刚存入的数据, 为啥非要从数据库中立即再次取出, 不能直接在前端提交时,在前端直接取数据吗?
    create_time = comment_obj.create_time.strftime('%Y-%m-%d %X')
    content = comment_obj.content
    username = request.user.username
    response = {}
    response["create_time"] = create_time
    response['content'] = content
    response['username'] = username

    return JsonResponse(response)
views.py

 六六. 博客系统之render显示子评论

66.1 最终效果

 

 

66.2 步骤  1. 先获取子评论的父评论对象  2. 渲染出该父对象

66.3 关键代码

                <ul class="list-group commnet-list">
                    {% for comment in comment_list %}
                        <li class="list-group-item">
                            <div>
                                <a href="">#{{ forloop.counter }}楼</a>&nbsp;&nbsp;
                                <span>{{ comment.create_time|date:"Y-m-d H:i" }}</span>&nbsp;&nbsp;
                                <a href="">{{ comment.user.username }}</a>
                                {#  新增自定义comment_pk属性目的是方便获取回复的pid值   #}
                                <div class="pull-right"><a username="{{ comment.user.username }}" comment_pk = "{{ comment.pk }}" class="reply-btn">回复</a></div>
                            </div>
                            <div id="content-show" style="margin-top: 10px">
                                {#  如果该评论是子评论,则需要将其父评论也显示出来  well嵌入效果#}
                                {% if comment.parent_comment_id %}
                                    <div class="well">{{ comment.parent_comment.user.username }}:&nbsp;{{ comment.parent_comment.content }}</div>
                                {% endif %}
                                <p>{{ comment.content }}</p>
                            </div>
                        </li>
                    {% endfor %}
                </ul>
{# 继承 #}
{% extends 'base.html' %}

{% block content %}
    <div class="article_info">
        {% csrf_token %}
        <h3 class="text-center">{{ article_obj.title }}</h3>
        <div class="cont">
            {{ article_obj.content|safe }}
        </div>
        <div class="clearfix">
            <div id="div_digg">
                {#    diggit action   推荐       #}
                <div class="diggit action">
                    <span class="diggnum" id="digg_count">{{ article_obj.up_count }}</span>
                </div>
                {#    diggit action   反对       #}
                <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" style="color: red;"></div>
            </div>
        </div>


        <div class="comment">
            <div id="">
                <p>评论列表</p>
                {#       复制bootstrap网站的组件中的列表组样式代码         #}
                <ul class="list-group commnet-list">
                    {% for comment in comment_list %}
                        <li class="list-group-item">
                            <div>
                                <a href="">#{{ forloop.counter }}楼</a>&nbsp;&nbsp;
                                <span>{{ comment.create_time|date:"Y-m-d H:i" }}</span>&nbsp;&nbsp;
                                <a href="">{{ comment.user.username }}</a>
                                {#  新增自定义comment_pk属性目的是方便获取回复的pid值   #}
                                <div class="pull-right"><a username="{{ comment.user.username }}" comment_pk = "{{ comment.pk }}" class="reply-btn">回复</a></div>
                            </div>
                            <div id="content-show" style="margin-top: 10px">
                                {#  如果该评论是子评论,则需要将其父评论也显示出来  #}
                                {% if comment.parent_comment_id %}
                                    <div class="well">{{ comment.parent_comment.user.username }}:&nbsp;{{ comment.parent_comment.content }}</div>
                                {% endif %}
                                <p>{{ comment.content }}</p>
                            </div>
                        </li>
                    {% endfor %}
                </ul>
            </div>

            <div id="comment_title">
                <p>发表评论:</p>
                昵称&ensp;<span class="glyphicon glyphicon-user"></span>
                <input disabled="disabled" value="{{ request.user.username }}">
            </div>
            <div>评论内容</div>
            <div><textarea class="form-control" id="comment-content" rows="10"></textarea></div>
            <p>
                <button class="btn btn-default comment-btn">提交评论</button>
            </p>
        </div>

        <script>
            // 点赞
            $('.action').click(function () {
                var is_up = $(this).hasClass('diggit');  // 有diggit属性,就是True, 否则就是False
                var $count_obj = $(this).children('span');  // 获取用于显示点赞数\或者是反对数的span标签对象
                $.ajax({
                    url: '/digg/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'is_up': is_up,
                    },
                    success: function (data) {
                        if (data.state) {
                            {#  state为True表示之前未点过  #}
                            var val = parseInt($count_obj.text());  // 获取点赞(反对)的数值
                            $count_obj.text(val + 1);  // 点赞(反对)数值加1
                        } else {
                            {# 三元运算符, True时,val=冒号前的内容, False时val=冒号后的内容 #}
                            var val = data.handled ? '您已经点过赞了!' : "您已经点过反对了!";
                            $(".diggword").html(val);
                            setTimeout(function () {
                                $(".diggword").html('')
                            }, 1000)
                        }
                    }
                })
            });


            // 评论
            var pid = ''; // 父评论id, pid为空则表示当前为父评论, 默认为空.
            $('.comment-btn').click(function () {
                // 截断评框中@用户名
                var content = $('#comment-content').val();  // 获取评论内容
                // pid有值说明是子评论, 需要切断换行符之前的内容, 取其后的内容
                if (pid){
                    var index = content.indexOf('\n');
                    content = content.slice(index+1);
                };
                console.log(pid);
                $.ajax({
                    url: '/comment/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'content': content,
                        'parent_comment':pid,
                    },
                    success: function (data) {
                        // 展示刚刚提交的评论, 注意这里是从后台数据库中获取数据.
                        var create_time = data.create_time;
                        var content = data.content;
                        var username = data.username;
                        // 将变量嵌入到字符串中, es6语法使用反引号将字符串包住, 使用${}嵌入变量. 如果不用es6语法则需要用+拼接字符串.
                        var s = `
                            <li class="list-group-item">
                                <div>
                                    <span>${create_time}</span>&nbsp;&nbsp;
                                    <a href="">${username}</a>
                                </div>
                                <div id="content-show" style="margin-top: 10px">
                                    <p>${content}</p>
                                </div>
                            </li>
                            `;
                        // 将新构建的评论加入到评论列表后面
                        $('ul.commnet-list').append(s);
                        $('#comment-content').val(''); // 将评论内容框清空
                        // 将pid设为空
                        pid = '';
                    }
                })
            });


            // 回复评论
            $('.reply-btn').click(function () {
                // 点击回复,光标定位到下面的输入框中, 获取焦点
                $('#comment-content').focus();
                // 拼接加在回复框前的@用户,\n是换行.
                var val = '@' + $(this).attr('username') + '\n';
                $('#comment-content').val(val);
                // 获取回复的pid值, 'comment_pk'是该标签的自定义属性
                pid = $(this).attr('comment_pk');

            })


        </script>
    </div>

{% endblock %}
article_detail.html

 六七. 博客系统之Ajax显示子评论的思路

67.1 点击评论按按钮中增加是否是子评论的判断

 

 

67.2 关键代码, 改动发表评论的js代码

            // 评论
            var pid = ''; // 父评论id, pid为空则表示当前为父评论, 默认为空.
            $('.comment-btn').click(function () {
                // 截断评框中@用户名
                var content = $('#comment-content').val();  // 获取评论内容
                // pid有值说明是子评论, 需要切断换行符之前的内容, 取其后的内容
                if (pid) {
                    var index = content.indexOf('\n');
                    content = content.slice(index + 1);
                }
                ;
                console.log(pid);
                $.ajax({
                    url: '/comment/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'content': content,
                        'parent_comment': pid,
                    },
                    success: function (data) {
                        // 展示刚刚提交的评论, 注意这里是从后台数据库中获取数据.
                        var create_time = data.create_time;
                        var content = data.content;
                        var username = data.username;
                        // 将变量嵌入到字符串中, es6语法使用反引号将字符串包住, 使用${}嵌入变量. 如果不用es6语法则需要用+拼接字符串.
                        if (pid) {
                            // 增加父评论的内容
                            var p_username = data.p_username;
                            var p_content = data.p_content;
                            var p_s = `
                                <div class="well">${p_username}:&nbsp;${p_content}</div>
                                <p>${content}</p>
                            `;
                        } else {  // 如果不是字评论,则不需加父评论信息
                            var p_s = `<p>${content}</p>`;
                        }
                        ;
                        var s = `
                            <li class="list-group-item">
                                <div>
                                    <span>${create_time}</span>&nbsp;&nbsp;
                                    <a href="">${username}</a>
                                </div>
                                <div id="content-show" style="margin-top: 10px">
                                    ${p_s}
                                </div>
                            </li>
                            `;
                        // 将新构建的评论加入到评论列表后面
                        $('ul.commnet-list').append(s);
                        $('#comment-content').val(''); // 将评论内容框清空
                        // 将pid设为空
                        pid = '';
                    }
                })
            });
{# 继承 #}
{% extends 'base.html' %}

{% block content %}
    <div class="article_info">
        {% csrf_token %}
        <h3 class="text-center">{{ article_obj.title }}</h3>
        <div class="cont">
            {{ article_obj.content|safe }}
        </div>
        <div class="clearfix">
            <div id="div_digg">
                {#    diggit action   推荐       #}
                <div class="diggit action">
                    <span class="diggnum" id="digg_count">{{ article_obj.up_count }}</span>
                </div>
                {#    diggit action   反对       #}
                <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" style="color: red;"></div>
            </div>
        </div>


        <div class="comment">
            <div id="">
                <p>评论列表</p>
                {#       复制bootstrap网站的组件中的列表组样式代码         #}
                <ul class="list-group commnet-list">
                    {% for comment in comment_list %}
                        <li class="list-group-item">
                            <div>
                                <a href="">#{{ forloop.counter }}楼</a>&nbsp;&nbsp;
                                <span>{{ comment.create_time|date:"Y-m-d H:i" }}</span>&nbsp;&nbsp;
                                <a href="">{{ comment.user.username }}</a>
                                {#  新增自定义comment_pk属性目的是方便获取回复的pid值   #}
                                <div class="pull-right"><a username="{{ comment.user.username }}"
                                                           comment_pk="{{ comment.pk }}" class="reply-btn">回复</a></div>
                            </div>
                            <div id="content-show" style="margin-top: 10px">
                                {#  如果该评论是子评论,则需要将其父评论也显示出来  #}
                                {% if comment.parent_comment_id %}
                                    <div class="well">{{ comment.parent_comment.user.username }}:&nbsp;{{ comment.parent_comment.content }}</div>
                                {% endif %}
                                <p>{{ comment.content }}</p>
                            </div>
                        </li>
                    {% endfor %}
                </ul>
            </div>

            <div id="comment_title">
                <p>发表评论:</p>
                昵称&ensp;<span class="glyphicon glyphicon-user"></span>
                <input disabled="disabled" value="{{ request.user.username }}">
            </div>
            <div>评论内容</div>
            <div><textarea class="form-control" id="comment-content" rows="10"></textarea></div>
            <p>
                <button class="btn btn-default comment-btn">提交评论</button>
            </p>
        </div>

        <script>
            // 点赞
            $('.action').click(function () {
                var is_up = $(this).hasClass('diggit');  // 有diggit属性,就是True, 否则就是False
                var $count_obj = $(this).children('span');  // 获取用于显示点赞数\或者是反对数的span标签对象
                $.ajax({
                    url: '/digg/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'is_up': is_up,
                    },
                    success: function (data) {
                        if (data.state) {
                            {#  state为True表示之前未点过  #}
                            var val = parseInt($count_obj.text());  // 获取点赞(反对)的数值
                            $count_obj.text(val + 1);  // 点赞(反对)数值加1
                        } else {
                            {# 三元运算符, True时,val=冒号前的内容, False时val=冒号后的内容 #}
                            var val = data.handled ? '您已经点过赞了!' : "您已经点过反对了!";
                            $(".diggword").html(val);
                            setTimeout(function () {
                                $(".diggword").html('')
                            }, 1000)
                        }
                    }
                })
            });


            // 评论
            var pid = ''; // 父评论id, pid为空则表示当前为父评论, 默认为空.
            $('.comment-btn').click(function () {
                // 截断评框中@用户名
                var content = $('#comment-content').val();  // 获取评论内容
                // pid有值说明是子评论, 需要切断换行符之前的内容, 取其后的内容
                if (pid) {
                    var index = content.indexOf('\n');
                    content = content.slice(index + 1);
                }
                ;
                console.log(pid);
                $.ajax({
                    url: '/comment/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'content': content,
                        'parent_comment': pid,
                    },
                    success: function (data) {
                        // 展示刚刚提交的评论, 注意这里是从后台数据库中获取数据.
                        var create_time = data.create_time;
                        var content = data.content;
                        var username = data.username;
                        // 将变量嵌入到字符串中, es6语法使用反引号将字符串包住, 使用${}嵌入变量. 如果不用es6语法则需要用+拼接字符串.
                        if (pid) {
                            // 增加父评论的内容
                            var p_username = data.p_username;
                            var p_content = data.p_content;
                            var p_s = `
                                <div class="well">${p_username}:&nbsp;${p_content}</div>
                                <p>${content}</p>
                            `;
                        } else {  // 如果不是字评论,则不需加父评论信息
                            var p_s = `<p>${content}</p>`;
                        }
                        ;
                        var s = `
                            <li class="list-group-item">
                                <div>
                                    <span>${create_time}</span>&nbsp;&nbsp;
                                    <a href="">${username}</a>
                                </div>
                                <div id="content-show" style="margin-top: 10px">
                                    ${p_s}
                                </div>
                            </li>
                            `;
                        // 将新构建的评论加入到评论列表后面
                        $('ul.commnet-list').append(s);
                        $('#comment-content').val(''); // 将评论内容框清空
                        // 将pid设为空
                        pid = '';
                    }
                })
            });


            // 回复评论
            $('.reply-btn').click(function () {
                // 点击回复,光标定位到下面的输入框中, 获取焦点
                $('#comment-content').focus();
                // 拼接加在回复框前的@用户,\n是换行.
                var val = '@' + $(this).attr('username') + '\n';
                $('#comment-content').val(val);
                // 获取回复的pid值, 'comment_pk'是该标签的自定义属性
                pid = $(this).attr('comment_pk');

            })


        </script>
    </div>

{% endblock %}
article_detail.html

 六八. 博客系统之评论树简介

68.1 树形结构

 68.2  展示树形,最大的难题是不知道有多少层级. 可以用递归来解决.

六九. 博客系统之评论树的请求数据

 69.1 效果

 

  69.2 urls.py

path('get_comment_tree/', views.get_comment_tree),  # 获取文章的树形结构评论
"""cnblog URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/3.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, re_path
from django.views.static import serve  # serve器

from blog import views
from cnblog import settings

urlpatterns = [
    path('admin/', admin.site.urls),
    path('login/', views.login),
    path('index/', views.index),
    path('logout/', views.logout),
    path('get_validCode_img/', views.get_validCode_img),
    path('reg/', views.reg),
    path('digg/', views.digg),  # 点赞
    path('comment/', views.comment),  # 评论
    path('get_comment_tree/', views.get_comment_tree),  # 获取文章的树形结构评论

    # media配置, 外网直接访问用户上传文件的media文件夹的路由
    re_path(r'media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),  # 固定写法

    # 个人站点页面
    re_path(r'^(?P<username>\w+)/$', views.home_site),

    # 个人站点页面跳转
    re_path(r'^(?P<username>\w+)/(?P<condition>tag|category|archive)/(?P<param>.*)/$', views.home_site),

    # 个人站点文章详情页面
    re_path(r'^(?P<username>\w+)/article/(?P<article_id>\d+)/$', views.article_detail),

    #

]
urls.py

69.3 模板页面article_detail.html

                {#  评论树点击可以展开此文章评论, 以树形结构显示, 这里完全动态  #}
                <p class="tree-btn">评论树</p>
                <div class="comment-tree">
                </div>
                <script>
                    // 获取评论树
                    $('.tree-btn').click(function () {
                        $.ajax({
                            url: '/get_comment_tree/',
                            type: 'post',
                            data: {
                                'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                                'article_id': {{ article_obj.pk }},
                            },
                            success: function (data) {
                                console.log(data);
                            }
                        })
                    })
                </script>
{# 继承 #}
{% extends 'base.html' %}

{% block content %}
    <div class="article_info">
        {% csrf_token %}
        <h3 class="text-center">{{ article_obj.title }}</h3>
        <div class="cont">
            {{ article_obj.content|safe }}
        </div>
        <div class="clearfix">
            <div id="div_digg">
                {#    diggit action   推荐       #}
                <div class="diggit action">
                    <span class="diggnum" id="digg_count">{{ article_obj.up_count }}</span>
                </div>
                {#    diggit action   反对       #}
                <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" style="color: red;"></div>
            </div>
        </div>


        <div class="comment">
            <div id="">
                {#  评论树点击可以展开此文章评论, 以树形结构显示, 这里完全动态  #}
                <p class="tree-btn">评论树</p>
                <div class="comment-tree">
                </div>
                <script>
                    // 获取评论树
                    $('.tree-btn').click(function () {
                        $.ajax({
                            url: '/get_comment_tree/',
                            type: 'post',
                            data: {
                                'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                                'article_id': {{ article_obj.pk }},
                            },
                            success: function (data) {
                                console.log(data);
                            }
                        })
                    })
                </script>

                <p>评论列表</p>
                {#       复制bootstrap网站的组件中的列表组样式代码         #}
                <ul class="list-group commnet-list">
                    {% for comment in comment_list %}
                        <li class="list-group-item">
                            <div>
                                <a href="">#{{ forloop.counter }}楼</a>&nbsp;&nbsp;
                                <span>{{ comment.create_time|date:"Y-m-d H:i" }}</span>&nbsp;&nbsp;
                                <a href="">{{ comment.user.username }}</a>
                                {#  新增自定义comment_pk属性目的是方便获取回复的pid值   #}
                                <div class="pull-right"><a username="{{ comment.user.username }}"
                                                           comment_pk="{{ comment.pk }}" class="reply-btn">回复</a></div>
                            </div>
                            <div id="content-show" style="margin-top: 10px">
                                {#  如果该评论是子评论,则需要将其父评论也显示出来  #}
                                {% if comment.parent_comment_id %}
                                    <div class="well">{{ comment.parent_comment.user.username }}:&nbsp;{{ comment.parent_comment.content }}</div>
                                {% endif %}
                                <p>{{ comment.content }}</p>
                            </div>
                        </li>
                    {% endfor %}
                </ul>
            </div>

            <div id="comment_title">
                <p>发表评论:</p>
                昵称&ensp;<span class="glyphicon glyphicon-user"></span>
                <input disabled="disabled" value="{{ request.user.username }}">
            </div>
            <div>评论内容</div>
            <div><textarea class="form-control" id="comment-content" rows="10"></textarea></div>
            <p>
                <button class="btn btn-default comment-btn">提交评论</button>
            </p>
        </div>

        <script>
            // 点赞
            $('.action').click(function () {
                var is_up = $(this).hasClass('diggit');  // 有diggit属性,就是True, 否则就是False
                var $count_obj = $(this).children('span');  // 获取用于显示点赞数\或者是反对数的span标签对象
                $.ajax({
                    url: '/digg/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'is_up': is_up,
                    },
                    success: function (data) {
                        if (data.state) {
                            {#  state为True表示之前未点过  #}
                            var val = parseInt($count_obj.text());  // 获取点赞(反对)的数值
                            $count_obj.text(val + 1);  // 点赞(反对)数值加1
                        } else {
                            {# 三元运算符, True时,val=冒号前的内容, False时val=冒号后的内容 #}
                            var val = data.handled ? '您已经点过赞了!' : "您已经点过反对了!";
                            $(".diggword").html(val);
                            setTimeout(function () {
                                $(".diggword").html('')
                            }, 1000)
                        }
                    }
                })
            });


            // 评论
            var pid = ''; // 父评论id, pid为空则表示当前为父评论, 默认为空.
            $('.comment-btn').click(function () {
                // 截断评框中@用户名
                var content = $('#comment-content').val();  // 获取评论内容
                // pid有值说明是子评论, 需要切断换行符之前的内容, 取其后的内容
                if (pid) {
                    var index = content.indexOf('\n');
                    content = content.slice(index + 1);
                }
                ;
                console.log(pid);
                $.ajax({
                    url: '/comment/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'content': content,
                        'parent_comment': pid,
                    },
                    success: function (data) {
                        // 展示刚刚提交的评论, 注意这里是从后台数据库中获取数据.
                        var create_time = data.create_time;
                        var content = data.content;
                        var username = data.username;
                        // 将变量嵌入到字符串中, es6语法使用反引号将字符串包住, 使用${}嵌入变量. 如果不用es6语法则需要用+拼接字符串.
                        if (pid) {
                            // 增加父评论的内容
                            var p_username = data.p_username;
                            var p_content = data.p_content;
                            var p_s = `
                                <div class="well">${p_username}:&nbsp;${p_content}</div>
                                <p>${content}</p>
                            `;
                        } else {  // 如果不是字评论,则不需加父评论信息
                            var p_s = `<p>${content}</p>`;
                        }
                        ;
                        var s = `
                            <li class="list-group-item">
                                <div>
                                    <span>${create_time}</span>&nbsp;&nbsp;
                                    <a href="">${username}</a>
                                </div>
                                <div id="content-show" style="margin-top: 10px">
                                    ${p_s}
                                </div>
                            </li>
                            `;
                        // 将新构建的评论加入到评论列表后面
                        $('ul.commnet-list').append(s);
                        $('#comment-content').val(''); // 将评论内容框清空
                        // 将pid设为空
                        pid = '';
                    }
                })
            });


            // 回复评论
            $('.reply-btn').click(function () {
                // 点击回复,光标定位到下面的输入框中, 获取焦点
                $('#comment-content').focus();
                // 拼接加在回复框前的@用户,\n是换行.
                var val = '@' + $(this).attr('username') + '\n';
                $('#comment-content').val(val);
                // 获取回复的pid值, 'comment_pk'是该标签的自定义属性
                pid = $(this).attr('comment_pk');

            })


        </script>
    </div>

{% endblock %}
article_detail.html

69.4 视图中获取数据库数据

def get_comment_tree(request):

    article_id = request.POST.get('article_id')
    # list强制将querset类型转换为list,然后再序列化.
    ret = list(models.Comment.objects.filter(article_id=article_id).values('pk', 'comment', 'parent_comment'))
    print(ret)
    return JsonResponse(ret, safe=False)  # 非字典序列化必须加上safe=False参数
from django.shortcuts import render, HttpResponse, redirect
from django.http import JsonResponse  # 传递json数据类型
from django.contrib import auth  # 导入用户组件,用于auth_user表的操作
from django.db.models import Avg, Max, Min, Count
from django.db.models.functions import TruncMonth
from blog import models

from blog.models import UserInfo
from blog.myforms import RegForm  # 导入自定义forms验证规则


def reg(request):
    """用户注册视图"""
    if request.is_ajax():
        form = RegForm(request.POST)
        respone = {'user': None, 'msg': None}
        if form.is_valid():
            respone['user'] = form.cleaned_data.get('user')
            user = form.cleaned_data.get('user')
            pwd = form.cleaned_data.get('pwd')
            email = form.cleaned_data.get('email')
            avatar_obj = request.FILES.get('avatar')
            extra = {}
            if avatar_obj:
                extra['avatar'] = avatar_obj
            user_obj = UserInfo.objects.create_user(username=user, password=pwd, email=email, **extra)
        else:
            respone['msg'] = form.errors
        return JsonResponse(respone)

    form = RegForm()  # 带入规则
    return render(request, 'reg.html', {"form": form})


def login(request):
    if request.method == "POST":
        response = {'user': None, 'msg': None}  # 构造登录提示信息
        user = request.POST.get('user')
        pwd = request.POST.get('pwd')
        valid_code = request.POST.get('valid_code')  # 获取用户输入的验证码
        valid_code_str = request.session.get('valid_code')  # 从session中提取自动生成的验证码字符串
        if valid_code_str.upper() == valid_code.upper():  # 全部转为大写字母
            user = auth.authenticate(username=user, password=pwd)
            if user:
                auth.login(request, user)  # yanz 成功在session中注册当前用户信息, 就有了request.user全局变量
                response['user'] = user.username  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.
            else:
                response['msg'] = '用户名或密码错误!'
        else:
            response['msg'] = '验证码错误!'
        return JsonResponse(response)  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.

    return render(request, 'login.html')


def get_validCode_img(request):
    """生成验证码图片"""
    from blog.utils.validCode import get_validCode_img  # 导入自定义的生成验证码功能函数
    data = get_validCode_img(request)
    return HttpResponse(data)


def index(request):
    article_list = models.Article.objects.all()
    return render(request, 'index.html', {'article_list': article_list})


def logout(request):
    auth.logout(request)
    return redirect('/login/')


def home_site(request, username, **kwargs):
    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')

    # 查询username用户的blog对象
    blog = user.blog
    article_list = models.Article.objects.filter(user=user)
    if kwargs:
        # 个人站点跳转
        condition = kwargs.get('condition')  # 使用**kwargs目的是可以接受任意多个参数
        param = kwargs.get('param')
        if condition == 'tag':
            article_list = article_list.filter(tags__title=param)
            # print(article_list)
        elif condition == 'category':
            article_list = article_list.filter(category__title=param)
        else:
            # 日期归档
            year, month = param.split('-')
            article_list = article_list.filter(create_time__year=year, create_time__month=month)

    return render(request, 'home_site.html', {'username': username, 'blog': blog, 'article_list': article_list})


def article_detail(request, username, article_id):
    """
    文章详情页视图
    http://127.0.0.1:8000/papa/article/2/
    """

    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')
    # 查询username用户的blog对象
    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, 'article_detail.html', locals())  # locals()不能丢


def digg(request):
    print(request.user)
    import json
    article_id = request.POST.get('article_id')
    is_up = json.loads(request.POST.get('is_up'))  # 传递过来的是"True"字符串, 所以这里需要反序列化为布尔值
    user_id = request.user.pk  # 直接获取user_id,没有必要通过客户端传送

    # 判断是否已经点过赞
    response = {'state': True}  # 返回的状态,将传递给模板中的回调函数,默认是True成功, 表示为点过赞或者反对.
    ret_obj = models.ArticleUpDown.objects.filter(user_id=user_id, article_id=article_id).first()
    if not ret_obj:  # 没点过赞
        ard = models.ArticleUpDown.objects.create(user_id=user_id, article_id=article_id, is_up=is_up)
        # 将点赞写入数据库中
        from django.db.models import F
        article_obj = models.Article.objects.filter(pk=article_id)
        if is_up:
            article_obj.update(up_count=F("up_count") + 1)
        else:
            article_obj.update(down_count=F("down_count") + 1)
    else:  # 已经点过赞或反对了
        response['state'] = False  # 表示失败
        response['handled'] = ret_obj.is_up  # 获取当前数据库中是点赞过还是点反对过.

    return JsonResponse(response)  # 将状态传递给模板中的回调函数


def comment(request):
    """
    发布评论
    :param request:
    :return:
    """
    # 前端ajax发送数据, 这里予以获取
    article_id = request.POST.get('article_id')
    content = request.POST.get('content')
    pid = request.POST.get('parent_comment')

    # 从当前用户登录信息中获取
    user_id = request.user.pk
    print("pid:", pid)

    # 写入到数据库comment表
    comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content,
                                                parent_comment_id=pid)

    # 从数据库提取刚刚存入的数据, 为啥非要从数据库中立即再次取出, 不能直接在前端提交时,在前端直接取数据吗?
    create_time = comment_obj.create_time.strftime('%Y-%m-%d %X')
    content = comment_obj.content
    username = request.user.username
    response = {}
    response["create_time"] = create_time
    response['content'] = content
    response['username'] = username

    # 如果是子评论,则还需要获取其父评论的内容,姓名和评论内容
    if pid:
        parent_obj = models.Comment.objects.filter(pk=pid).first()
        response['p_username'] = parent_obj.user.username
        response['p_content'] = parent_obj.content

    return JsonResponse(response)


def get_comment_tree(request):

    article_id = request.POST.get('article_id')
    # list强制将querset类型转换为list,然后再序列化.
    ret = list(models.Comment.objects.filter(article_id=article_id).values('pk', 'comment', 'parent_comment'))
    print(ret)
    return JsonResponse(ret, safe=False)  # 非字典序列化必须加上safe=False参数
View Code

 七零. 博客系统之展开评论树  显示展开根评论

70.1 效果, 单击评论树, 展示出了所有的根评论

70.2 模板代码

                {#  评论树点击可以展开此文章评论, 以树形结构显示, 这里完全动态  #}
                <p class="tree-btn">评论树</p>
                <div class="comment-tree">
                </div>
                <script>
                    // 获取评论树
                    $('.tree-btn').click(function () {
                        $.ajax({
                            url: '/get_comment_tree/',
                            type: 'post',
                            data: {
                                'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                                'article_id': {{ article_obj.pk }},
                            },
                            success: function (data) {
                                console.log(data);
                                // 传过来的data是数组, 内部套多个字典. index是数组的序号, comment_dic是数组内的每一个元素的值(这里是字典)
                                // each是循环取出数组内每一个值,相当于python的for循环
                                $.each(data, function (index, comment_dic) {
                                    var pk = comment_dic.pk;
                                    var comment = comment_dic.content;
                                    var parent_comment_id = comment_dic.parent_comment_id;
                                    // !parent_comment_id 感叹号表示非. 父id没有值,就是该评论没有父评论,它就是根评论.
                                    if(!parent_comment_id){
                                        var s = "<div comment_id="+pk+"><span>"+comment+"</span></div>";
                                        $('.comment-tree').append(s);
                                    }
                                })
                            }
                        })
                    })
                </script>
{# 继承 #}
{% extends 'base.html' %}

{% block content %}
    <div class="article_info">
        {% csrf_token %}
        <h3 class="text-center">{{ article_obj.title }}</h3>
        <div class="cont">
            {{ article_obj.content|safe }}
        </div>
        <div class="clearfix">
            <div id="div_digg">
                {#    diggit action   推荐       #}
                <div class="diggit action">
                    <span class="diggnum" id="digg_count">{{ article_obj.up_count }}</span>
                </div>
                {#    diggit action   反对       #}
                <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" style="color: red;"></div>
            </div>
        </div>


        <div class="comment">
            <div id="">
                {#  评论树点击可以展开此文章评论, 以树形结构显示, 这里完全动态  #}
                <p class="tree-btn">评论树</p>
                <div class="comment-tree">
                </div>
                <script>
                    // 获取评论树
                    $('.tree-btn').click(function () {
                        $.ajax({
                            url: '/get_comment_tree/',
                            type: 'post',
                            data: {
                                'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                                'article_id': {{ article_obj.pk }},
                            },
                            success: function (data) {
                                console.log(data);
                                // 传过来的data是数组, 内部套多个字典. index是数组的序号, comment_dic是数组内的每一个元素的值(这里是字典)
                                // each是循环取出数组内每一个值,相当于python的for循环
                                $.each(data, function (index, comment_dic) {
                                    var pk = comment_dic.pk;
                                    var comment = comment_dic.content;
                                    var parent_comment_id = comment_dic.parent_comment_id;
                                    // !parent_comment_id 感叹号表示非. 父id没有值,就是该评论没有父评论,它就是根评论.
                                    if(!parent_comment_id){
                                        var s = "<div comment_id="+pk+"><span>"+comment+"</span></div>";
                                        $('.comment-tree').append(s);
                                    }
                                })


                            }
                        })
                    })
                </script>

                <p>评论列表</p>
                {#       复制bootstrap网站的组件中的列表组样式代码         #}
                <ul class="list-group commnet-list">
                    {% for comment in comment_list %}
                        <li class="list-group-item">
                            <div>
                                <a href="">#{{ forloop.counter }}楼</a>&nbsp;&nbsp;
                                <span>{{ comment.create_time|date:"Y-m-d H:i" }}</span>&nbsp;&nbsp;
                                <a href="">{{ comment.user.username }}</a>
                                {#  新增自定义comment_pk属性目的是方便获取回复的pid值   #}
                                <div class="pull-right"><a username="{{ comment.user.username }}"
                                                           comment_pk="{{ comment.pk }}" class="reply-btn">回复</a></div>
                            </div>
                            <div id="content-show" style="margin-top: 10px">
                                {#  如果该评论是子评论,则需要将其父评论也显示出来  #}
                                {% if comment.parent_comment_id %}
                                    <div class="well">{{ comment.parent_comment.user.username }}:&nbsp;{{ comment.parent_comment.content }}</div>
                                {% endif %}
                                <p>{{ comment.content }}</p>
                            </div>
                        </li>
                    {% endfor %}
                </ul>
            </div>

            <div id="comment_title">
                <p>发表评论:</p>
                昵称&ensp;<span class="glyphicon glyphicon-user"></span>
                <input disabled="disabled" value="{{ request.user.username }}">
            </div>
            <div>评论内容</div>
            <div><textarea class="form-control" id="comment-content" rows="10"></textarea></div>
            <p>
                <button class="btn btn-default comment-btn">提交评论</button>
            </p>
        </div>

        <script>
            // 点赞
            $('.action').click(function () {
                var is_up = $(this).hasClass('diggit');  // 有diggit属性,就是True, 否则就是False
                var $count_obj = $(this).children('span');  // 获取用于显示点赞数\或者是反对数的span标签对象
                $.ajax({
                    url: '/digg/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'is_up': is_up,
                    },
                    success: function (data) {
                        if (data.state) {
                            {#  state为True表示之前未点过  #}
                            var val = parseInt($count_obj.text());  // 获取点赞(反对)的数值
                            $count_obj.text(val + 1);  // 点赞(反对)数值加1
                        } else {
                            {# 三元运算符, True时,val=冒号前的内容, False时val=冒号后的内容 #}
                            var val = data.handled ? '您已经点过赞了!' : "您已经点过反对了!";
                            $(".diggword").html(val);
                            setTimeout(function () {
                                $(".diggword").html('')
                            }, 1000)
                        }
                    }
                })
            });


            // 评论
            var pid = ''; // 父评论id, pid为空则表示当前为父评论, 默认为空.
            $('.comment-btn').click(function () {
                // 截断评框中@用户名
                var content = $('#comment-content').val();  // 获取评论内容
                // pid有值说明是子评论, 需要切断换行符之前的内容, 取其后的内容
                if (pid) {
                    var index = content.indexOf('\n');
                    content = content.slice(index + 1);
                }
                ;
                console.log(pid);
                $.ajax({
                    url: '/comment/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'content': content,
                        'parent_comment': pid,
                    },
                    success: function (data) {
                        // 展示刚刚提交的评论, 注意这里是从后台数据库中获取数据.
                        var create_time = data.create_time;
                        var content = data.content;
                        var username = data.username;
                        // 将变量嵌入到字符串中, es6语法使用反引号将字符串包住, 使用${}嵌入变量. 如果不用es6语法则需要用+拼接字符串.
                        if (pid) {
                            // 增加父评论的内容
                            var p_username = data.p_username;
                            var p_content = data.p_content;
                            var p_s = `
                                <div class="well">${p_username}:&nbsp;${p_content}</div>
                                <p>${content}</p>
                            `;
                        } else {  // 如果不是字评论,则不需加父评论信息
                            var p_s = `<p>${content}</p>`;
                        }
                        ;
                        var s = `
                            <li class="list-group-item">
                                <div>
                                    <span>${create_time}</span>&nbsp;&nbsp;
                                    <a href="">${username}</a>
                                </div>
                                <div id="content-show" style="margin-top: 10px">
                                    ${p_s}
                                </div>
                            </li>
                            `;
                        // 将新构建的评论加入到评论列表后面
                        $('ul.commnet-list').append(s);
                        $('#comment-content').val(''); // 将评论内容框清空
                        // 将pid设为空
                        pid = '';
                    }
                })
            });


            // 回复评论
            $('.reply-btn').click(function () {
                // 点击回复,光标定位到下面的输入框中, 获取焦点
                $('#comment-content').focus();
                // 拼接加在回复框前的@用户,\n是换行.
                var val = '@' + $(this).attr('username') + '\n';
                $('#comment-content').val(val);
                // 获取回复的pid值, 'comment_pk'是该标签的自定义属性
                pid = $(this).attr('comment_pk');

            })


        </script>
    </div>

{% endblock %}
article_detail.html

 七一. 博客系统之展开评论树2  将子评论放到根评论的下面

71.1 效果

 

 

 

71.2 难点, 在(已渲染好的所有父评论)网页中, 找到子评论的父标签, 这是树形结构的好的解决方法.

                    // 获取评论树
                    $('.tree-btn').click(function () {
                        $.ajax({
                            url: '/get_comment_tree/',
                            type: 'post',
                            data: {
                                'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                                'article_id': {{ article_obj.pk }},
                            },
                            success: function (data) {
                                console.log(data);
                                // 传过来的data是数组, 内部套多个字典. index是数组的序号, comment_dic是数组内的每一个元素的值(这里是字典)
                                // each是循环取出数组内每一个值,相当于python的for循环
                                $.each(data, function (index, comment_dic) {
                                    var pk = comment_dic.pk;
                                    var comment = comment_dic.content;
                                    var parent_comment_id = comment_dic.parent_comment_id;
                                    // 子评论需要整体向后移动,所以增加class 和 style
                                    var s = "<div class='comment-item' style='margin-left: 20px' comment_id="+pk+"><span>"+comment+"</span></div>";
                                    // !parent_comment_id 感叹号表示非. 父id没有值,就是该评论没有父评论,它就是根评论.
                                    if(!parent_comment_id){
                                        $('.comment-tree').append(s);  // 根评论直接加到标签中
                                    }else{
                                        $("[comment_id="+parent_comment_id+"]").append(s);  // 找到父评论标签, 把子评论标签添加进去
                                    }
                                })


                            }
                        })
{# 继承 #}
{% extends 'base.html' %}

{% block content %}
    <div class="article_info">
        {% csrf_token %}
        <h3 class="text-center">{{ article_obj.title }}</h3>
        <div class="cont">
            {{ article_obj.content|safe }}
        </div>
        <div class="clearfix">
            <div id="div_digg">
                {#    diggit action   推荐       #}
                <div class="diggit action">
                    <span class="diggnum" id="digg_count">{{ article_obj.up_count }}</span>
                </div>
                {#    diggit action   反对       #}
                <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" style="color: red;"></div>
            </div>
        </div>


        <div class="comment">
            <div id="">
                {#  评论树点击可以展开此文章评论, 以树形结构显示, 这里完全动态  #}
                <p class="tree-btn">评论树</p>
                <div class="comment-tree">
                </div>
                <script>
                    // 获取评论树
                    $('.tree-btn').click(function () {
                        $.ajax({
                            url: '/get_comment_tree/',
                            type: 'post',
                            data: {
                                'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                                'article_id': {{ article_obj.pk }},
                            },
                            success: function (data) {
                                console.log(data);
                                // 传过来的data是数组, 内部套多个字典. index是数组的序号, comment_dic是数组内的每一个元素的值(这里是字典)
                                // each是循环取出数组内每一个值,相当于python的for循环
                                $.each(data, function (index, comment_dic) {
                                    var pk = comment_dic.pk;
                                    var comment = comment_dic.content;
                                    var parent_comment_id = comment_dic.parent_comment_id;
                                    // 子评论需要整体向后移动,所以增加class 和 style
                                    var s = "<div class='comment-item' style='margin-left: 20px' comment_id="+pk+"><span>"+comment+"</span></div>";
                                    // !parent_comment_id 感叹号表示非. 父id没有值,就是该评论没有父评论,它就是根评论.
                                    if(!parent_comment_id){
                                        $('.comment-tree').append(s);  // 根评论直接加到标签中
                                    }else{
                                        $("[comment_id="+parent_comment_id+"]").append(s);  // 找到父评论标签, 把子评论标签添加进去
                                    }
                                })


                            }
                        })
                    })
                </script>

                <p>评论列表</p>
                {#       复制bootstrap网站的组件中的列表组样式代码         #}
                <ul class="list-group commnet-list">
                    {% for comment in comment_list %}
                        <li class="list-group-item">
                            <div>
                                <a href="">#{{ forloop.counter }}楼</a>&nbsp;&nbsp;
                                <span>{{ comment.create_time|date:"Y-m-d H:i" }}</span>&nbsp;&nbsp;
                                <a href="">{{ comment.user.username }}</a>
                                {#  新增自定义comment_pk属性目的是方便获取回复的pid值   #}
                                <div class="pull-right"><a username="{{ comment.user.username }}"
                                                           comment_pk="{{ comment.pk }}" class="reply-btn">回复</a></div>
                            </div>
                            <div id="content-show" style="margin-top: 10px">
                                {#  如果该评论是子评论,则需要将其父评论也显示出来  #}
                                {% if comment.parent_comment_id %}
                                    <div class="well">{{ comment.parent_comment.user.username }}:&nbsp;{{ comment.parent_comment.content }}</div>
                                {% endif %}
                                <p>{{ comment.content }}</p>
                            </div>
                        </li>
                    {% endfor %}
                </ul>
            </div>

            <div id="comment_title">
                <p>发表评论:</p>
                昵称&ensp;<span class="glyphicon glyphicon-user"></span>
                <input disabled="disabled" value="{{ request.user.username }}">
            </div>
            <div>评论内容</div>
            <div><textarea class="form-control" id="comment-content" rows="10"></textarea></div>
            <p>
                <button class="btn btn-default comment-btn">提交评论</button>
            </p>
        </div>

        <script>
            // 点赞
            $('.action').click(function () {
                var is_up = $(this).hasClass('diggit');  // 有diggit属性,就是True, 否则就是False
                var $count_obj = $(this).children('span');  // 获取用于显示点赞数\或者是反对数的span标签对象
                $.ajax({
                    url: '/digg/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'is_up': is_up,
                    },
                    success: function (data) {
                        if (data.state) {
                            {#  state为True表示之前未点过  #}
                            var val = parseInt($count_obj.text());  // 获取点赞(反对)的数值
                            $count_obj.text(val + 1);  // 点赞(反对)数值加1
                        } else {
                            {# 三元运算符, True时,val=冒号前的内容, False时val=冒号后的内容 #}
                            var val = data.handled ? '您已经点过赞了!' : "您已经点过反对了!";
                            $(".diggword").html(val);
                            setTimeout(function () {
                                $(".diggword").html('')
                            }, 1000)
                        }
                    }
                })
            });


            // 评论
            var pid = ''; // 父评论id, pid为空则表示当前为父评论, 默认为空.
            $('.comment-btn').click(function () {
                // 截断评框中@用户名
                var content = $('#comment-content').val();  // 获取评论内容
                // pid有值说明是子评论, 需要切断换行符之前的内容, 取其后的内容
                if (pid) {
                    var index = content.indexOf('\n');
                    content = content.slice(index + 1);
                }
                ;
                console.log(pid);
                $.ajax({
                    url: '/comment/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'content': content,
                        'parent_comment': pid,
                    },
                    success: function (data) {
                        // 展示刚刚提交的评论, 注意这里是从后台数据库中获取数据.
                        var create_time = data.create_time;
                        var content = data.content;
                        var username = data.username;
                        // 将变量嵌入到字符串中, es6语法使用反引号将字符串包住, 使用${}嵌入变量. 如果不用es6语法则需要用+拼接字符串.
                        if (pid) {
                            // 增加父评论的内容
                            var p_username = data.p_username;
                            var p_content = data.p_content;
                            var p_s = `
                                <div class="well">${p_username}:&nbsp;${p_content}</div>
                                <p>${content}</p>
                            `;
                        } else {  // 如果不是字评论,则不需加父评论信息
                            var p_s = `<p>${content}</p>`;
                        }
                        ;
                        var s = `
                            <li class="list-group-item">
                                <div>
                                    <span>${create_time}</span>&nbsp;&nbsp;
                                    <a href="">${username}</a>
                                </div>
                                <div id="content-show" style="margin-top: 10px">
                                    ${p_s}
                                </div>
                            </li>
                            `;
                        // 将新构建的评论加入到评论列表后面
                        $('ul.commnet-list').append(s);
                        $('#comment-content').val(''); // 将评论内容框清空
                        // 将pid设为空
                        pid = '';
                    }
                })
            });


            // 回复评论
            $('.reply-btn').click(function () {
                // 点击回复,光标定位到下面的输入框中, 获取焦点
                $('#comment-content').focus();
                // 拼接加在回复框前的@用户,\n是换行.
                var val = '@' + $(this).attr('username') + '\n';
                $('#comment-content').val(val);
                // 获取回复的pid值, 'comment_pk'是该标签的自定义属性
                pid = $(this).attr('comment_pk');

            })


        </script>
    </div>

{% endblock %}
article_detail.html

 七二. 博客系统之评论树的思考1

72.1 ajax的回调函数的参数,建议将名称改为comment_list.

             <script>
                    // 获取评论树
                    $('.tree-btn').click(function () {
                        $.ajax({
                            url: '/get_comment_tree/',
                            type: 'post',
                            data: {
                                'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                                'article_id': {{ article_obj.pk }},
                            },
                            success: function (comment_list) {
                                console.log(comment_list);
                                // 传过来的data是数组, 内部套多个字典. index是数组的序号, comment_dic是数组内的每一个元素的值(这里是字典)
                                // each是循环取出数组内每一个值,相当于python的for循环
                                $.each(comment_list, function (index, comment_obj) {
                                    var pk = comment_obj.pk;
                                    var comment = comment_obj.content;
                                    var parent_comment_id = comment_obj.parent_comment_id;
                                    // 子评论需要整体向后移动,所以增加class 和 style
                                    var s = "<div class='comment-item' style='margin-left: 20px' comment_id="+pk+"><span>"+comment+"</span></div>";
                                    // !parent_comment_id 感叹号表示非. 父id没有值,就是该评论没有父评论,它就是根评论.
                                    if(!parent_comment_id){
                                        $('.comment-tree').append(s);  // 根评论直接加到标签中
                                    }else{
                                        $("[comment_id="+parent_comment_id+"]").append(s);  // 找到父评论标签, 把子评论标签添加进去
                                    }
                                })


                            }
                        })
                    })
                </script>
{# 继承 #}
{% extends 'base.html' %}

{% block content %}
    <div class="article_info">
        {% csrf_token %}
        <h3 class="text-center">{{ article_obj.title }}</h3>
        <div class="cont">
            {{ article_obj.content|safe }}
        </div>
        <div class="clearfix">
            <div id="div_digg">
                {#    diggit action   推荐       #}
                <div class="diggit action">
                    <span class="diggnum" id="digg_count">{{ article_obj.up_count }}</span>
                </div>
                {#    diggit action   反对       #}
                <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" style="color: red;"></div>
            </div>
        </div>


        <div class="comment">
            <div id="">
                {#  评论树点击可以展开此文章评论, 以树形结构显示, 这里完全动态  #}
                <p class="tree-btn">评论树</p>
                <div class="comment-tree">
                </div>
                <script>
                    // 获取评论树
                    $('.tree-btn').click(function () {
                        $.ajax({
                            url: '/get_comment_tree/',
                            type: 'post',
                            data: {
                                'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                                'article_id': {{ article_obj.pk }},
                            },
                            success: function (comment_list) {
                                console.log(comment_list);
                                // 传过来的data是数组, 内部套多个字典. index是数组的序号, comment_dic是数组内的每一个元素的值(这里是字典)
                                // each是循环取出数组内每一个值,相当于python的for循环
                                $.each(comment_list, function (index, comment_obj) {
                                    var pk = comment_obj.pk;
                                    var comment = comment_obj.content;
                                    var parent_comment_id = comment_obj.parent_comment_id;
                                    // 子评论需要整体向后移动,所以增加class 和 style
                                    var s = "<div class='comment-item' style='margin-left: 20px' comment_id="+pk+"><span>"+comment+"</span></div>";
                                    // !parent_comment_id 感叹号表示非. 父id没有值,就是该评论没有父评论,它就是根评论.
                                    if(!parent_comment_id){
                                        $('.comment-tree').append(s);  // 根评论直接加到标签中
                                    }else{
                                        $("[comment_id="+parent_comment_id+"]").append(s);  // 找到父评论标签, 把子评论标签添加进去
                                    }
                                })


                            }
                        })
                    })
                </script>

                <p>评论列表</p>
                {#       复制bootstrap网站的组件中的列表组样式代码         #}
                <ul class="list-group commnet-list">
                    {% for comment in comment_list %}
                        <li class="list-group-item">
                            <div>
                                <a href="">#{{ forloop.counter }}楼</a>&nbsp;&nbsp;
                                <span>{{ comment.create_time|date:"Y-m-d H:i" }}</span>&nbsp;&nbsp;
                                <a href="">{{ comment.user.username }}</a>
                                {#  新增自定义comment_pk属性目的是方便获取回复的pid值   #}
                                <div class="pull-right"><a username="{{ comment.user.username }}"
                                                           comment_pk="{{ comment.pk }}" class="reply-btn">回复</a></div>
                            </div>
                            <div id="content-show" style="margin-top: 10px">
                                {#  如果该评论是子评论,则需要将其父评论也显示出来  #}
                                {% if comment.parent_comment_id %}
                                    <div class="well">{{ comment.parent_comment.user.username }}:&nbsp;{{ comment.parent_comment.content }}</div>
                                {% endif %}
                                <p>{{ comment.content }}</p>
                            </div>
                        </li>
                    {% endfor %}
                </ul>
            </div>

            <div id="comment_title">
                <p>发表评论:</p>
                昵称&ensp;<span class="glyphicon glyphicon-user"></span>
                <input disabled="disabled" value="{{ request.user.username }}">
            </div>
            <div>评论内容</div>
            <div><textarea class="form-control" id="comment-content" rows="10"></textarea></div>
            <p>
                <button class="btn btn-default comment-btn">提交评论</button>
            </p>
        </div>

        <script>
            // 点赞
            $('.action').click(function () {
                var is_up = $(this).hasClass('diggit');  // 有diggit属性,就是True, 否则就是False
                var $count_obj = $(this).children('span');  // 获取用于显示点赞数\或者是反对数的span标签对象
                $.ajax({
                    url: '/digg/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'is_up': is_up,
                    },
                    success: function (data) {
                        if (data.state) {
                            {#  state为True表示之前未点过  #}
                            var val = parseInt($count_obj.text());  // 获取点赞(反对)的数值
                            $count_obj.text(val + 1);  // 点赞(反对)数值加1
                        } else {
                            {# 三元运算符, True时,val=冒号前的内容, False时val=冒号后的内容 #}
                            var val = data.handled ? '您已经点过赞了!' : "您已经点过反对了!";
                            $(".diggword").html(val);
                            setTimeout(function () {
                                $(".diggword").html('')
                            }, 1000)
                        }
                    }
                })
            });


            // 评论
            var pid = ''; // 父评论id, pid为空则表示当前为父评论, 默认为空.
            $('.comment-btn').click(function () {
                // 截断评框中@用户名
                var content = $('#comment-content').val();  // 获取评论内容
                // pid有值说明是子评论, 需要切断换行符之前的内容, 取其后的内容
                if (pid) {
                    var index = content.indexOf('\n');
                    content = content.slice(index + 1);
                }
                ;
                console.log(pid);
                $.ajax({
                    url: '/comment/',
                    type: 'post',
                    data: {
                        'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                        'article_id': {{ article_obj.pk }},
                        'content': content,
                        'parent_comment': pid,
                    },
                    success: function (data) {
                        // 展示刚刚提交的评论, 注意这里是从后台数据库中获取数据.
                        var create_time = data.create_time;
                        var content = data.content;
                        var username = data.username;
                        // 将变量嵌入到字符串中, es6语法使用反引号将字符串包住, 使用${}嵌入变量. 如果不用es6语法则需要用+拼接字符串.
                        if (pid) {
                            // 增加父评论的内容
                            var p_username = data.p_username;
                            var p_content = data.p_content;
                            var p_s = `
                                <div class="well">${p_username}:&nbsp;${p_content}</div>
                                <p>${content}</p>
                            `;
                        } else {  // 如果不是字评论,则不需加父评论信息
                            var p_s = `<p>${content}</p>`;
                        }
                        ;
                        var s = `
                            <li class="list-group-item">
                                <div>
                                    <span>${create_time}</span>&nbsp;&nbsp;
                                    <a href="">${username}</a>
                                </div>
                                <div id="content-show" style="margin-top: 10px">
                                    ${p_s}
                                </div>
                            </li>
                            `;
                        // 将新构建的评论加入到评论列表后面
                        $('ul.commnet-list').append(s);
                        $('#comment-content').val(''); // 将评论内容框清空
                        // 将pid设为空
                        pid = '';
                    }
                })
            });


            // 回复评论
            $('.reply-btn').click(function () {
                // 点击回复,光标定位到下面的输入框中, 获取焦点
                $('#comment-content').focus();
                // 拼接加在回复框前的@用户,\n是换行.
                var val = '@' + $(this).attr('username') + '\n';
                $('#comment-content').val(val);
                // 获取回复的pid值, 'comment_pk'是该标签的自定义属性
                pid = $(this).attr('comment_pk');

            })


        </script>
    </div>

{% endblock %}
article_detail.html

 

72.2 视图函数获取数据库评论,建议加上order_by('pk'),即按键值顺序取出.默认也是按键值顺序,这里强化一下.

def get_comment_tree(request):

    article_id = request.POST.get('article_id')
    # list强制将querset类型转换为list,然后再序列化. values中是取需要的字段
    ret = list(models.Comment.objects.filter(article_id=article_id).order_by('pk').values('pk', 'content', 'parent_comment_id'))
    print(ret)
    return JsonResponse(ret, safe=False)  # 非字典序列化必须加上safe=False参数
from django.shortcuts import render, HttpResponse, redirect
from django.http import JsonResponse  # 传递json数据类型
from django.contrib import auth  # 导入用户组件,用于auth_user表的操作
from django.db.models import Avg, Max, Min, Count
from django.db.models.functions import TruncMonth
from blog import models

from blog.models import UserInfo
from blog.myforms import RegForm  # 导入自定义forms验证规则


def reg(request):
    """用户注册视图"""
    if request.is_ajax():
        form = RegForm(request.POST)
        respone = {'user': None, 'msg': None}
        if form.is_valid():
            respone['user'] = form.cleaned_data.get('user')
            user = form.cleaned_data.get('user')
            pwd = form.cleaned_data.get('pwd')
            email = form.cleaned_data.get('email')
            avatar_obj = request.FILES.get('avatar')
            extra = {}
            if avatar_obj:
                extra['avatar'] = avatar_obj
            user_obj = UserInfo.objects.create_user(username=user, password=pwd, email=email, **extra)
        else:
            respone['msg'] = form.errors
        return JsonResponse(respone)

    form = RegForm()  # 带入规则
    return render(request, 'reg.html', {"form": form})


def login(request):
    if request.method == "POST":
        response = {'user': None, 'msg': None}  # 构造登录提示信息
        user = request.POST.get('user')
        pwd = request.POST.get('pwd')
        valid_code = request.POST.get('valid_code')  # 获取用户输入的验证码
        valid_code_str = request.session.get('valid_code')  # 从session中提取自动生成的验证码字符串
        if valid_code_str.upper() == valid_code.upper():  # 全部转为大写字母
            user = auth.authenticate(username=user, password=pwd)
            if user:
                auth.login(request, user)  # yanz 成功在session中注册当前用户信息, 就有了request.user全局变量
                response['user'] = user.username  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.
            else:
                response['msg'] = '用户名或密码错误!'
        else:
            response['msg'] = '验证码错误!'
        return JsonResponse(response)  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.

    return render(request, 'login.html')


def get_validCode_img(request):
    """生成验证码图片"""
    from blog.utils.validCode import get_validCode_img  # 导入自定义的生成验证码功能函数
    data = get_validCode_img(request)
    return HttpResponse(data)


def index(request):
    article_list = models.Article.objects.all()
    return render(request, 'index.html', {'article_list': article_list})


def logout(request):
    auth.logout(request)
    return redirect('/login/')


def home_site(request, username, **kwargs):
    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')

    # 查询username用户的blog对象
    blog = user.blog
    article_list = models.Article.objects.filter(user=user)
    if kwargs:
        # 个人站点跳转
        condition = kwargs.get('condition')  # 使用**kwargs目的是可以接受任意多个参数
        param = kwargs.get('param')
        if condition == 'tag':
            article_list = article_list.filter(tags__title=param)
            # print(article_list)
        elif condition == 'category':
            article_list = article_list.filter(category__title=param)
        else:
            # 日期归档
            year, month = param.split('-')
            article_list = article_list.filter(create_time__year=year, create_time__month=month)

    return render(request, 'home_site.html', {'username': username, 'blog': blog, 'article_list': article_list})


def article_detail(request, username, article_id):
    """
    文章详情页视图
    http://127.0.0.1:8000/papa/article/2/
    """

    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')
    # 查询username用户的blog对象
    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, 'article_detail.html', locals())  # locals()不能丢


def digg(request):
    print(request.user)
    import json
    article_id = request.POST.get('article_id')
    is_up = json.loads(request.POST.get('is_up'))  # 传递过来的是"True"字符串, 所以这里需要反序列化为布尔值
    user_id = request.user.pk  # 直接获取user_id,没有必要通过客户端传送

    # 判断是否已经点过赞
    response = {'state': True}  # 返回的状态,将传递给模板中的回调函数,默认是True成功, 表示为点过赞或者反对.
    ret_obj = models.ArticleUpDown.objects.filter(user_id=user_id, article_id=article_id).first()
    if not ret_obj:  # 没点过赞
        ard = models.ArticleUpDown.objects.create(user_id=user_id, article_id=article_id, is_up=is_up)
        # 将点赞写入数据库中
        from django.db.models import F
        article_obj = models.Article.objects.filter(pk=article_id)
        if is_up:
            article_obj.update(up_count=F("up_count") + 1)
        else:
            article_obj.update(down_count=F("down_count") + 1)
    else:  # 已经点过赞或反对了
        response['state'] = False  # 表示失败
        response['handled'] = ret_obj.is_up  # 获取当前数据库中是点赞过还是点反对过.

    return JsonResponse(response)  # 将状态传递给模板中的回调函数


def comment(request):
    """
    发布评论
    :param request:
    :return:
    """
    # 前端ajax发送数据, 这里予以获取
    article_id = request.POST.get('article_id')
    content = request.POST.get('content')
    pid = request.POST.get('parent_comment')

    # 从当前用户登录信息中获取
    user_id = request.user.pk
    print("pid:", pid)

    # 写入到数据库comment表
    comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content,
                                                parent_comment_id=pid)

    # 从数据库提取刚刚存入的数据, 为啥非要从数据库中立即再次取出, 不能直接在前端提交时,在前端直接取数据吗?
    create_time = comment_obj.create_time.strftime('%Y-%m-%d %X')
    content = comment_obj.content
    username = request.user.username
    response = {}
    response["create_time"] = create_time
    response['content'] = content
    response['username'] = username

    # 如果是子评论,则还需要获取其父评论的内容,姓名和评论内容
    if pid:
        parent_obj = models.Comment.objects.filter(pk=pid).first()
        response['p_username'] = parent_obj.user.username
        response['p_content'] = parent_obj.content

    return JsonResponse(response)


def get_comment_tree(request):

    article_id = request.POST.get('article_id')
    # list强制将querset类型转换为list,然后再序列化. values中是取需要的字段
    ret = list(models.Comment.objects.filter(article_id=article_id).order_by('pk').values('pk', 'content', 'parent_comment_id'))
    print(ret)
    return JsonResponse(ret, safe=False)  # 非字典序列化必须加上safe=False参数
views.py

 七三. 博客系统之评论树的思考2

73.1 前面的设计是,点击评论树三个字,然后列出所有评论树, 现在想载入网页时能直接出现评论树.  只需要将ajax前的click事件去掉即可.

                <script>
                    // 获取评论树  去掉下面这句$('.tree-btn').click(function () {代码就可以了
                    $('.tree-btn').click(function () {
                        $.ajax({
                            url: '/get_comment_tree/',
                            type: 'post',
                            data: {
                                'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val(),
                                'article_id': {{ article_obj.pk }},
                            },
                            success: function (comment_list) {
                                console.log(comment_list);
                                // 传过来的data是数组, 内部套多个字典. index是数组的序号, comment_dic是数组内的每一个元素的值(这里是字典)
                                // each是循环取出数组内每一个值,相当于python的for循环
                                $.each(comment_list, function (index, comment_obj) {
                                    var pk = comment_obj.pk;
                                    var comment = comment_obj.content;
                                    var parent_comment_id = comment_obj.parent_comment_id;
                                    // 子评论需要整体向后移动,所以增加class 和 style
                                    var s = "<div class='comment-item' style='margin-left: 20px' comment_id="+pk+"><span>"+comment+"</span></div>";
                                    // !parent_comment_id 感叹号表示非. 父id没有值,就是该评论没有父评论,它就是根评论.
                                    if(!parent_comment_id){
                                        $('.comment-tree').append(s);  // 根评论直接加到标签中
                                    }else{
                                        $("[comment_id="+parent_comment_id+"]").append(s);  // 找到父评论标签, 把子评论标签添加进去
                                    }
                                })


                            }
                        })
                    })
                </script>

 七四. 博客系统之评论事务操作

74.1 事务的意思是, 比如甲向乙转账, 从甲账户上减, 乙账户上同时加上, 为了保持减加的一致, 就可以加上事务操作. 有事务操作时, 如果甲减了, 而乙没有加上, 则会回滚回转账之前.
74.2 本博客系统, 评论增加了一条, 在文章表中有一个计数, 也应当同时加1. 为了保证一致, 可以加上事务操作.
74.3 关键代码, 视图函数
from django.db.models import F  # 数据库表中自加1
from django.db import transaction  # 事务操作(用于保证多个数据的同步一致)

def comment(request):
    """
    发布评论
    :param request:
    :return:
    """
    # 前端ajax发送数据, 这里予以获取
    article_id = request.POST.get('article_id')
    content = request.POST.get('content')
    pid = request.POST.get('parent_comment')

    # 从当前用户登录信息中获取
    user_id = request.user.pk
    print("pid:", pid)

    with transaction.atomic():  # 事务同步, 保证评论内容写入comment表,同时,将article表中的评论数计数器加1
        # 写入到数据库comment表
        comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content,                                                    parent_comment_id=pid)
        # 同步写入评论数加1的数据到文章表中
        models.Article.objects.filter(pk=article_id).update(comment_count=F('comment_count')+1)

    # 从数据库提取刚刚存入的数据, 为啥非要从数据库中立即再次取出, 不能直接在前端提交时,在前端直接取数据吗?
    create_time = comment_obj.create_time.strftime('%Y-%m-%d %X')
    content = comment_obj.content
    username = request.user.username
    response = {}
    response["create_time"] = create_time
    response['content'] = content
    response['username'] = username

    # 如果是子评论,则还需要获取其父评论的内容,姓名和评论内容
    if pid:
        parent_obj = models.Comment.objects.filter(pk=pid).first()
        response['p_username'] = parent_obj.user.username
        response['p_content'] = parent_obj.content

    return JsonResponse(response)
from django.shortcuts import render, HttpResponse, redirect
from django.http import JsonResponse  # 传递json数据类型
from django.contrib import auth  # 导入用户组件,用于auth_user表的操作
from django.db.models import F  # 数据库表中自加1
from django.db import transaction  # 事务操作(用于保证多个数据的同步一致)

from django.db.models import Avg, Max, Min, Count
from django.db.models.functions import TruncMonth
from blog import models

from blog.models import UserInfo
from blog.myforms import RegForm  # 导入自定义forms验证规则


def reg(request):
    """用户注册视图"""
    if request.is_ajax():
        form = RegForm(request.POST)
        respone = {'user': None, 'msg': None}
        if form.is_valid():
            respone['user'] = form.cleaned_data.get('user')
            user = form.cleaned_data.get('user')
            pwd = form.cleaned_data.get('pwd')
            email = form.cleaned_data.get('email')
            avatar_obj = request.FILES.get('avatar')
            extra = {}
            if avatar_obj:
                extra['avatar'] = avatar_obj
            user_obj = UserInfo.objects.create_user(username=user, password=pwd, email=email, **extra)
        else:
            respone['msg'] = form.errors
        return JsonResponse(respone)

    form = RegForm()  # 带入规则
    return render(request, 'reg.html', {"form": form})


def login(request):
    if request.method == "POST":
        response = {'user': None, 'msg': None}  # 构造登录提示信息
        user = request.POST.get('user')
        pwd = request.POST.get('pwd')
        valid_code = request.POST.get('valid_code')  # 获取用户输入的验证码
        valid_code_str = request.session.get('valid_code')  # 从session中提取自动生成的验证码字符串
        if valid_code_str.upper() == valid_code.upper():  # 全部转为大写字母
            user = auth.authenticate(username=user, password=pwd)
            if user:
                auth.login(request, user)  # yanz 成功在session中注册当前用户信息, 就有了request.user全局变量
                response['user'] = user.username  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.
            else:
                response['msg'] = '用户名或密码错误!'
        else:
            response['msg'] = '验证码错误!'
        return JsonResponse(response)  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.

    return render(request, 'login.html')


def get_validCode_img(request):
    """生成验证码图片"""
    from blog.utils.validCode import get_validCode_img  # 导入自定义的生成验证码功能函数
    data = get_validCode_img(request)
    return HttpResponse(data)


def index(request):
    article_list = models.Article.objects.all()
    return render(request, 'index.html', {'article_list': article_list})


def logout(request):
    auth.logout(request)
    return redirect('/login/')


def home_site(request, username, **kwargs):
    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')

    # 查询username用户的blog对象
    blog = user.blog
    article_list = models.Article.objects.filter(user=user)
    if kwargs:
        # 个人站点跳转
        condition = kwargs.get('condition')  # 使用**kwargs目的是可以接受任意多个参数
        param = kwargs.get('param')
        if condition == 'tag':
            article_list = article_list.filter(tags__title=param)
            # print(article_list)
        elif condition == 'category':
            article_list = article_list.filter(category__title=param)
        else:
            # 日期归档
            year, month = param.split('-')
            article_list = article_list.filter(create_time__year=year, create_time__month=month)

    return render(request, 'home_site.html', {'username': username, 'blog': blog, 'article_list': article_list})


def article_detail(request, username, article_id):
    """
    文章详情页视图
    http://127.0.0.1:8000/papa/article/2/
    """

    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')
    # 查询username用户的blog对象
    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, 'article_detail.html', locals())  # locals()不能丢


def digg(request):
    print(request.user)
    import json
    article_id = request.POST.get('article_id')
    is_up = json.loads(request.POST.get('is_up'))  # 传递过来的是"True"字符串, 所以这里需要反序列化为布尔值
    user_id = request.user.pk  # 直接获取user_id,没有必要通过客户端传送

    # 判断是否已经点过赞
    response = {'state': True}  # 返回的状态,将传递给模板中的回调函数,默认是True成功, 表示为点过赞或者反对.
    ret_obj = models.ArticleUpDown.objects.filter(user_id=user_id, article_id=article_id).first()
    if not ret_obj:  # 没点过赞
        ard = models.ArticleUpDown.objects.create(user_id=user_id, article_id=article_id, is_up=is_up)
        # 将点赞写入数据库中

        article_obj = models.Article.objects.filter(pk=article_id)
        if is_up:
            article_obj.update(up_count=F("up_count") + 1)
        else:
            article_obj.update(down_count=F("down_count") + 1)
    else:  # 已经点过赞或反对了
        response['state'] = False  # 表示失败
        response['handled'] = ret_obj.is_up  # 获取当前数据库中是点赞过还是点反对过.

    return JsonResponse(response)  # 将状态传递给模板中的回调函数


def comment(request):
    """
    发布评论
    :param request:
    :return:
    """
    # 前端ajax发送数据, 这里予以获取
    article_id = request.POST.get('article_id')
    content = request.POST.get('content')
    pid = request.POST.get('parent_comment')

    # 从当前用户登录信息中获取
    user_id = request.user.pk
    print("pid:", pid)

    with transaction.atomic():  # 事务同步, 保证评论内容写入comment表,同时,将article表中的评论数计数器加1
        # 写入到数据库comment表
        comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content,                                                    parent_comment_id=pid)
        # 同步写入评论数加1的数据到文章表中
        models.Article.objects.filter(pk=article_id).update(comment_count=F('comment_count')+1)

    # 从数据库提取刚刚存入的数据, 为啥非要从数据库中立即再次取出, 不能直接在前端提交时,在前端直接取数据吗?
    create_time = comment_obj.create_time.strftime('%Y-%m-%d %X')
    content = comment_obj.content
    username = request.user.username
    response = {}
    response["create_time"] = create_time
    response['content'] = content
    response['username'] = username

    # 如果是子评论,则还需要获取其父评论的内容,姓名和评论内容
    if pid:
        parent_obj = models.Comment.objects.filter(pk=pid).first()
        response['p_username'] = parent_obj.user.username
        response['p_content'] = parent_obj.content

    return JsonResponse(response)


def get_comment_tree(request):

    article_id = request.POST.get('article_id')
    # list强制将querset类型转换为list,然后再序列化. values中是取需要的字段
    ret = list(models.Comment.objects.filter(article_id=article_id).order_by('pk').values('pk', 'content', 'parent_comment_id'))
    print(ret)
    return JsonResponse(ret, safe=False)  # 非字典序列化必须加上safe=False参数
views.py

 七五. 博客系统之评论的邮件发送new  文章有了新评论后,给作者发邮件提醒

75.1 settings.py中配置发件邮箱

# 配置邮件发送
EMAIL_HOST = 'smtp.qq.com'  # 如果是163 改成 smtp.163.com
EMAIL_PORT = 465  # 163端口25
EMAIL_HOST_USER = ''  # 发件账户
EMAIL_HOST_PASSWORD = ""  # 密码,客户端授权码, 不是邮箱密码
# DEFAULT_FORM_EMAIL = EMAIL_HOST_USER  # 配置默认发件账户
EMAIL_USE_SSL = True  # ssl证书支持
75.2 在视图函数中增加发邮件的功能
def comment(request):
    """
    发布评论
    :param request:
    :return:
    """
    # 前端ajax发送数据, 这里予以获取
    article_id = request.POST.get('article_id')
    content = request.POST.get('content')
    pid = request.POST.get('parent_comment')

    # 从当前用户登录信息中获取
    user_id = request.user.pk
    print("pid:", pid)

    with transaction.atomic():  # 事务同步, 保证评论内容写入comment表,同时,将article表中的评论数计数器加1
        # 写入到数据库comment表
        comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content,                                                    parent_comment_id=pid)
        # 同步写入评论数加1的数据到文章表中
        models.Article.objects.filter(pk=article_id).update(comment_count=F('comment_count')+1)

    # 从数据库提取刚刚存入的数据, 为啥非要从数据库中立即再次取出, 不能直接在前端提交时,在前端直接取数据吗?
    create_time = comment_obj.create_time.strftime('%Y-%m-%d %X')
    content = comment_obj.content
    username = request.user.username
    response = {}
    response["create_time"] = create_time
    response['content'] = content
    response['username'] = username

    # 如果是子评论,则还需要获取其父评论的内容,姓名和评论内容
    if pid:
        parent_obj = models.Comment.objects.filter(pk=pid).first()
        response['p_username'] = parent_obj.user.username
        response['p_content'] = parent_obj.content


    # 发送邮件通知文章作者,你的文章有了新的评论
    from django.core.mail import send_mail  # 发送邮件模块
    from cnblog import settings  # 导入配置
    import threading  # 为了提高效率, 另开一个线程发送邮件
    article_obj = models.Article.objects.filter(pk=article_id).first()  # 获取当前文章对象
    email = article_obj.user.email  # 获取文章的作者的邮箱
    print(email, type(email))
    # send_mail(
    #     '您的文章%s新增了一条评论内容' % article_obj.title,
    #     '评论内容:%s' % content,
    #     settings.EMAIL_HOST_USER,  # 配置发送邮件的邮箱账户, 如果在settings中配置了默认邮箱,则这里可以不写
    #     [email, '123456789@qq.com'],  # 作者邮箱列表
    # )
    # 开多线程,提高效率
    sendMail = threading.Thread(target=send_mail, args=(
        '您的文章%s新增了一条评论内容' % article_obj.title,
        '评论内容:%s' % content,
        settings.EMAIL_HOST_USER,
        [email, '123456789@qq.com'],
    ))
    sendMail.start()  # 多线程千万别忘了这一步.这里才是真正的执行发送任务.上面是配置.

    return JsonResponse(response)
from django.shortcuts import render, HttpResponse, redirect
from django.http import JsonResponse  # 传递json数据类型
from django.contrib import auth  # 导入用户组件,用于auth_user表的操作
from django.db.models import F  # 数据库表中自加1
from django.db import transaction  # 事务操作(用于保证多个数据的同步一致)

from django.db.models import Avg, Max, Min, Count
from django.db.models.functions import TruncMonth
from blog import models

from blog.models import UserInfo
from blog.myforms import RegForm  # 导入自定义forms验证规则


def reg(request):
    """用户注册视图"""
    if request.is_ajax():
        form = RegForm(request.POST)
        respone = {'user': None, 'msg': None}
        if form.is_valid():
            respone['user'] = form.cleaned_data.get('user')
            user = form.cleaned_data.get('user')
            pwd = form.cleaned_data.get('pwd')
            email = form.cleaned_data.get('email')
            avatar_obj = request.FILES.get('avatar')
            extra = {}
            if avatar_obj:
                extra['avatar'] = avatar_obj
            user_obj = UserInfo.objects.create_user(username=user, password=pwd, email=email, **extra)
        else:
            respone['msg'] = form.errors
        return JsonResponse(respone)

    form = RegForm()  # 带入规则
    return render(request, 'reg.html', {"form": form})


def login(request):
    if request.method == "POST":
        response = {'user': None, 'msg': None}  # 构造登录提示信息
        user = request.POST.get('user')
        pwd = request.POST.get('pwd')
        valid_code = request.POST.get('valid_code')  # 获取用户输入的验证码
        valid_code_str = request.session.get('valid_code')  # 从session中提取自动生成的验证码字符串
        if valid_code_str.upper() == valid_code.upper():  # 全部转为大写字母
            user = auth.authenticate(username=user, password=pwd)
            if user:
                auth.login(request, user)  # yanz 成功在session中注册当前用户信息, 就有了request.user全局变量
                response['user'] = user.username  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.
            else:
                response['msg'] = '用户名或密码错误!'
        else:
            response['msg'] = '验证码错误!'
        return JsonResponse(response)  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.

    return render(request, 'login.html')


def get_validCode_img(request):
    """生成验证码图片"""
    from blog.utils.validCode import get_validCode_img  # 导入自定义的生成验证码功能函数
    data = get_validCode_img(request)
    return HttpResponse(data)


def index(request):
    article_list = models.Article.objects.all()
    return render(request, 'index.html', {'article_list': article_list})


def logout(request):
    auth.logout(request)
    return redirect('/login/')


def home_site(request, username, **kwargs):
    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')

    # 查询username用户的blog对象
    blog = user.blog
    article_list = models.Article.objects.filter(user=user)
    if kwargs:
        # 个人站点跳转
        condition = kwargs.get('condition')  # 使用**kwargs目的是可以接受任意多个参数
        param = kwargs.get('param')
        if condition == 'tag':
            article_list = article_list.filter(tags__title=param)
            # print(article_list)
        elif condition == 'category':
            article_list = article_list.filter(category__title=param)
        else:
            # 日期归档
            year, month = param.split('-')
            article_list = article_list.filter(create_time__year=year, create_time__month=month)

    return render(request, 'home_site.html', {'username': username, 'blog': blog, 'article_list': article_list})


def article_detail(request, username, article_id):
    """
    文章详情页视图
    http://127.0.0.1:8000/papa/article/2/
    """

    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')
    # 查询username用户的blog对象
    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, 'article_detail.html', locals())  # locals()不能丢


def digg(request):
    print(request.user)
    import json
    article_id = request.POST.get('article_id')
    is_up = json.loads(request.POST.get('is_up'))  # 传递过来的是"True"字符串, 所以这里需要反序列化为布尔值
    user_id = request.user.pk  # 直接获取user_id,没有必要通过客户端传送

    # 判断是否已经点过赞
    response = {'state': True}  # 返回的状态,将传递给模板中的回调函数,默认是True成功, 表示为点过赞或者反对.
    ret_obj = models.ArticleUpDown.objects.filter(user_id=user_id, article_id=article_id).first()
    if not ret_obj:  # 没点过赞
        ard = models.ArticleUpDown.objects.create(user_id=user_id, article_id=article_id, is_up=is_up)
        # 将点赞写入数据库中

        article_obj = models.Article.objects.filter(pk=article_id)
        if is_up:
            article_obj.update(up_count=F("up_count") + 1)
        else:
            article_obj.update(down_count=F("down_count") + 1)
    else:  # 已经点过赞或反对了
        response['state'] = False  # 表示失败
        response['handled'] = ret_obj.is_up  # 获取当前数据库中是点赞过还是点反对过.

    return JsonResponse(response)  # 将状态传递给模板中的回调函数


def comment(request):
    """
    发布评论
    :param request:
    :return:
    """
    # 前端ajax发送数据, 这里予以获取
    article_id = request.POST.get('article_id')
    content = request.POST.get('content')
    pid = request.POST.get('parent_comment')

    # 从当前用户登录信息中获取
    user_id = request.user.pk
    print("pid:", pid)

    with transaction.atomic():  # 事务同步, 保证评论内容写入comment表,同时,将article表中的评论数计数器加1
        # 写入到数据库comment表
        comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content,                                                    parent_comment_id=pid)
        # 同步写入评论数加1的数据到文章表中
        models.Article.objects.filter(pk=article_id).update(comment_count=F('comment_count')+1)

    # 从数据库提取刚刚存入的数据, 为啥非要从数据库中立即再次取出, 不能直接在前端提交时,在前端直接取数据吗?
    create_time = comment_obj.create_time.strftime('%Y-%m-%d %X')
    content = comment_obj.content
    username = request.user.username
    response = {}
    response["create_time"] = create_time
    response['content'] = content
    response['username'] = username

    # 如果是子评论,则还需要获取其父评论的内容,姓名和评论内容
    if pid:
        parent_obj = models.Comment.objects.filter(pk=pid).first()
        response['p_username'] = parent_obj.user.username
        response['p_content'] = parent_obj.content


    # 发送邮件通知文章作者,你的文章有了新的评论
    from django.core.mail import send_mail  # 发送邮件模块
    from cnblog import settings  # 导入配置
    import threading  # 为了提高效率, 另开一个线程发送邮件
    article_obj = models.Article.objects.filter(pk=article_id).first()  # 获取当前文章对象
    email = article_obj.user.email  # 获取文章的作者的邮箱
    print(email, type(email))
    # send_mail(
    #     '您的文章%s新增了一条评论内容' % article_obj.title,
    #     '评论内容:%s' % content,
    #     settings.EMAIL_HOST_USER,  # 配置发送邮件的邮箱账户, 如果在settings中配置了默认邮箱,则这里可以不写
    #     [email, '123456789@qq.com'],  # 作者邮箱列表
    # )
    # 开多线程,提高效率
    sendMail = threading.Thread(target=send_mail, args=(
        '您的文章%s新增了一条评论内容' % article_obj.title,
        '评论内容:%s' % content,
        settings.EMAIL_HOST_USER,
        [email, '123456789@qq.com'],
    ))
    sendMail.start()  # 多线程千万别忘了这一步.这里才是真正的执行发送任务.上面是配置.


    return JsonResponse(response)


def get_comment_tree(request):

    article_id = request.POST.get('article_id')
    # list强制将querset类型转换为list,然后再序列化. values中是取需要的字段
    ret = list(models.Comment.objects.filter(article_id=article_id).order_by('pk').values('pk', 'content', 'parent_comment_id'))
    print(ret)
    return JsonResponse(ret, safe=False)  # 非字典序列化必须加上safe=False参数
views.py

 七六. 博客系统之后台团了页面文本编辑的功能

76.1 后台管理界面效果

 

 

76.2 后台管理模板代码  新建顶部左侧被继承模板backed_base.html, 后台管理首页文章列表backed_index.html, 添加文章add_article.html三个模板

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>网站管理后台</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css"
          integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <link rel="stylesheet" href="/static/css/home_site.css" type="text/css" charset="UTF-8">
    <link rel="stylesheet" href="/static/css/article_detail.css" type="text/css" charset="UTF-8">
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
    <style>
        .header .backend{
            color: white;
            font-size: 14px;
            padding-right: 10px;
        }
        .header .backend a{
            color: white;
        }
    </style>
</head>

<body>
<div class="header">
    <div class="content clearfix">
        <span class="title">网站管理后台</span>
        <div class="backend pull-right">
            <span class="glyphicon glyphicon-user"></span>
            <span>{{ request.user.username }}</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
            <a href="/logout/">注销</a>
        </div>
    </div>
</div>
<div class="container">
    <div class="row">
        <div class="col-md-2">
            <div class="panel panel-default text-center">
              <div class="panel-heading">&nbsp;&nbsp;&nbsp;&nbsp;</div>
              <div class="panel-body">
                  <div><span class="glyphicon glyphicon-circle-arrow-right"></span>&nbsp;&nbsp;<a href="/add_article/">添加文章</a></div>
                  <hr>
                  <div><span class="glyphicon glyphicon-circle-arrow-right"></span>&nbsp;&nbsp;<a href="/backed_index/">所有文章</a></div>
                  <hr>
                  <div><span class="glyphicon glyphicon-circle-arrow-right"></span>&nbsp;&nbsp;<a href="">修改密码</a></div>
                  <hr>
                  <div><span class="glyphicon glyphicon-circle-arrow-right"></span>&nbsp;&nbsp;<a href="">修改头像</a></div>
              </div>
            </div>

        </div>
        <div class="col-md-10">
            {% block backed_content %}
                {#      预留盒子, 进行扩展      #}
            {% endblock %}
        </div>
    </div>
</div>

</body>
</html>
backed_base.html
{% extends 'backed_base.html' %}


{% block backed_content %}
    <h2>{{ request.user.username }}的所有文章</h2>
    <hr>
    <table class="table table-hover">
        <thead>
            <tr>
                <th>ID</th>
                <th>文章标题</th>
                <th>评论数</th>
                <th>点赞数</th>
                <th>发文时间</th>
                <th>操作</th>
                <th>操作</th>
            </tr>
        </thead>
        <tbody>
            {% for article in article_list %}
                <tr>
                    <td>{{ article.pk }}</td>
                    <td>{{ article.title }}</td>
                    <td>{{ article.comment_count }}</td>
                    <td>{{ article.up_count }}</td>
                    <td>{{ article.create_time|date:"Y-m-d H:i" }}</td>
                    <td><a href="" class="btn btn-danger">修改</a></td>
                    <td><a href="" class="btn btn-warning">删除</a></td>
                </tr>
            {% endfor %}
        </tbody>



    </table>

{% endblock %}
backed_index.html
{% extends 'backed_base.html' %}


{% block backed_content %}
    <h2>新增{{ request.user.username }}的文章</h2>
    <hr>
    <div class="container">
        <div class="row">
            <div class="col-md-9">
                <form action="" method="post">
            {% csrf_token %}
            <div class="form-group">
                <label for="title">文章标题</label>
                <input type="text" id="title" name="title" class="form-control">
            </div>
            <div class="form-group">
                <label for="desc">文章描述</label>
                <input type="text" id="desc" name="desc" class="form-control">
            </div>
            <div class="form-group">
                <label for="content">文章详细内容</label>
                <textarea cols="30" id="content" name="content" class="form-control"></textarea>
            </div>
            <div class="form-group">
                <input type="submit" class="btn btn-success">
            </div>
        </form>
            </div>
        </div>

    </div>



{% endblock %}
add_article.html

76.3 后台管理视图代码, 使用用户认证装饰器,

from django.shortcuts import render, HttpResponse, redirect
from django.http import JsonResponse  # 传递json数据类型
from django.contrib import auth  # 导入用户组件,用于auth_user表的操作
from django.db.models import F  # 数据库表中自加1
from django.db import transaction  # 事务操作(用于保证多个数据的同步一致)
from django.contrib.auth.decorators import login_required  # 登录认证装饰器

from django.db.models import Avg, Max, Min, Count
from django.db.models.functions import TruncMonth
from blog import models

from blog.models import UserInfo
from blog.myforms import RegForm  # 导入自定义forms验证规则


def reg(request):
    """用户注册视图"""
    if request.is_ajax():
        form = RegForm(request.POST)
        respone = {'user': None, 'msg': None}
        if form.is_valid():
            respone['user'] = form.cleaned_data.get('user')
            user = form.cleaned_data.get('user')
            pwd = form.cleaned_data.get('pwd')
            email = form.cleaned_data.get('email')
            avatar_obj = request.FILES.get('avatar')
            extra = {}
            if avatar_obj:
                extra['avatar'] = avatar_obj
            user_obj = UserInfo.objects.create_user(username=user, password=pwd, email=email, **extra)
        else:
            respone['msg'] = form.errors
        return JsonResponse(respone)

    form = RegForm()  # 带入规则
    return render(request, 'reg.html', {"form": form})


def login(request):
    if request.method == "POST":
        response = {'user': None, 'msg': None}  # 构造登录提示信息
        user = request.POST.get('user')
        pwd = request.POST.get('pwd')
        valid_code = request.POST.get('valid_code')  # 获取用户输入的验证码
        valid_code_str = request.session.get('valid_code')  # 从session中提取自动生成的验证码字符串
        if valid_code_str.upper() == valid_code.upper():  # 全部转为大写字母
            user = auth.authenticate(username=user, password=pwd)
            if user:
                auth.login(request, user)  # yanz 成功在session中注册当前用户信息, 就有了request.user全局变量
                response['user'] = user.username  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.
            else:
                response['msg'] = '用户名或密码错误!'
        else:
            response['msg'] = '验证码错误!'
        return JsonResponse(response)  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.

    return render(request, 'login.html')


def get_validCode_img(request):
    """生成验证码图片"""
    from blog.utils.validCode import get_validCode_img  # 导入自定义的生成验证码功能函数
    data = get_validCode_img(request)
    return HttpResponse(data)


def index(request):
    article_list = models.Article.objects.all()
    return render(request, 'index.html', {'article_list': article_list})


def logout(request):
    auth.logout(request)
    return redirect('/login/')


def home_site(request, username, **kwargs):
    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')

    # 查询username用户的blog对象
    blog = user.blog
    article_list = models.Article.objects.filter(user=user)
    if kwargs:
        # 个人站点跳转
        condition = kwargs.get('condition')  # 使用**kwargs目的是可以接受任意多个参数
        param = kwargs.get('param')
        if condition == 'tag':
            article_list = article_list.filter(tags__title=param)
            # print(article_list)
        elif condition == 'category':
            article_list = article_list.filter(category__title=param)
        else:
            # 日期归档
            year, month = param.split('-')
            article_list = article_list.filter(create_time__year=year, create_time__month=month)

    return render(request, 'home_site.html', {'username': username, 'blog': blog, 'article_list': article_list})


def article_detail(request, username, article_id):
    """
    文章详情页视图
    http://127.0.0.1:8000/papa/article/2/
    """

    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')
    # 查询username用户的blog对象
    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, 'article_detail.html', locals())  # locals()不能丢


def digg(request):
    print(request.user)
    import json
    article_id = request.POST.get('article_id')
    is_up = json.loads(request.POST.get('is_up'))  # 传递过来的是"True"字符串, 所以这里需要反序列化为布尔值
    user_id = request.user.pk  # 直接获取user_id,没有必要通过客户端传送

    # 判断是否已经点过赞
    response = {'state': True}  # 返回的状态,将传递给模板中的回调函数,默认是True成功, 表示为点过赞或者反对.
    ret_obj = models.ArticleUpDown.objects.filter(user_id=user_id, article_id=article_id).first()
    if not ret_obj:  # 没点过赞
        ard = models.ArticleUpDown.objects.create(user_id=user_id, article_id=article_id, is_up=is_up)
        # 将点赞写入数据库中

        article_obj = models.Article.objects.filter(pk=article_id)
        if is_up:
            article_obj.update(up_count=F("up_count") + 1)
        else:
            article_obj.update(down_count=F("down_count") + 1)
    else:  # 已经点过赞或反对了
        response['state'] = False  # 表示失败
        response['handled'] = ret_obj.is_up  # 获取当前数据库中是点赞过还是点反对过.

    return JsonResponse(response)  # 将状态传递给模板中的回调函数


def comment(request):
    """
    发布评论
    :param request:
    :return:
    """
    # 前端ajax发送数据, 这里予以获取
    article_id = request.POST.get('article_id')
    content = request.POST.get('content')
    pid = request.POST.get('parent_comment')

    # 从当前用户登录信息中获取
    user_id = request.user.pk
    print("pid:", pid)

    with transaction.atomic():  # 事务同步, 保证评论内容写入comment表,同时,将article表中的评论数计数器加1
        # 写入到数据库comment表
        comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content,                                                    parent_comment_id=pid)
        # 同步写入评论数加1的数据到文章表中
        models.Article.objects.filter(pk=article_id).update(comment_count=F('comment_count')+1)

    # 从数据库提取刚刚存入的数据, 为啥非要从数据库中立即再次取出, 不能直接在前端提交时,在前端直接取数据吗?
    create_time = comment_obj.create_time.strftime('%Y-%m-%d %X')
    content = comment_obj.content
    username = request.user.username
    response = {}
    response["create_time"] = create_time
    response['content'] = content
    response['username'] = username

    # 如果是子评论,则还需要获取其父评论的内容,姓名和评论内容
    if pid:
        parent_obj = models.Comment.objects.filter(pk=pid).first()
        response['p_username'] = parent_obj.user.username
        response['p_content'] = parent_obj.content


    # 发送邮件通知文章作者,你的文章有了新的评论
    from django.core.mail import send_mail  # 发送邮件模块
    from cnblog import settings  # 导入配置
    import threading  # 为了提高效率, 另开一个线程发送邮件
    article_obj = models.Article.objects.filter(pk=article_id).first()  # 获取当前文章对象
    email = article_obj.user.email  # 获取文章的作者的邮箱
    print(email, type(email))
    # send_mail(
    #     '您的文章%s新增了一条评论内容' % article_obj.title,
    #     '评论内容:%s' % content,
    #     settings.EMAIL_HOST_USER,  # 配置发送邮件的邮箱账户, 如果在settings中配置了默认邮箱,则这里可以不写
    #     [email, '123456789@qq.com'],  # 作者邮箱列表
    # )
    # 开多线程,提高效率
    sendMail = threading.Thread(target=send_mail, args=(
        '您的文章%s新增了一条评论内容' % article_obj.title,
        '评论内容:%s' % content,
        settings.EMAIL_HOST_USER,
        [email, '123456789@qq.com'],
    ))
    sendMail.start()  # 多线程千万别忘了这一步.这里才是真正的执行发送任务.上面是配置.


    return JsonResponse(response)


def get_comment_tree(request):

    article_id = request.POST.get('article_id')
    # list强制将querset类型转换为list,然后再序列化. values中是取需要的字段
    ret = list(models.Comment.objects.filter(article_id=article_id).order_by('pk').values('pk', 'content', 'parent_comment_id'))
    print(ret)
    return JsonResponse(ret, safe=False)  # 非字典序列化必须加上safe=False参数


@login_required
def backed_index(request):
    """管理后台"""
    user_id = request.user.pk
    article_list = models.Article.objects.filter(user_id=user_id)

    return render(request, 'backed_index.html', locals())


@login_required
def add_article(request):
    """新增文章"""

    return render(request, 'add_article.html', locals())
views.py
"""cnblog URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/3.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, re_path
from django.views.static import serve  # serve器

from blog import views
from cnblog import settings

urlpatterns = [
    path('admin/', admin.site.urls),
    path('login/', views.login),
    path('index/', views.index),
    path('logout/', views.logout),
    path('get_validCode_img/', views.get_validCode_img),
    path('reg/', views.reg),
    path('digg/', views.digg),  # 点赞
    path('comment/', views.comment),  # 评论
    path('get_comment_tree/', views.get_comment_tree),  # 获取文章的树形结构评论
    path('backed_index/', views.backed_index),  # 获取文章的树形结构评论
    path('add_article/', views.add_article),  # 获取文章的树形结构评论

    # media配置, 外网直接访问用户上传文件的media文件夹的路由
    re_path(r'media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),  # 固定写法

    # 个人站点页面
    re_path(r'^(?P<username>\w+)/$', views.home_site),

    # 个人站点页面跳转
    re_path(r'^(?P<username>\w+)/(?P<condition>tag|category|archive)/(?P<param>.*)/$', views.home_site),

    # 个人站点文章详情页面
    re_path(r'^(?P<username>\w+)/article/(?P<article_id>\d+)/$', views.article_detail),

    #

]
urls.py
"""
Django settings for cnblog project.

Generated by 'django-admin startproject' using Django 3.1.7.

For more information on this file, see
https://docs.djangoproject.com/en/3.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""

from pathlib import Path
import os

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '43@l3&_w6qq@q3@$5u8gpzbz7ti_dgd2ppwcse%m8@t#zn*l#j'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog.apps.BlogConfig',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'cnblog.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')]
        ,
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'cnblog.wsgi.application'


# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}


# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = False  # 将评论时间调整为东八区


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/

STATIC_URL = '/static/'
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static')
]

AUTH_USER_MODEL = 'blog.UserInfo'

# 配置用户上传的文件存放位置, 项目根目录下的media目录. 当有了此配置后,Django会将用户上传的文件存储在此.
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'  # 设置后用于可在外网直接通过url访问MEDIA_ROOT设置的文件夹


# 配置邮件发送
EMAIL_HOST = 'smtp.qq.com'  # 如果是163 改成 smtp.163.com
EMAIL_PORT = 465  # 163端口25
EMAIL_HOST_USER = '407928668@qq.com'  # 发件账户
EMAIL_HOST_PASSWORD = "jjucyxowtmmlbgbd"  # 密码,客户端授权码, 不是邮箱密码
# DEFAULT_FORM_EMAIL = EMAIL_HOST_USER  # 配置默认发件账户
EMAIL_USE_SSL = True  # ssl证书支持

LOGIN_URL = '/login/'  # 配合登录验证装饰器, 设置跳转的路径
settings.py

76.4 单选,多选数据的添加保存到数据库, 文章分类和标签

{% extends 'backed_base.html' %}


{% block backed_content %}
    <h2>新增{{ request.user.username }}的文章</h2>
    <hr>
    <div class="container">
        <div class="row">
            <div class="col-md-9">
                <form action="" method="post">
                    {% csrf_token %}
                    <div class="form-group">
                        <label for="title">文章标题</label>
                        <input type="text" id="title" name="title" class="form-control">
                    </div>
                    <div class="form-group">
                        <label for="desc">文章描述</label>
                        <input type="text" id="desc" name="desc" class="form-control">
                    </div>
                    <div class="form-group">
                        <label>文章所属分类</label>
                        {% for category in category_list %}
                            <label class="radio-inline">
                                <input type="radio" name="category_id" value="{{ category.pk }}"> {{ category.title }}
                            </label>
                        {% endfor %}
                    </div>
                    <div class="form-group">
                        <label>标签(关键字)</label>
                        {% for tag in tag_list %}
                            <label class="checkbox-inline">
                                <input type="checkbox" name='tag_id_list' value="{{ tag.pk }}"> {{ tag.title }}
                            </label>
                        {% endfor %}
                    </div>
                    <div class="form-group">
                        <label for="content">文章详细内容</label>
                        <textarea cols="30" id="content" name="content" class="form-control"></textarea>
                    </div>
                    <div class="form-group">
                        <input type="submit" class="btn btn-success">
                    </div>
                </form>
            </div>
        </div>

    </div>
    <script charset="utf-8" src="/static/kindeditor/kindeditor-all.js"></script>
    <script charset="utf-8" src="/static/kindeditor/lang/zh-CN.js"></script>
    <script>
        KindEditor.ready(function (K) {
            {# content是上面textarea标签的ID值 #}
            window.editor = K.create('#content', {
                height: '500px',
            });
        });
    </script>



{% endblock %}
add_article.html
from django.shortcuts import render, HttpResponse, redirect
from django.http import JsonResponse  # 传递json数据类型
from django.contrib import auth  # 导入用户组件,用于auth_user表的操作
from django.db.models import F  # 数据库表中自加1
from django.db import transaction  # 事务操作(用于保证多个数据的同步一致)
from django.contrib.auth.decorators import login_required  # 登录认证装饰器

from django.db.models import Avg, Max, Min, Count
from django.db.models.functions import TruncMonth
from blog import models

from blog.models import UserInfo
from blog.myforms import RegForm  # 导入自定义forms验证规则


def reg(request):
    """用户注册视图"""
    if request.is_ajax():
        form = RegForm(request.POST)
        respone = {'user': None, 'msg': None}
        if form.is_valid():
            respone['user'] = form.cleaned_data.get('user')
            user = form.cleaned_data.get('user')
            pwd = form.cleaned_data.get('pwd')
            email = form.cleaned_data.get('email')
            avatar_obj = request.FILES.get('avatar')
            extra = {}
            if avatar_obj:
                extra['avatar'] = avatar_obj
            user_obj = UserInfo.objects.create_user(username=user, password=pwd, email=email, **extra)
        else:
            respone['msg'] = form.errors
        return JsonResponse(respone)

    form = RegForm()  # 带入规则
    return render(request, 'reg.html', {"form": form})


def login(request):
    if request.method == "POST":
        response = {'user': None, 'msg': None}  # 构造登录提示信息
        user = request.POST.get('user')
        pwd = request.POST.get('pwd')
        valid_code = request.POST.get('valid_code')  # 获取用户输入的验证码
        valid_code_str = request.session.get('valid_code')  # 从session中提取自动生成的验证码字符串
        if valid_code_str.upper() == valid_code.upper():  # 全部转为大写字母
            user = auth.authenticate(username=user, password=pwd)
            if user:
                auth.login(request, user)  # yanz 成功在session中注册当前用户信息, 就有了request.user全局变量
                response['user'] = user.username  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.
            else:
                response['msg'] = '用户名或密码错误!'
        else:
            response['msg'] = '验证码错误!'
        return JsonResponse(response)  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.

    return render(request, 'login.html')


def get_validCode_img(request):
    """生成验证码图片"""
    from blog.utils.validCode import get_validCode_img  # 导入自定义的生成验证码功能函数
    data = get_validCode_img(request)
    return HttpResponse(data)


def index(request):
    article_list = models.Article.objects.all()
    return render(request, 'index.html', {'article_list': article_list})


def logout(request):
    auth.logout(request)
    return redirect('/login/')


def home_site(request, username, **kwargs):
    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')

    # 查询username用户的blog对象
    blog = user.blog
    article_list = models.Article.objects.filter(user=user)
    if kwargs:
        # 个人站点跳转
        condition = kwargs.get('condition')  # 使用**kwargs目的是可以接受任意多个参数
        param = kwargs.get('param')
        if condition == 'tag':
            article_list = article_list.filter(tags__title=param)
            # print(article_list)
        elif condition == 'category':
            article_list = article_list.filter(category__title=param)
        else:
            # 日期归档
            year, month = param.split('-')
            article_list = article_list.filter(create_time__year=year, create_time__month=month)

    return render(request, 'home_site.html', {'username': username, 'blog': blog, 'article_list': article_list})


def article_detail(request, username, article_id):
    """
    文章详情页视图
    http://127.0.0.1:8000/papa/article/2/
    """

    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')
    # 查询username用户的blog对象
    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, 'article_detail.html', locals())  # locals()不能丢


def digg(request):
    print(request.user)
    import json
    article_id = request.POST.get('article_id')
    is_up = json.loads(request.POST.get('is_up'))  # 传递过来的是"True"字符串, 所以这里需要反序列化为布尔值
    user_id = request.user.pk  # 直接获取user_id,没有必要通过客户端传送

    # 判断是否已经点过赞
    response = {'state': True}  # 返回的状态,将传递给模板中的回调函数,默认是True成功, 表示为点过赞或者反对.
    ret_obj = models.ArticleUpDown.objects.filter(user_id=user_id, article_id=article_id).first()
    if not ret_obj:  # 没点过赞
        ard = models.ArticleUpDown.objects.create(user_id=user_id, article_id=article_id, is_up=is_up)
        # 将点赞写入数据库中

        article_obj = models.Article.objects.filter(pk=article_id)
        if is_up:
            article_obj.update(up_count=F("up_count") + 1)
        else:
            article_obj.update(down_count=F("down_count") + 1)
    else:  # 已经点过赞或反对了
        response['state'] = False  # 表示失败
        response['handled'] = ret_obj.is_up  # 获取当前数据库中是点赞过还是点反对过.

    return JsonResponse(response)  # 将状态传递给模板中的回调函数


def comment(request):
    """
    发布评论
    :param request:
    :return:
    """
    # 前端ajax发送数据, 这里予以获取
    article_id = request.POST.get('article_id')
    content = request.POST.get('content')
    pid = request.POST.get('parent_comment')

    # 从当前用户登录信息中获取
    user_id = request.user.pk
    print("pid:", pid)

    with transaction.atomic():  # 事务同步, 保证评论内容写入comment表,同时,将article表中的评论数计数器加1
        # 写入到数据库comment表
        comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content,
                                                    parent_comment_id=pid)
        # 同步写入评论数加1的数据到文章表中
        models.Article.objects.filter(pk=article_id).update(comment_count=F('comment_count') + 1)

    # 从数据库提取刚刚存入的数据, 为啥非要从数据库中立即再次取出, 不能直接在前端提交时,在前端直接取数据吗?
    create_time = comment_obj.create_time.strftime('%Y-%m-%d %X')
    content = comment_obj.content
    username = request.user.username
    response = {}
    response["create_time"] = create_time
    response['content'] = content
    response['username'] = username

    # 如果是子评论,则还需要获取其父评论的内容,姓名和评论内容
    if pid:
        parent_obj = models.Comment.objects.filter(pk=pid).first()
        response['p_username'] = parent_obj.user.username
        response['p_content'] = parent_obj.content

    # 发送邮件通知文章作者,你的文章有了新的评论
    from django.core.mail import send_mail  # 发送邮件模块
    from cnblog import settings  # 导入配置
    import threading  # 为了提高效率, 另开一个线程发送邮件
    article_obj = models.Article.objects.filter(pk=article_id).first()  # 获取当前文章对象
    email = article_obj.user.email  # 获取文章的作者的邮箱
    print(email, type(email))
    # send_mail(
    #     '您的文章%s新增了一条评论内容' % article_obj.title,
    #     '评论内容:%s' % content,
    #     settings.EMAIL_HOST_USER,  # 配置发送邮件的邮箱账户, 如果在settings中配置了默认邮箱,则这里可以不写
    #     [email, '123456789@qq.com'],  # 作者邮箱列表
    # )
    # 开多线程,提高效率
    sendMail = threading.Thread(target=send_mail, args=(
        '您的文章%s新增了一条评论内容' % article_obj.title,
        '评论内容:%s' % content,
        settings.EMAIL_HOST_USER,
        [email, '123456789@qq.com'],
    ))
    sendMail.start()  # 多线程千万别忘了这一步.这里才是真正的执行发送任务.上面是配置.

    return JsonResponse(response)


def get_comment_tree(request):
    article_id = request.POST.get('article_id')
    # list强制将querset类型转换为list,然后再序列化. values中是取需要的字段
    ret = list(models.Comment.objects.filter(article_id=article_id).order_by('pk').values('pk', 'content',
                                                                                          'parent_comment_id'))
    print(ret)
    return JsonResponse(ret, safe=False)  # 非字典序列化必须加上safe=False参数


@login_required
def backed_index(request):
    """管理后台"""
    user_id = request.user.pk
    article_list = models.Article.objects.filter(user_id=user_id)

    return render(request, 'backed_index.html', locals())


@login_required
def add_article(request):
    """新增文章"""
    category_list = models.Category.objects.filter(blog__userinfo__nid=request.user.pk).values('pk', 'title')
    tag_list = models.Tag.objects.filter(blog__userinfo__nid=request.user.pk).values('pk', 'title')
    if request.method == 'POST':
        title = request.POST.get('title')
        desc = request.POST.get('desc')
        content = request.POST.get('content')
        category_id = request.POST.get('category_id')
        print(category_id)
        tag_id_list = request.POST.getlist('tag_id_list')  # 多选取值
        print(tag_id_list)
        article_obj = models.Article.objects.create(title=title, desc=desc, content=content, user_id=request.user.pk,
                                      category_id=category_id)
        # 多对多的article2tag表数据添加, 文章添加关键字
        article_obj.tags.add(*tag_id_list)
        return redirect('/backed_index/')

    return render(request, 'add_article.html', locals())
views.py

 

 七七. 博客系统之后台管理的编辑器引入和参数

 77.1 效果, kineditor富文本编辑器, 参考网站:http://kindeditor.net/docs/usage.html

 

 77.2 关键代码

    <script>
        KindEditor.ready(function (K) {
            {# content是上面textarea标签的ID值 #}
            window.editor = K.create('#content',{
                height: '500px',  {# 配置编辑器的参数 #}
            });
        });
    </scrip

  配置编辑器:编辑器的工具栏配置http://kindeditor.net/docs/option.html#items

七八  七九. 博客系统之文本编辑器的上传功能  上传图片

from django.views.decorators.clickjacking import xframe_options_sameorigin  # 富文本编辑器上传图片,一直卡,需要这个模块

@xframe_options_sameorigin  # 富文本编辑器上传图片一直卡,需要使用这个装饰器
def upload(request):
    print(request.FILES)
    img = request.FILES.get('upload_img')  # <MultiValueDict: {'upload_img': [<InMemoryUploadedFile: Jellyfish.jpg (image/jpeg)>]}>
    print(img.name)
    # 读取文件对象,写入服务器指定位置
    import os
    path = os.path.join(settings.MEDIA_ROOT, 'add_article_img', img.name)
    with open(path, 'wb') as f:
        for line in img:
            f.write(line)

    # 将上传图片的数据传递给模板的富文本编辑器
    response={
        'error': 0,  # 0没有任何错误
        'url': '/media/add_article_img%s' % img.name,  # 通过网络可以访问该图片的路径
    }

    return HttpResponse(JsonResponse(response))  # 注意这里的传递方法
    <script charset="utf-8" src="/static/kindeditor/kindeditor-all.js"></script>
    <script>
        KindEditor.ready(function (K) {
            {# article-content是上面textarea标签的ID值 #}
            window.editor = K.create('#article-content', {
                height: '500px',
                {# 文件上传路由配置   #}
                uploadJson: "/upload/",
                extraFileUploadParams: {
                    csrfmiddlewaretoken: $('[name="csrfmiddlewaretoken"]').val()
                },
                {# 指定上传文件对象的键的名称 <MultiValueDict: {'upload_img': [<InMemoryUploadedFile: Hydrangeas.jpg (image/jpeg)>]}> #}
                filePostName: 'upload_img'
            });
        });
    </script>

 

from django.shortcuts import render, HttpResponse, redirect
from django.http import JsonResponse  # 传递json数据类型
from django.contrib import auth  # 导入用户组件,用于auth_user表的操作
from django.db.models import F  # 数据库表中自加1
from django.db import transaction  # 事务操作(用于保证多个数据的同步一致)
from django.contrib.auth.decorators import login_required  # 登录认证装饰器
from cnblog import settings  # 导入配置
from django.db.models import Avg, Max, Min, Count
from django.db.models.functions import TruncMonth
from blog import models

from blog.models import UserInfo
from blog.myforms import RegForm  # 导入自定义forms验证规则


def reg(request):
    """用户注册视图"""
    if request.is_ajax():
        form = RegForm(request.POST)
        respone = {'user': None, 'msg': None}
        if form.is_valid():
            respone['user'] = form.cleaned_data.get('user')
            user = form.cleaned_data.get('user')
            pwd = form.cleaned_data.get('pwd')
            email = form.cleaned_data.get('email')
            avatar_obj = request.FILES.get('avatar')
            extra = {}
            if avatar_obj:
                extra['avatar'] = avatar_obj
            user_obj = UserInfo.objects.create_user(username=user, password=pwd, email=email, **extra)
        else:
            respone['msg'] = form.errors
        return JsonResponse(respone)

    form = RegForm()  # 带入规则
    return render(request, 'reg.html', {"form": form})


def login(request):
    if request.method == "POST":
        response = {'user': None, 'msg': None}  # 构造登录提示信息
        user = request.POST.get('user')
        pwd = request.POST.get('pwd')
        valid_code = request.POST.get('valid_code')  # 获取用户输入的验证码
        valid_code_str = request.session.get('valid_code')  # 从session中提取自动生成的验证码字符串
        if valid_code_str.upper() == valid_code.upper():  # 全部转为大写字母
            user = auth.authenticate(username=user, password=pwd)
            if user:
                auth.login(request, user)  # yanz 成功在session中注册当前用户信息, 就有了request.user全局变量
                response['user'] = user.username  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.
            else:
                response['msg'] = '用户名或密码错误!'
        else:
            response['msg'] = '验证码错误!'
        return JsonResponse(response)  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.

    return render(request, 'login.html')


def get_validCode_img(request):
    """生成验证码图片"""
    from blog.utils.validCode import get_validCode_img  # 导入自定义的生成验证码功能函数
    data = get_validCode_img(request)
    return HttpResponse(data)


def index(request):
    article_list = models.Article.objects.all()
    return render(request, 'index.html', {'article_list': article_list})


def logout(request):
    auth.logout(request)
    return redirect('/login/')


def home_site(request, username, **kwargs):
    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')

    # 查询username用户的blog对象
    blog = user.blog
    article_list = models.Article.objects.filter(user=user)
    if kwargs:
        # 个人站点跳转
        condition = kwargs.get('condition')  # 使用**kwargs目的是可以接受任意多个参数
        param = kwargs.get('param')
        if condition == 'tag':
            article_list = article_list.filter(tags__title=param)
            # print(article_list)
        elif condition == 'category':
            article_list = article_list.filter(category__title=param)
        else:
            # 日期归档
            year, month = param.split('-')
            article_list = article_list.filter(create_time__year=year, create_time__month=month)

    return render(request, 'home_site.html', {'username': username, 'blog': blog, 'article_list': article_list})


def article_detail(request, username, article_id):
    """
    文章详情页视图
    http://127.0.0.1:8000/papa/article/2/
    """

    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')
    # 查询username用户的blog对象
    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, 'article_detail.html', locals())  # locals()不能丢


def digg(request):
    print(request.user)
    import json
    article_id = request.POST.get('article_id')
    is_up = json.loads(request.POST.get('is_up'))  # 传递过来的是"True"字符串, 所以这里需要反序列化为布尔值
    user_id = request.user.pk  # 直接获取user_id,没有必要通过客户端传送

    # 判断是否已经点过赞
    response = {'state': True}  # 返回的状态,将传递给模板中的回调函数,默认是True成功, 表示为点过赞或者反对.
    ret_obj = models.ArticleUpDown.objects.filter(user_id=user_id, article_id=article_id).first()
    if not ret_obj:  # 没点过赞
        ard = models.ArticleUpDown.objects.create(user_id=user_id, article_id=article_id, is_up=is_up)
        # 将点赞写入数据库中

        article_obj = models.Article.objects.filter(pk=article_id)
        if is_up:
            article_obj.update(up_count=F("up_count") + 1)
        else:
            article_obj.update(down_count=F("down_count") + 1)
    else:  # 已经点过赞或反对了
        response['state'] = False  # 表示失败
        response['handled'] = ret_obj.is_up  # 获取当前数据库中是点赞过还是点反对过.

    return JsonResponse(response)  # 将状态传递给模板中的回调函数


def comment(request):
    """
    发布评论
    :param request:
    :return:
    """
    # 前端ajax发送数据, 这里予以获取
    article_id = request.POST.get('article_id')
    content = request.POST.get('content')
    pid = request.POST.get('parent_comment')

    # 从当前用户登录信息中获取
    user_id = request.user.pk
    print("pid:", pid)

    with transaction.atomic():  # 事务同步, 保证评论内容写入comment表,同时,将article表中的评论数计数器加1
        # 写入到数据库comment表
        comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content,
                                                    parent_comment_id=pid)
        # 同步写入评论数加1的数据到文章表中
        models.Article.objects.filter(pk=article_id).update(comment_count=F('comment_count') + 1)

    # 从数据库提取刚刚存入的数据, 为啥非要从数据库中立即再次取出, 不能直接在前端提交时,在前端直接取数据吗?
    create_time = comment_obj.create_time.strftime('%Y-%m-%d %X')
    content = comment_obj.content
    username = request.user.username
    response = {}
    response["create_time"] = create_time
    response['content'] = content
    response['username'] = username

    # 如果是子评论,则还需要获取其父评论的内容,姓名和评论内容
    if pid:
        parent_obj = models.Comment.objects.filter(pk=pid).first()
        response['p_username'] = parent_obj.user.username
        response['p_content'] = parent_obj.content

    # 发送邮件通知文章作者,你的文章有了新的评论
    from django.core.mail import send_mail  # 发送邮件模块

    import threading  # 为了提高效率, 另开一个线程发送邮件
    article_obj = models.Article.objects.filter(pk=article_id).first()  # 获取当前文章对象
    email = article_obj.user.email  # 获取文章的作者的邮箱
    print(email, type(email))
    # send_mail(
    #     '您的文章%s新增了一条评论内容' % article_obj.title,
    #     '评论内容:%s' % content,
    #     settings.EMAIL_HOST_USER,  # 配置发送邮件的邮箱账户, 如果在settings中配置了默认邮箱,则这里可以不写
    #     [email, '123456789@qq.com'],  # 作者邮箱列表
    # )
    # 开多线程,提高效率
    sendMail = threading.Thread(target=send_mail, args=(
        '您的文章%s新增了一条评论内容' % article_obj.title,
        '评论内容:%s' % content,
        settings.EMAIL_HOST_USER,
        [email, '123456789@qq.com'],
    ))
    sendMail.start()  # 多线程千万别忘了这一步.这里才是真正的执行发送任务.上面是配置.

    return JsonResponse(response)


def get_comment_tree(request):
    article_id = request.POST.get('article_id')
    # list强制将querset类型转换为list,然后再序列化. values中是取需要的字段
    ret = list(models.Comment.objects.filter(article_id=article_id).order_by('pk').values('pk', 'content',
                                                                                          'parent_comment_id'))
    print(ret)
    return JsonResponse(ret, safe=False)  # 非字典序列化必须加上safe=False参数


@login_required
def backed_index(request):
    """管理后台"""
    user_id = request.user.pk
    article_list = models.Article.objects.filter(user_id=user_id)

    return render(request, 'backed_index.html', locals())


@login_required
def add_article(request):
    """新增文章"""
    category_list = models.Category.objects.filter(blog__userinfo__nid=request.user.pk).values('pk', 'title')
    tag_list = models.Tag.objects.filter(blog__userinfo__nid=request.user.pk).values('pk', 'title')
    if request.method == 'POST':
        title = request.POST.get('title')
        desc = request.POST.get('desc')
        content = request.POST.get('content')
        category_id = request.POST.get('category_id')
        print(category_id)
        tag_id_list = request.POST.getlist('tag_id_list')  # 多选取值
        print(tag_id_list)
        article_obj = models.Article.objects.create(title=title, desc=desc, content=content, user_id=request.user.pk,
                                      category_id=category_id)
        # 多对多的article2tag表数据添加, 文章添加关键字
        article_obj.tags.add(*tag_id_list)
        return redirect('/backed_index/')

    return render(request, 'add_article.html', locals())


@login_required
def upload(request):
    print(request.FILES)
    img = request.FILES.get('upload_img')  # <MultiValueDict: {'upload_img': [<InMemoryUploadedFile: Jellyfish.jpg (image/jpeg)>]}>
    print(img.name)
    # 读取文件对象,写入服务器指定位置
    import os
    path = os.path.join(settings.MEDIA_ROOT, 'add_article_img', img.name)
    with open(path, 'wb') as f:
        for line in img:
            f.write(line)

    # 将上传图片的数据传递给模板的富文本编辑器
    response={
        'error': 0,  # 0没有任何错误
        'url': '/media/add_article_img%s' % img.name,  # 通过网络可以访问该图片的路径
    }

    return HttpResponse(JsonResponse(response))  # 注意这里的传递方法
views.py
"""cnblog URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/3.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, re_path
from django.views.static import serve  # serve器

from blog import views
from cnblog import settings

urlpatterns = [
    path('admin/', admin.site.urls),
    path('login/', views.login),
    path('index/', views.index),
    path('logout/', views.logout),
    path('get_validCode_img/', views.get_validCode_img),
    path('reg/', views.reg),
    path('digg/', views.digg),  # 点赞
    path('comment/', views.comment),  # 评论
    path('get_comment_tree/', views.get_comment_tree),  # 获取文章的树形结构评论
    path('backed_index/', views.backed_index),  # 获取文章的树形结构评论
    path('add_article/', views.add_article),  # 获取文章的树形结构评论
    path('upload/', views.upload),  # 文件上传处理

    # media配置, 外网直接访问用户上传文件的media文件夹的路由
    re_path(r'media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),  # 固定写法

    # 个人站点页面
    re_path(r'^(?P<username>\w+)/$', views.home_site),

    # 个人站点页面跳转
    re_path(r'^(?P<username>\w+)/(?P<condition>tag|category|archive)/(?P<param>.*)/$', views.home_site),

    # 个人站点文章详情页面
    re_path(r'^(?P<username>\w+)/article/(?P<article_id>\d+)/$', views.article_detail),

    #

]
urls.py
{% extends 'backed_base.html' %}


{% block backed_content %}
    <h2>新增{{ request.user.username }}的文章</h2>
    <hr>
    <div class="container">
        <div class="row">
            <div class="col-md-9">
                <form action="" method="post">
                    {% csrf_token %}
                    <div class="form-group">
                        <label for="title">文章标题</label>
                        <input type="text" id="title" name="title" class="form-control">
                    </div>
                    <div class="form-group">
                        <label for="desc">文章描述</label>
                        <input type="text" id="desc" name="desc" class="form-control">
                    </div>
                    <div class="form-group">
                        <label>文章所属分类</label>
                        {% for category in category_list %}
                            <label class="radio-inline">
                                <input type="radio" name="category_id" value="{{ category.pk }}"> {{ category.title }}
                            </label>
                        {% endfor %}
                    </div>
                    <div class="form-group">
                        <label>标签(关键字)</label>
                        {% for tag in tag_list %}
                            <label class="checkbox-inline">
                                <input type="checkbox" name='tag_id_list' value="{{ tag.pk }}"> {{ tag.title }}
                            </label>
                        {% endfor %}
                    </div>
                    <div class="form-group">
                        <label for="content">文章详细内容</label>
                        <textarea cols="30" id="article-content" name="content" class="form-control"></textarea>
                    </div>
                    <div class="form-group">
                        <input type="submit" class="btn btn-success">
                    </div>
                </form>
            </div>
        </div>

    </div>
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
    <script charset="utf-8" src="/static/kindeditor/kindeditor-all.js"></script>
    <script>
        KindEditor.ready(function (K) {
            {# article-content是上面textarea标签的ID值 #}
            window.editor = K.create('#article-content', {
                height: '500px',
                {# 文件上传路由配置   #}
                uploadJson: "/upload/",
                extraFileUploadParams: {
                    csrfmiddlewaretoken: $('[name="csrfmiddlewaretoken"]').val()
                },
                {# 指定上传文件对象的键的名称 <MultiValueDict: {'upload_img': [<InMemoryUploadedFile: Hydrangeas.jpg (image/jpeg)>]}> #}
                filePostName: 'upload_img'
            });
        });
    </script>



{% endblock %}
add_article.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>网站管理后台</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css"
          integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <link rel="stylesheet" href="/static/css/home_site.css" type="text/css" charset="UTF-8">
    <link rel="stylesheet" href="/static/css/article_detail.css" type="text/css" charset="UTF-8">
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
    <style>
        .header .backend{
            color: white;
            font-size: 14px;
            padding-right: 10px;
        }
        .header .backend a{
            color: white;
        }
    </style>
</head>

<body>
<div class="header">
    <div class="content clearfix">
        <span class="title">网站管理后台</span>
        <div class="backend pull-right">
            <span class="glyphicon glyphicon-user"></span>
            <span>{{ request.user.username }}</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
            <a href="/logout/">注销</a>
        </div>
    </div>
</div>
<div class="container">
    <div class="row">
        <div class="col-md-2">
            <div class="panel panel-default text-left">
              <div class="panel-heading">&nbsp;&nbsp;&nbsp;&nbsp;</div>
              <div class="panel-body">
                  <div><span class="glyphicon glyphicon-circle-arrow-right"></span>&nbsp;&nbsp;<a href="/add_article/">添加文章</a></div>
                  <hr>
                  <div><span class="glyphicon glyphicon-circle-arrow-right"></span>&nbsp;&nbsp;<a href="/backed_index/">所有文章</a></div>
                  <hr>
                  <div><span class="glyphicon glyphicon-circle-arrow-right"></span>&nbsp;&nbsp;<a href="">新增文章分类</a></div>
                  <hr>
                  <div><span class="glyphicon glyphicon-circle-arrow-right"></span>&nbsp;&nbsp;<a href="">新增标签关键字</a></div>
                  <hr>
                  <div><span class="glyphicon glyphicon-circle-arrow-right"></span>&nbsp;&nbsp;<a href="">修改密码</a></div>
                  <hr>
                  <div><span class="glyphicon glyphicon-circle-arrow-right"></span>&nbsp;&nbsp;<a href="">修改头像</a></div>
              </div>
            </div>

        </div>
        <div class="col-md-10">
            {% block backed_content %}
                {#      预留盒子, 进行扩展      #}
            {% endblock %}
        </div>
    </div>
</div>

</body>
</html>
backed_base.html

 八零. 博客系统之文章摘要的保存

80.1 自动截取文章生成摘要.  一般摘要只需要纯文字.  这样会产生问题: 当文章内容中包含html标签时,会把标签当成文字截取,造成摘要显示出问题.

desc = content[0:150]  # 直接从内容中截取文字,当摘要用.

 八一. 博客系统之bs4的简单应用

81.1 通过bs4将含有html标签的文章内容提取出全部的纯文字.所有的标签被遗弃.

        from bs4 import BeautifulSoup
        soup = BeautifulSoup(content, 'html.parser')
        desc = soup.text[0:150]  # 直接从内容中截取文字,当摘要用.

 八二. 博客系统之基于bs4模块防御xss攻击

82.1 过滤文章中的script标签

        from bs4 import BeautifulSoup
        soup = BeautifulSoup(content, 'html.parser')
        # 过滤有害的script标签
        for tag in soup.find_all():  # 遍历所有标签
            if tag.name == 'script':
                tag.decompose()  # 删除script
        desc = str(soup.text[0:150])  # 直接从内容中截取文字,当摘要用.  过滤过后需要用str()包裹起来.
from django.shortcuts import render, HttpResponse, redirect
from django.http import JsonResponse  # 传递json数据类型
from django.contrib import auth  # 导入用户组件,用于auth_user表的操作
from django.db.models import F  # 数据库表中自加1
from django.db import transaction  # 事务操作(用于保证多个数据的同步一致)
from django.contrib.auth.decorators import login_required  # 登录认证装饰器
from cnblog import settings  # 导入配置
from django.views.decorators.clickjacking import xframe_options_sameorigin  # 富文本编辑器上传图片,一直卡,需要这个模块
from django.db.models import Avg, Max, Min, Count
from django.db.models.functions import TruncMonth
from blog import models

from blog.models import UserInfo
from blog.myforms import RegForm  # 导入自定义forms验证规则


def reg(request):
    """用户注册视图"""
    if request.is_ajax():
        form = RegForm(request.POST)
        respone = {'user': None, 'msg': None}
        if form.is_valid():
            respone['user'] = form.cleaned_data.get('user')
            user = form.cleaned_data.get('user')
            pwd = form.cleaned_data.get('pwd')
            email = form.cleaned_data.get('email')
            avatar_obj = request.FILES.get('avatar')
            extra = {}
            if avatar_obj:
                extra['avatar'] = avatar_obj
            user_obj = UserInfo.objects.create_user(username=user, password=pwd, email=email, **extra)
        else:
            respone['msg'] = form.errors
        return JsonResponse(respone)

    form = RegForm()  # 带入规则
    return render(request, 'reg.html', {"form": form})


def login(request):
    if request.method == "POST":
        response = {'user': None, 'msg': None}  # 构造登录提示信息
        user = request.POST.get('user')
        pwd = request.POST.get('pwd')
        valid_code = request.POST.get('valid_code')  # 获取用户输入的验证码
        valid_code_str = request.session.get('valid_code')  # 从session中提取自动生成的验证码字符串
        if valid_code_str.upper() == valid_code.upper():  # 全部转为大写字母
            user = auth.authenticate(username=user, password=pwd)
            if user:
                auth.login(request, user)  # yanz 成功在session中注册当前用户信息, 就有了request.user全局变量
                response['user'] = user.username  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.
            else:
                response['msg'] = '用户名或密码错误!'
        else:
            response['msg'] = '验证码错误!'
        return JsonResponse(response)  # 这里是ajax验证,所以不能用redirect('index.html')进行跳转.一般应当发送json数据到模板,由模板进行跳转.

    return render(request, 'login.html')


def get_validCode_img(request):
    """生成验证码图片"""
    from blog.utils.validCode import get_validCode_img  # 导入自定义的生成验证码功能函数
    data = get_validCode_img(request)
    return HttpResponse(data)


def index(request):
    article_list = models.Article.objects.all()
    return render(request, 'index.html', {'article_list': article_list})


def logout(request):
    auth.logout(request)
    return redirect('/login/')


def home_site(request, username, **kwargs):
    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')

    # 查询username用户的blog对象
    blog = user.blog
    article_list = models.Article.objects.filter(user=user)
    if kwargs:
        # 个人站点跳转
        condition = kwargs.get('condition')  # 使用**kwargs目的是可以接受任意多个参数
        param = kwargs.get('param')
        if condition == 'tag':
            article_list = article_list.filter(tags__title=param)
            # print(article_list)
        elif condition == 'category':
            article_list = article_list.filter(category__title=param)
        else:
            # 日期归档
            year, month = param.split('-')
            article_list = article_list.filter(create_time__year=year, create_time__month=month)

    return render(request, 'home_site.html', {'username': username, 'blog': blog, 'article_list': article_list})


def article_detail(request, username, article_id):
    """
    文章详情页视图
    http://127.0.0.1:8000/papa/article/2/
    """

    user = UserInfo.objects.filter(username=username).first()
    if not user:
        return render(request, 'not_fount.html')
    # 查询username用户的blog对象
    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, 'article_detail.html', locals())  # locals()不能丢


def digg(request):
    print(request.user)
    import json
    article_id = request.POST.get('article_id')
    is_up = json.loads(request.POST.get('is_up'))  # 传递过来的是"True"字符串, 所以这里需要反序列化为布尔值
    user_id = request.user.pk  # 直接获取user_id,没有必要通过客户端传送

    # 判断是否已经点过赞
    response = {'state': True}  # 返回的状态,将传递给模板中的回调函数,默认是True成功, 表示为点过赞或者反对.
    ret_obj = models.ArticleUpDown.objects.filter(user_id=user_id, article_id=article_id).first()
    if not ret_obj:  # 没点过赞
        ard = models.ArticleUpDown.objects.create(user_id=user_id, article_id=article_id, is_up=is_up)
        # 将点赞写入数据库中

        article_obj = models.Article.objects.filter(pk=article_id)
        if is_up:
            article_obj.update(up_count=F("up_count") + 1)
        else:
            article_obj.update(down_count=F("down_count") + 1)
    else:  # 已经点过赞或反对了
        response['state'] = False  # 表示失败
        response['handled'] = ret_obj.is_up  # 获取当前数据库中是点赞过还是点反对过.

    return JsonResponse(response)  # 将状态传递给模板中的回调函数


def comment(request):
    """
    发布评论
    :param request:
    :return:
    """
    # 前端ajax发送数据, 这里予以获取
    article_id = request.POST.get('article_id')
    content = request.POST.get('content')
    pid = request.POST.get('parent_comment')

    # 从当前用户登录信息中获取
    user_id = request.user.pk
    print("pid:", pid)

    with transaction.atomic():  # 事务同步, 保证评论内容写入comment表,同时,将article表中的评论数计数器加1
        # 写入到数据库comment表
        comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content,
                                                    parent_comment_id=pid)
        # 同步写入评论数加1的数据到文章表中
        models.Article.objects.filter(pk=article_id).update(comment_count=F('comment_count') + 1)

    # 从数据库提取刚刚存入的数据, 为啥非要从数据库中立即再次取出, 不能直接在前端提交时,在前端直接取数据吗?
    create_time = comment_obj.create_time.strftime('%Y-%m-%d %X')
    content = comment_obj.content
    username = request.user.username
    response = {}
    response["create_time"] = create_time
    response['content'] = content
    response['username'] = username

    # 如果是子评论,则还需要获取其父评论的内容,姓名和评论内容
    if pid:
        parent_obj = models.Comment.objects.filter(pk=pid).first()
        response['p_username'] = parent_obj.user.username
        response['p_content'] = parent_obj.content

    # 发送邮件通知文章作者,你的文章有了新的评论
    from django.core.mail import send_mail  # 发送邮件模块

    import threading  # 为了提高效率, 另开一个线程发送邮件
    article_obj = models.Article.objects.filter(pk=article_id).first()  # 获取当前文章对象
    email = article_obj.user.email  # 获取文章的作者的邮箱
    print(email, type(email))
    # send_mail(
    #     '您的文章%s新增了一条评论内容' % article_obj.title,
    #     '评论内容:%s' % content,
    #     settings.EMAIL_HOST_USER,  # 配置发送邮件的邮箱账户, 如果在settings中配置了默认邮箱,则这里可以不写
    #     [email, '123456789@qq.com'],  # 作者邮箱列表
    # )
    # 开多线程,提高效率
    sendMail = threading.Thread(target=send_mail, args=(
        '您的文章%s新增了一条评论内容' % article_obj.title,
        '评论内容:%s' % content,
        settings.EMAIL_HOST_USER,
        [email, '123456789@qq.com'],
    ))
    sendMail.start()  # 多线程千万别忘了这一步.这里才是真正的执行发送任务.上面是配置.

    return JsonResponse(response)


def get_comment_tree(request):
    article_id = request.POST.get('article_id')
    # list强制将querset类型转换为list,然后再序列化. values中是取需要的字段
    ret = list(models.Comment.objects.filter(article_id=article_id).order_by('pk').values('pk', 'content',
                                                                                          'parent_comment_id'))
    print(ret)
    return JsonResponse(ret, safe=False)  # 非字典序列化必须加上safe=False参数


@login_required
def backed_index(request):
    """管理后台"""
    user_id = request.user.pk
    article_list = models.Article.objects.filter(user_id=user_id)

    return render(request, 'backed_index.html', locals())


@login_required
def add_article(request):
    """新增文章"""
    category_list = models.Category.objects.filter(blog__userinfo__nid=request.user.pk).values('pk', 'title')
    tag_list = models.Tag.objects.filter(blog__userinfo__nid=request.user.pk).values('pk', 'title')
    if request.method == 'POST':
        title = request.POST.get('title')
        content = request.POST.get('content')  # 注意这里content没有做script筛选
        # desc = request.POST.get('desc')
        from bs4 import BeautifulSoup
        soup = BeautifulSoup(content, 'html.parser')
        # 过滤有害的script标签
        for tag in soup.find_all():  # 遍历所有标签
            if tag.name == 'script':
                tag.decompose()  # 删除script
        desc = str(soup.text[0:150])  # 直接从内容中截取文字,当摘要用.  过滤过后需要用str()包裹起来.
        category_id = request.POST.get('category_id')
        tag_id_list = request.POST.getlist('tag_id_list')  # 多选取值
        article_obj = models.Article.objects.create(title=title, desc=desc, content=content, user_id=request.user.pk,
                                      category_id=category_id)
        # 多对多的article2tag表数据添加, 文章添加关键字
        article_obj.tags.add(*tag_id_list)
        return redirect('/backed_index/')

    return render(request, 'add_article.html', locals())


@xframe_options_sameorigin  # 富文本编辑器上传图片,一直卡,需要这个模块
def upload(request):
    print(request.FILES)
    img = request.FILES.get('upload_img')  # <MultiValueDict: {'upload_img': [<InMemoryUploadedFile: Jellyfish.jpg (image/jpeg)>]}>
    print(img.name)
    # 读取文件对象,写入服务器指定位置
    import os
    path = os.path.join(settings.MEDIA_ROOT, 'add_article_img', img.name)
    with open(path, 'wb') as f:
        for line in img:
            f.write(line)

    # 将上传图片的数据传递给模板的富文本编辑器
    response={
        'error': 0,  # 0没有任何错误
        'url': '/media/add_article_img/%s' % img.name,  # 通过网络可以访问该图片的路径
    }

    return HttpResponse(JsonResponse(response))  # 注意这里的传递方法
views.py

 结束. 博客系统之完整代码(待整理)


结束. 博客园完整代码博客园完整代码
parent_comment_id
$('.tree-btn').click(function () {


posted @ 2021-03-06 09:24  蓝蓝的白云天!  阅读(279)  评论(0编辑  收藏  举报