文章列表篩選與搜尋¶
開始之前¶
任務目標
在這個章節中,我們會完成:
- 了解為什麼需要篩選功能
- 使用 GET 參數手動實作篩選
- 安裝 django-filter 套件
- 使用 FilterSet 實作進階篩選
- 整合篩選表單到 Template
為什麼需要篩選功能?¶
當文章數量增加時,使用者需要快速找到想要的內容:
情境一:使用者想找特定作者的文章
→ 需要「按作者篩選」功能
情境二:使用者想搜尋標題關鍵字
→ 需要「標題搜尋」功能
情境三:使用者想找特定標籤的文章
→ 需要「按標籤篩選」功能
情境四:使用者想找最近發布的文章
→ 需要「按日期排序」功能
良好的篩選功能
好的篩選系統應該:
- ✅ 直覺易用
- ✅ 回應快速
- ✅ 可以組合多個條件
- ✅ 清楚顯示目前的篩選狀態
方法一:手動實作 GET 參數篩選¶
讓我們先用最基本的方式實作篩選功能,了解其運作原理。
修改 View¶
修改 blog/views.py 中的 article_list view:
from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect, render
from blog.forms import ArticleForm
from blog.models import Article
def article_list(request):
# 從 GET 參數取得篩選條件
search = request.GET.get("search", "") # (1)!
author_id = request.GET.get("author", "") # (2)!
# 建立基本 QuerySet
articles = Article.objects.select_related("author").prefetch_related("tags")
# 根據搜尋關鍵字篩選標題
if search:
articles = articles.filter(title__icontains=search) # (3)!
# 根據作者篩選
if author_id:
articles = articles.filter(author_id=author_id) # (4)!
return render(
request, "blog/article_list.html", {"articles": articles, "search": search}
)
def article_detail(request, article_id):
article = get_object_or_404(
Article.objects.select_related("author").prefetch_related("tags"),
id=article_id,
)
return render(request, "blog/article_detail.html", {"article": article})
def article_create(request):
form = ArticleForm(request.POST or None)
if form.is_valid():
article = form.save()
messages.success(request, f"文章「{article.title}」已成功建立。")
return redirect("blog:article_detail", article_id=article.id)
return render(request, "blog/article_create.html", {"form": form})
def article_edit(request, article_id):
article = get_object_or_404(Article, id=article_id)
form = ArticleForm(request.POST or None, instance=article)
if form.is_valid():
article = form.save()
messages.success(request, f"文章「{article.title}」已成功更新。")
return redirect("blog:article_detail", article_id=article.id)
return render(request, "blog/article_edit.html", {"form": form, "article": article})
def article_delete(request, article_id):
article = get_object_or_404(Article, id=article_id)
if request.method == "POST":
article.delete()
messages.success(request, f"文章「{article.title}」已成功刪除。")
return redirect("blog:article_list")
return render(request, "blog/article_delete.html", {"article": article})
- 取得搜尋關鍵字,預設為空字串
- 取得作者 ID,預設為空字串
- 使用
icontains做不區分大小寫的模糊搜尋 - 根據作者 ID 精確篩選
程式碼重點
GET 參數取得:
request.GET.get("參數名稱", "預設值")- 如果 URL 是
?search=Django,則search會是"Django" - 如果沒有該參數,則使用預設值
條件篩選:
- 只有當參數有值時才執行篩選
- 使用
if search:檢查參數是否為空
QuerySet 鏈式調用:
- 可以連續呼叫多個
filter() - Django 會自動組合成一個 SQL 查詢
在 Template 加入搜尋表單¶
修改 blog/templates/blog/article_list.html,在文章列表上方加入搜尋表單:
{% extends "blog/base.html" %}
{% block title %}
文章列表 - Django 大冒險
{% endblock title %}
{% block blog_content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>文章列表</h2>
<a href="{% url 'blog:article_create' %}" class="btn btn-primary">
建立文章
</a>
</div>
<div class="card mb-4">
<div class="card-body">
<form method="get" class="row g-3"> {# (1)! #}
<div class="col-md-10">
<input type="text"
name="search"
class="form-control"
placeholder="搜尋標題..."
value="{{ search }}"> {# (2)! #}
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">
搜尋
</button>
</div>
</form>
</div>
</div>
<div class="row">
{% for article in articles %}
{% include "blog/components/article_card.html" %} {# (3)! #}
{% empty %}
<div class="col-12">
<div class="alert alert-info">
目前沒有文章
</div>
</div>
{% endfor %}
</div>
{% endblock blog_content %}
- 使用 GET 方法,參數會出現在 URL 中
- 顯示目前的搜尋關鍵字(保持表單狀態)
- 使用
article_card.html組件顯示文章卡片
GET vs POST
使用 GET 的原因:
- 可以分享連結:
/articles/?search=Django可以直接分享給別人 - 可以加入書籤:使用者可以收藏搜尋結果
- 符合語意:GET 用於取得資料,不修改資料
- 瀏覽器歷史:可以使用上一頁、下一頁
不使用 POST 的原因:
- POST 用於修改資料(建立、更新、刪除)
- POST 的結果無法被分享或加入書籤
- 重新整理頁面會出現「重新送出表單」警告
測試手動篩選¶
啟動開發伺服器:
測試搜尋功能¶
- 訪問 http://127.0.0.1:8000/blog/articles/
- 在搜尋框輸入關鍵字,例如「Django」
- 點擊「搜尋」
- URL 變成
/articles/?search=Django - 只顯示標題包含「Django」的文章
測試手動指定參數¶
直接訪問:http://127.0.0.1:8000/blog/articles/?author=1 → 只顯示作者 ID 為 1 的文章
手動篩選的優缺點
優點:
- ✅ 簡單直接
- ✅ 完全掌控邏輯
- ✅ 不需要額外套件
缺點:
- ❌ 程式碼重複(每個篩選條件都要寫一次)
- ❌ 難以維護(新增篩選條件要改 View 和 Template)
- ❌ 沒有驗證(使用者可以輸入任意參數)
- ❌ 功能有限(難以實作進階篩選)
讓我們使用 django-filter 來解決這些問題!
方法二:使用 django-filter 套件¶
django-filter 是一個強大的套件,可以輕鬆實作複雜的篩選功能。
安裝 django-filter¶
使用 uv 安裝:
設定 Django¶
在 core/settings.py 中加入 django-filter 到 INSTALLED_APPS:
INSTALLED_APPS = [
# Django 內建 apps
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# 第三方 apps
"django_bootstrap5",
"django_extensions",
"django_filters", # (1)!
# 本地 apps
"practices",
"blog",
]
- 注意是
django_filters(底線,有 s)
套件名稱注意
- 安裝時:
django-filter(連字號,無 s) - INSTALLED_APPS:
django_filters(底線,有 s)
這是套件的命名,不要搞混了!
建立 FilterSet¶
建立 blog/filters.py:
import django_filters
from blog.models import Article
class ArticleFilter(django_filters.FilterSet): # (1)!
class Meta:
model = Article
fields = { # (2)!
"title": ["icontains"], # (3)!
"author": ["exact"], # (4)!
}
- 繼承
FilterSet類別 - 使用字典格式定義欄位及其查詢方式
title使用icontains(不區分大小寫的模糊搜尋)author使用exact(精確符合)
FilterSet 的優勢
自動產生:
- ✅ 自動產生表單欄位
- ✅ 自動產生 QuerySet 篩選邏輯
- ✅ 自動處理參數驗證
彈性高:
- ✅ 可以客製化每個欄位的行為
- ✅ 支援多種篩選類型(精確、模糊、範圍等)
- ✅ 可以篩選關聯欄位
修改 View¶
修改 blog/views.py:
from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect, render
from blog.filters import ArticleFilter
from blog.forms import ArticleForm
from blog.models import Article
def article_list(request):
filter_ = ArticleFilter( # (1)!
request.GET or None,
queryset=Article.objects.select_related("author").prefetch_related("tags"),
)
return render(request, "blog/article_list.html", {"filter": filter_})
def article_detail(request, article_id):
article = get_object_or_404(
Article.objects.select_related("author").prefetch_related("tags"),
id=article_id,
)
return render(request, "blog/article_detail.html", {"article": article})
def article_create(request):
form = ArticleForm(request.POST or None)
if form.is_valid():
article = form.save()
messages.success(request, f"文章「{article.title}」已成功建立。")
return redirect("blog:article_detail", article_id=article.id)
return render(request, "blog/article_create.html", {"form": form})
def article_edit(request, article_id):
article = get_object_or_404(Article, id=article_id)
form = ArticleForm(request.POST or None, instance=article)
if form.is_valid():
article = form.save()
messages.success(request, f"文章「{article.title}」已成功更新。")
return redirect("blog:article_detail", article_id=article.id)
return render(request, "blog/article_edit.html", {"form": form, "article": article})
def article_delete(request, article_id):
article = get_object_or_404(Article, id=article_id)
if request.method == "POST":
article.delete()
messages.success(request, f"文章「{article.title}」已成功刪除。")
return redirect("blog:article_list")
return render(request, "blog/article_delete.html", {"article": article})
- 使用
filter_變數名稱(因為filter是 Python 內建函數)
FilterSet 的運作
# 1. 建立 Filter
filter_ = ArticleFilter(request.GET, queryset=Article.objects.all())
# 2. Filter 自動根據 GET 參數篩選
# 例如:?title__icontains=Django&author=1
# → filter_.qs 會是篩選後的 QuerySet
# 3. filter_.qs 就是篩選後的結果
articles = filter_.qs
Filter 物件包含:
filter_.form:篩選表單filter_.qs:篩選後的 QuerySetfilter_.data:GET 參數資料
為什麼使用 filter_ 而不是 filter?
原因:filter 是 Python 的內建函數
# filter 是 Python 內建函數,用於過濾序列
numbers = [1, 2, 3, 4, 5]
even_numbers = filter(lambda x: x % 2 == 0, numbers)
# 如果使用 filter 作為變數名稱,會覆蓋內建函數
filter = ArticleFilter(...) # ❌ 覆蓋了內建的 filter 函數
解決方法:
- 使用
filter_(加底線) - 或使用其他名稱如
article_filter
慣例上使用 filter_ 最簡潔明瞭。
更新 Template¶
修改 blog/templates/blog/article_list.html:
{% extends "blog/base.html" %}
{% load django_bootstrap5 %}
{% block title %}
文章列表 - Django 大冒險
{% endblock title %}
{% block blog_content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>
文章列表
</h2>
<a href="{% url 'blog:article_create' %}" class="btn btn-primary">
建立文章
</a>
</div>
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">
篩選條件
</h5>
<form method="get">
{% bootstrap_form filter.form %}
<div class="d-flex gap-2">
{% bootstrap_button button_type="submit" content="搜尋" %}
<a href="{% url 'blog:article_list' %}" class="btn btn-secondary">清除</a>
</div>
</form>
</div>
</div>
<div class="row">
{% for article in filter.qs %}
{% include "blog/components/article_card.html" %}
{% empty %}
<div class="col-12">
<div class="alert alert-info">
沒有符合條件的文章
</div>
</div>
{% endfor %}
</div>
{% endblock blog_content %}
bootstrap_form 的優勢
{% bootstrap_form filter.form %} 會自動:
- ✅ 渲染所有篩選欄位
- ✅ 套用 Bootstrap 樣式
- ✅ 顯示欄位標籤
- ✅ 保持表單狀態(顯示目前的篩選條件)
對比手動寫法:
<!-- ❌ 手動:需要逐一指定欄位 -->
{% bootstrap_field filter.form.title %}
{% bootstrap_field filter.form.author %}
<!-- 每新增一個篩選欄位都要加一行 -->
<!-- ✅ 自動:一行搞定所有欄位 -->
{% bootstrap_form filter.form %}
這樣新增篩選欄位時,Template 不需要修改!
測試 django-filter¶
測試標題搜尋¶
- 訪問文章列表頁
- 在「標題」欄位輸入關鍵字
- 點擊「搜尋」
- 只顯示標題包含關鍵字的文章
- 表單保持輸入的關鍵字
測試作者篩選¶
- 在「作者」下拉選單選擇特定作者
- 點擊「搜尋」
- 只顯示該作者的文章
- 下拉選單保持選擇的作者
測試組合篩選¶
- 同時輸入標題關鍵字和選擇作者
- 點擊「搜尋」
- 顯示「該作者」且「標題包含關鍵字」的文章
測試清除篩選¶
- 點擊「清除」按鈕
- 回到文章列表頁
- 顯示所有文章
進階:新增更多篩選欄位¶
加入標籤篩選¶
修改 blog/filters.py,加入標籤篩選:
import django_filters
from blog.models import Article
class ArticleFilter(django_filters.FilterSet):
class Meta:
model = Article
fields = {
"title": ["icontains"],
"author": ["exact"],
"tags": ["exact"], # (1)!
}
- 加入
tags篩選,使用exact(精確符合)
自動產生的優勢
django-filter 會根據欄位類型自動選擇合適的表單元件:
- title(CharField)→ 文字輸入框
- author(ForeignKey)→ 下拉選單
- tags(ManyToManyField)→ 多選框
完全不需要手動設定!
使用多個 lookup_expr¶
你可以為同一個欄位設定多個查詢方式:
import django_filters
from blog.models import Article
class ArticleFilter(django_filters.FilterSet):
class Meta:
model = Article
fields = {
"title": ["exact", "icontains"], # (1)!
"author": ["exact"],
"tags": ["exact"],
}
title會產生兩個篩選欄位:精確搜尋和模糊搜尋
常用 lookup_expr
| lookup_expr | 說明 | 適用欄位 |
|---|---|---|
exact |
精確符合 | 所有欄位 |
iexact |
不區分大小寫精確符合 | 文字欄位 |
contains |
包含 | 文字欄位 |
icontains |
不區分大小寫包含 | 文字欄位 |
gt / gte |
大於 / 大於等於 | 數字、日期 |
lt / lte |
小於 / 小於等於 | 數字、日期 |
何時需要自訂篩選欄位?¶
大多數情況下,使用 Meta.fields 就足夠了。只有在以下情況才需要自訂欄位:
- 需要客製化中文標籤
- 需要特殊的 widget(如日期選擇器)
- 需要自訂查詢邏輯
在這些情況下,可以自訂 Filter 欄位:
class ArticleFilter(django_filters.FilterSet):
# 自訂欄位:可以設定 label、widget 等
title = django_filters.CharFilter(
field_name="title",
lookup_expr="icontains",
label="文章標題", # 自訂標籤
)
class Meta:
model = Article
fields = {
"author": ["exact"], # 其他欄位仍使用自動產生
}
Meta.fields 的兩種寫法¶
寫法一:列表(簡單)¶
class ArticleFilter(django_filters.FilterSet):
class Meta:
model = Article
fields = ["title", "author", "tags"] # (1)!
- 使用預設的
exact查詢
優點:簡潔
缺點:無法指定查詢方式
寫法二:字典(彈性)¶
class ArticleFilter(django_filters.FilterSet):
class Meta:
model = Article
fields = {
"title": ["icontains"], # (1)!
"author": ["exact"],
"tags": ["exact"],
}
- 可以為每個欄位指定查詢方式
優點:可以自訂查詢方式
缺點:稍微冗長
建議
大多數情況下使用字典寫法,這樣可以:
- 明確知道每個欄位的查詢方式
- 方便日後調整
常見問題¶
為什麼篩選沒有生效?¶
檢查以下幾點:
-
是否在 INSTALLED_APPS 加入 django_filters:
-
FilterSet 的 Meta 中是否指定欄位:
-
View 中是否正確使用 filter_.qs:
如何客製化表單樣式?¶
使用 {% bootstrap_form %} 就已經有 Bootstrap 樣式了。
如果需要更進一步客製化,可以在 FilterSet 中設定 widget:
title = django_filters.CharFilter(
widget=forms.TextInput(attrs={
"class": "form-control",
"placeholder": "輸入標題...",
})
)
或者在 Template 中手動控制:
如何實作「清除篩選」按鈕?¶
最簡單的方法是連結回不帶參數的列表頁:
任務結束¶
完成!
恭喜你完成了這個章節!現在你已經:
- 了解為什麼需要篩選功能
- 使用 GET 參數手動實作篩選
- 安裝 django-filter 套件
- 使用 FilterSet 實作進階篩選
- 整合篩選表單到 Template
你學會了:
- GET 參數的使用:適合用於篩選和搜尋功能
- django-filter 的優勢:自動產生篩選邏輯和表單
- FilterSet 的設定:各種篩選欄位類型和選項
- 整合 django-bootstrap5:快速產生美觀的表單