使用 Mixin 与基于类的视图

注意

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

Django 内置的基于类的视图提供了许多功能,但您可能希望单独使用其中一些功能。例如,您可能希望编写一个视图来渲染模板以生成 HTTP 响应,但您不能使用TemplateView;也许您只需要在POST请求时渲染模板,而GET请求则执行其他操作。虽然您可以直接使用TemplateResponse,但这可能会导致代码重复。

因此,Django 还提供了一些 Mixin,它们提供了更独立的功能。例如,模板渲染封装在TemplateResponseMixin 中。Django 参考文档包含所有 Mixin 的完整文档

上下文和模板响应

提供了两个核心 Mixin,它们有助于在基于类的视图中提供与模板交互的一致接口。

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()(如上所述)。它实际上提供了一套相当复杂的选项,但大多数人将要使用的主要选项是 <app_label>/<model_name>_detail.html。可以通过在子类上设置 template_name_suffix 为其他值来更改 _detail 部分。(例如,泛型编辑视图 对创建和更新视图使用 _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 的泛型基于类的视图如何使用提供的混合,让我们看看我们可以结合它们的哪些其他方法。我们仍然会将它们与内置的基于类的视图或其他泛型基于类的视图结合使用,但是您可以解决比 Django 提供的默认解决方案更多的罕见问题。

警告

并非所有混合都可以一起使用,并非所有泛型基于类的视图都可以与所有其他混合一起使用。这里我们提供了一些有效的示例;如果您想整合其他功能,则需要考虑不同类之间重叠的属性和方法之间的交互,以及方法解析顺序 将如何影响方法的调用顺序。

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

如有疑问,最好退一步,将您的工作建立在 ViewTemplateView 上,也许可以结合 SingleObjectMixinMultipleObjectMixin。虽然您可能最终会编写更多代码,但它更有可能被后来接触到它的其他人清楚地理解,并且由于需要考虑的交互更少,因此您将节省一些思考时间。(当然,您始终可以参考 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})
        )

在实践中,您可能希望将兴趣记录在键值存储中而不是关系数据库中,因此我们省略了这部分。视图中唯一需要担心使用 SingleObjectMixin 的部分是,我们希望查找我们感兴趣的作者,它通过调用 self.get_object() 来实现。混合为我们处理了其他所有事情。

我们可以轻松地将其连接到我们的 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 的任何其他功能。

SingleObjectMixinListView 结合使用

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 到与我们使用 DetailView 显示对象的相同 URL 的幼稚尝试。

FormMixinDetailView 结合使用

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

此时,很自然地会使用 Form 来封装从用户浏览器发送到 Django 的信息。假设我们也大量使用 REST,因此我们希望对显示作者和捕获用户消息使用相同的 URL。让我们重写我们的 AuthorDetailView 来做到这一点。

我们将保留来自 DetailViewGET 处理,尽管我们必须将 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

然后 AuthorInterestFormView 是一个 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 混合类可能如下所示

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

同样,我们可以将我们的混合类与一个通用视图一起使用。我们可以通过将 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 子类,该子类能够根据 HTTP 请求的一些属性(例如查询参数或 HTTP 标头)返回 HTML 和 JSON 内容。混合 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) 的调用最终会调用 render_to_response()TemplateResponseMixin 实现。

返回顶部