django项目(博客二)

扩展1:admin路由分发的本质

路由分发本质 include 可以无限制的 嵌套N多层

url(r'^index/',([],None,None))

扩展2:

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

建好路由,先和视图函数继续试验一下,避免路由被顶替

  1.修改正则表达式

  2.调整url方法的位置,越往上,url的优先级越高

扩展3:

  时区问题报错,setting.py文件里设置,修改时区

TIME_ZONE = 'Asia/Shanghai'
USE_TZ = False

一 文章详情页

  个人站点页面和文章详细页面,两个的导航页和左侧边栏相同,就建一个共同的base.html页面,两个页面继承base.html页面,

两个页面的左侧边栏从视图函数要的数据是样的,就自定义inclusion_tag,自定义inclusion_tag的工作就是建mytags.py,

并建left_menu.html文件。base.html的左侧栏,调用mytats.py,再调用left_menu函数,并传username参数。

base.html的左侧面建好,个人站点页面和文章详情页面继承base.html,各自建内容区,需要视图函数的数据,就再各自的视图函数要。

 
'''
文章详情页和个人站点基本一致,所以用模版继承
个人站点和文章详情的导航条和左侧边栏一样,这该两个html页面就继承base.html,个人站点页面和文章详情页面的内容区,写自己的内容,
个人站点页面和文章详情页面的左侧栏页面,需要向视图函数要同样的数据,解决代码冗余,就用到自定义inclusion_tags


侧边栏的渲染需要传输数据才能渲染,并且该侧边栏在很多页面都需要使用
1. 在那个地方用就拷贝需要的代码(不推荐 有点繁琐)
2. 将侧边栏制作成inclusion_tag,作用就是,不同的页面,需要共同的视图函数数据,就自定义inclusion_tag,不同的页面,都向inclusion要数据,解决代码冗余的问题
'''

url.py

# url设计
https://www.cnblogs.com/xiaoyuanqujing/articles/12208376.html

/username/article/1

# 先验证url是否被其他url顶替,排查BUG
# 文章详情页
url(r'^(?P<username>\w+)/article/(?P<article_id>\d+)/', views.article_detail)

views.py

# 文章详情页视图函数
def article_detail(request, username, article_id):
    '''
    可以先进行用户名校验是否存在
    :param request:
    :param username:
    :param article_id:
    :return:
    '''
    # 筛选出文章对象,显示对应的用户访问对应的文章,文章查个人站点,正向,个人查用户,反向,跨表用__
    article_obj = models.Article.objects.filter(id=article_id, blog__userinfo__username=username).first()
    if not article_obj:
        # return redirect('/errors/')   # 从定向,需要走url路由,没设置路由,走不通
        return render(request, 'errors.html')
    # 导航条需要个人站点名,就从数据库取给前端
    user_obj = models.UserInfo.objects.filter(username=username).first()
    blog = user_obj.blog
    # 获取当前文章所有的评论内容
    comment_list = models.Comment.objects.filter(article=article_obj)
    return render(request, 'article_detail.html', locals())

base.html

个人站点和文章详情页都从base中模版继承,左侧边栏需要向不同 的视图函数要同样的数据,

用到自定义inclusion_tag

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="referrer" content="no-referrer" />
    <title>{{ blog }}的个人站点</title>
    <link rel="stylesheet" href="/static/bootstrap-3.4.1-dist/css/bootstrap.min.css">
    <script src="/static/js/jQuery-3.6.1.js"></script>
    <script src="/static/bootstrap-3.4.1-dist/js/bootstrap.min.js"></script>
    <script src="/static/js/my_setup.js"></script>
    {% block css %}

    {% endblock %}
</head>
<body>
<!--个人站点导航条-->
<div class="container-fluid">
    <nav class="navbar navbar-default">
        <div class="container-fluid">
            <!-- Brand and toggle get grouped for better mobile display -->
            <div class="navbar-header">
                <button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
                        data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#">{{ blog.site_title }}</a>
            </div>

            <!-- Collect the nav links, forms, and other content for toggling -->
            <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                <ul class="nav navbar-nav">
                    <li class="active"><a href="#">Link <span class="sr-only">(current)</span></a></li>
                    <li><a href="#">test</a></li>
                    <li class="dropdown">
                        <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
                           aria-expanded="false">Dropdown <span class="caret"></span></a>
                        <ul class="dropdown-menu">
                            <li><a href="#">Action</a></li>
                            <li><a href="#">Another action</a></li>
                            <li><a href="#">Something else here</a></li>
                            <li role="separator" class="divider"></li>
                            <li><a href="#">Separated link</a></li>
                            <li role="separator" class="divider"></li>
                            <li><a href="#">One more separated link</a></li>
                        </ul>
                    </li>
                </ul>
                <form class="navbar-form navbar-left">
                    <div class="form-group">
                        <input type="text" class="form-control" placeholder="Search">
                    </div>
                    <button type="submit" class="btn btn-default">Submit</button>
                </form>
                <ul class="nav navbar-nav navbar-right">
                    {% if request.user.is_authenticated %}
                        <li><a href="#">{{ request.user.username }}</a></li>
                        <li class="dropdown">
                            <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
                               aria-haspopup="true"
                               aria-expanded="false">更多 <span class="caret"></span></a>
                            <ul class="dropdown-menu">
                                <li><a href="#" data-toggle="modal" data-target=".bs-example-modal-lg">修改密码</a></li>
                                <li><a href="#">修改头像</a></li>
                                <li><a href="#">后台管理</a></li>
                                <li role="separator" class="divider"></li>
                                <li><a href="{% url 'logout' %}" id="logout">退出登录</a></li>
                            </ul>
                            <!-- Large modal -->
                            <div class="modal fade bs-example-modal-lg" tabindex="-1" role="dialog"
                                 aria-labelledby="myLargeModalLabel">
                                <div class="modal-dialog modal-lg" role="document">
                                    <div class="modal-content">
                                        <h3 class="text-center">修改密码</h3>
                                        <div class="row">
                                            <div class="col-md-8 col-md-offset-2">
                                                <div class="form-group">
                                                    <label for="">用户名</label>
                                                    <input type="text" disabled value="{{ request.user.username }}"
                                                           class="form-control">
                                                </div>
                                                <div class="form-group">
                                                    <label for="old_password">原密码</label>
                                                    <input type="password" id="old_password" class="form-control">
                                                    <span style="color: red" class="pull-right"></span>
                                                </div>
                                                <div class="form-group">
                                                    <label for="">新密码</label>
                                                    <input type="password" id="new_password" class="form-control">
                                                </div>
                                                <div class="form-group">
                                                    <label for="">确认密码</label>
                                                    <input type="password" id="confirm_password" class="form-control">
                                                    <span style="color: red" class="pull-right"></span>
                                                </div>
                                                <div class="form-group">
                                                    <button type="button" class="btn btn-default btn-sm"
                                                            data-dismiss="modal">
                                                        取消
                                                    </button>
                                                    <button type="button" class="btn btn-primary btn-sm"
                                                            id="set_commit">
                                                        确认修改
                                                    </button>
                                                </div>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </li>
                    {% else %}
                        <li><a href="{% url 'reg' %}">注册</a></li>
                        <li><a href="{% url 'log' %}">登录</a></li>
                    {% endif %}
                </ul>
            </div><!-- /.navbar-collapse -->
        </div><!-- /.container-fluid -->
    </nav>
</div>
<!--主板 3,9比例-->
<div class="container-fluid">
    <div class="row">
        <div class="col-md-3">
            <!--侧边栏调用mytags.py-->
            {% load mytags %}
            <!--调用mytags里面的left_menu函数-->
            {% left_menu username %}
        </div>
        <!--主板内容-->
        <div class="col-md-9">
            {% block content %}
            
            {% endblock %}
        </div>
    </div>
</div>

{% block js %}

{% endblock %}
</body>
</html>

mytags.py

在应用aap01文件下,建mytags.py

'''
步骤:
    1.在应用下创建一个名字必须叫templatetags文件夹
    2.在该文件夹内创建一个任意名称的py文件
    3.在该py文件内先固定写两行代码
        from django import template
        register = template.Library()
        自定义过滤器
        自定义标签
        自定义inclusion_tag
'''
from django import template
register = template.Library()

# 自定义inclusion_tag
@register.inclusion_tag('left_menu.html')
def left_menu(username):
    # 构造侧边栏需要的数据
    from app01 import models
    from django.db.models import Count
    # 先从site视图函数,剪切过来 ,需要什么参数,就给什么参数
    user_obj = models.UserInfo.objects.filter(username=username).first()
    blog = user_obj.blog
    # 1 查询当前用户所有的分类及分类下的文章数
    category_queryset = models.Category.objects.filter(blog=blog).annotate(
        count_num=Count('article__pk')).values_list('name', 'count_num', 'pk')  # pk是分组后新的虚拟结果的主键值,也就是queryset的索引值
    # values是列表套字典,values_list是列表套元祖
    # 2 查询当前用户所有的标签及标签下的文章数
    tag_queryset = models.Tag.objects.filter(blog=blog).annotate(
        count_num=Count('article__pk')).values_list('name', 'count_num', 'pk')
    # 3 按照年月统计所有的文章
    from django.db.models.functions import TruncMonth
    # 第一个.annotate是按文章进行分组,第二个.annotate是跟在values('month'),month的虚拟表基础上在进行分组,计数取值
    article_month = models.Article.objects.filter(blog=blog).annotate(month=TruncMonth('create_time')).values(
        'month').annotate(
        num=Count('pk')
    ).values_list('month', 'num')
    # print(article_month)
    return locals()

left_menu.html

自定义inclusio_tag中的左侧边栏页面

div class="panel panel-info">
    <div class="panel-heading text-center">文章分类</div>
    <div class="panel-body">
        {% for category in category_queryset %}
            <p><a href="/{{ username }}/category/{{ category.2 }}">{{ category.0 }}({{ category.1 }})</a></p>
        {% endfor %}
    </div>
</div>
<div class="panel panel-danger">
    <div class="panel-heading text-center">文章标签</div>
    <div class="panel-body">
        {% for tag in tag_queryset %}
            <a href="/{{ username }}/tag/{{ tag.2 }}"><p>{{ tag.0 }}<{{ tag.1 }}></p></a>
        {% endfor %}
    </div>
</div>
<div class="panel panel-success">
    <div class="panel-heading text-center">日期归档</div>
    <div class="panel-body">
        {% for month in article_month %}
            <p><a href="/{{ username }}/archive/{{ month.0|date:'Y-m' }}">{{ month.0|date:'Y年m月' }}({{ month.1 }})</a>
            </p>
        {% endfor %}
    </div>
</div>

article_detail.html

文章详情页

{% extends 'base.html' %}

{% block content %}
    <div class="container">
        <h1>{{ article_obj.title }}</h1>
        <div>
            {{ article_obj.content }}
        </div>
    </div>


{% endblock %}

site.html

通过模版继承后的个人站点页面

{% extends 'base.html' %}


{% block content %}
   <ul class="media-list">
                {% for article_obj in article_list %}
                    <li class="media">
                        <h3 class="media-heading"><a href="/{{ username }}/article/{{ article_obj.pk }}">{{ article_obj.title }}</a></h3>
                        <div class="media-left">
                            <a href="#">
                                <img class="media-object" src="/media/{{ article_obj.blog.userinfo.avatar }}" alt="..."
                                     width="50px"
                                     height="60px">
                            </a>
                        </div>
                        <div class="media-body">
                            {{ article_obj.desc }}
                        </div>

                        <div class="pull-right">
                            <span>posted</span>
                            <span>@</span>
                            <span>{{ article_obj.create_time|date:'Y-m-d' }}&nbsp;&nbsp;</span>
                            <span><a href="#">{{ username }}</a>&nbsp;&nbsp;</span>
                            <span><span class="glyphicon glyphicon-comment"></span>评论数({{ article_obj.comment_num }})&nbsp;&nbsp;</span>
                            <span><span class="glyphicon glyphicon-thumbs-up"></span>点赞数({{ article_obj.up_num }})&nbsp;&nbsp;</span>
                            <span><a href="#">编辑</a></span>
                        </div>
                    </li>
                    <hr> <!--分割线-->
                {% endfor %}
            </ul>
{% endblock %}

二 文章点赞点踩

文章点赞点踩功能是在,文章详情页面

'''
浏览器上你看到的花里胡哨的页面,内部都是HTML(前端)代码
在文章内容应该写什么?   --->html代码

如何拷贝文章
    copy outerhtml
解决加载不了图片,用到图片防盗链技术   
<meta name="referrer" content="no-referrer" />

1.拷贝文章
2.拷贝点赞点踩
    1) 拷贝前端点赞点踩图片,只拷贝html
    2) css也要拷贝
        由于有图片防盗链的问题,所以将图片直接下载到本地
        
课下思考:
    前端如何区分用户是点了赞还是点了踩
    1.给标签各自绑定一个事件
        两个标签对应的代码其实基本一样,仅仅是否点赞点踩这一个参数不一样而已
    2.二合一
        给两个标签绑定一个事件
        // 给所有的action类绑定事件
        $('.action').click(function () {
            alert($(this).hasClass('diggit'))
        })
        
3.书写ajax代码朝后端提交数
由于点赞点踩内部有一定的业务逻辑,所以后端单独开设视图函数
建议在写项目的时候吗,先把业务逻辑写完,在进行修正

4.后端逻辑书写完毕之后,前端针对点赞点踩动作实现需要动态展示提示信息
    1)前端点赞点踩数字自增1 需要注意数据类型的问题
    2)用户没有登录 需要展示没有登录提示  并且登录可以点击跳转
    html()
    |safe
    mark_safe()    后端向前端发送html代码
'''


'''
后端逻辑(先写成功逻辑,再写错误逻辑
1.先判断用户是否登录
    request.user.authenticated()
2.再判断当前文字是否是当前用户自己写的
     通过文章主键值获取文章对象
     之后利用orm查询获取文章对象对应的用户对象与request.user比对
3.判断当前用户是否已经给当前文章点了
    利用article_obj文章对象和request.user用户对象去点赞点踩表中筛选数据如果
有数据则点没哟
4.操作数据库,需要注意要同时操作两张表
前端发送过来的是否点赞是一个字符串 需要你自己转成布尔值或者用字符串判断
'''
'''
总结:在书写较为复杂的业务逻辑的时候,可以先按照一条线写下去
之后再去弥补其他线路情况 类似于先走树的主干  之后再分散
'''

urls.py

# 点赞点踩
    url(r'^up_or_down/', views.up_or_down),

views.py

# 点赞点踩视图函数
def up_or_down(request):
    '''
    1.校验用户是否登录
    2.判断当前文章是否是当前用户自己写的(自己不能点自己的文章)
    3.当前用户是否已经给当前文章点过了
    4.操作数据库
    :param request:
    :return:
    '''
    if request.is_ajax():
        back_dic = {'code': 1000, 'msg': ''}
        # 1.先判断用户是否登录
        if request.user.is_authenticated():
            article_id = request.POST.get('article_id')
            print(article_id, type(article_id))
            is_up = request.POST.get('is_up')
            import json
            # 前端发送过来的,是json格式的,需要转成python格式的bool值
            is_up = json.loads(is_up)
            print(is_up, type(is_up))
            # 2.判断当前文章是否是当前用户自己写的 根据文章id查询文章对象,根据文章对象查作者 跟request.user.username比对
            article_obj = models.Article.objects.filter(pk=article_id).first()
            print(article_obj.blog.userinfo.username, type(article_obj.blog.userinfo.username))
            print(request.user, type(request.user))
            if not article_obj.blog.userinfo.username == request.user.username:
                # 3. 校验当前用户是否已经点了    那个地方记录了用户到底点没点
                is_click = models.UpAndDown.objects.filter(user=request.user, article=article_obj)
                if not is_click:
                    # 4 操作数据库 记录数据    要同步操作普通字段
                    # 判断当前用户点了赞还是踩 从而决定给那个字段加一
                    from django.db.models import F  # F新的虚拟字段和原有字段进行比对
                    if is_up:
                        # 给点赞数加一
                        models.Article.objects.filter(pk=article_id).update(up_num=F('up_num') + 1)
                        back_dic['msg'] = '点赞成功'
                    else:
                        # 给点踩数加一
                        models.Article.objects.filter(pk=article_id).update(down_num=F('down_num') + 1)
                        back_dic['msg'] = '点踩成功'
                    # 操作点赞点踩表,记录数据
                    models.UpAndDown.objects.create(user=request.user, article=article_obj, is_up=is_up)
                else:
                    back_dic['code'] = 2000
                    back_dic['msg'] = '你已点过了,不能在点'
            else:
                back_dic['code'] = 3000
                back_dic['msg'] = '不能自己点自己'
        else:
            back_dic['code'] = 4000
            back_dic['msg'] = '需<a href="/login/">登录</a>了,才能点击'
        return JsonResponse(back_dic)

article_detail.html

{% extends 'base.html' %}

{% block css %}

    <style>
        /* 点赞点踩样式 */
        #div_digg {
            float: right;
            margin-bottom: 10px;
            margin-right: 30px;
            font-size: 12px;
            width: 128px;
            text-align: center;
            margin-top: 10px;
        }

        .diggit {
            float: left;
            width: 46px;
            height: 52px;
            background: url('/static/img/upup.gif') no-repeat;
            text-align: center;
            cursor: pointer;
            margin-top: 2px;
            padding-top: 5px;
        }

        .buryit {
            float: right;
            margin-left: 20px;
            width: 46px;
            height: 52px;
            background: url('/static/img/downdown.gif') no-repeat;
            text-align: center;
            cursor: pointer;
            margin-top: 2px;
            padding-top: 5px;
        }

        .clear {
            /*清除浮动*/
            clear: both;
        }
    </style>
{% endblock %}

{% block content %}

    <h1 class="text-center">{{ article_obj.title }}</h1>

    <div class="article_content" style="overflow: hidden">
        {{ article_obj.content|safe }}
    </div>
    <!--点赞点踩开始-->
    <div class="clearfix">
        <div id="div_digg">
            <div class="diggit action">
                <span class="diggnum" id="digg_count">{{ article_obj.up_num }}</span>
            </div>
            <div class="buryit action">
                <span class="burynum" id="bury_count">{{ article_obj.down_num }}</span>
            </div>
            <div class="clear"></div>
            <div class="diggword" id="digg_tips" style="color: red;"></div>
        </div>
    </div>
    <!--点赞点踩结束-->

    <!--根评论开始-->
    <!--评论楼开始-->
    <ul class="list-group" id="id_floor">
        {% for comment in comment_list %}
            <span>#{{ forloop.counter }}楼</span>
            <span>{{ comment.content_time|date:'Y-m-d h:i:s' }}</span>
            <span>{{ comment.user.username }}</span>

            <li class="list-group-item">
                <div>
                    {% if comment.parent_id %}
                        <!--字段parent是自关联字段,跨表还是跨的自己,在再通过外键user跨表查根评论用户-->
                        <p>@{{ comment.parent.user.username }}</p>
                    {% endif %}
                    <span>{{ comment.content }}</span>
                    <!--a标签自定义属性-->
                    <span class="pull-right"><a class="reply" username="{{ comment.user.username }}" comment_id="{{ comment.pk }}">回复</a></span>
                </div>

            </li>
        {% endfor %}
    </ul>
    <!--评论楼结束-->

    <!--前端判断是否登录-->
    {% if request.user.is_authenticated %}
        <div>
            <p><span class="glyphicon glyphicon-comment"></span>发表评论</p>
            <div>
                <textarea name="comment" id="id_comment" cols="60" rows="10"></textarea>
            </div>
            <input type="button" class="btn btn-success" id="id_submit" value="提交评论">
            <span style="color: red" id="id_error"></span>

        </div>
    {% else %}
        <ul class="nav navbar-nav navbar-link">
            <li><a href="{% url 'reg' %}">注册</a></li>
            <li><a href="{% url 'log' %}">登录</a></li>
        </ul>
    {% endif %}

    <!--根评论结束-->
{% endblock %}

{% block js %}
    <script>
        $(function () {
            // ajax向后端发送点赞点踩数
            $('.action').click(function () {
                // alert($(this).hasClass('diggit'))
                // isUp是bool值
                let isUp = $(this).hasClass('diggit');
                // 将点击过的div存起来
                let cilckDiv = $(this);
                // 朝后端发送ajax请求
                $.ajax({
                    url: '/up_or_down/',
                    type: 'post',
                    data: {
                        'article_id': '{{ article_obj.pk }}',
                        'is_up': isUp,
                    },
                    success: function (args) {
                        if (args.code == 1000) {
                            // 取得原来的数,并存给变量Num
                            let Num = cilckDiv.children().text();
                            // 点击过,该数加1,并修改过来,记得将数转成数字形,不然就是字符串拼接
                            cilckDiv.children().text(Number(Num) + 1);
                            $('#digg_tips').text(args.msg)
                        } else {
                            $('#digg_tips').html(args.msg)
                        }
                    }
                })
            });
            // ajax向后端发送评论
            // 全局设置一个parentId
                let parentId = null;
                console.log(parentId)
            $('#id_submit').click(function () {
                let dataComment = $('#id_comment').val();
                // 判断当前评论是否是子评论,如果是,需要将我们之前手动渲染的@username去除
                if (parentId){
                    // 找到\n对应的索引值,然后利用切片,但是切片顾头不顾尾,所以索引要加1
                    let indexNum = dataComment.indexOf('\n')+1
                    dataComment = dataComment.slice(indexNum) // 把换行符之前的切去
                }
                $.ajax({
                    url: '/comment/',
                    type: 'post',
                    data: {
                        'article_id': '{{ article_obj.pk }}',
                        'comment': dataComment,
                        // 前端parentId没有值,后端就存null
                        'parent_id':parentId
                    },
                    success: function (args) {
                        if (args.code = 1000) {
                            $('#id_error').text(args.msg);
                            // 情况评论框里的内容
                            $('#id_comment').val('');
                            // 临时渲染评论数
                            let userName = '{{ request.user.username }}'
                            let temp = `
            <span style="color: red">${userName}</span>
            <li class="list-group-item">
                <div>
                    ${dataComment}
                </div>
            </li>
                            `
                            // 将生成好的标签添加到ul标签内
                            $('#id_floor').append(temp);
                            // 提交后,需要将全局变量parentId恢复初始值
                            parentId = null;
                        }
                    }
                })
            });
            // 点击根评论的回复点击事件
            $('.reply').click(function () {
                // 需要评论对应的评论人姓名  还需要评论的主键值
                // 获取用户名
                let commentUsername = $(this).attr('username');
                // 获取主键值  直接修改parentId全局变量名
                parentId = $(this).attr('comment_id');

                // 拼接信息,聚焦事件传给评论框
                $('#id_comment').val('@'+commentUsername+'\n').focus();
            })
        })
    </script>
{% endblock %}

三 文章评论

'''
先写根评论
再写子评论

点击评论按钮需要将评论框里面的内容清空

根评论有两步渲染方式
    1.DOM临时渲染
    2.页面刷新render渲染
    
子评论
    点击回复按钮发生了几件事
        1.评论框自动聚焦
        2.将回复按钮所在的哪一行评论人的姓名
            @username
        3.评论框内部自动换行
        
根评论子评论都是点击一个按钮朝后端提交数据的
    parent_id
根评论子评论区别在哪?
    parent_id
'''

urls.py

# 点赞点踩
    url(r'^up_or_down/', views.up_or_down),

views.py

# 评论视图函数
def comment(request):
    if request.is_ajax():
        back_dic = {'code': 1000, 'msg': ''}
        article_id = request.POST.get('article_id')
        comment = request.POST.get('comment')
        parent_id = request.POST.get('parent_id')
        print(comment)
        print(parent_id)
        # 后端判断用户是否登录
        if request.user.is_authenticated():
            # 操作数据库,两张表,复习一下数据库的事务(两张表操作成功,才能成功)
            from django.db import transaction
            # 给文章表的评论数更新数据
            with transaction.atomic():
                from django.db.models import F
                models.Article.objects.filter(pk=article_id).update(comment_num=(F('comment_num') + 1))
                # 记录评论表
                models.Comment.objects.create(user=request.user, article_id=article_id, content=comment,
                                              parent_id=parent_id)
            back_dic['msg'] = '评论成功'
        else:
            back_dic['code'] = '2000'
            back_dic['msg'] = '请先登录'
        return JsonResponse(back_dic)

article_detal.html

{% extends 'base.html' %}

{% block css %}

    <style>
        /* 点赞点踩样式 */
        #div_digg {
            float: right;
            margin-bottom: 10px;
            margin-right: 30px;
            font-size: 12px;
            width: 128px;
            text-align: center;
            margin-top: 10px;
        }

        .diggit {
            float: left;
            width: 46px;
            height: 52px;
            background: url('/static/img/upup.gif') no-repeat;
            text-align: center;
            cursor: pointer;
            margin-top: 2px;
            padding-top: 5px;
        }

        .buryit {
            float: right;
            margin-left: 20px;
            width: 46px;
            height: 52px;
            background: url('/static/img/downdown.gif') no-repeat;
            text-align: center;
            cursor: pointer;
            margin-top: 2px;
            padding-top: 5px;
        }

        .clear {
            /*清除浮动*/
            clear: both;
        }
    </style>
{% endblock %}

{% block content %}

    <h1 class="text-center">{{ article_obj.title }}</h1>

    <div class="article_content" style="overflow: hidden">
        {{ article_obj.content|safe }}
    </div>
    <!--点赞点踩开始-->
    <div class="clearfix">
        <div id="div_digg">
            <div class="diggit action">
                <span class="diggnum" id="digg_count">{{ article_obj.up_num }}</span>
            </div>
            <div class="buryit action">
                <span class="burynum" id="bury_count">{{ article_obj.down_num }}</span>
            </div>
            <div class="clear"></div>
            <div class="diggword" id="digg_tips" style="color: red;"></div>
        </div>
    </div>
    <!--点赞点踩结束-->

    <!--根评论开始-->
    <!--评论楼开始-->
    <ul class="list-group" id="id_floor">
        {% for comment in comment_list %}
            <span>#{{ forloop.counter }}楼</span>
            <span>{{ comment.content_time|date:'Y-m-d h:i:s' }}</span>
            <span>{{ comment.user.username }}</span>

            <li class="list-group-item">
                <div>
                    {% if comment.parent_id %}
                        <!--字段parent是自关联字段,跨表还是跨的自己,在再通过外键user跨表查根评论用户-->
                        <p>@{{ comment.parent.user.username }}</p>
                    {% endif %}
                    <span>{{ comment.content }}</span>
                    <!--a标签自定义属性-->
                    <span class="pull-right"><a class="reply" username="{{ comment.user.username }}" comment_id="{{ comment.pk }}">回复</a></span>
                </div>

            </li>
        {% endfor %}
    </ul>
    <!--评论楼结束-->

    <!--前端判断是否登录-->
    {% if request.user.is_authenticated %}
        <div>
            <p><span class="glyphicon glyphicon-comment"></span>发表评论</p>
            <div>
                <textarea name="comment" id="id_comment" cols="60" rows="10"></textarea>
            </div>
            <input type="button" class="btn btn-success" id="id_submit" value="提交评论">
            <span style="color: red" id="id_error"></span>

        </div>
    {% else %}
        <ul class="nav navbar-nav navbar-link">
            <li><a href="{% url 'reg' %}">注册</a></li>
            <li><a href="{% url 'log' %}">登录</a></li>
        </ul>
    {% endif %}

    <!--根评论结束-->
{% endblock %}

{% block js %}
    <script>
        $(function () {
            // ajax向后端发送点赞点踩数
            $('.action').click(function () {
                // alert($(this).hasClass('diggit'))
                // isUp是bool值
                let isUp = $(this).hasClass('diggit');
                // 将点击过的div存起来
                let cilckDiv = $(this);
                // 朝后端发送ajax请求
                $.ajax({
                    url: '/up_or_down/',
                    type: 'post',
                    data: {
                        'article_id': '{{ article_obj.pk }}',
                        'is_up': isUp,
                    },
                    success: function (args) {
                        if (args.code == 1000) {
                            // 取得原来的数,并存给变量Num
                            let Num = cilckDiv.children().text();
                            // 点击过,该数加1,并修改过来,记得将数转成数字形,不然就是字符串拼接
                            cilckDiv.children().text(Number(Num) + 1);
                            $('#digg_tips').text(args.msg)
                        } else {
                            $('#digg_tips').html(args.msg)
                        }
                    }
                })
            });
            // ajax向后端发送评论
            // 全局设置一个parentId
                let parentId = null;
                console.log(parentId)
            $('#id_submit').click(function () {
                let dataComment = $('#id_comment').val();
                // 判断当前评论是否是子评论,如果是,需要将我们之前手动渲染的@username去除
                if (parentId){
                    // 找到\n对应的索引值,然后利用切片,但是切片顾头不顾尾,所以索引要加1
                    let indexNum = dataComment.indexOf('\n')+1
                    dataComment = dataComment.slice(indexNum) // 把换行符之前的切去
                }
                $.ajax({
                    url: '/comment/',
                    type: 'post',
                    data: {
                        'article_id': '{{ article_obj.pk }}',
                        'comment': dataComment,
                        // 前端parentId没有值,后端就存null
                        'parent_id':parentId
                    },
                    success: function (args) {
                        if (args.code = 1000) {
                            $('#id_error').text(args.msg);
                            // 情况评论框里的内容
                            $('#id_comment').val('');
                            // 临时渲染评论数
                            let userName = '{{ request.user.username }}'
                            let temp = `
            <span style="color: red">${userName}</span>
            <li class="list-group-item">
                <div>
                    ${dataComment}
                </div>
            </li>
                            `
                            // 将生成好的标签添加到ul标签内
                            $('#id_floor').append(temp);
                            // 提交后,需要将全局变量parentId恢复初始值
                            parentId = null;
                        }
                    }
                })
            });
            // 点击根评论的回复点击事件
            $('.reply').click(function () {
                // 需要评论对应的评论人姓名  还需要评论的主键值
                // 获取用户名
                let commentUsername = $(this).attr('username');
                // 获取主键值  直接修改parentId全局变量名
                parentId = $(this).attr('comment_id');

                // 拼接信息,聚焦事件传给评论框
                $('#id_comment').val('@'+commentUsername+'\n').focus();
            })
        })
    </script>
{% endblock %}

四 后台管理

新建页面,将文章个人文章展示出来,用到table标签,新增文章和其他功能的页面,导航条和左侧边栏一样,需要用到模版继承。

'''
当一个文件夹下文件比较多的时候,你还可以继续创建文件夹分类处理
    templates文件夹
        backend文件夹
        应用1文件
        应用2文件
'''

urls.py

 # 后台管理
    url(r'^backend/$', views.backend),

views.py

@login_required
def backend(request):
    article_list = models.Article.objects.filter(blog=request.user.blog)
    return render(request, 'backend/backend.html', locals())

base.html

用了 bootstrap中标签页,列表组

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{ request.user.username }}的后台管理</title>
    <link rel="stylesheet" href="/static/bootstrap-3.4.1-dist/css/bootstrap.min.css">
    <script src="/static/js/jQuery-3.6.1.js"></script>
    <script src="/static/bootstrap-3.4.1-dist/js/bootstrap.min.js"></script>
</head>
<body>
<!--导航条-->
<div class="container">
    <nav class="navbar navbar-default">
        <div class="container-fluid">
            <!-- Brand and toggle get grouped for better mobile display -->
            <div class="navbar-header">
                <button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
                        data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="/home/">{{ request.user.blog.site_title }}</a>
            </div>

            <!-- Collect the nav links, forms, and other content for toggling -->
            <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                <ul class="nav navbar-nav">
                    <li class="active"><a href="#">Link <span class="sr-only">(current)</span></a></li>
                    <li><a href="#">test</a></li>
                    <li class="dropdown">
                        <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
                           aria-expanded="false">Dropdown <span class="caret"></span></a>
                        <ul class="dropdown-menu">
                            <li><a href="#">Action</a></li>
                            <li><a href="#">Another action</a></li>
                            <li><a href="#">Something else here</a></li>
                            <li role="separator" class="divider"></li>
                            <li><a href="#">Separated link</a></li>
                            <li role="separator" class="divider"></li>
                            <li><a href="#">One more separated link</a></li>
                        </ul>
                    </li>
                </ul>
                <form class="navbar-form navbar-left">
                    <div class="form-group">
                        <input type="text" class="form-control" placeholder="Search">
                    </div>
                    <button type="submit" class="btn btn-default">Submit</button>
                </form>
                <ul class="nav navbar-nav navbar-right">
                    {% if request.user.is_authenticated %}
                        <li><a href="#">{{ request.user.username }}</a></li>
                        <li class="dropdown">
                            <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
                               aria-haspopup="true"
                               aria-expanded="false">更多 <span class="caret"></span></a>
                            <ul class="dropdown-menu">
                                <li><a href="#" data-toggle="modal" data-target=".bs-example-modal-lg">修改密码</a></li>
                                <li><a href="#">修改头像</a></li>
                                <li><a href="#">后台管理</a></li>
                                <li role="separator" class="divider"></li>
                                <li><a href="{% url 'logout' %}" id="logout">退出登录</a></li>
                            </ul>
                            <!-- Large modal -->
                            <div class="modal fade bs-example-modal-lg" tabindex="-1" role="dialog"
                                 aria-labelledby="myLargeModalLabel">
                                <div class="modal-dialog modal-lg" role="document">
                                    <div class="modal-content">
                                        <h3 class="text-center">修改密码</h3>
                                        <div class="row">
                                            <div class="col-md-8 col-md-offset-2">
                                                <div class="form-group">
                                                    <label for="">用户名</label>
                                                    <input type="text" disabled value="{{ request.user.username }}"
                                                           class="form-control">
                                                </div>
                                                <div class="form-group">
                                                    <label for="old_password">原密码</label>
                                                    <input type="password" id="old_password" class="form-control">
                                                    <span style="color: red" class="pull-right"></span>
                                                </div>
                                                <div class="form-group">
                                                    <label for="">新密码</label>
                                                    <input type="password" id="new_password" class="form-control">
                                                </div>
                                                <div class="form-group">
                                                    <label for="">确认密码</label>
                                                    <input type="password" id="confirm_password" class="form-control">
                                                    <span style="color: red" class="pull-right"></span>
                                                </div>
                                                <div class="form-group">
                                                    <button type="button" class="btn btn-default btn-sm"
                                                            data-dismiss="modal">
                                                        取消
                                                    </button>
                                                    <button type="button" class="btn btn-primary btn-sm"
                                                            id="set_commit">
                                                        确认修改
                                                    </button>
                                                </div>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </li>
                    {% else %}
                        <li><a href="{% url 'reg' %}">注册</a></li>
                        <li><a href="{% url 'log' %}">登录</a></li>
                    {% endif %}
                </ul>
            </div><!-- /.navbar-collapse -->
        </div><!-- /.container-fluid -->
    </nav>
</div>

<!--主板2,10-->
<div class="container">
    <div class="row">
        <!--左侧栏-->
        <div class="col-md-2">
            <div class="list-group">
                <a href="#" class="list-group-item active">
                    操作
                </a>
                <a href="/add/article/" class="list-group-item">新建文章</a>
                <a href="#" class="list-group-item">新建随笔</a>
                <a href="#" class="list-group-item">回收站</a>
                <a href="#" class="list-group-item">建议</a>
            </div>
        </div>
        <!--右侧内容区-->
        <div class="col-md-10">
            <div>

                <!-- Nav tabs -->
                <ul class="nav nav-tabs" role="tablist">
                    <li role="presentation" class="active"><a href="#home" aria-controls="home" role="tab"
                                                              data-toggle="tab">文章</a></li>
                    <li role="presentation"><a href="#profile" aria-controls="profile" role="tab" data-toggle="tab">随笔</a>
                    </li>
                    <li role="presentation"><a href="#messages" aria-controls="messages" role="tab" data-toggle="tab">文件</a>
                    </li>
                    <li role="presentation"><a href="#settings" aria-controls="settings" role="tab" data-toggle="tab">设置</a>
                    </li>
                </ul>

                <!-- Tab panes -->
                <div class="tab-content">
                    <div role="tabpanel" class="tab-pane active" id="home">
                         {% block content %}
                         
                         {% endblock %}
                    </div>
                    <div role="tabpanel" class="tab-pane" id="profile">随笔页面</div>
                    <div role="tabpanel" class="tab-pane" id="messages">文件页面</div>
                    <div role="tabpanel" class="tab-pane" id="settings">设置</div>
                </div>

            </div>
        </div>
    </div>
</div>
{% block js %}

{% endblock %}
</body>
</html>

五 文章添加

属于后台管理页面中的功能

'''
有两个需要注意的问题
    1.文章的简介
        不能直接切去
            应该先想办法获取到当前页面的文本内容之后截取150个文本字符
    
    2.XSS攻击
        针对支持用户直接编写html代码的网址
        针对用户直接书写的script标签 需要处理
            (1)注释标签内部的内容
            (2)直接将script删除

如何解决?
    针对1 后端通过正则表达式筛选
    针对2 首先需要确定及获取script标签
    
    beautifulsoup模块   bs4模块
        专门用来帮你处理html页面的
        该模块主要用于爬虫程序
    pip3 install beautifulsoup4
'''


# 模块使用
soup = BeautifulSoup(content, 'html.parser')
# 获取所有的标签
tags = soup.find_all()
for tag in tags:
    # print(tag.name)  # 获取页面所有的标签
    # print(tags)
    # 针对script标签  直接删除
    if tag.name == 'script':
        # 删除标签
        tag.decompose()

# 文章简介
# 1 先简单暴力的直接切去content  150个字符
# desc = content[0:150]
# 2 截取文本150个
desc = soup.text[0:150]

'''
当你发现一个数据起来不是很方便的时候
可以考虑百度搜索有没有现成的模块帮你完成相应的功能
'''

urls.py

 # 增加文章
    url(r'^add/article/', views.add_article),

views.py

@login_required
def add_article(request):
    if request.method == 'POST':
        title = request.POST.get('title')
        # content是含html标签的字符串
        content = request.POST.get('content')
        category_id = request.POST.get('category')
        # print(category_id)
        tag_id_list = request.POST.getlist('tag')
        # print(tag_id_list)
        # beautifulsoup模块使用
        from bs4 import BeautifulSoup
        # 'html.parser'说明是用的python内置标准库
        soup = BeautifulSoup(content, 'html.parser')
        print(soup, type(soup))
        # tags是整个html标签页,含文本内容
        tags = soup.find_all()
        # 从tags中获取标签,只有标签
        for tag in tags:
            print(tag.name)  # 获取页面所有的标签
            # 防止xss攻击,删除script标签
            if tag.name == 'script':
                # 删除script标签,删除给script标签
                tag.decompose()
        # 文章简介
        # 1 截取文本150个字符,之前的直接截取,是含有html标签的
        desc = soup.text[0:150]
        # 操作数据,记录文章
        article_obj = models.Article.objects.create(
            title=title,
            desc=desc,
            # 接受删除script标签的内容
            content=str(soup),
            category_id=category_id,
            blog=request.user.blog
        )
        # 文章和标签的关系表  半自动是我们自己创建的 没法使用add set remove clear方法
        # 自己去操作关系表  一次性可能需要创建多条数据  批量插入bulk_create()
        article_obj_list = []
        for i in tag_id_list:
            # 用批量插入,是对象添加到列表,bulk_create(列表),是对象,不能用orm,不然要报错
            tag_article_obj = models.ArticleToTag(article=article_obj, tag_id=i)
            print(tag_article_obj)
            article_obj_list.append(tag_article_obj)
        # 批量插入,文章和标签的第三张表
        print(article_obj_list)
        try:
            models.ArticleToTag.objects.bulk_create(article_obj_list)
        except Exception as e:
            print(e)
        obj = models.ArticleToTag.objects.filter(pk=1).first()
        print(obj)
        return redirect('/backend/')
    category_list = models.Category.objects.filter(blog=request.user.blog)
    tag_list = models.Tag.objects.filter(blog=request.user.blog)
    return render(request, 'backend/add_article.html', locals())

add_articel.html

{% extends 'backend/base.html' %}


{% block content %}
    <h3>添加文章</h3>
    <form action="" method="post">
        {% csrf_token %}
        <p>标题</p>
        <input type="text" name="title" id="id_title" class="form-control">
        <div>
            <p>内容</p>
            <textarea name="content" id="editor_id" cols="40" rows="10"></textarea>
        </div>
        <p>分类</p>
        <div>
            <select name="category">
                {% for category in category_list %}
                    <option value="{{ category.pk }}">{{ category.name }}</option>
                {% endfor %}
            </select>
        </div>
        <p>标签</p>
        <div>
            <select multiple name="tag">
                {% for tag in tag_list %}
                    <option value="{{ tag.pk }}">{{ tag.name }}</option>
                {% endfor %}
            </select>
        </div>
        <input type="submit" class="btn btn-danger">
    </form>

{% endblock %}

{% block js %}
    {% load static %}
    <script charset="utf-8" src="/static/kindeditor/kindeditor-all-min.js"></script>
    <script>
        KindEditor.ready(function (K) {
            window.editor = K.create('#editor_id', {
                // 设置编辑 器大小
                width: '100%',
                height: '600px',
                // 编辑器编辑按钮设置
                items: [
                    'source', '|', 'undo', 'redo', '|', 'preview', 'print', 'template', 'code', 'cut', 'copy', 'paste',
                    'plainpaste', 'wordpaste', '|', 'justifyleft', 'justifycenter', 'justifyright',
                    'justifyfull', 'insertorderedlist', 'insertunorderedlist', 'indent', 'outdent', 'subscript',
                    'superscript', 'clearhtml', 'quickformat', 'selectall', '|', 'fullscreen', '/',
                    'formatblock', 'fontname', 'fontsize', '|', 'forecolor', 'hilitecolor', 'bold',
                    'italic', 'underline', 'strikethrough', 'lineheight', 'removeformat', '|', 'image', 'multiimage',
                    'flash', 'media', 'insertfile', 'table', 'hr', 'emoticons', 'baidumap', 'pagebreak',
                    'anchor', 'link', 'unlink', '|', 'about'
                ],
                // 编辑器,左右固定,上下可以拖动,改变大小
                resizeType: 1,
                // 上传图片的后端提交路径
                uploadJson: '/upload/',
                // 上传图片、Flash、视音频、文件时,支持添加别的参数一并传到服务器。
                extraFileUploadParams: {
                    'csrfmiddlewaretoken': '{{ csrf_token }}',
                }
            });
        });
    </script>
{% endblock %}

六 kindeditor富文本编辑器

编辑器的种类很多,可以自己去网上搜索,

<a href="http://kindeditor.net/doc.php">kindeditor官网</a>

绑定textarea标签的id

<div>
            <p>内容</p>
            <textarea name="content" id="editor_id" cols="40" rows="10"></textarea>
        </div>

七 编辑器如何上传图片

别人写好了接口 但是接口不是你自己的 需要手动去修改

在使用别人的框架或者模块的时候 出现了问题不要慌 看看文档就用对应的处理方法

urls.py

# 修改用户头像
    url(r'^set/avatar/', views.set_avatar),

views.py

# 富文本编辑器上传文件视图函数
def upload(request):
    '''
    富文本编辑器,需要返回的格式数据
    //成功时
{
        "error" : 0,
        "url" : "http://www.example.com/path/to/file.ext"
}
//失败时
{
        "error" : 1,
        "message" : "错误信息"
}
    :param request:
    :return:
    '''
    # 用户写文章上传的图片  也算静态资源 也应该放到media文件夹下
    back_dic = {'error': 0}  # 先定义给返回给前端编辑器的数据格式
    if request.method == 'POST':
        # 获取用户上传的图片对象
        print(request.FILES)  # {'imgFile': [<InMemoryUploadedFile: 1603159551507.mp4 (video/mp4)>]}
        file_obj = request.FILES.get('imgFile')
        print(file_obj, file_obj.name)
        # 手动拼接存储文件的路径
        from bbs01 import settings
        import os
        file_dir = os.path.join(settings.BASE_DIR, 'media', 'article')
        # 优化操作,先判断当前文件夹是否存在 不存在 自动创建
        if not os.path.isdir(file_dir):
            os.mkdir(file_dir)  # 创建一层目录结构  article
        # 拼接图片的完整路径,如果要 唯一的文件名,可以用uuid模块
        file_path = os.path.join(file_dir, file_obj.name)
        print(file_path)
        with open(file_path, 'wb') as f:
            for line in file_obj:
                f.write(line)
        # 返回给编辑器的数据
        back_dic['url'] = '/media/article/{}'.format(file_obj.name)

    return JsonResponse(back_dic)

八 修改用户头像

用的方式跟注册用户 头像一致,需要注意的是,修改后,在操作数据库的时候,

不用update方式修改数据库,obj.save(),第一种,要修改保存路径

urls.py

# 修改用户头像
    url(r'^set/avatar/', views.set_avatar),

views.py

# 修改用户头像
@login_required
def set_avatar(request):
    if request.method == 'POST':
        file_obj = request.FILES.get('avatar')
        # update的更新方式,会将userinfo表的avatar字段的路径改动了,所以跳转到home页面时,显示不了头像
        # models.UserInfo.objects.filter(blog=request.user.blog).update(avatar=file_obj)
        # 所以要用一下方式
        # 1 自己手动添加avatar路径
        # 2 新的更新方式
        user_obj = request.user
        user_obj.avatar = file_obj
        user_obj.save()
        return redirect('/home/')
    blog = request.user.blog
    username = request.user.username
    return render(request, 'set_avatar.html', locals())

set_avatar.html

后期,建议用模态框的形式进行修改,或者另起一页

{% extends 'base.html' %}

{% block content %}
    <h3 class="text-center">修改头像</h3>
    <!--form表单上传文件,一定要设置enctype-->
    <form action="" method="post" enctype="multipart/form-data">
        {% csrf_token %}
        <p>
            原头像:
            <img src="/media/{{ request.user.avatar }}" alt="">
        </p>
        <p>
            新头像:
            <label for="my_file">头像
                {% load static %}
                <img src="{% static 'img/defaut.png' %}" id="my_img" alt="" class="img-rounded"
                     style="margin-left: 10px">
            </label>
            <input type="file" id="my_file" name="avatar" style="display: none">
        </p>
        <input type="submit" class="btn btn-info">
    </form>
{% endblock %}

{% block js %}
    <script>
        $(function () {
            $("#my_file").change(function () {
                // 文件阅读器对象
                // 1 先生成一个文件阅读器对象,内置对象
                let myFileReaderObj = new FileReader();
                // 2 获取用户上传的头像文件
                let fileObj = $(this)[0].files[0];
                // console.log(fileObj)
                // 3 将文件对象交给阅读器对象读取
                myFileReaderObj.readAsDataURL(fileObj)  // 异步操作 IO操作
                // 4 利用文件阅读器将文件展示到前端页面  修改src属性
                // 等待文件阅读器加载完毕之后再执行
                myFileReaderObj.onload = function () {
                    // 修改图像地址属性
                    $('#my_img').attr('src', myFileReaderObj.result)
                }
            })
        })
    </script>
{% endblock %}

 九 BBS项目总结

在开发任意的web项目的时候,到了后期需要写的代码越来越少 都是用已经写好的url填写到a标签href属性完成跳转即可

主要功能总结

表设计

开发流程(粗糙流程 还可以细化)

注册功能 forms组件使用 头像动态展示 错误信息提示

登录功能 图片验证码 滑动验证码

首页展示 media配置 主动暴露任意资源接口

个人站点展示 侧边栏展示 侧边栏筛选 侧边栏inclusion_tag

文章详情页 点赞点踩 评论

后台管理

针对bbs需要掌握每一个功能的书写思路 内部逻辑 之后再去敲代码熟悉 找感觉

 

posted @ 2023-09-14 17:05  coder雪山  阅读(31)  评论(0编辑  收藏  举报