在基于类的视图中使用 mixin — Django 文档

来自菜鸟教程
Django/docs/3.2.x/topics/class-based-views/mixins
跳转至:导航、​搜索

在基于类的视图中使用混入

警告

这是一个高级话题。 在探索这些技术之前,建议先了解 Django 的基于类的视图


Django 内置的基于类的视图提供了很多功能,但您可能希望单独使用其中的一些功能。 例如,您可能想编写一个视图来呈现模板以进行 HTTP 响应,但您不能使用 TemplateView; 也许您只需要在 POST 上渲染模板,而 GET 则完全做其他事情。 虽然您可以直接使用 TemplateResponse,但这可能会导致重复代码。

出于这个原因,Django 还提供了许多提供更多离散功能的 mixin。 例如,模板渲染被封装在 TemplateResponseMixin 中。 Django 参考文档包含所有 mixins 的完整文档

上下文和模板响应

提供了两个重要的混入,它们有助于在基于类的视图中使用模板时提供一个一致的接口。

TemplateResponseMixin

每个返回 TemplateResponse 的内置视图都将调用 TemplateResponseMixin 提供的 render_to_response() 方法。 大多数情况下,它会为您调用(例如,由 TemplateViewDetailView 实现的 get() 方法调用); 同样,您不太可能需要覆盖它,尽管如果您希望您的响应返回未通过 Django 模板呈现的内容,那么您将需要这样做。 有关此示例,请参阅 JSONResponseMixin 示例

render_to_response() 本身调用 get_template_names(),默认会在基于类的视图上查找 template_name; 另外两个 mixin(SingleObjectTemplateResponseMixinMultipleObjectTemplateResponseMixin)覆盖它以在处理实际对象时提供更灵活的默认值。

ContextMixin

每个需要上下文数据的内置视图,例如用于渲染模板(包括上面的 TemplateResponseMixin),都应该调用 get_context_data() 传递他们想要确保的任何数据作为关键字参数. get_context_data() 返回一个字典; 在 ContextMixin 中,它返回其关键字参数,但通常会覆盖它以向字典添加更多成员。 您还可以使用 extra_context 属性。


构建 Django 的基于类的通用视图

让我们看看 Django 的两个基于类的通用视图是如何用提供离散功能的 mixin 构建的。 我们将考虑 DetailView,它呈现对象的“详细”视图,以及 ListView,它会呈现对象列表,通常来自查询集,并可选择对它们进行分页。 这将向我们介绍四个 mixin,它们在处理单个 Django 对象或多个对象时提供有用的功能。

通用编辑视图(FormView 和模型特定视图 CreateViewUpdateViewDeleteView)中也涉及 mixin,并在基于日期的通用视图中。 这些在 mixin 参考文档 中有介绍。

DetailView:使用单个 Django 对象

为了显示一个对象的细节,我们基本上需要做两件事:我们需要查找该对象,然后我们需要使用合适的模板制作一个 TemplateResponse,并将该对象作为上下文。

为了获取对象,DetailView 依赖于 SingleObjectMixin,它提供了一个 get_object() 方法,它根据请求的 URL 找出对象(看起来对于 URLConf 中声明的 pkslug 关键字参数,并从视图上的 model 属性或 queryset 中查找对象] 属性(如果提供)。 SingleObjectMixin 还覆盖了 get_context_data(),它在所有 Django 内置的基于类的视图中使用,为模板渲染提供上下文数据。

然后制作 TemplateResponseDetailView 使用 SingleObjectTemplateResponseMixin,它扩展了 TemplateResponseMixin,覆盖 get_template_names(X)以上。 它实际上提供了一组相当复杂的选项,但大多数人将使用的主要选项是 <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 的一部分来找出要使用的查询集,因此默认使用 querysetmodel 属性在视图类上。 在这里覆盖 get_queryset() 的一个常见原因是动态改变对象,例如根据当前用户或将来为博客排除帖子。

MultipleObjectMixin 还覆盖了 get_context_data() 以包含适当的分页上下文变量(如果分页被禁用,则提供假人)。 它依赖于 object_list 作为关键字参数传入,ListView 为它安排。

要制作 TemplateResponseListView 然后使用 MultipleObjectTemplateResponseMixin; 与上面的 SingleObjectTemplateResponseMixin 一样,这会覆盖 get_template_names() 以提供 一系列选项 ,最常用的是 <app_label>/<model_name>_list.html_list 部分再次取自 template_name_suffix 属性。 (基于日期的通用视图使用诸如 _archive_archive_year 等后缀来为各种专门的基于日期的列表视图使用不同的模板。)


使用 Django 的基于类的视图混合

现在我们已经看到了 Django 的基于类的通用视图如何使用提供的 mixin,让我们看看我们可以组合它们的其他方式。 我们仍然会将它们与内置的基于类的视图或其他通用的基于类的视图结合起来,但是您可以解决一系列比 Django 开箱即用的问题更罕见的问题。

警告

并非所有的 mixin 都可以一起使用,也不是所有基于类的通用视图都可以与所有其他 mixin 一起使用。 在这里,我们展示了一些有效的例子; 如果您想将其他功能组合在一起,那么您必须考虑在您使用的不同类之间重叠的属性和方法之间的交互,以及 方法解析顺序 将如何影响方法的哪些版本以什么顺序调用。

Django的基于类的视图基于类的视图mixins的参考文档将帮助您了解哪些属性和方法可能会导致不同类和mixin之间发生冲突。

如果有疑问,通常最好退避并基于 ViewTemplateView,也许使用 SingleObjectMixinMultipleObjectMixin。 尽管您最终可能会编写更多代码,但它更有可能被其他人清楚地理解,并且需要担心的交互更少,您将节省一些思考。 (当然,您总是可以深入了解 Django 的基于类的通用视图的实现,以获取有关如何解决问题的灵感。)


在视图中使用 SingleObjectMixin

如果我们想编写一个只响应 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}))

在实践中,您可能希望在键值存储中而不是在关系数据库中记录兴趣,因此我们忽略了这一点。 唯一需要担心使用 SingleObjectMixin 的视图是我们想要查找我们感兴趣的作者的地方,它通过调用 self.get_object() 来完成。 其他一切都由 mixin 为我们处理。

我们可以很简单的将它挂接在我们的 URLs 中:

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 提供内置分页,但您可能希望对所有(通过外键)链接到另一个对象的对象列表进行分页。 在我们的出版示例中,您可能希望对特定出版商的所有书籍进行分页。

一种方法是将 ListViewSingleObjectMixin 结合起来,这样分页书籍列表的查询集就可以与作为单个对象找到的出版商挂钩。 为了做到这一点,我们需要有两个不同的查询集:

Book 查询集供 ListView 使用
由于我们可以访问要列出其书籍的 Publisher,我们覆盖 get_queryset() 并使用 Publisher反向外键管理器
Publisher 查询集用于 get_object()
我们将依赖 get_object() 的默认实现来获取正确的 Publisher 对象。 但是,我们需要明确地传递一个 queryset 参数,否则 get_object() 的默认实现将调用 get_queryset(),我们已经覆盖它以返回 Book 对象而不是Publisher 的。

笔记

我们必须仔细考虑get_context_data()。 由于 SingleObjectMixinListView 都将把东西放在 context_object_name 的值下的上下文数据中,如果它被设置,我们将改为明确地确保 Publisher ] 在上下文数据中。 ListView 会为我们添加合适的 page_objpaginator,前提是我们记得调用 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",因为它是书籍列表; ListViewSingleObjectMixin 一无所知,因此它不知道该视图与 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 %}

避免过度复杂的事情

通常,当您需要它们的功能时,您可以使用 TemplateResponseMixinSingleObjectMixin。 如上所示,稍加注意,您甚至可以将 SingleObjectMixinListView 结合起来。 然而,当您尝试这样做时,事情会变得越来越复杂,一个好的经验法则是:

暗示

您的每个视图都应仅使用来自以下一组通用基于类的视图中的 mixin 或视图:detail、listediting 和 date。 例如,可以将 TemplateView(内置视图)与 MultipleObjectMixin(通用列表)结合使用,但在结合使用 SingleObjectMixin(通用详细信息)时可能会遇到问题与 MultipleObjectMixin(通用列表)。


为了展示当您尝试变得更复杂时会发生什么,我们展示了一个示例,该示例在存在更简单的解决方案时牺牲了可读性和可维护性。 首先,让我们看看将 DetailViewFormMixin 结合起来的天真尝试,使我们能够 POST 一个 Django Form 到与我们相同的 URL '正在使用 DetailView 显示一个对象。

使用 FormMixin 和 DetailView

回想一下我们之前一起使用 ViewSingleObjectMixin 的例子。 我们正在记录用户对特定作者的兴趣; 现在说我们想让他们留言说明他们为什么喜欢他们。 同样,假设我们不打算将其存储在关系数据库中,而是将其存储在更深奥的东西中,在这里我们不会担心。

此时很自然的会用到一个 Form 来封装从用户浏览器发送到 Django 的信息。 还要说,我们在 REST 上投入了大量资金,所以我们希望使用相同的 URL 来显示作者和捕获来自用户的消息。 让我们重写我们的 AuthorDetailView 来做到这一点。

我们将保留 DetailView 中的 GET 处理,尽管我们必须将 Form 添加到上下文数据中,以便我们可以在模板中呈现它。 我们还想从 FormMixin 引入表单处理,并编写一些代码,以便在 POST 上正确调用表单。

笔记

我们使用 FormMixin 并自己实现 post(),而不是尝试将 DetailViewFormView(已经提供了合适的 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()


更好的解决方案

FormMixinDetailView 之间微妙的交互数量已经在测试我们管理事物的能力。 您不太可能想自己编写这种类。

在这种情况下,您可以自己编写 post() 方法,将 DetailView 作为唯一的通用功能,尽管编写 Form 处理代码涉及大量重复。

或者,与上述方法相比,使用单独的视图来处理表单仍然会少一些工作,它可以使用 FormViewDetailView 不同而无需担心。


另一种更好的解决方案

我们在这里真正想做的是使用来自同一 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

那么 AuthorInterestForm 是一个 FormView,但是我们必须引入 SingleObjectMixin 以便我们可以找到我们正在谈论的作者,我们必须记住设置template_name 确保表单错误将呈现与 AuthorDetailViewGET 上使用相同的模板:

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() 会给我们一些行为与基于函数的视图完全一样的东西,所以我们可以在两个子视图之间进行选择时这样做。

您可以像在 URLconf 中一样将关键字参数传递给 as_view(),例如,如果您希望 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)

这种方法也可以与任何其他通用的基于类的视图或直接从 ViewTemplateView 继承的基于类的视图一起使用,因为它使不同的视图尽可能分开.


不仅仅是 HTML

当你想要多次做同样的事情时,基于类的视图会发光。 假设您正在编写一个 API,并且每个视图都应该返回 JSON 而不是呈现的 HTML。

我们可以创建一个混入类来在所有视图里使用,用它来进行一次转换到 JSON。

例如,JSON mixin 可能如下所示:

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 的更多信息。


这个 mixin 提供了一个 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 与通用视图之一一起使用。 我们可以通过将 JSONResponseMixinBaseDetailView 混合来制作我们自己的 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 子类,该子类能够返回 both HTML 和 JSON 内容,具体取决于 HTTP 请求的某些属性,例如查询参数或 HTTP 标头。 混合 JSONResponseMixinSingleObjectTemplateResponseMixin,并覆盖 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) 的调用最终会调用 TemplateResponseMixinrender_to_response() 实现。