注意
这是一个进阶的话题。 需要建立在了解 Django’s class-based views的基础上。
Django的基于类的视图提供了许多功能,但是你可能只想使用其中的一部分。 例如,您可能需要编写一个视图来呈现模板以进行HTTP响应,但不能使用TemplateView
;也许您只需要在POST
上呈现模板,GET
完全执行其他操作。 虽然你可以直接使用TemplateResponse
,但是这将导致重复的代码。
由于这些原因,Django 提供许多Mixin,它们提供更细致的功能。 例如,渲染模板封装在TemplateResponseMixin
中。 Django 参考手册包含full documentation of all the
mixins。
在基于类的视图中使用模板具有一致的接口,有两个Mixin 起了核心的作用。
TemplateResponseMixin
返回TemplateResponse
的每个视图都将调用render_to_response()
方法,这个方法由 TemplateResponseMixin
提供。 Most of the time this
will be called for you (for instance, it is called by the get()
method
implemented by both TemplateView
and
DetailView
); similarly, it’s unlikely
that you’ll need to override it, although if you want your response to
return something not rendered via a Django template then you’ll want to do
it. 其示例用法请参见JSONResponseMixin example。
render_to_response()
本身调用get_template_names()
,默认情况下只会在基于类的视图上查找template_name
另外另外两个混合(SingleObjectTemplateResponseMixin
和MultipleObjectTemplateResponseMixin
)会覆盖此值,以便在处理实际对象时提供更灵活的默认值。
ContextMixin
TemplateResponseMixin
),都应该以关键字参数调用get_context_data()
,以确保它们想要的数据在里面。
get_context_data()
返回字典;在ContextMixin
中,它只是返回它的关键字参数,但是通常要重写这个来添加更多的成员到字典。让我们看下Django 的两个通用的基于类的视图是如何通过互不相关的Mixin 构建的。 我们将考虑DetailView
,它渲染一个对象的“详细”视图,和ListView
,它渲染一个对象列表,通常来自一个查询集,需要时还会分页。 这将会向我们接收四个Mixin,这些Mixin 在用到单个或多个Django对象时非常有用。
在通用的编辑视图(FormView
和模型相关的视图CreateView
、UpdateView
和DeleteView
)和基于日期的通用视图中都会涉及到Minxin。 它们在mixin reference
documentation中讲述。
DetailView
:使用单个Django对象¶为了显示一个对象的详细信息,我们通常需要做两件事情:查询对象然后利用合适的模板和包含该对象的Context 生成TemplateResponse
。
为了获得对象,DetailView
依赖SingleObjectMixin
,它提供一个get_object()
方法,这个方法基于请求的URL 获取对象(它查找URLconf 中声明的pk
和slug
关键字参数,然后从视图的model
属性或queryset
属性查询对象)。 SingleObjectMixin
还覆盖get_context_data()
,这个方法在Django 所有的内建的基于类的视图中都有用到,用来给模板的渲染提供Context 数据。
然后,为了生成TemplateResponse
,DetailView
使用SingleObjectTemplateResponseMixin
,它扩展自TemplateResponseMixin
并覆盖上文讨论过的get_template_names()
。 实际上,它提供比较复杂的选项集合,但是大部分人用到的主要的一个是 <app_label>/<model_name>_detail.html
。 _detail
部分可以通过设置子类的template_name_suffix
来改变。 (例如,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()
以包含合适的Context 变量用于分页(如果禁止分页,则提供一些假的)。 这个方法依赖传递给它的关键字参数object_list
,ListView
会负责准备好这个参数。
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_year
、_archive
等等这样的后缀来针对各种基于日期的列表视图使用不同的模板)。
既然我们已经看到Django 通用的基于类的视图时如何使用Mixin,让我们在看看其它组合它们的方式。 当然,我们仍将它们与内建的基于类的视图或其它通用的基于类的视图组合,但是对于Django 提供的便利性你将解决一些更加罕见的问题。
警告
不是所有的Mixin 都可以一起使用,也不是所有的基于类的视图都可以与其它Mixin 一起使用。 在这里,我们提供几个做工作的例子;如果要汇集其他功能,则必须考虑属性和方法之间的交互,这些方法与您使用的不同类之间重叠,以及方法解析顺序将如何影响哪些版本的方法将以什么顺序调用。
Django 的class-based views 和class-based view mixins 的文档将帮助你理解在不同的类和Mixin 之间那些属性和方法可能引起冲突。
如果有担心,通常最好退避并基于View
或TemplateView
,或者可能的话加上SingleObjectMixin
和 MultipleObjectMixin
。 虽然你可能最终会编写更多的代码,但是对于后来的人更容易理解,而且你自己也少了份担心。 (当然,您可以随时参与Django实施通用的基于类的视图,以启发如何解决问题。)
SingleObjectMixin
与View ¶如果你想编写一个简单的基于类的视图,它只响应post()
,我们将子类化View
并在子类中只编写一个POST
方法。 但是,如果我们想处理一个由URL 标识的特定对象,我们将需要SingleObjectMixin
提供的功能。
我们将使用在generic class-based views introduction 中用到的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
的视图唯一需要担心的是在哪里查询我们感兴趣的Author,而它会用一个简单的self.get_object()
调用实现。 其它的所有事情都有该Mixin 帮我们处理。
我们可以将它这样放入URL 中,非常简单:
from django.conf.urls import url
from books.views import RecordInterest
urlpatterns = [
#...
url(r'^author/(?P<pk>[0-9]+)/interest/$', RecordInterest.as_view(), name='author-interest'),
]
注意Author
命名组,get_object()
将用它来查询pk
实例。 你还可以使用slug,或者SingleObjectMixin
的其它功能。
SingleObjectMixin
与ListView
¶ListView
提供内建的分页,但是可能你分页的列表中每个对象都与另外一个对象(通过一个外键)关联。 在我们的Publishing 例子中,你可能想通过一个特定的Publisher 分页所有的Book。
一种方法是组合ListView
和SingleObjectMixin
,这样分页的Book 列表的查询集能够与找到的单个Publisher 对象关联。 为了实现这点,我们需要两个不同的查询集:
Book
查询使用ListView
Publisher
,我们只需覆盖get_queryset()
,并使用Publisher
的reverse foreign key manager。Publisher
在get_object()
中使用的查询器get_object()
的默认实现来获取正确的Publisher
对象。
但是,我们需要明确地传递一个queryset
参数,否则默认的get_object()
实现将调用我们覆盖的get_queryset()
返回Book
对象,而不是Publisher
。注
我们必须仔细考虑get_context_data()
。
因为SingleObjectMixin
和ListView
都会将Context 数据的context_object_name
下,我们必须显式确保Publisher
位于Context 数据中。 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(PublisherDetail, self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(PublisherDetail, self).get_context_data(**kwargs)
context['publisher'] = self.object
return context
def get_queryset(self):
return self.object.book_set.all()
注意我们 在 get_queryset()
方法里设置了get()
,这样我们就可以在后面的 get_context_data()
和self.object
方法里再次用到它.
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 %}
通常情况下你只在需要相关功能时才会使用 TemplateResponseMixin
和SingleObjectMixin
这两个类。 如上所示,只要加点儿小心,你甚至可以把SingleObjectMixin
和ListView
结合在一起来使用. 但是这么搞可能会让事情变得有点复杂,作为一个好的原则:
提示:
你的视图扩展应该仅仅使用那些来自于同一组通用基类的view或者mixins。如: detail,
list, editing 和 date. 例如:把 TemplateView
(内建视图)和 MultipleObjectMixin
(通用列表)整合在一起是极好的, 但是若想把SingleObjectMixin
(generic detail) 和 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
.
FormMixin
与DetailView
¶想想我们之前合用 View
和SingleObjectMixin
的例子. 我们正在记录用户对特定作者的兴趣;现在说,我们想让他们留言说他们为什么喜欢他们。 同样的,我们假设这些数据并没有存放在关系数据库里,而是存在另外一个奥妙之地(其实这里不用关心具体存放到了哪里)。
要实现这一点,自然而然就要设计一个 Form
,让用户把相关信息通过浏览器发送到Django后台。 另外,我们要巧用REST方法,这样我们就可以用相同的URL来显示作者和捕捉来自用户的消息了。 让我们重写 AuthorDetailView
来实现它。
我们将保持DetailView
的GET
处理,虽然我们必须在上下文数据中添加一个Form
,以便我们可以渲染它模板。 我们还想从FormMixin
中提取表单处理,并写一些代码,以便在POST
上适当地调用表单。
注
我们使用FormMixin
并实现post()
,而不是尝试将DetailView
与FormView
结合(FormView已经提供了get()
),因为这两个视图都实现了post()
,事情会变得更加混乱。
我们的新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(AuthorDetail, self).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(AuthorDetail, self).form_valid(form)
form_valid()
只是提供重定向的地方,它在get_success_url()
的默认实现中使用。 如上所述,我们必须提供我们自己的get_context_data()
,并覆盖post()
,以使Form
在上下文数据中可用。
很明显,FormMixin
和DetailView
之间的微妙交互的数量已经在测试我们管理事物的能力。 你不太可能会去想自己写这种类的。
在这种情况下,只需自己写post()
方法,保持DetailView
作为唯一的通用功能,虽然写Form
处理代码涉及很多重复。
或者,仍然比上述方法更容易具有用于处理表单的单独视图,这可以使用FormView
与DetailView
不同的地方。
我们真正想在这里做的是使用来自同一个URL的两个不同的基于类的视图。 那么为什么不这样做呢? 我们在这里有一个非常清楚的划分:POST
请求应该获得DetailView
(将Form
添加到上下文数据),GET
请求应该获得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(AuthorDisplay, self).get_context_data(**kwargs)
context['form'] = AuthorInterestForm()
return context
template_name
是一个简单的 FormView
, 但是我们不得不把SingleObjectMixin
引入进来,这样我们才能定位我们评论的作者,并且我们还要记得设置AuthorDisplay
来确保form出错时使用 GET
会渲染到 AuthorInterest
相同的模板 :
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(AuthorInterest, self).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
行为也出现在另一个网址但使用不同的模板:
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)
此方法也可以与任何其他基于类的视图或您自己的基于类的视图直接从View
或TemplateView
继承,因为它独尽可能的保持不同的视图单。
当基于类的观点发光时,当你想做同样的事情很多次。 假设你正在编写API,每个视图应该返回JSON 而不是渲染后的HTML。
我们可以创建一个Mixin 类来处理JSON 的转换,并将它用于所有的视图。
例如,一个简单的JSON Mixin 可能像这样:
from django.http import JsonResponse
class JSONResponseMixin(object):
"""
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()
的参数相同。
要使用它,我们只需要将它与render_to_response()
组合,并覆盖render_to_json_response()
来调用TemplateView
:
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 与某个通用的视图一起使用。 我们可以实现自己的DetailView
版本,将JSONResponseMixin
和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)
这个视图可以和其它DetailView
一样使用,它们的行为完全相同 —— 除了响应的格式之外。
如果你想更进一步,你可以组合DetailView
的子类,它根据HTTP 请求的某个属性既能够返回HTML 又能够返回JSON 内容,例如查询参数或HTTP 头部。 这只需将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(HybridDetailView, self).render_to_response(context)
由于Python 解析方法重载的方式,super(HybridDetailView, self).render_to_response(context)
调用将以调用 TemplateResponseMixin
的render_to_response()
实现结束。
2017年9月6日