与基于类的视图一起使用 mixinsLink to this heading

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

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

上下文和模板响应Link to this heading

提供了两个重要的 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 基于类的通用视图Link to this heading

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

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

DetailView :使用单个 Django 对象Link to this heading

为了显示对象详情,我们基本上需要做两件事:我们需要查询对象,然后我们需要使用合适的模板创建 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 对象Link to this heading

对象列表大致遵循相同的模式:我们需要一个对象列表(可能会分页),通常是 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 的基于类的视图 mixinsLink to this heading

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

视图和 SingleObjectMixin 一起使用Link to this heading

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

我们将使用 Author 模型来演示。

views.py
Python
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
Python
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 一起使用Link to this heading

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 对象。

现在我们编写一个新的 PublisherDetail

Code
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 有意在这个例子中保持简单,因为我们不需要建立很多书籍来看分页工作!这是你需要的模板:

Django template
{% 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 %}

避免过度复杂的事情Link to this heading

当你使用它们的功能时,通常你可以使用 TemplateResponseMixinSingleObjectMixin 。如上所示,你甚至可以将 SingleObjectMixinListView 结合起来。然而当你试着这么做时,事情将变得复杂,一个好的经验法则是:

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

DetailViewFormMixin 一起使用Link to this heading

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

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

我们将保持 GET 来处理 DetailView,虽然我们不得不在上下文数据里添加 Form ,但这样我们就可以在模板里渲染它。我们也想从 FormMixin 中引入表单处理并编写一些代码,这样在 POST 表单的时候可以调用它。

新的 AuthorDetail 看起来是这样的:

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

更好的解决方案Link to this heading

FormMixinDetailView 之间细微的联系已经在测试我们管理事务的能力了。你不太可能想写这样的类。

在这个例子里,你可以编写 post()DetailView 作为唯一的通用功能,尽管编写 Form 的处理代码会包含大量重复。

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

另一种更好的解决方案Link to this heading

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

AuthorDisplay 视图几乎与 当我们第一次介绍AuthorDetail时 相同。我们必须编写自己的 get_context_data() 来使 AuthorInterestForm 可用于模板。为清楚所见,我们将跳过重写 get_object()

Code
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 渲染相同的模板。

Code
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 里,但使用不同的模板:

Code
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:View 或 TemplateView 使用,因为它使不同视图尽可能分离。

不仅仅是HTMLLink to this heading

基于类的视图的优势是你可以多次执行相同操作。假设你正在编写API,那么每个视图应该返回 JSON,而不是渲染HTML。

我们可以创建一个 mixin 类来在所有视图里使用,它用来进行一次转换JSON。

比如,一个 JSON mixin 可以是这样:

Code
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

mixin 提供了 render_to_json_response() 方法,其签名与 render_to_response() 相同。为了使用它,我们需要把它混在 TemplateView 里,并且重写 render_to_response() 来调用 render_to_json_response()

Code
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 在模板渲染行为前被混合):

Code
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() 的实现,来根据用户请求的响应类型来返回适当的渲染方法。

Code
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() 实现。