# Django 3.2.11.dev 文档

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

警告

这是一个进阶主题。在探索这些技术之前，建议先了解 Django 基于类的视图 。

Django 内置的基于类的视图提供了很多功能，但你可能想单独使用有些功能。例如，你可能想写一个渲染一个模板来生成 HTTP 响应的视图，但你不能使用 `TemplateView` ；也许你只需要在 `POST` 时渲染一个模板，用 `GET` 来处理其他所有事。虽然你可以直接使用 `TemplateResponse`，但这很可能会导致重复代码。

因此 Django 也提供了很多混入，它们提供了更多的离散功能。比如模板渲染，被封装在 `TemplateResponseMixin` 中。Django 参考文档中包含 所有混入的完整文档。

## 上下文和模板响应

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

`TemplateResponseMixin`

  每个返回 `TemplateResponse` 的内置视图都将调用 `TemplateResponseMixin` 提供的 `render_to_response()` 方法。大多数时候，这个方法会被你调用（例如，它被 `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` 或 `model` 属性。在这里覆盖 `get_queryset()` 的常见原因是为了动态变化的对象，比如根据当前用户的情况，或者为了排除博客未来的文章。

`MultipleObjectMixin` 还覆盖了 `get_context_data()`，为分页加入了适当的上下文变量（如果分页被禁用，则提供虚假分页）。它依赖于 `ListView` 作为关键字参数传入的 `object_list`。

要生成一个 `TemplateResponse` ，`ListView` 则使用 `MultipleObjectTemplateResponseMixin` ；和上面的 `SingleObjectTemplateResponseMixin` 一样，它覆盖 `get_template_names()` 来提供一系列选项，最常用的 `<app_label>/<model_name>_list.html` ，`_list` 部分同样从 `template_name_suffix` 属性中获取。（基于日期的通用视图使用诸如 `_archive` 、`_archive_year` 等后缀来为各种专门的基于日期的列表视图使用不同的模板。）

## 使用 Django 的基于类的视图混入

现在我们已经知道 Django 的基于类的通用视图如何使用所提供的混入，让我们看看使用它们的其他方式。我们仍然会将它们与内置的基于类的视图，或者其他通用的基于类的视图结合起来，但是，有一系列比 Django 开箱即用所提供的更罕见的问题可以被解决。

警告

不是所有的混入都可以一起使用，并且不是所有的基于类的通用视图能和所有其他的混入一起使用。这里我们介绍一些有用的例子；如果你想把其他功能汇集在一起，那么你就必须考虑你正在使用的不同类之间重叠的属性和方法之间的相互作用，以及 method resolution order 将如何影响哪些版本的方法将以何种顺序被调用。

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

如果有问题，最好还是退而求其次，以 `View` 或 `TemplateView` 为基础，或许可以用 `SingleObjectMixin` 和 `MultipleObjectMixin` 。虽然你最终可能会写出更多的代码，但对于以后再来的人来说，更有可能清楚地理解，并且由于需要担心的交互较少，你可以省去一些思考。（当然，你可以随时查阅 Django 的基于类的通用视图的实现，以获得如何处理问题的灵感）。

### 在视图中使用 `SingleObjectMixin`

如果我们想编写一个只响应 `POST` 的基于类的视图，我们将子类化 `View` 并且在子类中编写一个 `post()` 方法。但是如果想让我们的程序在一个从 URL 中识别出来特定的对象上工作，我们就需要 `SingleObjectMixin` 提供的功能。

我们将用在 :doc: 基于类的通用视图介绍 <generic-display> 中使用的 `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()` 来实现。其他的一切都由混入替我们处理。

我们可以很简单的将它挂接在我们的 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` 的任何其他功能。

### 在 `ListView` 中使用 `SingleObjectMixin`

`ListView` 提供了内置的分页功能，但你可能想将一个对象列表分页，而这些对象都是通过一个外键链接到另一个对象的。在我们的出版示例中，你可能想对某一出版商的所有书籍进行分页。

一种方法是将 `ListView` 和 `SingleObjectMixin` 结合起来，这样一来，用于图书分页列表的查询集就可以脱离作为单个对象找到的出版商对象。 为此，我们需要两个不同的查询集：

`ListView` 使用的 `Book` 查询集
  由于我们已经得到了我们所想要书籍列表的 `Publisher` ，我们只需覆盖 `get_queryset()` 并使用的 `Publisher` 的 反向外键管理器。
`get_object()` 使用的 `Publisher` 查询集
  我们将依赖 `get_object()` 的默认实现来获取正确的 `Publisher` 对象。然而，我们需要显式地传递一个 `queryset` 参数，因为 `get_object()` 的默认实现会调用 `get_queryset()` ，我们已经覆盖了它并返回了 `Book` 对象而不是 `Publisher` 对象。

注解

我们必须认真考虑 `get_context_data()`。由于 `SingleObjectMixin` 和 `ListView` 会将上下文数据放在 `context_object_name` 的值下（如果它已设置），我们要明确确保 `Publisher` 在上下文数据中。`ListView` 将为我们添加合适的 `page_obj` 和 `paginator`，只要我们记得调用 `super()`。

Now we can write a new `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` 结合起来。然而当你尝试这样做时，事情会变得越来越复杂，一个好的经验法则是：

提示

你的每个视图应该只使用混入或者来自一个通用基于类的视图的组里视图： 详情，列表，编辑 和日期。例如，将 `TemplateView` （内置视图）和 `MultipleObjectMixin` （通用列表）结合起来，但你可能会在 `SingleObjectMixin` （通用详情）和 `MultipleObjectMixin` （通用列表）结合时遇到问题。

为了给你展示当变得更复杂时发生了什么，我们展示了一个当有更简单的解决方案时，牺牲了可读写和可维护性的例子。首先，让我们看看一个天真的尝试，将 `DetailView` 和 `FormMixin` 结合起来，使我们能够在 `POST` 一个 Django `Form` 和显示一个 `DetailView` 时使用同一个 URL。

### `DetailView` 和 `FormMixin` 一起使用

回想一下我们之前使用 `View` 和 `SingleObjectMixin` 一起使用的例子。我们当时记录的是一个用户对某个作者的兴趣；比如说现在我们想让他们留言说为什么喜欢他们。同样，我们假设我们不打算把这个存储在关系型数据库中，而是存储在更深奥的东西中，我们在这里就不关心了。

这时自然而然就会用到一个 `Form` 来封装从用户浏览器发送到 Django 的信息。又比如说我们在 REST 上投入了大量的精力，所以我们希望用同样的 URL 来显示作者和捕捉用户的信息。让我们重写我们的 `AuthorDetailView` 来实现这个目标。

我们将保留 `DetailView` 中的 `GET` 处理，尽管我们必须在上下文数据中添加一个 `Form`，这样我们就可以在模板中渲染它。我们还要从 `FormMixin` 中调入表单处理，并写一点代码，这样在 ``POST` `时，表单会被适当地调用。

注解

我们使用 `FormMixin` 并自己实现了 `post()` ，而不是试着把 `DetailView` 和 `FormView` （也都提供合适的 `post()`）混着用，因为这两个视图都实现了 `get()` ，这样会让事情变得更复杂。

Our new `AuthorDetailView` looks like this:

    # 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` 的处理代码会涉及到很多重复的地方。

或者，使用单独的视图来处理表单仍然比上述方法工作量小，它可以使用 `FormView` ，而不必担心任何问题。

### 另一种更好的解决方案

我们在这里真正想做的是在同一个 URL 中使用两个不同的基于类的视图。那么为什么不这样做呢？我们在这里有一个非常明确的划分。`GET` 请求应该得到 `DetailView` （在上下文数据中添加了 `Form` ），而 `POST` 请求应该得到 `FormView`。我们先来设置一下这些视图。

The `AuthorDetailView` view is almost the same as when we first introduced AuthorDetailView; we have to write our own `get_context_data()` to make the `AuthorInterestForm` available to the template. We'll skip the `get_object()` override from before for clarity:

    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

Then the `AuthorInterestForm` is a `FormView`, but we have to bring in `SingleObjectMixin` so we can find the author we're talking about, and we have to remember to set `template_name` to ensure that form errors will render the same template as `AuthorDetailView` is using on `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})

Finally we bring this together in a new `AuthorView` view. We already know that calling `as_view()` on a class-based view gives us something that behaves exactly like a function based view, so we can do that at the point we choose between the two subviews.

You can pass through keyword arguments to `as_view()` in the same way you would in your URLconf, such as if you wanted the `AuthorInterestFormView` behavior to also appear at another URL but using a different template:

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

Equally we could use our mixin with one of the generic views. We can make our own version of `DetailView` by mixing `JSONResponseMixin` with the `BaseDetailView` -- (the `DetailView` before template rendering behavior has been mixed in):

    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)` 的调用最终会调用 `TemplateResponseMixin` 的 `render_to_response()` 实现。