Python Django 學習紀錄:架設個人部落格(六)
點讚功能與模態框ajax登入
已經學了許多Django相關的知識
這次的點讚功能大致會使用到以下知識
- Django基礎(models、views、urls...)
- ContentType
- ajax
- 自定義模板標籤
一.、前端開發建議步驟
- 功能需求分析
- 模型設計
- 前端初步開發
- 後端實現
- 完善前端代碼
二.、點讚功能開發
1.功能設計
- 部落格文章、評論、回復可以點讚
- 可以取消點讚
- 可以看到點讚總數
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">×</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">×</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('用戶名稱或密碼錯誤'); } } }); });