使用通用視圖
使用通用視圖的方法是在URLconf文件中創建配置字典,然后把這些字典作為URLconf元組的第三個成員。 (對於這個技巧的應用可以參看第八章向視圖傳遞額外選項。)
例如,下面是一個呈現靜態“關於”頁面的URLconf:
from django.conf.urls.defaults import *
from django.views.generic.simple import direct_to_template
urlpatterns = patterns('',
(r'^about/$', direct_to_template, {
'template': 'about.html'
})
)
一眼看上去似乎有點不可思議,不需要編寫代碼的視圖! 它和第八章中的例子完全一樣:direct_to_template視圖僅僅是直接從傳遞過來的額外參數獲取信息並用於渲染視圖。
因為通用視圖都是標准的視圖函數,我們可以在我們自己的視圖中重用它。 例如,我們擴展 about例子,把映射的URL從 /about//修改到一個靜態渲染 about/.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('',
(r'^about/$', direct_to_template, {
'template': 'about.html'
}),
**(r'^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對象,我們只需要簡單的返回它就好了。 這里唯一有點棘手的事情是要處理找不到模板的情況。 我們不希望一個不存在的模板導致一個服務端錯誤,所以我們捕獲TemplateDoesNotExist異常並且返回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(max_length=30)
address = models.CharField(max_length=50)
city = models.CharField(max_length=60)
state_province = models.CharField(max_length=30)
country = models.CharField(max_length=50)
website = models.URLField()
def __unicode__(self):
return self.name
class Meta:
ordering = ['name']
要為所有的出版商創建一個列表頁面,我們使用下面的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視圖使用哪個模板:
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(),
**'template_name': 'publisher_list_page.html',**
}
urlpatterns = patterns('',
(r'^publishers/$', list_detail.object_list, publisher_info)
)
在缺少template_name的情況下,object_list通用視圖將自動使用一個對象名稱。 在這個例子中,這個推導出的模板名稱將是 "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 %}
(注意,這里我們假定存在一個base.html模板。)
這就是所有要做的事。 要使用通用視圖酷酷的特性只需要修改參數字典並傳遞給通用視圖函數。
擴展通用視圖
毫無疑問,使用通用視圖可以充分加快開發速度。 然而,在多數的工程中,也會出現通用視圖不能 滿足需求的情況。 實際上,剛接觸Django的開發者最常見的問題就是怎樣使用通用視圖來處理更多的情況。
幸運的是,幾乎每種情況都有相應的方法來簡易地擴展通用視圖以處理這些情況。 這時總是使用下面的 這些方法。
制作友好的模板Context
你也許已經注意到范例中的出版商列表模板在變量 object_list 里保存所有的書籍。這個方法工作的很好,只是對編寫模板的人不太友好。 他們必須知道這里正在處理的是書籍。 更好的變量名應該是publisher_list,這樣變量所代表的內容就顯而易見了。1
我們可以很容易地像下面這樣修改 template_object_name 參數的名稱:
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(),
'template_name': 'publisher_list_page.html',
'template_object_name': 'publisher',
}
urlpatterns = patterns('',
(r'^publishers/$', list_detail.object_list, publisher_info)
)
在模板中,通用視圖會通過在template_object_name后追加一個_list的方式來創建一個表示列表項目的變量名。
使用有用的 template_object_name 總是個好想法。
添加額外的Context1
你常常需要呈現比通用視圖提供的更多的額外信息。 例如,考慮一下在每個出版商的詳細頁面顯示所有其他出版商列表。 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的解釋請看第五章的 “選擇對象”章節,詳細資料請參看附錄B)。
舉一個簡單的例子,我們打算對書籍列表按出版日期排序,最近的排在最前:
book_info = {
'queryset': Book.objects.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配置,如果有很多的出版商,這個方法就不能接受了。
用函數包裝來處理復雜的數據過濾
另一個常見的需求是按URL里的關鍵字來過濾數據對象。 之前,我們在URLconf中硬編碼了出版商的名字,但是如果我們想用一個視圖就顯示某個任意指定的出版商的所有書籍,那該怎么辦呢? 我們可以通過對 object_list 通用視圖進行包裝來避免 寫一大堆的手工代碼。 按慣例,我們先從寫URL配置開始:
urlpatterns = patterns('',
(r'^publishers/$', list_detail.object_list, publisher_info),
**(r'^books/(\w+)/$', books_by_publisher),**
)
接下來,我們寫 books_by_publisher 這個視圖:
from django.shortcuts import get_object_or_404
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).
publisher = get_object_or_404(Publisher, name__iexact=name)
# 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 = 'book',
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 django.shortcuts import get_object_or_404
from django.views.generic import list_detail
from mysite.books.models import Author
def author_detail(request, author_id):
# Delegate to the generic view and get an HttpResponse.
response = list_detail.object_detail(
request,
queryset = Author.objects.all(),
object_id = author_id,
)
# Record the last accessed date. We do this *after* the call
# to object_detail(), not before it, so that this won't be called
# unless the Author actually exists. (If the author doesn't exist,
# object_detail() will raise Http404, and we won't reach this point.)
now = datetime.datetime.now()
Author.objects.filter(id=author_id).update(last_accessed=now)
return response
注意
除非你添加 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 的含義是 告訴瀏覽器下載並保存這個頁面,而不是在瀏覽器中顯示它。
推薦: