Class Based View¶
開始之前¶
任務目標
在這個任務中,你將學習:
- 了解 Class Based View (CBV) 的概念與優勢
- 將 DetailView 從 FBV 改寫為 CBV
- 將 ListView 從 FBV 改寫為 CBV
- 將 CreateView 從 FBV 改寫為 CBV
- 將 UpdateView 從 FBV 改寫為 CBV
- 將 DeleteView 從 FBV 改寫為 CBV
- 使用 Mixin 加入權限控制
- 了解 CBV 的常用客製化方法
什麼是 Class Based View¶
在前面的任務中,我們一直使用的是 Function Based View (FBV),也就是用函式來處理 HTTP 請求。而 Class Based View (CBV) 則是使用類別來處理請求。
FBV 的特性¶
讓我們回顧一下目前的 article_detail view:
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})
FBV 的特點:
- 直觀易懂,適合簡單的邏輯
- 每個 view 都是一個獨立的函式
- 如果有重複的邏輯,需要自己抽取成共用函式
CBV 的優勢¶
CBV 透過類別的繼承和組合,提供以下優勢:
- 可重用性:透過繼承 Django 提供的 Generic Views,大幅減少重複的程式碼
- 易於擴展:可以透過覆寫方法來客製化行為
- Mixin 組合:可以透過多重繼承組合多個 Mixin,如權限控制、分頁等功能
何時使用 CBV¶
CBV 特別適合以下情境:
- CRUD 操作:列表、詳細、新增、編輯、刪除等標準化操作
- 表單處理:有固定的表單處理流程
- 需要繼承和擴展:多個 view 有相似的邏輯
而 FBV 則適合:
- 邏輯簡單、不需要重用的 view
- 需要高度客製化的特殊邏輯
- 團隊成員對 OOP 不熟悉的情況
Django Generic Views 概覽¶
Django 提供了許多內建的 Generic Views,讓我們可以快速實作常見的功能。
常用的 Generic Views¶
- TemplateView:單純顯示模板,不涉及資料庫操作
- ListView:顯示資料列表(如文章列表)
- DetailView:顯示單筆資料的詳細內容(如文章詳細頁面)
- CreateView:建立新資料(如新增文章)
- UpdateView:更新資料(如編輯文章)
- DeleteView:刪除資料(如刪除文章)
接下來,我們將逐步把 blog app 中的 FBV 改寫為 CBV。
改寫 DetailView¶
讓我們從最簡單的 article_detail 開始改寫。
參考文件
想了解更多 DetailView 的細節,可以參考 Django 官方文件。
目前的 article_detail¶
目前 blog/views.py 中的 article_detail 是這樣的:
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})
這個 view 的工作很簡單:
- 根據
article_id取得文章 - 如果找不到就回傳 404
- 渲染模板並傳入
article變數
改寫為 CBV¶
Django 的 DetailView 已經幫我們處理好這些邏輯了,讓我們來改寫:
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.shortcuts import get_object_or_404, redirect, render
from django.views.generic import DetailView
from blog.filters import ArticleFilter
from blog.forms import ArticleForm
from blog.models import Article
def article_list(request):
filter_ = ArticleFilter(
request.GET or None,
queryset=Article.objects.select_related("author").prefetch_related("tags"),
)
return render(request, "blog/article_list.html", {"filter": filter_})
class ArticleDetailView(DetailView): # (1)!
model = Article # (2)!
pk_url_kwarg = "article_id" # (3)!
@permission_required("blog.add_article", raise_exception=True)
def article_create(request):
form = ArticleForm(request.POST or None, request.FILES or None)
if form.is_valid():
article = form.save(commit=False)
article.created_by = request.user
article.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})
@permission_required("blog.change_article", raise_exception=True)
def article_edit(request, article_id):
article = get_object_or_404(Article, id=article_id)
form = ArticleForm(request.POST or None, request.FILES 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})
@permission_required("blog.delete_article", raise_exception=True)
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})
- 繼承
DetailView類別 - 指定要使用的 Model
- 設定
pk_url_kwarg為article_id,讓 URL 參數保持使用article_id
是不是簡潔多了?DetailView 自動幫我們處理了:
- 根據 URL 參數取得物件
- 找不到時回傳 404
- 渲染模板(預設為
blog/article_detail.html) - 傳入 context 變數(預設為
object和article)
DetailView 的預設行為
DetailView 會自動根據 Model 名稱來決定:
- template_name:
<app_label>/<model_name>_detail.html(blog/article_detail.html) - context_object_name:
object和<model_name>(object和article) - pk_url_kwarg:
pk(從 URL 取得物件的主鍵參數名稱,預設為pk)
因為我們的設定都符合預設值(除了 pk_url_kwarg),所以不需要明確指定 template_name 和 context_object_name。
但我們還沒有加入查詢優化(select_related 和 prefetch_related),稍後會說明如何加入。
調整 URL 設定¶
現在需要調整 urls.py 來使用 CBV:
from django.urls import path
from blog import views
app_name = "blog"
urlpatterns = [
path("articles/", views.article_list, name="article_list"),
path("articles/create/", views.article_create, name="article_create"),
path("articles/<int:article_id>/", views.ArticleDetailView.as_view(), name="article_detail"), # (1)!
path("articles/<int:article_id>/edit/", views.article_edit, name="article_edit"),
path("articles/<int:article_id>/delete/", views.article_delete, name="article_delete"),
]
- 使用
.as_view()方法將 CBV 轉換為 view 函式
重點說明:
- 使用
.as_view()方法將 CBV 轉換為可以處理請求的函式 - URL 參數保持使用
article_id,並在 view 中透過pk_url_kwarg對應 DetailView預設會從 URL 參數中找pk或slug來查詢物件
加入查詢優化¶
現在讓我們加入查詢優化,使用 queryset 屬性:
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.shortcuts import get_object_or_404, redirect, render
from django.views.generic import DetailView
from blog.filters import ArticleFilter
from blog.forms import ArticleForm
from blog.models import Article
def article_list(request):
filter_ = ArticleFilter(
request.GET or None,
queryset=Article.objects.select_related("author").prefetch_related("tags"),
)
return render(request, "blog/article_list.html", {"filter": filter_})
class ArticleDetailView(DetailView):
queryset = Article.objects.select_related("author").prefetch_related("tags")
pk_url_kwarg = "article_id"
@permission_required("blog.add_article", raise_exception=True)
def article_create(request):
form = ArticleForm(request.POST or None, request.FILES or None)
if form.is_valid():
article = form.save(commit=False)
article.created_by = request.user
article.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})
@permission_required("blog.change_article", raise_exception=True)
def article_edit(request, article_id):
article = get_object_or_404(Article, id=article_id)
form = ArticleForm(request.POST or None, request.FILES 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})
@permission_required("blog.delete_article", raise_exception=True)
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})
queryset 是一個類別屬性,用來定義要查詢的資料集。如果你需要更複雜的邏輯(例如根據 request 動態決定查詢條件),可以覆寫 get_queryset() 方法。但對於簡單的查詢優化,使用 queryset 屬性就足夠了。
如果 view 中有定義 queryset 那 model 就可以被省略
改寫 ListView¶
接下來改寫 article_list。
參考文件
想了解更多 ListView 的細節,可以參考 Django 官方文件。
改寫 article_list¶
先來看看基本的 ListView 用法:
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.shortcuts import get_object_or_404, redirect, render
from django.views.generic import DetailView, ListView
from blog.filters import ArticleFilter
from blog.forms import ArticleForm
from blog.models import Article
class ArticleListView(ListView): # (1)!
queryset = Article.objects.select_related("author").prefetch_related("tags") # (2)!
class ArticleDetailView(DetailView):
queryset = Article.objects.select_related("author").prefetch_related("tags")
pk_url_kwarg = "article_id"
@permission_required("blog.add_article", raise_exception=True)
def article_create(request):
form = ArticleForm(request.POST or None, request.FILES or None)
if form.is_valid():
article = form.save(commit=False)
article.created_by = request.user
article.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})
@permission_required("blog.change_article", raise_exception=True)
def article_edit(request, article_id):
article = get_object_or_404(Article, id=article_id)
form = ArticleForm(request.POST or None, request.FILES 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})
@permission_required("blog.delete_article", raise_exception=True)
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})
- 繼承
ListView類別 - 使用
queryset屬性加入查詢優化(有queryset就不需要定義model)
ListView 和 DetailView 一樣,會自動根據 Model 名稱決定預設值:
- template_name:
blog/article_list.html - context_object_name:
object_list和article_list
因為符合預設值,所以不需要明確指定。
現在更新 urls.py:
from django.urls import path
from blog import views
app_name = "blog"
urlpatterns = [
path("articles/", views.ArticleListView.as_view(), name="article_list"),
path("articles/create/", views.article_create, name="article_create"),
path("articles/<int:article_id>/", views.ArticleDetailView.as_view(), name="article_detail"),
path("articles/<int:article_id>/edit/", views.article_edit, name="article_edit"),
path("articles/<int:article_id>/delete/", views.article_delete, name="article_delete"),
]
整合 django-filter¶
我們的文章列表需要整合 django-filter 來實現篩選功能。有兩種方式可以整合:
方式一:在 ListView 中手動整合¶
要在 ListView 中使用 django-filter,我們需要透過 get_context_data() 方法:
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.shortcuts import get_object_or_404, redirect, render
from django.views.generic import DetailView, ListView
from blog.filters import ArticleFilter
from blog.forms import ArticleForm
from blog.models import Article
class ArticleListView(ListView):
queryset = Article.objects.select_related("author").prefetch_related("tags")
def get_queryset(self): # (1)!
queryset = super().get_queryset()
self.filter = ArticleFilter(self.request.GET or None, queryset=queryset)
return self.filter.qs
def get_context_data(self, **kwargs): # (2)!
context = super().get_context_data(**kwargs)
context["filter"] = self.filter
return context
class ArticleDetailView(DetailView):
queryset = Article.objects.select_related("author").prefetch_related("tags")
pk_url_kwarg = "article_id"
@permission_required("blog.add_article", raise_exception=True)
def article_create(request):
form = ArticleForm(request.POST or None, request.FILES or None)
if form.is_valid():
article = form.save(commit=False)
article.created_by = request.user
article.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})
@permission_required("blog.change_article", raise_exception=True)
def article_edit(request, article_id):
article = get_object_or_404(Article, id=article_id)
form = ArticleForm(request.POST or None, request.FILES 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})
@permission_required("blog.delete_article", raise_exception=True)
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})
- 覆寫
get_queryset(),先建立 FilterSet 並保存到self.filter,然後回傳篩選後的 queryset (self.filter.qs) - 覆寫
get_context_data(),將self.filter加入 context 中
什麼是 get_context_data()?
get_context_data() 是用來準備要傳遞給模板的 context 變數的方法。當你需要在模板中使用額外的資料時,就可以透過這個方法加入。
為什麼要改寫 get_queryset()?
你可能會思考我們在 Template 中是使用 filter.qs 這樣為什麼還要改寫 get_queryset() 呢?
因為 ListView 會使用篩選後的 queryset 來進行後續的處理(例如分頁)。同時我們在使用 ListView 時也會預期在 template 中可以使用 object_list 來取得文章列表。所以就算不使用分頁等功能,我們還是要確保 object_list 是篩選後的文章列表,這樣比較符合 ListView 的預設行為。
方式二:使用 django-filter 的 FilterView¶
django-filter 提供了 FilterView,它已經整合好篩選功能,使用起來更簡單。詳細資訊可參考 django-filter 官方文件:
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.shortcuts import get_object_or_404, redirect, render
from django.views.generic import DetailView
from django_filters.views import FilterView
from blog.filters import ArticleFilter
from blog.forms import ArticleForm
from blog.models import Article
class ArticleListView(FilterView): # (1)!
queryset = Article.objects.select_related("author").prefetch_related("tags")
filterset_class = ArticleFilter # (2)!
template_name = "blog/article_list.html" # (3)!
class ArticleDetailView(DetailView):
queryset = Article.objects.select_related("author").prefetch_related("tags")
pk_url_kwarg = "article_id"
@permission_required("blog.add_article", raise_exception=True)
def article_create(request):
form = ArticleForm(request.POST or None, request.FILES or None)
if form.is_valid():
article = form.save(commit=False)
article.created_by = request.user
article.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})
@permission_required("blog.change_article", raise_exception=True)
def article_edit(request, article_id):
article = get_object_or_404(Article, id=article_id)
form = ArticleForm(request.POST or None, request.FILES 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})
@permission_required("blog.delete_article", raise_exception=True)
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})
- 繼承
FilterView而不是ListView - 指定要使用的 FilterSet 類別
- 指定要使用的 template,
FilterView預設尋找_filter的 template。在這個案例中會是blog/article_filter.html
FilterView 的特點:
- 自動處理 GET 參數並套用篩選
- 自動將 filter 物件傳入 template context
- 程式碼更簡潔,不需要覆寫
get_context_data() - FilterView 預設尋找
<app_label>/<model_name>_filter.html(blog/article_filter.html)樣板,如果要使用的名稱不一樣則需要特別指定
選擇哪種方式?
- 方式一(手動整合):適合需要更多客製化的情況,或是想要完全掌控篩選邏輯
- 方式二(FilterView):適合標準的篩選需求,程式碼更簡潔
在本教學中,我們將使用方式二(FilterView),因為它已經整合好篩選功能,程式碼更簡潔且易於維護。
改寫 CreateView¶
接下來處理表單相關的 view,從 article_create 開始。
參考文件
想了解更多 CreateView 的細節,可以參考 Django 官方文件。
改寫 article_create¶
CreateView 用來處理新增資料的邏輯:
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.shortcuts import get_object_or_404, redirect, render
from django.views.generic import CreateView, DetailView
from django_filters.views import FilterView
from blog.filters import ArticleFilter
from blog.forms import ArticleForm
from blog.models import Article
class ArticleListView(FilterView):
queryset = Article.objects.select_related("author").prefetch_related("tags")
filterset_class = ArticleFilter
template_name = "blog/article_list.html"
class ArticleDetailView(DetailView):
queryset = Article.objects.select_related("author").prefetch_related("tags")
pk_url_kwarg = "article_id"
class ArticleCreateView(CreateView): # (1)!
model = Article
form_class = ArticleForm # (2)!
template_name = "blog/article_create.html" # (3)!
@permission_required("blog.change_article", raise_exception=True)
def article_edit(request, article_id):
article = get_object_or_404(Article, id=article_id)
form = ArticleForm(request.POST or None, request.FILES 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})
@permission_required("blog.delete_article", raise_exception=True)
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})
- 繼承
CreateView類別 - 指定要使用的 Form 類別
- 指定模板名稱(預設是
blog/article_form.html,但我們使用article_create.html)
CreateView 自動處理了:
- GET 請求時顯示空白表單
- POST 請求時驗證並儲存資料
- 驗證失敗時重新顯示表單(含錯誤訊息)
但我們還需要:
- 設定
created_by欄位 - 加入成功訊息
- 設定成功後的導向
繼續修改 View 之前修改之前,先來更新 urls.py:
from django.urls import path
from blog import views
app_name = "blog"
urlpatterns = [
path("articles/", views.ArticleListView.as_view(), name="article_list"),
path("articles/create/", views.ArticleCreateView.as_view(), name="article_create"),
path("articles/<int:article_id>/", views.ArticleDetailView.as_view(), name="article_detail"),
path("articles/<int:article_id>/edit/", views.article_edit, name="article_edit"),
path("articles/<int:article_id>/delete/", views.article_delete, name="article_delete"),
]
設定 created_by¶
透過覆寫 form_valid() 方法,可以在表單驗證通過後、儲存前進行額外的處理:
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.shortcuts import get_object_or_404, redirect, render
from django.views.generic import CreateView, DetailView
from django_filters.views import FilterView
from blog.filters import ArticleFilter
from blog.forms import ArticleForm
from blog.models import Article
class ArticleListView(FilterView):
queryset = Article.objects.select_related("author").prefetch_related("tags")
filterset_class = ArticleFilter
template_name = "blog/article_list.html"
class ArticleDetailView(DetailView):
queryset = Article.objects.select_related("author").prefetch_related("tags")
pk_url_kwarg = "article_id"
class ArticleCreateView(CreateView):
model = Article
form_class = ArticleForm
template_name = "blog/article_create.html"
def form_valid(self, form): # (1)!
self.object = form.save(commit=False) # (2)!
self.object.created_by = self.request.user # (3)!
self.object.save()
form.save_m2m() # (4)!
return redirect(self.get_success_url()) # (5)!
@permission_required("blog.change_article", raise_exception=True)
def article_edit(request, article_id):
article = get_object_or_404(Article, id=article_id)
form = ArticleForm(request.POST or None, request.FILES 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})
@permission_required("blog.delete_article", raise_exception=True)
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})
- 覆寫
form_valid()方法 - 將表單資料保存到
self.object,但先不寫入資料庫 - 設定
created_by欄位 - 處理 many-to-many 關係(如果有的話)
- 手動重定向到成功頁面
form_valid() 會在表單驗證通過後被呼叫,這時候我們可以在儲存前做額外的處理。
加入成功訊息¶
現在加入成功訊息:
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.shortcuts import get_object_or_404, redirect, render
from django.views.generic import CreateView, DetailView
from django_filters.views import FilterView
from blog.filters import ArticleFilter
from blog.forms import ArticleForm
from blog.models import Article
class ArticleListView(FilterView):
queryset = Article.objects.select_related("author").prefetch_related("tags")
filterset_class = ArticleFilter
template_name = "blog/article_list.html"
class ArticleDetailView(DetailView):
queryset = Article.objects.select_related("author").prefetch_related("tags")
pk_url_kwarg = "article_id"
class ArticleCreateView(CreateView):
model = Article
form_class = ArticleForm
template_name = "blog/article_create.html"
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.created_by = self.request.user
self.object.save()
form.save_m2m()
messages.success(self.request, f"文章「{self.object.title}」已成功建立。") # (1)!
return redirect(self.get_success_url())
@permission_required("blog.change_article", raise_exception=True)
def article_edit(request, article_id):
article = get_object_or_404(Article, id=article_id)
form = ArticleForm(request.POST or None, request.FILES 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})
@permission_required("blog.delete_article", raise_exception=True)
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})
- 在
form_valid()中加入成功訊息
現在我們有了成功訊息,但還需要設定成功後要導向的位置。CreateView 預設會在儲存成功後導向到物件的詳細頁面,但前提是 Model 必須定義 get_absolute_url() 方法。
使用 Model 的 get_absolute_url()¶
接著我們要決定當表單成功被儲存後該跳轉到哪,我們可以透過在 View 中定義 success_url 屬性
class ArticleCreateView(CreateView):
model = Article
form_class = ArticleForm
template_name = "blog/article_create.html"
success_url = reverse_lazy("blog:article_list")
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.created_by = self.request.user
self.object.save()
form.save_m2m()
messages.success(self.request, f"文章「{self.object.title}」已成功建立。")
return redirect(self.get_success_url())
或是在需要使用建立完的物件可以透過 get_success_url() 方法
class ArticleCreateView(CreateView):
model = Article
form_class = ArticleForm
template_name = "blog/article_create.html"
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.created_by = self.request.user
self.object.save()
form.save_m2m()
messages.success(self.request, f"文章「{self.object.title}」已成功建立。")
return redirect(self.get_success_url())
def get_success_url(self):
return reverse("blog:article_detail", kwargs={"article_id": self.object.pk})
不過,每次都要寫 get_success_url() 有點麻煩。Django 有個更好的做法:在 Model 中定義 get_absolute_url() 方法。這樣不管多少 view 使用到這個 Model 都可以使用同一個邏輯。
讓我們在 Article model 中加入這個方法:
from django.conf import settings
from django.db import models
from django.templatetags.static import static
from django.urls import reverse
from blog.validators import validate_image_dimensions, validate_image_extension, validate_image_size
class Author(models.Model):
name = models.CharField(max_length=100)
email = models.EmailField(unique=True)
bio = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
def __str__(self):
return self.name
class Article(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
cover_image = models.ImageField(
upload_to="articles/covers/",
blank=True,
null=True,
validators=[
validate_image_size,
validate_image_extension,
validate_image_dimensions,
],
)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
is_published = models.BooleanField(default=False)
author = models.ForeignKey(
Author,
on_delete=models.CASCADE,
related_name="articles",
null=True,
blank=True,
)
tags = models.ManyToManyField(
Tag,
related_name="articles",
blank=True,
)
def __str__(self):
return self.title
def get_absolute_url(self): # (1)!
return reverse("blog:article_detail", kwargs={"article_id": self.pk})
def get_cover_image_url(self):
if self.cover_image:
return self.cover_image.url
return static("blog/images/default-cover.jpg")
- 定義
get_absolute_url()方法,回傳該物件的詳細頁面 URL
有了 get_absolute_url() 之後,Django 的 Generic Views 會自動使用它作為 success_url,不需要額外設定。
get_absolute_url() 的好處
使用 get_absolute_url() 的好處:
- 一致性:整個專案中,只要有 Article 物件就能取得它的 URL
- 簡潔:不需要在每個 View 中重複寫
get_success_url() - 可重用:可以在 template 中使用
{{ article.get_absolute_url }} - 最佳實踐:這是 Django 官方推薦的做法
改寫 UpdateView¶
UpdateView 和 CreateView 非常相似,主要差別在於它是編輯現有的資料。
參考文件
想了解更多 UpdateView 的細節,可以參考 Django 官方文件。
改寫 article_edit¶
讓我們改寫 article_edit:
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.shortcuts import get_object_or_404, redirect, render
from django.views.generic import CreateView, DetailView, UpdateView
from django_filters.views import FilterView
from blog.filters import ArticleFilter
from blog.forms import ArticleForm
from blog.models import Article
class ArticleListView(FilterView):
queryset = Article.objects.select_related("author").prefetch_related("tags")
filterset_class = ArticleFilter
template_name = "blog/article_list.html"
class ArticleDetailView(DetailView):
queryset = Article.objects.select_related("author").prefetch_related("tags")
pk_url_kwarg = "article_id"
class ArticleCreateView(CreateView):
model = Article
form_class = ArticleForm
template_name = "blog/article_create.html"
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.created_by = self.request.user
self.object.save()
form.save_m2m()
messages.success(self.request, f"文章「{self.object.title}」已成功建立。")
return redirect(self.get_success_url())
class ArticleUpdateView(UpdateView): # (1)!
model = Article
form_class = ArticleForm
template_name = "blog/article_edit.html" # (2)!
pk_url_kwarg = "article_id" # (3)!
def form_valid(self, form):
self.object = form.save()
messages.success(self.request, f"文章「{self.object.title}」已成功更新。") # (4)!
return redirect(self.get_success_url())
@permission_required("blog.delete_article", raise_exception=True)
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})
- 繼承
UpdateView類別 - 指定模板名稱(預設是
blog/article_form.html,但我們使用article_edit.html) - 設定
pk_url_kwarg為article_id,讓UpdateView知道要從 URL 的article_id參數取得物件 - 在
form_valid()中加入成功訊息
有更簡潔的方式!
你可能注意到在 form_valid() 中手動加入 messages.success() 有點重複。別擔心,在後面的「SuccessMessageMixin」章節中,我們會學到使用 SuccessMessageMixin 來簡化這個流程。
但在那之前,讓我們先理解基本的 form_valid() 覆寫方式,這對於理解 CBV 的運作機制很重要。
UpdateView 和 CreateView 的主要差異:
UpdateView會自動取得現有的物件並傳入 form- 不需要設定
created_by,因為這是編輯而非新增 - 需要指定
pk_url_kwarg來對應 URL 參數名稱(因為我們的 URL 使用article_id而不是預設的pk) - 和
CreateView一樣,會自動使用 model 的get_absolute_url()作為success_url
現在更新 urls.py:
from django.urls import path
from blog import views
app_name = "blog"
urlpatterns = [
path("articles/", views.ArticleListView.as_view(), name="article_list"),
path("articles/create/", views.ArticleCreateView.as_view(), name="article_create"),
path("articles/<int:article_id>/", views.ArticleDetailView.as_view(), name="article_detail"),
path("articles/<int:article_id>/edit/", views.ArticleUpdateView.as_view(), name="article_edit"),
path("articles/<int:article_id>/delete/", views.article_delete, name="article_delete"),
]
改寫 DeleteView¶
最後來改寫刪除功能。
參考文件
想了解更多 DeleteView 的細節,可以參考 Django 官方文件。
改寫 article_delete¶
DeleteView 用來處理刪除資料的邏輯:
from django.contrib import messages
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.views.generic import CreateView, DeleteView, DetailView, UpdateView
from django_filters.views import FilterView
from blog.filters import ArticleFilter
from blog.forms import ArticleForm
from blog.models import Article
class ArticleListView(FilterView):
queryset = Article.objects.select_related("author").prefetch_related("tags")
filterset_class = ArticleFilter
template_name = "blog/article_list.html"
class ArticleDetailView(DetailView):
queryset = Article.objects.select_related("author").prefetch_related("tags")
pk_url_kwarg = "article_id"
class ArticleCreateView(CreateView):
model = Article
form_class = ArticleForm
template_name = "blog/article_create.html"
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.created_by = self.request.user
self.object.save()
form.save_m2m()
messages.success(self.request, f"文章「{self.object.title}」已成功建立。")
return redirect(self.get_success_url())
class ArticleUpdateView(UpdateView):
model = Article
form_class = ArticleForm
template_name = "blog/article_edit.html"
pk_url_kwarg = "article_id"
def form_valid(self, form):
self.object = form.save()
messages.success(self.request, f"文章「{self.object.title}」已成功更新。")
return redirect(self.get_success_url())
class ArticleDeleteView(DeleteView): # (1)!
model = Article
template_name = "blog/article_delete.html" # (2)!
pk_url_kwarg = "article_id" # (3)!
success_url = reverse_lazy("blog:article_list") # (4)!
- 繼承
DeleteView類別 - 指定模板名稱(預設是
blog/article_confirm_delete.html,但我們使用article_delete.html) - 設定
pk_url_kwarg為article_id - 使用
reverse_lazy()設定刪除成功後的導向
使用 reverse_lazy
你可能會好奇,為什麼這裡使用 reverse_lazy() 而不是 reverse()?
class ArticleDeleteView(DeleteView):
model = Article
template_name = "blog/article_delete.html"
pk_url_kwarg = "article_id"
success_url = reverse_lazy("blog:article_list") # 為什麼用 reverse_lazy?
原因是 success_url 是在類別定義時就會被執行的屬性,而此時 Django 的 URL 設定可能還沒載入完成。reverse_lazy() 會延遲到真正需要時才執行,確保 URL 設定已經載入。
簡單來說:
reverse():立即執行,適合在方法中使用reverse_lazy():延遲執行,適合在類別屬性中使用
加入刪除成功訊息¶
現在加入刪除成功的訊息:
from django.contrib import messages
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.views.generic import CreateView, DeleteView, DetailView, UpdateView
from django_filters.views import FilterView
from blog.filters import ArticleFilter
from blog.forms import ArticleForm
from blog.models import Article
class ArticleListView(FilterView):
queryset = Article.objects.select_related("author").prefetch_related("tags")
filterset_class = ArticleFilter
template_name = "blog/article_list.html"
class ArticleDetailView(DetailView):
queryset = Article.objects.select_related("author").prefetch_related("tags")
pk_url_kwarg = "article_id"
class ArticleCreateView(CreateView):
model = Article
form_class = ArticleForm
template_name = "blog/article_create.html"
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.created_by = self.request.user
self.object.save()
form.save_m2m()
messages.success(self.request, f"文章「{self.object.title}」已成功建立。")
return redirect(self.get_success_url())
class ArticleUpdateView(UpdateView):
model = Article
form_class = ArticleForm
template_name = "blog/article_edit.html"
pk_url_kwarg = "article_id"
def form_valid(self, form):
self.object = form.save()
messages.success(self.request, f"文章「{self.object.title}」已成功更新。")
return redirect(self.get_success_url())
class ArticleDeleteView(DeleteView):
model = Article
template_name = "blog/article_delete.html"
pk_url_kwarg = "article_id"
success_url = reverse_lazy("blog:article_list")
def form_valid(self, form): # (1)!
messages.success(self.request, f"文章「{self.object.title}」已成功刪除。") # (2)!
self.object.delete() # (3)!
return redirect(self.get_success_url()) # (4)!
- 覆寫
form_valid()來加入刪除成功訊息 - 顯示成功訊息(
self.object已經由DeleteView自動設定) - 執行刪除操作
- 手動重定向到成功頁面
現在更新 urls.py:
from django.urls import path
from blog import views
app_name = "blog"
urlpatterns = [
path("articles/", views.ArticleListView.as_view(), name="article_list"),
path("articles/create/", views.ArticleCreateView.as_view(), name="article_create"),
path("articles/<int:article_id>/", views.ArticleDetailView.as_view(), name="article_detail"),
path("articles/<int:article_id>/edit/", views.ArticleUpdateView.as_view(), name="article_edit"),
path("articles/<int:article_id>/delete/", views.ArticleDeleteView.as_view(), name="article_delete"),
]
使用 Mixin 加入權限控制¶
目前我們已經把所有的 FBV 改寫成 CBV 了,但是還缺少權限控制。讓我們使用 Mixin 來加入權限檢查。
什麼是 Mixin¶
Mixin 是一種設計模式,透過多重繼承來為類別加入額外的功能。在 CBV 中,Mixin 是一個包含特定功能的類別,可以和其他類別組合使用。
例如,Django 提供的 LoginRequiredMixin 可以要求使用者必須登入才能存取 view,PermissionRequiredMixin 則可以檢查使用者是否有特定權限。
使用 Mixin 的關鍵是繼承順序:
- Mixin 要放在最左邊,基礎類別(如
CreateView)放在最右邊
Python 的多重繼承使用 MRO(Method Resolution Order)來決定方法的查找順序,按照從左到右、從子類別到父類別的順序。因此 Mixin 要放在最左邊,才能在基礎類別之前執行。
PermissionRequiredMixin¶
讓我們為 ArticleCreateView、ArticleUpdateView 和 ArticleDeleteView 加入權限檢查。詳細資訊可參考 Django 官方文件:
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.views.generic import CreateView, DeleteView, DetailView, UpdateView
from django_filters.views import FilterView
from blog.filters import ArticleFilter
from blog.forms import ArticleForm
from blog.models import Article
class ArticleListView(FilterView):
queryset = Article.objects.select_related("author").prefetch_related("tags")
filterset_class = ArticleFilter
template_name = "blog/article_list.html"
class ArticleDetailView(DetailView):
queryset = Article.objects.select_related("author").prefetch_related("tags")
pk_url_kwarg = "article_id"
class ArticleCreateView(PermissionRequiredMixin, CreateView): # (1)!
model = Article
form_class = ArticleForm
template_name = "blog/article_create.html"
permission_required = "blog.add_article" # (2)!
raise_exception = True # (3)!
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.created_by = self.request.user
self.object.save()
form.save_m2m()
messages.success(self.request, f"文章「{self.object.title}」已成功建立。")
return redirect(self.get_success_url())
class ArticleUpdateView(PermissionRequiredMixin, UpdateView):
model = Article
form_class = ArticleForm
template_name = "blog/article_edit.html"
pk_url_kwarg = "article_id"
permission_required = "blog.change_article"
raise_exception = True
def form_valid(self, form):
self.object = form.save()
messages.success(self.request, f"文章「{self.object.title}」已成功更新。")
return redirect(self.get_success_url())
class ArticleDeleteView(PermissionRequiredMixin, DeleteView):
model = Article
template_name = "blog/article_delete.html"
pk_url_kwarg = "article_id"
success_url = reverse_lazy("blog:article_list")
permission_required = "blog.delete_article"
raise_exception = True
def form_valid(self, form):
messages.success(self.request, f"文章「{self.object.title}」已成功刪除。")
self.object.delete()
return redirect(self.get_success_url())
- 在最左邊加入
PermissionRequiredMixin - 設定需要的權限
- 當權限不足時回傳 403 錯誤(而不是導向登入頁面)
現在我們的 view 已經有權限保護了,不需要再使用 @permission_required 裝飾器。
SuccessMessageMixin¶
你可能注意到我們在每個 view 中都需要覆寫 form_valid() 來加入成功訊息,這樣有點重複。Django 提供了 SuccessMessageMixin 來簡化這個流程。
SuccessMessageMixin 讓我們只需要設定 success_message 屬性,就能自動顯示成功訊息,不需要覆寫 form_valid()。讓我們用它來改寫 ArticleCreateView 和 ArticleUpdateView:
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from django.views.generic import CreateView, DeleteView, DetailView, UpdateView
from django_filters.views import FilterView
from blog.filters import ArticleFilter
from blog.forms import ArticleForm
from blog.models import Article
class ArticleListView(FilterView):
queryset = Article.objects.select_related("author").prefetch_related("tags")
filterset_class = ArticleFilter
template_name = "blog/article_list.html"
class ArticleDetailView(DetailView):
queryset = Article.objects.select_related("author").prefetch_related("tags")
pk_url_kwarg = "article_id"
class ArticleCreateView(PermissionRequiredMixin, SuccessMessageMixin, CreateView):
model = Article
form_class = ArticleForm
template_name = "blog/article_create.html"
permission_required = "blog.add_article"
raise_exception = True
success_message = "文章「%(title)s」已成功建立。"
def form_valid(self, form):
form.instance.created_by = self.request.user
return super().form_valid(form)
class ArticleUpdateView(PermissionRequiredMixin, SuccessMessageMixin, UpdateView):
model = Article
form_class = ArticleForm
template_name = "blog/article_edit.html"
pk_url_kwarg = "article_id"
permission_required = "blog.change_article"
raise_exception = True
success_message = "文章「%(title)s」已成功更新。"
class ArticleDeleteView(PermissionRequiredMixin, SuccessMessageMixin, DeleteView):
model = Article
template_name = "blog/article_delete.html"
pk_url_kwarg = "article_id"
success_url = reverse_lazy("blog:article_list")
permission_required = "blog.delete_article"
raise_exception = True
def get_success_message(self, cleaned_data):
return f"文章「{self.object.title}」已成功刪除。"
使用 SuccessMessageMixin 的好處:
- 程式碼更簡潔:不需要在每個 view 中重複寫
messages.success() - 宣告式風格:直接設定
success_message屬性,意圖更清楚 - 支援格式化:可以使用
%(field_name)s來動態插入物件的欄位值
DeleteView 使用 SuccessMessageMixin 的注意事項
DeleteView 也可以使用 SuccessMessageMixin,但需要注意的是:
- 不能直接使用
success_message = "文章「%(title)s」已成功刪除。",因為在 title 資訊不會存在 form 中。 - 需要覆寫
get_success_message()方法,並在方法中使用self.object來取得物件資訊 self.object在get_success_message()被呼叫時還存在,所以可以安全地取得物件的欄位值
ArticleCreateView 的 form_valid
可以注意一下 ArticleCreateView 的 form_valid() 方法,這邊我們換了一個寫法。
之前的寫法
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.created_by = self.request.user
self.object.save()
form.save_m2m()
return redirect(self.get_success_url())
前面我們自己處理物件的儲存(不呼叫 super().form_valid(form) )這與我們在寫 FBV 時的做法相同,但現在我們使用 SuccessMessageMixin 時用這個方法會造成 message 無法正常被發出。
因為發送 message 的動作是寫在 SuccessMessageMixin 的 form_valid 方法中,所以我們這邊得換一個做法讓父層的 form_valid 會被呼叫到。
會進入 form_valid 方法表單已經過驗證成功,我們就直接更新表單產生的物件(instance)並透過呼叫父層的 form_valid 讓父層來負責儲存物件與觸發後續的處理。
Mixin 的順序¶
再次強調,Mixin 的順序很重要:
# 正確 ✓
class ArticleCreateView(PermissionRequiredMixin, SuccessMessageMixin, CreateView):
pass
# 錯誤 ✗ - 基礎類別不應該在最左邊
class ArticleCreateView(CreateView, PermissionRequiredMixin, SuccessMessageMixin):
pass
# 也可以接受 - Mixin 順序調換(但不建議)
class ArticleCreateView(SuccessMessageMixin, PermissionRequiredMixin, CreateView):
pass
順序原則:
- 基礎類別(如 CreateView)要放在最右邊:這是最重要的規則
- 功能性 Mixin 的順序:通常按照「檢查 → 處理 → 呈現」的邏輯排列
- 權限檢查 Mixin(如
PermissionRequiredMixin)放在最左邊 - 資料處理 Mixin(如
SuccessMessageMixin)放在中間 - 基礎類別放在最右邊
- 權限檢查 Mixin(如
如果順序錯誤,可能導致:
- 權限檢查不會被執行(安全性問題)
- 成功訊息不會顯示(功能問題)
- 方法覆寫的順序不如預期(邏輯問題)
使用內建的 CBV¶
除了前面介紹的 CRUD 相關 Generic Views,Django 還提供了一些簡單實用的基礎 View,其中最常用的就是 RedirectView。
RedirectView¶
RedirectView 專門用來處理網址重定向。
在 FBV 中,如果我們要將首頁重定向到文章列表,通常會這樣寫:
但使用 RedirectView,我們可以直接在 urls.py 中設定,完全不需要寫 View 程式碼。
讓我們實際操作看看!目前連線到首頁 / 時會出現 404,讓我們把它重定向到文章列表。請將 core/urls.py 的內容修改如下:
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import include, path, reverse_lazy
from django.views.generic import RedirectView
from core import views
auth_urlpatterns = [
path(
"login/",
auth_views.LoginView.as_view(template_name="registration/login.html"),
name="login",
),
path(
"logout/",
auth_views.LogoutView.as_view(),
name="logout",
),
path("register/", views.register, name="register"),
path(
"password-change/",
auth_views.PasswordChangeView.as_view(
template_name="registration/password_change.html",
success_url=reverse_lazy("auth:password_change_done"),
),
name="password_change",
),
path(
"password-change/done/",
auth_views.PasswordChangeDoneView.as_view(
template_name="registration/password_change_done.html"
),
name="password_change_done",
),
path(
"password-reset/",
auth_views.PasswordResetView.as_view(
template_name="registration/password_reset.html",
success_url=reverse_lazy("auth:password_reset_done"),
),
name="password_reset",
),
path(
"password-reset/done/",
auth_views.PasswordResetDoneView.as_view(
template_name="registration/password_reset_done.html"
),
name="password_reset_done",
),
path(
"password-reset/<uidb64>/<token>/",
auth_views.PasswordResetConfirmView.as_view(
template_name="registration/password_reset_confirm.html",
success_url=reverse_lazy("auth:password_reset_complete"),
),
name="password_reset_confirm",
),
path(
"password-reset/complete/",
auth_views.PasswordResetCompleteView.as_view(
template_name="registration/password_reset_complete.html"
),
name="password_reset_complete",
),
]
urlpatterns = [
path("", RedirectView.as_view(pattern_name="blog:article_list"), name="root"),
path("admin/", admin.site.urls),
path("practices/", include("practices.urls")),
path("blog/", include("blog.urls")),
path("auth/", include((auth_urlpatterns, "auth"))),
]
if settings.DEBUG:
from debug_toolbar.toolbar import debug_toolbar_urls
urlpatterns = [
*urlpatterns,
*debug_toolbar_urls(),
*static(settings.STATIC_URL, document_root=settings.STATIC_ROOT),
*static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT),
]
這樣一來,當使用者訪問首頁時,就會自動被導向到文章列表頁面了!
RedirectView 的常用屬性:
- pattern_name: 目標 URL pattern 名稱(類似
reverse),最推薦使用 - url: 目標網址(字串),適合外部連結
- permanent: 是否為永久重定向(HTTP 301),預設為
False(HTTP 302)
這在處理網址重構或是設定首頁導向時非常實用。
CBV 的常用客製化方法¶
讓我們整理一下在 CBV 中常用的客製化方法。
常用的可覆寫方法¶
queryset 屬性與 get_queryset() 方法¶
使用 queryset 屬性(適合簡單的查詢優化):
class ArticleListView(ListView):
queryset = Article.objects.select_related("author").prefetch_related("tags")
使用 get_queryset() 方法(適合需要根據 request 動態決定查詢條件):
class ArticleListView(ListView):
model = Article
def get_queryset(self):
# 根據 request.user 動態決定查詢條件
if self.request.user.is_staff:
return Article.objects.all()
return Article.objects.filter(is_published=True)
如果查詢條件是固定的,優先使用 queryset 屬性,程式碼更簡潔。
get_context_data()¶
用來加入額外的 context 變數到模板中:
class ArticleListView(ListView):
model = Article
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) # 取得預設的 context
context["total_count"] = self.get_queryset().count() # 加入總數
context["filter"] = ArticleFilter(self.request.GET) # 加入篩選器
return context
form_valid()¶
在表單驗證通過後、儲存前執行額外的處理:
class ArticleCreateView(CreateView):
model = Article
form_class = ArticleForm
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.created_by = self.request.user # 設定建立者
self.object.save()
form.save_m2m() # 處理 many-to-many 關係
messages.success(self.request, "文章建立成功")
return redirect(self.get_success_url())
get_success_url() 與 get_absolute_url()¶
有兩種方式設定表單提交成功後的導向位置:
方式一:覆寫 get_success_url()
class ArticleCreateView(CreateView):
model = Article
form_class = ArticleForm
def get_success_url(self):
# 動態決定導向位置
return reverse("blog:article_detail", kwargs={"pk": self.object.pk})
方式二:使用 Model 的 get_absolute_url()(推薦)
# 在 Model 中定義
class Article(models.Model):
# ... 其他欄位 ...
def get_absolute_url(self):
return reverse("blog:article_detail", kwargs={"article_id": self.pk})
# View 中不需要定義 get_success_url()
class ArticleCreateView(CreateView):
model = Article
form_class = ArticleForm
# Django 會自動使用 self.object.get_absolute_url()
推薦使用 get_absolute_url(),因為它可以在整個專案中重用,也是 Django 的最佳實踐。
get_context_data() 的使用時機¶
當你需要在模板中使用額外的資料時,就可以使用 get_context_data()。常見的使用情境:
- 加入相關資料:在文章詳細頁面中加入相關文章列表
- 加入統計資料:顯示文章總數、評論數等
- 加入篩選器:如我們在
ArticleListView中整合django-filter
dispatch() 方法¶
dispatch() 是 view 中最早被呼叫的方法,它會根據 HTTP 方法(GET、POST 等)來呼叫對應的處理方法(如 get()、post())。
你可以覆寫 dispatch() 來在請求處理前做一些檢查或處理:
class ArticleDetailView(DetailView):
model = Article
def dispatch(self, request, *args, **kwargs):
# 在處理請求前做一些檢查
article = self.get_object()
if not article.is_published and not request.user.is_staff:
# 非管理員無法查看未發布的文章
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)
FBV vs CBV 的選擇¶
最後,讓我們討論何時該使用 FBV、何時該使用 CBV。
FBV 的優勢¶
- 直觀易懂:對於初學者或不熟悉 OOP 的開發者更友善
- 彈性高:可以隨意組織程式碼邏輯
- 除錯容易:錯誤訊息更直接,不需要追蹤繼承鏈
適合使用 FBV 的情境:
- 簡單的 view,不需要重用
- 高度客製化的邏輯,不適合套用 Generic View
- 原型開發或快速驗證想法
CBV 的優勢¶
- 可重用性:透過繼承和 Mixin 重用程式碼
- 結構清晰:職責分明,易於維護
- 減少重複:標準的 CRUD 操作幾乎不需要寫程式碼
適合使用 CBV 的情境:
- 標準的 CRUD 操作
- 需要在多個 view 間共享邏輯
- 大型專案,需要維護一致的程式碼風格
實務建議¶
在實務中,你可以混合使用 FBV 和 CBV:
- 對於標準的 CRUD 操作,使用 Generic Views(CBV)
- 對於複雜的業務邏輯或特殊需求,使用 FBV
- 將共用的邏輯抽取成 Mixin 或輔助函式
不要為了使用 CBV 而強行使用 CBV,選擇最適合當前情境的方式才是最重要的。
任務結束¶
完成!
恭喜你完成了這個任務!現在你已經學會:
- 了解 Class Based View (CBV) 的概念與優勢
- 將 DetailView 從 FBV 改寫為 CBV
- 將 ListView 從 FBV 改寫為 CBV
- 將 CreateView 從 FBV 改寫為 CBV
- 將 UpdateView 從 FBV 改寫為 CBV
- 將 DeleteView 從 FBV 改寫為 CBV
- 使用 Mixin 加入權限控制
- 了解 CBV 的常用客製化方法
透過這個任務,你已經把整個 blog app 的 view 從 Function Based View 改寫成了 Class Based View。你學會了如何使用 Django 提供的 Generic Views 來減少重複的程式碼,以及如何透過覆寫方法來客製化 CBV 的行為。
在實務開發中,CBV 特別適合處理標準的 CRUD 操作,可以大幅減少開發時間和維護成本。但記住,選擇 FBV 或 CBV 要視情況而定,不要為了使用而使用,選擇最適合當前需求的方式才是最重要的。