将mixin与基于类的视图一起使用

小心

这是一个高级主题。工作知识 Django's class-based views 建议在探索这些技术之前。

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

因此,Django还提供了许多混合函数,它们提供了更离散的功能。例如,模板呈现封装在 TemplateResponseMixin . Django参考文件包含 full documentation of all the mixins .

上下文和模板响应

提供了两个中央混音器,有助于为基于类的视图中的模板提供一致的接口。

TemplateResponseMixin

每个内置视图返回 TemplateResponse 将调用 render_to_response() 方法 TemplateResponseMixin 提供。大多数情况下,这将为您调用(例如,它由 get() 方法由两者实现 TemplateViewDetailView );同样,您不太可能需要重写它,但是如果您希望您的响应返回一些不是通过django模板呈现的内容,那么您将希望这样做。有关此示例,请参见 JSONResponseMixin example .

render_to_response() 本身调用 get_template_names() ,默认情况下会查找 template_name 基于类的视图;两个其他混合 (SingleObjectTemplateResponseMixinMultipleObjectTemplateResponseMixin )在处理实际对象时,重写此项以提供更灵活的默认值。

ContextMixin

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

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

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

通用编辑视图中也涉及到混合。 (FormView 以及特定于模型的视图 CreateViewUpdateViewDeleteView )和基于日期的常规视图中。这些都包含在 mixin reference documentation .

DetailView :使用单个Django对象

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

为了得到目标, DetailView 依赖于 SingleObjectMixin ,它提供了 get_object() 根据请求的URL(它查找 pkslug 在urlconf中声明的关键字参数,并从 model 视图上的属性,或 queryset 属性(如果已提供)。 SingleObjectMixin 也超越 get_context_data() 它在所有Django的基于类的内置视图中使用,为模板呈现提供上下文数据。

然后做一个 TemplateResponseDetailView 使用 SingleObjectTemplateResponseMixin 延伸 TemplateResponseMixin 压倒一切 get_template_names() 如上所述。它实际上提供了一组相当复杂的选项,但大多数人将使用的主要选项是 <app_label>/<model_name>_detail.html . 这个 _detail 可通过设置更改零件 template_name_suffix 在其他类的子类上。(例如, generic edit views 使用 _form 用于创建和更新视图,以及 _confirm_delete 用于删除视图。)

ListView :使用许多Django对象

对象列表遵循大致相同的模式:我们需要一个(可能是分页的)对象列表,通常是 QuerySet 然后我们需要 TemplateResponse 使用合适的模板,使用该对象列表。

要获取对象, ListView 使用 MultipleObjectMixin ,它同时提供 get_queryset()paginate_queryset() . 不像 SingleObjectMixin ,不需要关闭URL的部分来确定要使用的queryset,因此默认使用 querysetmodel 视图类的属性。重写的常见原因 get_queryset() 这里将动态地改变对象,例如根据当前用户或将来排除博客中的文章。

MultipleObjectMixin 也超越 get_context_data() 为分页包含适当的上下文变量(如果禁用分页,则提供假人)。它依赖于 object_list 作为关键字参数传入,其中 ListView 安排一下。

做一个 TemplateResponseListView 然后使用 MultipleObjectTemplateResponseMixin 一样 SingleObjectTemplateResponseMixin 上面,这将覆盖 get_template_names() 提供 a range of options 最常用的是 <app_label>/<model_name>_list.html_list 再次从 template_name_suffix 属性。(基于日期的通用视图使用后缀,例如 _archive_archive_year 等等,为各种专门的基于日期的列表视图使用不同的模板。)

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

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

警告

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

Django的参考文件 class-based viewsclass-based view mixins 将帮助您理解哪些属性和方法可能会导致不同类和混合之间的冲突。

如果有疑问,最好退后一步,把工作放在基础上。 ViewTemplateView 也许与 SingleObjectMixinMultipleObjectMixin . 虽然你最终可能会写更多的代码,但对于以后来写代码的人来说,这更容易被理解,而且你担心的交互更少,这样你就可以省去一些思考。(当然,您可以随时了解Django基于类的通用视图的实现,以获得解决问题的灵感。)

使用 SingleObjectMixin 有观点

如果我们想编写一个只响应 POST ,我们将子类 View 写一篇 post() 子类中的方法。但是,如果我们希望我们的处理在一个特定的对象上工作,从URL识别,我们将希望 SingleObjectMixin .

我们将用 Author 我们使用的模型 generic class-based views introduction .

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 .

使用 SingleObjectMixin 具有 ListView

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

一种方法是结合 ListView 具有 SingleObjectMixin 这样,分页书籍列表的查询集可以挂起作为单个对象找到的发布者。为此,我们需要有两个不同的查询集:

Book queryset for use by ListView

因为我们可以访问 Publisher 我们想把谁的书列出来,我们就重写 get_queryset() 并使用 Publisherreverse foreign key manager .

Publisher queryset for use in 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()

注意我们的设置 self.object 在内部 get() 所以我们可以稍后再使用它 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 %}

避免任何更复杂的事情

一般你可以用 TemplateResponseMixinSingleObjectMixin 当你需要他们的功能时。如上图所示,您甚至可以小心地组合 SingleObjectMixin 具有 ListView . 然而,当你试图这样做时,事情变得越来越复杂,一个好的经验法则是:

提示

每个视图都应该只使用基于类的通用视图组中的一个组中的混合或视图: detail, listediting 和日期。例如,可以合并 TemplateView (内置视图)具有 MultipleObjectMixin (通用列表),但您可能在组合时遇到问题 SingleObjectMixin (一般细节) MultipleObjectMixin (通用列表)。

为了展示当您试图变得更复杂时会发生什么,我们展示了一个示例,当有一个更简单的解决方案时,它会牺牲可读性和可维护性。首先,让我们来看一个天真的尝试 DetailView 具有 FormMixin 使我们能够 POST 阿让戈 Form 与显示对象时使用的URL相同 DetailView .

使用 FormMixin 具有 DetailView

回想一下我们之前使用的示例 ViewSingleObjectMixin 一起。我们记录了用户对某个特定作者的兴趣;现在说我们想让他们留下一条信息,告诉他们为什么喜欢他们。再一次,假设我们不打算将它存储在关系数据库中,而是将它存储在一些更深奥的地方,我们在这里不必担心。

在这一点上,很自然地 Form 封装从用户浏览器发送到Django的信息。还要说,我们在 REST 因此,我们希望使用相同的URL来显示作者,就像从用户那里捕获消息一样。我们重写一下 AuthorDetailView 这样做。

我们会保持 GET 处理从 DetailView ,尽管我们必须添加 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() 如前所述。

更好的解决方案

FormMixinDetailView 已经在测试我们的管理能力。你不太可能想自己写这样的课。

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

或者,与上述方法相比,使用单独的视图来处理表单的工作量仍然要少 FormView 不同于 DetailView 不用担心。

另一个更好的解决方案

我们真正要做的是使用来自同一个URL的两个不同的基于类的视图。为什么不这么做呢?我们有一个非常明确的划分: GET 请求应该得到 DetailView (与 Form 添加到上下文数据中),以及 POST 请求应该得到 FormView . 让我们先设置这些视图。

这个 AuthorDetailView 视图几乎与 when we first introduced 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 正在使用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})

最后,我们将这一切汇聚在一个新的 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)

这种方法也可以用于任何其他基于类的通用视图或直接继承自 ViewTemplateView ,因为它将不同的视图尽可能地分开。

不仅仅是HTML

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

我们可以创建一个mixin类来在所有视图中使用,只需处理一次到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的详细信息,请参阅文档。

这个混音器提供了一个 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与一个通用视图一起使用。我们可以制作我们自己版本的 DetailView 通过混合 JSONResponseMixinBaseDetailView --( 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头。将两者混合在一起 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 .