Django4.0 基于類的視圖-在基于類的視圖中使用混入

2022-03-16 17:58 更新

Django 內置的基于類的視圖提供了很多功能,但你可能想單獨使用有些功能。例如,你可能想寫一個渲染一個模板來生成 HTTP 響應的視圖,但你不能使用 ?TemplateView ?;也許你只需要在 POST 時渲染一個模板,用 GET 來處理其他所有事。雖然你可以直接使用 ?TemplateResponse?,但這很可能會導致重復代碼。

因此 Django 也提供了很多混入,它們提供了更多的離散功能。比如模板渲染,被封裝在 ?TemplateResponseMixin ?中。

上下文和模板響應

提供了兩個重要的混入,它們有助于在基于類的視圖中使用模板時提供一個一致的接口。

TemplateResponseMixin

每個返回 ?TemplateResponse的內置視圖都將調用 ?TemplateResponseMixin提供的 ?render_to_response() 方法。大多數(shù)時候,這個方法會被你調用(例如,它被 ?TemplateView?和 ?DetailView?共同實現(xiàn)的 ?get()方法調用);同樣,你也不太可能需要覆蓋它,但如果你想讓你的響應返回一些沒有通過 Django 模板渲染的東西,那么你會想要這樣做。

render_to_response()本身會調用 ?
get_template_names()? ,默認情況下,它會在基于類的視圖上查找 ?
template_name;另外兩個混入( ?SingleObjectTemplateResponseMixin?和 ?MultipleObjectTemplateResponseMixin)覆蓋了這一點,以在處理實際對象時提供更靈活的默認值。

ContextMixin

每個需要上下文數(shù)據(jù)的內置視圖,比如為了渲染一個模板(包括上面的 ?TemplateResponseMixin?),都應該將他們想確定傳入的數(shù)據(jù)作為關鍵字參數(shù)傳入 ?get_context_data()

調用。?get_context_data()返回一個字典;在 ?ContextMixin?中它返回它的關鍵字參數(shù),但通常覆蓋此項來增加更多成員到字典中。你也可以使用 ?extra_context屬性。

構造 Django 基于類的通用視圖

讓我們看看 Django 的兩個基于類的通用視圖是如何由提供離散功能的混入構建的。我們將考慮 ?DetailView ?,它渲染一個對象的 “詳情” 視圖,以及 ?ListView ?,它渲染一個對象列表,通常來自一個查詢集,并可選擇將它們分頁。這里將介紹四個混入,無論是在處理單個 Django 對象還是多個對象時,它們都提供了有用的功能。
通用編輯視圖( ?FormView?,和模型專用的視圖 ?CreateView?,?UpdateView ?和 ?DeleteView ?),以及基于日期的通用視圖中也涉及到混入

DetailView :使用單個 Django 對象

要顯示一個對象的詳情,我們基本上需要做兩件事:我們需要查詢對象,然后將該對象作為上下文,用一個合適的模板生成一個 ?TemplateResponse ?。
為了得到對象,?DetailView ?依賴于 ?SingleObjectMixin ?,它提供一個 ?get_object()? 方法,該方法根據(jù)請求的 URL 來找出對象(它查找 ?URLconf ?中聲明的 ?pk ?和 ?slug ?關鍵字參數(shù),并從視圖上的 ?model ?屬性查找對象,或者從提供的 ?queryset ?屬性中查找)。?SingleObjectMixin ?還覆蓋了 ?get_context_data()? ,它被用于所有 Django 內置的基于類的視圖,為模板渲染提供上下文數(shù)據(jù)。
然后為了生成一個 ?TemplateResponse?, ?DetailView ?使用了 ?SingleObjectTemplateResponseMixin?,它擴展了 ?TemplateResponseMixin?,如上所述的覆蓋了 ?get_template_names()?。它實際上提供了一組相當復雜的選項,但大多數(shù)人都會使用的主要選項是 ?<app_label>/<model_name> _detail.html?。?_detail? 部分可以通過在子類上設置 ?template_name_suffix ?來改變。(例如 通用編輯視圖 的創(chuàng)建和更新視圖使用 ?_form?,刪除視圖使用 ?_confirm_delete?。)

ListView :使用多個 Django 對象

對象列表大致遵循相同的模式:我們需要一個(可能是分頁的)對象列表,通常是 ?QuerySet ?,然后根據(jù)這個對象列表使用合適的模板生成 ?TemplateResponse ?。
為了得到對象,?ListView ?使用了 ?MultipleObjectMixin ?,它同時提供 ?get_queryset()? 和 ?paginate_queryset()? 。與 ?SingleObjectMixin ?不同的是,不需要使用部分 URL 來找出要使用的查詢集,所以默認使用視圖類上的 ?queryset ?或 ?model ?屬性。在這里覆蓋 ?get_queryset()? 的常見原因是為了動態(tài)變化的對象,比如根據(jù)當前用戶的情況,或者為了排除博客未來的文章。
?MultipleObjectMixin ?還覆蓋了 ?get_context_data()?,為分頁加入了適當?shù)纳舷挛淖兞浚ㄈ绻猪摫唤?,則提供虛假分頁)。它依賴于 ?ListView ?作為關鍵字參數(shù)傳入的 ?object_list?。
要生成一個 ?TemplateResponse ?,?ListView ?則使用 ?MultipleObjectTemplateResponseMixin ?;和上面的 ?SingleObjectTemplateResponseMixin ?一樣,它覆蓋 ?get_template_names()? 來提供一系列選項,最常用的 ?<app_label>/<model_name>_list.html ?,?_list? 部分同樣從 ?template_name_suffix ?屬性中獲取。(基于日期的通用視圖使用諸如 ?_archive? 、?_archive_year? 等后綴來為各種專門的基于日期的列表視圖使用不同的模板。)

使用 Django 的基于類的視圖混入

現(xiàn)在我們已經知道 Django 的基于類的通用視圖如何使用所提供的混入,讓我們看看使用它們的其他方式。我們仍然會將它們與內置的基于類的視圖,或者其他通用的基于類的視圖結合起來,但是,有一系列比 Django 開箱即用所提供的更罕見的問題可以被解決。

注意:不是所有的混入都可以一起使用,并且不是所有的基于類的通用視圖能和所有其他的混入一起使用。這里我們介紹一些有用的例子;如果你想把其他功能匯集在一起,那么你就必須考慮你正在使用的不同類之間重疊的屬性和方法之間的相互作用,以及 ?method resolution order? 將如何影響哪些版本的方法將以何種順序被調用。

如果有問題,最好還是退而求其次,以 ?View ?或 ?TemplateView ?為基礎,或許可以用 ?SingleObjectMixin? 和 ?MultipleObjectMixin ?。雖然你最終可能會寫出更多的代碼,但對于以后再來的人來說,更有可能清楚地理解,并且由于需要擔心的交互較少,你可以省去一些思考。

在視圖中使用 SingleObjectMixin

如果我們想編寫一個只響應 POST 的基于類的視圖,我們將子類化 View 并且在子類中編寫一個 post() 方法。但是如果想讓我們的程序在一個從 URL 中識別出來特定的對象上工作,我們就需要 ?SingleObjectMixin ?提供的功能。

我們將使用我們在基于類的通用視圖介紹中使用的 Author 模型來演示這一點。

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}))

在實際操作中,你可能會希望把興趣記錄在一個鍵值存儲中,而不是關系數(shù)據(jù)庫中,所以我們把關于數(shù)據(jù)庫的省略了。視圖在使用 ?SingleObjectMixin ?時,我們唯一需要擔心的地方是想要查找我們感興趣的作者,它通過調用? self.get_object() ?來實現(xiàn)。其他的一切都由混入替我們處理。

我們可以很簡單的將它掛接在我們的 URLs 中:

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 ?的任何其他功能。

在 ListView 中使用 SingleObjectMixin

?ListView ?提供了內置的分頁功能,但你可能想將一個對象列表分頁,而這些對象都是通過一個外鍵鏈接到另一個對象的。在我們的出版示例中,你可能想對某一出版商的所有書籍進行分頁。
一種方法是將 ?ListView ?和 ?SingleObjectMixin ?結合起來,這樣一來,用于圖書分頁列表的查詢集就可以脫離作為單個對象找到的出版商對象。 為此,我們需要兩個不同的查詢集:
?ListView ?使用的 Book 查詢集

由于我們已經得到了我們所想要書籍列表的 Publisher ,我們只需覆蓋 ?get_queryset()? 并使用的 ?Publisher ?的 反向外鍵管理器。

?get_object()? 使用的 ?Publisher ?查詢集

我們將依賴 ?get_object()? 的默認實現(xiàn)來獲取正確的 ?Publisher ?對象。然而,我們需要顯式地傳遞一個 ?queryset ?參數(shù),因為 ?get_object() ?的默認實現(xiàn)會調用 ?get_queryset()? ,我們已經覆蓋了它并返回了 Book 對象而不是 Publisher 對象。

注解:我們必須認真考慮 ?get_context_data()?。由于 ?SingleObjectMixin ?和 ?ListView ?會將上下文數(shù)據(jù)放在 ?context_object_name? 的值下(如果它已設置),我們要明確確保 ?Publisher ?在上下文數(shù)據(jù)中。?ListView ?將為我們添加合適的 ?page_obj ?和 ?paginator?,只要我們記得調用 ?super()?。

現(xiàn)在我們可以編寫一個新的 ?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 ?被刻意地縮小了,所以你不需要創(chuàng)建很多書就能看到分頁的效果。這里是你要使用的模板:

{% 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 ?結合起來。然而當你嘗試這樣做時,事情會變得越來越復雜,一個好的經驗法則是:

提示:你的每個視圖應該只使用混入或者來自一個通用基于類的視圖的組里視圖: 詳情,列表,編輯 和日期。例如,將 ?TemplateView ?(內置視圖)和 ?MultipleObjectMixin ?(通用列表)結合起來,但你可能會在 ?SingleObjectMixin ?(通用詳情)和 ?MultipleObjectMixin ?(通用列表)結合時遇到問題。

為了說明當您嘗試變得更復雜時會發(fā)生什么,我們展示了一個示例,該示例在有更簡單的解決方案時會犧牲可讀性和可維護性。 首先,讓我們看一個將 ?DetailView ?與 ?FormMixin ?結合起來的天真的嘗試,使我們能夠將 Django 表單發(fā)布到與使用 ?DetailView ?顯示對象相同的 URL。

DetailView 和 FormMixin 一起使用

回想一下我們之前使用 ?View ?和 ?SingleObjectMixin ?一起使用的例子。我們當時記錄的是一個用戶對某個作者的興趣;比如說現(xiàn)在我們想讓他們留言說為什么喜歡他們。同樣,我們假設我們不打算把這個存儲在關系型數(shù)據(jù)庫中,而是存儲在更深奧的東西中,我們在這里就不關心了。
這時自然而然就會用到一個 ?Form ?來封裝從用戶瀏覽器發(fā)送到 Django 的信息。又比如說我們在 ?REST ?上投入了大量的精力,所以我們希望用同樣的 URL 來顯示作者和捕捉用戶的信息。讓我們重寫我們的 ?AuthorDetailView ?來實現(xiàn)這個目標。
我們將保留 ?DetailView ?中的 ?GET ?處理,盡管我們必須在上下文數(shù)據(jù)中添加一個 ?Form?,這樣我們就可以在模板中渲染它。我們還要從 ?FormMixin ?中調入表單處理,并寫一點代碼,這樣在 ?POST?時,表單會被適當?shù)卣{用。

注解:我們使用 ?FormMixin ?并自己實現(xiàn) ?post()?,而不是嘗試將 ?DetailView ?與 ?FormView ?混合(它已經提供了合適的 ?post()?),因為兩個視圖都實現(xiàn)了 ?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()? 的默認實現(xiàn)中使用。如前所述,我們需要提供自己的 ?post()? 。

更好的解決方案

?FormMixin ?和 ?DetailView ?之間微妙交互已經在測試我們管理事務的能力了。你不太可能想寫這樣的類。
在這個例子里,你可以編寫 ?post()? 讓 ?DetailView ?作為唯一的通用功能,盡管編寫 ?Form ?的處理代碼會涉及到很多重復的地方。
或者,使用單獨的視圖來處理表單仍然比上述方法工作量小,它可以使用 ?FormView ?,而不必擔心任何問題。

另一種更好的解決方案

我們在這里真正想做的是使用來自同一個 URL 的兩個不同的基于類的視圖。 那么為什么不這樣做呢? 我們這里有一個非常明確的劃分:?GET ?請求應該獲取 ?DetailView?(將 ?Form ?添加到上下文數(shù)據(jù)中),?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

那么?AuthorInterestForm?是一個?FormView?,但是我們必須引入?SingleObjectMixin?,這樣我們才能找到我們正在談論的作者,并且我們必須記住設置?template_name?以確保表單錯誤會呈現(xiàn)與?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()? 會給我們一些行為與基于函數(shù)的視圖完全相同的東西,因此我們可以在兩個子視圖之間進行選擇時這樣做。

您可以像在 ?URLconf ?中一樣將關鍵字參數(shù)傳遞給 ?as_view()?,例如,如果您希望 ?AuthorInterestFormView? 行為也出現(xiàn)在另一個 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)

這個方式也可以被任何其他通用基于類的視圖,或你自己實現(xiàn)的直接繼承自 ?View ?或 ?TemplateView ?的基于類的視圖使用,因為它使不同視圖盡可能分離。

不僅僅是HTML

基于類的視圖的優(yōu)勢是你可以多次執(zhí)行相同操作。假設你正在編寫 API,那么每個視圖應該返回 JSON,而不是渲染 HTML。

我們可以創(chuàng)建一個混入類來在所有視圖里使用,用它來進行一次轉換到 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

混入提供了 ?render_to_json_response()? 方法,其簽名與 ?render_to_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)

同樣,我們可以將我們的 ?mixin ?與通用視圖之一一起使用。 我們可以通過將 ?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 ?相同的方式部署此視圖,具有完全相同的行為——除了響應的格式。

您甚至可以混合一個能夠返回 HTML 和 JSON 內容的 ?DetailView ?子類,具體取決于 HTTP 請求的某些屬性,例如查詢參數(shù)或 HTTP 表頭。 混合 ?JSONResponseMixin ?和 ?SingleObjectTemplateResponseMixin?,并覆蓋 ?render_to_response()? 的實現(xiàn),以根據(jù)用戶請求的響應類型推遲到適當?shù)某尸F(xiàn)方法:

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)? 的調用最終會調用 ?TemplateResponseMixin ?的 ?render_to_response()? 實現(xiàn)。


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號