Django 學習小組:博客開發實戰第三週教程——文章列表分頁和代碼語法高亮

本教程內容已過期,更新版教程請訪問: django 博客開發入門教程css

摘要:前兩期教程咱們實現了博客的 Model 部分,以及 Blog 的首頁視圖 IndexView,詳情頁面 DetailView,以及分類頁面 CategoryView,前兩期教程連接請戳:html

Django 學習小組:博客開發實戰第一週教程 —— 編寫博客的 Model 與首頁面python

Django 學習小組:博客開發實戰第二週教程 —— 博客詳情頁面和分類頁面git

本週咱們將繼續完善咱們的我的博客,來實現分頁和代碼高亮的功能。github

提示:在閱讀教程的過程當中,若有任何問題請訪問咱們項目的 GithHub 或評論留言以獲取幫助,本教程的相關代碼已所有上傳在 Github。若是你對咱們的教程或者項目有任何改進建議,請您隨時告知咱們。更多交流請加入咱們的郵件列表 django_study@groups.163.com 和關注咱們在 Github 上的項目。數據庫

本文首發於編程派微信公衆號:編程派(微信號:codingpy)是一個專一Python編程的公衆號,天天更新有關Python的國外教程和優質書籍等精選乾貨,歡迎關注。django


實現文章展現列表的分頁功能

咱們的數據庫中會有愈來愈多的文章,把它們所有用一個列表顯示在首頁好像不太合適,若是顯示必定數量的文章,好比8篇,這就須要用到分頁功能。
Django提供了一些類來幫助你管理分頁的數據 -- 也就是說,數據被分在不一樣頁面中,並帶有「上一頁/下一頁」標籤。這些類位於django/core/paginator.py中。編程

文章過多,爲了提升用戶體驗,一次只展現部分文章,爲用戶提供一個分頁功能,就像下面這樣:segmentfault

分頁效果圖

比較完善的分頁效果,應該是這樣的:微信

  • 用戶在哪一頁,則當前頁號高亮以提示用戶所在位置,好比上圖顯示用戶正處在第二頁。

  • 當用戶所處的位置還有上一頁時,顯示上一頁按鈕;當還有下一頁時,顯示下一頁按鈕,不然不顯示。

  • 當分頁較多時,老是顯示當前頁及其前幾頁和後幾頁的頁碼(教程中使用的是兩頁),其餘頁碼用省略號代替。

  • 老是顯示第一頁和最後一頁的頁碼。

根據上面的需求,咱們開始編寫相應代碼。

關於分頁須要使用到的的 API ,Django 官方文檔對此有十分詳細的介紹,它還給出了一個完整示例,讀懂它的代碼後仿照它便可實現基本的分頁功能。請參考官方文檔對於分頁的示例,若是你不習慣英文的話,也能夠參照網友的翻譯版本Django 中文文檔:分頁。下面就根據官方的示例來實現咱們的需求。

儘管能夠把分頁邏輯直接寫在視圖內,可是爲了通用性,咱們使用一點點 Django 更加高級的技巧——模板標籤(TemplateTags)。分頁功能的實現有不少第三方 APP 能夠直接使用,可是爲了學習 Django 的知識,因此咱們本身實現一個。這些第三方 APP 基本都是使用的模板標籤,所以這多是一種比較好的實踐。

爲了使用模板標籤,Django 要求咱們先創建一個 templatetags 文件夾,並在裏面加上 __init__.py文件以指示 python 這是一個模塊(python 把含有該問價的文件夾當作一個模塊,具體請參考任何一個關於 python 模塊的教程)。而且 templatetags 文件夾和你的 model.py,views.py 文件是同級的,也就是說你的目錄結構看起來應該是這樣:

polls/
    __init__.py
    models.py
    templatetags/
        __init__.py
        poll_extras.py
    views.py

(這個目錄結構引自官方文檔,關於詳細的模板標籤的介紹請參考官方文檔:custom template tags,不必定所有讀懂,但仍是推薦花幾十分鐘掃一遍明白其大體說了什麼)。

在 templatetags 目錄下創建一個 paginate_tags .py 文件,準備工做作完,結合 Django 的模板系統,咱們來看看該如何編寫咱們的程序。

首先來回顧一下 Django 的模板系統是如何工做的,回想一下視圖函數的工做流程,視圖函數接收一個 Http 請求,通過一系列處理,一般狀況下其會渲染某個模板文件,把模板文件中的一些用 {{ }} 包裹的變量替換成從該視圖函數中相應變量的值。事實上在此過程當中 Django 悄悄幫咱們作了一些事情,它把視圖函數中的變量的值封裝在了一個 Context (通常翻譯成上下文)對象中,只要模板文件中的變量在 Context 中有對應的值,它就會被相應的值替換。所以,咱們的程序能夠這樣作:首先把取到的文章列表(官方術語是一個 queryset)分頁,用戶請求第幾頁,咱們就把第幾頁的文章列表傳遞給模板文件;另外還要根據上面的需求傳遞頁碼值給模板文件,這樣只要把模板文件中的變量替換成咱們傳遞過去的值,那麼就達到本文開篇處那樣的分頁顯示效果了。

開始編寫咱們的代碼了,慣例依然是先看代碼,而後咱們再逐行解釋:

paginate_tags.py

from django import template
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage

register = template.Library()


@register.simple_tag(takes_context=True)
def paginate(context, object_list, page_count):
    left = 3
    right = 3

    paginator = Paginator(object_list, page_count)
    page = context['request'].GET.get('page')

    try:
        object_list = paginator.page(page)
        context['current_page'] = int(page)
        pages = get_left(context['current_page'], left, paginator.num_pages) + get_right(context['current_page'], right,
                                                                                         paginator.num_pages)
    except PageNotAnInteger:
        object_list = paginator.page(1)
        context['current_page'] = 1
        pages = get_right(context['current_page'], right, paginator.num_pages)
    except EmptyPage:
        object_list = paginator.page(paginator.num_pages)
        context['current_page'] = paginator.num_pages
        pages = get_left(context['current_page'], left, paginator.num_pages)

    context['article_list'] = object_list
    context['pages'] = pages
    context['last_page'] = paginator.num_pages
    context['first_page'] = 1
    try:
        context['pages_first'] = pages[0]
        context['pages_last'] = pages[-1] + 1
    except IndexError:
        context['pages_first'] = 1
        context['pages_last'] = 2

    return ''  # 必須加這個,不然首頁會顯示個None


def get_left(current_page, left, num_pages):
    if current_page == 1:
        return []
    elif current_page == num_pages:
        l = [i - 1 for i in range(current_page, current_page - left, -1) if i - 1 > 1]
        l.sort()
        return l
    l = [i for i in range(current_page, current_page - left, -1) if i > 1]
    l.sort()
    return l


def get_right(current_page, right, num_pages):
    if current_page == num_pages:
        return []
    return [i + 1 for i in range(current_page, current_page + right - 1) if i < num_pages - 1]

首先讓咱們來看看整個分頁程序的執行過程,模板標籤本質上來講就是一個 python 函數而已,只是該函數能夠被用在 Django 的模板系統裏面。函數就是接受參數,返回一個值。例如咱們這裏定義的 def paginate(context, object_list, page_count): 分頁函數,它接收了這麼一些參數,通過各類處理,最終返回了 None 。

逐行解釋:

paginate.py

from django import template
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
# 這是分頁功能涉及的一些類和異常,官方文檔對此有詳細介紹。固然從命名也能夠直接看出它們的用途:Paginator(分頁),PageNotAnInteger(頁碼不是一個整數異常),EmptyPage(空的頁碼號異常)

register = template.Library()
# 這是定義模板標籤要用到的

@register.simple_tag(takes_context=True) 
# 這個裝飾器代表這個函數是一個模板標籤,takes_context = True 表示接收上下文對象,就是前面所說的封裝了各類變量的 Context 對象。
def paginate(context, object_list, page_count): 
    # context是Context 對象,object_list是你要分頁的對象,page_count表示每頁的數量
    
    left = 3 # 當前頁碼左邊顯示幾個頁碼號 -1,好比3就顯示2個
    right = 3 # 當前頁碼右邊顯示幾個頁碼號 -1

    paginator = Paginator(object_list, page_count) # 經過object_list分頁對象
    page = context['request'].GET.get('page') # 從 Http 請求中獲取用戶請求的頁碼號

    try:
        object_list = paginator.page(page) # 根據頁碼號獲取第幾頁的數據
        context['current_page'] = int(page) # 把當前頁封裝進context(上下文)中
        pages = get_left(context['current_page'], left, paginator.num_pages) + get_right(context['current_page'], right, paginator.num_pages)
        # 調用了兩個輔助函數,根據當前頁獲得了左右的頁碼號,好比設置成獲取左右兩邊2個頁碼號,那麼假如當前頁是5,則 pages = [3,4,5,6,7],固然一些細節須要處理,好比若是當前頁是2,那麼獲取的是pages = [1,2,3,4]
      
    except PageNotAnInteger:
        # 異常處理,若是用戶傳遞的page值不是整數,則把第一頁的值返回給他
        object_list = paginator.page(1)
        context['current_page'] = 1 # 當前頁是1
        pages = get_right(context['current_page'], right, paginator.num_pages)
    except EmptyPage:
        # 若是用戶傳遞的 page 值是一個空值,那麼把最後一頁的值返回給他
        object_list = paginator.page(paginator.num_pages)
        context['current_page'] = paginator.num_pages # 當前頁是最後一頁,num_pages的值是總分頁數
        pages = get_left(context['current_page'], left, paginator.num_pages)

    context['article_list'] = object_list # 把獲取到的分頁的數據封裝到上下文中
    context['pages'] = pages # 把頁碼號列表封裝進去
    context['last_page'] = paginator.num_pages # 最後一頁的頁碼號
    context['first_page'] = 1 # 第一頁的頁碼號爲1
    try:
        # 獲取 pages 列表第一個值和最後一個值,主要用於在是否該插入省略號的判斷,在模板文件中將會體會到它的用處。注意這裏可能產生異常,由於pages多是一個空列表,好比自己只有一個分頁,那麼pages就爲空,由於咱們永遠不會獲取頁碼爲1的頁碼號(至少有1頁,1的頁碼號已經固定寫在模板文件中)
        context['pages_first'] = pages[0]
        context['pages_last'] = pages[-1] + 1
        # +1的緣由是爲了方便判斷,在模板文件中將會體會到其做用。
        
    except IndexError:
        context['pages_first'] = 1 # 發生異常說明只有1頁
        context['pages_last'] = 2 # 1 + 1 後的值 

    return ''  # 必須加這個,不然首頁會顯示個None


def get_left(current_page, left, num_pages):
    """
    輔助函數,獲取當前頁碼的值得左邊兩個頁碼值,要注意一些細節,好比不夠兩個那麼最左取到2,爲了方便處理,包含當前頁碼值,好比當前頁碼值爲5,那麼pages = [3,4,5]
    """
    if current_page == 1:
        return []
    elif current_page == num_pages:
        l = [i - 1 for i in range(current_page, current_page - left, -1) if i - 1 > 1]
        l.sort()
        return l
    l = [i for i in range(current_page, current_page - left, -1) if i > 1]
    l.sort()
    return l


def get_right(current_page, right, num_pages):
    """
    輔助函數,獲取當前頁碼的值得右邊兩個頁碼值,要注意一些細節,好比不夠兩個那麼最右取到最大頁碼值。不包含當前頁碼值。好比當前頁碼值爲5,那麼pages = [6,7]
    """
    if current_page == num_pages:
        return []
    return [i + 1 for i in range(current_page, current_page + right - 1) if i < num_pages - 1]

把須要變量值都添加到上下文了,看看咱們的模板文件該怎麼寫:

templates/blog/pagination.html

<div id="pagenavi" class="noselect">
    {% if article_list.has_previous %} # 判斷是否還有上一頁,有的話要顯示一個上一頁按鈕
        <a class="previous-page" href="?page={{ article_list.previous_page_number }}">
            <span class="icon-previous"></span>上一頁
        </a>
    {% endif %}

    # 頁碼號爲1永遠顯示
    {% if first_page == current_page %} # 當前頁就是第一頁
        <span class="first-page current">1</span>
    {% else %} # 不然的話,第一頁是能夠點擊的,點擊後經過?page=1的形式把頁碼號傳遞給視圖函數
        <a href="?page=1" class="first-page">1</a>
    {% endif %}

    {% if pages_first > 2 %} # 2之前的頁碼號要被顯示成省略號了
        <span>...</span>
    {% endif %}

    {% for page in pages %} # 經過for循環把pages中的值顯示出來
        {% if page == current_page %} # 是否當前頁,按鈕會顯示不一樣的樣式
            <span class="current">{{ page }}</span>
        {% else %}
            <a href="?page={{ page }}">{{ page }}</a>
        {% endif %}
    {% endfor %}
    
      # pages最後一個值+1的值小於最大頁碼號,說明有頁碼號須要被省略號替換
    {% if pages_last < last_page %}
        <span>...</span>
    {% endif %}
    
      # 永遠顯示最後一頁的頁碼號,若是隻有一頁則前面已經顯示了1就不用再顯示了
    {% if last_page != 1 %}
        {% if last_page == current_page %}
            <span class="current">{{ last_page }}</span>
        {% else %}
            <a href="?page={{ last_page }}">{{ last_page }}</a>
        {% endif %}
    {% endif %}
    
    # 還有下一頁,則顯示一個下一頁按鈕
    {% if article_list.has_next %}
        <a class="next-page" href="?page={{ article_list.next_page_number }}">
            下一頁<span class="icon-next"></span>
        </a>
    {% endif %}
</div>

至此代碼部分編寫完了,看看如何使用這個模板標籤吧,好比咱們要在首頁對文章列表進行分頁:

templates/blog/index.html

{% load paginate_tags %} # 首先必須經過load模板標籤載入分頁標籤
{% paginate article_list 7 %} 把文章列表傳給paginate函數,每頁分7個,context上下文則自動被傳入,無需顯示指定

{% for article in article_list %}
    display the article information
{% endfor %}

{% include 'blog/pagination.html' %}
# 這裏用到一個 include 技巧,把pagination的模板代碼寫在單獨的pagination.html文件中,這樣哪裏須要用到哪裏就 include 進來就行,提升代碼的複用性。

至此,整個分頁功能就完成了,看看效果:

文章分頁演示

支持 fetch code 與代碼高亮

fetch code

咱們的博客文章是支持 markdown 語法標記的(使用的是 markdown2 第三方 app),markdown 比較經常使用的兩個特性是 fetch code 和語法高亮。因爲咱們目前沒有對博客文章的 markdown 標記作任何拓展,所以要標記一段代碼,咱們必須在每行代碼前縮進 4 個空格,這很不方便。而 fetch code 可讓咱們在寫文章時只按照下面的輸入就能夠標記一段代碼,相比每行縮進四個空格要方便不少:

​```
def test_function():
    print('fectch code like this!')
​```

下面來拓展它,很簡單,把用 markdown 標記的語句拓展一下,在 Views.py 中找到 IndexView,其中有一句代碼的做用是來 markdown 咱們的博客文章的:

for article in article_list:
    article.body = markdown2.markdown(article.body, )

將 markdown 函數拓展一下,傳入以下參數便可:

for article in article_list:
    article.body = markdown2.markdown(article.body, extras=['fenced-code-blocks'], )

這樣,每次要輸入一段代碼時,按照上面的語法輸入就能夠了,好比我輸入下面的代碼段:

​``` # 注意這個符號是半角下波浪符號,即數字1左邊的那個鍵對應的符號
class ArticleDetailView(DetailView):
    model = Article
    template_name = "blog/detail.html"
    context_object_name = "article"
    pk_url_kwarg = 'article_id'

    def get_object(self, queryset=None):
        obj = super(ArticleDetailView, self).get_object()
        obj.body = markdown2.markdown(obj.body, extras=['fenced-code-blocks'], )
        return obj
​```

來看看效果:

fech code

此外別忘了把其餘作了 markdown 標記的地方也作相應拓展,目前咱們一共有三處:IndexView,DetailView,CategoryView。

代碼高亮

如今輸入代碼方便了,可是美中不足的是代碼只有一種顏色,咱們想要代碼高亮,須要使用到 Pygments 包。先安裝它:pip install pygments,安裝好後別忘了添加到 settings.py 中:

settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',
    'markdown2',
    'pygments', # 添加進來
]

pygments 的工做原理是把代碼切分紅一個個單詞,而後爲這些單詞添加 css 樣式,不一樣的詞應用不一樣的樣式,這樣就實現了代碼顏色的區分,即高亮了語法,所以咱們要引入一些 css 樣式文件。在咱們的 GitHub 項目的 DjangoBlog/blog/static/blog/css 目錄下有相應的文件,拷貝下來添加到你的項目相同目錄下就能夠了。以後再模板中引入樣式文件:

templates/base.html

<head>
    <meta charset="UTF-8">
    <title>Myblog</title>
    ...
    <link rel="stylesheet" href="{% static 'blog/css/pygments/github.css' %}">
    引入上面的樣式文件,固然裏面有不少樣式文件,喜歡哪一個引哪一個,好比我引的是github風格的語法高亮
    ...
</head>

再次輸入代碼塊看看:

​```python # 注:這裏必定要指定相應語言,不然沒法高亮代碼
class ArticleDetailView(DetailView):
    model = Article
    template_name = "blog/detail.html"
    context_object_name = "article"
    pk_url_kwarg = 'article_id'

    def get_object(self, queryset=None):
        obj = super(ArticleDetailView, self).get_object()
        obj.body = markdown2.markdown(obj.body, extras=['fenced-code-blocks'], )
        return obj
​```

看看效果:

代碼高亮

這裏比較麻煩的是必須指定代碼對應的語言,有人說 pygments 能夠自動識別語言的,可是我目前的測試來看彷佛沒有效果。目前沒有找到設置方法,若有知道的朋友請告知。

整個完整的 Blog 項目代碼請訪問咱們的 GitHub 組織倉庫獲取。

聲明:本教程只是演示如何實現分頁和 markdown 語法高亮功能,在細節上處理上還有不少須要斟酌的地方,若是您有更好的實現方式或者實踐經驗,懇請傳授咱們。若是您對本教程有任何不清晰的地方或者其餘意見和建議,請及時經過郵件列表或者 GitHub Issue 或者評論留言反饋給咱們。您的反饋和建議是咱們持續改善本教程的最佳方式。

接下來作什麼?

我的博客功能逐步完善,接下來的教程咱們將繼續實現我的博客常帶的功能:標籤雲文章歸檔,敬請期待下一期教程。若是你還有其餘想實現的功能,也請告訴咱們,咱們會在教程中陸續實現。

Django學習小組簡介

django學習小組是一個促進 django 新手互相學習、互相幫助的組織。

小組在一邊學習 django 的同時將一塊兒完成幾個項目,包括:

  • 一個簡單的 django 博客,用於發佈小組每週的學習和開發文檔;

  • django中國社區,爲國內的 django 開發者們提供一個長期維護的 django 社區;

上面所說的這個社區相似於 segmentfault 和 stackoverflow ,但更加專一(只專一於 django 開發的問題)。

目前小組正在完成第一個項目,本文便是該項目第三週的相關文檔。

更多的信息請關注咱們的 github 組織,本教程項目的相關源代碼也已上傳到 github 上。

同時,你也能夠加入咱們的郵件列表 django_study@groups.163.com ,隨時關注咱們的動態。咱們會將每週的詳細開發文檔和代碼經過郵件列表發出。

若有任何建議,歡迎提 Issue,歡迎 fork,pr,固然也別忘了 star 哦!

相關文章
相關標籤/搜索