Django通用视图讲解
Web 开发是一项无聊而且单调的工作。到目前为止,我们已经介绍了 Django怎样在模型和模板的层面上减小开发的单调性,但是 Web开发在视图的层面上,也经历着这种令人厌倦的事情。
Django 的generic views可以减少这些痛苦。它抽象出一些在视图开发中常用的代码和模式,这样就可以在无需编写大量代码的情况下,快速编写出常用的数据视图。事实上,前面章节中的几乎所有视图的示例都可以在通用视图的帮助下重写。
在第八章简单的向大家介绍了怎样使视图更加的“通用”。回顾一下,我们会发现一些比较常见的任务,比如显示一系列对象,写一段代码来显示任何对象内容。解决办法就是传递一个额外的参数到URLConf。
Django内建通用视图可以实现如下功能:
§ 完成常用的简单任务:重定向到另一个页面以及渲染一个指定的模板。
§ 显示列表和某个特定对象的详细内容页面。第8章中提到的event_list和entry_list视图就是列表视图的一个例子。一个单一的
event 页面就是我们所说的详细内容页面。
§ 呈现基于日期的数据的年/月/日归档页面,关联的详情页面,最新页面。Django
Weblogs (http://www.djangoproject.com/weblog/)的年、月、日的归档就是使用通用视图架构的,就像是典型的新闻报纸归档。
§ 允许用户在授权或者未经授权的情况下创建、修改和删除对象。
综上所述,这些视图为开发者日常开发中常见的任务提供了易用的接口。
使用通用视图
使用通用视图的方法是在URLconf文件中创建配置字典,然后把这些字典作为URLconf元组的第三个成员。
例如,下面是一个呈现静态“关于”页面的URLconf:
from django.conf.urls.defaults import * from django.views.generic.simple import direct_to_template urlpatterns = patterns('', ('^about/$', direct_to_template, { 'template': 'about.html' }) )
一眼看上去似乎有点不可思议,不需要编写代码的视图!它和第八章中的例子完全一样:direct_to_template视图从参数中获取渲染视图所需的相关信息。
因为通用视图都是标准的视图函数,我们可以在我们自己的视图中重用它。例如,我们扩展 about例子把映射的URL从/about/<whatever>/到一个静态渲染about/<whatever>.html。我们首先修改URL配置到新的视图函数:
from django.conf.urls.defaults import * from django.views.generic.simple import direct_to_template **from mysite.books.views import about_pages** urlpatterns = patterns('', ('^about/$', direct_to_template, { 'template': 'about.html' }), **('^about/(w+)/$', about_pages),** )
接下来,我们编写about_pages视图的代码:
from django.http import Http404 from django.template import TemplateDoesNotExist from django.views.generic.simple import direct_to_template def about_pages(request, page): try: return direct_to_template(request, template="about/%s.html" % page) except TemplateDoesNotExist: raise Http404()
在这里我们象使用其他函数一样使用direct_to_template。因为它返回一个HttpResponse对象,我们只需要简单的返回它就好了。有一个稍微复杂的地方,要处理没有找到模板的情况。我们不希望一个不存在的模板引发服务器错误,所以我们捕捉TempalteDoesNotExist异常并返回404错误。
这里有没有安全性问题?
眼尖的读者可能已经注意到一个可能的安全漏洞:我们直接使用从客户端浏览器来的数据构造模板名称(template="about/%s.html"%page)。乍看起来,这像是一个经典的目录遍历(directory
traversal)攻击(详情请看第十九章)。事实真是这样吗?
完全不是。是的,一个恶意的page值可以导致目录跨越,但是尽管page是从请求的URL中获取的,并不是所有的值都被接受。这就是URL配置的关键所在:我们使用正则表达式/w+来从URL里匹配page,而/w只接受字符和数字。因此,任何恶意的字符(例如在这里是点.和正斜线/)将在URL解析时被拒绝,根本不会传递给视图函数。
对象的通用视图
direct_to_template毫无疑问是非常有用的,但Django通用视图最有用的是在呈现数据库中的数据。因为这个应用实在太普遍了,Django带有很多内建的通用视图来帮助你很容易的生成对象的列表和明细视图。
让我们先看看其中的一个通用视图:对象列表视图。我们使用第五章中的Publisher来举例:
class Publisher(models.Model): name = models.CharField(maxlength=30) address = models.CharField(maxlength=50) city = models.CharField(maxlength=60) state_province = models.CharField(maxlength=30) country = models.CharField(maxlength=50) website = models.URLField() def __str__(self): return self.name class Meta: ordering = ["-name"] class Admin: pass
要为所有的书籍创建一个列表页面,我们使用下面的URL配置:
from django.conf.urls.defaults import * from django.views.generic import list_detail from mysite.books.models import Publisher publisher_info = { "queryset" : Publisher.objects.all(), } urlpatterns = patterns('', (r'^publishers/$', list_detail.object_list, publisher_info) )
这就是所要编写的所有Python代码。当然,我们还需要编写一个模板。我们可以通过在额外参数字典里包含template_name来清楚的告诉object_list视图使用哪个模板,但是由于Django在不给定模板的时候会用对象的名称推导出一个。在这个例子中,这个推导出的模板名称将是"books/publisher_list.html",其中books部分是定义这个模型的app的名称,
publisher部分是这个模型名称的小写。
这个模板将按照 context
中包含的变量object_list来渲染,这个变量包含所有的书籍对象。一个非常简单的模板看起来象下面这样:
{% extends "base.html" %} {% block content %} <h2>Publishers</h2> <ul> {% for publisher in object_list %} <li>{{ publisher.name }}</li> {% endfor %} </ul> {% endblock %}
这就是所有要做的事。要使用通用视图酷酷的特性只需要修改参数字典并传递给通用视图函数。附录D是通用视图的完全参考资料;本章接下来的章节将讲到自定义和扩展通用视图的一些方法。
扩展通用视图
毫无疑问,使用通用视图可以充分加快开发速度。然而,在多数的工程中,也会出现通用视图不能满足需求的情况。实际上,刚接触Django的开发者最常见的问题就是怎样使用通用视图来处理更多的情况。
幸运的是,几乎每种情况都有相应的方法来简单的扩展通用视图来处理它。这时总是使用下面的这些方法。
制作友好的模板Context
你也许已经注意到范例中的出版商列表模板在变量object_list里保存所有的书籍。这个方法工作的很好,只是对编写模板的人不太友好:他们不得不去了解他们现在处理的数据是什么,比方说在这里是书籍。用象publisher_list这样的变量名会更好一点,这样变量的值看起来就很清楚了。
我们可以很容易的象下面这样修改template_object_name参数的名称:
publisher_info = { "queryset" : Publisher.objects.all(), **"template_object_name" : "publisher",** } urlpatterns = patterns('', (r'^publishers/$', list_detail.object_list, publisher_info) )
使用有用的template_object_name总是个好想法。你的设计模板的合作伙伴会感谢你的。
添加额外的Context
你常常需要呈现比通用视图提供的更多的额外信息。例如,考虑一下在每个出版商页面实现所有其他出版商列表。object_detail通用视图提供了出版商到context,但是看起来没有办法在模板中获取所有出版商列表。
这是解决方法:所有的通用视图都有一个额外的可选参数extra_context。这个参数是一个字典数据类型,包含要添加到模板的context中的额外的对象。所以要提供所有的出版商明细给视图,我们就用这样的info字典:
publisher_info = { "queryset" : Publisher.objects.all(), "template_object_name" : "publisher", **"extra_context" : {"book_list" : Book.objects.all()}** }
这样就把一个{{book_list}}变量放到模板的context中。这个方法可以用来传递任意数据到通用视图模板中去,非常方便。
不过,这里有一个很隐蔽的BUG,不知道你发现了没有?
我们现在来看一下,extra_context里包含数据库查询的问题。因为在这个例子中,我们把Publisher.objects.all()放在URLconf中,它只会执行一次(当URLconf第一次加载的时候)。当你添加或删除出版商,你会发现在重启Web服务器之前,通用视图不会反映出这些修改的(有关QuerySet何时被缓存和赋值的更多信息请参考附录C中“缓存与查询集”一节)。
备注
这个问题不适用于通用视图的queryset参数。因为Django知道有些特别的 QuerySet永远不能被缓存,通用视图在渲染前都做了缓存清除工作。
解决这个问题的办法是在extra_context中用一个回调(callback)来代替使用一个变量。任何可以调用的对象(例如一个函数)在传递给extra_context后都会在每次视图渲染前执行(而不是只执行一次)。你可以象这样定义一个函数:
def get_books(): return Book.objects.all() publisher_info = { "queryset" : Publisher.objects.all(), "template_object_name" : "publisher", "extra_context" : **{"book_list" : get_books}** }
或者你可以使用另一个不是那么清晰但是很简短的方法,事实上Publisher.objects.all本身就是可以调用的:
publisher_info = { "queryset" : Publisher.objects.all(), "template_object_name" : "publisher", "extra_context" : **{"book_list" : Book.objects.all}** }
注意Book.objects.all后面没有括号;这表示这是一个函数的引用,并没有真调用它(通用视图将会在渲染时调用它)。
显示对象的子集
现在让我们来仔细看看这个queryset。大多数通用视图有一个queryset参数,这个参数告诉视图要显示对象的集合(有关QuerySet的解释请看第五章的
“选择对象”章节,详细资料请参看附录C)。
举一个简单的例子,我们打算对书籍列表按出版日期排序,最近的排在最前:
book_info = { "queryset" : Book.objects.all().order_by("-publication_date"), } urlpatterns = patterns('', (r'^publishers/$', list_detail.object_list, publisher_info), **(r'^books/$', list_detail.object_list, book_info),** )
这是一个相当简单的例子,但是很说明问题。当然,你通常还想做比重新排序更多的事。如果你想要呈现某个特定出版商出版的所有书籍列表,你可以使用同样的技术:
**apress_books = {** **"queryset": Book.objects.filter(publisher__name="Apress Publishing"),** **"template_name" : "books/apress_list.html"** **}** urlpatterns = patterns('', (r'^publishers/$', list_detail.object_list, publisher_info), **(r'^books/apress/$', list_detail.object_list, apress_books),** )
注意在使用一个过滤的queryset的同时,我们还使用一个自定义的模板名称。如果我们不这么做,通用视图就会用以前的模板,这可能不是我们想要的结果。
同样要注意的是这并不是一个处理出版商相关书籍的最好方法。如果我们想要添加另一个出版商页面,我们就得在URL配置中写URL配置,如果有很多的出版商,这个方法就不能接受了。在接下来的章节我们将来解决这个问题。
备注
如果你在请求/books/apres/时出现404错误,请检查以确保你的数据库中出版商中有名为Apress
Publishing的记录。通用视图有一个allow_empty参数可以用来处理这个情况,详情请看附录D。
用函数包装来处理复杂的数据过滤
另一个常见的需求是按URL里的关键字来过滤数据对象。在前面我们用在URL配置中硬编码出版商名称的方法来做这个,但是我们想要用一个视图就能显示某个出版商的所有书籍该怎么办呢?我们可以通过对object_list通用视图进行包装来避免写一大堆的手工代码。按惯例,我们先从写URL配置开始:
urlpatterns = patterns('', (r'^publishers/$', list_detail.object_list, publisher_info), **(r'^books/(w+)/$', books_by_publisher),** )
接下来,我们写books_by_publisher这个视图:(上面的代码中正则表达式有误,在 ‘w’前要加反斜杠)
from django.http import Http404 from django.views.generic import list_detail from mysite.books.models import Book, Publisher def books_by_publisher(request, name): # Look up the publisher (and raise a 404 if it can't be found). try: publisher = Publisher.objects.get(name__iexact=name) except Publisher.DoesNotExist: raise Http404 # Use the object_list view for the heavy lifting. return list_detail.object_list( request, queryset = Book.objects.filter(publisher=publisher), template_name = "books/books_by_publisher.html", template_object_name = "books", extra_context = {"publisher" : publisher} )
这是因为通用视图就是Python函数。和其他的视图函数一样,通用视图也是接受一些参数并返回HttpResponse对象。因此,通过包装通用视图函数可以做更多的事。
注意
注意到在前面这个例子中我们在extra_context传递了当前出版商这个参数。这在包装时通常是一个好注意;它让模板知道当前显示内容的上一层对象。
处理额外工作
我们再来看看最后一个常用模式:在调用通用视图前后做些额外工作。
想象一下我们在Author对象里有一个last_accessed字段,我们用这个字段来更正对author的最近访问时间。当然通用视图object_detail并不能处理这个问题,我们可以很容易的写一个自定义的视图来更新这个字段。
首先,我们需要在URL配置里设置指向到新的自定义视图:
from mysite.books.views import author_detail urlpatterns = patterns('', #... **(r'^authors/(?P<author_id>d+)/$', author_detail),** )
接下来写包装函数:
import datetime from mysite.books.models import Author from django.views.generic import list_detail from django.shortcuts import get_object_or_404 def author_detail(request, author_id): # Look up the Author (and raise a 404 if she's not found) author = get_object_or_404(Author, pk=author_id) # Record the last accessed date author.last_accessed = datetime.datetime.now() author.save() # Show the detail page return list_detail.object_detail( request, queryset = Author.objects.all(), object_id = author_id, )
注意
除非你添加last_accessed字段到你的Author模型并创建books/author_detail.html模板,否则这段代码不能真正工作。
我们可以用同样的方法修改通用视图的返回值。如果我们想要提供一个供下载用的纯文本版本的author列表,我们可以用下面这个视图:
def author_list_plaintext(request): response = list_detail.object_list( request, queryset = Author.objects.all(), mimetype = "text/plain", template_name = "books/author_list.txt" ) response["Content-Disposition"] = "attachment; filename=authors.txt" return response
这个方法之所以工作是因为通用视图返回的HttpResponse对象可以象一个字典一样的设置HTTP的头部。随便说一下,这个Content-Disposition的含义是告诉浏览器下载并保存这个页面,而不是在浏览器中显示它。
以上就是Django通用视图讲解的内容,更多相关内容请关注PHP中文网(www.php.cn)!