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

原文章地址: EOSONES博客

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

設計文章相關模型

一、功能分析

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

二、編寫 Storm 應用模型

首先打開項目根目錄,建立 Storm APPpython

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)表的建立:django

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功能,前端引用模板語言能夠達到一樣效果並使用更自由。markdown

查詢文章與分頁視圖

在此以前先配置urlapp

#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

設計模板

一、獲取文章

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

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

二、獲取分頁

分頁傳來的數據中,除了咱們自定義的 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>

<small>參考:追夢任務 | Django Pagination分頁功能 </small>

相關文章
相關標籤/搜索