close

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 來完成圖表的部分

 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;
}

完成後,範例如下:

14.JPG

三.、閱讀計數排行

 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

15.JPG

 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)

 
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 ivankao 的頭像
    ivankao

    IvanKao的部落格

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