以下的文章是 Django 類別基礎視圖(CBV)探索系列的 最後一篇,主要介紹如何利用 ListView 深入應用 分頁搜尋排序 等在實務中常用的功能。若您已經閱讀過前七篇文章,了解 CBV 的基礎及 Mixin 的運用,本篇將綜合這些內容,探討如何以 CBV 優雅地處理實際網頁開發中的一般需求。

前一篇直接連結,請點擊以下鏈接!

類別基礎視圖(CBV)探索系列 ⑦ - Mixin 的利用及權限管理


“擴展 Django ListView,實現強大的列表功能,提升  

使用者體驗!”


1. ListView,更深化的可能性

ListView 是一個非常有用的通用視圖,能夠顯示數據列表。然而在實際網頁應用中,僅僅顯示列表是不夠的,很多數據 的有效管理需要 分頁、快速找到所需信息的 搜尋,以及基於使用者偏好的 排序 功能,這些都是必不可少的。

本章將探討如何擴展 ListView,以便簡單而優雅地實現這些實務上的需求,充分發揮 CBV 的優勢。


2. 應用分頁(Pagination)

一次顯示大量數據會影響使用者體驗。分頁 通過將數據分為多個頁面來減輕使用者的加載負擔,幫助他們更輕鬆地找到所需的信息。

2.1 基本分頁設定

ListView 提供了內建的分頁功能。只需在視圖類中設置 paginate_by 屬性即可。

views.py 範例

from django.views.generic import ListView
from .models import Article

class ArticleListView(ListView):
    model = Article
    template_name = 'articles/article_list.html'
    context_object_name = 'articles'
    paginate_by = 10 # 每頁顯示 10 個 Article 對象
  • paginate_by = 10: 這樣設置後,ListView 會自動將結果劃分為每頁 10 個,並將與分頁相關的對象(paginatorpage_objis_paginated)傳遞給模板上下文。

2.2 在模板中實現分頁 UI

在模板中,可以使用傳遞的 page_obj 顯示頁碼鏈接,並實現前一頁/下一頁按鈕等功能。

articles/article_list.html 範例

<ul>
    {% for article in articles %}
        <li>{{ article.title }} - {{ article.created_at }}</li>
    {% empty %}
        <li>尚未有發布的文章。</li>
    {% endfor %}
</ul>

<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?page=1">&laquo; 首頁</a>
            <a href="?page={{ page_obj.previous_page_number }}">前一頁</a>
        {% endif %}

        <span class="current">
            頁面 {{ page_obj.number }} / {{ paginator.num_pages }}
        </span>

        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}&search_term={{ search_form.search_term.value|default:'' }}">下一頁</a>
            <a href="?page={{ paginator.num_pages }}&search_term={{ search_form.search_term.value|default:'' }}">最後一頁 &raquo;</a>
        {% endif %}
    </span>
</div>
  • page_obj: 包含當前頁面數據的對象,提供 has_previoushas_nextprevious_page_numbernext_page_numbernumber 等屬性。
  • paginator: 包含整體頁面相關信息的對象,提供 num_pages(總頁數)等屬性。

2.3 保持 URL 連接

當生成分頁鏈接時,可以看到使用了 ?page=2page 查詢參數。ListView 會自動識別此參數,以顯示該頁面的數據。


在網站上快速找到所需的信息非常重要。我們將擴展 ListView 來實現搜尋功能。

3.1 定義表單(Form)

首先,定義一個簡單的表單來接收搜尋關鍵字。

forms.py 範例

from django import forms

class ArticleSearchForm(forms.Form):
    search_term = forms.CharField(label='搜尋關鍵字', required=False)

3.2 修改 ListView

重寫 ListViewget_queryset() 方法來實現搜尋功能。

views.py 範例

from django.views.generic import ListView
from django.db.models import Q
from .models import Article
from .forms import ArticleSearchForm

class ArticleListView(ListView):
    model = Article
    template_name = 'articles/article_list.html'
    context_object_name = 'articles'
    paginate_by = 10

    def get_queryset(self):
        queryset = super().get_queryset()
        self.form = ArticleSearchForm(self.request.GET) # 從 GET 請求中獲取表單數據

        if self.form.is_valid():
            search_term = self.form.cleaned_data.get('search_term')
            if search_term:
                # 過濾標題或內容中包含搜尋詞的 Article
                queryset = queryset.filter(
                    Q(title__icontains=search_term) | Q(content__icontains=search_term)
                )
        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['search_form'] = self.form # 傳遞表單到模板
        return context
  • get_queryset(): 重寫此方法以修改基本查詢集。
  • 建立表單實例,若通過有效性檢查,則從 cleaned_data 中獲取搜尋關鍵字。
  • 使用 Q 物件 過濾標題(title)或內容(content)中包含搜尋關鍵字的 Article 對象(__icontains 用於不區分大小寫的包含檢查)。
  • get_context_data(): 將表單實例添加到上下文中,以便呈現在模板中。

3.3 在模板中顯示搜尋表單並保持結果

在模板中添加搜尋表單,並構建 URL 以保留搜尋結果,使其可以進行分頁。

articles/article_list.html

<form method="get">
    {{ search_form }}
    <button type="submit">搜尋</button>
</form>

<ul>
    {% for article in articles %}
        <li>{{ article.title }} - {{ article.created_at }}</li>
    {% empty %}
        <li>沒有搜尋結果。</li>
    {% endfor %}
</ul>

<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?page=1&search_term={{ search_form.search_term.value|default:'' }}">&laquo; 首頁</a>
            <a href="?page={{ page_obj.previous_page_number }}&search_term={{ search_form.search_term.value|default:'' }}">前一頁</a>
        {% endif %}

        <span class="current">
            頁面 {{ page_obj.number }} / {{ paginator.num_pages }}
        </span>

        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}&search_term={{ search_form.search_term.value|default:'' }}">下一頁</a>
            <a href="?page={{ paginator.num_pages }}&search_term={{ search_form.search_term.value|default:'' }}">最後一頁 &raquo;</a>
        {% endif %}
    </span>
</div>
  • 將表單的 method 設定為 get,以將搜尋關鍵字作為 URL 查詢參數傳遞。
  • 在生成頁面鏈接時,同時包含當前搜尋關鍵字(search_form.search_term.value)作為 URL 參數,以保持搜尋結果的分頁。使用 |default:'' 過濾器來處理無搜尋字串的情況。

ListView 畫面示例 - 具備搜尋及排序 UI 的列表頁


4. 添加排序(Ordering)功能

為使用者提供按照所需標準進行排序的功能。

4.1 添加排序選擇表單(可選)

可以添加一個下拉表單來選擇排序標準,也可以簡單地透過 URL 參數處理。在此,我們將說明使用 URL 參數的方法。

4.2 修改 ListView

修改 get_queryset() 方法以應用 order_by()

views.py 範例(包含搜尋功能)

from django.views.generic import ListView
from django.db.models import Q
from .models import Article
from .forms import ArticleSearchForm

class ArticleListView(ListView):
    model = Article
    template_name = 'articles/article_list.html'
    context_object_name = 'articles'
    paginate_by = 10

    def get_queryset(self):
        queryset = super().get_queryset()
        self.form = ArticleSearchForm(self.request.GET)
        ordering = self.request.GET.get('ordering', '-created_at') # 預設排序:最新優先

        if self.form.is_valid():
            search_term = self.form.cleaned_data.get('search_term')
            if search_term:
                queryset = queryset.filter(
                    Q(title__icontains=search_term) | Q(content__icontains=search_term)
                )

        queryset = queryset.order_by(ordering)
        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['search_form'] = self.form
        context['ordering'] = self.request.GET.get('ordering', '-created_at') # 將當前排序標準傳遞到模板
        return context
  • self.request.GET.get('ordering', '-created_at'): 從 URL 查詢參數中獲取 ordering 的值。如果沒有值,預設為 -created_at(最新優先)。
  • queryset = queryset.order_by(ordering): 使用獲取的 ordering 值來排序查詢集。如果模型字段名稱前加上 -,會按相反順序排序。

4.3 在模板中提供排序鏈接

在模板中提供可以按每個字段進行排序的鏈接。

articles/article_list.html 範例

<form method="get">
    {{ search_form }}
    <button type="submit">搜尋</button>
</form>

<table>
    <thead>
        <tr>
            <th><a href="?ordering=title&search_term={{ search_form.search_term.value|default:'' }}">標題</a></th>
            <th><a href="?ordering=-created_at&search_term={{ search_form.search_term.value|default:'' }}">創建日期</a></th>
            </tr>
    </thead>
    <tbody>
        {% for article in articles %}
            <tr>
                <td>{{ article.title }}</td>
                <td>{{ article.created_at }}</td>
            </tr>
        {% empty %}
            <tr><td colspan="2">沒有搜尋結果。</td></tr>
        {% endfor %}
    </tbody>
</table>

<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?page=1&search_term={{ search_form.search_term.value|default:'' }}&ordering={{ ordering }}">&laquo; 首頁</a>
            <a href="?page={{ page_obj.previous_page_number }}&search_term={{ search_form.search_term.value|default:'' }}&ordering={{ ordering }}">前一頁</a>
        {% endif %}

        <span class="current">
            頁面 {{ page_obj.number }} / {{ paginator.num_pages }}
        </span>

        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}&search_term={{ search_form.search_term.value|default:'' }}&ordering={{ ordering }}">下一頁</a>
            <a href="?page={{ paginator.num_pages }}&search_term={{ search_form.search_term.value|default:'' }}&ordering={{ ordering }}">最後一頁 &raquo;</a>
        {% endif %}
    </span>
</div>
  • 在每個表格頭部提供包含排序標準(ordering)的鏈接。
  • 在生成頁面鏈接時,除了當前搜尋關鍵字,還要包含當前排序標準(ordering),以便在保持排序狀態的同時進行分頁。

搜尋/排序流程圖 - ListView 擴展流向可視化


5. 使用 Mixin 結合功能

利用之前學習的 Mixin,我們可以更簡潔地管理分頁、搜尋和排序功能。例如,可以將搜尋功能分離為 Mixin。

mixins.py 範例

from django.db.models import Q
from .forms import ArticleSearchForm

class ArticleSearchMixin:
    def get_queryset(self):
        queryset = super().get_queryset()
        self.form = ArticleSearchForm(self.request.GET)

        if self.form.is_valid():
            search_term = self.form.cleaned_data.get('search_term')
            if search_term:
                queryset = queryset.filter(
                    Q(title__icontains=search_term) | Q(content__icontains=search_term)
                )
        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['search_form'] = getattr(self, 'form', ArticleSearchForm()) # 若無表單則使用默認表單
        return context

views.py 範例(應用 Mixin)

from django.views.generic import ListView
from .models import Article
from .mixins import ArticleSearchMixin

class ArticleListView(ArticleSearchMixin, ListView):
    model = Article
    template_name = 'articles/article_list.html'
    context_object_name = 'articles'
    paginate_by = 10

    def get_queryset(self):
        queryset = super().get_queryset()
        ordering = self.request.GET.get('ordering', '-created_at')
        queryset = queryset.order_by(ordering)
        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['ordering'] = self.request.GET.get('ordering', '-created_at')
        return context
  • ArticleSearchMixin 被創建以分離與搜尋相關的邏輯。
  • ArticleListView 繼承自 ArticleSearchMixinListView,因此擁有搜尋與列表功能。

6. CBV 的優勢與限制(系列結尾)

通過 8 篇 CBV 探索系列,我們瞭解了為何從函數基礎視圖(FBV)轉換到類別基礎視圖(CBV)、CBV 的基本結構與應用、各種通用視圖的使用方法,以及如何利用 Mixin 進行功能擴展等核心內容。

CBV 的主要優勢:

  • 代碼重用性: 通過通用視圖和 Mixin 來減少重複代碼,提高開發效率。
  • 結構化代碼: 類別基礎的代碼能清晰地分離和管理邏輯。
  • 擴展性: 通過繼承和 Mixin,輕鬆擴展現有功能並添加新功能。
  • 維護性: 模組化的代碼使維護更簡單,並減少變更對整體代碼的影響。
  • 提高開發效率: 利用 Django 提供的各種通用視圖和 Mixin,可以快速開發。

CBV 的限制與考量:

  • 初學者曲線: 相比 FBV 需要對類別基礎結構有一定的理解。
  • 過度繼承: 使用過多 Mixin 可能使代碼流程難以理解,適度使用 Mixin 是重要的。
  • 對簡單邏輯的過度設計: 對於非常簡單的頁面或功能,FBV 可能更簡潔。在不同情況下選擇合適的視圖方式是明智的。

總的來說,Django CBV 是開發複雜且功能多樣的網頁應用程式非常強大而有用的工具。理解並適當利用 CBV 的優勢,將使我們能夠編寫更高效、維護性更好的代碼。

非常感謝您閱讀類別基礎視圖(CBV)探索系列!希望此系列能在理解 Django CBV 及在實際專案中應用上對您有所幫助。


重新查看前一篇文章

  1. 類別基礎視圖(CBV)探索系列 #1 – FBV 到 CBV 的轉變理由及開發者的心態
  2. 類別基礎視圖(CBV)探索系列 #2 – 理解 Django 的基本 View 類別
  3. 類別基礎視圖(CBV)探索系列 #3 – 使用 FormView 簡化表單處理
  4. 類別基礎視圖(CBV)探索系列 #4 – ListView & DetailView 的使用方法
  5. 類別基礎視圖(CBV)探索系列 #5 – 使用 CreateView、UpdateView 和 DeleteView 實現 CRUD
  6. 類別基礎視圖(CBV)探索系列 #6 – 使用 TemplateView & RedirectView
  7. 類別基礎視圖(CBV)探索系列 #7 – Mixin 的利用及權限管理

“通過擴展 ListView 實現更豐富且使用者友好的列表功能,  

精通 CBV,讓你的 Django 開發技術更上一步!”