原文章地址: 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">«</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">»</span>
</a>
</li>
</ul>
</nav>
</div>
