Django從零搭建個人博客 | 文章列表頁查詢篩選及分頁


原文章地址: EOSONES博客

在整個博客的搭建中,文章相關的功能是最關鍵的,比如文章相關數據模型的設計、不同分類下文章的篩選顯示、以及對顯示功能完善的分頁功能。本文針對本博客的文章主要功能通過這幾方面進行介紹,參考全部代碼請到Github查看。

設計文章相關模型

1、功能分析

在數據庫設計之前,我們首先要確定網站功能,結合本站,最主要的是我們的博文表,名字可以直接叫做 article,其中包含博文的標題、內容、發表時間、修改時間、分類、標簽、閱讀量、喜歡量、作者、關鍵詞等。博文表直接關聯的有分類表(一對多)、標簽表(多對多)和文章關鍵詞表 (多對多),分類表是隸屬在導航欄下,到此我們可以確定出這些最基本的數據表,博客(Article)、分類(Category)、標簽(Tag)與文章關鍵詞 (Keyword)、導航(Bigcategory)。

2、編寫 Storm 應用模型

首先打開項目根目錄,創建 Storm APP

python manage.py startapp Storm

在 Myblog -> storm -> models.py 中首先設計導航表 (Bigcategory)與分類表(Category)。

from django.db import models
from django.conf import settings  #引入定義字段SEO設置(提前設置)與自定義User(參考管理用戶登錄與注冊博文)
from django.shortcuts import reverse #查找URL
import re

# 網站導航菜單欄表
class BigCategory(models.Model):
    # 導航名稱
    name = models.CharField('導航大分類', max_length=20)
    # 用作文章的訪問路徑,每篇文章有獨一無二的標識
    slug = models.SlugField(unique=True) #此字符串字段可以建立唯一索引
    # 分類頁描述
    description = models.TextField('描述', max_length=240, default=settings.SITE_DESCRIPTION,help_text='用來作為SEO中description,長度參考SEO標准')
    # 分類頁Keywords
    keywords = models.TextField('關鍵字', max_length=240, default=settings.SITE_KEYWORDS,help_text='用來作為SEO中keywords,長度參考SEO標准')
	
    class Meta: #元信息
        # admin中顯示的表名稱
        verbose_name = '一級導航'
        verbose_name_plural = verbose_name #復數形式相同

    def __str__(self):
        return self.name

# 導航菜單分類下的下拉菜單分類
class Category(models.Model):
    # 分類名字
    name = models.CharField('文章分類', max_length=20)
    # 用作分類路徑,獨一無二
    slug = models.SlugField(unique=True)
    # 分類欄目頁描述
    description = models.TextField('描述', max_length=240, default=settings.SITE_DESCRIPTION,help_text='用來作為SEO中description,長度參考SEO標准')
    # 導航菜單一對多二級菜單,django2.0后定義外鍵和一對一關系的時候需要加on_delete選項,此參數為了避免兩個表里的數據不一致問題
    bigcategory = models.ForeignKey(BigCategory,related_name="Category", on_delete=models.CASCADE,verbose_name='大分類')

    class Meta:#元信息
        # admin中顯示的表名稱
        verbose_name = '二級導航'
        verbose_name_plural = verbose_name
        # 默認排序
        ordering = ['name']

    def __str__(self):
        return self.name

    #返回當前的url(一級分類+二級分類)
    def get_absolute_url(self):
        return reverse('blog:category', kwargs={'slug': self.slug, 'bigslug': self.bigcategory.slug}) #尋找路由為blog:category的url
    #返回當前二級分類下所有發表的文章列表
    def get_article_list(self):
        return Article.objects.filter(category=self)

標簽(Tag)與關鍵字(Keyword)表的創建:

# 文章標簽
class Tag(models.Model):
    name = models.CharField('文章標簽', max_length=20)
    slug = models.SlugField(unique=True)
    description = models.TextField('描述', max_length=240, default=settings.SITE_DESCRIPTION,help_text='用來作為SEO中description,長度參考SEO標准')

    class Meta:
        verbose_name = '標簽'
        verbose_name_plural = verbose_name
        ordering = ['id']

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('blog:tag', kwargs={'tag': self.name})
    def get_article_list(self):
    #返回當前標簽下所有發表的文章列表
    return Article.objects.filter(tags=self)

# 文章關鍵詞,用來作為 SEO 中 keywords
class Keyword(models.Model):
    name = models.CharField('文章關鍵詞', max_length=20)

    class Meta:
    verbose_name = '關鍵詞'
    verbose_name_plural = verbose_name
    ordering = ['name']

    def __str__(self):
        return self.name

博客(Article)表的創建:

from mdeditor.fields import MDTextField #admin markdown編輯器插件
import markdown #導入markdown
# 文章
class Article(models.Model):
    # 文章默認縮略圖
    IMG_LINK = '/static/images/article/default.jpg'
    # 文章信息(作者一對多注冊用戶,這樣用戶也可以有發文權限)
    author = models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE, verbose_name='作者')
    title = models.CharField(max_length=150, verbose_name='文章標題')
    summary = models.TextField('文章摘要', max_length=230, default='文章摘要等同於網頁description內容,請務必填寫...')
    # 文章內容(普通字段models.TextField(verbose_name='文章內容'))
    body = MDTextField(verbose_name='文章內容')
    #圖片鏈接
    img_link = models.CharField('圖片地址', default=IMG_LINK, max_length=255)
    #自動添加創建時間
    create_date = models.DateTimeField(verbose_name='創建時間', auto_now_add=True)
    #自動添加修改時間
    update_date = models.DateTimeField(verbose_name='修改時間', auto_now=True)
    #瀏覽點贊整數字段
    views = models.IntegerField('閱覽量', default=0)
    loves = models.IntegerField('喜愛量', default=0)
    # 文章唯一標識符
    slug = models.SlugField(unique=True)
    #分類一對多文章 #related_name反向查詢
    category = models.ForeignKey(Category,on_delete=models.CASCADE, verbose_name='文章分類')
    #標簽多對多文章
    tags = models.ManyToManyField(Tag, verbose_name='標簽')
    #文章關鍵詞多對多文章
    keywords = models.ManyToManyField(Keyword, verbose_name='文章關鍵詞',help_text='文章關鍵詞,用來作為SEO中keywords,最好使用長尾詞,3-4個足夠')

    class Meta:
        verbose_name = '博文'
        verbose_name_plural = verbose_name
        ordering = ['-create_date']

    def __str__(self):
        return self.title[:20]
    #返回當前文章的url
    def get_absolute_url(self):
        return reverse('blog:article', kwargs={'slug': self.slug})
    #將內容markdown
    def body_to_markdown(self):
        return markdown.markdown(self.body, extensions=[
        # 包含 縮寫、表格等常用擴展
        'markdown.extensions.extra',
        # 語法高亮擴展
        'markdown.extensions.codehilite',
        # 自動生成目錄擴展
        'markdown.extensions.toc',
    ])

    #點贊+1方法
    def update_loves(self):
        self.loves += 1
        self.save(update_fields=['loves']) #更新字段

    #瀏覽+1方法
    def update_views(self):
        self.views += 1
        self.save(update_fields=['views']) #更新字段

    #前篇方法:當前小於文章並倒序排列的第一個
    def get_pre(self):
        return Article.objects.filter(id__lt=self.id).order_by('-id').first()
    #后篇方法:當前大於文章並正序排列的第一個
    def get_next(self):
        return Article.objects.filter(id__gt=self.id).order_by('id').first()

其中模型中定義的一些方便給前端傳遞數據的方法,可以使用Django的自定義templatetags功能,前端引用模板語言可以達到同樣效果並使用更自由。

查詢文章與分頁視圖

在此之前先配置url

#Myblog/urls.py

from django.conf.urls import re_path,include

urlpatterns = [
    ...
    # storm博客應用
    re_path(r'^',include('Storm.urls', namespace='blog')), 
    ...
]
#Myblog/Storm/urls.py

from django.urls import path
from django.conf.urls import re_path
from Storm import views

app_name='Storm'

urlpatterns = [
   ...
    #一級二級菜單分類文章列表
    #django 2.x中用re_path兼容1.x中的url中的方法(如正則表達式)
    re_path(r'category/(?P<bigslug>.*?)/(?P<slug>.*?)/',views.CtegoryView.as_view(),name='category'),#?分隔實際的URL和參數,?p數據庫里面唯一索引 & URL中指定的參數間的分隔符
    re_path(r'category/(?P<bigslug>.*?)/',views.CtegoryView.as_view(),name='category'),
    # 標簽搜索文章列表
    re_path(r'tags/(?P<tagslug>.*?)/', views.CtegoryView.as_view(),name='tag'),
    ...
]

網站前端功能中,可以進行篩選文章列表顯示的途徑有:通過一級導航、二級分類、標簽以及自定義一級導航下的最新與最熱篩選,我們通過url傳參進行視圖分別的處理。
一般的,視圖函數從數據庫中獲取文章列表數據:

def index(request):
    # ...

def archives(request, year, month):
    # ...

def category(request, pk):
    # ...

在Django中專門提供了各種功能的處理類來使我們快捷的處理數據,其中ListView視圖幫我們內部做這些查詢等操作,只需將 model 指定為 Article,告訴 Django 我要獲取的模型是 Article。template_name 指定這個視圖渲染的模板。context_object_name 指定獲取的模型列表數據保存的變量名。這個變量會被傳遞給模板。 paginate_by 通過指定屬性即可開啟分頁功能。

from django.shortcuts import render,get_object_or_404
from Storm import models
#從數據庫中獲取某個模型列表數據基類ListView
from django.views.generic import ListView
#Django自帶的分頁模塊
from django.core.paginator import Paginator
#分類查找文章列表視圖類
class CtegoryView(ListView):
    model=models.Article
    template_name = 'articleList.html' 
    context_object_name = 'articleList' 
    paginate_by = 8 

由於針對不同url進行文章篩選的方式不同,所以我們通過覆寫了父類的 get_queryset 方法獲取定制文章列表數據,通過覆寫def get_context_data方法來獲取定制的分頁效果,其中調用了自定義方法 pagination_data 獲得顯示分頁導航條需要的數據。

#分類查詢文章與視圖類
class CtegoryView(ListView):
    model=models.Article
    template_name = 'articleList.html' 
    context_object_name = 'articleList' 
    paginate_by = 8 #指定 paginate_by 屬性來開啟分頁功能
    #覆寫了父類的 get_queryset 方法獲取定制數據
    #類視圖中,從 URL 捕獲的命名組參數值保存在實例的 kwargs 屬性(是一個字典)里,非命名組參數值保存在實例的 args 屬性(是一個列表)里
    def get_queryset(self):
        #get_queryset方法獲得全部文章列表
        queryset = super(CtegoryView, self).get_queryset()

        # 導航菜單
        big_slug = self.kwargs.get('bigslug', '')

        # 二級菜單
        slug = self.kwargs.get('slug', '')

        # 標簽
        tag_slug = self.kwargs.get('tagslug', '')

        if big_slug:
        big = get_object_or_404(models.BigCategory, slug=big_slug)
        queryset = queryset.filter(category__bigcategory=big)
        if slug:
            if slug=='newest':
                queryset = queryset.filter(category__bigcategory=big).order_by('-create_date')
            elif slug=='hottest':
                queryset = queryset.filter(category__bigcategory=big).order_by('-loves')
            else :
                slu = get_object_or_404(models.Category, slug=slug)
                queryset = queryset.filter(category=slu)
        if tag_slug:
            tlu = get_object_or_404(models.Tag, slug=tag_slug)
            queryset = queryset.filter(tags=tlu)
        return queryset


    #在視圖函數中將模板變量傳遞給模板是通過給 render 函數的 context 參數傳遞一個字典實現的
    def get_context_data(self, **kwargs):
        # 首先獲得父類生成的傳遞給模板的字典。
        context = super().get_context_data(**kwargs)
        paginator = context.get('paginator')
        page = context.get('page_obj')
        is_paginated = context.get('is_paginated')
        # 調用自己寫的 pagination_data 方法獲得顯示分頁導航條需要的數據,見下方。
        pagination_data = self.pagination_data(paginator, page, is_paginated)
        # 將分頁導航條的模板變量更新到 context 中,注意 pagination_data 方法返回的也是一個字典。
        context.update(pagination_data)
    return context

    def pagination_data(self, paginator, page, is_paginated):
        if not is_paginated:# 如果沒有分頁,則無需顯示分頁導航條,不用任何分頁導航條的數據,因此返回一個空的字典
            return {}
        # 當前頁左邊連續的頁碼號,初始值為空
        left = []
        # 當前頁右邊連續的頁碼號,初始值為空
        right = []
        # 標示第 1 頁頁碼后是否需要顯示省略號
        left_has_more = False
        # 標示最后一頁頁碼前是否需要顯示省略號
        right_has_more = False
        # 標示是否需要顯示第 1 頁的頁碼號。
        first = False
        # 標示是否需要顯示最后一頁的頁碼號
        last = False

        # 獲得用戶當前請求的頁碼號
        page_number = page.number
        # 獲得分頁后的總頁數
        total_pages = paginator.num_pages
        # 獲得整個分頁頁碼列表,比如分了四頁,那么就是 [1, 2, 3, 4]
        page_range = paginator.page_range
        #請求的是第一頁的數據
        if page_number == 1:
            #獲取了當前頁碼后連續兩個頁碼
            right = page_range[page_number:(page_number + 2) if (page_number + 2) < paginator.num_pages else paginator.num_pages]
            # 如果最右邊的頁碼號比最后一頁的頁碼號減去 1 還要小,
            # 說明最右邊的頁碼號和最后一頁的頁碼號之間還有其它頁碼,因此需要顯示省略號,通過 right_has_more 來指示。
            if right[-1] < total_pages - 1:
                right_has_more = True
            # 如果最右邊的頁碼號比最后一頁的頁碼號小,說明當前頁右邊的連續頁碼號中不包含最后一頁的頁碼
            # 所以需要顯示最后一頁的頁碼號,通過 last 來指示
            if right[-1] < total_pages:
                last = True

        # 如果用戶請求的是最后一頁的數據,
        elif page_number == total_pages:
            #獲取了當前頁碼前連續兩個頁碼
            left = page_range[(page_number - 3) if (page_number - 3) > 0 else 0:page_number - 1]
            # 如果最左邊的頁碼號比第 2 頁頁碼號還大,
            # 說明最左邊的頁碼號和第 1 頁的頁碼號之間還有其它頁碼,因此需要顯示省略號,通過 left_has_more 來指示。
            if left[0] > 2:
                left_has_more = True
            # 如果最左邊的頁碼號比第 1 頁的頁碼號大,說明當前頁左邊的連續頁碼號中不包含第一頁的頁碼,
            # 所以需要顯示第一頁的頁碼號,通過 first 來指示
            if left[0] > 1:
                first = True
        else:
            # 用戶請求的既不是最后一頁,也不是第 1 頁,則需要獲取當前頁左右兩邊的連續頁碼號,
            # 這里只獲取了當前頁碼前后連續兩個頁碼,你可以更改這個數字以獲取更多頁碼。
            left = page_range[(page_number - 3) if (page_number - 3) > 0 else 0:page_number - 1]
            right = page_range[page_number:(page_number + 2) if (page_number + 2) < paginator.num_pages else paginator.num_pages]
            # 是否需要顯示最后一頁和最后一頁前的省略號
            if right[-1] < total_pages - 1:
                right_has_more = True
            if right[-1] < total_pages:
                last = True

            # 是否需要顯示第 1 頁和第 1 頁后的省略號
            if left[0] > 2:
                left_has_more = True
            if left[0] > 1:
                first = True

        data = {
            'left': left,
            'right': right,
            'left_has_more': left_has_more,
            'right_has_more': right_has_more,
            'first': first,
            'last': last,
        }
        return data

設計模板

1、獲取文章

通過視圖類處理后的文章數據 articleList 在前端中用Django的模板語言可以直接引用,前端模板根據需求進行自定義。

{% for article in articleList %}
{{article.category.name}}
{{article.title}} 
...
{{article.create_date | date:"Y-m-j"}}<
{{article.loves}}
{% endfor %}

2、獲取分頁

分頁傳來的數據中,除了我們自定義的 data 數據,還自帶了paginator:Paginator 的實例,page_obj :當前請求頁面分頁對象,is_paginated:是否開啟分頁,其中page_obj具有當前頁屬性page_obj.number、判斷是否含有上一頁:page_obj.has_previous,是否含有下一頁:page_obj.has_next 。注意我們在這里用了Bootstrap的分頁模板,需要在開頭引入相關文件。

{% if is_paginated %}
<div class="PageList">
    <nav aria-label="Page navigation">
        <ul class="pagination pagination-sm">

            <li class="{% if not page_obj.has_previous %} disabled {% endif %}">
                <a href="{% if page_obj.has_previous %} ?page={{ page_obj.previous_page_number }} {% endif %}" aria-label="Previous">
                    <span aria-hidden="true">&laquo;</span>
                </a>
            </li>

            {% if first %}
            <li>
                <a href="?page=1">1</a>
            </li>
            {% endif %}
            {% if left %}
            {% if left_has_more %}
            <li>
                <span>...</span>
            </li>
            {% endif %}
            {% for i in left %}
            <li>
                <a href="?page={{ i }}">{{ i }}</a>
            </li>
            {% endfor %}
            {% endif %}
            <li class="active"><a href="?page={{ page_obj.number }}">{{ page_obj.number }}</a></li>
            {% if right %}
            {% for i in right %}
            <li>
                <a href="?page={{ i }}">{{ i }}</a>
            </li>
            {% endfor %}
            {% if right_has_more %}
            <li>
                <span>...</span>
            </li>
            {% endif %}
            {% endif %}
            {% if last %}
            <li>
                <a href="?page={{ paginator.num_pages }}">{{ paginator.num_pages }}</a>
            </li>
            {% endif %}
            <li class="{% if not page_obj.has_next %} disabled {% endif %}">
                <a href="{% if page_obj.has_next %} ?page={{ page_obj.next_page_number }} {% endif %}" aria-label="Next">
                    <span aria-hidden="true">&raquo;</span>
                </a>
            </li>
        </ul>
    </nav>
</div>

參考:追夢任務 | Django Pagination分頁功能


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM