使用混入與基於類別的視圖¶
注意
這是一個進階主題。建議在探索這些技巧之前,先具備Django 的基於類別的視圖的實務知識。
Django 內建的基於類別的視圖提供了許多功能,但您可能想要單獨使用其中一些功能。例如,您可能想要編寫一個視圖,該視圖會渲染一個範本以產生 HTTP 回應,但您無法使用 TemplateView
;也許您只需要在 POST
上渲染範本,而 GET
則完全做其他事情。雖然您可以直接使用 TemplateResponse
,但這可能會導致重複的程式碼。
因此,Django 還提供了一些混入,這些混入提供了更獨立的功能。例如,範本渲染封裝在 TemplateResponseMixin
中。Django 參考文件包含所有混入的完整文件。
內容和範本回應¶
提供兩個主要的混入,可協助為在基於類別的視圖中使用範本提供一致的介面。
TemplateResponseMixin
每個傳回
TemplateResponse
的內建視圖都會呼叫render_to_response()
方法,該方法由TemplateResponseMixin
提供。大多數情況下,這會為您呼叫 (例如,它由TemplateView
和DetailView
實作的get()
方法呼叫);同樣地,您不太可能需要覆寫它,但是如果您希望您的回應傳回一些不是透過 Django 範本渲染的內容,那麼您就會想要這麼做。有關此範例,請參閱 JSONResponseMixin 範例。render_to_response()
本身會呼叫get_template_names()
,該方法預設會尋找基於類別的視圖上的template_name
;另外兩個混入 (SingleObjectTemplateResponseMixin
和MultipleObjectTemplateResponseMixin
) 會覆寫此方法,以便在處理實際物件時提供更彈性的預設值。ContextMixin
每個需要內容資料的內建視圖 (例如用於渲染範本的視圖 (包括上面的
TemplateResponseMixin
)) 都應該呼叫get_context_data()
,並傳入它們想要確保作為關鍵字引數的任何資料。get_context_data()
會傳回字典;在ContextMixin
中,它會傳回其關鍵字引數,但通常會覆寫此方法,以在字典中新增更多成員。您也可以使用extra_context
屬性。
建立 Django 的通用基於類別的視圖¶
讓我們看看 Django 的兩個通用基於類別的視圖如何由提供獨立功能的混入建構而成。我們將考慮 DetailView
,它會渲染物件的「詳細資料」視圖,以及 ListView
,它會渲染物件清單,通常來自查詢集,並且可以選擇性地對它們進行分頁。這將向我們介紹四個混入,它們之間在處理單個 Django 物件或多個物件時提供有用的功能。
通用編輯視圖 (FormView
和模型專用視圖 CreateView
、UpdateView
和 DeleteView
) 和基於日期的通用視圖中也涉及一些混入。這些內容在混入參考文件中涵蓋。
DetailView
:使用單個 Django 物件¶
為了顯示物件的詳細資料,我們基本上需要做兩件事:我們需要尋找物件,然後我們需要建立一個帶有合適範本和該物件作為內容的 TemplateResponse
。
為了取得物件,DetailView
依賴 SingleObjectMixin
,它提供了 get_object()
方法,該方法會根據要求的 URL 來找出物件 (它會尋找在 URLConf 中宣告的 pk
和 slug
關鍵字引數,並從視圖上的 model
屬性或提供的 queryset
屬性中尋找物件)。SingleObjectMixin
也會覆寫 get_context_data()
,它會在 Django 的所有內建基於類別的視圖中使用,以提供範本渲染的內容資料。
為了接著建立 TemplateResponse
,DetailView
使用了 SingleObjectTemplateResponseMixin
,它繼承了 TemplateResponseMixin
,並覆寫了 get_template_names()
,如上所述。它實際上提供了一組相當複雜的選項,但大多數人主要會用到的是 <app_label>/<model_name>_detail.html
。 _detail
部分可以透過在子類別上設定 template_name_suffix
為其他值來更改。(例如,通用編輯視圖在建立和更新視圖中使用 _form
,在刪除視圖中使用 _confirm_delete
。)
ListView
:使用多個 Django 物件¶
物件列表大致遵循相同的模式:我們需要一個(可能分頁的)物件列表,通常是 QuerySet
,然後我們需要使用該物件列表建立一個具有適當範本的 TemplateResponse
。
為了取得物件,ListView
使用了 MultipleObjectMixin
,它同時提供了 get_queryset()
和 paginate_queryset()
。與 SingleObjectMixin
不同,這裡不需要從 URL 的部分中推斷出要使用的 queryset,因此預設會使用視圖類別上的 queryset
或 model
屬性。這裡覆寫 get_queryset()
的一個常見原因是動態變更物件,例如根據目前使用者或排除部落格中未來的文章。
MultipleObjectMixin
也覆寫了 get_context_data()
以包含分頁的適當內容變數(如果停用分頁則提供虛擬變數)。它依賴於作為關鍵字引數傳入的 object_list
,這是 ListView
安排的。
為了建立 TemplateResponse
,ListView
接著使用 MultipleObjectTemplateResponseMixin
;與上面的 SingleObjectTemplateResponseMixin
相同,這個會覆寫 get_template_names()
以提供 一系列 選項
,其中最常用的是 <app_label>/<model_name>_list.html
,其中 _list
部分同樣取自 template_name_suffix
屬性。(基於日期的通用視圖使用例如 _archive
、_archive_year
等後綴,以便為各種特殊的基於日期的列表視圖使用不同的範本。)
使用 Django 的基於類別的視圖 mixins¶
現在我們已經了解了 Django 的通用基於類別的視圖如何使用提供的 mixins,讓我們看看其他可以結合使用它們的方法。我們仍然會將它們與內建的基於類別的視圖或其他通用基於類別的視圖結合使用,但您可以解決一系列 Django 無法直接提供的更罕見的問題。
警告
並非所有 mixins 都可以一起使用,也並非所有通用基於類別的視圖都可以與所有其他 mixins 一起使用。這裡我們提供幾個可行的範例;如果您想要整合其他功能,那麼您必須考慮您正在使用的不同類別之間重疊的屬性和方法之間的互動,以及方法解析順序將如何影響方法調用的版本及其順序。
Django 的基於類別的視圖和基於類別的視圖 mixins的參考文件將幫助您了解哪些屬性和方法可能會導致不同類別和 mixins 之間發生衝突。
如有疑問,最好退一步,將您的工作基於 View
或 TemplateView
,或許搭配 SingleObjectMixin
和 MultipleObjectMixin
。儘管您可能最終會編寫更多程式碼,但對於稍後接觸它的人來說,它更有可能清楚易懂,並且需要擔心的互動更少,這樣您就可以省下一些思考的時間。(當然,您可以隨時參考 Django 的通用基於類別的視圖的實作,以尋找解決問題的靈感。)
將 SingleObjectMixin
與 View 一起使用¶
如果我們想編寫一個僅回應 POST
的基於類別的視圖,我們將繼承 View
,並在子類別中編寫一個 post()
方法。但是,如果我們希望我們的處理針對從 URL 識別出的特定物件,我們將需要 SingleObjectMixin
提供的功能。
我們將使用我們在通用基於類別的視圖介紹中使用的 Author
模型來示範這一點。
views.py
¶from django.http import HttpResponseForbidden, HttpResponseRedirect
from django.urls import reverse
from django.views import View
from django.views.generic.detail import SingleObjectMixin
from books.models import Author
class RecordInterestView(SingleObjectMixin, View):
"""Records the current user's interest in an author."""
model = Author
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden()
# Look up the author we're interested in.
self.object = self.get_object()
# Actually record interest somehow here!
return HttpResponseRedirect(
reverse("author-detail", kwargs={"pk": self.object.pk})
)
實際上,你可能更希望將興趣記錄在鍵值儲存(key-value store)中,而不是關係型資料庫,所以我們省略了這部分。視圖中唯一需要考慮使用 SingleObjectMixin
的部分是我們想要查找感興趣的作者的地方,它通過調用 self.get_object()
來實現。其他所有事情都由 mixin 為我們處理。
我們可以很容易地將其掛鉤到我們的 URL 中
urls.py
¶from django.urls import path
from books.views import RecordInterestView
urlpatterns = [
# ...
path(
"author/<int:pk>/interest/",
RecordInterestView.as_view(),
name="author-interest",
),
]
請注意 pk
這個具名群組,get_object()
使用它來查找 Author
實例。你也可以使用 slug,或 SingleObjectMixin
的任何其他功能。
將 SingleObjectMixin
與 ListView
一起使用¶
ListView
提供了內建的分頁功能,但你可能希望對一個列表進行分頁,而該列表中的所有物件都通過外鍵連結到另一個物件。在我們的出版範例中,你可能希望翻閱特定出版商的所有書籍。
一種方法是將 ListView
與 SingleObjectMixin
結合使用,以便分頁書籍列表的查詢集可以掛在作為單一物件找到的出版商上。為了做到這一點,我們需要有兩個不同的查詢集
Book
查詢集,供ListView
使用由於我們可以訪問想要列出其書籍的
Publisher
,因此我們覆寫get_queryset()
並使用Publisher
的 反向外鍵管理員。Publisher
查詢集,用於get_object()
我們將依賴
get_object()
的預設實作來獲取正確的Publisher
物件。但是,我們需要明確地傳遞queryset
參數,因為否則get_object()
的預設實作會調用get_queryset()
,而我們已經覆寫它以返回Book
物件,而不是Publisher
物件。
注意
我們必須仔細考慮 get_context_data()
。由於 SingleObjectMixin
和 ListView
如果設置了 context_object_name
,都會將內容放入上下文資料中,因此我們將明確確保 Publisher
在上下文資料中。ListView
將為我們添加合適的 page_obj
和 paginator
,前提是我們記得調用 super()
。
現在我們可以編寫一個新的 PublisherDetailView
from django.views.generic import ListView
from django.views.generic.detail import SingleObjectMixin
from books.models import Publisher
class PublisherDetailView(SingleObjectMixin, ListView):
paginate_by = 2
template_name = "books/publisher_detail.html"
def get(self, request, *args, **kwargs):
self.object = self.get_object(queryset=Publisher.objects.all())
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["publisher"] = self.object
return context
def get_queryset(self):
return self.object.book_set.all()
請注意我們如何在 get()
內設置 self.object
,以便我們稍後可以在 get_context_data()
和 get_queryset()
中再次使用它。如果沒有設置 template_name
,模板將預設為普通的 ListView
選項,在這種情況下將是 "books/book_list.html"
,因為它是書籍列表;ListView
不知道 SingleObjectMixin
,所以它不知道這個視圖與 Publisher
有任何關係。
範例中的 paginate_by
刻意設置得很小,這樣你就不必建立很多書籍來查看分頁是否正常工作!以下是你想要使用的模板
{% extends "base.html" %}
{% block content %}
<h2>Publisher {{ publisher.name }}</h2>
<ol>
{% for book in page_obj %}
<li>{{ book.title }}</li>
{% endfor %}
</ol>
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
{% endif %}
</span>
</div>
{% endblock %}
避免更複雜的情況¶
通常,當你需要它們的功能時,可以使用 TemplateResponseMixin
和 SingleObjectMixin
。如上所示,經過一些處理,你甚至可以將 SingleObjectMixin
與 ListView
結合使用。但是,當你嘗試這樣做時,事情會變得越來越複雜,一個好的經驗法則是
提示
每個視圖應僅使用來自其中一組泛型類別視圖的 mixin 或視圖:詳細、列表、編輯 和日期。例如,將 TemplateView
(內建視圖)與 MultipleObjectMixin
(泛型列表)結合使用是可行的,但是將 SingleObjectMixin
(泛型詳細)與 MultipleObjectMixin
(泛型列表)結合使用可能會遇到問題。
為了展示當你嘗試變得更複雜時會發生什麼,我們展示一個範例,當有更簡單的解決方案時,它會犧牲可讀性和可維護性。首先,讓我們看看將 DetailView
與 FormMixin
結合使用的天真嘗試,以便我們能夠將 Django Form
POST
到與我們使用 DetailView
顯示物件相同的 URL。
將 FormMixin
與 DetailView
一起使用¶
回想一下我們之前將 View
和 SingleObjectMixin
一起使用的範例。我們記錄了使用者對特定作者的興趣;現在假設我們想讓他們留下訊息說明他們喜歡該作者的原因。再次假設我們不將其儲存在關係型資料庫中,而是儲存在一些更深奧的東西中,我們在這裡不必擔心。
此時,自然會想到使用 Form
來封裝從使用者瀏覽器發送到 Django 的資訊。假設我們也大量投資 REST,因此我們想使用相同的 URL 來顯示作者,並從使用者捕獲訊息。讓我們重寫我們的 AuthorDetailView
來做到這一點。
我們會保留來自 DetailView
的 GET
處理,儘管我們必須在上下文資料中加入一個 Form
,以便我們可以在模板中渲染它。我們還需要從 FormMixin
中引入表單處理,並編寫一些程式碼,以便在 POST
時能適當地呼叫表單。
注意
我們使用 FormMixin
並自己實作 post()
,而不是嘗試將 DetailView
與 FormView
(它已經提供了合適的 post()
)混合,因為這兩個視圖都實作了 get()
,事情會變得更加混亂。
我們新的 AuthorDetailView
看起來像這樣
# CAUTION: you almost certainly do not want to do this.
# It is provided as part of a discussion of problems you can
# run into when combining different generic class-based view
# functionality that is not designed to be used together.
from django import forms
from django.http import HttpResponseForbidden
from django.urls import reverse
from django.views.generic import DetailView
from django.views.generic.edit import FormMixin
from books.models import Author
class AuthorInterestForm(forms.Form):
message = forms.CharField()
class AuthorDetailView(FormMixin, DetailView):
model = Author
form_class = AuthorInterestForm
def get_success_url(self):
return reverse("author-detail", kwargs={"pk": self.object.pk})
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden()
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
# Here, we would record the user's interest using the message
# passed in form.cleaned_data['message']
return super().form_valid(form)
get_success_url()
提供了一個重新導向的位置,它在 form_valid()
的預設實作中使用。如前所述,我們必須提供自己的 post()
。
一個更好的解決方案¶
FormMixin
和 DetailView
之間微妙的互動數量已經考驗了我們管理事物的能力。您不太可能想自己編寫這種類別。
在這種情況下,您可以自己編寫 post()
方法,僅保留 DetailView
作為唯一通用的功能,儘管編寫 Form
處理程式碼會涉及大量重複。
或者,使用單獨的視圖來處理表單仍然比上述方法要少做一些工作,它可以不帶任何顧慮地使用與 DetailView
不同的 FormView
。
另一個更好的解決方案¶
我們在這裡真正想要做的是從同一個 URL 使用兩個不同的基於類別的視圖。那麼為什麼不這樣做呢?我們在這裡有一個非常清晰的區分:GET
請求應該取得 DetailView
(將 Form
添加到上下文資料中),而 POST
請求應該取得 FormView
。我們先設定好這些視圖。
AuthorDetailView
視圖幾乎與 我們首次引入 AuthorDetailView 時相同;我們必須編寫自己的 get_context_data()
,以使 AuthorInterestForm
可用於範本。為了清楚起見,我們將跳過之前的 get_object()
覆寫。
from django import forms
from django.views.generic import DetailView
from books.models import Author
class AuthorInterestForm(forms.Form):
message = forms.CharField()
class AuthorDetailView(DetailView):
model = Author
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["form"] = AuthorInterestForm()
return context
然後 AuthorInterestFormView
是一個 FormView
,但我們必須引入 SingleObjectMixin
,以便我們找到我們正在談論的作者,並且我們必須記得設定 template_name
,以確保表單錯誤將呈現與 AuthorDetailView
在 GET
上使用的相同範本。
from django.http import HttpResponseForbidden
from django.urls import reverse
from django.views.generic import FormView
from django.views.generic.detail import SingleObjectMixin
class AuthorInterestFormView(SingleObjectMixin, FormView):
template_name = "books/author_detail.html"
form_class = AuthorInterestForm
model = Author
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden()
self.object = self.get_object()
return super().post(request, *args, **kwargs)
def get_success_url(self):
return reverse("author-detail", kwargs={"pk": self.object.pk})
最後,我們在新的 AuthorView
視圖中將其組合在一起。我們已經知道,在基於類別的視圖上呼叫 as_view()
會給我們一些與基於函數的視圖完全相同的行為,因此我們可以在我們選擇兩個子視圖之間進行選擇時執行此操作。
您可以將關鍵字引數傳遞給 as_view()
,就像在 URLconf 中一樣,例如,如果您希望 AuthorInterestFormView
行為也出現在另一個 URL 上,但使用不同的範本。
from django.views import View
class AuthorView(View):
def get(self, request, *args, **kwargs):
view = AuthorDetailView.as_view()
return view(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
view = AuthorInterestFormView.as_view()
return view(request, *args, **kwargs)
這種方法也可以與任何其他基於類別的通用視圖或您自己的直接從 View
或 TemplateView
繼承的基於類別的視圖一起使用,因為它使不同的視圖盡可能保持分離。
不僅僅是 HTML¶
當您想多次執行相同的操作時,基於類別的視圖會發揮作用。假設您正在編寫 API,並且每個視圖都應返回 JSON 而不是渲染的 HTML。
我們可以建立一個混合類別以在我們所有的視圖中使用,一次性處理到 JSON 的轉換。
例如,JSON 混合類別可能看起來像這樣
from django.http import JsonResponse
class JSONResponseMixin:
"""
A mixin that can be used to render a JSON response.
"""
def render_to_json_response(self, context, **response_kwargs):
"""
Returns a JSON response, transforming 'context' to make the payload.
"""
return JsonResponse(self.get_data(context), **response_kwargs)
def get_data(self, context):
"""
Returns an object that will be serialized as JSON by json.dumps().
"""
# Note: This is *EXTREMELY* naive; in reality, you'll need
# to do much more complex handling to ensure that arbitrary
# objects -- such as Django model instances or querysets
# -- can be serialized as JSON.
return context
注意
請查看序列化 Django 物件文件,以獲取有關如何正確將 Django 模型和查詢集轉換為 JSON 的更多資訊。
此混合類別提供了一個與 render_to_response()
具有相同簽章的 render_to_json_response()
方法。要使用它,我們需要將其混合到一個 TemplateView
中,例如,並覆寫 render_to_response()
來呼叫 render_to_json_response()
來代替。
from django.views.generic import TemplateView
class JSONView(JSONResponseMixin, TemplateView):
def render_to_response(self, context, **response_kwargs):
return self.render_to_json_response(context, **response_kwargs)
同樣,我們可以將我們的混合類別與其中一個通用視圖一起使用。我們可以透過將 JSONResponseMixin
與 BaseDetailView
混合來製作我們自己的 DetailView
版本 –(在混合範本渲染行為之前)DetailView
。
from django.views.generic.detail import BaseDetailView
class JSONDetailView(JSONResponseMixin, BaseDetailView):
def render_to_response(self, context, **response_kwargs):
return self.render_to_json_response(context, **response_kwargs)
然後,可以像部署任何其他 DetailView
一樣部署此視圖,具有完全相同的行為 – 除了回應的格式。
如果您想真正冒險,您甚至可以混合一個 DetailView
子類別,該子類別能夠根據 HTTP 請求的某些屬性(例如查詢引數或 HTTP 標頭)返回 HTML 和 JSON 內容。混合 JSONResponseMixin
和 SingleObjectTemplateResponseMixin
,並覆寫 render_to_response()
的實作,以根據使用者請求的回應類型延遲到適當的渲染方法。
from django.views.generic.detail import SingleObjectTemplateResponseMixin
class HybridDetailView(
JSONResponseMixin, SingleObjectTemplateResponseMixin, BaseDetailView
):
def render_to_response(self, context):
# Look for a 'format=json' GET argument
if self.request.GET.get("format") == "json":
return self.render_to_json_response(context)
else:
return super().render_to_response(context)
由於 Python 解析方法多載的方式,呼叫 super().render_to_response(context)
最後會呼叫 render_to_response()
的 TemplateResponseMixin
實作。