与基于类的视图一起使用 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()
方法。大部分时间里会被你调用(比如,它通过TemplateView
和DetailView
) 共同实现的get()
方法调用);类似的,它不太可能需要你去覆盖它,尽管如果你希望响应返回一些没有被 Django 模板渲染的东西,那么你就会想要这么做。
render_to_response()
本身调用 get_template_names()
,它默认只是检查基于类的视图上的 template_name
。当处理真实对象时,两个其他 mixins (SingleObjectTemplateResponseMixin
和 MultipleObjectTemplateResponseMixin
) 覆盖它来提供更多灵活默认值。
ContextMixin
- 每个需要上下文数据的内建视图,比如为了渲染一个模板(在上面包含
TemplateResponseMixin
),应该调用get_context_data()
传递任何它们想要确定的数据当做关键参数。get_context_data()
返回一个字典;在ContextMixin
里它只返回它的关键参数,但它通常覆盖此项以添加更多成员到字典中。你也可以使用extra_context
属性。
构造 Django 基于类的通用视图
让我们看 Django 的两个基于类的通用视图如何由 mixins 构建,提供分离功能的。我们考虑 DetailView
,它渲染一个对象的详情视图,还有 ListView
,它渲染一个对象列表,通常来自查询集,并且可以分页。这里将介绍四个 mixins ,它们在使用单个 Django 对象或多个对象时,提供常用功能。
mixins 也包含在通用编辑视图( FormView
、指定模型视图 CreateView
、 UpdateView
和 DeleteView
) 里,基于日期的通用视图。这些包括在 mixin reference documentation 。
DetailView :使用单个 Django 对象
为了显示对象详情,我们基本上需要做两件事:我们需要查询对象,然后我们需要使用合适的模板创建 TemplateResponse
,并将该对象作为上下文。
为了获取对象,DetailView
依靠于 SingleObjectMixin
,它提供一个 get_object()
方法,该方法根据请求的 URL 来计算对象(它寻找 URLconf 中的声明的 pk
和 slug
关键参数,并从视图上的 model
属性查找对象,或者如果提供了 queryset
属性,将使用这个属性)。SingleObjectMixin
也覆盖了 get_context_data()
,它被用于所有 Django 内置的基于类的视图,为模板渲染提供上下文数据。
然后创建一个 TemplateResponse
,DetailView
使用 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 的某些部分来找出要使用的查询集,因此只使用在视图类上的 queryset
或 model
属性即可。覆盖 get_queryset()
的常见原因是动态改变对象,比如根据当前对象在将来排除帖子。
MultipleObjectMixin
也会覆盖 get_context_data()
来包含适合分页的上下文变量(如果关闭分页则提供虚假分页)。它依赖 object_list
作为关键字参数来传入,ListView
排列它。
要创建 TemplateResponse
,ListView
然后使用 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之间的冲突。
如果有疑问,通常最好是回退并最好在 View
或 TemplateView
上工作,或许使用 SingleObjectMixin
和 MultipleObjectMixin
。尽管你有可能最后写了很多代码,但它会被其他后来者理解,而且你节省了很多精力在沟通上面。(当然,你可以随时了解使用 Django 实现的基于类的通用视图,来获取如何解决问题的灵感。)
视图和 SingleObjectMixin 一起使用
如果我们想编写一个简单的只响应 POST
的基于类的视图,我们将子类化 View
并且在子类中编写一个 post()
方法。你希望我们的处理工作在一个来自 URL 标识的特定的对象,我们将需要 SingleObjectMixin
提供的功能。
我们将使用 Author
模型来演示。
- 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 中:
- 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
的任何其他功能。
ListView 和 SingleObjectMixin 一起使用
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()
。由于 SingleObjectMixin
和 ListView
会将上下文数据放在 context_object_name
的值下(如果它已设置),我们要确保 Publisher
在上下文数据中。ListView
将为我们添加合适的 page_obj
和 paginator
(如果我们记得调用 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"
,因为它是书籍列表;ListView
对 SingleObjectMixin
一无所知,因此这个视图和 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 %}
避免过度复杂的事情
当你使用它们的功能时,通常你可以使用 TemplateResponseMixin
和 SingleObjectMixin
。如上所示,你甚至可以将 SingleObjectMixin
和 ListView
结合起来。然而当你试着这么做时,事情将变得复杂,一个好的经验法则是:
提示
你的每个视图应该只使用 mixins 或者来自一个通用基于类的视图的组里视图:detail, list1, editing2 和日期。举例它将 TemplateView
(在视图里内建) 和 MultipleObjectMixin
(通用列表) 结合起来,但你可能会在 SingleObjectMixin
(通用详情) 和 MultipleObjectMixin
结合时遇到问题。
为了给你展示当变得复杂时发生了什么,我们显示了一个例子,当这里有一个更简单的解决方案时,我们牺牲了可读写和可维护性。首先,让我们试着结合 DetailView
和 FormMixin
,这样当我们使用 DetailView
显示对象时,可以 POST
一个 Form
到同样的 URL 里。
DetailView 和 FormMixin 一起使用
让我们回到先前关于同时使用 View
和 SingleObjectMixin
的例子。我们已经记录了用户对特定作者的喜好;现在我们需要留言说为什么我们喜欢他们。再次说明,我们假设不在关系数据里保存它,而存储在更难理解的东西里,我们现在先不考虑细节。
在这点上,很自然的找到一个 a Form
来封装从浏览器传递到 Django 的信息。也可以说我们在 REST 上投入了很多精力,我们想使用相同的 URL 来显示作者,以便从用户那里捕获信息。让我们重写 AuthorDetailView
来实现吧。
我们将保持 GET
来处理 DetailView
,虽然我们不得不在上下文数据里添加 Form
,但这样我们就可以在模板里渲染它。我们也想从 FormMixin
中引入表单处理并编写一些代码,这样在 POST
表单的时候可以调用它。
注解
我们使用 FormMixin
并亲自实现了 post()
,而不是试着把 DetailView
和 FormView
(也提供合适的 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()
。
更好的解决方案
很明显的是,FormMixin
和 DetailView
之间细微的联系已经在测试我们管理事务的能力了。你不太可能想写这样的类。
在这个例子里,你编写 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:View 或 TemplateView
使用,因为它使不同视图尽可能分离。
不仅仅是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 和其中一个通用视图一起使用。我们可以把 JSONResponseMixin
和 django.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。只需混合 JSONResponseMixin
和 SingleObjectTemplateResponseMixin
,并重写 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().renderto_response(context)
的调用最终会调用 TemplateResponseMixin
的 的:meth:~django.views.generic.base.TemplateResponseMixin.renderto_response() 实现。