Python Django 學習紀錄:架設個人部落格(三)
通用模型與閱讀計數
一.、閱讀計數與通用模型
1.簡單計數方法:
(1)在模組中添加計數方法
在教程中有實際操作,但由於最後不採用,所以在此處省略詳細操作過程
在blog應用的模型中添加計數的方法,使用外鍵相連
並根據使用者點擊detail頁面來增加閱讀次數
(2)簡單計數方法的缺點
後台編輯部落格文章可能會影響數據
功能單一,無法統計某一天的閱讀次數
2.計數功能獨立:
(1)使用ContentTypes
ContentTypes框架是一個默認安裝在Django中的框架
它可以追蹤 Django 安裝的模型,並為模型提供高級的通用接口
(2)創建 閱讀計數 應用(read_statistic)
開啟虛擬環境,並創建一個名為 read_statistic的應用:
python manage.py startapp read_statistic
並在<settings.py>的應用中,添加以下代碼:
'read_statistic',
打開read_statistic應用的<models.py>,依照 contenttypes 的框架,添加以下代碼:
from django.db import models from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType class ReadNum(models.Model): read_num = models.IntegerField(default=0) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)#透過外鍵指向模型 object_id = models.PositiveIntegerField() #紀錄對應模型的主鍵值 content_object = GenericForeignKey('content_type', 'object_id') #集合前兩項成一個通用外鍵
記得進行數據庫遷移 (makemigrations)與應用 (migrate)
接著打開read_statistic應用的<admin.py>添加以下代碼:
from django.contrib import admin from .models import ReadNum @admin.register(ReadNum) class ReadNumAdmin(admin.ModelAdmin): list_display = ('read_num','content_object')
將模型註冊到admin後就能runserver來檢查程式是否有出現紕漏
(3)通用模型與應用
接下來要撰寫blog模型與read_statistic的關聯
後續還有將閱讀次數儲存入cookie的方法
如果將這兩個部分寫在blog應用會導致不夠通用的問題
所以會將這些方法寫在read_statistic應用中
首先是外鍵關聯的部分,寫在<models.py>中:
from django.db import models from django.db.models.fields import exceptions #導入例外處理 from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType class ReadNum(models.Model): read_num = models.IntegerField(default=0) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)#透過外鍵指向模型 object_id = models.PositiveIntegerField() #紀錄對應模型的主鍵值 content_object = GenericForeignKey('content_type', 'object_id') #集合前兩項成一個通用外鍵 class ReadNumExpandMethod(): #創建擴展用的類別 def get_read_num(self): #讓被導入的模組,取得關聯的項目編號,若不存在則返回0 try: ct = ContentType.objects.get_for_model(self) #獲取模型內容並存入變數 readnum = ReadNum.objects.get(content_type=ct, object_id=self.pk) return readnum.read_num except exceptions.ObjectDoesNotExist: return 0
修改blog應用的<models.py>,並導入ReadNumExpandMethod方法:
from django.db import models from django.contrib.auth.models import User #導入內建的使用者模組 from ckeditor_uploader.fields import RichTextUploadingField #改為從uploader導入 RichTextUploadingField from read_statistic.models import ReadNumExpandMethod #導入閱讀計數應用的模組 class BlogType(models.Model): #部落格文章類型 type_name = models.CharField(max_length=15) def __str__(self): return self.type_name class Blog(models.Model, ReadNumExpandMethod): #部落格文章,導入閱讀計數應用中的擴展類別 title = models.CharField(max_length=50) content = RichTextUploadingField() author = models.ForeignKey(User, on_delete=models.CASCADE) #使用外鍵連接到使用者模組,由於尚未建立先不做任何事情 created_time = models.DateTimeField(auto_now_add = True) last_updated_time = models.DateTimeField(auto_now = True) blog_type = models.ForeignKey(BlogType, on_delete=models.CASCADE) #使用外鍵連接到部落格文章類型 def __str__(self): return "<Blog: %s>" % self.title class Meta: ordering = ['-created_time']
(3)通用計數工具與前端顯示
每開啟一次<blog_detail.html>將計數一次,並使用cookie紀錄下來
如果把這段方法寫在blog應用會變得不夠通用,所以獨立出來
在read_statistic應用中創建一個新的python文件,名為 <utils.py>
並且撰寫以下計數方法:
from django.contrib.contenttypes.models import ContentType from .models import ReadNum def read_statistic_once_read(request, obj): ct = ContentType.objects.get_for_model(obj) key = "%s_%s_read" % (ct.model, obj.pk) if not request.COOKIES.get(key): #如果cookie中沒有紀錄 if ReadNum.objects.filter(content_type=ct, object_id=obj.pk).count(): #查找是否創建過該項目 #存在項目 readnum = ReadNum.objects.get(content_type=ct, object_id=obj.pk) #對應該項目 else: #不存在項目 readnum = ReadNum(content_type=ct, object_id=obj.pk) #創建項目 readnum.read_num += 1 #在該項目增加1 readnum.save() #儲存資料 return key #返回的模型與值
修改blog應用中的blog_detail 方法,導入計數方法
若開啟<blog_detail.html>網頁則使用計數方法:
def blog_detail(request, blog_pk): #導入 blog_pk 變數 context={} #建立字典 blog = get_object_or_404(Blog, pk=blog_pk) #獲取blog_pk read_cookie_key = read_statistic_once_read(request, blog) #使用閱讀計數 #資料庫文章日期已經為由新到舊排列(新文章日期大於舊文章日期) #獲取前一篇文章,取得大於該文章的日期中的最後一篇 context['previous_blog'] = Blog.objects.filter(created_time__gt=blog.created_time).last() #獲取後一篇文章,取得小於該文章的日期中的第一篇 context['next_blog'] = Blog.objects.filter(created_time__lt=blog.created_time).first() context['blog'] = blog response = render(request, 'blog/blog_detail.html', context) #將字典返回到'blog_detail.html' response.set_cookie(read_cookie_key, 'true') #閱讀cookie return response
將閱讀統計顯示到前端頁面<blog_list.html>:
<p class="blog-info"> <span class="glyphicon glyphicon-tag"></span>分類:<a href="{% url 'blogs_with_type' blog.blog_type.pk %}">{{ blog.blog_type }}</a> <span class="glyphicon glyphicon-time"></span>發表日期:{{ blog.created_time|date:"Y-m-d" }} <span>閱讀({{ blog.get_read_num }})</span> </p>
二.、閱讀計數統計和顯示
1.統計需要紀錄明細:
(1)在模組中添加紀錄明細的方法
在read_statistic應用的<models.py>中導入 timezone 套件
接著添加一個名為 ReadDetail 的方法 來記錄明細:
from django.utils import timezone class ReadDetail(models.Model): date = models.DateField(default=timezone.now) #日期 read_num = models.IntegerField(default=0) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)#透過外鍵指向模型 object_id = models.PositiveIntegerField() #紀錄對應模型的主鍵值 content_object = GenericForeignKey('content_type', 'object_id') #集合前兩項成一個通用外鍵
記得進行數據庫遷移 (makemigrations)與應用 (migrate)
(2)註冊到admin
在<admin.py>添加以下代碼來註冊,方能到Django Admin 進行操作:
from .models import ReadNum, ReadDetail @admin.register(ReadDetail) class ReadDetailAdmin(admin.ModelAdmin): list_display = ('date','read_num','content_object')
(3)日期閱讀次數添加
在<utils.py>已經撰寫了一個增加總閱讀次數的方法
而現在也要將增加當日閱讀次數加入其中
這裡優化了原本的條件選擇,使用 get_or_create 來查詢是否已經創建,若無則新建:
from django.contrib.contenttypes.models import ContentType from django.utils import timezone from .models import ReadNum, ReadDetail def read_statistic_once_read(request, obj): ct = ContentType.objects.get_for_model(obj) key = "%s_%s_read" % (ct.model, obj.pk) if not request.COOKIES.get(key): #如果cookie中沒有紀錄 #閱讀總數 +1 #查找是否創建過該項目,若無則自動創建 readnum, created = ReadNum.objects.get_or_create(content_type=ct, object_id=obj.pk) readnum.read_num += 1 #在該項目增加1 readnum.save() #儲存資料 # 當天閱讀數 +1 date = timezone.now().date() #查找是否創建過該項目,若無則自動創建 readDetail, created = ReadDetail.objects.get_or_create(content_type=ct, object_id=obj.pk, date=date) readDetail.read_num += 1 readDetail.save() return key #返回的模型與值
(4)七日閱讀次數統計
在<utils.py>撰寫一個儲存日期與統計七日閱讀次數的方法
這裡採用聚合計算的方式,將同日期的閱讀次數加總,並存到串列:
from django.utils import timezone #導入日期方法 from django.db.models import Sum #導入求和方法 def get_seven_days_read_data(content_type): today = timezone.now().date() #當天日期 dates =[] #儲存日期 read_nums = [] #該日期加總聚合計算串列 for i in range(7, 0, -1): #迴圈遞減(七天) date = today - datetime.timedelta(hours=-8,days=i) #台灣時間快8小時,所以減去 dates.append(date.strftime('%m/%d')) read_details = ReadDetail.objects.filter(content_type=content_type, date=date) #找出所有該日期資料 #聚合計算 result = read_details.aggregate(read_num_sum=Sum('read_num')) #將聚合計算結果加入串列 read_nums.append(result['read_num_sum'] or 0) #若無數據,則返回 0 return dates, read_nums
在 mysite 專案 的<views.py>修改以下內容來傳入七日閱讀次數統計
將字典傳送到<home.html>來顯示資料:
from django.shortcuts import render, render_to_response from read_statistic.utils import get_seven_days_read_data from django.contrib.contenttypes.models import ContentType from blog.models import Blogfrom django.shortcuts import render, render_to_response from read_statistic.utils import get_seven_days_read_data from django.contrib.contenttypes.models import ContentType from blog.models import Blog def home(request): blog_content_type = ContentType.objects.get_for_model(Blog) #導入Blog模組關聯 dates, read_nums = get_seven_days_read_data(blog_content_type ) #代入Blog 模組來統計 context ={} context['dates'] = dates #日期 context['read_nums'] = read_nums #七日統計 return render(request, 'home.html',context)
(5)前端頁面圖表顯示
我們將閱讀次數統計顯示在首頁上
這裡使用 Highcharts 來完成圖表的部分
在<head>內引入js檔,並依照官網教學創建圖表
並且置換掉其中的內容改為方才的日期、七日統計次數
日期的部分要使用safe轉換html編碼
以下為<home.html>修改後的代碼:
{% extends "base.html" %} {% load staticfiles %} <!-- 加載靜態文件 --> {% block titles %} 我的網站 | 首頁 {% endblock %} {% block header_extends %} <!-- 導入 css 文件 --> <link rel="stylesheet" href="{% static 'css/home.css' %}"> <script src="http://cdn.hcharts.cn/highcharts/highcharts.js"></script> {% endblock %} {% block nav_home_active %}active{% endblock %} {% block content %} <!-- 加入 class 標籤讓 css 文件辨別 --> <h3 class="home-content">歡迎訪問我的網站!</h3> <!-- 图表容器 DOM --> <div id="container" style="width: 600px;height:400px;"></div> <script> // 图表配置 var chart = Highcharts.chart('container', { chart: { type: 'area' }, title: { text: '近日閱讀次數統計' }, xAxis: { categories: {{ dates|safe }}, tickmarkPlacement: 'on', title: { enabled: false } }, yAxis: { title: { text: null }, labels: { enabled: false }, gridLineDashStyle: 'Dash', }, plotOptions: { area: { stacking: 'normal', lineColor: '#666666', lineWidth: 1, marker: { lineWidth: 1, lineColor: '#666666' } } }, series: [{ name: '閱讀量', data: {{ read_nums }} }] }); </script> {% endblock %}
(6)調整CSS樣式
最後將 <home.css>作一些調整,讓圖表顯示的位置與文字顯得整齊:
h3.home-content { font-size: 222%; text-align: center; /*置中*/ margin-top: 4em; margin-bottom: 2em; } div#container { margin: 0 auto; height: 20em; min-width: 20em; max-width: 30em; }
完成後,範例如下:
三.、閱讀計數排行
1.當日、昨日熱門文章:
(1)後端方法撰寫
在read_statistic應用的<utils.py>中t創建兩個方法
get_today_hot_data 用來統計當日熱門文章
get_yesterday_hot_data 用來統計昨日熱門文章
兩者皆使用 Contenttypes通用接口,以下為代碼:
#當日熱門文章 def get_today_hot_data(content_type): #選取當日時間 today = timezone.now().date() #篩選當日文章,依照時間遞減排序 read_details = ReadDetail.objects.filter(content_type=content_type, date=today).order_by('-read_num') #返回摘選後資料,並使用切片器挑選前3項 return read_details[:3] #昨日熱門文章 def get_yesterday_hot_data(content_type): #選取當日時間 today = timezone.now().date() yesterday = today - datetime.timedelta(days=1) #篩選昨日文章,依照時間遞減排序 read_details = ReadDetail.objects.filter(content_type=content_type, date=yesterday).order_by('-read_num') #返回摘選後資料,並使用切片器挑選前3項 return read_details[:3]
將方法導入 mysite 的 <views.py>在首頁來顯示熱門文章
以下為代碼:
from django.shortcuts import render, render_to_response from read_statistic.utils import get_seven_days_read_data, get_today_hot_data, get_yesterday_hot_data from django.contrib.contenttypes.models import ContentType from blog.models import Blog def home(request): blog_content_type = ContentType.objects.get_for_model(Blog) #導入Blog模組關聯 dates, read_nums = get_seven_days_read_data(blog_content_type ) #代入Blog 模組來統計 context ={} context['dates'] = dates #日期 context['read_nums'] = read_nums #七日統計 context['today_hot_data'] = get_today_hot_data(blog_content_type) #取得當日熱門貼文資料 context['yesterday_hot_data'] = get_yesterday_hot_data(blog_content_type) return render(request, 'home.html',context)
(2)前端頁面顯示
在<home.html>頁面添加以下代碼,來顯示當日、昨日熱門文章:
<div class="hot_data"> <!-- 當日熱門文章 --> <h3>當日熱門文章</h3> <ul> {% for hot_data in today_hot_data %} <li> <a href="{% url 'blog_detail' hot_data.content_object.pk %}"> {{ hot_data.content_object.title }} </a> ({{ hot_data.read_num }}) </li> {% empty %} <li>今日暫時沒有熱門文章</li> {% endfor %} </ul> </div> <div class="hot_data"> <!-- 昨日熱門文章 --> <h3>昨日熱門文章</h3> <ul> {% for hot_data in yesterday_hot_data %} <li> <a href="{% url 'blog_detail' hot_data.content_object.pk %}"> {{ hot_data.content_object.title }} </a> ({{ hot_data.read_num }}) </li> {% empty %} <li>昨日暫時沒有熱門文章</li> {% endfor %} </ul> </div>
2.七日、三十日熱門文章:
(1)後端方法撰寫
與當日、昨日熱門文章不同的地方在於需要將多個日期的次數加總
而採用Contenttypes的方法會不好獲取文章標題
所以這裡採用反向關聯模型的方法,將 blog應用中 <models.py> 的 Blog 模型關連到 ReadDetail模型
所以在blog應用的<models.py>中 添加與修改以下內容:
from django.contrib.contenttypes.fields import GenericRelation #反向關聯模型 class Blog(models.Model, ReadNumExpandMethod): title = models.CharField(max_length=50) content = RichTextUploadingField() author = models.ForeignKey(User, on_delete=models.CASCADE) created_time = models.DateTimeField(auto_now_add = True) last_updated_time = models.DateTimeField(auto_now = True) blog_type = models.ForeignKey(BlogType, on_delete=models.CASCADE) read_details = GenericRelation(ReadDetail) #反向關聯模型 def __str__(self): return "<Blog: %s>" % self.title class Meta: ordering = ['-created_time']
在read_statistic應用的<utils.py>中t創建兩個方法
get_seven_hot_data 用來統計七日熱門文章
get_month_hot_data 用來統計三十日熱門文章
這裡就不須用 contenttypes 框架,而是導入Blog的模型來進行反向關聯
也可以將這兩個方法寫在mysite的<views.py>中,來避免read_statistic應用分工不明確的問題
依照開發者的習慣可以自行調整,而我個人偏向將所有熱門排行的方法放在同一個文件
聚合計算的部分寫在註解中,以下為代碼:
#七日熱門文章 def get_seven_hot_data(): #選取當日時間 today = timezone.now().date() date = today - datetime.timedelta(days=7) #篩選七日文章,依照時間遞減排序,__lte小於等於,__gte大於等於,並使用 values、annotate與Sum 聚合計算 blogs = Blog.objects \ .filter(read_details__date__lte=today, read_details__date__gte=date) \ .values('id', 'title') \ .annotate(read_num_sum=Sum('read_details__read_num')) \ .order_by('-read_num_sum') #返回摘選後資料,並使用切片器挑選前3項 return blogs[:3] #三十日熱門文章 def get_month_hot_data(): #選取當日時間 today = timezone.now().date() date = today - datetime.timedelta(days=30) #篩選三十日文章,依照時間遞減排序,__lte小於等於,__gte大於等於,並使用 values、annotate與Sum 聚合計算 blogs = Blog.objects \ .filter(read_details__date__lte=today, read_details__date__gte=date) \ .values('id', 'title') \ .annotate(read_num_sum=Sum('read_details__read_num')) \ .order_by('-read_num_sum') #返回摘選後資料,並使用切片器挑選前3項 return blogs[:3]
將方法導入 mysite 的 <views.py>在首頁來顯示熱門文章
以下為代碼:
from django.shortcuts import render, render_to_response from read_statistic.utils import get_seven_days_read_data, get_today_hot_data, get_yesterday_hot_data, get_seven_hot_data, get_month_hot_data from django.contrib.contenttypes.models import ContentType from blog.models import Blog def home(request): blog_content_type = ContentType.objects.get_for_model(Blog) #導入Blog模組關聯 dates, read_nums = get_seven_days_read_data(blog_content_type ) #代入Blog 模組來統計 context ={} context['dates'] = dates #日期 context['read_nums'] = read_nums #七日統計 context['today_hot_data'] = get_today_hot_data(blog_content_type) #取得當日熱門貼文資料 context['yesterday_hot_data'] = get_yesterday_hot_data(blog_content_type) context['seven_hot_data'] = get_seven_hot_data() context['month_hot_data'] = get_month_hot_data() return render(request, 'home.html',context)
(2)前端頁面顯示
在<home.html>頁面添加以下代碼,來顯示七日、三十日熱門文章:
<div class="hot_data"> <!-- 七日熱門文章 --> <h3>七日熱門文章</h3> <ul> {% for hot_data in seven_hot_data %} <li> <a href="{% url 'blog_detail' hot_data.id %}"> {{ hot_data.title }} </a> ({{ hot_data.read_num_sum }}) </li> {% empty %} <li>近七日暫時沒有熱門文章</li> {% endfor %} </ul> </div> <div class="hot_data"> <!-- 三十日熱門文章 --> <h3>三十日熱門文章</h3> <ul> {% for hot_data in month_hot_data %} <li> <a href="{% url 'blog_detail' hot_data.id %}"> {{ hot_data.title }} </a> ({{ hot_data.read_num_sum }}) </li> {% empty %} <li>近三十日暫時沒有熱門文章</li> {% endfor %} </ul> </div>
3.CSS方法撰寫:
在<home.html>中四種熱門文章的部分,都有將class命名為hot_data
所以在<home.css>添加以下代碼,將其置中:
div.hot_data { text-align: center; margin-top: 2em; }
四.、緩存提速
1.設置緩存:
如果每次都重新計算,會十分耗時
所以藉由緩存數據,來提升網頁的運行速度的
緩存有分為:內存緩存(Memcached、Redis)、數據庫緩存、文件緩存
這裡我採用數據庫緩存的方式
Django cache 框架說明文件 詳細的部分可以參考此文件
首先在 <settings.py>文件的最下方添加緩存配置:
#緩存配置 CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.db.DatabaseCache', 'LOCATION': 'my_cache_table', } }
接著在虛擬環境創建緩存表:
python manage.py createcachetable
2.撰寫緩存程式:
在mysite 的 <views.py> 的開頭導入cache方法
from django.core.cache import cache
接著撰寫緩存數據的代碼,註解已經寫在代碼中:
from django.shortcuts import render, render_to_response from read_statistic.utils import get_seven_days_read_data, get_today_hot_data, get_yesterday_hot_data, get_seven_hot_data, get_month_hot_data from django.contrib.contenttypes.models import ContentType from blog.models import Blog from django.core.cache import cache #導入cache方法 def home(request): blog_content_type = ContentType.objects.get_for_model(Blog) #導入Blog模組關聯 dates, read_nums = get_seven_days_read_data(blog_content_type ) #代入Blog 模組來統計 #緩存數據 today_hot_data = cache.get('today_hot_data') #取得緩存數據 if today_hot_data is None: #若無數據,則從資料庫查找資料 today_hot_data = get_today_hot_data(blog_content_type) #從資料庫查找資料 cache.set('today_hot_data', today_hot_data, 3600) #取得資料後,將資料存到緩存表中,並設定時間為1小時 yesterday_hot_data = cache.get('yesterday_hot_data') if yesterday_hot_data is None: yesterday_hot_data = get_yesterday_hot_data(blog_content_type) cache.set('yesterday_hot_data', yesterday_hot_data, 3600) seven_hot_data = cache.get('seven_hot_data') if seven_hot_data is None: seven_hot_data = get_seven_hot_data() cache.set('seven_hot_data', seven_hot_data, 3600) month_hot_data = cache.get('month_hot_data') if month_hot_data is None: month_hot_data = get_month_hot_data() cache.set('month_hot_data', month_hot_data, 3600) context ={} context['dates'] = dates #日期 context['read_nums'] = read_nums #七日統計 context['today_hot_data'] = today_hot_data #取得當日熱門貼文資料 context['yesterday_hot_data'] = yesterday_hot_data context['seven_hot_data'] = seven_hot_data context['month_hot_data'] = month_hot_data return render(request, 'home.html',context)
留言列表