使用mixins和基于类的视图

警告

这是一个高级的话题。 建议在研究这些技术之前,先了解Django’s class-based views

Django内置的基于类的视图提供了很多功能,但是其中一些可能需要单独使用。 例如,您可能希望编写一个呈现模板来创建HTTP响应的视图,但不能使用TemplateView;也许你只需要在POST上渲染一个模板,用GET完成其他的工作。 虽然您可以直接使用TemplateResponse,但这可能会导致重复的代码。

出于这个原因,Django还提供了一些提供更多离散功能的mixin。 例如,模板渲染被封装在TemplateResponseMixin中。 Django参考文档包含full documentation of all the mixins

上下文和模板响应

提供了两个中央混合模式,可以帮助提供一个一致的界面,以便在基于类的视图中处理模板。

TemplateResponseMixin

Every built in view which returns a TemplateResponse will call the render_to_response() method that TemplateResponseMixin provides. 大多数情况下,这将被调用(例如,它由TemplateViewDetailView实现的get() 有关这方面的示例,请参阅JSONResponseMixin example

render_to_response() itself calls get_template_names(), which by default will just look up template_name on the class-based view; two other mixins (SingleObjectTemplateResponseMixin and MultipleObjectTemplateResponseMixin) override this to provide more flexible defaults when dealing with actual objects.

ContextMixin
Every built in view which needs context data, such as for rendering a template (including TemplateResponseMixin above), should call get_context_data() passing any data they want to ensure is in there as keyword arguments. get_context_data() returns a dictionary; in ContextMixin it simply returns its keyword arguments, but it is common to override this to add more members to the dictionary. 您也可以使用extra_context属性。

构建Django的泛型基于类的视图

我们来看看Django的两个通用的基于类的视图是如何通过提供离散功能的mixin构建的。 We’ll consider DetailView, which renders a “detail” view of an object, and ListView, which will render a list of objects, typically from a queryset, and optionally paginate them. 这将向我们介绍四个mixin,它们在使用单个Django对象或多个对象时提供了有用的功能。

在通用编辑视图(FormView和特定于模型的视图CreateViewUpdateViewDeleteView 这些包含在mixin reference documentation中。

DetailView: working with a single Django object

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

To get the object, DetailView relies on SingleObjectMixin, which provides a get_object() method that figures out the object based on the URL of the request (it looks for pk and slug keyword arguments as declared in the URLConf, and looks the object up either from the model attribute on the view, or the queryset attribute if that’s provided). SingleObjectMixin also overrides get_context_data(), which is used across all Django’s built in class-based views to supply context data for template renders.

To then make a TemplateResponse, DetailView uses SingleObjectTemplateResponseMixin, which extends TemplateResponseMixin, overriding get_template_names() as discussed above. It actually provides a fairly sophisticated set of options, but the main one that most people are going to use is <app_label>/<model_name>_detail.html. 通过将子类上的template_name_suffix设置为别的,可以改变_detail部分。 (例如,generic edit views使用_form创建和更新视图,_confirm_delete用于删除视图。

ListView:与许多Django对象一起使用

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

To get the objects, ListView uses MultipleObjectMixin, which provides both get_queryset() and paginate_queryset(). SingleObjectMixin不同的是,不需要关闭部分URL来找出要使用的查询集,所以默认只使用querysetmodel 这里覆盖get_queryset()的一个常见原因是动态地改变对象,比如取决于当前的用户,或者排除将来博客的帖子。

MultipleObjectMixin also overrides get_context_data() to include appropriate context variables for pagination (providing dummies if pagination is disabled). 它依赖作为关键字参数传入的object_listListView将其排列。

To make a TemplateResponse, ListView then uses MultipleObjectTemplateResponseMixin; as with SingleObjectTemplateResponseMixin above, this overrides get_template_names() to provide a range of options, with the most commonly-used being <app_label>/<model_name>_list.html, with the _list part again being taken from the template_name_suffix attribute. (基于日期的通用视图使用后缀(如_archive_archive_year等)为各种专用的基于日期的列表视图使用不同的模板。

使用Django的基于类的视图mixin

现在我们已经看到了Django的泛型基于类的视图如何使用提供的mixin,让我们看看我们可以将它们结合起来的其他方式。 当然,我们仍然将它们与内置的基于类的视图或其他通用的基于类的视图相结合,但是与Django开箱即用提供的解决方案相比,您可以解决一系列的问题。

警告

并不是所有的mixin都可以一起使用,并不是所有基于类的视图都可以与所有其他mixin一起使用。 这里我们举几个例子,如果要将其他功能集成在一起,则必须考虑在使用的不同类之间重叠的属性和方法之间的交互,以及方法解析顺序将如何影响方法的哪个版本将以什么顺序被调用。

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

If in doubt, it’s often better to back off and base your work on View or TemplateView, perhaps with SingleObjectMixin and MultipleObjectMixin. 虽然你最终可能会写更多的代码,但是稍后有人会更容易理解,而更少的交互来担心你会为自己节省一些思考。 (当然,你总是可以深入到Django的通用基于类的视图的实现中,以获得如何解决问题的灵感。)

SingleObjectMixin与View 一起使用

如果我们想写一个只响应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为我们照顾。

我们可以很容易地将它们链接到我们的URL中:

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的任何其他功能。

使用SingleObjectMixinListView

ListView provides built-in pagination, but you might want to paginate a list of objects that are all linked (by a foreign key) to another object. 在我们的发布示例中,您可能希望对特定发布商的所有图书进行分页。

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

Book queryset供ListView使用
由于我们可以访问我们想要列出书籍的Publisher,因此我们简单地覆盖get_queryset()并使用Publisherreverse foreign key manager
get_object()中使用的Publisher queryset
我们将依靠get_object()的默认实现来获取正确的Publisher对象。 However, we need to explicitly pass a queryset argument because otherwise the default implementation of get_object() would call get_queryset() which we have overridden to return Book objects instead of Publisher ones.

注意

我们必须仔细考虑get_context_data() Since both SingleObjectMixin and ListView will put things in the context data under the value of context_object_name if it’s set, we’ll instead explicitly ensure the Publisher is in the context data. ListView will add in the suitable page_obj and paginator for us providing we remember to call 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() If you don’t set template_name, the template will default to the normal ListView choice, which in this case would be "books/book_list.html" because it’s a list of books; ListView knows nothing about SingleObjectMixin, so it doesn’t have any clue this view is anything to do with a 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和日期。 For example it’s fine to combine TemplateView (built in view) with MultipleObjectMixin (generic list), but you’re likely to have problems combining SingleObjectMixin (generic detail) with MultipleObjectMixin (generic list).

为了展示当您尝试变得更加复杂时会发生什么,我们展示一个牺牲可读性和可维护性的例子,当有一个更简单的解决方案时。 First, let’s look at a naive attempt to combine DetailView with FormMixin to enable us to POST a Django Form to the same URL as we’re displaying an object using DetailView.

使用FormMixinDetailView

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

在这一点上,很自然的,用一个Form来封装从用户浏览器发送给Django的信息。 Say also that we’re heavily invested in REST, so we want to use the same URL for displaying the author as for capturing the message from the user. 我们重写我们的AuthorDetailView来做到这一点。

我们将继续从DetailView处理GET,不过我们必须在表单数据中添加一个Form模板。 We’ll also want to pull in form processing from FormMixin, and write a bit of code so that on POST the form gets called appropriately.

注意

We use FormMixin and implement post() ourselves rather than try to mix DetailView with FormView (which provides a suitable post() already) because both of the views implement get(), and things would get much more confusing.

我们新的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().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().form_valid(form)

get_success_url()只是提供了一个重定向到的地方,它在form_valid()的默认实现中使用。 如前所述,我们必须提供我们自己的post(),并覆盖get_context_data()来使上下文数据中的Form可用。

更好的解决方案

It should be obvious that the number of subtle interactions between FormMixin and DetailView is already testing our ability to manage things. 你不太可能想自己写这样的课。

在这种情况下,只要自己写post()方法,保持DetailView作为唯一的通用功能就相当容易了,尽管写了Form >处理代码涉及很多重复。

另外,比上面的方法还要容易一些,可以有一个单独的视图来处理表单,它可以使用不同于DetailViewFormView

另一个更好的解决方案

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

The AuthorDisplay view is almost the same as when we first introduced AuthorDetail; we have to write our own get_context_data() to make the AuthorInterestForm available to the template. 为了清晰起见,我们将跳过前面的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().get_context_data(**kwargs)
        context['form'] = AuthorInterestForm()
        return context

Then the AuthorInterest is a simple 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 AuthorDisplay is using on 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().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)

这种方法还可以用于任何其他通用的基于类的视图,或者直接从ViewTemplateView继承的基于类的视图,因为它将不同的视图保持分离尽可能。

不仅仅是HTML

基于课堂的观点闪耀的是当你想多次做同样的事情。 假设你正在编写一个API,并且每个视图都应该返回JSON而不是呈现的HTML。

我们可以创建一个mixin类来在我们所有的视图中使用,处理一次到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

注意

查看Serializing Django objects文档,了解如何将Django模型和查询集正确转换为JSON。

This mixin provides a render_to_json_response() method with the same signature as render_to_response(). To use it, we simply need to mix it into a TemplateView for example, and override render_to_response() to call render_to_json_response() instead:

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)

同样,我们可以使用我们的混合与一般的意见之一。 We can make our own version of DetailView by mixing JSONResponseMixin with the django.views.generic.detail.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)

This view can then be deployed in the same way as any other DetailView, with exactly the same behavior – except for the format of the response.

If you want to be really adventurous, you could even mix a DetailView subclass that is able to return both HTML and JSON content, depending on some property of the HTTP request, such as a query argument or a HTTP header. 只需混合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)

Because of the way that Python resolves method overloading, the call to super().render_to_response(context) ends up calling the render_to_response() implementation of TemplateResponseMixin.