close

Python Django 學習紀錄:架設個人部落格(六) 

點讚功能與模態框ajax登入

 

已經學了許多Django相關的知識

這次的點讚功能大致會使用到以下知識

  1. Django基礎(models、views、urls...)
  2. ContentType
  3. ajax
  4. 自定義模板標籤  

一.、前端開發建議步驟

  1. 功能需求分析
  2. 模型設計
  3. 前端初步開發
  4. 後端實現
  5. 完善前端代碼  

 

 二.、點讚功能開發

 1.功能設計

  1. 部落格文章、評論、回復可以點讚
  2. 可以取消點讚
  3. 可以看到點讚總數 

2.創建應用 : 

為了使點讚功能通用且獨立,所以將創建一個 點讚 應用

在虛擬環境下輸入以下指令創建 名為 likes 的應用:   

python manage.py startapp likes

接著到<settings.py>加入app:   

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'ckeditor',
    'ckeditor_uploader',
    'blog',
    'read_statistic',
    'comment',
    'likes',
]

3.模型設計與路徑設置

(1)模型設計 

打開 likes 應用的 <models.py> 文件

創建點讚統計與點讚紀錄的資料表

這邊會使用到 Contenttypes 框架來連接跟部落格模型的關係

以下為代碼:   

from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User

#點讚統計
class LikeCount(models.Model):
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) #透過外鍵指向模型
    object_id = models.PositiveIntegerField() #紀錄對應模型的主鍵值
    content_object = GenericForeignKey('content_type', 'object_id') #集合前兩項成一個通用外鍵

    liked_num = models.IntegerField(default=0) #讚數統計,預設為0

#點讚紀錄
class LikeRecord(models.Model):
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) #透過外鍵指向模型
    object_id = models.PositiveIntegerField() #紀錄對應模型的主鍵值
    content_object = GenericForeignKey('content_type', 'object_id') #集合前兩項成一個通用外鍵

    user = models.ForeignKey(User, on_delete=models.CASCADE)
    liked_time = models.DateTimeField(auto_now_add=True)

創建好模型後,進行數據庫遷移 (makemigrations)與應用 (migrate)

(2)路徑設置 

在 likes 應用創建 <urls.py> 文件

以下為代碼:   

from django.urls import path
from . import views

urlpatterns = [
    path('like_change',views.like_change, name='like_change'), 


]

在mysite專案目錄的主路徑檔<urls.py>導入likes應用的路徑:   

    path('likes/',include('likes.urls')),

3.前端初步開發

bootstap中文文檔選擇好想要的點讚圖標

這邊我選用"glyphicon glyphicon-thumbs-up"圖標

接著在<blog_detail.html>插入一個div標籤來顯示點讚功能

並插入一個onclick事件,當點擊下去的時候,插入或移除 class中的active

將使用js的ajax異步處理來完成

並使用CSS將無active的標籤設置為藍色,有active設置為紅色

以下為<blog_detail.html>代碼:   

    <div class="like" onclick="LikeChange(this, 'blog', {{ blog.pk }} )">
        <span class="glyphicon glyphicon-thumbs-up"></span>
        <span class="liked-num">0</span>
        <span>喜歡</span>
    </div>

以下為 js 代碼:   

        function LikeChange(obj, content_type, object_id){
            //判斷該標籤中是否有 active,沒有的話為True(數量為0)
            var is_like = obj.getElementsByClassName('active').length == 0
            //異步更新點讚狀態
            $.ajax({
                //調用 likes 應用 <views.py> 的 like_change 方法
                url: "{% url 'like_change' %}",
                type: 'GET',
                data: {
                    content_type: content_type,
                    object_id: object_id,
                    is_like: is_like
                },
                cache:false,
                success: function(data){
                    console.log(data)
                },
                error: function(xhr){
                    console.log(xhr)
                }
            });
        }

以下為<blog.css>代碼:   

div.like {
    color: #337ab7; /*藍色*/
    display: inline-block; /*改為行內*/
    padding: 0.5em 0.3em; /*縮距*/
    cursor: pointer; /*游標改為點擊*/
}
div.like .active {
    color: #f22; /*紅色*/
}

4.後端實現

在 likes 應用 的<views.py>撰寫 like_change 方法來獲取與處理資料

根據處理資料的結果將結果傳給SuccessResponse或ErrorResponse方法

SuccessResponse方法回傳成功訊息的資料給前端js

ErrorResponse方法回傳錯誤訊息的資料給前端js

(1)建立初步方法 

點下點讚的按鈕後,經由 js 代碼呼叫 like_change 方法 

使用GET方法獲取js傳遞的資料

並對於點讚或取消點讚、資料表為新創建或已經存在、增加或是減少讚數等方面進行判斷

以下為初步的代碼,部分內容先以註解代替來建構判斷的方法:   

from django.shortcuts import render
from .models import LikeCount, LikeRecord
from django.contrib.contenttypes.models import ContentType
from django.http import JsonResponse
from django.db.models import ObjectDoesNotExist

#點讚判斷
def like_change(request):
    #獲取數據
    user = request.user
    content_type = request.GET.get('content_type')
    object_id = request.GET.get('object_id')
    is_like = request.GET.get('is_like')

    #處理數據
    if is_like == 'true': #如果<div>標籤active屬性數量為0
        #要點讚,取得兩個返回值(like_record為對象,created為是否創建的狀態)
        if created: #如果創建了點讚紀錄
            #則該用戶未點讚過,進行點讚
            #點讚總數加1
            pass
            return SuccessResponse(like_count.liked_num)#點讚總數
        else: #已有點讚紀錄
            #該用戶點讚過,不重複點讚
            return ErrorResponse(402,'you were liked')
    else: #如果<div>標籤active屬性數量不為0
        #要取消點讚
        if LikeRecord.objects.filter(content_type=content_type, object_id=object_id, user=user).exists():#如果點讚紀錄存在
            #有點讚過,要取消點讚
            pass           
            if not created: #如果點讚記錄存在
            #點讚總數減1
                pass
                return SuccessResponse(like_count.liked_num)#點讚總數
            else: #如果點讚紀錄不存在
                return ErrorResponse(404,'data error')
        else: #如果點讚紀錄不存在
            return ErrorResponse(403,'you were not liked')

#傳遞成功狀態的訊息給前端js
def SuccessResponse(liked_num):
    data = {}
    data['status'] = 'SUCCESS'
    data['liked_num'] = liked_num
    return JsonResponse(data)

#傳遞錯誤狀態的訊息給前端js
def ErrorResponse(code, message):
    data = {}
    data['status'] = 'ERROR'
    data['code'] = code
    data['message'] = message
    return JsonResponse(data)

(2)完善方法

添加"使用者是否登入"的驗證

添加"傳入的模組與對象是否存在"的驗證

處理數據會使用到兩個模型:點讚紀錄(LikeRecord)、點讚統計(LikeCount)

點讚或取消點讚會調用LikeRecord

增加或減少讚數會調用LikeCount

以下為完善後的代碼:   

from django.shortcuts import render
from .models import LikeCount, LikeRecord
from django.contrib.contenttypes.models import ContentType
from django.http import JsonResponse
from django.db.models import ObjectDoesNotExist




#點讚判斷
def like_change(request):
    #獲取數據
        #驗證使用者
    user = request.user
    if not user.is_authenticated:
        return ErrorResponse(400,'you were not login')
        #驗證對象是否存在
    content_type = request.GET.get('content_type')
    object_id = int(request.GET.get('object_id'))
    try:
        content_type = ContentType.objects.get(model=content_type)  #取得模型
        model_class = content_type.model_class() #取得模型中的方法
        model_obj = model_class.objects.get(pk=object_id) #從model_class取得對象
    except ObjectDoesNotExist:
        return ErrorResponse(401,'object not exist')

    #處理數據
    if request.GET.get('is_like') == 'true': #如果<div>標籤active屬性數量為0
        like_record, created = LikeRecord.objects.get_or_create(content_type=content_type, object_id=object_id, user=user)#要點讚,取得兩個返回值(like_record為對象,created為是否創建的狀態)
        if created: #如果創建了點讚紀錄
            like_count, created = LikeCount.objects.get_or_create(content_type=content_type, object_id=object_id)#則該用戶未點讚過,進行點讚,調出LikeCount來操作
            like_count.liked_num += 1#點讚總數加1
            like_count.save()
            return SuccessResponse(like_count.liked_num)#點讚總數
        else: #已有點讚紀錄
            #該用戶點讚過,不重複點讚
            return ErrorResponse(402,'you were liked')
    else: #如果<div>標籤active屬性數量不為0
        if LikeRecord.objects.filter(content_type=content_type, object_id=object_id, user=user).exists():#如果點讚紀錄存在
            like_record = LikeRecord.objects.get(content_type=content_type, object_id=object_id, user=user) #要取消點讚紀錄
            like_record.delete()
            like_count, created = LikeCount.objects.get_or_create(content_type=content_type, object_id=object_id) #有點讚過,要取消點讚,調出LikeCount來操作
            if not created: #如果點讚記錄存在
                like_count.liked_num -= 1#點讚總數減1
                like_count.save()
                return SuccessResponse(like_count.liked_num)#點讚總數
            else: #如果點讚紀錄不存在
                return ErrorResponse(404,'data error')
        else: #如果點讚紀錄不存在
            return ErrorResponse(403,'you were not liked')

#傳遞成功狀態的訊息給前端js
def SuccessResponse(liked_num):
    data = {}
    data['status'] = 'SUCCESS'
    data['liked_num'] = liked_num
    return JsonResponse(data)

#傳遞錯誤狀態的訊息給前端js
def ErrorResponse(code, message):
    data = {}
    data['status'] = 'ERROR'
    data['code'] = code
    data['message'] = message
    return JsonResponse(data)

5.透過自定義模板標籤傳遞點讚總數與狀態

使用自定義模板標籤來傳遞點讚總數

通用性高,只要加載templatetags包,就可以顯示對應文章的點讚總數

而點讚狀態則是當點下讚後,使圖標獲得active狀態

由於需要驗證用戶是否登入,這邊使用了導入模板變數的方式來獲取用戶資料

(1)創建templatetags包

在likes應用創建templatetags目錄,並在其中創建<__init__.py>檔

在該目錄創建 <likes_tags.py> 並撰寫以下代碼:   

from django import template
from ..models import LikeCount, LikeRecord
from django.contrib.contenttypes.models import ContentType

register = template.Library()

@register.simple_tag
def get_like_count(obj):
    content_type = ContentType.objects.get_for_model(obj) #獲取Blog模型
    like_count, created =  LikeCount.objects.get_or_create(content_type=content_type, object_id=obj.pk) #LikeCount模型連接到Blog模型來獲取點讚總數
    return like_count.liked_num

@register.simple_tag(takes_context=True) #直接獲取所在模板標籤的變數
def get_like_status(context, obj):
    content_type = ContentType.objects.get_for_model(obj)
    user = context['user']  #從所在模板標籤獲取用戶資料
    if not user.is_authenticated: #如果驗證用戶失敗
        return ''
    if LikeRecord.objects.filter(content_type=content_type, object_id=obj.pk, user=user).exists(): #如果點讚紀錄存在
        return 'active'
    else:
        return ''

(2)前端模板顯示自定義模板標籤

在<blog_list.html>顯示總讚數:   

<span>讚({% get_like_count blog %})</span>

在<blog_detail.html>顯示總讚數與將圖標插入active狀態:   

<li>評論({% get_comment_count blog %})</li>
<div class="like" onclick="LikeChange(this, 'blog', {{ blog.pk }} )">
    <span class="glyphicon glyphicon-thumbs-up {% get_like_status blog %} "></span>
    <span class="liked-num">{% get_like_count blog %}</span>
    <span>喜歡</span>
</div>

 

6.評論與回復功能點讚

由於點讚功能設計的通用性高

因此只需要更換導入的模型即可變換點讚的對象

需要注意的是,模型與js方法的名字可能會產生衝突

所以這邊使用自定義模板標籤傳入模板,以避免衝突

(1)自定義模板標籤導入模型

在 likes 應用 templatetags 目錄的<likes_tags.py>並撰寫以下代碼:   

@register.simple_tag
def get_content_type(obj):
    content_type = ContentType.objects.get_for_model(obj)
    return content_type.model

(2)評論與回復加入點讚功能

這邊只要將點讚功能的html代碼更換導入的模型即可:   

                    <div id="comment_list">
                        {% get_comment_list blog as comments %}
                        {% for comment in comments %}
                            <div id="root_{{ comment.pk }}" class="comment">
                                <span>{{ comment.user }}</span>
                                <span>({{ comment.comment_time|date:"Y-m-d H:i:s" }}):</span>
                                <!-- 設置id,根據傳入的編號改變,並將最終的id的內容顯示到編輯器上方 -->
                                <div id="comment_{{ comment.pk }}">
                                    <span>{{ comment.text |safe }}</span>
                                </div>
                                <div class="like" onclick="LikeChange(this, 'comment', {{ comment.pk }} )">
                                    <span class="glyphicon glyphicon-thumbs-up {% get_like_status comment %} "></span>
                                    <span class="liked-num">{% get_like_count comment %}</span>
                                </div>
                                <!-- 點擊回復的同時取得編號 -->
                                <a href="javascript:reply({{ comment.pk }})">回復</a>
                               

                                {% for reply in comment.root_comment.all %}
                                    <div id="root_{{ reply.pk }}" class="reply">
                                        <span>{{ reply.user.username }}</span>
                                        <span>({{ reply.comment_time|date:"Y-m-d H:i:s" }}):</span>
                                        <span>回復</span>
                                        <span>{{ reply.reply_to.username }}</span>
                                        <!-- 設置id,根據傳入的編號改變,並將最終的id的內容顯示到編輯器上方 -->
                                        <div id="comment_{{ reply.pk }}">
                                            <span>{{ reply.text |safe }}
                                        </div>
                                        <div class="like" onclick="LikeChange(this, '{% get_content_type reply %}', {{ reply.pk }} )">
                                            <span class="glyphicon glyphicon-thumbs-up {% get_like_status reply %} "></span>
                                            <span class="liked-num">{% get_like_count reply %}</span>
                                        </div>
                                        <!-- 點擊回復的同時取得編號 -->
                                        <a href="javascript:reply({{ reply.pk }})">回復</a>

                                    </div>
                                {% endfor %}
                            </div>
                        {% empty %}
                            <span id="no_comment">暫無評論</span>
                        {% endfor %}
                    </div>

7.ajax更新後的評論與回復點讚

如果原先並沒有進行過評論回復,而是透過ajax才產生的

這裡產生的評論回復會沒有點讚功能

所以要修改原先ajax異步更新的評論回復代碼

將點讚功能的標籤也加入其中

(1) js 字串連接與變數替換

而由於會使用到的變數過多

所以會撰寫一個 js 的 function來替換變數:   

        //字串連接變數替換
        String.prototype.format = function(){
            var str = this;
            for (var i = 0; i < arguments.length; i++){
                var str = str.replace(new RegExp('\\{' + i + '\\}', 'g'), arguments[i]) //正則表達式全局替換
            };
            return str;
        }

接著因為使用評論與回復的點讚功能會使用到comment模型

文章點讚是採用自定義模板標籤來導入模型,但是js裡面沒辦法使用

所以要從comment應用<views.py>的update_comment方法來傳遞,

在送出評論回復後傳送data資料時同時傳送comment模型的資料到js

以下為 update_comment 需要修改的代碼:   

        #返回數據
        data['status'] = 'SUCCESS'
        data['username'] = comment.user.username
        data['comment_time'] = (comment.comment_time + datetime.timedelta(hours=8)).strftime("%Y-%m-%d %H:%M:%S")
        data['text'] = comment.text
        data['content_type'] = ContentType.objects.get_for_model(comment).model #從comment獲取關聯的的模型

接著為ajax評論與回復添加點讚功能

這邊使用到前面撰寫的字串連接變數替換的方法

而導入模版的部分因為單引號的緣故,所以要使用 \ 做轉譯處理:   

//插入評論
var comment_html = '<div id="root_{0}" class="comment">' +
    '<span>{1}</span>' +
    '<span>({2}):</span>' +
    '<div id="comment_{0}"><span>{3}</span></div>' +
        '<div class="like" onclick="LikeChange(this, \'{4}\', {0} )">' +
        '<span class="glyphicon glyphicon-thumbs-up "></span>' +
        '<span class="liked-num">0</span></div>' +
        '<a href="javascript:reply({0})">回復</a>' +
    '</div>';
    comment_html = comment_html.format(data['pk'],data['username'],data['comment_time'],data['text'],data['content_type']);
$("#comment_list").prepend(comment_html);
//動畫跳轉至評論
$('html').animate({scrollTop: $("#comment_list").offset().top - 120}, 300);

}else{
//插入回復
var reply_html =
            '<div id="root_{0}" class="reply">' +
                '<span>{1}</span>' +
                '<span>({2}):</span>' +
                '<span>回復</span>' +
                '<span>{3}</span>' +
                '<div id="comment_{0}">' +
                    '<span>{4}</span>' +
                '</div>' +
                '<div class="like" onclick="LikeChange(this, \'{5}\', {0} )">' +
                    '<span class="glyphicon glyphicon-thumbs-up "></span>' +
                    '<span class="liked-num">0</span>' +
                '</div>' +
                '<a href="javascript:reply({0})">回復</a>' +
            '</div>'
    reply_html = reply_html.format(data['pk'],data['username'],data['comment_time'],data['reply_to'],data['text'],data['content_type']);

$('#root_'+ data['root_pk']).append(reply_html);
//動畫跳轉至回復
$('html').animate({scrollTop: $("#root_" + data['pk']).offset().top - 300}, 300);
}

 

 三.、模態框與ajax登入

1.使用Bootstrap模態框js插件

打開Bootstrap文檔,找到模態框js插件範例

將其加入到<blog_detail.html>模板的content模塊尾端

以下為代碼:   

<div class="modal fade" id="login_modal" tabindex="-1" role="dialog">
    <div class="modal-dialog modal-sm" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
                <h4 class="modal-title">登錄</h4>
            </div>
            <div class="modal-body">
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-primary">登錄</button>
                <button type="button" class="btn btn-default" data-dismiss="modal">關閉</button>
            </div>
        </div><!-- /.modal-content -->
    </div><!-- /.modal-dialog -->
</div><!-- /.modal -->

2. 使用 js 開啟登錄模態框

在點讚功能的例外處理添加打開登錄模態框的代碼:   

                    else{
                        if(data['code']==400){ //如果使用者未登錄
                            $('#login_modal').modal('show'); //開啟登錄模態框
                        }else{
                            alert(data['message']); //顯示錯誤訊息
                        }
                    }

3. 模態框加入登錄表單

在blog應用的<views.py>導入Loginform表單

並在blog_detail方法將表單資料傳送到前端模板:   

from mysite.forms import LoginForm

context['login_form'] = LoginForm()

從mysite專案的<login.html>將登錄表單加入到登錄模態框的代碼中

將<form>標籤包住整個模態框,並加入id讓js選取

改動登錄按鈕、並將錯誤訊息改為透過js傳遞:   

        <div class="modal fade" id="login_modal" tabindex="-1" role="dialog">
            <div class="modal-dialog modal-sm" role="document">
                <div class="modal-content">
                    <form id="login_modal_form" action="" method="POST">
                        <div class="modal-header">
                            <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
                            <h4 class="modal-title">登錄</h4>
                        </div>
                        <div class="modal-body">
                                {% csrf_token %}
                                <!-- 進行表單細部調整 -->
                                {% for field in login_form %}
                                    <label for="{{ field.id_for_label }}">{{field.label}}</label>
                                    {{ field }}
                                {% endfor %}
                                <!-- 錯誤訊息透過id由js傳入 -->
                                <span class="text-danger" id="login_modal_tip"></span>
                        </div>
                        <div class="modal-footer">
                            <button type="submit" class="btn btn-primary">登錄</button>
                            <button type="button" class="btn btn-default" data-dismiss="modal">關閉</button>
                        </div>
                    </form>
                </div><!-- /.modal-content -->
            </div><!-- /.modal-dialog -->
        </div><!-- /.modal -->

4. 撰寫模態框登錄方法

在 mysite 專案的 <views.py>添加模態框登錄方法

最後將資料轉為Json格式傳給前端的 js 處理:   

from django.http import JsonResponse
def login_for_modal(request):
    login_form = LoginForm(request.POST)
    data ={}
    if login_form.is_valid(): #若表單有效
        user = login_form.cleaned_data['user']  #取得用戶資料
        auth.login(request, user) #進行登錄
        data['status'] = 'SUCCESS'
    else:
        data['status'] = 'ERROR'
    return JsonResponse(data)

寫完方法後要設置觸發方法的路徑

在mysite專案的<url.py>添加路徑:   

    path('login_for_modal/', views.login_for_modal, name='login_for_modal'),

5. js提交表單

點擊登錄按鈕觸發submit事件,為了不刷新頁面所以導入一個event阻止刷新

接著透過模態框登錄方法取得用戶資料與登錄:   

        $('#login_modal_form').submit(function(event){ //送出後阻止頁面刷新
            event.preventDefault(); //送出後阻止頁面刷新
            $.ajax({
                url: "{% url 'login_for_modal' %}",
                type: 'POST',
                data: $(this).serialize(), //序列化表單值
                cache: false,
                success: function(data){
                    console.log(data);
                    if(data['status']=='SUCCESS'){
                        window.location.reload(); //當前窗口重新加載
                    }else{
                        $('#login_modal_tip').text('用戶名稱或密碼錯誤');
                    }
                }
            });
        });
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 ivankao 的頭像
    ivankao

    IvanKao的部落格

    ivankao 發表在 痞客邦 留言(0) 人氣()