扩展通用视图

毫无疑问,使用通用视图可以充分加快开发速度。 然而,在多数的工程中,也会出现通用视图不能 满足需求的情况。 实际上,刚接触Django的开发者最常见的问题就是怎样使用通用视图来处理更多的情况。

幸运的是,几乎每种情况都有相应的方法来简易地扩展通用视图以处理这些情况。 这时总是使用下面的 这些方法。

制作友好的模板Context

你也许已经注意到范例中的出版商列表模板在变量 object_list 里保存所有的书籍。这个方法工作的很好,只是对编写模板的人不太友好。 他们必须知道这里正在处理的是书籍。 更好的变量名应该是publisher_list,这样变量所代表的内容就显而易见了。

我们可以很容易地像下面这样修改 template_object_name 参数的名称:

  1. from django.conf.urls.defaults import *
  2. from django.views.generic import list_detail
  3. from mysite.books.models import Publisher
  4. publisher_info = {
  5. 'queryset': Publisher.objects.all(),
  6. 'template_name': 'publisher_list_page.html',
  7. 'template_object_name': 'publisher',
  8. }
  9. urlpatterns = patterns('',
  10. (r'^publishers/$', list_detail.object_list, publisher_info)
  11. )

在模板中,通用视图会通过在template_object_name后追加一个_list的方式来创建一个表示列表项目的变量名。

使用有用的 template_object_name 总是个好想法。 你的设计模板的合作伙伴会感谢你的。

添加额外的Context

你常常需要呈现比通用视图提供的更多的额外信息。 例如,考虑一下在每个出版商的详细页面显示所有其他出版商列表。 objectdetail 通用视图为context提供了出版商信息,但是看起来没有办法在模板中 获取 所有_ 出版商列表。

这是解决方法: 所有的通用视图都有一个额外的可选参数 extra_context 。这个参数是一个字典数据类型,包含要添加到模板的context中的额外的对象。 所以要给视图提供所有出版商的列表,我们就用这样的info字典:

  1. publisher_info = {
  2. 'queryset': Publisher.objects.all(),
  3. 'template_object_name': 'publisher',
  4. **'extra_context': {'book_list': Book.objects.all()}**
  5. }

这样就把一个 {{ book_list }} 变量放到模板的context中。 这个方法可以用来传递任意数据 到通用视图模板中去,非常方便。 这是非常方便的

不过,这里有一个很隐蔽的BUG,不知道你发现了没有?

我们现在来看一下, extra_context 里包含数据库查询的问题。 因为在这个例子中,我们把 Publisher.objects.all() 放在URLconf中,它只会执行一次(当URLconf第一次加载的时候)。 当你添加或删除出版商,你会发现在重启Web服务器之前,通用视图不会反映出这些修改(有关QuerySet何时被缓存和赋值的更多信息请参考附录C中“缓存与查询集”一节)。

备注

这个问题不适用于通用视图的 queryset 参数。 因为Django知道有些特别的 QuerySet 永远不能 被缓存,通用视图在渲染前都做了缓存清除工作。

解决这个问题的办法是在 extra_context 中用一个回调(callback)来代替使用一个变量。 任何传递给extra_context的可调用对象(例如一个函数)都会在每次视图渲染前执行(而不是只执行一次)。 你可以象这样定义一个函数:

  1. **def get_books():**
  2. **return Book.objects.all()**
  3. publisher_info = {
  4. 'queryset': Publisher.objects.all(),
  5. 'template_object_name': 'publisher',
  6. 'extra_context': **{'book_list': get_books}**
  7. }

或者你可以使用另一个不是那么清晰但是很简短的方法,事实上 Publisher.objects.all 本身就是可以调用的:

  1. publisher_info = {
  2. 'queryset': Publisher.objects.all(),
  3. 'template_object_name': 'publisher',
  4. 'extra_context': **{'book_list': Book.objects.all}**
  5. }

注意 Book.objects.all 后面没有括号;这表示这是一个函数的引用,并没有真正调用它(通用视图将会在渲染时调用它)。

显示对象的子集

现在让我们来仔细看看这个 queryset 。 大多数通用视图有一个queryset参数,这个参数告诉视图要显示对象的集合 (有关QuerySet的解释请看第五章的 “选择对象”章节,详细资料请参看附录B)。

举一个简单的例子,我们打算对书籍列表按出版日期排序,最近的排在最前:

  1. book_info = {
  2. 'queryset': Book.objects.order_by('-publication_date'),
  3. }
  4. urlpatterns = patterns('',
  5. (r'^publishers/$', list_detail.object_list, publisher_info),
  6. **(r'^books/$', list_detail.object_list, book_info),**
  7. )

这是一个相当简单的例子,但是很说明问题。 当然,你通常还想做比重新排序更多的事。 如果你想要呈现某个特定出版商出版的所有书籍列表,你可以使用同样的技术:

  1. **apress_books = {**
  2. **'queryset': Book.objects.filter(publisher__name='Apress Publishing'),**
  3. **'template_name': 'books/apress_list.html'**
  4. **}**
  5. urlpatterns = patterns('',
  6. (r'^publishers/$', list_detail.object_list, publisher_info),
  7. **(r'^books/apress/$', list_detail.object_list, apress_books),**
  8. )

注意 在使用一个过滤的 queryset 的同时,我们还使用了一个自定义的模板名称。 如果我们不这么做,通用视图就会用以前的模板,这可能不是我们想要的结果。

同样要注意的是这并不是一个处理出版商相关书籍的最好方法。 如果我们想要添加另一个 出版商页面,我们就得在URL配置中写URL配置,如果有很多的出版商,这个方法就不能 接受了。 在接下来的章节我们将来解决这个问题。

用函数包装来处理复杂的数据过滤

另一个常见的需求是按URL里的关键字来过滤数据对象。 之前,我们在URLconf中硬编码了出版商的名字,但是如果我们想用一个视图就显示某个任意指定的出版商的所有书籍,那该怎么办呢? 我们可以通过对 object_list 通用视图进行包装来避免 写一大堆的手工代码。 按惯例,我们先从写URL配置开始:

  1. urlpatterns = patterns('',
  2. (r'^publishers/$', list_detail.object_list, publisher_info),
  3. **(r'^books/(\w+)/$', books_by_publisher),**
  4. )

接下来,我们写 books_by_publisher 这个视图:

  1. from django.shortcuts import get_object_or_404
  2. from django.views.generic import list_detail
  3. from mysite.books.models import Book, Publisher
  4. def books_by_publisher(request, name):
  5. # Look up the publisher (and raise a 404 if it can't be found).
  6. publisher = get_object_or_404(Publisher, name__iexact=name)
  7. # Use the object_list view for the heavy lifting.
  8. return list_detail.object_list(
  9. request,
  10. queryset = Book.objects.filter(publisher=publisher),
  11. template_name = 'books/books_by_publisher.html',
  12. template_object_name = 'book',
  13. extra_context = {'publisher': publisher}
  14. )

这样写没问题,因为通用视图就是Python函数。 和其他的视图函数一样,通用视图也是接受一些 参数并返回 HttpResponse 对象。 因此,通过包装通用视图函数可以做更多的事。

注意

注意在前面这个例子中我们在 extra_context中传递了当前出版商这个参数。

处理额外工作

我们再来看看最后一个常用模式:

想象一下我们在 Author 对象里有一个 last_accessed 字段,我们用这个字段来记录最近一次对author的访问。 当然通用视图 object_detail 并不能处理这个问题,但是我们仍然可以很容易地编写一个自定义的视图来更新这个字段。

首先,我们需要在URL配置里设置指向到新的自定义视图:

  1. from mysite.books.views import author_detail
  2. urlpatterns = patterns('',
  3. # ...
  4. **(r'^authors/(?P<author_id>\d+)/$', author_detail),**
  5. # ...
  6. )

接下来写包装函数:

  1. import datetime
  2. from django.shortcuts import get_object_or_404
  3. from django.views.generic import list_detail
  4. from mysite.books.models import Author
  5. def author_detail(request, author_id):
  6. # Delegate to the generic view and get an HttpResponse.
  7. response = list_detail.object_detail(
  8. request,
  9. queryset = Author.objects.all(),
  10. object_id = author_id,
  11. )
  12. # Record the last accessed date. We do this *after* the call
  13. # to object_detail(), not before it, so that this won't be called
  14. # unless the Author actually exists. (If the author doesn't exist,
  15. # object_detail() will raise Http404, and we won't reach this point.)
  16. now = datetime.datetime.now()
  17. Author.objects.filter(id=author_id).update(last_accessed=now)
  18. return response

注意

除非你添加 last_accessed 字段到你的 Author 模型并创建 books/author_detail.html 模板,否则这段代码不能真正工作。

我们可以用同样的方法修改通用视图的返回值。 如果我们想要提供一个供下载用的 纯文本版本的author列表,我们可以用下面这个视图:

  1. def author_list_plaintext(request):
  2. response = list_detail.object_list(
  3. request,
  4. queryset = Author.objects.all(),
  5. mimetype = 'text/plain',
  6. template_name = 'books/author_list.txt'
  7. )
  8. response["Content-Disposition"] = "attachment; filename=authors.txt"
  9. return response

这个方法之所以工作是因为通用视图返回的 HttpResponse 对象可以象一个字典 一样的设置HTTP的头部。 随便说一下,这个 Content-Disposition 的含义是 告诉浏览器下载并保存这个页面,而不是在浏览器中显示它。