Django學習筆記(18)——BBS+Blog專案開發(2)主體思路及流程

戰爭熱誠發表於2019-06-22

  這篇部落格主要完成一個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-width: 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("\n");
                    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") + "\n";
                $('#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)

                        }
                    }
                })
            });

  

  結果展示

 

 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',{
                        width:"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)
            }
        })
    })

  

 

 

 

 

相關文章