zoukankan      html  css  js  c++  java
  • 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))
    

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

     

     

  • 相关阅读:
    飞入飞出效果
    【JSOI 2008】星球大战 Starwar
    POJ 1094 Sorting It All Out
    POJ 2728 Desert King
    【ZJOI 2008】树的统计 Count
    【SCOI 2009】生日快乐
    POJ 3580 SuperMemo
    POJ 1639 Picnic Planning
    POJ 2976 Dropping Tests
    SPOJ QTREE
  • 原文地址:https://www.cnblogs.com/wj-1314/p/10960621.html
Copyright © 2011-2022 走看看