Skip to main content

使用基于类的视图的mixins

警告

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

Django的内置基于类的视图提供了很多功能,但有些它可能需要单独使用。例如,您可能想编写一个视图来渲染模板以生成HTTP响应,但不能使用 TemplateView;也许你需要渲染一个模板只在 POST,与 GET 做一些完全不同。虽然你可以直接使用 TemplateResponse,这可能会导致重复的代码。

出于这个原因,Django还提供了许多混合,提供更多的离散功能。例如,模板呈现封装在 TemplateResponseMixin 中。 Django参考文档包含 full documentation of all the mixins

上下文和模板响应

提供了两个中心mixins,它们有助于提供一致的接口来处理基于类的视图中的模板。

TemplateResponseMixin

每个返回 TemplateResponse 的内置视图将调用 TemplateResponseMixin 提供的 render_to_response() 方法。大多数时候,这将为你调用(例如,它由 TemplateViewDetailView 实现的 get() 方法调用);同样,它不太可能需要重写它,虽然如果你想让你的响应返回的东西没有通过Django模板渲染,那么你会想要这样做。有关示例,请参阅 JSONResponseMixin示例

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

ContextMixin

每个需要上下文数据的内置视图(例如用于渲染模板(包括上面的 TemplateResponseMixin))应该调用 get_context_data() 传递他们想要的任何数据,以确保作为关键字参数存在。 get_context_data() 返回一个字典;在 ContextMixin 它简单地返回它的关键字参数,但通常覆盖这个添加更多的成员到字典。

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

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

在通用编辑视图(FormView,以及特定于模型的视图 CreateViewUpdateViewDeleteView)中以及在基于日期的通用视图中还涉及混合。这些包括在 mixin reference documentation

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 部分。 (例如,generic edit views 使用 _form 创建和更新视图,_confirm_delete 用于删除视图)。

ListView:使用许多Django对象

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

为了获得对象,ListView 使用 MultipleObjectMixin,它提供 get_queryset()paginate_queryset()。与 SingleObjectMixin 不同,没有必要关闭URL的某些部分来找出查询集,因此默认情况下只使用视图类上的 querysetmodel 属性。在这里重写 get_queryset() 的常见原因是动态地改变对象,例如取决于当前用户或者排除将来博客的帖子。

MultipleObjectMixin 还覆盖 get_context_data() 以包括用于分页的适当的上下文变量(如果禁用分页,则提供假体)。它依赖于作为关键字参数传递的 object_listListView 对其进行排列。

为了制作 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开箱即用。

警告

并非所有mixins都可以一起使用,并且并非所有基于泛型类的视图都可以与所有其他mixins一起使用。在这里我们提出几个例子,做工作;如果你想集合其他功能,那么你必须考虑在你使用的不同类之间重叠的属性和方法之间的交互,以及 method resolution order 将如何影响将以什么顺序调用哪些版本的方法。

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

如果有疑问,通常最好将您的工作放在 ViewTemplateView 上,也许使用 SingleObjectMixinMultipleObjectMixin。虽然你可能最终会写更多的代码,它更可能是清楚可以理解的人,以后,以减少互动,担心你会救你自己一些思考。 (当然,你可以随时调用Django的基于类的泛型视图的实现来获得如何解决问题的灵感。)

使用 SingleObjectMixin 与视图

如果我们想写一个简单的基于类的视图,只响应 POST,我们将子类化 View 并在子类中写一个 post() 方法。然而,如果我们希望我们的处理工作在一个特定的对象,从URL中识别,我们将需要 SingleObjectMixin 提供的功能。

我们将使用我们在 generic class-based views introduction 中使用的 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 RecordInterest(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.py
from django.conf.urls import url
from books.views import RecordInterest

urlpatterns = [
    #...
    url(r'^author/(?P<pk>[0-9]+)/interest/$', RecordInterest.as_view(), name='author-interest'),
]

注意 pk 命名组,get_object() 用它来查找 Author 实例。您也可以使用lug,或 SingleObjectMixin 的任何其他功能。

使用 SingleObjectMixinListView

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

一种方法是将 ListViewSingleObjectMixin 组合,以便分页列表的查询集可以挂起作为单个对象的发布者。为了做到这一点,我们需要有两个不同的查询集:

Book 查询集,供 ListView 使用

由于我们可以访问 Publisher 的书,我们想列出,我们只是重写 get_queryset() 和使用 Publisherreverse foreign key manager

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

现在我们可以写一个新的 PublisherDetail:

from django.views.generic import ListView
from django.views.generic.detail import SingleObjectMixin
from books.models import Publisher

class PublisherDetail(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(PublisherDetail, self).get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        context = super(PublisherDetail, self).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。然而,事情变得越来越复杂,因为你试图这样做,一个好的经验法则是:

提示

您的每个视图应仅使用来自基于类的基本视图组之一的混合视图或视图:detail, listediting 和date。例如,将 TemplateView (内置视图)与 MultipleObjectMixin (通用列表)组合很好,但是您可能会遇到将 SingleObjectMixin (通用详细信息)与 MultipleObjectMixin (通用列表)组合的问题。

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

使用 FormMixinDetailView

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

在这一点上,自然可以达到一个 Form 封装从用户的浏览器发送到Django的信息。也请注意,我们大量投资于 REST,因此我们希望使用相同的网址显示作者,以便捕获用户的邮件。让我们重写我们的 AuthorDetailView 来做。

我们将保持来自 DetailViewGET 处理,尽管我们必须将 Form 添加到上下文数据中,以便我们可以在模板中呈现它。我们还要从 FormMixin 中提取表单处理,并写一点代码,以便在 POST 上适当调用表单。

注解

我们使用 FormMixin 和实现 post() 自己,而不是试图混合 DetailViewFormView (已经提供了一个合适的 post()),因为这两个视图实现 get(),事情会变得更加混乱。

我们的新 AuthorDetail 看起来像这样:

# 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 AuthorDetail(FormMixin, DetailView):
    model = Author
    form_class = AuthorInterestForm

    def get_success_url(self):
        return reverse('author-detail', kwargs={'pk': self.object.pk})

    def get_context_data(self, **kwargs):
        context = super(AuthorDetail, self).get_context_data(**kwargs)
        context['form'] = self.get_form()
        return context

    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(AuthorDetail, self).form_valid(form)

get_success_url() 只是提供在某处重定向,这在 form_valid() 的默认实现中使用。我们必须提供我们自己的 post(),如前所述,并且重写 get_context_data() 以使 Form 在上下文数据中可用。

更好的解决方案

很明显,FormMixinDetailView 之间的微妙交互的数量已经在测试我们管理事物的能力。你不太可能想要自己写这样的类。

在这种情况下,自己编写 post() 方法将是相当容易的,保持 DetailView 作为唯一的通用功能,虽然编写 Form 处理代码涉及大量的重复。

或者,仍然比上述方法更容易具有用于处理表单的单独视图,其可以使用与 DetailView 不同的 FormView 而不用担心。

另一个更好的解决方案

我们真正想在这里做的是使用来自同一个URL的两个不同的基于类的视图。那么为什么不这样做呢?我们有一个非常清晰的划分:GET 请求应该获得 DetailView (将 Form 添加到上下文数据中),POST 请求应该获取 FormView。让我们先设置这些视图。

AuthorDisplay 视图几乎与 when we first introduced AuthorDetail 相同;我们必须编写我们自己的 get_context_data() 来使 AuthorInterestForm 可用于模板。为了清楚起见,我们将跳过之前的 get_object() 覆盖:

from django.views.generic import DetailView
from django import forms
from books.models import Author

class AuthorInterestForm(forms.Form):
    message = forms.CharField()

class AuthorDisplay(DetailView):
    model = Author

    def get_context_data(self, **kwargs):
        context = super(AuthorDisplay, self).get_context_data(**kwargs)
        context['form'] = AuthorInterestForm()
        return context

然后 AuthorInterest 是一个简单的 FormView,但我们必须引入 SingleObjectMixin,所以我们可以找到我们正在谈论的作者,我们必须记住设置 template_name 以确保表单错误将渲染与 AuthorDisplay 使用的相同的模板 GET:

from django.urls import reverse
from django.http import HttpResponseForbidden
from django.views.generic import FormView
from django.views.generic.detail import SingleObjectMixin

class AuthorInterest(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(AuthorInterest, self).post(request, *args, **kwargs)

    def get_success_url(self):
        return reverse('author-detail', kwargs={'pk': self.object.pk})

最后,我们将这个在一个新的 AuthorDetail 视图中。我们已经知道,在基于类的视图上调用 as_view() 可以让我们看起来像一个基于函数的视图,所以我们可以在两个子视图之间选择。

您当然可以将关键字参数传递给 as_view(),方式与您在URLconf中的相同,例如,如果您希望 AuthorInterest 行为也出现在另一个URL,但使用不同的模板:

from django.views import View

class AuthorDetail(View):

    def get(self, request, *args, **kwargs):
        view = AuthorDisplay.as_view()
        return view(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        view = AuthorInterest.as_view()
        return view(request, *args, **kwargs)

此方法也可以与任何其他基于类的视图或您自己的基于类的视图直接从 ViewTemplateView 继承,因为它保持不同的视图尽可能独立。

不仅仅是HTML

基于类的视图闪耀是当你想做同样的事情很多次。假设你在写一个API,每个视图都应该返回JSON而不是渲染的HTML。

我们可以创建一个mixin类来在我们的所有视图中使用,处理转换为JSON一次。

例如,一个简单的JSON mixin可能看起来像这样:

from django.http import JsonResponse

class JSONResponseMixin(object):
    """
    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模型和查询集转换为JSON的更多信息,请参阅 序列化Django对象 文档。

此混合提供了与 render_to_response() 具有相同签名的 render_to_json_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与一个通用的意见。我们可以通过混合 JSONResponseMixindjango.views.generic.detail.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 相同的方式部署该视图,具有完全相同的行为 - 除了响应的格式。

如果你想要真正冒险,你甚至可以混合一个 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(HybridDetailView, self).render_to_response(context)

由于Python解析方法重载的方式,对 super(HybridDetailView, self).render_to_response(context) 的调用最终调用 TemplateResponseMixinrender_to_response() 实现。