Using mixins with class-based views

Caution

This is an advanced topic. A working knowledge of Django’s class-based views is advised before exploring these techniques.

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

出于这个原因,Django还提供了许多可提供更多离散功能的mixin。 例如,模板渲染封装在TemplateResponseMixin中。 Django参考文档包含所有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中,它只返回其关键字参数,但通常会覆盖它以向字典中添加更多成员。 您还可以使用extra_context属性。

构建Django基于类的通用视图

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

通用编辑视图中还包含mixin(FormView,以及特定于模型的视图CreateViewUpdateViewDeleteView ),以及基于日期的通用视图。 这些内容包含在mixin参考文档中。

DetailView:使用单个Django对象

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

要获取该对象,DetailView依赖于SingleObjectMixin,它提供了一个get_object()方法,该方法根据请求的URL计算出对象(它查找URLConf中声明的pkslug关键字参数,并从视图上的model属性查找对象,或queryset属性(如果提供)。 SingleObjectMixin还会覆盖get_context_data(),它用于所有Django内置的基于类的视图,以提供模板渲染的上下文数据。

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. The _detail part can be changed by setting template_name_suffix on a subclass to something else. (For instance, the generic edit views use _form for create and update views, and _confirm_delete for delete views.)

ListView:使用许多Django对象

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

To get the objects, ListView uses MultipleObjectMixin, which provides both get_queryset() and paginate_queryset(). Unlike with SingleObjectMixin, there’s no need to key off parts of the URL to figure out the queryset to work with, so the default just uses the queryset or model attribute on the view class. A common reason to override get_queryset() here would be to dynamically vary the objects, such as depending on the current user or to exclude posts in the future for a blog.

MultipleObjectMixin also overrides get_context_data() to include appropriate context variables for pagination (providing dummies if pagination is disabled). It relies on object_list being passed in as a keyword argument, which ListView arranges for it.

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. (The date based generic views use suffixes such as _archive, _archive_year and so on to use different templates for the various specialized date-based list views.)

使用Django基于类的视图mixins

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

Warning

并非所有mixin都可以一起使用,并非所有基于泛型类的视图都可以与所有其他mixin一起使用。 这里我们举几个可行的例子;如果你想将其他功能集合在一起,那么你将不得不考虑在你正在使用的不同类之间重叠的属性和方法之间的交互,以及方法解析顺序将如何影响方法的哪个版本将按什么顺序调用。

Django的基于类的视图基于类的视图mixins的参考文档将帮助您了解哪些属性和方法可能导致不同类和mixin之间的冲突。

如果有疑问,通常最好退出并基于查看TemplateView,可能使用SingleObjectMixinMultipleObjectMixin 虽然你最终可能会编写更多的代码,但是对于其他人稍后会更容易理解,并且由于担心你的交互较少,你会省去一些思考。 (当然,您可以随时了解Django对基于类的通用视图的实现,以获得有关如何解决问题的灵感。)

Using SingleObjectMixin with View

如果我们想编写一个简单的基于类的视图,只响应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}))

In practice you’d probably want to record the interest in a key-value store rather than in a relational database, so we’ve left that bit out. The only bit of the view that needs to worry about using SingleObjectMixin is where we want to look up the author we’re interested in, which it just does with a simple call to self.get_object(). Everything else is taken care of for us by the mixin.

We can hook this into our URLs easily enough:

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

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

Note the pk named group, which get_object() uses to look up the Author instance. You could also use a slug, or any of the other features of SingleObjectMixin.

使用SingleObjectMixinListView

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

One way to do this is to combine ListView with SingleObjectMixin, so that the queryset for the paginated list of books can hang off the publisher found as the single object. In order to do this, we need to have two different querysets:

Book queryset for use by ListView
由于我们可以访问要列出其书籍的Publisher,我们只需覆盖get_queryset()并使用Publisher反向外键管理员
Publisher queryset for use in get_object()
我们将依赖get_object()的默认实现来获取正确的Publisher对象。 但是,我们需要显式传递queryset参数,否则get_object()的默认实现将调用get_queryset(),我们已将其覆盖为返回Book对象而不是Publisher

Note

We have to think carefully about 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()

Notice how we set self.object within get() so we can use it again later in get_context_data() and 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.

The paginate_by is deliberately small in the example so you don’t have to create lots of books to see the pagination working! Here’s the template you’d want to use:

{% 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组合在一起。 然而,当你尝试这样做时,事情变得越来越复杂,一个好的经验法则是:

Hint

您的每个视图应仅使用其中一组基于类的通用视图的mixins或视图:detail,listediting和date。 例如,将TemplateView(内置视图)与MultipleObjectMixin(通用列表)结合起来很好,但是你可能会遇到组合SingleObjectMixin的问题。 (通用细节)与MultipleObjectMixin(通用列表)。

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

Using FormMixin with DetailView

Think back to our earlier example of using View and SingleObjectMixin together. We were recording a user’s interest in a particular author; say now that we want to let them leave a message saying why they like them. Again, let’s assume we’re not going to store this in a relational database but instead in something more esoteric that we won’t worry about here.

At this point it’s natural to reach for a Form to encapsulate the information sent from the user’s browser to 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. Let’s rewrite our AuthorDetailView to do that.

We’ll keep the GET handling from DetailView, although we’ll have to add a Form into the context data so we can render it in the template. 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.

Note

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() is just providing somewhere to redirect to, which gets used in the default implementation of form_valid(). We have to provide our own post() as noted earlier, and override get_context_data() to make the Form available in the context data.

A better solution

It should be obvious that the number of subtle interactions between FormMixin and DetailView is already testing our ability to manage things. It’s unlikely you’d want to write this kind of class yourself.

In this case, it would be fairly easy to just write the post() method yourself, keeping DetailView as the only generic functionality, although writing Form handling code involves a lot of duplication.

Alternatively, it would still be easier than the above approach to have a separate view for processing the form, which could use FormView distinct from DetailView without concerns.

An alternative better solution

What we’re really trying to do here is to use two different class based views from the same URL. So why not do just that? We have a very clear division here: GET requests should get the DetailView (with the Form added to the context data), and POST requests should get the FormView. Let’s set up those views first.

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. We’ll skip the get_object() override from before for clarity:

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

Finally we bring this together in a new AuthorDetail view. We already know that calling as_view() on a class-based view gives us something that behaves exactly like a function based view, so we can do that at the point we choose between the two subviews.

You can of course pass through keyword arguments to as_view() in the same way you would in your URLconf, such as if you wanted the AuthorInterest behavior to also appear at another URL but using a different template:

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)

This approach can also be used with any other generic class-based views or your own class-based views inheriting directly from View or TemplateView, as it keeps the different views as separate as possible.

More than just HTML

Where class-based views shine is when you want to do the same thing many times. Suppose you’re writing an API, and every view should return JSON instead of rendered HTML.

We can create a mixin class to use in all of our views, handling the conversion to JSON once.

For example, a simple JSON mixin might look something like this:

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

Note

Check out the Serializing Django objects documentation for more information on how to correctly transform Django models and querysets into JSON.

这个mixin提供了一个render_to_json_response()方法,其签名与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)

同样,我们可以将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子类,它能够返回 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().render_to_response(context)

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