与基于类的视图一起使用 mixins

警告

这是一个高级主题。浏览这些技术之前,建议先看 Django's class-based views 的知识。

Django 的内建的基于类的视图提供很多函数特性,但有些你可能想单独使用。比如,你可能想编写一个视图用来呈现一个模板来进行 HTTP 响应,但你不能使用 TemplateView ;或许你只需要在 POST 上渲染模板,用 GET 来处理其他所有事情。当你直接使用 TemplateResponse 时,这将容易导致重复代码。

因为这个原因,Django 也提供了很多提供分离特性的mixins。比如,模板渲染被封装在 TemplateResponseMixin 里。Django 参考文档包含所有有关 mixins 的资料( full documentation of all the mixins)。

上下文和模板响应

提供了两个重要的 mixins,有助于提供一致的界面,以便在基于类的视图中使用。

TemplateResponseMixin

每一个返回 TemplateResponse  的内置视图将调用 TemplateResponseMixin 提供的 render_to_response() 方法。大部分时间里会被你调用(比如,它通过 TemplateViewDetailView) 共同实现的 get() 方法调用);类似的,它不太可能需要你去覆盖它,尽管如果你希望响应返回一些没有被 Django 模板渲染的东西,那么你就会想要这么做。

render_to_response() 本身调用 get_template_names() ,它默认只是检查基于类的视图上的 template_name 。当处理真实对象时,两个其他 mixins (SingleObjectTemplateResponseMixinMultipleObjectTemplateResponseMixin) 覆盖它来提供更多灵活默认值。

ContextMixin
每个需要上下文数据的内建视图,比如为了渲染一个模板(在上面包含 TemplateResponseMixin ),应该调用 get_context_data() 传递任何它们想要确定的数据当做关键参数。get_context_data() 返回一个字典;在 ContextMixin 里它只返回它的关键参数,但它通常覆盖此项以添加更多成员到字典中。你也可以使用 extra_context 属性。

构造 Django 基于类的通用视图

让我们看 Django 的两个基于类的通用视图如何由 mixins 构建,提供分离功能的。我们考虑 DetailView ,它渲染一个对象的详情视图,还有 ListView ,它渲染一个对象列表,通常来自查询集,并且可以分页。这里将介绍四个 mixins ,它们在使用单个 Django 对象或多个对象时,提供常用功能。

mixins 也包含在通用编辑视图( FormView 、指定模型视图 CreateViewUpdateViewDeleteView ) 里,基于日期的通用视图。这些包括在 mixin reference documentation

DetailView :使用单个 Django 对象

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

为了获取对象,DetailView 依靠于 SingleObjectMixin ,它提供一个 get_object() 方法,该方法根据请求的 URL 来计算对象(它寻找 URLconf 中的声明的 pkslug 关键参数,并从视图上的 model 属性查找对象,或者如果提供了 queryset  属性,将使用这个属性)。SingleObjectMixin 也覆盖了 get_context_data() ,它被用于所有 Django 内置的基于类的视图,为模板渲染提供上下文数据。

然后创建一个 TemplateResponseDetailView 使用 SingleObjectTemplateResponseMixin ,它用来扩展:class:~django.views.generic.base.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_list 作为关键字参数来传入,ListView 排列它。

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

使用 Django 的基于类的视图 mixins

现在我们已经知道 Django 的基于类的通用视图如何使用提供的mixins ,让我们看看结合它们的其他方式。当然,我们仍然准备将它们和内置的基于类的视图或其他基于类的通用视图结合在一起,但是你可以解决的一系列罕见问题,而不是使用 Django 提供的开箱即用的方法。

警告

不是所有的mixins能被一起使用,并且不是所有的基于类的通用视图能和所有其他mixins一起使用。这里我们介绍一些有用的例子;如果你想集合其他功能,那么你将必须考虑你正在使用的不同类之间的重叠的属性和方法之间的交互,还有方法解析顺序如何影响这些方法的版本将以何种顺序调用。

Django 的有关 class-based views and class-based view mixins 的参考文档将帮助你理解哪一些属性和方法可能导致不同类和mixins之间的冲突。

如果有疑问,通常最好是回退并最好在 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 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 中:

urls.py
from django.urls import path
from books.views import RecordInterest

urlpatterns = [
    #...
    path('author/<int:pk>/interest/', RecordInterest.as_view(), name='author-interest'),
]

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

ListViewSingleObjectMixin 一起使用

ListView 提供内置的分页,但你可能想对所有被链接到其他对象的对象列表进行分页。在这个例子里,你可以想对特定出版者的所有书籍进行分页。

一个办法是将 ListView 结合 SingleObjectMixin 使用,因此书籍分页列表的查询集挂上找到的出版者(作为单个对象)。为了实现它,我们需要两个不同的查询集:

ListView 使用的 Book 查询集
因为我们已经访问了 我们想要书籍列表的 Publisher ,我们只需覆盖 get_queryset() 并使用反向外键管理(reverse foreign key manager)。
get_object() 里使用的 Publisher 查询集
我们将依赖 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().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 结合起来。然而当你试着这么做时,事情将变得复杂,一个好的经验法则是:

提示

你的每个视图应该只使用 mixins 或者来自一个通用基于类的视图的组里视图:detail, list1, editing2 和日期。举例它将 TemplateView (在视图里内建) 和 MultipleObjectMixin (通用列表) 结合起来,但你可能会在 SingleObjectMixin (通用详情) 和 MultipleObjectMixin 结合时遇到问题。

为了给你展示当变得复杂时发生了什么,我们显示了一个例子,当这里有一个更简单的解决方案时,我们牺牲了可读写和可维护性。首先,让我们试着结合 DetailViewFormMixin ,这样当我们使用 DetailView 显示对象时,可以 POST 一个 Form 到同样的 URL 里。

DetailViewFormMixin 一起使用

让我们回到先前关于同时使用 ViewSingleObjectMixin 的例子。我们已经记录了用户对特定作者的喜好;现在我们需要留言说为什么我们喜欢他们。再次说明,我们假设不在关系数据里保存它,而存储在更难理解的东西里,我们现在先不考虑细节。

在这点上,很自然的找到一个 a Form 来封装从浏览器传递到 Django 的信息。也可以说我们在 REST 上投入了很多精力,我们想使用相同的 URL 来显示作者,以便从用户那里捕获信息。让我们重写 AuthorDetailView 来实现吧。

我们将保持 GET 来处理 DetailView,虽然我们不得不在上下文数据里添加 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 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 ,而不必担心任何问题。

另一种更好的解决方案

我们在这里尝试使用来自相同 URL 的两种不同的基于类的视图。我们为什么要这样做?GET 请求应该获取 DetailView (将 Form 添加到上下文数据中),POST 请求应该获取 FormView 。让我们首先设置这些视图吧。

AuthorDisplay 视图几乎与 当我们第一次介绍AuthorDetail时 相同。我们必须编写自己的 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 AuthorDisplay(DetailView):
    model = Author

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

然后 AuthorInterest 是一个 FormView,但我们必须带入到 SingleObjectMixin ,这样我们才可以找到我们正在谈论的作者,并且我们必须记住要设置 template_name 来确保表单错误与 GET 上使用的 AuthorDisplay 渲染相同的模板。

from django.http import HttpResponseForbidden
from django.urls import reverse
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().post(request, *args, **kwargs)

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

最后我们将它们一起放在新的 AuthorDetail 视图中。我们已经知道在一个基于类的视图上调用 as_view() 方法带给我们像基于函数的视图一样的东西,所以我们可以在两个子视图直接进行选择。

你当然可以像在 URLconf 中一样将关键字参数传递到 as_view() ,比如你想 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)

这个方式也可以被任何其他通用基于类的视图或你自己实现的直接继承自:class: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

注解

查看 Serializing Django objects 文档来获取更多有关如何正确转换 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 和其中一个通用视图一起使用。我们可以把 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 子类,它可以根据 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) 的调用最终会调用 TemplateResponseMixin 的 的:meth:~django.views.generic.base.TemplateResponseMixin.render_to_response() 实现。