• Django学习笔记(18)——BBS+Blog项目开发(2)主体思路及流程


      这篇博客主要完成一个BBS+Blog项目,那么主要是模仿博客园的博客思路,使用Django框架进行练习。

    准备:项目需求分析

      在做一个项目的时候,我们首先做的就是谈清楚项目需求,功能需求,然后才开始写,要是没有和产品经理聊清楚需求,到时候改的话就非常非常麻烦。

      那此次写项目的话,我会严格按着此次写的项目流程完成项目。那下面就是此次的项目流程。

    1,项目流程

    1.1,功能需求分析(和产品经理聊清楚需求)

      1,基于用户认证组件和AJAX实现登录验证(图片验证码)

      2,基于AJAX 和Forms组件实现注册功能

      3,设计系统首页(完成文章列表的渲染)

      4,设计个人站点页面

      5,文章详情页面

      6,实现一个点赞的功能

      7,实现文章的评论功能

      ——对文章的评论

      ——对评论的评论(就是子评论,反驳评论的评论)

      8,后台管理页面(后面新增文章的功能)——富文本编辑框

      9,防止XSS攻击框

    1.2,设计表结构

    1.3,按着每一个功能进行开发

    1.4,功能测试阶段

    1.5,项目部署上线(开发人员最难熬的阶段)

    2,开发功能的主要设计思路

       那么下面我们要开发这个网站,而我此次是严格按照经典的软件开发所遵循的MVC设计模型。(如果不懂软件设计的MVC模式,请参考这篇博客:请点击我,后面有MVC的介绍。

      下面写的内容呢,就是我在review整个BBS+Blog项目,其实整体学完,我在这里梳理一遍,做个笔记,那么下面我的记录笔记肯定是按照Django网站开发的四件套Model(模型),URL(链接),View(视图)和Template(模板)完成的。其实这四个就对应着经典的MVC。分别是:

    • Django Model(模型):这个与经典MVC模式下的Model差不多。
    • Django URL+View(视图):这个合起来就与经典MVC下的Controller更像。原因就在于Django的URL和View合起来才能向Template传递正确的数据。用户输入提供的数据也需要Django的View来处理。
    • Django Template(模板):这个与经典MVC模式下的View一致。Django模板用来呈现Django View 传来的数据,也决定了用户界面的外观。Template里面也包含了表单,可以用来收集用户的输入。

    一,Django model(模型)  ==  Model(MVC)

    1,创建项目,迁移表

    1.1,创建Django项目,然后建立url路径

    1.2,在mysql建数据库,然后在settings中配置

    import pymysql
     
    pymysql.install_as_MySQLdb()
    
    
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.mysql',
            'NAME':'blog',           # 要连接的数据库,连接前需要创建好
            'USER':'root',        # 连接数据库的用户名
            'PASSWORD':'',        # 连接数据库的密码
            'HOST':'127.0.0.1',       # 连接主机,默认本级
            'PORT':3306            #  端口 默认3306
        }
    }
    

     

    1.3,设置时区和语言

      Django默认使用美国时间和英语,在项目的settings文件中,如下图所示:

    LANGUAGE_CODE = 'en-us'
       
    TIME_ZONE = 'UTC'
       
    USE_I18N = True
       
    USE_L10N = True
       
    USE_TZ = True
          
        我们将其改为 亚洲/上海  时间和中文
      
    LANGUAGE_CODE = 'zh-hans'
       
    TIME_ZONE = 'Asia/Shanghai'
       
    USE_I18N = True
       
    USE_L10N = True
       
    USE_TZ = False
    

      

    1.4,创建模型

      这里模型表设计多表操作,不懂的可以先学习这篇博客:Django学习笔记(7):单表操作和多表操作

    1.4.1,设计表结构

      分析表结构

      跨表查询效率非常低。不建议使用。

      所以为了保证查询的效率,经常会牺牲增删改的效率。

    1.4.2,完成表内容

      继承AbstractUser,对比继承user。

       每个人的个人站点,可以添加个人标签,和随笔分类:

       一个人可以创建多个分类,一个人可以拥有多个分类,人user和分类时一对多的关系

      分类和站点的关系:一个站点blog有多个分类category,一个分类只能属于一个站点,所以站点和分类是一对多。

      站点blog 和人user是一对一的关系。(跨表查询的问题)

      一个博客存的最核心的数据就是文章,所以展示文章表:

      

       关系表,联合唯一

    1.5,迁移表

    python manage.py  makemigrations
    
    python manage.py migrate
    

      

    2,Django URL+View  ==  Controller(MVC)

    2.1  url的设计

      由于博客系统只有一个APP,所以我们这里不做分发路由。直接在根URL里面写即可。

    from django.contrib import admin
    from django.urls import path, re_path
    from blog import views
    from cnblog_review import settings
    from django.views.static import serve
    
    urlpatterns = [
        path('admin/', admin.site.urls),
        path('login/', views.login),
        path('get_validCode_image/', views.get_validCode_image),
        re_path(r'^$', views.index),
        path('register/', views.register),
        path('logout/', views.logout),
        
        # 点赞
        path('digg/', views.digg),
        # 评论
        path('comment/', views.comment),
        # 树形评论
        path('get_comment_tree/', views.get_comment_tree),
        
        # media配置
        re_path(r'media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
        
        re_path(r'^(?P<username>w+)/articles/(?P<article_id>d+)$', views.article_detail),
        
        # 后台管理url
        re_path(r'cn_backend/$', views.cn_backend),
        re_path(r'cn_backend/add_article/$', views.add_article),
        
        # 关于个人站点的URL
        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),
    
    ]
    

      

    2.2  登录页面的设计 

      在登录页面设计之前,我们可以参考我这两篇博客:

    Django学习笔记(9)——开发用户注册与登录系统

    Django学习笔记(16)——扩展Django自带User模型,实现用户注册与登录

     下面我就不多解释,直接完成登录页面。代码如下:

      views.py

    def login(request):
        '''
        登录视图函数:
            get请求响应页面
            post(Ajax)请求响应字典
        :param request:
        :return:
        '''
        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_str')
            if valid_code.upper() == valid_code_str.upper():
                user = auth.authenticate(username=user, password=pwd)
                if user:
                    # request.user == 当前登录对象
                    auth.login(request, user)
                    response['user'] = user.username
                else:
                    response['msg'] = '用户名或者密码错误!'
            else:
                # 校验失败了
                response['msg'] = 'valid code error!'
    
            return JsonResponse(response)
    
        return render(request, 'login.html')
    

      login.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <link rel="stylesheet" href="/static/bootstrap-3.3.7/dist/css/bootstrap.min.css">
    
    </head>
    <body>
    
    <h3 class="text-center">登录页面</h3>
    <div class="container">
        <div class="row">
            <div class="col-md-6 col-lg-offset-3">
                <form>
                    {% csrf_token %}
                    <div class="form-group">
                        <label for="user">用户名</label>
                        <input type="text" id="user" class="form-control">
                    </div>
    
                    <div class="form-group">
                        <label for="pwd">密码</label>
                        <input type="password" id="pwd" class="form-control">
                    </div>
    
                    <div class="form-group">
                        <label for="pwd">验证码</label>
                        <div class="row">
                            <div class="col-md-6">
                                <input type="text" class="form-control" id="valid_code">
                            </div>
                            <div class="col-md-6">
                                <img width="270" height="36" id="valid_code_img" src="/get_validCode_image/" alt="">
                            </div>
                        </div>
                    </div>
    
                    <input type="button" class="btn btn-default login_btn" value="submit"><span class="error"></span>
    
                    <a href="/register/" class="btn btn-success pull-right">注册</a>
    
                </form>
            </div>
        </div>
    </div>
    
    <script src="/static/JS/jquery-3.2.1.min.js"></script>
    <script>
        // 刷新验证码
        $("#valid_code_img").click(function () {
            $(this)[0].src += "?"
        });
        
        // 登录验证
        $(".login_btn").click(function () {
            $.ajax({
                url: "",
                type: "post",
                data: {
                    user: $("#user").val(),
                    pwd: $("#pwd").val(),
                    valid_code: $("#valid_code").val(),
                    csrfmiddlewaretoken: $("[name='csrfmiddlewaretoken']").val(),
                },
                success: function (data) {
                    console.log(data);
    
                    if (data.user) {
                        if (location.search){
                            location.href = location.search.slice(6)
                        }
                        else {
                             location.href = "/index/"
                        }
    
                    }
                    else {
                        $(".error").text(data.msg).css({"color": "red", "margin-left": "10px"});
                        setTimeout(function(){
                             $(".error").text("");
                        },1000)
    
                    }
                }
        })
    </script>
    
    </body>
    </html>
    

      结果展示:

    2.3  基于forms组件的注册页面设计

      在注册页面设计之前,我们要学习验证码的代码。

    参考这篇博客:Django学习笔记(17)——验证码功能的实现

    2.3.1 注册页面总体代码展示

       views.py

    def get_validCode_image(request):
        """
        基于PIL模块动态生成响应状态码图片
        :param request:
        :return:
        """
        data = get_valid_code_img(request)
        return HttpResponse(data)
    
    
    def index(request):
        """
        系统首页
        :param request:
        :return:
        """
        article_list = models.Article.objects.all()
    
        return render(request, 'index.html', locals())
    
    
    def logout(request):
        """
        注销视图
        :param request:
        :return:
        """
        auth.logout(request)
        # 等同于执行了  request.session.fulsh()
        return redirect('/login/')
    
    
    def register(request):
        """
        注册视图函数:
           get请求响应注册页面
           post(Ajax)请求,校验字段,响应字典
        :param request:
        :return:
        """
        if request.is_ajax():
            print(request.POST)
            form = UserForm(request.POST)
    
            response = {'user': None, 'msg': None}
            if form.is_valid():
                response['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
                # 要是逻辑没有用到值,我们可以不用赋值,等用到的时候,则添加
                UserInfo.objects.create_user(username=user, password=pwd, email=email, avatar=avatar_obj, **extra)
    
    
            else:
                print(form.cleaned_data)
                print(form.errors)
                response['msg'] = form.errors
    
            return JsonResponse(response)
    
        form = UserForm()
        return render(request, 'register.html', locals())
    

      register.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <link rel="stylesheet" href="/static/CSS/register.css">
        <link rel="stylesheet" href="/static/bootstrap-3.3.7/dist/css/bootstrap.css">
        <script src="/static/JS/jquery-3.2.1.min.js"></script>
    
    
    </head>
    <body>
    <h3 class="text-center">注册页面</h3>
    <div class="container">
        <div class="row">
            <div class="col-md-6 col-lg-offset-3">
    
                <form id="form">
                    {% csrf_token %}
    
                    {% for field in form %}
                        <div class="form-group">
                            <label for="{{ field.auto_id }}">{{ field.label }}</label>
                            {{ field }} <span class="error pull-right"></span>
                        </div>
                    {% endfor %}
    
                    <div class="form-group">
                        <label for="avatar">
                            头像
                            <img id="avatar_img" width="60" height="60" src="/static/img/default.png" alt="">
                        </label>
                        <input type="file" id="avatar" name="avatar">
                    </div>
    
                    <input type="button" class="btn btn-default reg_btn" value="submit"><span class="error"></span>
    
                </form>
    
            </div>
        </div>
    </div>
    
    
    <script>
        // 头像预览
        $("#avatar").change(function () {
    
            // 获取用户选中的文件对象
            var file_obj = $(this)[0].files[0];
            // 获取文件对象的路径
            var reader = new FileReader();
            reader.readAsDataURL(file_obj);
            // 修改img的src属性 ,src=文件对象的路径
            reader.onload = function () {
                $("#avatar_img").attr("src", reader.result)
            };
    
        });
    
        // 基于Ajax提交数据
    
        $(".reg_btn").click(function () {
            //console.log($("#form").serializeArray());
            var formdata = new FormData();
            var request_data = $("#form").serializeArray();
            $.each(request_data, function (index, data) {
                formdata.append(data.name, data.value)
            });
    
            formdata.append("avatar", $("#avatar")[0].files[0]);
    
            $.ajax({
                url: "",
                type: "post",
                contentType: false,
                processData: false,
                data: formdata,
                success: function (data) {
                    //console.log(data);
    
                    if (data.user) {
                        // 注册成功
                        location.href="/login/"
                    }
                    else { // 注册失败
    
                        //console.log(data.msg)
                        // 清空错误信息
                        $("span.error").html("");
                        $(".form-group").removeClass("has-error");
    
                        // 展此次提交的错误信息!
                        $.each(data.msg, function (field, error_list) {
                            console.log(field, error_list);
                            if (field=="__all__"){
                                $("#id_re_pwd").next().html(error_list[0]).parent().addClass("has-error");
                            }
                            $("#id_" + field).next().html(error_list[0]);
                            $("#id_" + field).parent().addClass("has-error");
    
    
                        })
    
                    }
                }
            })
    
        })
    
    
    </script>
    
    </body>
    </html>
    

      index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <link rel="stylesheet" href="/static/CSS/index.css">
        <link rel="stylesheet" href="/static/bootstrap-3.3.7/dist/css/bootstrap.css">
        <script rel="stylesheet" src="/static/JS/jquery-3.2.1.js"></script>
        <script rel="stylesheet" src="/static/bootstrap-3.3.7/dist/js/bootstrap.min.js"></script>
    
    </head>
    <body>
    
    <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="#">博客园</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="#">随笔<span class="sr-only">(current)</span></a></li>
            <li><a href="#">新闻</a></li>
            <li><a href="#">博文</a></li>
    
          </ul>
    
          <ul class="nav navbar-nav navbar-right">
              {% if request.user.is_authenticated %}
                  <li><a href="#"><span id="user_icon" class="glyphicon glyphicon-user"></span>{{ request.user.username }}</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="#">修改密码</a></li>
                        <li><a href="#">修改头像</a></li>
                        <li><a href="/logout/">注销</a></li>
                        <li role="separator" class="divider"></li>
                        <li><a href="#">Separated link</a></li>
                  </ul>
                </li>
              {% else %}
                  <li><a href="/login/">登录</a> </li>
                  <li><a href="/register/">注册</a> </li>
              {% endif %}
          </ul>
        </div>
      </div>
    </nav>
    
    <div class="container-fluid">
        <div class="row">
            <div class="col-md-3">
                <div class="panel panel-warning">
                    <div class="panel-heading">Panel heading without title</div>
                    <div class="panel-body">
                        Panel content
                    </div>
                </div>
                <div class="panel panel-info">
                    <div class="panel-heading">Panel heading without title</div>
                    <div class="panel-body">
                        Panel content
                    </div>
                </div>
                <div class="panel panel-danger">
                    <div class="panel-heading">Panel heading without title</div>
                    <div class="panel-body">
                        Panel content
                    </div>
                </div>
            </div>
            <div class="col-md-6">
                <div class="article_list">
                    {% for article in article_list %}
                        <div class="article-item">
                            <h5><a href="">{{ article.title }}</a></h5>
                            <div class="article-desc">
                                <span class="media-left">
                                    <a href=""><img height="56" width="56" src="media/{{ article.user.avatar }}" alt=""></a>
                                </span>
                                <span class="media-right">
                                    {{ article.desc }}
                                </span>
                            </div>
                            <div class="small pub_info">
                                <span><a href="">{{ article.user.username }}</a> </span>    
                                <span>发布于  {{ article.create_time|date:'Y-m-d:H:i' }}</span>   
                                <span class="glyphicon glyphicon-comment"></span>评论({{ article.comment_count }})  
                                <span class="glyphicon glyphicon-thumbs-up"></span>点赞({{ article.up_count }})
                            </div>
                        </div>
                            <hr>
                    {% endfor %}
                </div>
            </div>
            <div class="col-md-3">
                <div class="panel panel-default">
                    <div class="panel-heading">Panel heading without title</div>
                    <div class="panel-body">
                        Panel content
                    </div>
                </div>
                <div class="panel panel-primary">
                    <div class="panel-heading">Panel heading without title</div>
                    <div class="panel-body">
                        Panel content
                    </div>
                </div>
            </div>
        </div>
    </div>
    
    </body>
    </html>
    

      

      结果展示:

     2.3.2  头像的设置

     点击头像===点击input(这里使用label标签属性方法)

      首先,我们下载一个默认头像:

       注册头像的预览方法

       1,获取用户选中的问卷对象

       2,获取文件对象的路径

       3,修改img的src,src=文件路径对象

      取用户的标签,基于AJAX提交formdata数据

    // 基于AJAX 提交数据
        $(".reg_btn").click(function () {
    
            var formdata = new FormData();
            formdata.append('user', $("#id_user").val());
            formdata.append('pwd', $("#id_pwd").val());
            formdata.append('re_pwd', $("#id_re_pwd").val());
            formdata.append('email', $("#id_email").val());
            formdata.append('avatar', $("#avatar")[0].files[0]);
            formdata.append('csrfmiddlewaretoken', $('[name= "csrfmiddlewaretoken"]').val());
    
            $.ajax({
                url:"",
                type:"post",
                data: formdata,
                processData:false,
                contentType:false,
                success:function (data) {
                    console.log(data)
                }
            })
        })
    

      效果:

    2.3.3  AJAX在注册页面显示错误信息

      views: form.errors 

      Ajax.success方法  data.msg 就是上面的errors

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    
        <link rel="stylesheet" href="/static/blog/bootstrap-3.3.7-dist/css/bootstrap.css">
        <style>
            #avatar_image{
                margin-left: 20px;
            }
            #avatar{
                display: none;
            }
            .error{
                color: red;
            }
        </style>
    
    </head>
    <body>
    <h3 class=" text-center">注册页面</h3>
    <div class="container">
        <div class="row">
            <div class="col-md-6 col-lg-offset-3">
                <form>
                    {% csrf_token %}
                    {% for field in form %}
                        <div class="form-group">
    {#                        <label for="user">{{ field.label }}</label>#}
                            <label for={{ field.auto_id }}>{{ field.label }}</label>
                            {{ field }}<span class="error pull-right"></span>
                        </div>
                    {% endfor %}
    
                    <div class="form-group">
                        <label for="avatar">
                            头像
                            <img id="avatar_image" src="/static/img/default.jpg" alt="" width="60" height="60">
                        </label>
    
                        <input type="file" id="avatar">
                    </div>
    
                    <input type="button" value="Submit" class="btn btn-default reg_btn">
    
                </form>
            </div>
        </div>
    </div>
    
    </body>
    <script src="/static/JS/jquery-3.2.1.js"></script>
    <script>
    
        // 基于AJAX 提交数据
        $(".reg_btn").click(function () {
            console.log($("#form").serializeArray());
    
            var request_data = $("#form").serializeArray();
            $.each(request_data, function (index, data) {
               formdata.append(data.name, data.value)
            });
    
            var formdata = new FormData();
    
            formdata.append('avatar', $("#avatar")[0].files[0]);
            formdata.append('csrfmiddlewaretoken', $('[name= "csrfmiddlewaretoken"]').val());
    
            $.ajax({
                url:"",
                type:"post",
                data: formdata,
                processData:false,
                contentType:false,
                success:function (data) {
                    console.log(data);
                    if(data.user){
                        //  注册成功
                    }else{
                        // 注册失败
                        console.log(data.msg);
                        $.each(data.msg, function (field, error_list){
                            console.log(field, error_list);
                            $("#id_" +field).next().html(error_list[0])
                        })
                    }
                }
            })
        })
    </script>
    
    </html>
    

        结果展示:

       当我们用户输入后,需要清空用户名的错误信息。

    2.4  使用Admin 去录入数据

      (关于Django admin的详细内容,我们后面补充)

      这里我们基于admin 去录入文章数据。

      为了让admin界面管理我们的数据模型,我们需要先注册数据模型到admin。所以我们去 admin.py 中注册模型,代码如下:

    from django.contrib import admin
    
    # Register your models here.
    from blog import models
    
    admin.site.register(models.UserInfo)
    admin.site.register(models.Blog)
    admin.site.register(models.Category)
    admin.site.register(models.Tag)
    admin.site.register(models.Article)
    admin.site.register(models.ArticleUpDown)
    admin.site.register(models.Article2Tag)
    admin.site.register(models.Comment)
    

      注册后,我们需要通过下面命令来创建超级用户:

    python manage.py createsuperuser
    

      然后登陆Django的后台(http://127.0.0.1:8000/admin/),我们输入超级用户,进去如下:

      然后我们就可以录入数据了。

     

    2.5 个人站点页面的设计

    1.我的标签,随机分类,标签列表
        随机分类:     /username/category/
        我的标签:     /username/tag/
        随笔归档:     /username/archive/
       
    ​
    2.模板继承
        {% extends 'base.html' %}
    ​
        {% block content %}
        {% endblock content%}}
    ​
    3.自定义标签
        /blog/templatetags/my_tag.py
    ​
        @register.inclusion_tag('classification.html')
        def get_classification_style(username):
            ...
            return {} # 去渲染 menu.html
    ​
    4.分组查询 .annotate() / extra()应用
        多表分组
            tag_list = Tag.objects.filter(blog=blog).annotate(
                count = Count('article')).values_list('title', 'count')
    ​
        单表分组 / DATE_FORMAT() /  extra()
            date_list = Article.objects.filter(user=user).extra(
                select={"create_ym": "DATE_FORMAT(create_time,'%%Y-%%m')"}).values('create_ym').annotate(
                c = Count('nid')).values_list('create_ym', 'c')
    ​
    5. 时间、区域配置
         TIME_ZONE = 'Asia/Shanghai'
         USE_TZ = False
    

    2.5.1  个人站点设计的总体代码展示

      views.py

    def home_site(request, username, **kwargs):
        '''
        个人站点视图函数
        :param request:
        :return:
        '''
        print("执行的是home_site的内容")
        print('username:', username)
        user = UserInfo.objects.filter(username=username).first()
        #  判断用户是否存在
        if not user:
            return render(request, 'not_found.html')
    
        # 当用户存在的话 当前用户或者当前站点对应所有文章取出来
        # 1, 查询当前站点
        blog = user.blog
    
        # kwargs是为了区分访问的是站点页面还是站点下的跳转页面
        article_list = models.Article.objects.filter(user=user)
    
        if kwargs:
            condition = kwargs.get('condition')
            param = kwargs.get('param')
            print(condition)
            print(param)
            if condition == 'category':
                print(1)
                article_list = article_list.filter(category__title=param)
            elif condition == 'tag':
                print(2)
                article_list = article_list.filter(tags__title=param)
            else:
                print(3)
                year, month, day = 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, })
    

      home_site.html

    {% extends 'base.html' %}
    
    
    {% block content %}
     <div class="article_list">
        {% for article in article_list %}
            <div class="article-item clearfix">
                <h5><a href="/{{ article.user.username }}/articles/{{ article.pk }}">{{ article.title }}</a></h5>
                <div class="article-desc">
                    {{ article.desc }}
                </div>
                <div class="small pub_info pull-right">
                    <span>发布于   {{ article.create_time|date:"Y-m-d H:i" }}</span>  
                    <span class="glyphicon glyphicon-comment"></span>评论({{ article.comment_count }})  
                    <span class="glyphicon glyphicon-thumbs-up"></span>点赞({{ article.up_count }})  
                </div>
            </div>
            <hr>
        {% endfor %}
    
    </div>
    {% endblock %}
    

       结果展示:

    2.5.2  个人站点页面的文章查询

      当博客园用户站点不存在的时候,我们发现,会返回一个下面页面:

      当然,我们也可以做与上面一样的页面,其代码入下:

      not_found.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Error_404_资源不存在</title>
    
        <link rel="stylesheet" href="/static/bootstrap-3.3.7/dist/css/bootstrap.css">
    
        <style type="text/css">
            body{
                margin:8% auto 0;
                max- 550px;
                min-height: 200px;
                padding:10px;
                font-family: Verdana,Arial,Helvetica,sans-serif;
                font-size:14px;
                }
            p{
                color:#555;
                margin:10px 10px;
                }
            img {
                border:0px;
                }
            .d{
                color:#404040;
                }
    
        </style>
    
    </head>
    <body>
    
    <div class="container" style="margin-top: 100px">
        <div class="text-center">
            <a href="">
                <img src="/static/img/logo_small.gif" alt="">
            </a>
            <p>
                <b>404.</b>
                抱歉!您访问的资源不存在!
            </p>
            <p class="d">
                请确认您输入的网址是否正确,如果问题持续存在,请发邮件至
                <b>contact@qq.com</b>
                与我们联系。
            </p>
            <p>
                <a href="http://www.baidu.com/">返回百度查询</a>
            </p>
        </div>
    </div>
    
    </body>
    </html>
    

      

    2.5.3  个人站点页面的日期查询

      如何只拿出来 年和月?

     2.5.4  Extra函数的学习

       Django对一些复杂的函数不能一一对应,所以提供了一种extra函数。

     

    2.5.5  跳转过滤功能的实现

      views.py (home_site函数)

        article_list = models.Article.objects.filter(user=user)
        if kwargs:
            condition = kwargs.get('condition')
            param = kwargs.get('param')
            print(condition)
            print(param)
            if condition == 'category':
                print(1)
                article_list = article_list.filter(category__title=param)
            elif condition == 'tag':
                print(2)
                article_list = article_list.filter(tags__title=param)
            else:
                print(3)
                year, month, day = param.split("-")
                article_list = article_list.filter(create_time__year=year, create_time__month=month)
    

      home_site.html

    <div class="col-md-3">
                <div class="panel panel-warning">
                    <div class="panel-heading">我的标签</div>
                    <div class="panel-body">
                        {% for tag in tag_list %}
                            <p><a href="/{{ username }}/tag/{{ tag.0 }}" >{{ tag.0 }}({{ tag.1 }})</a></p>
                        {% endfor %}
                    </div>
                </div>
    
                <div class="panel panel-danger">
                    <div class="panel-heading">随笔分类</div>
                    <div class="panel-body">
                        {% for cate in cate_list %}
                            <p><a href="/{{ username }}/category/{{ cate.0 }}" >{{ cate.0 }}({{ cate.1 }})</a></p>
                        {% endfor %}
                    </div>
                </div>
    
                <div class="panel panel-success">
                    <div class="panel-heading">随笔归档</div>
                    <div class="panel-body">
                        {% for data in data_list %}
                            <p><a href="/{{ username }}/archive/{{ data.0 }}" >{{ data.0 }}({{ data.1 }})</a></p>
                        {% endfor %}
                    </div>
                </div>
            </div>
    

     

    2.6  文章详细页的设计

    1.文章详情页的设计
    
    2.文章详情页的数据构建
    
    3.文章详情页点赞样式的完成(基本仿照博客园)
    
    4.文章评论样式的添加(基本仿照博客园)
    
    5.文章评论树的添加(支持对对评论的评论)
    
    6.文章评论中邮件发送
    

    2.6.1 总体的代码及其样式展示

      views.py

    def get_classification_data(username):
        user = UserInfo.objects.filter(username=username).first()
        blog = user.blog
    
        cate_list = models.Category.objects.filter(blog=blog).values('pk').annotate(c=Count("article__title")).values_list(
            "title", "c")
    
        tag_list = models.Tag.objects.filter(blog=blog).values("pk").annotate(c=Count("article")).values_list("title", 'c')
    
        data_list = models.Article.objects.filter(user=user).extra(
            select={"y_m_date": "date_format(create_time, '%%Y-%%m-%%d')"}).values(
            'y_m_date').annotate(c=Count('nid')).values_list('y_m_date', 'c')
    
        return {"blog": blog, 'cate_list': cate_list, 'tag_list': tag_list, 'data_list': data_list}
    
    
    def article_detail(request, username, article_id):
        print("执行的是article_detail的内容")
    
        user = UserInfo.objects.filter(username=username).first()
        blog = user.blog
    
        article_obj = models.Article.objects.filter(pk=article_id).first()
    
        comment_list = models.Comment.objects.filter(article_id=article_id)
    
        return render(request, 'article_detail.html', locals())
    

      article_detail.html

    {% extends "base.html" %}
    
    
    {% block content %}
        {% csrf_token %}
        <div class="article_info">
            <h3 class="text-center title">{{ article_obj.title }}</h3>
            <div class="cont">
                {{ article_obj.content|safe }}
            </div>
    
            <div class="clearfix">
                <div id="div_digg">
                    <div class="diggit action">
                        <span class="diggnum" id="digg_count">{{ article_obj.up_count }}</span>
                    </div>
                    <div class="buryit action">
                        <span class="burynum" id="bury_count">{{ article_obj.down_count }}</span>
                    </div>
                    <div class="clear"></div>
                    <div class="diggword" id="digg_tips" style="color: red;"></div>
                </div>
            </div>
    
            <div class="comments list-group">
                <p class="tree_btn">评论树</p>
                <div class="comment_tree">
    
    
                </div>
    
                <script>
    
                     $.ajax({
                            url: "/get_comment_tree/",
                            type: "get",
                            data: {
                                article_id: "{{ article_obj.pk }}"
                            },
                            success: function (comment_list) {
                                console.log(comment_list);
    
                                $.each(comment_list, function (index, comment_object) {
    
                                    var pk = comment_object.pk;
                                    var content = comment_object.content;
                                    var parent_comment_id = comment_object.parent_comment_id;
                                    var s = '<div class="comment_item" comment_id=' + pk + '><span>' + content + '</span></div>';
    
                                    if (!parent_comment_id) {
    
                                        $(".comment_tree").append(s);
                                    } else {
    
                                        $("[comment_id=" + parent_comment_id + "]").append(s);
                                    }
                                })
    
                            }
                        })
    
                </script>
    
    
                <p>评论列表</p>
    
                <ul class="list-group comment_list">
    
                    {% for comment in comment_list %}
                        <li class="list-group-item">
                            <div>
                                <a href=""># {{ forloop.counter }}楼</a>   
                                <span>{{ comment.create_time|date:"Y-m-d H:i" }}</span>  
                                <a href=""><span>{{ comment.user.username }}</span></a>
                                <a class="pull-right reply_btn" username="{{ comment.user.username }}"
                                   comment_pk="{{ comment.pk }}">回复</a>
                            </div>
    
                            {% if comment.parent_comment_id %}
                                <div class="pid_info well">
                                    <p>
                                        {{ comment.parent_comment.user.username }}: {{ comment.parent_comment.content }}
                                    </p>
                                </div>
                            {% endif %}
    
                            <div class="comment_con">
                                <p>{{ comment.content }}</p>
                            </div>
    
                        </li>
                    {% endfor %}
    
    
                </ul>
    
                <p>发表评论</p>
                <p>昵称:<input type="text" id="tbCommentAuthor" class="author" disabled="disabled" size="50"
                             value="{{ request.user.username }}">
                </p>
                <p>评论内容:</p>
                <textarea name="" id="comment_content" cols="60" rows="10"></textarea>
                <p>
                    <button class="btn btn-default comment_btn">提交评论</button>
                </p>
            </div>
            <script>
                // 点赞请求
                $("#div_digg .action").click(function () {
                    var is_up = $(this).hasClass("diggit");
    
    
                    $obj = $(this).children("span");
    
                    $.ajax({
                        url: "/digg/",
                        type: "post",
                        data: {
                            "csrfmiddlewaretoken": $("[name='csrfmiddlewaretoken']").val(),
                            "is_up": is_up,
                            "article_id": "{{ article_obj.pk }}",
                        },
                        success: function (data) {
                            console.log(data);
    
                            if (data.state) {
                                var val = parseInt($obj.text());
                                $obj.text(val + 1);
                            }
                            else {
                                var val = data.handled ? "您已经推荐过!" : "您已经反对过!";
                                $("#digg_tips").html(val);
    
                                setTimeout(function () {
                                    $("#digg_tips").html("")
                                }, 1000)
    
                            }
    
                        }
                    })
    
                });
    
                // 评论请求
                var pid = "";
    
                $(".comment_btn").click(function () {
    
                    var content = $("#comment_content").val();
    
                    if (pid) {
                        var index = content.indexOf("
    ");
                        content = content.slice(index + 1)
                    }
    
    
                    $.ajax({
                        url: "/comment/",
                        type: "post",
                        data: {
                            "csrfmiddlewaretoken": $("[name='csrfmiddlewaretoken']").val(),
                            "article_id": "{{ article_obj.pk }}",
                            "content": content,
                            pid: pid
                        },
                        success: function (data) {
    
                            console.log(data);
    
                            var create_time = data.create_time;
                            var username = data.username;
                            var content = data.content;
    
                            var s = `
                               <li class="list-group-item">
                                  <div>
    
                                      <span>${create_time}</span>  
                                      <a href=""><span>${username}</span></a>
    
                                  </div>
                                  <div class="comment_con">
                                      <p>${content}</p>
                                  </div>
    
                                </li>`;
    
                            $("ul.comment_list").append(s);
    
                            // 清空评论框
                            pid = "",
                                    $("#comment_content").val("");
    
                        }
                    })
    
    
                });
    
                // 回复按钮事件
    
                $(".reply_btn").click(function () {
    
                    $('#comment_content').focus();
                    var val = "@" + $(this).attr("username") + "
    ";
                    $('#comment_content').val(val);
    
    
                    pid = $(this).attr("comment_pk");
    
    
                })
            </script>
    
        </div>
    {% endblock %}
    

      结果展示:

     2.6.2  点赞,评论,评论树及其发送邮件

     

    根评论:对文章的评论

    子评论:对评论的评论

    区别:是否有父评论

    评论:   1.构建样式

        2.提交根评论

        3.显示跟评论——render显示   ——AJAX显示

        4.提交子评论

        5.显示子评论——render显示   ——AJAX显示

        6.评论树的显示

      其代码展示

    def digg(request):
        """
        点赞功能
        :param request:
        :return:
        """
        print(request.POST)
    
        article_id = request.POST.get("article_id")
        is_up = json.loads(request.POST.get("is_up"))  # "true"
        # 点赞人即当前登录人
        user_id = request.user.pk
        obj = models.ArticleUpDown.objects.filter(user_id=user_id, article_id=article_id).first()
    
        response = {"state": True}
        if not obj:
            ard = models.ArticleUpDown.objects.create(user_id=user_id, article_id=article_id, is_up=is_up)
    
            queryset = models.Article.objects.filter(pk=article_id)
            if is_up:
                queryset.update(up_count=F("up_count") + 1)
            else:
                queryset.update(down_count=F("down_count") + 1)
        else:
            response["state"] = False
            response["handled"] = obj.is_up
    
        return JsonResponse(response)
    
    
    def comment(request):
        """
        提交评论视图函数
        功能:
        1 保存评论
        2 创建事务
        3 发送邮件
        :param request:
        :return:
        """
        print(request.POST)
    
        article_id = request.POST.get("article_id")
        pid = request.POST.get("pid")
        content = request.POST.get("content")
        user_id = request.user.pk
    
        article_obj = models.Article.objects.filter(pk=article_id).first()
    
        # 事务操作
        with transaction.atomic():
            comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content,
                                                        parent_comment_id=pid)
            models.Article.objects.filter(pk=article_id).update(comment_count=F("comment_count") + 1)
    
        response = {}
    
        response["create_time"] = comment_obj.create_time.strftime("%Y-%m-%d %X")
        response["username"] = request.user.username
        response["content"] = content
    
        # 发送邮件
    
        from django.core.mail import send_mail
        from cnblog import settings
    
        # send_mail(
        #     "您的文章%s新增了一条评论内容"%article_obj.title,
        #     content,
        #     settings.EMAIL_HOST_USER,
        #     ["916852314@qq.com"]
        # )
        ...
        import threading
    
        t = threading.Thread(target=send_mail, args=("您的文章%s新增了一条评论内容" % article_obj.title,
                                                     content,
                                                     settings.EMAIL_HOST_USER,
                                                     ["916852314@qq.com"])
                             )
        t.start()
       ...
        return JsonResponse(response)
    
    
    def get_comment_tree(request):
        article_id = request.GET.get("article_id")
        response = list(models.Comment.objects.filter(article_id=article_id).order_by("pk").values("pk", "content",
                                                                                                   "parent_comment_id"))
    
        return JsonResponse(response, safe=False)
    

      点赞的jQuery代码展示:

    $("#div_digg .action").click(function () {
                  var is_up = $(this).hasClass("diggit");
    
                    $obj = $(this).children('span');
                    $.ajax({
                        url: '/digg/',
                        type: 'post',
                        data: {
                            "csrfmiddlewaretoken": $("[name='csrfmiddlewaretoken']").val(),
                            "is_up": is_up,
                            "article_id": "{{ article_obj.pk }}",
    
                        },
                        success:function (data) {
                            //alert(is_up);
                            console.log(data);
                            if (data.state){
                                var val = parseInt($obj.text());
                                $obj.text(val+1);
                                {#if (is_up){#}
                                {#    var val=parseInt($("#digg_count").text());#}
                                {#    $("#digg_count").text(val+1);#}
                                //}
                                {#else{#}
                                {#    var val=parseInt($("#bury_count").text());#}
                                {#    $("#bury_count").text(val+1);#}
                                //}
                            }else {
                                var val = data.handled?"您已经推荐过!":"您已经反对过!";
                                $("#digg_tips").html(val);
                                {#if (data.handled){#}
                                {#    $("#digg_tips").html("您已经推荐过!")#}
                                //}else {
                                {#    $("#digg_tips").html("您已经反对过!")#}
                                //}
    
                                setTimeout(function () {
                                    $("#digg_tips").html()
                                }, 2000)
    
                            }
                        }
                    })
                });
    

      

      结果展示

       笔记:

       render显示根评论:

       效果如下:

     评论树的显示

      (我们需要将评论楼改为评论树展开)

     2.7  后台管理页面设计

    1.支持文章编辑
    
    2.支持富文本编辑器(支持渲染已有文章,并支持文本编辑器的上传功能)
    
    3.支持删除文章(未添加,很简单,可自行添加)
    
    4.防止Xss攻击(基于BS4)
    

        views.py

    @login_required
    def cn_backend(request):
        article_list = models.Article.objects.filter(user=request.user)
    
        return render(request, 'backend/backend.html', locals())
    
    
    from bs4 import BeautifulSoup
    
    
    @login_required
    def add_article(request):
        if request.method == 'POST':
            title = request.POST.get("title")
            content = request.POST.get("content")
    
            # 防止XSS攻击,过滤script
            soup = BeautifulSoup(content, "html.parser")
            for tag in soup.find_all():
    
                print(tag.name)
                if tag.name == 'script':
                    tag.decompose()
    
            # 构建摘要数据,获取标签字符串的文本前150个符号
            desc = soup.text[0:150] + "..."
            models.Article.objects.create(title=title, desc=desc, content=str(soup), user=request.user)
            return redirect('/cn_backend/')
        return render(request, "backend/add_article.html")
    
    
    def upload(request):
        '''
            编辑器上传文件接收视图函数
            :param request:
            :return:
            '''
        print(request.FILES)
        img_obj = request.FILES.get('upload_img')
        print(img_obj.name)
    
        path = os.path.join(settings.MEDIA_ROOT, 'add_article_img', img_obj.name)
    
        with open(path, 'wb') as f:
            for line in img_obj:
                f.write(line)
    
        response = {
            'error': 0,
            'url': '/media/add_article_img/%s' % img_obj.name
        }
        import json
        return HttpResponse(json.dumps(response))
    

      add_articles.html

    {% extends 'backend/base.html' %}
    
    {% block content %}
    
        <form action="" method="post">
            {% csrf_token %}
           <div class="add_article">
             <div class="alert-success text-center">添加文章</div>
    
             <div class="add_article_region">
                  <div class="title form-group">
                     <label for="">标题</label>
                     <div>
                         <input type="text" name="title">
                     </div>
                 </div>
    
                 <div class="content form-group">
                     <label for="">内容(Kindeditor编辑器,不支持拖放/粘贴上传图片) </label>
                     <div>
                         <textarea name="content" id="article_content" cols="30" rows="10"></textarea>
                     </div>
                 </div>
    
                 <input type="submit" class="btn btn-default">
    
             </div>
    
    
    
        </div>
        </form>
       <script src="/static/JS/jquery-3.2.1.min.js"></script>
       <script charset="utf-8" src="/static/kindeditor/kindeditor-all.js"></script>
    
        <script>
                KindEditor.ready(function(K) {
                        window.editor = K.create('#article_content',{
                            "800",
                            height:"600",
                            resizeType:0,
                            uploadJson:"/upload/",
                            extraFileUploadParams:{
                                csrfmiddlewaretoken:$("[name='csrfmiddlewaretoken']").val()
                            },
                            filePostName:"upload_img"
    
    
                        });
                });
        </script>
    
    
    {% endblock %}
    

      结果展示:

      添加文章

    3,Django Template  ==  View(MVC)

       Django的模板与经典MVC模式下的View一致。Django模板用来呈现Django View传来的数据,也决定了用户界面的外观。Template里面也包含了表单,可以用来收集用户的输入。

      那这部分内容,我决定单独写一篇博客记录内容。请参考:

     

    4,代码优化

    4.1  优化思路

      1,导入包的时候,需要先导入python标准库的包,再导入第三方插件的包,最后导入我们自己定义的包。

      2,冗余代码可以优化的话,自己优化

      3,开发中所有的 print得去掉,我们测试的时候可以加。

               # if avatar_obj:
                #     user_obj = UserInfo.objects.create_user(username=user, password=pwd, 
    email=email, avatar=avatar_obj)
                # else:
                #     user_obj = UserInfo.objects.create_user(username=user, password=pwd, 
    email=email)
    
    
                # 代码优化
                extra = {}
                if avatar_obj:
                    extra['avatar'] = avatar_obj
                # 要是逻辑没有用到值,我们可以不用赋值,等用到的时候,则添加
                UserInfo.objects.create_user(username=user, password=pwd, email=email, avatar=avatar_obj, **extra)
    

      

    4.2 注意问题

      注意问题1:由于我们使用django自带的AbstractUser扩展之后,需要进行更改AUTH_USER_MODEL的配置。

      我们在settings.py中设置:

    AUTH_USER_MODEL = 'blog.UserInfo'
    

      AUTH_USER_MODEL是等于APP blog下面的UserInfo表。因为UserInfo表集成的是自带的AbstractUser表。

      然后进行迁移。

       注意问题2:对于最新版的Django2.0,在使用一对一(OneToOneField)和外键(ForeignKey)时,需要加上on_delete 参数,不然就会报错。

    on_delete=models.CASCADE,    
    # 删除关联数据,与之关联也删除
    

      如果直接执行上述代码,遇到的报错如下:

    TypeError: __init__() missing 1 required positional argument: 'on_delete'
    

      因为 on_delete 在最新版的Django中已经是位置参数了。

    4.3  使用inclution_tag 优化代码

      base.html

     <div class="col-md-3 menu">
                {% load my_tags %}
                {% get_classification_style username %}
    
     </div>
    

      my_tags.py

    from django import template
    
    from blog import models
    from django.db.models import Count
    register = template.Library()
    
    @register.simple_tag
    def multi_tag(x, y):
        return x*y
    
    @register.inclusion_tag('classification.html')
    def get_classification_style(username):
        user = models.UserInfo.objects.filter(username=username).first()
        blog = user.blog
    
        cate_list = models.Category.objects.filter(blog=blog).values('pk').annotate(c=Count("article__title")).values_list(
            "title", "c")
    
        tag_list = models.Tag.objects.filter(blog=blog).values("pk").annotate(c=Count("article")).values_list("title", 'c')
    
        data_list = models.Article.objects.filter(user=user).extra(
            select={"y_m_date": "date_format(create_time, '%%Y-%%m-%%d')"}).values(
            'y_m_date').annotate(c=Count('nid')).values_list('y_m_date', 'c')
    
        return {"blog": blog, 'cate_list': cate_list, 'tag_list': tag_list, 'data_list': data_list}
    

     

    4.4  头像设置的代码优化

      取用户的标签,基于AJAX提交formdata数据

    // 基于AJAX 提交数据
        $(".reg_btn").click(function () {
    
            var formdata = new FormData();
            formdata.append('user', $("#id_user").val());
            formdata.append('pwd', $("#id_pwd").val());
            formdata.append('re_pwd', $("#id_re_pwd").val());
            formdata.append('email', $("#id_email").val());
            formdata.append('avatar', $("#avatar")[0].files[0]);
            formdata.append('csrfmiddlewaretoken', $('[name= "csrfmiddlewaretoken"]').val());
    
            $.ajax({
                url:"",
                type:"post",
                data: formdata,
                processData:false,
                contentType:false,
                success:function (data) {
                    console.log(data)
                }
            })
        })
    

      代码优化:

      // 基于AJAX 提交数据
        $(".reg_btn").click(function () {
            console.log($("#form").serializeArray());
    
            var request_data = $("#form").serializeArray();
            $.each(request_data, function (index, data) {
               formdata.append(data.name, data.value)
            });
    
            formdata.append('avatar', $("#avatar")[0].files[0]);
    
            $.ajax({
                url:"",
                type:"post",
                data: formdata,
                processData:false,
                contentType:false,
                success:function (data) {
                    console.log(data)
                }
            })
        })
    

      

     4.5  admin的代码优化

      我们查看之前写的admin.py的代码:

    from django.contrib import admin
    
    # Register your models here.
    from blog import models
    
    admin.site.register(models.UserInfo)
    admin.site.register(models.Blog)
    admin.site.register(models.Category)
    admin.site.register(models.Tag)
    admin.site.register(models.Article)
    admin.site.register(models.ArticleUpDown)
    admin.site.register(models.Article2Tag)
    admin.site.register(models.Comment)
    

      那如果要是存在100个表,我们需要写100个注册吗?当然不可能,为了简化代码,我们这样写。

      首先在model.py中,把所有的表名称写入一个列表中,如下:

    from django.db import models
    
    # Create your models here.
    __all__ = ['UserInfo', 'Blog', 'Category', 'Tag', 'Article', 'Article2Tag', 
           'ArticleUpDown', 'Comment']
    

      然后我们在admin.py中使用一个for循环进行注册。代码如下:

    from django.contrib import admin
    
    # Register your models here.
    from blog import models
    
    for table in models.__all__:
        admin.site.register(getattr(models, table))
    

      这样我们就可以节省代码。

     

     

  • 相关阅读:
    重新开发四则运算器
    结对编程
    四则运算 个人项目整理
    DAY 012--dict(增、删、改、查、合并)
    001--面向对象 V.S 面向过程
    DAY 011--Tuple使用方法汇总
    001--二分法(Binary search)
    DAY 006--查找某个值是否在列表中(二分法)
    DAY 009--str(替换三种方法)
    DAY 008--str(常见判断条件)
  • 原文地址:https://www.cnblogs.com/wj-1314/p/10960621.html
Copyright © 2020-2023  润新知