本书所讲的是Django:一个可以使
Web开发工作愉快并且高效的Web开
发框架。 使用Django,使你能够以最
小的代价构建和维护高质量的Web应
用。
从好的方面来看,Web 开发激动人心
且富于创造性;从另一面来看,它却
是份繁琐而令人生厌的工作。 通过
减少重复的代码,Django 使你能够专
注于 We b 应用上有 趣的关键性的东
西。 为了达到这个目标,Django 提
供了通用Web开发模式的高度抽象,
提供了频繁进行的编程作业的快速解
决方法,以及为“如何解决问题”提供
了清晰明了的约定。 同时,Django
尝试留下一些方法,来让你根据需要
在framew ork之外来开发。
本书的目的是将你培养成Django专
家。 主要侧重于两方面: 第一,我
们深度解释 Django 到底做了哪些工
作以及如何用她构建Web应用;第
二,我们将会在适当的地方讨论更高
级的概念,并解释如何 在自己的项
目中高效的使用这些工具。 通过阅
读此书,你将学会快速开发功能强大
网站的技巧,并且你的代码将会十分
清晰,易于维护。 本书的代码清
晰,易维护,通过学习,可以快速开
发功能强大的网站。
框架是什麼?
Django 在新一代的 Web框架 中非常
出色,为什么这么说呢?
为回答该问题,让我们考虑一下_不
使用_框架设计 Python 网页应用程序
的情形。 贯穿整本书,我们多次展
示不使用框架实现网站基本功能的方
法,让读者认识到框架开发的方便。
(不使用框架,更多情况是没有合适
的框架可用。 最重要的是,理解实
现的来龙去脉会使你成为一个优秀的
web开发者。)
使用Python开发Web,最简单,原始
和直接的办法是使用CGI标准,在
1998年这种方式很流行。 现在从应
用角度解释它是如何工作: 首先做
一个Python脚本,输出HTML代码,
然后保存成.cgi扩展名的文件,通过
浏览器访问此文件。 就是这样。
如下示例,用Python CGI脚本显示数
据库中最新出版的10本书: 不用关
心语法细节;仅仅感觉一下基本实现
的方法:
#
!/usr/bin/env python
import MySQLdb
print "Content-Type: text/html\n"
print "<html><head><title>Books</tit
le></head>"
print "<body>"
print "<h1>Books</h1>"
print "<ul>"
connection = MySQLdb.connect(user='m
e', passwd='letmein', db='my_db')
cursor = connection.cursor()
cursor.execute("SELECT name FROM boo
ks ORDER BY pub_date DESC LIMIT 10")
for row in cursor.fetchall():
print "<li>%s</li>" % row[0]
print "</ul>"
print "</body></html>"
connection.close()
首先,用户请求CGI,脚本代码打印
Content- Type行,后面跟着换行。 再
接下 来是一些HTML的起始标签,然
后连接数据库并执行一些查询操作,
获取最新的十本书。 在遍历这些书
的同时,生成一个书名的HTML列表
项。 最后,输出HTML的结束标签并
且关闭数据库连接。
像这样的一次性的动态页面,从头写
起的方法并非一定不好。 其中一
点: 这些代码简单易懂,就算是一
个初起步的 开发者都能读明白这16
行的Python的代码,而且这些代码从
头到尾做了什么都能了解得一清二
楚。 不需要学习额外 的背景知识,
没有额外的代码需要去了解。 同
样,也易于部署这16行代码,只需要
将它保存为一个latestbooks.cgi 的 文
件,上传到网络服务器上,通过浏览
器访问即可。
尽管实现很简单,还是暴露了一些问
题和不便的地方。 问你自己这几个
问题:
应用中有多处需要连接数据库会
怎样呢? 每个独立的CGI脚本,
不应该重复写数据库连接的代
码。 比较实用的办法是写一个共
享函数,可被多个代码调用。
一个开发人员 确实 需要去关注如
何输出Content- Type以及完成所有
操作后去关闭数据 库么? 此类问
题只会降低开发人员的工作效
率,增加犯错误的几率。 那些初
始化和释放 相关的工作应该交给
一些通用的框架来完成。
如果这样的代码被重用到一个复
合的环境中会发生什么? 每个页
面都分别对应独立的数据库和密
码吗?
如果一个Web设计师,完全没有
Python开发经验,但是又需要重新
设计页面的话,又将 发生什么
呢? 一个字符写错了,可能导致
整个应用崩溃。 理想的情况是,
页面显示的逻辑与从数据库中读
取书本记录分隔开,这样 Web设
计师的重新设计不会影响到之前
的业务逻辑。
以上正是Web框架致力于解决的问
题。 Web框架为应用程序提供了一套
程序框架, 这样你可以专注于编写
清晰、易维护的代码,而无需从头做
起。 简单来说,这就是Django所能做
的。
MVC 设计模式
让我们来研究一个简单的例子,通过
该实例,你可以分辨出,通过Web框
架来实现的功能与之前的方式有何不
同。 下面就是通过使用Django来完成
以上功能的例子: 首先,我们分成4
个Python的文件,
(models.py ,views.py , urls.py ) 和html
模板文件 (latest_books.html )
#
models.py (the database tables)
from django.db import models
class Book(models.Model):
name = models.CharField(max_leng
th=50)
pub_date = models.DateField()
#
views.py (the business logic)
from django.shortcuts import render_
to_response
from models import Book
def latest_books(request):
book_list = Book.objects.order_b
y('-pub_date')[:10]
return render_to_response('lates
t_books.html', {'book_list': book_li
st})
#
urls.py (the URL configuration)
from django.conf.urls.defaults impor
t *
import views
urlpatterns = patterns('',
(r'^latest/$', views.latest_book
s),
)
#
<
latest_books.html (the template)
html><head><title>Books</title></he
ad>
<
<
<
{
<
{
<
<
body>
h1>Books</h1>
ul>
% for book in book_list %}
li>{{ book.name }}</li>
% endfor %}
/ul>
/body></html>
然后,不用关心语法细节;只要用心
感觉整体的设计。 这里只关注分割
后的几个文件:
models.py 文件主要用一个 Python
类来描述数据表。 称为 模型
(model) 。 运用这个类,你可以通
过简单的 Python 的代码来创建、
检索、更新、删除 数据库中的记
录而无需写一条又一条的SQL语
句。
views.py文件包含了页面的业务逻
辑。 latest_books()函数叫做视
图。
urls.py 指出了什么样的 URL调用
什么的视图。 在这个例子
中 /latest/ URL将会调
用 latest_books()这个函数。 换句
话说,如果你的域名是
exampl e.com,任何人浏览网
址http://example.com/latest/将会调
用latest_books()这个函数。
latest_books.html 是 html 模板,它
描述了这个页面的设计是如何
的。 使用带基本逻辑声明的模板
语言,如
{
% for book in book_list %}
结合起来,这些部分松散遵循的模式
称为模型-视图-控制器( MVC)。 简单
的说, MVC 是一种软件开发的方
法,它把代码的定义和数据访问的方
法(模型)与请求逻辑 (控制器)
还有用户接口(视图)分开来。 我
们将在第5章更深入地讨论MVC。
这种设计模式关键的优势在于各种组
件都是 松散结合 的。这样,每个由
Django驱动 的Web应用都有着明确的
目的,并且可独立更改而不影响到其
它的部分。 比如,开发者 更改一个
应用程序中的 URL而不用影响到这
个程序底层的实现。 设计师可以改
变 HTML页面 的样式而不用接触
Python 代码。 数据库管理员可以重新
命名数据表并且只需更改一个地方,
无需从一大堆文件中进行查找和替
换。
本书中,每个组件都有它自己的一个
章节。 比如,第三章涵盖了视图,
第四章是模板, 而第五章是模型。
Django 历史
在我们讨论代码之前我们需要先了解
一下 Django 的历史。 从上面我们注
意到:我们将向你展示如何不使用捷
径来完成工作,以便能更好的理解捷
径的原理 同样,理解Django产生的背
景,历史有助于理解Django的实现方
式。
如果你曾编写过网络应用程序。 那
么你很有可能熟悉之前我们的 CGI 例
子。
1
. 从头开始编写网络应用程序。
. 从头编写另一个网络应用程序。
2
3. 从第一步中总结(找出其中通用
的代码),并运用在第二步中。
4
. 重构代码使得能在第 2 个程序中
使用第 1 个程序中的通用代码。
5. 重复 2-4 步骤若干次。
6. 意识到你发明了一个框架。
这正是为什么 Django 建立的原因!
Django 是从真实世界的应用中成长起
来的,它是由 堪萨斯(Kansas)州
Lawrence 城中的一个 网络开发小组
编写的。 它诞生于 2003 年秋天,那
时 Lawrence Journal-World 报纸的 程
序员 Adrian Holovaty 和 Si mon
Willison 开始用 Python 来编写程序。
当时他们的 World Online 小组制作并
维护当地的几个新闻站点, 并在以新
闻界特有的快节奏开发环境中逐渐发
展。 这些站点包括有 LJWorld.com、
Lawrence.com 和 KUsports.com, 记
者(或管理层) 要求增加的特征或
整个程序都能在计划时间内快速的被
建立,这些时间通常只有几天 或几
个小时。 因此,Adrian 和 Si mon 开
发了一种节省时间的网络程序开发框
架, 这是在截止时间前能完成程序
的唯一途径。
2
005 年的夏天,当这个框架开发完
成时,它已经用来制作了很多个
World Online 的站点。 当时 World
Online 小组中的 Jacob Kaplan-Moss
决定把这个框架发布为一个开源软
件。
从今往后数年,Django是一个有着数
以万计的用户和贡献者,在世界广泛
传播的完善开源项目。 原来的World
Online的两个开发者(Adrian and
Jacob)仍然掌握着Django,但是其发
展方向受社区团队的影响更大。
这些历史都是相关联的,因为她们帮
助解释了很重要的两点。
第一,Django最可爱的地方。 Django
诞生于新闻网站的环境中,因此它提
供很多了特性(如第6章会说到的管
理后台),非常适合内容类的网站,
如Amazon.com, craigslist.org和
washingtonpost.com,这些网站提供动
态的,数据库驱动的信息。 (不要
看到这就感到沮丧,尽管Django擅长
于动态内容管理系统, 但并不表示
Django主要的目的就是用来创建动态
内容的网站。 某些方面 特别高效 与
其他方面 不高效 是有区别的, Django
在其他方面也同样高效。)
第二,Django的起源造就了它的开源
社区的文化。 因为Django来自于真实
世界中的代码,而不是 来自于一个
科研项目或者商业产品,她主要集中
力量来解决Web开发中遇到的问题,
同样 也是Django的开发者经常遇到的
问题。 这样,Django每天在现有的基
础上进步。 框架的开发者对于让开
发人员节省时间,编写更加容易维护
的程序,同时保证程序运行的效率具
有极大的兴趣。 无他,开发者动力
来源于自己的目标:节省时间,快乐
工作。 (坦率地讲,他们使用了自
己公司的产品。)
如何阅读本书
在编写本书时,我们努力尝试在可读
性和参考性间做一个平衡,当然本书
会偏向于可 读性。 本书的目标,之
前也提过,是要将你培养成一名
Django专家,我们相信,最好 的方式
就是提供文章和充足的实例,而不是
一堆详尽却乏味的关于Django特色的
手册。 (曾经有人说过,如果仅仅
教字母表是无法教会别人说话的。
按照这种思路,我们推荐按顺序阅读
第 1-12 章。 这些章节构成了如何使
用 Django 的基础;读过之后,你就
可以搭建由 Django 支撑的网站了。
1-7章是核心课程,8-11章讲述Django
的高级应用,12章讲述部署相关的知
识。 剩下的13-20章,讲述Django特
有的特点,可以任意顺序阅读。
附录部分用作参考资料。 要回忆语
法或查阅 Django 某部分的功能概要
时,你偶尔可能会回来翻翻这些资料
以及 http://www.djangoproject.com/ 上
的免费文档。
所需编程知识
本书读者需要理解基本的面向过程和
面向对象编程: 流程控制
(
if , while 和 for ),数据结构
列表,哈希表/字典),变量,类
(
和对象。
Web开发经验,正如你所想的,也是
非常有帮助的,但是对于阅读本书,
并不是必须的。 通过本书,我们尽
量给缺乏经验的开发人员提供在Web
开发中最好的实践。
Python所需知识
本质上来说, Django 只不过是用
Python 编写的一组类库。 用 Django
开发站点就是使用这些类库编写
Python 代码。 因此,学习 Django 的
关键就是学习如何进行 Python 编程并
理解 Django 类库的运作方式。
如果你有Python开发经验,在学习过
程中应该不会有任何问题。 基本
上,Django的代码并 没有使用一些黑
色魔法(例如代码中的花哨技巧,某
个实现解释或者理解起来十分困
难)。 对你来说,学习Django就是学
习她的命名规则和API。
如果你没有使用 Python 编程的经验,
你一定会学到很多东西。 它是非常
易学易用的。 虽然这本书没有包括
一个完整的 Python 教程, 但也算是
一个恰当的介绍了 Python特征和 功能
的集锦。 当然,我们推荐你读一下
官方的 Python 教程,它可 以
从 http://docs.python.org/tut/ 在线获
得。 另外我们也推荐 Mark Pilgrims
的 书Dive Into
Python ( http://www.diveintopython.or
g/ )
Django版本支持
此书内容对Django 1.1兼容。
Django的开发者保证主要版本号向后
兼容。 这意味着,你用Django 1.1写
的应用,可以用于1.2,1.3,1.9等所
有以1开头的版本
如果Django到了2.0,你的应用可能不
再兼容,需要重写,但是,2.0是很
遥远的事情。 对此,可以参考一下
1
.0的开发周期,整整3年的时间。
这与Python语言的兼容策略非常
(
像: 在python 2.0下写的代码可以在
python 2.6下运行,但不一定能在
python3.0下运行
所以,此书覆盖1.1版本,可以使用
很长时间。
获取帮助
Django的最大的益处是,有一群乐于助
人的人在Django社区上。 你可以毫无
约束的提各种 问题在上面,如:django
的安装,app 设计,db 设计,发布。
Django邮件列表是很多Django用户
提出问题、回答问题的地方。 可
以通过
http://www.djangoproject.com/r/dja
ngo-users 来免费注册。
如果Django用户遇到棘手的问题,
希望得到及时地回复,可以使用
Django IRC channel。 在Freenode
IRC network加入#django
由于现代Web开发环境由多个部件组
成,安装Django需要几个步骤。 这一
章,我们将演示如何安装框架以及一
些依赖关系。
因为Django就是纯Python代码,它可
以运行在任何Python可以运行的环
境,甚至是手机上! 但是这章只提
及Django安装的通用脚本。 我们假设
你把它安装在桌面/笔记本电脑或服
务器。
往后,在第12章,我们将讨论如何部
署Django到一个生产站点。
Python 安装
Django本身是纯Python编写的,所以
安装框架的第一步是确保你已经安装
了Python。
Python版本
核心Django框架可以工作在2.3至
2.6(包括2.3和2.6)之间的任何
Python版本。 Django的可选GIS(地
理信息系统)支持需要Python 2.4到
2.6。
如果你不确定要安装Python的什么版
本,并且你完全拿不定主意的话,那
就选2.x系列的最新版本吧。 版本
2.6。 虽然Django在2.3至2.6版之间的
任意Python版本下都一样运行得很
好,但是新版本的Python提供了一些
你可能比较想应用在你的程序里的,
更加丰富和额外的语言特性。 另
外,某些你可能要用到的Django第三
方插件会要求比Python 2.3更新的版
本,所以使用比较新的Python版本会
让你有更多选择。
Django和 Python 3.0
在写作本书的时候,Python3.0已经发
布,但Django暂时还不支持。
Python3.0这个语言本身引入了大量不
向后兼容的改变,因此,我们预期大
多数主要的Python库和框架将花几年
才能衔接,包括Django。
如果你是个Python新手并且正迷茫于
到底是学习Python 2.x还是Python 3.x
的话,我们建议你选择Python 2.x。
安装
如果使用的是 Linux 或 Mac OS X ,
系统可能已经预装了 Python 。在命令
提示符下 (或 OS X 的终端中) 输入
python ,如果看到如下信息,说明
Python 已经装好了: 在命令行窗口中
输入python (或是在OS X的程序/工
具/终端中)。 如果你看到这样的信
息,说明 python 已经安装好了.
Python 2.4.1 (#2, Mar 31 2005, 00:05
:
[
10)
GCC 3.3 20030304 (Apple Computer, I
nc. build 1666)] on darwin
Type "help", "copyright", "credits"
or "license" for more information.
>>>
否则, 你需要下载并安装Python. 它既
快速又方便,而详细说明可参
考http://www.python.org/download/
安装 Django
任何时候,都有两个不同版本的
Django供您选择。 最新的官方发行版
和有风险的主干版本。 安装的版本
取决于您的优先选择。 你需要一个
稳定的通过测试的Django,或是你想
要包括最新功能的版本,也许你可对
Django本身作贡献,而把稳定作为代
价?
我们推荐选定一个正式发布版本,但
重要的是了解到主干开发版本的存
在,因为在文档和社区成员中你会发
现它被提到。
安装官方发布版
官方发布的版本带有一个版本号,例
如1.0.3或1.1,而最新版本总是可以
在http://www.djangoproject.com/downl
oad/找到。
如果您在用Linux系统,其中包括
Django的包,使用默认的版本是个好
主意。 这样,你将会通过系统的包
管理得到安全的升级。
如果你的系统没有自带Django,你可
以自己下载然后安装框架。 首先,
下载名字类似于Django-1.0.2-
final.tar.gz压缩文件。(下载到哪里无
所谓,安装程序会把Django文件放到
正确的地方。)解压缩之后运行
setup.py install,像操作大多数Python
库一样。
以下是如何在Uni x系统上安装的方
法:
1
2
3
. tar xzvf Django-*.tar.gz 。
. cd Django-* 。
. sudo python setup.py install 。
Windows系统上,推荐使用7-
Zip(http://www.djangoproject.com/r/7zi
p/)来解压缩.tar.gz文件。 解压缩完成
后,以管理员权限启动一个DOS
Shell(命令提示符),然后在名字以
Django-开始的目录里执行如下命
令:
python setup.py install
如果你很好奇: Django将被安装到你
的Python安装目录 的site-package
目录(Python从该目录寻找第三方
库)。 通常情况下,这个目录
在/usr/lib/python2.4/site-packages。
安装Trunk版本
最新最好的django的开发版本称为
trunk,可以从django的subversion处获
得。 如果你想尝鲜,或者想为django
贡献代码,那么你应当安装这个版
本。
Subversion 是一种与 CVS 类似的免费
开源版本控制系统,Django 开发团队
使用它管理 Django 代码库的更新。
你可以使用 Subversion 客户端获取最
新的 Django 源代码,并可任何时候
使用 local checkout 更新本地 Django
代码的版本,以获取 Django 开发者
所做的最近更新和改进。
请记住,即使是使用trunk版本,也是
有保障的。 因为很多django的开发者
在正式网站上就是用的trunk版本,他
们会保证trunk版本的稳定性。
遵循以下步骤以获取最新的 Django
主流代码:
确保安装了 Subversion 客户端。
可以
从 http://subversion.tigris.org/ 免费
下载该软件,并从
http://svnbook.red-bean.com/ 获取出
色的文档。
(如果你在使用Mac OS X 10.5或
者更新的版本,你很走运,
Subversion应该就可以安装
Django。 你可以在终端上输入
svn --version来验证。
使
用 svn co http://code.djangoproject.c
om/svn/django/trunk djtrunk 命令查
看主体代码。
找到你的python的site-packages目
录。 一般为/usr/lib/python2.4/site-
packages,如果你不确定,可以输
入如下命令:
python -c 'import sys, pprint; pprin
t.pprint(sys.path)'
上面的结果会包含site-packages的
目录
在site-packages目录下,创建一个
文件
django.pth,编辑这个文件,包
含djtrunk目录的全路径 利润,
此文件包含如下行:
/
home/me/code/djtrunk
1
. 将 djtrunk/django/bin 加入系统变量
PAT H 中。该目录中包含一些
像 django-admin.py 之类的管理工
具。 此目录包含管理工具,例
如:django-admin.py
提示:
如果之前没有接触过 .pth 文件,你可
以
从 http://www.djangoproject.com/r/pyth
on/site-module/ 中获取更多相关知
识。
从 Subversion 完成下载并执行了前述
步骤后,就没有必要再执行
python setup.py install 了,你
刚才已经手动完成了安装!
由于 Django 主干代码的更新经常包
括 bug 修正和特性添加,如果真的着
迷的话,你可能每隔一小段时间就想
更新一次。 在 djtrunk 目录下运
行 svn update 命令即可进行更新。 当
你使用这个命令时,Subversion 会联
络http://code.djangoproject.com ,判断
代码是否有更新,然后把上次更新以
来的所有变动应用到本地代码。 就
这么简单。
最后,如果你使用trunk,你要知道使
用的是哪个trunk版本。 如果你去社
区寻求帮助,或是为Django框架提供
改进,知道你使用的版本号是非常重
要的。 因此,当你到社区去求助,
或者为 django 提供改进意见的时候,
请时刻记住说明你正在使用的 django
的版本号。 如何知道你正在使用的
django 的版本号呢?进入 djtrunk
目录,然后键入 svn info ,在输出信
息中查看 Revision: (版本:) 后跟的数
字。 Django在每次更新后,版本号都
是递增的,无论是修复Bug、增加特
性、改进文档或者是其他。 在一些
Django社区中,版本号甚至成为了一
种荣誉的象征,我从[写上非常低的
版本号]开始就已经使用Djano了。
测试Django安装
让我们花点时间去测试 Django 是否
安装成功,并工作良好。同时也可以
了解到一些明确的安装后的反馈信
息。 在Shell中,更换到另外一个目
录(不是包含Django的目录),然后
输入python来打开Python的交互解释
器。如果安装成功,你应该可以导入
django模块了:
>
>> import django
>
>> django.VERSION
(1, 1, 0, final', 1)
交互解释器示例
Python 交互解释器是命令行窗口的程
序,通过它可以交互式地编写 Python
程序。 要启动它只需运行 python命
令。
我们在交互解释器中演示Python示例
将贯穿整本书。 你可以用三个大于
号 (>>> )来分辨出示例,三个大于号
就表示交互提示符。 如果你要从本
书中拷贝示例,请不要拷贝提示符。
在交互式解释器中,多行声明用三个
点 (...)来填补。 例如:
>
.
.
>> print """This is a
.. string that spans
.. three lines."""
This is a
string that spans
three lines.
>
.
>
>> def my_function(value):
.. print value
>> my_function('hello')
hello
这三个在新行开始插入的点,是
Python Shell自行加入的,不属于我们
的输入。 但是包含它们是为了追求
解释器的正确输出。 如果你拷贝我
们的示例去运行,千万别拷贝这些
点。
安装数据库
这会儿,你可以使用django写web应
用了,因为django只要求python正确
安装后就可以跑起来了。 不过,当
你想开发一个_数据库驱动_的web站
点时,你应当需要配置一个数据库服
务器。
如果你只想玩一下,可以不配置数据
库,直接跳到 开始一个project 部分
去,不过你要注意本书的例子都是假
设你配置好了一个正常工作的数据
库。
Django支持四种数据库:
PostgreSQL
SQLite 3 (http://www.sqlite.org/)
MySQL (http://www.mysql.com/)
Oracle (http://www.oracle.com/)
大部分情况下,这四种数据库都会和
Django框架很好的工作。 (一个值得
注意的例外是Django的可选GIS支
持,它为PostgreSQL提供了强大的功
能。)如果你不准备使用一些老旧系
统,而且可以自由的选择数据库后
端,我们推荐你使用PostgreSQL,它
在成本、特性、速度和稳定性方面都
做的比较平衡。
设置数据库只需要两步:
首先,你需要安装和配置数据库
服务器本身。 这个过程超出了本
书的内容,不过这四种数据库后
端在它的网站上都有丰富的文档
说明。 如果你使用的是共享主
机,可能它们已经为你设置好
了。
其次,你需要为你的服务器后端
安装必要的Python库。 这是一些
允许Python连接数据库的第三方代
码。 我们会在之后的章节简要介
绍,对于某一种数据库来说,它
单独需要安装的东西。
如果你只是玩一下,不想安装数据库
服务,那么可以考虑使用SQLite。 如
果你用python2.5或更高版本的话,
SQLite是唯一一个被支持的且不需要
以上安装步骤的数据库。 它仅对你
的文件系统中的单一文件读写数据,
并且Python2.5和以后版本内建了对它
的支持。
在Windows上,取得数据库驱动程序
可能会令人沮丧。 如果你急着用
它,我们建议你使用python2.5。
在 Django 中使用
PostgreSQL
使用 PostgreSQL的话,你需要
从 http://www.djangoproject.com/r/pyth
on-pgsql/ 下载 psycopg 这个开发包。
我们建议使用psycopg2,因为它是新
留意你所用的是 版本 1 还是 2,稍后
你会需要这项信息。
如果在 Window s 平台上使用
PostgreSQL,可以
从 http://www.djangoproject.com/r/pyth
on-pgsql/windows/ 获取预编译
的 psycopg 开发包的二进制文件。
如果你在用Linux,检查你的发行版
的软件包管理系统是否提供了一套叫
做python-psycopg2,psycopg2-
python,python-postgresql这类名字的
包。
在 Django 中使用 SQLite 3
如果你正在使用Python 2.5版本或者更
高,那么你很幸运: 不要求安装特
定的数据库,因为Python支持和
SQLite进行通信。 向前跳到下一节。
如果你用的是Python2.4或更早的版
本,你需要 SQLite 3_而不是_版本
2
,这个可从
http://www.djangoproject.com/r/sqlite/ p
ysqlitehttp://www.djangoproject.com/r/
python-sqlite/ 确认一下你的pysqlite版
本是2.0.3或者更高。
在 Window s 平台上,可以跳过单独
的 SQLite 二进制包安装工作,因为
它们已被静态链接到 pysqlite 二进制
开发包中。
如果你在用Linux,检查你的发行版
的软件包管理系统是否提供了一套叫
做python-sqlite3,sqlite-python,
pysqlite这类名字的包。
在 Django 中使用 MySQL
django要求MySQL4.0或更高的版本。
3
.X 版本不支持嵌套子查询和一些其
它相当标准的SQL语句。
你还需要
从 http://www.djangoproject.com/r/pyth
on-mysql / 下载安装 MySQLdb 。
如果你正在使用Linux,检查下你系
统的包管理器是否提供了叫做python-
mysql,python-mysqldb,myspl-python或
者相似的包。
在Django中使用Oracle数据
库
django需要Oracle9i或更高版本。
如果你用Oracle,你需要安装
cx_Oracle库,可以从http://cx-
oracle.sourceforge.net/获得。 要用
4
.3.1或更高版本,但要避开5.0,这
是因为这个版本的驱动有bug。
使用无数据库支持的
Django
正如之前提及过的,Django 并不是非
得要数据库才可以运行。 如果只用
它提供一些不涉及数据库的动态页面
服务,也同样可以完美运行。
尽管如此,还是要记住:
Django 所捆绑的一些附加工具 一
定 需要数据库,因此如果选择不
使用数据库,你将不能使用那些
功能。 (我们将在本书中自始至终
强调这些功能)
开始一个项目
一但你安装好了python,django和
(可选的)数据库及相关库,你就可
以通过创建一个project,迈出开发
django应用的第一步。
项目 是 Django 实例的一系列设置的
集合,它包括数据库配置、Django 特
定选项以及应用程序的特定设置。
如果第一次使用 Django,必须进行一
些初始化设置工作。 新建一个工作
目录,例如 /home/username/djcode/,
然后进入该目录。
这个目录应该放哪儿?
有过 PHP 编程背景的话,你可能习
惯于将代码都放在 Web 服务器的文
档根目录 (例如 /var/www 这样的地
方)。 而在 Django 中,把任何Python
代码和web server的文档根(root)放在
一起并不是一个好主意。因为这样做
有使人能通过网路看到你原代码的风
险. 那就太糟了。
把代码放置在文档根目录 之外 的某
些目录中。
转到你创建的目录,运行命令django-
admin.py startproject mysi te。这样会在
你的当前目录下创建一个目录。
mysi te
注意
如果用的是 setup.py 工具安装的
Django , django-admin.py 应该已
被加入了系统路径中。
如果你使用一个trunk版本,你会
在 djtrunk/django/bin 下发现 django-
admin.py 。你将来会常用到django-
admin.py,考虑把它加到你的系统路
径中去比较好。 在Uni x中, 你也可以
用来自/usr/local/bin 的符号连接, 用一
个命令, 诸如sudo ln -
s /path/to/django/bin/django-
admin.py /usr/local/bin/django-
admin.py . 在Windows中, 你需要修改
你的 PAT H 环境变量.
如果你的django是从l i nux发行版中安
装的,那么,常会被django-admin.py
替代。django-admin
如果在运行时,你看到权限拒绝的提
示,你应当修改这个文件的权限。
django-admin.py startproject 为
此, 键入 cd /usr/local/bin转到django-
admin.py所在的目录,运行命令
chmod +x django-admin.py
startproject 命令创建一个目录,包含
4个文件:
mysite/
_
_init__.py
manage.py
settings.py
urls.py
文件如下:
init.py :让 Python 把该目录当成
一个开发包 (即一组模块)所需的
文件。 这是一个空文件,一般你
不需要修改它。
manage.py :一种命令行工具,允
许你以多种方式与该 Django 项目
进行交互。 键入
python manage.py help,看一下它
能做什么。 你应当不需要编辑这
个文件;在这个目录下生成它纯
是为了方便。
settings.py :该 Django 项目的设置
或配置。 查看并理解这个文件中
可用的设置类型及其默认值。
urls.py:Django项目的URL设置。
可视其为你的django网站的目录。
目前,它是空的。
尽管这些的文件很小,但这些文件已
经构成了一个可运行的Django应用。
运行开发服务器
为了安装后更多的体验,让我们运行
一下django开发服务器看看我们的准
系统。
django开发服务是可用在开发期间
的,一个内建的,轻量的web服务。
我们提供这个服务器是为了让你快速
开发站点,也就是说在准备发布产品
之前,无需进行产品级 We b 服务器
(
比如 Apache)的配置工作。 开发
服务器监测你的代码并自动加载它,
这样你会很容易修改代码而不用重启
动服务。
如果你还没启动服务器的话,请切换
到你的项目目录里 (cd mysi te ),运行
下面的命令:
python manage.py runserver
你会看到些像这样的
Validating models...
0
errors found.
Django version 1.0, using settings '
mysite.settings'
Development server is running at htt
p://127.0.0.1:8000/
Quit the server with CONTROL-C.
这将会在端口8000启动一个本地服务
器, 并且只能从你的这台电脑连接和
访问。 既然服务器已经运行起来
了,现在用网页浏览器访
问 http://127.0.0.1:8000/ 。 你应该可
以看到一个令人赏心悦目的淡蓝色
Django欢迎页面。 它开始工作了。
在进一步学习之前, 一个重要的,
关于开发网络服务器的提示很值得一
说。 虽然 django 自带的这个 web 服
务器对于开发很方便,但是,千万不
要在正式的应用布署环境中使用它。
在同一时间,该服务器只能可靠地处
理一次单个请求,并且没有进行任何
类型的安全审计。 发布站点前,请
参阅第 20 章了解如何部署 Django 。
更改这个 Development Server 的主机
地址或端口
默认情况下, runserver 命令在 8000
端口启动开发服务器,且仅监听本地
连接。 要想要更改服务器端口的
话,可将端口作为命令行参数传入:
python manage.py runserver 8080
通过指定一个 IP 地址,你可以告诉
服务器–允许非本地连接访问。 如果
你想和其他开发人员共享同一开发站
点的话,该功能特别有用。
0
.0.0.0 这个 IP 地址,告诉服务器
去侦听任意的网络接口。
python manage.py runserver 0.0.0.0:8
0
00
完成这些设置后,你本地网络中的其
它计算机就可以在浏览器中访问你的
IP 地址了。比
如:http://192.168.1.103:8000/ . (注
意,你将需要校阅一下你的网络配置
来决定你在本地网络中的IP 地址)
Uni x用户可以在命令提示符中输入
ifconfig来获取以上信息。 使用
Windows的用户,请尝试使用
ipconfig 命令。
接下来做什么?
好了,你已经安装好所需的一切,
并且开发服务器也运行起来了,你已
经准备好继续 学习基础知识–视图和
URL配置 这一章的内容了。
前一章中,我们解释了如何建立一个
Django 项目并启动 Django 开发服务
器。 在这一章,你将会学到用Django
创建动态网页的基本知识。
你的第一个基于
Django的页面: Hello
World
正如我们的第一个目标,创建一个网
页,用来输出这个著名的示例信息:
Hello world.
如果你曾经发布过Hello world页面,
但是没有使用网页框架,只是简单的
在hel l o.html文本文件中输入Hello
World,然后上传到任意的一个网页
服务器上。 注意,在这个过程中,
你已经说明了两个关于这个网页的关
键信息: 它包括(字符
串 "Hello world")和它的
URL( http://www.example.com/hello.ht
ml , 如果你把文件放在子目录,也可
能
是 http://www.example.com/files/hello.
html)。
使用Django,你会用不同的方法来说
明这两件事 页面的内容是靠view
function(视图函数) 来产生,URL
定义在 URLconf 中。首先,我们先写
一个Hello World视图函数。
第一份视图:
在上一章使用django-
admin.py startproject制作的mysi te文件
夹中,创建一个叫做views.py的空文
件。这个Python模块将包含这一章的
视图。 请留意,Django对于view.py
的文件命名没有特别的要求,它不在
乎这个文件叫什么。但是根据约定,
把它命名成view.py是个好主意,这样
有利于其他开发者读懂你的代码,正
如你很容易的往下读懂本文。
我们的Hello world视图非常简单。 这
些是完整的函数和导入声明,你需要
输入到views.py文件:
from django.http import HttpResponse
def hello(request):
return HttpResponse("Hello world
"
)
我们逐行逐句地分析一遍这段代码:
首先,我们从 django.http 模块导入
(
import) HttpResponse 类。参阅
附录 H 了解更多关于 HttpRequest
和 HttpResponse 的细节。 我们需
要导入这些类,因为我们会在后
面用到。
接下来,我们定义一个叫做
hello 的视图函数。
每个视图函数至少要有一个参
数,通常被叫作request。 这是一
个触发这个视图、包含当前Web请
求信息的对象,是类
django.http.HttpRequest的一个实
例。在这个示例中,我们虽然不
用request做任何事情,然而它仍必
须是这个视图的第一个参数。
注意视图函数的名称并不重要;
并不一定非得以某种特定的方式
命名才能让 Django 识别它。 在这
里我们把它命名为:hello,是因
为这个名称清晰的显示了视图的
用意。同样地,你可以用诸如:
hello_wonderful_beautiful_world,
这样难看的短句来给它命名。 在
下一小节(Yo ur First URLconf),
将告诉你Django是如何找到这个函
数的。
这个函数只有简单的一行代码:
它仅仅返回一个HttpResponse对
象,这个对象包含了文本“Hello
world”。
这里主要讲的是: 一个视图就是
Python的一个函数。这个函数第一个
参数的类型是HttpRequest;它返回一
个HttpResponse实例。为了使一个
Python的函数成为一个Django可识别
的视图,它必须满足这两个条件。
(
也有例外,但是我们稍后才会接触
到。
你的第一个URLconf
现在,如果你再运行:python
manage.py runserver,你还将看到
Django的欢迎页面,而看不到我们刚
才写的Hello world显示页面。 那是因
为我们的mysi te项目还对hello视图一
无所知。我们需要通过一个详细描述
的URL来显式的告诉它并且激活这个
视图。 (继续我们刚才类似发布静
态HTML文件的例子。现在我们已经
创建了HTML文件,但还没有把它上
传至服务器的目录。)为了绑定视图
函数和URL,我们使用URLconf。
URLconf 就像是 Django 所支撑网站
的目录。 它的本质是 URL模式以及
要为该 URL模式调用的视图函数之
间的映射表。 你就是以这种方式告
诉 Django,对于这个 URL调用这段
代码,对于那个 URL调用那段代
码。 例如,当用户访问/foo/时,调用
视图函数foo_view(),这个视图函数
存在于Python模块文件view.py中。
前一章中执行 django-
admin.py startproject 时,该脚本会自
动为你建了一份
URLconf(即 urls.py 文件)。 默认的
urls.py会像下面这个样子:
from django.conf.urls.defaults impor
t *
#
Uncomment the next two lines to en
able the admin:
#
#
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
#
#
Example:
(r'^mysite/', include('mysite.
foo.urls')),
#
Uncomment the admin/doc line b
elow and add 'django.contrib.admindo
cs'
#
to INSTALLED_APPS to enable ad
min documentation:
#
(r'^admin/doc/', include('djan
go.contrib.admindocs.urls')),
#
Uncomment the next line to ena
ble the admin:
#
(r'^admin/', include(admin.sit
e.urls)),
)
默认的URLconf包含了一些被注释起
来的Django中常用的功能,仅仅只需
去掉这些注释就可以开启这些功能 .
下面是URLconf中忽略被注释的行后
的实际内容
from django.conf.urls.defaults impor
t *
urlpatterns = patterns('',
)
让我们逐行解释一下代码:
第一行导入django.conf.urls.defaults
下的所有模块,它们是Django
URLconf的基本构造。 这包含了
一个patterns函数。
第二行调用 patterns() 函数并将返
回结果保存到 urlpatterns 变量。
patterns函数当前只有一个参数—
一个空的字符串。 (这个字符串
可以被用来表示一个视图函数的
通用前缀。具体我们将在第八章
里面介绍。)
当前应该注意是 urlpatterns 变量,
Django 期望能
从 ROOT_URLCONF 模块中找到它。
该变量定义了 URL以及用于处理这
些 URL的代码之间的映射关系。 默
认情况下,URLconf 所有内容都被注
释起来了——Django 应用程序还是白
版一块。 (注:那是上一节中Django
怎么知道显示欢迎页面的原因。 如
果 URLconf 为空,Django 会认定你才
创建好新项目,因此也就显示那种信
息。
如果想在URLconf中加入URL和
view,只需增加映射URL模式和view
功能的Python tuple即可. 这里演示如
何添加view中hello功能.
from django.conf.urls.defaults impor
t *
from mysite.views import hello
urlpatterns = patterns('',
('^hello/$', hello),
)
请留意:为了简洁,我们移除了注释
代码。 如果你喜欢的话,你可以保
留那些行。)
我们做了两处修改。
首先,我们从模块 (在 Python 的
import 语法
中, mysite/views.py 转译
为 mysite.views ) 中引入了hello 视
图。 (这假设mysite/views.py在你
的Python搜索路径上。关于搜索路
径的解释,请参照下文。)
接下来,我们为urlpatterns加上一
行: (‘^hello/$’, hello), 这行被称
作URLpattern,它是一个Python的
元组。元组中第一个元素是模式
匹配字符串(正则表达式);第
二个元素是那个模式将使用的视
图函数。
简单来说,我们只是告诉 Django,所
有指向 URL /hello/ 的请求都应
由 hello 这个视图函数来处理。
Python 搜索路径
Python 搜索路径 就是使
用 import 语句时,Python 所查找
的系统目录清单。
举例来说,假定你将 Python 路径设置
为
[
'','/usr/lib/python2.4/site-package
s','/home/username/djcode/']
如果执行代码from foo import bar ,
Python 将会首先在当前目录查
找 foo.py 模块( Python 路径第一项的
空字符串表示当前目录)。 如果文件
不存在,Python将查找
/
usr/lib/python2.4/site-packages
文件。
如果你想看Python搜索路径的值,运
行Python交互解释器,然后输入:
>
>> import sys
>
>> print sys.path
通常,你不必关心 Python 搜索路径的
设置。 Python 和 Django 会在后台自
动帮你处理好。
讨论一下URLpattern的语法是值得
的,因为它不是显而易见的。 虽然
我们想匹配地址/hello/,但是模式看
上去与这有点差别。 这就是为什
么:
Django在检查URL模式前,移除每
一个申请的URL开头的斜杠(/)。
这意味着我们为/hello/写URL模式
不用包含斜杠(/)。(刚开始,这
样可能看起来不直观,但这样的
要求简化了许多工作,如URL模式
内嵌,我们将在第八章谈及。)
模式包含了一个尖号(^)和一个美
元符号($)。这些都是正则表达式
符号,并且有特定的含义: 上箭
头要求表达式对字符串的头部进
行匹配,美元符号则要求表达式
对字符串的尾部进行匹配。
最好还是用范例来说明一下这个
概念。 如果我们用尾部不是$的模
式’^hello/’,那么任何以/hello/开
头的URL将会匹配,例
如:/hello/foo 和/hello/bar,而不
仅仅是/hello/。类似地,如果我们
忽略了尖号(^),即’hello/$’,那么
任何以hello/结尾的URL将会匹
配,例如:/foo/bar/hello/。如果我
们简单使用hello/,即没有^开头和
$结尾,那么任何包含hello/的URL
将会匹配,如:/foo/hello/bar。因
此,我们使用这两个符号以确保
只有/hello/匹配,不多也不少。
你大多数的URL模式会以^开始、
以$结束,但是拥有复杂匹配的灵
活性会更好。
你可能会问:如果有人申请访
问/hello(尾部没有斜杠/)会怎
样。 因为我们的URL模式要求尾
部有一个斜杠(/),那个申请URL将
不匹配。 然而,默认地,任何不
匹配或尾部没有斜杠(/)的申请
URL,将被重定向至尾部包含斜杠
的相同字眼的URL。 (这是受配
置文件setting中APPEND_SLASH项
控制的,参见附件D。)
如果你是喜欢所有URL都以’/’结尾
的人(Django开发者的偏爱),那
么你只需要在每个URL后添加斜
杠,并且设
置”APPEND_SLASH”为”True”. 如
果不喜欢URL以斜杠结尾或者根据
每个URL来决定,那么需要设
置”APPEND_SLASH”为”False”,并
且根据你自己的意愿来添加结尾
斜杠/在URL模式后.
另外需要注意的是,我们把hello视图
函数作为一个对象传递,而不是调用
它。 这是 Python (及其它动态语言的)
的一个重要特性: 函数是一级对象
(first-class objects), 也就是说你
可以像传递其它变量一样传递它们。
很酷吧?
启动Django开发服务器来测试修改好
的 URLconf, 运行命令
行 python manage.py runserver 。 (如果
你让它一直运行也可以,开发服务器
会自动监测代码改动并自动重新载
入,所以不需要手工重启) 开发服
务器的地址是http://127.0.0.1:8000/ ,
打开你的浏览器访
问 http://127.0.0.1:8000/hello/ 。 你就
可以看到输出结果了。 开发服务器
将自动检测Python代码的更改来做必
要的重新加载, 所以你不需要重启
Server在代码更改之后。服务器运行
地址 http://127.0.0.1:8000/ ,所以打
开浏览器直接输
入 http://127.0.0.1:8000/hello/ ,你将
看到由你的Django视图输出的Hello
world。
万岁! 你已经创建了第一个Django的
web页面。
正则表达式
正则表达式 (或 regexes ) 是通用的文
本模式匹配的方法。 Django
URLconfs 允许你 使用任意的正则表
达式来做强有力的URL映射,不过通
常你实际上可能只需要使用很少的一
部分功能。 这里是一些基本的语
法。
符号
匹配
.
(dot) 任意单一字符
\d
任意一位数字
A 到 Z中任意一个字符
[
A-Z]
(
大写)
a 到 z中任意一个字符
小写)
[
[
a-z]
A-
(
a 到 z中任意一个字符
(不区分大小写)
Za-z]
匹配一个或更多 (例
如, \d+ 匹配一个或 多个
数字字符)
+
[
^/]+
一个或多个不为‘/’的字符
零个或一个之前的表达式
(例如:\d? 匹配零个或
一个数字)
*
匹配0个或更多 (例
如, \d* 匹配0个 或更多数
字字符)
*
{
介于一个和三个(包含)
之前的表达式(例如,
1,3}
\d{1,3}匹配一个或两个或
三个数字)
有关正则表达式的更多内容,请访
问 http://www.djangoproject.com/r/pyth
on/re-module/ .
关于“404错误”的快速参考
目前,我们的URLconf只定义了一个
单独的URL模式: 处理URL /hello/ 。
当请求其他URL会怎么样呢?
让我们试试看,运行Django开发服务
器并访问类
似 http://127.0.0.1:8000/goodbye/ 或者
http://127.0.0.1:8000/hello/subdirectory
/
,甚至 http://127.0.0.1:8000/ (网站根
目录)。 你将会看到一个 “Page not
found” 页面(图 3-1)。 因为你的
URL申请在URLconf中没有定义,所
以Django显示这条信息。
图3-1: Django的404 Error页
这个页面比原始的404错误信息更加
实用。 它同时精确的告诉你Django调
用哪个URLconf及其包含的每个模
式。 这样,你应该能了解到为什么
这个请求会抛出404错误。
当然,这些敏感的信息应该只呈现给
你-开发者。 如果是部署到了因特
网上的站点就不应该暴露 这些信
息。 出于这个考虑,这个“Page not
found”页面只会在 调试模式(debug
mode) 下 显示。 我们将在以后说明
怎么关闭调试模式。
关于网站根目录的快速参
考。
在最后一节,如果你想通过
http://127.0.0.1:8000/看网站根目录你
将看到一个404错误消息。Django不
会增加任何东西在网站根目录,在任
何情况下这个URL都不是特殊的 就像
在URLconf中的其他条目一样,它也
依赖于指定给它的URL模式.
尽管匹配网站根目录的URL模式不能
想象,但是还是值得提一下的. 当为
网站根目录实现一个视图,你需要使
用URL模式 ‘^$’ , 它代表一个空字
符串。 例如:
from mysite.views import hello, my_h
omepage_view
urlpatterns = patterns('',
url(r'^$', my_homepage_view),
#
...
)
Django是怎么处理请
求的
在继续我们的第二个视图功能之前,
让我们暂停一下去了解更多一些有关
Django是怎么工作的知识. 具体地
说,当你通过在浏览器里敲
http://127.0.0.1:8000/hello/来访问
Hello world消息得时候,Django在后
台有些什么动作呢?
所有均开始于setting文件。当你运行
python manage.py runserver,脚本将在
于manage.py同一个目录下查找名为
setting.py的文件。这个文件包含了所
有有关这个Django项目的配置信息,
均大写: TEMPLATE_DIRS ,
DATABASE_NAME , 等. 最重要的设
置时ROOT_URLCONF,它将作为
URLconf告诉Django在这个站点中那
些Python的模块将被用到
还记得什么时候django-admin.py
startproject创建文件settings.py和
urls.py吗?自动创建的settings.py包含
一个ROOT_URLCONF配置用来指向
自动产生的urls.py. 打开文件
settings.py你将看到如下:
ROOT_URLCONF = 'mysite.urls'
相对应的文件是mysite/urls.py
当访问 URL /hello/ 时,Django 根
据 ROOT_URLCONF 的设置装载
URLconf 。 然后按顺序逐个匹配
URLconf里的URLpatterns,直到找到
一个匹配的。 当找到这个匹配 的
URLpatterns就调用相关联的view函
数,并把HttpRequest 对象作为第一个
参数。 (稍后再给出 HttpRequest 的
更多信息) (我们将在后面看到
HttpRequest的标准)
正如我们在第一个视图例子里面看到
的,一个视图功能必须返回一个
HttpResponse。 一旦做完,Django将
完成剩余的转换Python的对象到一个
合适的带有HTTP头和body的We b
Response,(例如,网页内容)。
总结一下:
1
2
. 进来的请求转入/hello/.
. Django通过在ROOT_URLCONF配
置来决定根URLconf.
3
. Django在URLconf中的所有URL模
式中,查找第一个匹配/hello/的条
目。
4
. 如果找到匹配,将调用相应的视
图函数
5
6
. 视图函数返回一个HttpResponse
. Django转换HttpResponse为一个适
合的HTTP response, 以We b page
显示出来
你现在知道了怎么做一个 Django-
powered 页面了,真的很简单,只需
要写视图函数并用 URLconfs把它们
和URLs对应起来。 你可能会认为用
一系列正则表达式将URLs映射到函
数也许会比较慢,但事实却会让你惊
讶。
第二个视图: 动态内
容
我们的Hello world视图是用来演示基
本的Django是如何工作的,但是它不
是一个动态网页的例子,因为网页的
内容一直是一样的. 每次去查
看/hello/,你将会看到相同的内容,
它类似一个静态HTML文件。
我们的第二个视图,将更多的放些动
态的东西例如当前日期和时间显示在
网页上 这将非常好,简单的下一
步,因为它不引入了数据库或者任何
用户的输入,仅仅是输出显示你的服
务器的内部时钟. 它仅仅有限度的比
Helloworld刺激一些,但是它将演示
一些新的概念
这个视图需要做两件事情: 计算当
前日期和时间,并返回包含这些值的
HttpResponse 如果你对python很有经
验,那肯定知道在python中需要利用
datetime模块去计算时间 下面演示如
何去使用它:
>
>
>
>> import datetime
>> now = datetime.datetime.now()
>> now
datetime.datetime(2008, 12, 13, 14,
9
>
2
, 39, 2731)
>> print now
008-12-13 14:09:39.002731
以上代码很简单,并没有涉及
Django。 它仅仅是Python代码。 需要
强调的是,你应该意识到哪些是纯
Python代码,哪些是Django特性代
码。 (见上) 因为你学习了
Django,希望你能将Django的知识应
用在那些不一定需要使用Django的项
目上。
为了让Django视图显示当前日期和时
间,我们仅需要把语句:
datetime.datetime.now()放入视图函
数,然后返回一个HttpResponse对象
即可。代码如下:
from django.http import HttpResponse
import datetime
def current_datetime(request):
now = datetime.datetime.now()
html = "<html><body>It is now %s
.
</body></html>" % now
return HttpResponse(html)
正如我们的hello函数一样,这个函数
也保存在view.py中。为了简洁,上面
我们隐藏了hello函数。下面是完整的
view.py文件内容:
from django.http import HttpResponse
import datetime
def hello(request):
return HttpResponse("Hello world
"
)
def current_datetime(request):
now = datetime.datetime.now()
html = "<html><body>It is now %s
.
</body></html>" % now
return HttpResponse(html)
(
从现在开始,如非必要,本文不再
重复列出先前的代码。 你应该懂得
识别哪些是新代码,哪些是先前
的。) (见上)
让我们分析一下改动后的views.py:
在文件顶端,我们添加了一条语
句:import datetime。这样就可以
计算日期了。
函数中的第一行代码计算当前日
期和时间,并
以 datetime.datetime 对象的形式保
存为局部变量 now 。
函数的第二行代码用 Python 的格
式化字符串(format-string)功能
构造了一段 HTML响应。 字符串
中的%s是占位符,字符串后面的
百分号表示用它后面的变量now的
值来代替%s。变量%s是一个
datetime.datetime对象。它虽然不是
一个字符串,但是%s(格式化字
符串)会把它转换成字符串,
如:2008-12-13 14:09:39.002731。
这将导致HTML的输出字符串为:
It is now 2008-12-13
4:09:39.002731 。
目前HTML是有错误的,但我们
1
(
这样做是为了保持例子的简
短。)
最后,正如我们刚才写的hello函
数一样,视图返回一个
HttpResponse对象,它包含生成的
响应。
添加上述代码之后,还要在urls.py中
添加URL模式,以告诉Django由哪一
个URL来处理这个视图。 用/time/之
类的字眼易于理解:
from django.conf.urls.defaults impor
t *
from mysite.views import hello, curr
ent_datetime
urlpatterns = patterns('',
('^hello/$', hello),
('^time/$', current_datetime),
)
这里,我们修改了两个地方。 首
先,在顶部导入current_datetime函
数; 其次,也是比较重要的:添加
URL模式来映射URL中的/time/和新视
图。 理解了么?
写好视图并且更新URLconf之后,运
行命令python manage.py runserver以启
动服务,在浏览器中输入
http://127.0.0.1:8000/time/。 你将看到
当前的日期和时间。
Django时区
视乎你的机器,显示的日期与时间可
能和实际的相差几个小时。 这是因
为Django是有时区意识的,并且默认
时区为America/Chicago。 (它必须
有个值,它的默认值是Django的诞生
地:美国/芝加哥)如果你处在别的
时区,你需要在settings.py文件中更改
这个值。请参见它里面的注释,以获
得最新世界时区列表。
URL配置和松耦合
现在是好时机来指出Django和URL配
置背后的哲学: 松耦合 原则。 简单
的说,松耦合是一个 重要的保证互
换性的软件开发方法。
Django的URL配置就是一个很好的例
子。 在Django的应用程序中,URL的
定义和视图函数之间是松 耦合的,
换句话说,决定URL返回哪个视图函
数和实现这个视图函数是在两个不同
的地方。 这使得 开发人员可以修改
一块而不会影响另一块。
例如,考虑一下current_datetime视
图。 如果我们想把它的URL从原来
的 /time/ 改变到 /currenttime/ ,我们
只需要快速的修改一下URL配置即
可, 不用担心这个函数的内部实
现。 同样的,如果我们想要修改这
个函数的内部实现也不用担心会影响
到对应的URL。
此外,如果我们想要输出这个函数
到 一些 URL, 我们只需要修改URL
配置而不用 去改动视图的代码。 在
这个例子里,current_datetime被两个
URL使用。 这是一个故弄玄虚的例
子,但这个方法迟早会用得上。
urlpatterns = patterns('',
('^hello/$', hello),
('^time/$', current_datetime),
('^another-time-page/$', current
_
datetime),
)
URLconf和视图是松耦合的。 我们将
在本书中继续给出这一重要哲学的相
关例子。
第三个视图 动态URL
在我们的 current_datetime 视图范
例中,尽管内容是动态的,但是URL
(
/time/ )是静态的。 在 大多数动
态web应用程序,URL通常都包含有
相关的参数。 举个例子,一家在线
书店会为每一本书提供一个URL,
如:/books/243/、/books/81196/。
让我们创建第三个视图来显示当前时
间和加上时间偏差量的时间,设计是
这样的: /time/plus/1/ 显示当前时间
+
1个小时的页面 /time/plus/2/ 显示当
前时间+2个小时的页
面 /time/plus/3/ 显示当前时间+3个小
时的页面,以此类推。
新手可能会考虑写不同的视图函数来
处理每个时间偏差量,URL配置看起
来就象这样:
urlpatterns = patterns('',
('^time/$', current_datetime),
('^time/plus/1/$', one_hour_ahea
d),
('^time/plus/2/$', two_hours_ahe
ad),
('^time/plus/3/$', three_hours_a
head),
('^time/plus/4/$', four_hours_ah
ead),
)
很明显,这样处理是不太妥当的。
不但有很多冗余的视图函数,而且整
个应用也被限制了只支持 预先定义
好的时间段,2小时,3小时,或者4
小时。 如果哪天我们要实现 5 小
时,我们就 不得不再单独创建新的
视图函数和配置URL,既重复又混
乱。 我们需要在这里做一点抽象,
提取 一些共同的东西出来。
关于漂亮URL的一点建议
如果你有其它web平台的开发经验
(
如PHP或Java),你可能会想:
嘿!让我们用查询字符串参数吧!
就像/time/plus?hours=3里面的小时应
该在查询字符串中被参数hours指定
(
问号后面的是参数)。
你 可以 在Django里也这样做 (如果你
真的想要这样做,我们稍后会告诉你
怎么做), 但是Django的一个核心理
念就是URL必须看起来漂亮。
URL /time/plus/3/ 更加清晰, 更简
单,也更有可读性,可以很容易的大
声念出来,因为它是纯文本,没有查
询字符串那么 复杂。 漂亮的URL就
像是高质量的Web应用的一个标志。
Django的URL配置系统可以使你很容
易的设置漂亮的URL,而尽量不要考
虑它的 反面 。
那么,我们如何设计程序来处理任意
数量的时差? 答案是:使用通配符
(wildcard URLpatterns)。正如我们
之前提到过,一个URL模式就是一个
正则表达式。因此,这里可以使用
d+来匹配1个以上的数字。
urlpatterns = patterns('',
#
...
(r'^time/plus/\d+/$', hours_ahea
d),
)
#
...
这里使用# …来表示省略了其它可能
存在的URL模式定义。 (见上)
这个URL模式将匹配类
似 /time/plus/2/ , /time/plus/25/ ,甚
至 /time/plus/100000000000/ 的任何
URL。 更进一步,让我们把它限制在
最大允许99个小时, 这样我们就只
允许一个或两个数字,正则表达式的
语法就是\d{1,2} :
(r'^time/plus/\d{1,2}/$', hours_ahea
d),
备注
在建造Web应用的时候,尽可能多
考虑可能的数据输入是很重要
的,然后决定哪些我们可以接
受。 在这里我们就设置了99个小
时的时间段限制。
另外一个重点,正则表达式字符串的
开头字母“r”。 它告诉Python这是个原
始字符串,不需要处理里面的反斜杠
(转义字符)。 在普通Python字符串
中,反斜杠用于特殊字符的转义。比
如n转义成一个换行符。 当你用r把它
标示为一个原始字符串后,Python不
再视其中的反斜杠为转义字符。也就
是说,“n”是两个字符串:“”和“n”。
由于反斜杠在Python代码和正则表达
式中有冲突,因此建议你在Python定
义正则表达式时都使用原始字符串。
从现在开始,本文所有URL模式都用
原始字符串。
现在我们已经设计了一个带通配符的
URL,我们需要一个方法把它传递到
视图函数里去,这样 我们只用一个
视图函数就可以处理所有的时间段
了。 我们使用圆括号把参数在URL模
式里标识 出来。 在这个例子中,我
们想要把这些数字作为参数,用圆括
号把 \d{1,2} 包围起来:
(r'^time/plus/(\d{1,2})/$', hours_ah
ead),
如果你熟悉正则表达式,那么你应该
已经了解,正则表达式也是用圆括号
来从文本里 提取 数据的。
最终的URLconf包含上面两个视图,
如:
from django.conf.urls.defaults impor
t *
from mysite.views import hello, curr
ent_datetime, hours_ahead
urlpatterns = patterns('',
(r'^hello/$', hello),
(r'^time/$', current_datetime),
(r'^time/plus/(\d{1,2})/$', hour
s_ahead),
)
现在开始写 hours_ahead 视图。
编码次序
这个例子中,我们先写了URLpattern
,
然后是视图,但是在前面的例子
中, 我们先写了视图,然后是
URLpattern 。 哪一种方式比较好?
嗯,怎么说呢,每个开发者是不一样
的。
如果你是喜欢从总体上来把握事物
(注: 或译为“大局观”)类型的人,
你应该会想在项目开始 的时候就写
下所有的URL配置。
如果你从更像是一个自底向上的开发
者,你可能更喜欢先写视图, 然后
把它们挂接到URL上。 这同样是可以
的。
最后,取决与你喜欢哪种技术,两种
方法都是可以的。 (见上)
hours_ahead 和我们以前写
的 current_datetime 很象,关键的区别
在于: 它多了一个额外参数,时间
差。 以下是view代码:
from django.http import Http404, Htt
pResponse
import datetime
def hours_ahead(request, offset):
try:
offset = int(offset)
except ValueError:
raise Http404()
dt = datetime.datetime.now() + d
atetime.timedelta(hours=offset)
html = "<html><body>In %s hour(s
)
, it will be %s.</body></html>" % (
offset, dt)
return HttpResponse(html)
让我们逐行分析一下代码:
视图函数, hours_ahead , 有 两个 参
数: request 和 offset . (见上)
request 是一个 HttpRequest 对象,
就像在 current_datetime 中一样.
再说一次好了: 每一个视图 _总
是_以一个 HttpRequest 对象作
为 它的第一个参数。 (见上)
offset 是从匹配的URL里提取出
来的。 例如:如果请求URL
是/time/plus/3/,那么offset将会
是3;如果请求URL
是/time/plus/21/,那么offset将会
是21。请注意:捕获值永远都
是字符串(string)类型,而不
会是整数(integer)类型,即使
这个字符串全由数字构成
(
(
如:“21”)。
从技术上来说,捕获值总是
Uni code objects,而不是简单的
Python字节串,但目前不需要担
心这些差别。)
在这里我们命名变量为 offset ,
你也可以任意命名它,只要符
合Python 的语法。 变量名是无
关紧要的,重要的是它的位
置,它是这个函数的第二个 参
数 (在 request 的后面)。 你还
可以使用关键字来定义它,而
不是用 位置。
我们在这个函数中要做的第一件
事情就是在 offset 上调用 int() . 这
会把这个字符串值转换为整数。
请留意:如果你在一个不能转换
成整数类型的值上调用int(),
Python将抛出一个ValueError异
常。如:int(‘foo’)。在这个例子
中,如果我们遇到ValueError异
常,我们将转为抛出
django.http.Http404异常——正如你
想象的那样:最终显示404页面
(
提示信息:页面不存在)。
机灵的读者可能会问: 我们在
URL模式中用正则表达式(d{1,2})
约束它,仅接受数字怎么样?这
样无论如何,offset都是由数字构
成的。 答案是:我们不会这么
做,因为URLpattern提供的是“适
度但有用”级别的输入校验。万一
这个视图函数被其它方式调用,
我们仍需自行检查ValueError。 实
践证明,在实现视图函数时,不
臆测参数值的做法是比较好的。
松散耦合,还记得么?
下一行,计算当前日期/时间,然
后加上适当的小时数。 在
current_datetime视图中,我们已经
见过datetime.datetime.now()。这里
新的概念是执行日期/时间的算术
操作。我们需要创建一个
datetime.timedelta对象和增加一个
datetime.datetime对象。 结果保存
在变量dt中。
这一行还说明了,我们为什么在
offset上调用int()——
datetime.timedelta函数要求hours参
数必须为整数类型。
这行和前面的那行的的一个微小
差别就是,它使用带有两个值的
Python的格式化字符串功能, 而不
仅仅是一个值。 因此,在字符串
中有两个 %s 符号和一个以进行插
入的值的元组: (offset, dt) 。
最终,返回一个HTML的
HttpResponse。 如今,这种方式已
经过时了。
在完成视图函数和URL配置编写后,
启动Django开发服务器,用浏览器访
问http://127.0.0.1:8000/time/plus/3/ 来
确认它工作正常。 然后
是 http://127.0.0.1:8000/time/plus/5/ 。
再然后
是 http://127.0.0.1:8000/time/plus/24/
。
最后,访
问 http://127.0.0.1:8000/time/plus/100/
来检验URL配置里设置的模式是否只
接受一个或两个数字;Django会显示
一个 Page not found error 页面, 和以前
看到的 404 错误一样。 访问
URL http://127.0.0.1:8000/time/plus/ (
没有 定义时间差) 也会抛出404错
误。
Django 漂亮的出错页
面
花几分钟时间欣赏一下我们写好的
Web应用程序,然后我们再来搞点小
破坏。 我们故意在 views.py 文件中
引入一项 Python 错误,注释
掉 hours_ahead 视图中
的 offset = int(offset) 一行。
def hours_ahead(request, offset):
#
#
try:
offset = int(offset)
#
#
except ValueError:
raise Http404()
dt = datetime.datetime.now() + d
atetime.timedelta(hours=offset)
html = "<html><body>In %s hour(s
)
, it will be %s.</body></html>" % (
offset, dt)
return HttpResponse(html)
启动开发服务器,然后访
问 /time/plus/3/ 。你会看到一个包含
大量信息的出错页,最上面 的一
条 TypeError信息
是: "unsupported type for timedelta ho
urs component: unicode" .
怎么回事呢? 是
的, datetime.timedelta 函数要
求 hours 参数必须为整型, 而我们注
释掉了将 offset 转为整型的代码。 这
样导致 datetime.timedelta 弹
出 TypeError 异常。
这个例子是为了展示 Django 的出错
页面。 我们来花些时间看一看这个
出错页,了解一下其中 给出了哪些
信息。
以下是值得注意的一些要点:
在页面顶部,你可以得到关键的
异常信息: 异常数据类型、异常
的参数 (如本例中
的 "unsupported type")、在哪个文
件中引发了异常、出错的行号等
等。
在关键异常信息下方,该页面显
示了对该异常的完整 Python 追踪
信息。 这类似于你在 Python 命令
行解释器中获得的追溯信息,只
不过后者更具交互性。 对栈中的
每一帧,Django 均显示了其文件
名、函数或方法名、行号及该行
源代码。
点击该行代码 (以深灰色显示),你
可以看到出错行的前后几行,从
而得知相关上下文情况。
点击栈中的任何一帧的“Local
vars”可以看到一个所有局部变量
的列表,以及在出错 那一帧时它
们的值。 这些调试信息相当有
用。
注意“Traceback”下面的“Switch to
copy-and-paste view”文字。 点击
这些字,追溯会 切换另一个视
图,它让你很容易地复制和粘贴
这些内容。 当你想同其他人分享
这些异常 追溯以获得技术支持时
(
比如在 Django 的 IRC 聊天室或
邮件列表中),可以使用它。
你按一下下面的“Share this
traceback on a public We b site”按
钮,它将会完成这项工作。 点击
它以传回追溯信息至
http://www.dpaste.com/,在那里你
可以得到一个单独的URL并与其他
人分享你的追溯信息。
接下来的“Request information”部分
包含了有关产生错误的 We b 请求
的大量信息: GET 和 POST、
cookie 值、元数据(象 CGI
头)。 在附录H里给出了request的
对象的 完整参考。
Request信息的下面,“Settings”列
出了 Django 使用的具体配置信
息。 (我们已经提及过
ROOT_URLCONF,接下来我们将
向你展示各式的Django设置。 附
录D覆盖了所有可用的设置。)
Django 的出错页某些情况下有能力显
示更多的信息,比如模板语法错误。
我们讨论 Django 模板系统时再说它
们。 现在,取消 offset = int(offset) 这
行的注释,让它重新正常 工作。
不知道你是不是那种使用小心放置
的 print 语句来帮助调试的程序员?
你其实可以用 Django 出错页来做这
些,而不用 print 语句。 在你视图的
任何位置,临时插入一
个 assert False 来触发出错页。 然
后,你就可以看到局部变量和程序语
句了。 这里有个使用hours_ahead视
图的例子:
def hours_ahead(request, offset):
try:
offset = int(offset)
except ValueError:
raise Http404()
dt = datetime.datetime.now() + d
atetime.timedelta(hours=offset)
assert False
html = "<html><body>In %s hour(s
)
, it will be %s.</body></html>" % (
offset, dt)
return HttpResponse(html)
最后,很显然这些信息很多是敏感
的,它暴露了你 Python 代码的内部结
构以及 Django 配置,在 Internet 上公
开这信息是很愚蠢的。 不怀好意的
人会尝试使用它攻击你的 Web 应用
程序,做些下流之事。 因此,Django
出错信息仅在 debug 模式下才会显
现。 我们稍后 说明如何禁用 debug
模式。 现在,你只要知道 Django 服
务器在你开启它时默认运行在 debug
模式就行了。 (听起来很熟悉? 页
面没有发现错误,如前所述,工作正
常。)
下一章
目前为止,我们已经写好了视图函数
和硬编码的HTML。 在演示核心概念
时,我们所作的是为了保持简单。但
是在现实世界中,这差不多总是个坏
主意。
幸运的是,Django内建有一个简单有
强大的模板处理引擎来让你分离两种
工作: 下一章,我们将学习模板引
擎。
在前一章中,你可能已经注意到我们
在例子视图中返回文本的方式有点特
别。 也就是说,HTML被直接硬编码
在 Python 代码之中。
def current_datetime(request):
now = datetime.datetime.now()
html = "<html><body>It is now %s
.
</body></html>" % now
return HttpResponse(html)
尽管这种技术便于解释视图是如何工
作的,但直接将HTML硬编码到你的
视图里却并不是一个好主意。 让我
们来看一下为什么:
对页面设计进行的任何改变都必
须对 Python 代码进行相应的修
改。 站点设计的修改往往比底层
Python 代码的修改要频繁得多,
因此如果可以在不进行 Python 代
码修改的情况下变更设计,那将
会方便得多。
Python 代码编写和 HTML设计是
两项不同的工作,大多数专业的
网站开发环境都将他们分配给不
同的人员(甚至不同部门)来完
成。 设计者和HTML/CSS的编码
人员不应该被要求去编辑Python的
代码来完成他们的工作。
程序员编写 Python代码和设计人
员制作模板两项工作同时进行的
效率是最高的,远胜于让一个人
等待另一个人完成对某个既包含
Python又包含 HTML的文件的编
辑工作。
基于这些原因,将页面的设计和
Python的代码分离开会更干净简洁更
容易维护。 我们可以使用 Django
的 模板系统 (Template System)来实现
这种模式,这就是本章要具体讨论的
问题。
模板系统基本知识
模板是一个文本,用于分离文档的表
现形式和内容。 模板定义了占位符
以及各种用于规范文档该如何显示的
各部分基本逻辑(模板标签)。 模
板通常用于产生HTML,但是Django
的模板也能产生任何基于文本格式的
文档。
让我们从一个简单的例子模板开始。
该模板描述了一个向某个与公司签单
人员致谢 HTML页面。 可将其视为
一个格式信函:
<
<
<
html>
head><title>Ordering notice</title>
/head>
<
<
<
body>
h1>Ordering notice</h1>
p>Dear {{ person_name }},</p>
<
{
p>Thanks for placing an order from
{ company }}. It's scheduled to
ship on {{ ship_date|date:"F j, Y" }
}
.</p>
<
:
p>Here are the items you've ordered
</p>
<
{
ul>
% for item in item_list %}
<
li>{{ item }}</li>
{
<
% endfor %}
/ul>
{
% if ordered_warranty %}
<
p>Your warranty information wil
l be included in the packaging.</p>
% else %}
p>You didn't order a warranty,
{
<
so you're on your own when
the products inevitably stop wor
king.</p>
{
% endif %}
<
p>Sincerely,<br />{{ company }}</p>
<
<
/body>
/html>
该模板是一段添加了些许变量和模板
标签的基础 HTML。 让我们逐步分
析一下:
用两个大括号括起来的文字(例
如 {{ person_name }} )称为 变量
(variable) 。这意味着在此处插入
指定变量的值。 如何指定变量的
值呢? 稍后就会说明。
被大括号和百分号包围的文本(例
如 {% if ordered_warranty %} )
是 模板标签(template tag) 。标签
(tag)定义比较明确,即: 仅通知
模板系统完成某些工作的标签。
这个例子中的模板包含一个for标
签( {% for item in item_list %} )
和一个if 标签
(
{% if ordered_warranty %} )
for标签类似Python的for语句,可让
你循环访问序列里的每一个项
目。 if 标签,正如你所料,是用
来执行逻辑判断的。 在这里,tag
标签检查ordered_warranty值是否
为Tr ue。如果是,模板系统将显示
{
% if ordered_warranty %}和{%
else %}之间的内容;否则将显示
{
% else %}和{% endif %}之间的
内容。{% else %}是可选的。
最后,这个模板的第二段中有一
个关于_filter_过滤器的例子,它是
一种最便捷的转换变量输出格式
的方式。 如这个例子中的
{
{ship_date|date:”F j, Y” }},我们
将变量ship_date传递给date过滤
器,同时指定参数”F j,Y”。date过
滤器根据参数进行格式输出。 过
滤器是用管道符(|)来调用的,具体
可以参见Uni x管道符。
Django 模板含有很多内置的tags和
filters,我们将陆续进行学习. 附录F列
出了很多的tags和filters的列表,熟悉这
些列表对你来说是个好建议. 你依然
可以利用它创建自己的tag和filters。
这些我们在第9章会讲到。
如何使用模板系统
让我们深入研究模板系统,你将会明
白它是如何工作的。但我们暂不打算
将它与先前创建的视图结合在一起,
因为我们现在的目的是了解它是如何
独立工作的。 。 (换言之, 通常你
会将模板和视图一起使用,但是我们
只是想突出模板系统是一个Python
库,你可以在任何地方使用它,而不
仅仅是在Django视图中。)
在Python代码中使用Django模板的最
基本方式如下:
1
. 可以用原始的模板代码字符串创
建一个 Templ ate 对象, Django同
样支持用指定模板文件路径的方
式来创建 Templ ate 对象;
2
. 调用模板对象的render方法,并且
传入一套变量context。它将返回
一个基于模板的展现字符串,模
板中的变量和标签会被context值
替换。
代码如下:
>
>
>> from django import template
>> t = template.Template('My name i
s {{ name }}.')
>> c = template.Context({'name': 'A
drian'})
>> print t.render(c)
My name is Adrian.
>> c = template.Context({'name': 'F
red'})
>> print t.render(c)
My name is Fred.
>
>
>
>
以下部分逐步的详细介绍
创建模板对象
创建一个 Templ ate 对象最简单的方法
就是直接实例化它。 Templ ate 类就
在 django.template 模块中,构造函数
接受一个参数,原始模板代码。 让
我们深入挖掘一下 Python的解释器看
看它是怎么工作的。
转到project目录(在第二章由 django-
admin.py startproject 命令创建), 输
入命令python manage.py shell 启动交
互界面。
一个特殊的Python提示符
如果你曾经使用过Python,你一定好
奇,为什么我们运行
python manage.py shell而不是python。
这两个命令都会启动交互解释器,但
是manage.py shell命令有一个重要的
不同: 在启动解释器之前,它告诉
Django使用哪个设置文件。 Django框
架的大部分子系统,包括模板系统,
都依赖于配置文件;如果Django不知
道使用哪个配置文件,这些系统将不
能工作。
如果你想知道,这里将向你解释它背
后是如何工作的。 Django搜索
DJANGO_SETTINGS_MODULE环境
变量,它被设置在settings.py中。例
如,假设mysi te在你的Python搜索路
径中,那么
DJANGO_SETTINGS_MODULE应该
被设置为:’mysite.settings’。
当你运行命令:python manage.py
shell,它将自动帮你处理
DJANGO_SETTINGS_MODULE。 在
当前的这些示例中,我们鼓励你使用
python manage.py shell 这个方
法,这样可以免去你大费周章地去配
置那些你不熟悉的环境变量。
随着你越来越熟悉Django,你可能会
偏向于废弃使用 manage.py shell
,
而是在你的配置文件.bash_profile
中手动添
加 DJANGO_SETTINGS_MODULE这
个环境变量。
让我们来了解一些模板系统的基本知
识:
>
>> from django.template import Temp
late
>
>
>> t = Template('My name is {{ name
}.')
>> print t
}
如果你跟我们一起做,你将会看到下
面的内容:
0xb7d5f24c 每次都会不一样,这没什
么关系;这只是Python运行
时 Templ ate 对象的ID。
当你创建一个 Templ ate 对象,模板系
统在内部编译这个模板到内部格式,
并做优化,做好 渲染的准备。 如果
你的模板语法有错误,那么在调
用 Template() 时就会抛
出 Templ ateSyntaxError 异常:
>
>> from django.template import Temp
late
>
>> t = Template('{% notatag %}')
Traceback (most recent call last):
File "<stdin>", line 1, in ?
.
..
django.template.TemplateSyntaxError:
Invalid block tag: 'notatag'
这里,块标签(block tag)指向的是
{
% notatag %} ,块标签与模板标签
是同义的。
系统会在下面的情形抛
出 Templ ateSyntaxError 异常:
无效的tags
标签的参数无效
无效的过滤器
过滤器的参数无效
无效的模板语法
未封闭的块标签 (针对需要封闭
的块标签)
模板渲染
一旦你创建一个 Templ ate 对象,你可
以用 context 来传递数据给它。 一个
context是一系列变量和它们值的集
合。
context在Django里表现为 Context 类,
在 django.template 模块里。 她的构造
函数带有一个可选的参数: 一个字
典映射变量和它们的值。 调
用 Templ ate 对象 的 render() 方法并传
递context来填充模板:
>
>> from django.template import Cont
ext, Template
>
>> t = Template('My name is {{ name
}}.')
>
)
>
>> c = Context({'name': 'Stephane'}
>> t.render(c)
u'My name is Stephane.'
我们必须指出的一点是,t.render(c)
返回的值是一个Unicode对象,不是
普通的Python字符串。 你可以通过字
符串前的u来区分。 在框架中,
Django会一直使用Unicode对象而不是
普通的字符串。 如果你明白这样做
给你带来了多大便利的话,尽可能地
感激Django在幕后有条不紊地为你所
做这这么多工作吧。 如果不明白你
从中获益了什么,别担心。你只需要
知道Django对Unicode的支持,将让你
的应用程序轻松地处理各式各样的字
符集,而不仅仅是基本的A-Z英文字
符。
字典和Contexts
Python的字典数据类型就是关键字和
它们值的一个映射。 Context 和字典
很类似, Context 还提供更多的功
能,请看第九章。
变量名必须由英文字符开始 (A-Z或
a-z)并可以包含数字字符、下划线
和小数点。 (小数点在这里有特别
的用途,稍后我们会讲到)变量是大
小写敏感的。
下面是编写模板并渲染的示例:
>
>> from django.template import Temp
late, Context
>> raw_template = """<p>Dear {{ per
son_name }},</p>
>
.
.
..
.. <p>Thanks for placing an order f
rom {{ company }}. It's scheduled to
.. ship on {{ ship_date|date:"F j,
Y" }}.</p>
.
.
.
.
..
.. {% if ordered_warranty %}
.. <p>Your warranty information wil
l be included in the packaging.</p>
.
.
.. {% else %}
.. <p>You didn't order a warranty,
so you're on your own when
.. the products inevitably stop wor
king.</p>
.
.
.
.
<
>
>
>
.. {% endif %}
..
.. <p>Sincerely,<br />{{ company }}
/p>"""
>> t = Template(raw_template)
>> import datetime
>> c = Context({'person_name': 'Joh
n Smith',
..
t',
.
'company': 'Outdoor Equipmen
'ship_date': datetime.date(2
.
0
.
>
..
09, 4, 2),
..
'ordered_warranty': False})
>> t.render(c)
u"<p>Dear John Smith,</p>\n\n<p>Than
ks for placing an order from Outdoor
Equipment. It's scheduled to\nship o
n April 2, 2009.</p>\n\n\n<p>You
didn't order a warranty, so you're o
n your own when\nthe products
inevitably stop working.</p>\n\n\n<p
>
<
Sincerely,<br />Outdoor Equipment
/p>"
让我们逐步来分析下这段代码:
首先我们导入 (import)
类 Templ ate 和 Context ,它们都在
模块 django.template 里。
我们把模板原始文本保存到变
量 raw_template 。注意到我们使用
了三个引号来 标识这些文本,因
为这样可以包含多行。
接下来,我们创建了一个模板对
象 t ,把 raw_template 作
为 Templ ate 类构造函数的参数。
我们从Python的标准库导
入 datetime 模块,以后我们将会使
用它。
然后,我们创建一个 Context 对
象, c 。 Context 构造的参数是
Python 字典数据类型。 在这里,
我们指定参数 person_name 的值
是 'John Smi th' , 参数company 的值
为 ‘Outdoor Equipment’ ,等等。
最后,我们在模板对象上调
用 render() 方法,传递 context参数
给它。 这是返回渲染后的模板的
方法,它会替换模板变量为真实
的值和执行块标签。
注意,warranty paragraph显示是因
为 ordered_warranty 的值为 Tr ue .
注意时间的显示, April 2, 2009,
它是按 'F j, Y' 格式显示的。
如果你是Python初学者,你可能在
想为什么输出里有回车换行的字
符('\n' )而不是 显示回车换行? 因
为这是Python交互解释器的缘故:
调用 t.render(c) 返回字符串, 解释
器缺省显示这些字符串的 真实内
容呈现 ,而不是打印这个变量的
值。 要显示换行而不是 '\n' ,使
用 print 语句: print t.render(c) 。
这就是使用Django模板系统的基本规
则: 写模板,创建 Templ ate 对象,
创建 Context , 调用 render() 方法。
同一模板,多个上下文
一旦有了 模板 对象,你就可以通过
它渲染多个context, 例如:
>
>> from django.template import Temp
late, Context
>
)
>
'
>> t = Template('Hello, {{ name }}'
>> print t.render(Context({'name':
John'}))
Hello, John
>
'
>> print t.render(Context({'name':
Julie'}))
Hello, Julie
>
'
>> print t.render(Context({'name':
Pat'}))
Hello, Pat
无论何时我们都可以像这样使用同一
模板源渲染多个context,只进行 一次
模板创建然后多次调用render()方法
渲染会更为高效:
#
Bad
for name in ('John', 'Julie', 'Pat')
:
t = Template('Hello, {{ name }}'
)
print t.render(Context({'name':
name}))
#
Good
t = Template('Hello, {{ name }}')
for name in ('John', 'Julie', 'Pat')
:
print t.render(Context({'name':
name}))
Django 模板解析非常快捷。 大部分
的解析工作都是在后台通过对简短正
则表达式一次性调用来完成。 这和
基于 XML的模板引擎形成鲜明对
比,那些引擎承担了 XML解析器的
开销,且往往比 Django 模板渲染引
擎要慢上几个数量级。
深度变量的查找
在到目前为止的例子中,我们通过
context 传递的简单参数值主要是字符
串,还有一个 datetime.date 范例。 然
而,模板系统能够非常简洁地处理更
加复杂的数据结构,例如list、
dictionary和自定义的对象。
在 Django 模板中遍历复杂数据结构
的关键是句点字符 (.)。
最好是用几个例子来说明一下。 比
如,假设你要向模板传递一个 Python
字典。 要通过字典键访问该字典的
值,可使用一个句点:
>
>> from django.template import Temp
late, Context
>
:
>> person = {'name': 'Sally', 'age'
'43'}
>
>> t = Template('{{ person.name }}
is {{ person.age }} years old.')
>
>
>> c = Context({'person': person})
>> t.render(c)
u'Sally is 43 years old.'
同样,也可以通过句点来访问对象的
属性。 比方说, Python
的 datetime.date 对象
有 year 、 month 和 day几个属性,你
同样可以在模板中使用句点来访问这
些属性:
>
>> from django.template import Temp
late, Context
>
>
>
1
>> import datetime
>> d = datetime.date(1993, 5, 2)
>> d.year
993
>
5
>
2
>
>> d.month
>> d.day
>> t = Template('The month is {{ da
te.month }} and the year is {{ date.
year }}.')
>
>
>> c = Context({'date': d})
>> t.render(c)
u'The month is 5 and the year is 199
3.'
这个例子使用了一个自定义的类,演
示了通过实例变量加一点(dots)来访
问它的属性,这个方法适用于任意的
对象。
>
>> from django.template import Temp
late, Context
>
.
>> class Person(object):
.. def __init__(self, first_nam
e, last_name):
.. self.first_name, self.la
st_name = first_name, last_name
>> t = Template('Hello, {{ person.f
irst_name }} {{ person.last_name }}.
.
>
'
>
)
>> c = Context({'person': Person('J
ohn', 'Smith')})
>
>> t.render(c)
u'Hello, John Smith.'
点语法也可以用来引用对象的 方
法。 例如,每个 Python 字符串都
有 upper() 和 isdigit() 方法,你在模板
中可以使用同样的句点语法来调用它
们:
>
>> from django.template import Temp
late, Context
>> t = Template('{{ var }} -- {{ va
>
r.upper }} -- {{ var.isdigit }}')
>
}
>> t.render(Context({'var': 'hello'
))
u'hello -- HELLO -- False'
>
)
>> t.render(Context({'var': '123'})
u'123 -- 123 -- True'
注意这里调用方法时并 没有 使用圆
括号 而且也无法给该方法传递参
数;你只能调用不需参数的方法。
(
我们将在本章稍后部分解释该设计
观。)
最后,句点也可用于访问列表索引,
例如:
>
>> from django.template import Temp
late, Context
>
.
>
>> t = Template('Item 2 is {{ items
2 }}.')
>> c = Context({'items': ['apples',
'
bananas', 'carrots']})
>
>> t.render(c)
u'Item 2 is carrots.'
不允许使用负数列表索引。
像 {{ items.-1 }} 这样的模板变量将
会引发 TemplateSyntaxError
Python 列表类型
一点提示: Python的列表是从0开始
索引。 第一项的索引是0,第二项的
是1,依此类推。
句点查找规则可概括为: 当模板系
统在变量名中遇到点时,按照以下顺
序尝试进行查找:
字典类型查找 (比如 foo["bar"] )
属性查找 (比如 foo.bar )
方法调用 (比如 foo.bar() )
列表类型索引查找 (比如 foo[bar] )
系统使用找到的第一个有效类型。
这是一种短路逻辑。
句点查找可以多级深度嵌套。 例如
在下面这个例子
中 {{person.name.upper}} 会转换成字
典类型查找(person['name'] ) 然后是
方法调用( upper() ):
>
>> from django.template import Temp
late, Context
>
:
>
>> person = {'name': 'Sally', 'age'
'43'}
>> t = Template('{{ person.name.upp
er }} is {{ person.age }} years old.
'
>
>
)
>> c = Context({'person': person})
>> t.render(c)
u'SALLY is 43 years old.'
方法调用行为
方法调用比其他类型的查找略为复杂
一点。 以下是一些注意事项:
在方法查找过程中,如果某方法
抛出一个异常,除非该异常有一
个 silent_variable_failure 属性并且
值为 Tr ue ,否则的话它将被传
播。如果异常被传播,模板里的
指定变量会被置为空字符串,比
如:
>
>> t = Template("My name is {{ pers
on.first_name }}.")
>> class PersonClass3:
>
.
.
..
..
def first_name(self):
raise AssertionError, "f
oo"
>
>
>> p = PersonClass3()
>> t.render(Context({"person": p}))
Traceback (most recent call last):
..
AssertionError: foo
.
>
>> class SilentAssertionError(Asser
tionError):
.
..
silent_variable_failure = Tr
ue
>
.
.
>> class PersonClass4:
..
..
def first_name(self):
raise SilentAssertionErr
or
>
>
>> p = PersonClass4()
>> t.render(Context({"person": p}))
u'My name is .'
仅在方法无需传入参数时,其调
用才有效。 否则,系统将会转移
到下一个查找类型(列表索引查
找)。
显然,有些方法是有副作用的,
好的情况下允许模板系统访问它
们可能只是干件蠢事,坏的情况
下甚至会引发安全漏洞。
例如,你的一个 BankAccount 对象
有一个 delete() 方法。 如果某个模
板中包含了像{{ account.delete }}
这样的标签,其中 account 又是
BankAccount 的一个实例,请注意
在这个模板载入时,account对象
将被删除。
要防止这样的事情发生,必须设
置该方法的 alters_data 函数属性:
def delete(self):
#
Delete the account
delete.alters_data = True
模板系统不会执行任何以该方式
进行标记的方法。 接上面的例
子,如果模板文件里包含了
{
{ account.delete }} ,对象又具
有 delete()方法,而且delete() 有
alters_data=True这个属性,那么在
模板载入时, delete()方法将不会
被执行。 它将静静地错误退出。
如何处理无效变量
默认情况下,如果一个变量不存在,
模板系统会把它展示为空字符串,不
做任何事情来表示失败。 例如:
>
>> from django.template import Temp
late, Context
>> t = Template('Your name is {{ na
me }}.')
>> t.render(Context())
u'Your name is .'
>
>
>
}
>> t.render(Context({'var': 'hello'
))
u'Your name is .'
>
'
>> t.render(Context({'NAME': 'hello
}))
u'Your name is .'
>
'
>> t.render(Context({'Name': 'hello
}))
u'Your name is .'
系统静悄悄地表示失败,而不是引发
一个异常,因为这通常是人为错误造
成的。 这种情况下,因为变量名有
错误的状况或名称, 所有的查询都
会失败。 现实世界中,对于一个web
站点来说,如果仅仅因为一个小的模
板语法错误而造成无法访问,这是不
可接受的。
玩一玩上下文(context)对象
多数时间,你可以通过传递一个完全
填充(full populated)的字典
给 Context() 来初始化 上下文
(Context) 。 但是初始化以后,你也
可以使用标准的Python字典语法
(syntax)向 上下文(Context) 对象添加
或者删除条目 :
>
>> from django.template import Cont
ext
>
>
'
>
>
>> c = Context({"foo": "bar"})
>> c['foo']
bar'
>> del c['foo']
>> c['foo']
Traceback (most recent call last):
..
KeyError: 'foo'
.
>
>
'
>> c['newvariable'] = 'hello'
>> c['newvariable']
hello'
基本的模板标签和过
滤器
像我们以前提到过的,模板系统带有
内置的标签和过滤器。 下面的章节
提供了一个多数通用标签和过滤器的
简要说明。
标签
if/else
{
% if %} 标签检查(evaluate)一个变
量,如果这个变量为真(即,变量存
在,非空,不是布尔值假),系统会
显示在 {% if %} 和 {% endif %} 之间
的任何内容,例如:
{
{
% if today_is_weekend %}
p>Welcome to the weekend!</p>
% endif %}
<
{
% else %} 标签是可选的:
{
{
% if today_is_weekend %}
p>Welcome to the weekend!</p>
% else %}
<
<
p>Get back to work.</p>
{
% endif %}
Python 的“真值”
在Python和Django模板系统中,以下
这些对象相当于布尔值的False
空列表([] )
空元组(() )
空字典({} )
空字符串('' )
零值(0 )
特殊对象None
对象False(很明显)
提示:你也可以在自定义的对象
里定义他们的布尔值属性(这个是
python的高级用法)。
除以上几点以外的所有东西都视为
True
{
% if %} 标签接受 and , or 或
者 not 关键字来对多个变量做判断 ,
或者对变量取反( not ),例如: 例
如:
{
% if athlete_list and coach_list %}
Both athletes and coaches are av
ailable.
{
{
{
{
% endif %}
% if not athlete_list %}
There are no athletes.
% endif %}
% if athlete_list or coach_list %}
There are some athletes or some
coaches.
{
% endif %}
{
% if not athlete_list or coach_list
%
}
There are no athletes or there a
re some coaches.
{
% endif %}
{
% if athlete_list and not coach_lis
t %}
There are some athletes and abso
lutely no coaches.
% endif %}
{
{
% if %} 标签不允许在同一个标签中
同时使用 and 和 or ,因为逻辑上可
能模糊的,例如,如下示例是错误
的: 比如这样的代码是不合法的:
{
% if athlete_list and coach_list or
cheerleader_list %}
系统不支持用圆括号来组合比较操
作。 如果你确实需要用到圆括号来
组合表达你的逻辑式,考虑将它移到
模板之外处理,然后以模板变量的形
式传入结果吧。 或者,仅仅用嵌套
的{% if %}标签替换吧,就像这样:
{
% if athlete_list %}
% if coach_list or cheerleader_
list %}
{
We have athletes, and either
coaches or cheerleaders!
% endif %}
% endif %}
{
{
多次使用同一个逻辑操作符是没有问
题的,但是我们不能把不同的操作符
组合起来。 例如,这是合法的:
{
% if athlete_list or coach_list or
parent_list or teacher_list %}
并没有 {% elif %} 标签, 请使用嵌
套的 {% if %} 标签来达成同样的效
果:
{
% if athlete_list %}
Here are the athletes: {{ athlet
e_list }}.
% else %}
No athletes are available.
{
{
% if coach_list %}
Here are the coaches: {{ coa
ch_list }}.
{
% endif %}
{
% endif %}
一定要用 {% endif %} 关闭每一
个 {% if %} 标签。
for
{
% for %} 允许我们在一个序列上迭
代。 与Python的 for 语句的情形类
似,循环语法是 for X in Y ,Y是要
迭代的序列而X是在每一个特定的循
环中使用的变量名称。 每一次循环
中,模板系统会渲染在 {% for %} 和
{
% endfor %} 之间的所有内容。
例如,给定一个运动员列
表 athlete_list 变量,我们可以使用下
面的代码来显示这个列表:
<
{
ul>
% for athlete in athlete_list %}
<
li>{{ athlete.name }}</li>
{
<
% endfor %}
/ul>
给标签增加一个 reversed 使得该列表
被反向迭代:
{
% for athlete in athlete_list rever
sed %}
.
{
..
% endfor %}
可以嵌套使用 {% for %} 标签:
{
% for athlete in athlete_list %}
<
<
{
h1>{{ athlete.name }}</h1>
ul>
% for sport in athlete.sports_p
layed %}
<
li>{{ sport }}</li>
{
<
% endfor %}
/ul>
{
% endfor %}
在执行循环之前先检测列表的大小是
一个通常的做法,当列表为空时输出
一些特别的提示。
{
}
% if athlete_list %}
{
% for athlete in athlete_list %
<
p>{{ athlete.name }}</p>
{
% endfor %}
{
% else %}
p>There are no athletes. Only c
omputer programmers.</p>
% endif %}
<
{
因为这种做法十分常见,所以 for
标签支持一个可选的 {% empty %}
分句,通过它我们可以定义当列表为
空时的输出内容 下面的例子与之前
那个等价:
{
% for athlete in athlete_list %}
p>{{ athlete.name }}</p>
% empty %}
p>There are no athletes. Only c
omputer programmers.</p>
% endfor %}
<
{
<
{
Django不支持退出循环操作。 如果我
们想退出循环,可以改变正在迭代的
变量,让其仅仅包含需要迭代的项
目。 同理,Django也不支持continue
语句,我们无法让当前迭代操作跳回
到循环头部。 (请参看本章稍后的
理念和限制小节,了解下决定这个设
计的背后原因)
在每个 {% for %} 循环里有一个称为
forloop 的模板变量。这个变量有
一些提示循环进度信息的属性。
forloop.counter 总是一个表示当前
循环的执行次数的整数计数器。
这个计数器是从1开始的,所以在
第一次循环时 forloop.counter 将会
被设置为1。
{
% for item in todo_list %}
p>{{ forloop.counter }}: {{ ite
m }}</p>
% endfor %}
<
{
forloop.counter0 类似
于 forloop.counter ,但是它是从0
计数的。 第一次执行循环时这个
变量会被设置为0。
forloop.revcounter 是表示循环中剩
余项的整型变量。 在循环初次执
行时 forloop.revcounter 将被设置为
序列中项的总数。 最后一次循环
执行中,这个变量将被置1。
forloop.revcounter0 类似
于 forloop.revcounter ,但它以0做
为结束索引。 在第一次执行循环
时,该变量会被置为序列的项的
个数减1。
forloop.first 是一个布尔值,如果
该迭代是第一次执行,那么它被
置为```` 在下面的情形中这个变量
是很有用的:
System Message: WARNING/ 2 (,
line 1071); backlink
Inline literal start-string without end-
string.
{
{
% for object in objects %}
% if forloop.first %}<li class="
first">{% else %}<li>{% endif %}
{
<
{
{ object }}
/li>
% endfor %}r %}
forloop.last 是一个布尔值;在最后
一次执行循环时被置为Tr ue。 一
个常见的用法是在一系列的链接
之间放置管道符(|)
{
% for link in links %}{{ link }}{%
if not forloop.last %} | {% endif %}
% endfor %}
{
上面的模板可能会产生如下的结
果:
Link1 | Link2 | Link3 | Link4
另一个常见的用途是为列表的每
个单词的加上逗号。
Favorite places:
{
% for p in places %}{{ p }}{% if no
t forloop.last %}, {% endif %}{% end
for %}
forloop.parentloop 是一个指向当前
循环的上一级循环的 forloop 对象
的引用(在嵌套循环的情况
下)。 例子在此:
{
% for country in countries %}
<
{
table>
% for city in country.city_list
%
}
<
<
tr>
td>Country #{{ forloop.pare
ntloop.counter }}</td>
<
td>City #{{ forloop.counter
}
}</td>
<
<
td>{{ city }}</td>
/tr>
{
% endfor %}
<
/table>
{
% endfor %}
forloop 变量仅仅能够在循环中使
用。 在模板解析器碰到{% endfor %}
标签后,forloop就不可访问了。
Context和forloop变量
在一个 {% for %} 块中,已存在的变
量会被移除,以避免 forloop 变量被
覆盖。 Django会把这个变量移动到
forloop.parentloop 中。通常我们不用
担心这个问题,但是一旦我们在模板
中定义了 forloop 这个变量(当然我
们反对这样做),在 {% for %} 块中
它会在 forloop.parentloop 被重新命
名。
ifequal/ifnotequal
Django模板系统压根儿就没想过实现
一个全功能的编程语言,所以它不允
许我们在模板中执行Python的语句
(
还是那句话,要了解更多请参看理
念和限制小节)。 但是比较两个变
量的值并且显示一些结果实在是个太
常见的需求了,所以Django提供
了 {% ifequal %} 标签供我们使用。
% ifequal %} 标签比较两个值,当
{
他们相等时,显示
在 {% ifequal %} 和 {% endifequal %}
之中所有的值。
下面的例子比较两个模板变
量 user 和 currentuser :
{
{
% ifequal user currentuser %}
h1>Welcome!</h1>
% endifequal %}
<
参数可以是硬编码的字符串,随便用
单引号或者双引号引起来,所以下列
代码都是正确的:
{
{
{
{
% ifequal section 'sitenews' %}
h1>Site News</h1>
% endifequal %}
<
% ifequal section "community" %}
<
h1>Community</h1>
% endifequal %}
和 {% if %} 类似, {% ifequal %} 支
持可选的 {% else%} 标签:
{
{
{
% ifequal section 'sitenews' %}
h1>Site News</h1>
% else %}
<
<
h1>No News Here</h1>
% endifequal %}
只有模板变量,字符串,整数和小数
可以作为 {% ifequal %} 标签的参
数。下面是合法参数的例子:
{
{
{
{
% ifequal variable 1 %}
% ifequal variable 1.23 %}
% ifequal variable 'foo' %}
% ifequal variable "foo" %}
其他任何类型,例如Python的字典类
型、列表类型、布尔类型,不能用
在 {% ifequal %} 中。 下面是些错误
的例子:
{
{
% ifequal variable True %}
% ifequal variable [1, 2, 3] %}
{
% ifequal variable {'key': 'value'}
%
}
如果你需要判断变量是真还是假,请
使用 {% if %} 来替
代 {% ifequal %} 。
注释
就像HTML或者Python,Django模板
语言同样提供代码注释。 注释使
用 {# #} :
{
# This is a comment #}
注释的内容不会在模板渲染时输出。
用这种语法的注释不能跨越多行。
这个限制是为了提高模板解析的性
能。 在下面这个模板中,输出结果
和模板本身是 完全一样的(也就是
说,注释标签并没有被解析为注
释):
This is a {# this is not
a comment #}
test.
如果要实现多行注释,可以使用
{
% comment %} 模板标签,就像这
样:
{
% comment %}
This is a
multi-line comment.
{
% endcomment %}
过滤器
就象本章前面提到的一样,模板过滤
器是在变量被显示前修改它的值的一
个简单方法。 过滤器使用管道字
符,如下所示:
{
{ name|lower }}
显示的内容是变量 {{ na me }} 被过滤
器 lower 处理后的结果,它功能是转
换文本为小写。
过滤管道可以被 套接 ,既是说,一
个过滤器管道的输出又可以作为下一
个管道的输入,如此下去。 下面的
例子实现查找列表的第一个元素并将
其转化为大写。
{
{ my_list|first|upper }}
有些过滤器有参数。 过滤器的参数
跟随冒号之后并且总是以双引号包
含。 例如:
{
{ bio|truncatewords:"30" }}
这个将显示变量 bio 的前30个词。
以下几个是最为重要的过滤器的一部
分。 附录F包含其余的过滤器。
addslashes : 添加反斜杠到任何反
斜杠、单引号或者双引号前面。
这在处理包含JavaScript的文本时
是非常有用的。
date : 按指定的格式字符串参数格
式化 date 或者 datetime 对象, 范
例:
{
{ pub_date|date:"F j, Y" }}
格式参数的定义在附录F中。
length : 返回变量的长度。 对于列
表,这个参数将返回列表元素的
个数。 对于字符串,这个参数将
返回字符串中字符的个数。 你可
以对列表或者字符串,或者任何
知道怎么测定长度的Python 对象使
用这个方法(也就是说,
有 le n() 方法的对象)。
理念与局限
现在你已经对Django的模板语言有一
些认识了,我们将指出一些特意设置
的限制和为什么要这样做 背后的一
些设计哲学。
相对与其他的网络应用的组件,模板
的语法很具主观性,因此可供程序员
的选择方案也很广泛。 事实上,
Python有成十上百的 开放源码的模板
语言实现。 每个实现都是因为开发
者认为现存的模板语言不够用。
(
事实上,对一个 Python开发者来
说,写一个自己的模板语言就象是某
种“成人礼”一样! 如果你还没有完成
一个自己的 模板语言,好好考虑写
一个,这是一个非常有趣的锻炼。
)
明白了这个,你也许有兴趣知道事实
上Django并不强制要求你必须使用它
的模板语言。 因为Django 虽然被设
计成一个FULL-Stack的Web框架,它
提供了开发者所必需的所有组件,而
且在大多数情况 使用Django模板系统
会比其他的Python模板库要 更方便 一
点,但是并不是严格要求你必须使用
它。 你将在后续的“视图中应用模
板”这一章节中看到,你还可以非常
容易地在Django中使用其他的模板语
言。
虽然如此,很明显,我们对Django模
板语言的工作方式有着强烈的偏爱。
这个模板语言来源于World Online的
开发经验和Django创造者们集体智慧
的结晶。 下面是关于它的一些设计
哲学理念:
业务逻辑应该和表现逻辑相对分
开 。我们将模板系统视为控制表
现及表现相关逻辑的工具,仅此
而已。 模板系统不应提供超出此
基本目标的功能。
出于这个原因,在 Django 模板中
是不可能直接调用 Python 代码
的。 所有的编程工作基本上都被
局限于模板标签的能力范围。 当
然, 是 有可能写出自定义的模板
标签来完成任意工作,但这些“超
范围”的 Django 模板标签有意地不
允许执行任何 Python 代码。
语法不应受到 HTML/XML 的束
缚 。尽管 Django 模板系统主要用
于生成 HTML,它还是被有意地设
计为可生成非 HTML格式,如纯
文本。 一些其它的模板语言是基
于 XML的,将所有的模板逻辑置
于 XML标签与属性之中,而
Django 有意地避开了这种限制。
强制要求使用有效 XML编写模板
将会引发大量的人为错误和难以
理解的错误信息,而且使用 XML
引擎解析模板也会导致令人无法
容忍的模板处理开销。
假定设计师精通 HTML 编码 。模
板系统的设计意图并不是为了让
模板一定能够很好地显示在
Dreamweaver 这样的所见即所得编
辑器中。 这种限制过于苛刻,而
且会使得语法不能像目前这样的
完美。 Django 要求模板创作人员
对直接编辑 HTML非常熟悉。
假定设计师不是 Python 程序员 。
模板系统开发人员认为:模板通
常由设计师而非程序员来编写,
因此不应被假定拥有Python开发知
识。
当然,系统同样也特意地提供了
对那些 由 Python 程序员进行模板
制作的小型团队的支持。 它提供
了一种工作模式,允许通过编写
原生 Python 代码进行系统语法拓
展。 (详见第十章)
目标并不是要发明一种编程语
言 。目标是恰到好处地提供如分
支和循环这一类编程式功能,这
是进行与表现相关判断的基础。
在视图中使用模板
在学习了模板系统的基础之后,现在
让我们使用相关知识来创建视图。
重新打开我们在前一章
在 mysite.views中创建
的 current_datetime 视图。 以下是其
内容:
from django.http import HttpResponse
import datetime
def current_datetime(request):
now = datetime.datetime.now()
html = "<html><body>It is now %s
.
</body></html>" % now
return HttpResponse(html)
让我们用 Django 模板系统来修改该
视图。 第一步,你可能已经想到了
要做下面这样的修改:
from django.template import Template
,
Context
from django.http import HttpResponse
import datetime
def current_datetime(request):
now = datetime.datetime.now()
t = Template("<html><body>It is
now {{ current_date }}.</body></html
>
")
html = t.render(Context({'curren
t_date': now}))
return HttpResponse(html)
没错,它确实使用了模板系统,但是
并没有解决我们在本章开头所指出的
问题。 也就是说,模板仍然嵌入在
Python代码里,并未真正的实现数据
与表现的分离。 让我们将模板置于
一个 单独的文件 中,并且让视图加
载该文件来解决此问题。
你可能首先考虑把模板保存在文件系
统的某个位置并用 Python 内建的文件
操作函数来读取文件内容。 假设文
件保存
在 / home/ dj angouser/ templ ates/ mytempl
ate.html 中的话,代码就会像下面这
样:
from django.template import Template
,
Context
from django.http import HttpResponse
import datetime
def current_datetime(request):
now = datetime.datetime.now()
#
Simple way of using templates
from the filesystem.
This is BAD because it doesn't
#
account for missing files!
fp = open('/home/djangouser/temp
lates/mytemplate.html')
t = Template(fp.read())
fp.close()
html = t.render(Context({'curren
t_date': now}))
return HttpResponse(html)
然而,基于以下几个原因,该方法还
算不上简洁:
它没有对文件丢失的情况做出处
理。 如果文件 mytempl ate.html 不
存在或者不可读, open() 函数调
用将会引发 IOError 异常。
这里对模板文件的位置进行了硬
编码。 如果你在每个视图函数都
用该技术,就要不断复制这些模
板的位置。 更不用说还要带来大
量的输入工作!
它包含了大量令人生厌的重复代
码。 与其在每次加载模板时都调
用 open() 、 fp.read() 和 fp.close()
,
还不如做出更佳选择。
为了解决这些问题,我们采用了 模
板自加载 跟 模板目录 的技巧.
模板加载
为了减少模板加载调用过程及模板本
身的冗余代码,Django 提供了一种使
用方便且功能强大的 API ,用于从磁
盘中加载模板,
要使用此模板加载API,首先你必须
将模板的保存位置告诉框架。 设置
的保存文件就是我们前一章节讲述
ROOT_URLCONF配置的时候提到
的 settings.py 。
如果你是一步步跟随我们学习过来
的,马上打开你的settings.py配置文
件,找到TEMPLATE_DIRS这项设置
吧。 它的默认设置是一个空元组
(tuple),加上一些自动生成的注
释。
TEMPLATE_DIRS = (
Put strings here, like "/home/
#
html/django_templates" or "C:/www/dj
ango/templates".
#
Always use forward slashes, ev
en on Windows.
#
Don't forget to use absolute p
aths, not relative paths.
)
该设置告诉 Django 的模板加载机制
在哪里查找模板。 选择一个目录用
于存放模板并将其添加到
TEMPLATE_DIRS 中:
TEMPLATE_DIRS = (
'
/home/django/mysite/templates',
)
下面是一些注意事项:
你可以任意指定想要的目录,只
要运行 We b 服务器的用户可以读
取该目录的子目录和模板文件。
如果实在想不出合适的位置来放
置模板,我们建议在 Django 项目
中创建一个 templates 目录(也就
是说,如果你一直都按本书的范
例操作的话,在第二章创建
的 mysi te 目录中)。
如果你的 TEMPLATE_DIRS只包含
一个目录,别忘了在该目录后加
上个逗号。
Bad:
#
Missing comma!
TEMPLATE_DIRS = (
'
/home/django/mysite/templates'
)
>
Good:
#
Comma correctly in place.
TEMPLATE_DIRS = (
/home/django/mysite/templates',
'
)
Python 要求单元素元组中必须使用
逗号,以此消除与圆括号表达式
之间的歧义。 这是新手常犯的错
误。
如果使用的是 Window s 平台,请
包含驱动器符号并使用Uni x风格的
斜杠(/)而不是反斜杠(),就像
下面这样:
TEMPLATE_DIRS = (
C:/www/django/templates',
'
)
最省事的方式是使用绝对路径
(即从文件系统根目录开始的目
录路径)。 如果想要更灵活一点
并减少一些负面干扰,可利用
Django 配置文件就是 Python 代码
这一点来动态构
建 TEMPLATE_DIRS 的内容,
如: 例如:
import os.path
TEMPLATE_DIRS = (
os.path.join(os.path.dirname(__f
ile__), 'templates').replace('\\','/
'
)
),
这个例子使用了神奇的 Python 内
部变量 file ,该变量被自动设置为
代码所在的 Python 模块文件名。
os.path.dirname(__file__) 将
会获取自身所在的文件,即
settings.py 所在的目录,然后由
os.path.join 这个方法将这目录
与 templates 进行连接。如果在
windows下,它会智能地选择正确
的后向斜杠”“进行连接,而不是前
向斜杠”/”。
在这里我们面对的是动态语言
python代码,我需要提醒你的是,
不要在你的设置文件里写入错误
的代码,这很重要。 如果你在这
里引入了语法错误,或运行错
误,你的Django-powered站点将很
可能就要被崩溃掉。
完成 TEMPLATE_DIRS 设置后,下一
步就是修改视图代码,让它使用
Django 模板加载功能而不是对模板路
径硬编码。 返回 current_datetime 视
图,进行如下修改:
from django.template.loader import g
et_template
from django.template import Context
from django.http import HttpResponse
import datetime
def current_datetime(request):
now = datetime.datetime.now()
t = get_template('current_dateti
me.html')
html = t.render(Context({'curren
t_date': now}))
return HttpResponse(html)
此范例中,我们使用了函
数 django.template.loader.get_template(
) ,而不是手动从文件系统加载模
板。 该get_template() 函数以模板名
称为参数,在文件系统中找出模块的
位置,打开文件并返回一个编译好的
Templ ate 对象。
在这个例子里,我们选择的模板文件
是current_datetime.html,但这个
与.html后缀没有直接的联系。 你可
以选择任意后缀的任意文件,只要是
符合逻辑的都行。甚至选择没有后缀
的文件也不会有问题。
要确定某个模板文件在你的系统里的
位置, get_template()方法会自动为你
连接已经设置的 TEMPLATE_DIRS目
录和你传入该法的模板名称参数。比
如,你的 TEMPLATE_DIRS目录设置
为 '/home/django/mysite/templates',上
面的 get_template()调用就会为你找
到 /home/django/mysite/templates/curre
nt_datetime.html 这样一个位置。
如果 get_template() 找不到给定名称
的模板,将会引发一
个 Templ ateDoesNotExi st 异常。 要了
解究竟会发生什么,让我们按照第三
章内容,在 Django 项目目录中运
行 python manage.py runserver 命令,
再次启动Django开发服务器。 接着,
告诉你的浏览器,使其定位到指定页
面以激活current_datetime视图(如
http://127.0.0.1:8000/time/ )。假设你
的 DEBUG项设置为 Tr ue,而你有没
有建立current_datetime.html 这个模板
文件,你会看到Django的错误提示网
页,告诉你发生
了 Templ ateDoesNotExi st 错误。
图 4-1: 模板文件无法找到时,将会发
送提示错误的网页给用户。
该页面与我们在第三章解释过的错误
页面相似,只不过多了一块调试信息
区: 模板加载器事后检查区。 该区
域显示 Django 要加载哪个模板、每
次尝试出错的原因(如:文件不存在
等)。 当你尝试调试模板加载错误
时,这些信息会非常有帮助。
接下来,在模板目录中创建包括以下
模板代码 current_datetime.html 文件:
<
html><body>It is now {{ current_dat
e }}.</body></html>
在网页浏览器中刷新该页,你将会看
到完整解析后的页面。
render_to_response()
我们已经告诉你如何载入一个模板文
件,然后用 Context渲染它,最后返
回这个处理好的HttpResponse对象给
用户。 我们已经优化了方案,使
用 get_template() 方法代替繁杂的用
代码来处理模板及其路径的工作。
但这仍然需要一定量的时间来敲出这
些简化的代码。 这是一个普遍存在
的重复苦力劳动。Django为此提供了
一个捷径,让你一次性地载入某个模
板文件,渲染它,然后将此作
为 HttpResponse返回。
该捷径就是位于 django.shortcuts 模块
中名为 render_to_response() 的函数。
大多数情况下,你会使用``\ [ ]
(http://docs.30c.org/djangobook2/chapte
r04/index.html#id21)[``]
(http://docs.30c.org/djangobook2/chapte
r04/index.html#id23)对象,除非你的
老板以代码行数来衡量你的工作。
System Message: WARNING/ 2 (, line
1736); backlink
Inline literal start-string without end-
string.
System Message: WARNING/ 2 (, line
1736); backlink
Inline literal start-string without end-
string.
System Message: WARNING/ 2 (, line
736); backlink
1
Inline literal start-string without end-
string.
下面就是使用 render_to_response() 重
新编写过的 current_datetime 范例。
from django.shortcuts import render_
to_response
import datetime
def current_datetime(request):
now = datetime.datetime.now()
return render_to_response('curre
nt_datetime.html', {'current_date':
now})
大变样了! 让我们逐句看看代码发
生的变化:
我们不再需要导
入 get_template 、 Templ ate 、 Cont
ext 和 HttpResponse 。相反,我们
导入
django.shortcuts.render_to_response
。
import datetime 继续保留.
在 current_datetime 函数中,我们
仍然进行 now 计算,但模板加
载、上下文创建、模板解析和
HttpResponse 创建工作均在
对 render_to_response() 的调用中
完成了。 由
于 render_to_response() 返
回 HttpResponse 对象,因此我们
仅需在视图中 return 该值。
render_to_response() 的第一个参数必
须是要使用的模板名称。 如果要给
定第二个参数,那么该参数必须是为
该模板创建 Context 时所使用的字
典。 如果不提供第二个参
数, render_to_response() 使用一个空
字典。
locals() 技巧
思考一下我们对 current_datetime 的最
后一次赋值 :
def current_datetime(request):
now = datetime.datetime.now()
return render_to_response('curre
nt_datetime.html', {'current_date':
now})
很多时候,就像在这个范例中那样,
你发现自己一直在计算某个变量,保
存结果到变量中(比如前面代码中的
now ),然后将这些变量发送给模
板。 尤其喜欢偷懒的程序员应该注
意到了,不断地为临时变量_和_临时
模板命名有那么一点点多余。 不仅
多余,而且需要额外的输入。
如果你是个喜欢偷懒的程序员并想让
代码看起来更加简明,可以利用
Python 的内建函数 locals() 。它返回
的字典对所有局部变量的名称与值进
行映射。 因此,前面的视图可以重
写成下面这个样子:
def current_datetime(request):
current_date = datetime.datetime
.
now()
return render_to_response('curre
nt_datetime.html', locals())
在此,我们没有像之前那样手工指定
context 字典,而是传入了 locals() 的
值,它囊括了函数执行到该时间点时
所定义的一切变量。 因此,我们
将 now 变量重命名为 current_date ,
因为那才是模板所预期的变量名称。
在本例中, locals() 并没有带来
多 大 的改进,但是如果有多个模板
变量要界定而你又想偷懒,这种技术
可以减少一些键盘输入。
使用 locals() 时要注意是它将包括 所
有 的局部变量,它们可能比你想让
模板访问的要多。 在前例中,
locals() 还包含了 request 。对此如何
取舍取决你的应用程序。
get_template()中使用子目
录
把所有的模板都存放在一个目录下可
能会让事情变得难以掌控。 你可能
会考虑把模板存放在你模板目录的子
目录中,这非常好。 事实上,我们
推荐这样做;一些Django的高级特性
(
例如将在第十一章讲到的通用视图
系统)的缺省约定就是期望使用这种
模板布局。
把模板存放于模板目录的子目录中是
件很轻松的事情。 只需在调
用 get_template() 时,把子目录名和
一条斜杠添加到模板名称之前,如:
t = get_template('dateapp/current_da
tetime.html')
由于 render_to_response() 只是
对 get_template() 的简单封装, 你可
以对 render_to_response() 的第一个参
数做相同处理。
return render_to_response('dateapp/c
urrent_datetime.html', {'current_dat
e': now})
对子目录树的深度没有限制,你想要
多少层都可以。 只要你喜欢,用多
少层的子目录都无所谓。
注意
Windows用户必须使用斜杠而不是
反斜杠。 get_template() 假定的是
Uni x 风格的文件名符号约定。
include 模板标签
在讲解了模板加载机制之后,我们再
介绍一个利用该机制的内建模板标
签: {% include %} 。该标签允许在
(
模板中)包含其它的模板的内容。
标签的参数是所要包含的模板名称,
可以是一个变量,也可以是用单/双
引号硬编码的字符串。 每当在多个
模板中出现相同的代码时,就应该考
虑是否要使用 {% include %} 来减少
重复。
下面这两个例子都包含了 nav.html 模
板。这两个例子是等价的,它们证明
单/双引号都是允许的。
{
{
% include 'nav.html' %}
% include "nav.html" %}
下面的例子包含
了 includes/nav.html 模板的内容:
{
% include 'includes/nav.html' %}
下面的例子包含了以变
量 template_name 的值为名称的模板
内容:
{
% include template_name %}
和在 get_template() 中一样, 对模板
的文件名进行判断时会在所调取的模
板名称之前加上来
自 TEMPLATE_DIRS的模板目录。
所包含的模板执行时的 context 和包
含它们的模板是一样的。 举例说,
考虑下面两个模板文件:
#
mypage.html
<
<
{
<
<
<
html>
body>
% include "includes/nav.html" %}
h1>{{ title }}</h1>
/body>
/html>
#
<
includes/nav.html
div id="nav">
You are in: {{ current_section }
}
<
/div>
如果你用一个包含 current_section的
上下文去渲染 mypage.html这个模板
文件,这个变量将存在于它所包含
(
include)的模板里,就像你想象的
那样。
如果{% include %}标签指定的模板没
找到,Django将会在下面两个处理方
法中选择一个:
如果 DEBUG 设置为 Tr ue ,你将
会在 Django 错误信息页面看
到 Templ ateDoesNotExi st 异常。
如果 DEBUG 设置为 False ,该标
签不会引发错误信息,在标签位
置不显示任何东西。
模板继承
到目前为止,我们的模板范例都只是
些零星的 HTML片段,但在实际应用
中,你将用 Django 模板系统来创建
整个 HTML页面。 这就带来一个常
见的 We b 开发问题: 在整个网站
中,如何减少共用页面区域(比如站
点导航)所引起的重复和冗余代码?
解决该问题的传统做法是使用 服务
器端的 includes ,你可以在 HTML页
面中使用该指令将一个网页嵌入到另
一个中。 事实上, Django 通过刚才
讲述的 {% include %} 支持了这种方
法。 但是用 Django 解决此类问题的
首选方法是使用更加优雅的策略
—
— 模板继承 。
本质上来说,模板继承就是先构造一
个基础框架模板,而后在其子模板中
对它所包含站点公用部分和定义块进
行重载。
让我们通过修
改 current_datetime.html 文件,
为 current_datetime 创建一个更加完整
的模板来体会一下这种做法:
<
!DOCTYPE HTML PUBLIC "-//W3C//DTD H
TML 4.01//EN">
<
<
html lang="en">
head>
<
title>The current time</title>
<
<
/head>
body>
<
h1>My helpful timestamp site</h
1
<
>
<
p>It is now {{ current_date }}.
/p>
<
<
hr>
p>Thanks for visiting my site.<
/
<
<
p>
/body>
/html>
这看起来很棒,但如果我们要为第三
章的 hours_ahead 视图创建另一个模
板会发生什么事情呢?
<
!DOCTYPE HTML PUBLIC "-//W3C//DTD H
TML 4.01//EN">
<
<
html lang="en">
head>
<
title>Future time</title>
<
<
/head>
body>
<
h1>My helpful timestamp site</h
1
>
<
p>In {{ hour_offset }} hour(s),
it will be {{ next_time }}.</p>
<
<
hr>
p>Thanks for visiting my site.<
/
<
<
p>
/body>
/html>
很明显,我们刚才重复了大量的
HTML代码。 想象一下,如果有一个
更典型的网站,它有导航条、样式
表,可能还有一些 JavaScript 代码,
事情必将以向每个模板填充各种冗余
的 HTML而告终。
解决这个问题的服务器端 include 方
案是找出两个模板中的共同部分,将
其保存为不同的模板片段,然后在每
个模板中进行 include。 也许你会把
模板头部的一些代码保存
为 header.html 文件:
<
!DOCTYPE HTML PUBLIC "-//W3C//DTD H
TML 4.01//EN">
<
<
html lang="en">
head>
你可能会把底部保存到文
件 footer.html :
<
<
hr>
p>Thanks for visiting my site.<
/
<
<
p>
/body>
/html>
对基于 include 的策略,头部和底部
的包含很简单。 麻烦的是中间部
分。 在此范例中,每个页面都有一
个My helpful ti mestamp site 标题,但
是这个标题不能放在 header.html 中,
因为每个页面的 是不同的。 如果我
们将 包含在头部,我们就不得不包
含 ,但这样又不允许在每个页面对
它进行定制。 何去何从呢?
Django 的模板继承系统解决了这些问
题。 你可以将其视为服务器端
include 的逆向思维版本。 你可以对
那些不同 的代码段进行定义,而不
是 共同 代码段。
第一步是定义 基础模板 , 该框架之
后将由 子模板 所继承。 以下是我们
目前所讲述范例的基础模板:
<
!DOCTYPE HTML PUBLIC "-//W3C//DTD H
TML 4.01//EN">
<
<
html lang="en">
head>
<
title>{% block title %}{% endbl
ock %}</title>
<
<
/head>
body>
<
h1>My helpful timestamp site</h
1
}
>
{
% block content %}{% endblock %
{
<
<
% block footer %}
hr>
p>Thanks for visiting my site.<
/
p>
{
% endblock %}
<
<
/body>
/html>
这个叫做 base.html 的模板定义了一
个简单的 HTML框架文档,我们将在
本站点的所有页面中使用。 子模板
的作用就是重载、添加或保留那些块
的内容。 (如果你一直按顺序学习
到这里,保存这个文件到你的
template目录下,命名为 base.html .)
我们使用一个以前已经见过的模板标
签: {% block %} 。 所有
的 {% block %} 标签告诉模板引擎,
子模板可以重载这些部分。 每个
{
% block %}标签所要做的是告诉模
板引擎,该模板下的这一块内容将有
可能被子模板覆盖。
现在我们已经有了一个基本模板,我
们可以修改 current_datetime.html 模板
来 使用它:
{
{
% extends "base.html" %}
% block title %}The current time{%
endblock %}
{
<
{
% block content %}
p>It is now {{ current_date }}.</p>
% endblock %}
再为 hours_ahead 视图创建一个模
板,看起来是这样的:
{
{
% extends "base.html" %}
% block title %}Future time{% endbl
ock %}
{
<
% block content %}
p>In {{ hour_offset }} hour(s), it
will be {{ next_time }}.</p>
{
% endblock %}
看起来很漂亮是不是? 每个模板只
包含对自己而言 独一无二 的代码。
无需多余的部分。 如果想进行站点
级的设计修改,仅需修
改 base.html ,所有其它模板会立即
反映出所作修改。
以下是其工作方式。 在加
载 current_datetime.html 模板时,模板
引擎发现了 {% extends %} 标签, 注
意到该模板是一个子模板。 模板引
擎立即装载其父模板,即本例中
的 base.html 。
此时,模板引擎注意到 base.html 中
的三个 {% block %} 标签,并用子模
板的内容替换这些 block 。因此,引
擎将会使用我们在 { block title %} 中
定义的标题,
对 {% block content %} 也是如此。
所以,网页标题一块将
由 {% block title %}替换,同样地,
网页的内容一块将
由 {% block content %}替换。
注意由于子模板并没有定
义 footer 块,模板系统将使用在父模
板中定义的值。 父模
板 {% block %} 标签中的内容总是被
当作一条退路。
继承并不会影响到模板的上下文。
换句话说,任何处在继承树上的模板
都可以访问到你传到模板中的每一个
模板变量。
你可以根据需要使用任意多的继承次
数。 使用继承的一种常见方式是下
面的三层法:
1
. 创建 base.html 模板,在其中定义
站点的主要外观感受。 这些都是
不常修改甚至从不修改的部分。
2
. 为网站的每个区域创
建 base_SECTION.html 模板(例
如, base_photos.html 和 base_forum.
html )。这些模板对 base.html 进行
拓展,并包含区域特定的风格与
设计。
3
. 为每种类型的页面创建独立的模
板,例如论坛页面或者图片库。
这些模板拓展相应的区域模板。
这个方法可最大限度地重用代码,并
使得向公共区域(如区域级的导航)
添加内容成为一件轻松的工作。
以下是使用模板继承的一些诀窍:
如果在模板中使
用 {% extends %} ,必须保证其为
模板中的第一个模板标记。 否
则,模板继承将不起作用。
一般来说,基础模板中
的 {% block %} 标签越多越好。
记住,子模板不必定义父模板中
所有的代码块,因此你可以用合
理的缺省值对一些代码块进行填
充,然后只对子模板所需的代码
块进行(重)定义。 俗话说,钩
子越多越好。
如果发觉自己在多个模板之间拷
贝代码,你应该考虑将该代码段
放置到父模板的某
个 {% block %} 中。
如果你需要访问父模板中的块的
内容,使用 {{ block.super }}这个
标签吧,这一个魔法变量将会表
现出父模板中的内容。 如果只想
在上级代码块基础上添加内容,
而不是全部重载,该变量就显得
非常有用了。
不允许在同一个模板中定义多个
同名的 {% block %} 。 存在这样
的限制是因为block 标签的工作方
式是双向的。 也就是说,block 标
签不仅挖了一个要填的坑,也定
义了在_父模板中这个坑所填充的
内容。如果模板中出现了两个相
同名称的 {% block %}_ 标签,父
模板将无从得知要使用哪个块的
内容。
{
% extends %} 对所传入模板名称
使用的加载方法
和 get_template() 相同。 也就是
说,会将模板名称被添加
到 TEMPLATE_DIRS 设置之后。
多数情况下, {% extends %} 的参
数应该是字符串,但是如果直到
运行时方能确定父模板名,这个
参数也可以是个变量。 这使得你
能够实现一些很酷的动态功能。
下一章
你现在已经掌握了模板系统的基本知
识。 接下来呢?
时下大多数网站都是 数据库驱
动 的:网站的内容都是存储在关系
型数据库中。 这使得数据和逻辑能
够彻底地分开(视图和模板也以同样
方式对逻辑和显示进行了分隔。 )
下一章将讲述如何与数据库打交道。
在第三章,我们讲述了用 Django 建
造网站的基本途径: 建立视图和
URLConf 。 正如我们所阐述的,视
图负责处理一些主观逻辑,然后返回
响应结果。 作为例子之一,我们的
主观逻辑是要计算当前的日期和时
间。
在当代 Web 应用中,主观逻辑经常
牵涉到与数据库的交互。 数据库驱
动网站 在后台连接数据库服务器,
从中取出一些数据,然后在 Web 页
面用漂亮的格式展示这些数据。 这
个网站也可能会向访问者提供修改数
据库数据的方法。
许多复杂的网站都提供了以上两个功
能的某种结合。 例如 Amazon.com 就
是一个数据库驱动站点的良好范例。
本质上,每个产品页面都是数据库中
数据以 HTML格式进行的展现,而当
你发表客户评论时,该评论被插入评
论数据库中。
由于先天具备 Python 简单而强大的数
据库查询执行方法,Django 非常适合
开发数据库驱动网站。 本章深入介
绍了该功能: Django 数据库层。
(注意: 尽管对 Django 数据库层的
使用中并不特别强调这点,但是我们
还是强烈建议您掌握一些数据库和
SQL原理。 对这些概念的介绍超越
了本书的范围,但就算你是数据库方
面的菜鸟,我们也建议你继续阅读。
你也许能够跟上进度,并在上下文学
习过程中掌握一些概念。)
在视图中进行数据库
查询的笨方法
正如第三章详细介绍的那个在视图中
输出 HTML的笨方法(通过在视图里
对文本直接硬编码HTML),在视图
中也有笨方法可以从数据库中获取数
类库执行一条 SQL查询并对结果进
行一些处理。
在本例的视图中,我们使用
了 MySQLdb 类库(可以
从 http://www.djangoproject.com/r/pyth
on-mysql / 获得)来连接 MySQL数据
库,取回一些记录,将它们提供给模
板以显示一个网页:
from django.shortcuts import render_
to_response
import MySQLdb
def book_list(request):
db = MySQLdb.connect(user='me',
db='mydb', passwd='secret', host='lo
calhost')
cursor = db.cursor()
cursor.execute('SELECT name FROM
books ORDER BY name')
names = [row[0] for row in curso
r.fetchall()]
db.close()
return render_to_response('book_
list.html', {'names': names})
这个方法可用,但很快一些问题将出
现在你面前:
我们将数据库连接参数硬行编码
于代码之中。 理想情况下,这些
参数应当保存在 Django 配置中。
我们不得不重复同样的代码: 创
建数据库连接、创建数据库游
标、执行某个语句、然后关闭数
据库。 理想情况下,我们所需要
应该只是指定所需的结果。
它把我们栓死在 MySQL之上。
如果过段时间,我们要从 MySQL
换到 PostgreSQL,就不得不使用
不同的数据库适配器(例
如 psycopg 而不是 MySQLdb ),
改变连接参数,根据 SQL语句的
类型可能还要修改SQL。 理想情
况下,应对所使用的数据库服务
器进行抽象,这样一来只在一处
修改即可变换数据库服务器。
(
如果你正在建立一个开源的
Django应用程序来尽可能让更多
人使用的话,这个特性是非常适
当的。)
正如你所期待的,Django数据库层正
是致力于解决这些问题。 以下提前
揭示了如何使用 Django 数据库 API
重写之前那个视图。
from django.shortcuts import render_
to_response
from mysite.books.models import Book
def book_list(request):
books = Book.objects.order_by('n
ame')
return render_to_response('book_
list.html', {'books': books})
我们将在本章稍后的地方解释这段代
码。 目前而言,仅需对它有个大致
的认识。
MTV 开发模式
在钻研更多代码之前,让我们先花点
时间考虑下 Django 数据驱动 Web 应
用的总体设计。
我们在前面章节提到过,Django 的设
计鼓励松耦合及对应用程序中不同部
分的严格分割。 遵循这个理念的
话,要想修改应用的某部分而不影响
其它部分就比较容易了。 在视图函
数中,我们已经讨论了通过模板系统
把业务逻辑和表现逻辑分隔开的重要
性。 在数据库层中,我们对数据访
问逻辑也应用了同样的理念。
把数据存取逻辑、业务逻辑和表现逻
辑组合在一起的概念有时被称为软件
架构的 Model-View-Controller(MVC)
模式。 在这个模式中, Model 代表
数据存取层,View 代表的是系统中
选择显示什么和怎么显示的部分,
Controller 指的是系统中根据用户输
入并视需要访问模型,以决定使用哪
个视图的那部分。
为什么用缩写?
像 MVC 这样的明确定义模式的主要
用于改善开发人员之间的沟通。 比
起告诉同事,“让我们采用抽象的数
据存取方式,然后单独划分一层来显
示数据,并且在中间加上一个控制它
的层”,一个通用的说法会让你收
益,你只需要说:“我们在这里使用
MVC模式吧。”。
Django 紧紧地遵循这种 MVC 模式,
可以称得上是一种 MVC 框架。 以下
是 Django 中 M、V 和 C 各自的含
义:
M ,数据存取部分,由django数据
库层处理,本章要讲述的内容。
V ,选择显示哪些数据要显示以
及怎样显示的部分,由视图和模
板处理。
C ,根据用户输入委派视图的部
分,由 Django 框架根据 URLconf
设置,对给定 URL调用适当的
Python 函数。
由于 C 由框架自行处理,而 Django
里更关注的是模型(Model)、模板
(Template)和视图(Views),Django
也被称为 MTV 框架 。在 MTV 开发
模式中:
M 代表模型(Model),即数据存
取层。 该层处理与数据相关的所
有事务: 如何存取、如何验证有
效性、包含哪些行为以及数据之
间的关系等。
T 代表模板(Template),即表现
层。 该层处理与表现相关的决
定: 如何在页面或其他类型文档
中进行显示。
V 代表视图(View),即业务逻
辑层。 该层包含存取模型及调取
恰当模板的相关逻辑。 你可以把
它看作模型与模板之间的桥梁。
如果你熟悉其它的 MVC Web开发框
架,比方说 Ruby on Rails,你可能会
认为 Django 视图是控制器,而
Django 模板是视图。 很不幸,这是
对 MVC 不同诠释所引起的错误认
识。 在 Django 对 MVC 的诠释中,
视图用来描述要展现给用户的数据;
不是数据 _如何_展现 ,而且展现 哪
些 数据。 相比之下,Ruby on Rails
及一些同类框架提倡控制器负责决定
向用户展现哪些数据,而视图则仅决
定 如何 展现数据,而不是展现 哪
些 数据。
两种诠释中没有哪个更加正确一些。
重要的是要理解底层概念。
数据库配置
记住这些理念之后,让我们来开始
Django 数据库层的探索。 首先,我
们需要做些初始配置;我们需要告诉
Django使用什么数据库以及如何连接
数据库。
我们假定你已经完成了数据库服务器
的安装和激活,并且已经在其中创建
了数据库(例如,
用 CREATE DATABASE语句)。 如
果你使用SQLite,不需要这步安装,
因为SQLite使用文件系统上的独立文
件来存储数据。
象前面章节提到
的 TEMPLATE_DIRS 一样,数据库配
置也是在Django的配置文件里,缺省
是 settings.py 。 打开这个文件并查找
数据库配置:
DATABASE_ENGINE = ''
DATABASE_NAME = ''
DATABASE_USER = ''
DATABASE_PASSWORD = ''
DATABASE_HOST = ''
DATABASE_PORT = ''
配置纲要如下。
DATABASE_ENGINE 告诉Django使
用哪个数据库引擎。 如果你在
Django 中使用数据
库, DATABASE_ENGINE必须是
Table 5-1 中所列出的值。
表 5-1. 数据库引
擎设置
设置
postgresql
PostgreSQL
postgresql_psycopg2 PostgreSQL
mys ql
sqlite3
oracle
MySQL
SQLite
Oracle
要注意的是无论选择使用哪个数据库
服务器,都必须下载和安装对应的数
配器”一栏中的链接,可通过互联网
Linux,你的发布包管理系统会提供
合适的包。 比如说查找
python-postgresql 或者
python-psycopg 的软件包。
配置示例:
DATABASE_ENGINE = 'postgresql_psycop
g2'
DATABASE_NAME 将数据库名称告
知 Django 。 例如:
DATABASE_NAME = 'mydb'
如果使用 SQLite,请对数据库文件指
定完整的文件系统路径。 例如:
DATABASE_NAME = '/home/django/mydata
.
db'
在这个例子中,我们将SQLite数据库
放在/home/django目录下,你可以任
意选用最合适你的目录。
DATABASE_USER 告诉 Django 用哪
个用户连接数据库。 例如: 如果用
SQLite,空白即可。
DATABASE_PASSWORD 告诉Django
连接用户的密码。 SQLite 用空密码
即可。
DATABASE_HOST 告诉 Django 连接
哪一台主机的数据库服务器。 如果
数据库与 Django 安装于同一台计算
机(即本机),可将此项保留空白。
如果你使用SQLite,此项留空。
此处的 MySQL是一个特例。 如果使
用的是 MySQL且该项设置值由斜杠
(
'/' )开头,MySQL将通过 Uni x
socket 来连接指定的套接字,例如:
DATABASE_HOST = '/var/run/mysql'
一旦在输入了那些设置并保存之后应
当测试一下你的配置。 我们可以在
mysite 项目目录下执行上章所提到
的 python manage.py shell 来进行
测试。 (我们上一章提到过在,
manager.py shell 命令是以正确
Django配置启用Python交互解释器的
一种方法。 这个方法在这里是很有
必要的,因为Django需要知道加载哪
个配置文件来获取数据库连接信
息。)
输入下面这些命令来测试你的数据库
配置:
>
>
>> from django.db import connection
>> cursor = connection.cursor()
如果没有显示什么错误信息,那么你
的数据库配置是正确的。 否则,你
就得 查看错误信息来纠正错误。 表
5-2 是一些常见错误。
表 5-2. 数据库配置错误信息
错
不
D
表
You haven’t set the
DATABASE_ENGINE setting yet.
使
p
Environment variable
DJANGO_SETTINGS_MODULE 命
is undefined.
以
互
未
(例
Error loading module: No
module named .
如
Dj
以
把
置
库
误
_
____ isn’t an available database
backend.
设
存
据
C
database _____ does not exist
role _____ does not exist
建
设
存
库
查
DA
could not connect to server
确
器
第一个应用程序
你现在已经确认数据库连接正常工作
了,让我们来创建一个 Django app-
一个包含模型,视图和Django代码,
并且形式为独立Python包的完整
Django应用。
在这里要先解释一些术语,初学者可
能会混淆它们。 在第二章我们已经
创建了 project , 那么 project 和 _app_
之间到底有什么不同呢?它们的区别
就是一个是配置另一个是 代码:
一个project包含很多个Django app
以及对它们的配置。
技术上,project的作用是提供配置
文件,比方说哪里定义数据库连
接信息, 安装的app列表,
TEMPLATE_DIRS ,等等。
一个app是一套Django功能的集
合,通常包括模型和视图,按
Python的包结构的方式存在。
例如,Django本身内建有一些
app,例如注释系统和自动管理界
面。 app的一个关键点是它们是很
容易移植到其他project和被多个
project复用。
对于如何架构Django代码并没有快速
成套的规则。 如果你只是建造一个
简单的Web站点,那么可能你只需要
一个app就可以了; 但如果是一个包
含许多不相关的模块的复杂的网站,
例如电子商务和社区之类的站点,那
么你可能需要把这些模块划分成不同
的app,以便以后复用。
不错,你可以不用创建app,这一点
应经被我们之前编写的视图函数的例
子证明了 。 在那些例子中,我们只
是简单的创建了一个称为views.py的
文件,编写了一些函数并在URLconf
中设置了各个函数的映射。 这些情
况都不需要使用apps。
但是,系统对app有一个约定: 如果
你使用了Django的数据库层(模
型),你 必须创建一个Django app。
模型必须存放在apps中。 因此,为了
开始建造 我们的模型,我们必须创
建一个新的app。
在 mysite 项目文件下输入下面的命
令来创建 books app:
python manage.py startapp books
这个命令并没有输出什么,它只
在 mysi te 的目录里创建了一
个 books 目录。 让我们来看看这个目
录的内容:
books/
_
_init__.py
models.py
tests.py
views.py
这个目录包含了这个app的模型和视
图。
使用你最喜欢的文本编辑器查看一
下 models.py 和 views.py 文件的内
容。 它们都是空的,除
了 models.py 里有一个 import。这就
是你Django app的基础。
在Python代码里定义
模型
我们早些时候谈到。MTV里的M代表
模型。 Django模型是用Python代码形
式表述的数据在数据库中的定义。
对数据层来说它等同于 CREATE
TABLE 语句,只不过执行的是Python
代码而不是 SQL,而且还包含了比数
据库字段定义更多的含义。 Django用
模型在后台执行SQL代码并把结果用
Python的数据结构来描述。 Django也
使用模型来呈现SQL无法处理的高级
概念。
如果你对数据库很熟悉,你可能马上
就会想到,用Python 和 SQL来定义数
据模型是不是有点多余? Django这样
做是有下面几个原因的:
自省(运行时自动识别数据库)会导
致过载和有数据完整性问题。 为了
提供方便的数据访问API, Django需
要以 某种方式 知道数据库层内部信
息,有两种实现方式。 第一种方式
是用Python明确地定义数据模型,第
二种方式是通过自省来自动侦测识别
数据模型。
第二种方式看起来更清晰,因为数据
表信息只存放在一个地方-数据库
里,但是会带来一些问题。 首先,
运行时扫描数据库会带来严重的系统
过载。 如果每个请求都要扫描数据
库的表结构,或者即便是 服务启动
时做一次都是会带来不能接受的系统
过载。 (有人认为这个程度的系统
过载是可以接受的,而Django开发者
的目标是尽可能地降低框架的系统过
载)。第二,某些数据库,尤其是老
版本的MySQL,并未完整存储那些精
确的自省元数据。
编写Python代码是非常有趣的,保持
用Python的方式思考会避免你的大脑
在不同领域来回切换。 尽可能的保
持在单一的编程环境/思想状态下可
以帮助你提高生产率。 不得不去重
复写SQL,再写Python代码,再写
SQL,…,会让你头都要裂了。
把数据模型用代码的方式表述来让你
可以容易对它们进行版本控制。 这
样,你可以很容易了解数据层 的变
动情况。
SQL只能描述特定类型的数据字段。
例如,大多数数据库都没有专用的字
段类型来描述Email地址、URL。 而
用Django的模型可以做到这一点。 好
处就是高级的数据类型带来更高的效
率和更好的代码复用。
SQL还有在不同数据库平台的兼容性
问题。 发布Web应用的时候,使用
Python模块描述数据库结构信息可以
避免为MySQL, PostgreSQL, and
SQLite编写不同的CREATE TABLE。
当然,这个方法也有一个缺点,就是
Python代码和数据库表的同步问题。
如果你修改了一个Django模型, 你要
自己来修改数据库来保证和模型同
步。 我们将在稍后讲解解决这个问
题的几种策略。
最后,我们要提醒你Django提供了实用
工具来从现有的数据库表中自动扫描
生成模型。 这对已有的数据库来说
是非常快捷有用的。 我们将在第18
章中对此进行讨论。
第一个模型
在本章和后续章节里,我们把注意力
放在一个基本的 书籍/作者/出版商 数
据库结构上。 我们这样做是因为 这
是一个众所周知的例子,很多SQL有
关的书籍也常用这个举例。 你现在
看的这本书也是由作者 创作再由出
版商出版的哦!
我们来假定下面的这些概念、字段和
关系:
一个作者有姓,有名及email地
址。
出版商有名称,地址,所在城
市、省,国家,网站。
书籍有书名和出版日期。 它有一
个或多个作者(和作者是多对多
的关联关系[ many- to- many]), 只
有一个出版商(和出版商是一对
多的关联关系[one-to-many],也被
称作外键[foreign key])
第一步是用Python代码来描述它们。
打开由 startapp 命令创建的
models.py 并输入下面的内容:
from django.db import models
class Publisher(models.Model):
name = models.CharField(max_leng
th=30)
address = models.CharField(max_l
ength=50)
city = models.CharField(max_leng
th=60)
state_province = models.CharFiel
d(max_length=30)
country = models.CharField(max_l
ength=50)
website = models.URLField()
class Author(models.Model):
first_name = models.CharField(ma
x_length=30)
last_name = models.CharField(max
_
length=40)
email = models.EmailField()
class Book(models.Model):
title = models.CharField(max_len
gth=100)
authors = models.ManyToManyField
(Author)
publisher = models.ForeignKey(Pu
blisher)
publication_date = models.DateFi
eld()
让我们来快速讲解一下这些代码的含
义。 首先要注意的事是每个数据模
型都是 django.db.models.Model 的子
类。它的父类 Model 包含了所有必要
的和数据库交互的方法,并提供了一
个简洁漂亮的定义数据库字段的语
法。 信不信由你,这些就是我们需
要编写的通过Django存取基本数据的
所有代码。
每个模型相当于单个数据库表,每个
属性也是这个表中的一个字段。 属
性名就是字段名,它的类型(例如
CharField )相当于数据库的字段类
型 (例如 varchar )。例
如, Publisher 模块等同于下面这张
表(用PostgreSQL
的 CREATE TABLE 语法描述):
CREATE TABLE "books_publisher" (
"
id" serial NOT NULL PRIMARY KEY
,
"
"
"
"
name" varchar(30) NOT NULL,
address" varchar(50) NOT NULL,
city" varchar(60) NOT NULL,
state_province" varchar(30) NOT
NULL,
"
"
country" varchar(50) NOT NULL,
website" varchar(200) NOT NULL
)
;
事实上,正如过一会儿我们所要展示
的,Django 可以自动生成这
些 CREATE TABLE 语句。
“
每个数据库表对应一个类”这条规则
的例外情况是多对多关系。 在我们
的范例模型中, Book 有一个多对多
字段 叫做 authors 。 该字段表明一本
书籍有一个或多个作者,但 Book 数
据库表却并没有 authors 字段。 相
反,Django创建了一个额外的表(多
对多连接表)来处理书籍和作者之间
的映射关系。
请查看附录 B 了解所有的字段类型和
模型语法选项。
最后需要注意的是,我们并没有显式
地为这些模型定义任何主键。 除非
你单独指明,否则Django会自动为每
个模型生成一个自增长的整数主键字
段每个Django模型都要求有单独的主
键。id
模型安装
完成这些代码之后,现在让我们来在
数据库中创建这些表。 要完成该项
工作,第一步是在 Django 项目中 _激
活_这些模型。 将 books app 添加到
配置文件的已安装应用列表中即可完
成此步骤。
再次编辑 settings.py 文件, 找
到 INSTALLED_APPS 设
置。 INSTALLED_APPS 告诉 Django
项目哪些 app 处于激活状态。 缺省
情况下如下所示:
INSTALLED_APPS = (
'
'
'
'
django.contrib.auth',
django.contrib.contenttypes',
django.contrib.sessions',
django.contrib.sites',
)
把这四个设置前面加#临时注释起
来。 (这四个app是经常使用到的,
我们将在后续章节里讨论如何使用它
们)。同时,注释掉
MIDDLEWARE_CLASSES的默认设置
条目,因为这些条目是依赖于刚才我
们刚在INSTALLED_APPS注释掉的
apps。 然后,添加 ‘mysite.books’
到 INSTALLED_APPS 的末尾,此时设
置的内容看起来应该是这样的:
MIDDLEWARE_CLASSES = (
#
'django.middleware.common.Comm
onMiddleware',
#
'django.contrib.sessions.middl
eware.SessionMiddleware',
#
'django.contrib.auth.middlewar
e.AuthenticationMiddleware',
)
INSTALLED_APPS = (
#
#
#
#
'
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
mysite.books',
)
(就像我们在上一章设置
TEMPLATE_DIRS所提到的逗号,同
样在INSTALLED_APPS的末尾也需添
加一个逗号,因为这是个单元素的元
组。 另外,本书的作者喜欢在 每一
个 tuple元素后面加一个逗号,不管
它是不是 只有一个元素。 这是为了
避免忘了加逗号,而且也没什么坏
处。 )
'
mysite.books'指示我们正在编写的
books app。 INSTALLED_APPS 中的
每个app都使用 Python的路径描述,
包的路径,用小数点“.”间隔。
现在我们可以创建数据库表了。 首
先,用下面的命令验证模型的有效
性:
python manage.py validate
validate 命令检查你的模型的语法和
逻辑是否正确。 如果一切正常,你
会看到 0 errors found 消息。如果出
错,请检查你输入的模型代码。 错
误输出会给出非常有用的错误信息来
帮助你修正你的模型。
一旦你觉得你的模型可能有问题,运
行 python manage.py validate 。 它可
以帮助你捕获一些常见的模型定义错
误。
模型确认没问题了,运行下面的命令
来生成 CREATE TABLE 语句(如果
你使用的是Uni x,那么可以启用语法
高亮):
python manage.py sqlall books
在这个命令行中, books 是app的名
称。 和你运行 manage.py startapp 中
的一样。执行之后,输出如下:
BEGIN;
CREATE TABLE "books_publisher" (
"
id" serial NOT NULL PRIMARY KEY
,
"
"
"
"
name" varchar(30) NOT NULL,
address" varchar(50) NOT NULL,
city" varchar(60) NOT NULL,
state_province" varchar(30) NOT
NULL,
"
"
country" varchar(50) NOT NULL,
website" varchar(200) NOT NULL
)
;
CREATE TABLE "books_author" (
"
"
"
"
id" serial NOT NULL PRIMARY KEY
first_name" varchar(30) NOT NUL
last_name" varchar(40) NOT NULL
email" varchar(75) NOT NULL
,
L,
,
)
;
CREATE TABLE "books_book" (
"
id" serial NOT NULL PRIMARY KEY
,
"
"
title" varchar(100) NOT NULL,
publisher_id" integer NOT NULL
REFERENCES "books_publisher" ("id")
DEFERRABLE INITIALLY DEFERRED,
"
publication_date" date NOT NULL
)
;
CREATE TABLE "books_book_authors" (
"
id" serial NOT NULL PRIMARY KEY
,
"
book_id" integer NOT NULL REFER
ENCES "books_book" ("id") DEFERRABLE
INITIALLY DEFERRED,
"
author_id" integer NOT NULL REF
ERENCES "books_author" ("id") DEFERR
ABLE INITIALLY DEFERRED,
UNIQUE ("book_id", "author_id")
)
;
CREATE INDEX "books_book_publisher_i
d" ON "books_book" ("publisher_id");
COMMIT;
注意:
自动生成的表名是app名称
(
(
books )和模型的小写名称
publisher , book , author )的组
合。你可以参考附录B重写这个规
则。
我们前面已经提到,Django为每
个表格自动添加加了一个 id 主
键, 你可以重新设置它。
按约定,Django添加 "_id" 后缀到
外键字段名。 你猜对了,这个同
样是可以自定义的。
外键是用 REFERENCES 语句明确
定义的。
这些 CREATE TABLE 语句会根据
你的数据库而作调整,这样象数
据库特定的一些字段例如:
(
MySQL),
auto_increment(PostgreSQL),seri
al(SQLite),都会自动生成。
integer primary key同样的,字段名
称也是自动处理(例如单引号还
好是双引号)。 例子中的输出是
基于PostgreSQL语法的。
sqlall 命令并没有在数据库中真正创
建数据表,只是把SQL语句段打印出
来,这样你可以看到Django究竟会做
些什么。 如果你想这么做的话,你
可以把那些SQL语句复制到你的数据
库客户端执行,或者通过Uni x管道直
接进行操作(例如,
python manager.py sqlall books |
)
。不过,Django提供了一种更为简
易的提交SQL语句至数据库的方法:
syncdb 命令
python manage.py syncdb
执行这个命令后,将看到类似以下的
内容:
Creating table books_publisher
Creating table books_author
Creating table books_book
Installing index for books.Book model
syncdb 命令是同步你的模型到数据库
的一个简单方法。 它会根
据 INSTALLED_APPS 里设置的app来
检查数据库, 如果表不存在,它就
会创建它。 需要注意的
是, syncdb 并 _不能_将模型的修改
或删除同步到数据库;如果你修改或
删除了一个模型,并想把它提交到数
据库,syncdb并不会做出任何处理。
(更多内容请查看本章最后的“修改
数据库的架构”一段。)
如果你再次运
行 python manage.py syncdb ,什么也
没发生,因为你没有添加新的模型或
者 添加新的app。因此,运行
python manage.py syncdb总是安全的,
因为它不会重复执行SQL语句。
如果你有兴趣,花点时间用你的SQL
客户端登录进数据库服务器看看刚才
Django创建的数据表。 你可以手动启
动命令行客户端(例如,执行
PostgreSQL的 psql 命令),也可以
执行 python manage.py dbshell ,
这个命令将依据 DATABASE_SERVER
的里设置自动检测使用哪种命令行客
户端。 常言说,后来者居上。
基本数据访问
一旦你创建了模型,Django自动为这
些模型提供了高级的Python API。 运
行 python manage.py shell 并输入下面
的内容试试看:
>
>> from books.models import Publish
er
>
>> p1 = Publisher(name='Apress', ad
dress='2855 Telegraph Avenue',
.
..
city='Berkeley', state_provi
nce='CA', country='U.S.A.',
.. website='http://www.apress.c
om/')
.
>
>
>> p1.save()
>> p2 = Publisher(name="O'Reilly",
address='10 Fawcett St.',
.. city='Cambridge', state_prov
ince='MA', country='U.S.A.',
.. website='http://www.oreilly.
com/')
.
.
>
>
>> p2.save()
>> publisher_list = Publisher.objec
ts.all()
>
[
>> publisher_list
, ]
这短短几行代码干了不少的事。 这
里简单的说一下:
首先,导入Publisher模型类, 通
过这个类我们可以与包含 出版社
的数据表进行交互。
接着,创建一个 Publisher 类的
实例并设置了字段
name, address 等的值。
调用该对象的 save() 方法,将对
象保存到数据库中。 Django 会在
后台执行一条 INSERT 语句。
最后,使用 Publisher.objects
属性从数据库取出出版商的信
息,这个属性可以认为是包含出
版商的记录集。 这个属性有许多
方法, 这里先介绍调用
Publisher.objects.all() 方法
获取数据库中 Publisher 类的所
有对象。这个操作的幕后,
Django执行了一条SQL SELECT 语
句。
这里有一个值得注意的地方,在这个
例子可能并未清晰地展示。 当你使
用Django modle API创建对象时
Django并未将对象保存至数据库内,
除非你调用 save() 方法:
p1 = Publisher(...)
At this point, p1 is not saved to
#
the database yet!
p1.save()
#
Now it is.
如果需要一步完成对象的创建与存储
至数据库,就使用
objects.create() 方法。 下面的例
子与之前的例子等价:
>
>> p1 = Publisher.objects.create(na
me='Apress',
..
ue',
..
.
address='2855 Telegraph Aven
.
city='Berkeley', state_provi
nce='CA', country='U.S.A.',
.. website='http://www.apress.c
om/')
>> p2 = Publisher.objects.create(na
.
>
me="O'Reilly",
.. address='10 Fawcett St.', ci
ty='Cambridge',
.
.
=
.
..
'U.S.A.',
.. website='http://www.oreilly.
state_province='MA', country
com/')
>> publisher_list = Publisher.objec
ts.all()
>> publisher_list
>
>
当然,你肯定想执行更多的Django数
据库API试试看,不过,还是让我们
先解决一点烦人的小问题。
添加模块的字符串表
现
当我们打印整个publisher列表时,我
们没有得到想要的有用信息,无法把
[
<Publisher: Publisher object>, <Pub
lisher: Publisher object>]
我们可以简单解决这个问题,只需要
为Publisher 对象添加一个方
法 unicode() 。 unicode() 方法告诉
Python如何将对象以unicode的方式显
示出来。 为以上三个模型添
加unicode()方法后,就可以看到效果
了:
from django.db import models
class Publisher(models.Model):
name = models.CharField(max_leng
th=30)
address = models.CharField(max_l
ength=50)
city = models.CharField(max_leng
th=60)
state_province = models.CharFiel
d(max_length=30)
country = models.CharField(max_l
ength=50)
website = models.URLField()
def __unicode__(self):
return self.name
class Author(models.Model):
first_name = models.CharField(ma
x_length=30)
last_name = models.CharField(max
_
length=40)
email = models.EmailField()
def __unicode__(self):
return u'%s %s' % (self.firs
t_name, self.last_name)
class Book(models.Model):
title = models.CharField(max_len
gth=100)
authors = models.ManyToManyField
(Author)
publisher = models.ForeignKey(Pu
blisher)
publication_date = models.DateFi
eld()
def __unicode__(self):
return self.title
就象你看到的一样, unicode() 方法
可以进行任何处理来返回对一个对象
的字符串表示。 Publisher和Book对象
的unicode()方法简单地返回各自的名
称和标题,Author对象的unicode()方
法则稍微复杂一些,它将first_name和
last_name字段值以空格连接后再返
回。
对unicode()的唯一要求就是它要返回
一个unicode对象 如果
_
_unicode__() 方法未返回一个
Unicode对象,而返回比如说一个整
型数字,那么Python将抛出一个
TypeError 错误,并提
示:”coercing to Unicode: need string
or buffer, i nt found” 。
Unicode对象
什么是Unicode对象呢?
你可以认为unicode对象就是一个
Python字符串,它可以处理上百万
不同类别的字符——从古老版本
的Latin字符到非Latin字符,再到
曲折的引用和艰涩的符号。
普通的python字符串是经过_编码_
的,意思就是它们使用了某种编
码方式(如ASCII,ISO-8859-1或
者UTF-8)来编码。 如果你把奇特
的字符(其它任何超出标准128个
如0-9和A-Z之类的ASCII字符)保
存在一个普通的Python字符串里,
你一定要跟踪你的字符串是用什
么编码的,否则这些奇特的字符
可能会在显示或者打印的时候出
现乱码。 当你尝试要将用某种编
码保存的数据结合到另外一种编
码的数据中,或者你想要把它显
示在已经假定了某种编码的程序
中的时候,问题就会发生。 我们
都已经见到过网页和邮件被???弄
得乱七八糟。 ?????? 或者其它出
现在奇怪位置的字符:这一般来
说就是存在编码问题了。
但是Unicode对象并没有编码。它
们使用Unicode,一个一致的,通
用的字符编码集。 当你在Python中
处理Unicode对象的时候,你可以
直接将它们混合使用和互相匹配
而不必去考虑编码细节。
Django 在其内部的各个方面都使
用到了 Uni code 对象。 模型 对象
中,检索匹配方面的操作使用的
是 Uni code 对象,视图 函数之间
的交互使用的是 Uni code 对象,模
板的渲染也是用的 Uni code 对象。
通常,我们不必担心编码是否正
确,后台会处理的很好。
注意,我们这里只是对Unicode对象
进行非常浅显的概述,若要深入了解
你可能需要查阅相关的资料。 这是
一个很好的起
点:http://www.joelonsoftware.com/art
icles/Unicode.html。
为了让我们的修改生效,先退出
Python Shell,然后再次运
行 python manage.py shell 进入。(这
是保证代码修改生效的最简单方
法。)现在 Publisher 对象列表容易
理解多了。
>
>> from books.models import Publish
er
>
>> publisher_list = Publisher.objec
ts.all()
>
[
>> publisher_list
<Publisher: Apress>, <Publisher: O'
Reilly>]
请确保你的每一个模型里都包
含 unicode() 方法,这不只是为了交
互时方便,也是因为 Django会在其他
一些地方用 unicode() 来显示对象。
最后, unicode() 也是一个很好的例
子来演示我们怎么添加 行为 到模型
里。 Django的模型不只是为对象定义
了数据库表的结构,还定义了对象的
行为。 unicode() 就是一个例子来演
示模型知道怎么显示它们自己。
插入和更新数据
你已经知道怎么做了: 先使用一些
关键参数创建对象实例,如下:
>
.
>> p = Publisher(name='Apress',
..
address='2855 Telegraph
Ave.',
.
.
.
.
..
..
..
..
city='Berkeley',
state_province='CA',
country='U.S.A.',
website='http://www.apre
ss.com/')
这个对象实例并 没有 对数据库做修
改。 在调用 save() 方法之前,记录
并没有保存至数据库,像这样:
>
>> p.save()
在SQL里,这大致可以转换成这样:
INSERT INTO books_publisher
(name, address, city, state_prov
ince, country, website)
VALUES
('Apress', '2855 Telegraph Ave.'
,
'Berkeley', 'CA',
U.S.A.', 'http://www.apress.co
'
m/');
因为 Publisher 模型有一个自动增加
的主键 id ,所以第一次调
用 save() 还多做了一件事: 计算这
个主键的值并把它赋值给这个对象实
例:
>
>> p.id
5
2
# this will differ based on yo
ur own data
接下来再调用 save() 将不会创建新的
记录,而只是修改记录内容(也就是
执行 UPDATE SQL语句,而不是
INSERT 语句):
>
>
>> p.name = 'Apress Publishing'
>> p.save()
前面执行的 save() 相当于下面的SQL
语句:
UPDATE books_publisher SET
name = 'Apress Publishing',
address = '2855 Telegraph Ave.',
city = 'Berkeley',
state_province = 'CA',
country = 'U.S.A.',
website = 'http://www.apress.com
'
WHERE id = 52;
注意,并不是只更新修改过的那个字
段,所有的字段都会被更新。 这个
操作有可能引起竞态条件,这取决于
你的应用程序。 请参阅后面的“更新
多个对象”小节以了解如何实现这种
轻量的修改(只修改对象的部分字
段)。
UPDATE books_publisher SET
name = 'Apress Publishing'
WHERE id=52;
选择对象
当然,创建新的数据库,并更新之中
的数据是必要的,但是,对于 We b
应用程序来说,更多的时候是在检索
查询数据库。 我们已经知道如何从
一个给定的模型中取出所有记录:
>
[
>> Publisher.objects.all()
<Publisher: Apress>, <Publisher: O'
Reilly>]
这相当于这个SQL语句:
SELECT id, name, address, city, stat
e_province, country, website
FROM books_publisher;
注意
注意到Django在选择所有数据时并
没有使用 SELECT ,而是显式列
出了所有字段。 设计的时候就是
这样:SELECT 会更慢,而且最重
要的是列出所有字段遵循了Python
界的一个信条: 明言胜于暗示。
有关Python之禅(戒律) :-),在Python
提示行输入 import this 试试看。
让我们来仔细看
看 Publisher.objects.all() 这行的每个
部分:
首先,我们有一个已定义的模
型 Publisher 。没什么好奇怪的:
你想要查找数据, 你就用模型来
获得数据。
然后,是objects属性。 它被称为
管理器,我们将在第10章中详细
讨论它。 目前,我们只需了解管
理器管理着所有针对数据包含、
还有最重要的数据查询的表格级
操作。
所有的模型都自动拥有一
个 objects 管理器;你可以在想要
查找数据时使用它。
最后,还有 all() 方法。这个方法
返回返回数据库中所有的记录。
尽管这个对象 看起来 象一个列表
(list),它实际是一个 QuerySet
对象, 这个对象是数据库中一些
记录的集合。 附录C将详细描述
QuerySet。 现在,我们就先当它是
一个仿真列表对象好了。
所有的数据库查找都遵循一个通用模
式:
数据过滤
我们很少会一次性从数据库中取出所
有的数据;通常都只针对一部分数据
进行操作。 在Django API中,我们可
以使用 filter() 方法对数据进行过
滤:
>
>> Publisher.objects.filter(name='A
press')
<Publisher: Apress>]
[
filter() 根据关键字参数来转换
成 WHERE SQL语句。 前面这个例子
相当于这样:
SELECT id, name, address, city, stat
e_province, country, website
FROM books_publisher
WHERE name = 'Apress';
你可以传递多个参数到 filter() 来缩小
选取范围:
>
>> Publisher.objects.filter(country
=
[
"U.S.A.", state_province="CA")
<Publisher: Apress>]
多个参数会被转换成 AND SQL从
句, 因此上面的代码可以转化成这
样:
SELECT id, name, address, city, stat
e_province, country, website
FROM books_publisher
WHERE country = 'U.S.A.'
AND state_province = 'CA';
注意,SQL缺省的 = 操作符是精确匹
配的, 其他类型的查找也可以使
用:
>
>> Publisher.objects.filter(name__c
ontains="press")
<Publisher: Apress>]
[
在 name 和 contains 之间有双下划
线。和Python一样,Django也使用双
下划线来表明会进行一些魔术般的操
作。这里,contains部分会被Django翻
译成LIKE语句:
SELECT id, name, address, city, stat
e_province, country, website
FROM books_publisher
WHERE name LIKE '%press%';
其他的一些查找类型有:icontains(大
小写无关的LIKE),startswith和
endswith, 还有range(SQLBETWEEN查
询)。 附录C详细描述了所有的查找
类型。
获取单个对象
上面的例子中 filter() 函数返回一
个记录集,这个记录集是一个列表。
相对列表来说,有些时候我们更需要
获取单个的对象, get() 方法就是
在此时使用的:
>
>> Publisher.objects.get(name="Apre
ss")
<
Publisher: Apress>
这样,就返回了单个对象,而不是列
表(更准确的说,QuerySet)。 所
以,如果结果是多个对象,会导致抛
出异常:
Publisher.objects.get(country="
U.S.A.")
Traceback (most recent call
last):
.
..
MultipleObjectsReturned: get()
returned more than one
Publisher --
it returned 2! Lookup
parameters were {'country':
'
U.S.A.'}
如果查询没有返回结果也会抛出异
常:
>
>> Publisher.objects.get(name="Peng
uin")
Traceback (most recent call last):
.
..
DoesNotExist: Publisher matching que
ry does not exist.
这个 DoesNotExist 异常 是 Publisher
这个 model 类的一个属性,
即 Publisher.DoesNotExist。在你的应
用中,你可以捕获并处理这个异常,
像这样:
try:
p = Publisher.objects.get(name='
Apress')
except Publisher.DoesNotExist:
print "Apress isn't in the datab
ase yet."
else:
print "Apress is in the database
.
"
数据排序
在运行前面的例子中,你可能已经注
意到返回的结果是无序的。 我们还
没有告诉数据库 怎样对结果进行排
序,所以我们返回的结果是无序的。
在你的 Django 应用中,你或许希望
根据某字段的值对检索结果排序,比
如说,按字母顺序。 那么,使用
order_by() 这个方法就可以搞定了。
>
"
[
>> Publisher.objects.order_by("name
)
<Publisher: Apress>, <Publisher: O'
Reilly>]
跟以前的 all() 例子差不多,SQL语句
里多了指定排序的部分:
SELECT id, name, address, city, stat
e_province, country, website
FROM books_publisher
ORDER BY name;
我们可以对任意字段进行排序:
>
>> Publisher.objects.order_by("addr
ess")
<Publisher: O'Reilly>, <Publisher:
Apress>]
[
>
>> Publisher.objects.order_by("stat
e_province")
<Publisher: Apress>, <Publisher: O'
Reilly>]
[
如果需要以多个字段为标准进行排序
(第二个字段会在第一个字段的值相
同的情况下被使用到),使用多个参
数就可以了,如下:
>
>> Publisher.objects.order_by("stat
e_province", "address")
<Publisher: Apress>, <Publisher: O
Reilly>]
[
'
我们还可以指定逆向排序,在前面加
一个减号 - 前缀:
>
>> Publisher.objects.order_by("-nam
e")
[
<Publisher: O'Reilly>, <Publisher:
Apress>]
尽管很灵活,但是每次都要
用 order_by() 显得有点啰嗦。 大多数
时间你通常只会对某些 字段进行排
序。 在这种情况下,Django让你可以
指定模型的缺省排序方式:
class Publisher(models.Model):
name = models.CharField(max_leng
th=30)
address = models.CharField(max_l
ength=50)
city = models.CharField(max_leng
th=60)
state_province = models.CharFiel
d(max_length=30)
country = models.CharField(max_l
ength=50)
website = models.URLField()
def __unicode__(self):
return self.name
class Meta:
ordering = ['name']
现在,让我们来接触一个新的概
念。 class Meta,内嵌于 Publisher 这
个类的定义中(如果 class Publisher
是顶格的,那么 class Meta 在它之下
要缩进4个空格--按 Python 的传统
)
。你可以在任意一个 模型 类中使
用 Meta 类,来设置一些与特定模型
相关的选项。 在 附录B 中有 Meta 中
所有可选项的完整参考,现在,我们
关注 ordering 这个选项就够了。 如果
你设置了这个选项,那么除非你检索
时特意额外地使用了 order_by(),否
则,当你使用 Django 的数据库 API
去检索时,Publisher对象的相关返回
值默认地都会按 na me 字段排序。
连锁查询
我们已经知道如何对数据进行过滤和
排序。 当然,通常我们需要同时进
行过滤和排序查询的操作。 因此,
你可以简单地写成这种“链式”的形
式:
>
=
[
>> Publisher.objects.filter(country
"U.S.A.").order_by("-name")
<Publisher: O'Reilly>, <Publisher:
Apress>]
你应该没猜错,转换成SQL查询就
是 WHERE 和 ORDER BY 的组合:
SELECT id, name, address, city, stat
e_province, country, website
FROM books_publisher
WHERE country = 'U.S.A'
ORDER BY name DESC;
限制返回的数据
另一个常用的需求就是取出固定数目
的记录。 想象一下你有成千上万的
出版商在你的数据库里, 但是你只
想显示第一个。 你可以使用标准的
Python列表裁剪语句:
>
'
<
>> Publisher.objects.order_by('name
)[0]
Publisher: Apress>
这相当于:
SELECT id, name, address, city, stat
e_province, country, website
FROM books_publisher
ORDER BY name
LIMIT 1;
类似的,你可以用Python的range-
slicing语法来取出数据的特定子集:
>
'
>> Publisher.objects.order_by('name
)[0:2]
这个例子返回两个对象,等同于以下
的SQL语句:
SELECT id, name, address, city, stat
e_province, country, website
FROM books_publisher
ORDER BY name
OFFSET 0 LIMIT 2;
注意,不支持Python的负索引
(negative slicing):
>
'
>> Publisher.objects.order_by('name
)[-1]
Traceback (most recent call last):
.
..
AssertionError: Negative indexing is
not supported.
虽然不支持负索引,但是我们可以使
用其他的方法。 比如,稍微修改
order_by() 语句来实现:
>
>> Publisher.objects.order_by('-nam
e')[0]
更新多个对象
在“插入和更新数据”小节中,我们有
提到模型的save()方法,这个方法会
更新一行里的所有列。 而某些情况
下,我们只需要更新行里的某几列。
例如说我们现在想要将Apress
Publisher的名称由原来的”Apress”更
改为”Apress Publishing”。若使用
save()方法,如:
>
>> p = Publisher.objects.get(name='
Apress')
>
>
>> p.name = 'Apress Publishing'
>> p.save()
这等同于如下SQL语句:
SELECT id, name, address, city, stat
e_province, country, website
FROM books_publisher
WHERE name = 'Apress';
UPDATE books_publisher SET
name = 'Apress Publishing',
address = '2855 Telegraph Ave.',
city = 'Berkeley',
state_province = 'CA',
country = 'U.S.A.',
website = 'http://www.apress.com
'
WHERE id = 52;
(
注意在这里我们假设Apress的ID为
5
2)
在这个例子里我们可以看到Django的
save()方法更新了不仅仅是name列的
值,还有更新了所有的列。 若na me
以外的列有可能会被其他的进程所改
动的情况下,只更改name列显然是更
加明智的。 更改某一指定的列,我
们可以调用结果集(QuerySet)对象
的update()方法: 示例如下:
>
>> Publisher.objects.filter(id=52).
update(name='Apress Publishing')
与之等同的SQL语句变得更高效,并
且不会引起竞态条件。
UPDATE books_publisher
SET name = 'Apress Publishing'
WHERE id = 52;
update()方法对于任何结果集
(
QuerySet)均有效,这意味着你可
以同时更新多条记录。 以下示例演
示如何将所有Publisher的country字段
值由’U.S.A’更改为’USA’:
>
>> Publisher.objects.all().update(c
ountry='USA')
2
update()方法会返回一个整型数值,
表示受影响的记录条数。 在上面的
例子中,这个值是2。
删除对象
删除数据库中的对象只需调用该对象
的delete()方法即可:
>
>> p = Publisher.objects.get(name="
O'Reilly")
>
>
[
>> p.delete()
>> Publisher.objects.all()
<Publisher: Apress Publishing>]
同样我们可以在结果集上调用delete()
方法同时删除多条记录。这一点与我
们上一小节提到的update()方法相
似:
>
=
>
>
[
>> Publisher.objects.filter(country
'USA').delete()
>> Publisher.objects.all().delete()
>> Publisher.objects.all()
]
删除数据时要谨慎! 为了预防误删
除掉某一个表内的所有数据,Django
要求在删除表内所有数据时显示使用
all()。 比如,下面的操作将会出错:
>
>> Publisher.objects.delete()
Traceback (most recent call last):
File "<console>", line 1, in <modu
le>
AttributeError: 'Manager' object has
no attribute 'delete'
而一旦使用all()方法,所有数据将会
被删除:
>
>> Publisher.objects.all().delete()
如果只需要删除部分的数据,就不需
要调用all()方法。再看一下之前的例
子:
>
=
>> Publisher.objects.filter(country
'USA').delete()
下一章
通过本章的学习,你应该可以熟练地
使用Django模型来编写一些简单的数
据库应用程序。 在第十章我们将讨
论Django数据库层的高级应用。
一旦你定义了你的模型,接下来就是
要把数据导入数据库里了。 你可能
已经有现成的数据了,请看第十八章
以获得有关如何集成现有数据库的建
议。 也可能数据是用户提供的,第
七章中还会教你怎么处理用户提交的
数据。
有时候,你和你的团队成员也需要手
工输入数据,这时候如果有一个基于
Web的数据输入和管理的界面就会很
有帮助。 下一章将介绍解决手工录
入问题的方法——Django管理界面。
对于某一类网站, 管理界面 是基础
设施中非常重要的一部分。 这是以
网页和有限的可信任管理者为基础的
界面,它可以让你添加,编辑和删除
网站内容。 一些常见的例子: 你可
以用这个界面发布博客,后台的网站
管理者用它来润色读者提交的内容,
你的客户用你给他们建立的界面工具
更新新闻并发布在网站上,这些都是
使用管理界面的例子。
但是管理界面有一问题: 创建它太
繁琐。 当你开发对公众的功能时,
网页开发是有趣的,但是创建管理界
面通常是千篇一律的。 你必须认证
用户,显示并管理表格,验证输入的
有效性诸如此类。 这很繁琐而且是
重复劳动。
Django 在对这些繁琐和重复的工作进
行了哪些改进? 它用不能再少的代
码为你做了所有的一切。 Django 中
创建管理界面已经不是问题。
这一章是关于 Django 的自动管理界
面。 这个特性是这样起作用的: 它
读取你模式中的元数据,然后提供给
你一个强大而且可以使用的界面,网
站管理者可以用它立即工作。
请注意我们建议你读这章,即使你不
打算用admi n。因为我们将介绍一些
概念,这些概念可以应用到Django的
所有方面,而不仅仅是admin
django.contrib 包
Django自动管理工具是django.contrib
的一部分。django.contrib是一套庞大
的功能集,它是Django基本代码的组
成部分,Django框架就是由众多包含
附加组件(add-on)的基本代码构成
的。 你可以把django.contrib看作是可
选的Python标准库或普遍模式的实际
实现。 它们与Django捆绑在一起,这
样你在开发中就不用“重复发明轮
子”了。
管理工具是本书讲述django.contrib的
第一个部分。从技术层面上讲,它被
称作django.contrib.admin 。
django.contrib中其它可用的特性,如
用户鉴别系统(django.contrib.auth)、
支持匿名会话
(django.contrib.sessioins)以及用户评
注系统(django.contrib.comments)。这
些,我们将在第十六章详细讨论。在
成为一个Django专家以前,你将会知
道更多django.contrib的特性。 目前,
你只需要知道Django自带很多优秀的
附加组件,它们都存在于
django.contrib包里。
激活管理界面
Django管理站点完全是可选择的,因
为仅仅某些特殊类型的站点才需要这
些功能。 这意味着你需要在你的项
目中花费几个步骤去激活它。
第一步,对你的settings文件做如下这
些改变:
1. 将'django.contrib.admin'加入setting
的INSTALLED_APPS配置中
(INSTALLED_APPS中的配置顺
序是没有关系的, 但是我们喜欢保
持一定顺序以方便人来阅读)
2
. 保证INSTALLED_APPS中包
含'django.contrib.auth','django.cont
rib.contenttypes'和'django.contrib.se
ssions',Django的管理工具需要这
3个包。 (如果你跟随本文制作
mysi te项目的话,那么请注意我们
在第五章的时候把这三项
INSTALLED_APPS条目注释了。
现在,请把注释取消。)
3
. 确保MIDDLEWARE_CLASSES 包
含'django.middleware.common.Com
monMiddleware'、'django.contrib.se
ssions.middleware.SessionMiddlew
are'和'django.contrib.auth.middlewar
e.AuthenticationMiddleware' 。(再
次提醒,如果有跟着做mysi te的
话,请把在第五章做的注释取
消。)
运行 python manage.py syncdb 。这一
步将生成管理界面使用的额外数据库
表。 当你把'django.contrib.auth'加进
INSTALLED_APPS后,第一次运行
syncdb命令时, 系统会请你创建一个
超级用户。 如果你不这么作,你需
要运行
python manage.py createsuperuser来另
外创建一个admi n的用户帐号,否则
你将不能登入admin (提醒一句: 只有
当INSTALLED_APPS包
含'django.contrib.auth'时,
python manage.py createsuperuser这个
命令才可用.)
第三,将admi n访问配置在
URLconf(记住,在urls.py中). 默认情
况下,命令django-
admin.py startproject生成的文件urls.py
是将Django admi n的路径注释掉的,
你所要做的就是取消注释。 请注
意,以下内容是必须确保存在的:
#
Include these import statements...
from django.contrib import admin
admin.autodiscover()
#
And include this URLpattern...
urlpatterns = patterns('',
...
(r'^admin/', include(admin.site.
urls)),
...
#
#
)
当这一切都配置好后,现在你将发现
Django管理工具可以运行了。 启动开
发服务器(如前:
python manage.py runserver ),
然后在浏览器中访问:
http://127.0.0.1:8000/admin/
使用管理工具。
管理界面的设计是针对非技术人员
的,所以它应该是自我解释的。 尽
管如此,这里简单介绍一下它的基本
特性。
你看到的第一件事是如图6-1所示的
登录屏幕。
图 6-1. Django的登录截图
你要使用你原来设置的超级用户的用
户名和密码。 如果无法登录,请运
行
python manage.py createsuperuser
,
确保你已经创建了一个超级用户。
一旦登录了,你将看到管理页面。
这个页面列出了管理工具中可编辑的
所有数据类型。 现在,由于我们还
没有创建任何模块,所以这个列表只
有寥寥数条类目: 它仅有两个默认
的管理-编辑模块:用户组(Groups)和
用户(Users) 。
图 6-2。 Django admi n的首页
在Django管理页面中,每一种数据类
型都有一个change list 和edit form 。
前者显示数据库中所有的可用对象;
后者可让你添加、更改和删除数据库
中的某条记录。
其它语言
如果你的母语不是英语,而你不想用
它来配置你的浏览器,你可以做一个
快速更改来观察Django管理工具是否
被翻译成你想要的语言。 仅需添加
‘
django.middleware.locale.Locale
到 MIDDLEWARE_CLASSES 设置中,并
确保它
在’django.contrib.sessions.middleware.
SessionMiddleware’之后 。 (见上)
完成后,请刷新页面。 如果你设置
的语言可用,一系列的链接文字将被
显示成这种语言。这些文字包括页面
顶端的Change password和Log out,页
面中部的Groups和Users。 Django自
带了多种语言的翻译。
关于Django更多的国际化特性,请参
见第十九章。
点击Uers行中的Change链接,引导用
户更改列表。
图 6-3. 典型的改变列表视图 (见
上)
这个页面显示了数据库中所有的用
户。你可以将它看作是一个漂亮的网
页版查询:
SELECT * FROM auth_user; 如果你
一直跟着作练习,并且只添加了一个
用户,你会在这个页面中看到一个用
户。但是如果你添加了多个用户,你
会发现页面中还有过滤器、排序和查
询框。 过滤器在右边;排序功能可
通过点击列头查看;查询框在页面顶
部,它允许你通过用户名查询。
点击其中一个用户名,你会看见关于
这个用户的编辑窗口。
图 6-4. 典型的编辑表格 (见上)
这个页面允许你修改用户的属性,如
姓名和权限。 (如果要更改用户密
码,你必须点击密码字段下的change
password for m,而不是直接更改字段
值中的哈西码。)另外需要注意的
是,不同类型的字段会用不同的窗口
控件显示。例如,日期/时间型用日
历控件,布尔型用复选框,字符型用
简单文本框显示。
你可以通过点击编辑页面下方的删除
按钮来删除一条记录。 你会见到一
个确认页面。有时候,它会显示有哪
些关联的对象将会一并被删除。
(例如,如果你要删除一个出版社,
它下面所有的图书也将被删除。)
你可以通过点击管理主页面中某个对
象的Add来添加一条新记录。 一个空
白记录的页面将被打开,等待你填
充。
你还能看到管理界面也控制着你输入
的有效性。 你可以试试不填必需的
栏目或者在时间栏里填错误的时间,
你会发现当你要保存时会出现错误信
息,如图6-5所示。
图6-5. 编辑表格显示错误信息 (见
上)
当你编辑已有的对像时,你在窗口的
右上角可以看到一个历史按钮。 通
过管理界面做的每一个改变都留有记
录,你可以按历史键来检查这个记录
(见图6-6)。
图6-6. Django 对像历史页面 (见
上)
将你的Models加入到
Admin管理中
有一个关键步骤我们还没做。 让我
们将自己的模块加入管理工具中,这
样我们就能够通过这个漂亮的界面添
加、修改和删除数据库中的对象了。
我们将继续第五章中的 book 例子。
在其中,我们定义了三个模块:
Publisher 、 Author 和 Book 。
在 books 目录下( mysite/books ),
创建一个文件: admin.py ,然后输
入以下代码:
from django.contrib import admin
from mysite.books.models import Publ
isher, Author, Book
admin.site.register(Publisher)
admin.site.register(Author)
admin.site.register(Book)
这些代码通知管理工具为这些模块逐
一提供界面。
完成后,打开页面
http://127.0.0.1:8000/admin/
,
你会看到一个Books区域,其中包
含Authors、Books和Publishers。
你可能需要先停止,然后再启动服
(
务( runserver ),才能使其生效。)
现在你拥有一个功能完整的管理界面
来管理这三个模块了。 很简单吧!
花点时间添加和修改记录,以填充数
据库。 如果你跟着第五章的例子一
起创建Publisher对象的话(并且没有
删除),你会在列表中看到那些记
录。
这里需要提到的一个特性是,管理工
具处理外键和多对多关系(这两种关
系可以在 Book 模块中找到)的方
法。 作为提醒,这里有个 Book 模块
的例子:
class Book(models.Model):
title = models.CharField(max_len
gth=100)
authors = models.ManyToManyField
(Author)
publisher = models.ForeignKey(Pu
blisher)
publication_date = models.DateFi
eld()
def __unicode__(self):
return self.title
在Add book页面中(
http://127.0.0.1:8000/admin/boo
)
, 外键 publisher用一个选择框显
示, 多对多 字段author用一个多选框
显示。 点击两个字段后面的绿色加
号,可以让你添加相关的记录。 举
个例子,如果你点击Publisher后面的
加号,你将会得到一个弹出窗口来添
加一个publisher。 当你在那个窗口中
成功创建了一个publisher后,Add
book表单会自动把它更新到字段上去
花巧 .
Admin是如何工作的
在幕后,管理工具是如何工作的呢?
其实很简单。
当服务启动时,Django从 url.py 引
导URLconf,然后执行
admin.autodiscover() 语句。 这个
函数遍历INSTALLED_APPS配置,并
且寻找相关的 admin.py文件。 如果在
指定的app目录下找到admin.py,它就
执行其中的代码。
在 books 应用程序目录下的
admin.py 文件中,每次调用
admin.site.register() 都将那个
模块注册到管理工具中。 管理工具
只为那些明确注册了的模块显示一个
编辑/修改的界面。
应用程序 django.contrib.auth 包
含自身的 admin.py ,所以Users和
Groups能在管理工具中自动显示。 其
它的django.contrib应用程序,如
django.contrib.redirects,其它从网上
下在的第三方Django应用程序一样,
都会自行添加到管理工具。
综上所述,管理工具其实就是一个
Django应用程序,包含自己的模块、
模板、视图和URLpatterns。 你要像
添加自己的视图一样,把它添加到
URLconf里面。 你可以在Django基本
代码中的django/contrib/admin 目录
下,检查它的模板、视图和
URLpatterns,但你不要尝试直接修改
其中的任何代码,因为里面有很多地
方可以让你自定义管理工具的工作方
式。 (如果你确实想浏览Django管理
工具的代码,请谨记它在读取关于模
块的元数据过程中做了些不简单的工
作,因此最好花些时间阅读和理解那
些代码。)
设置字段可选
在摆弄了一会之后,你或许会发现管
理工具有个限制:编辑表单需要你填
写每一个字段,然而在有些情况下,
你想要某些字段是可选的。 举个例
子,我们想要Author模块中的email字
段成为可选,即允许不填。 在现实
世界中,你可能没有为每个作者登记
邮箱地址。
为了指定email字段为可选,你只要
编辑Book模块(回想第五章,它在
mysite/books/models.py文件里),在
email字段上加上blank=True。代码如
下:
class Author(models.Model):
first_name = models.CharField(ma
x_length=30)
last_name = models.CharField(max
length=40)
email = models.EmailField(blank=
_
True )
这些代码告诉Django,作者的邮箱地
址允许输入一个空值。 所有字段都
默认blank=False,这使得它们不允许
输入空值。
这里会发生一些有趣的事情。 直到
现在,除了unicode()方法,我们的模
块充当数据库中表定义的角色,即本
质上是用Python的语法来写
CREATE TABLE语句。 在添加
blank=True过程中,我们已经开始在
简单的定义数据表上扩展我们的模块
了。 现在,我们的模块类开始成为
一个富含Author对象属性和行为的集
合了。 email不但展现为一个数据库
中的VARCHAR类型的字段,它还是
页面中可选的字段,就像在管理工具
中看到的那样。
当你添加blank=True以后,刷新页面
Add author edit form
(http://127.0.0.1:8000/admin/books/auth
or/add/ ),将会发现Email的标签不再
是粗体了。 这意味它不是一个必填
字段。 现在你可以添加一个作者而
不必输入邮箱地址,即使你为这个字
段提交了一个空值,也再不会得到那
刺眼的红色信息“This field is
required”。
设置日期型和数字型字段
可选
虽然blank=True同样适用于日期型和
数字型字段,但是这里需要详细讲解
一些背景知识。
SQL有指定空值的独特方式,它把空
值叫做NULL。NULL可以表示为未知
的、非法的、或其它程序指定的含
义。
在SQL中, NULL的值不同于空字符
串,就像Python中None不同于空字符
串("")一样。这意味着某个字符型
字段(如VARCHAR)的值不可能同
时包含NULL和空字符串。
这会引起不必要的歧义或疑惑。 为
什么这条记录有个NULL,而那条记
录却有个空字符串? 它们之间有区
别,还是数据输入不一致? 还有:
我怎样才能得到全部拥有空值的记
录,应该按NULL和空字符串查找
么?还是仅按字符串查找?
为了消除歧义,Django生成
CREATE TABLE语句自动为每个字段
显式加上NOT NULL。 这里有个第五
章中生成Author模块的例子:
CREATE TABLE "books_author" (
"
"
"
"
id" serial NOT NULL PRIMARY KEY
first_name" varchar(30) NOT NUL
last_name" varchar(40) NOT NULL
email" varchar(75) NOT NULL
,
L,
,
)
;
在大多数情况下,这种默认的行为对
你的应用程序来说是最佳的,因为它
可以使你不再因数据一致性而头痛。
而且它可以和Django的其它部分工作
得很好。如在管理工具中,如果你留
空一个字符型字段,它会为此插入一
个空字符串(而不是NULL)。
但是,其它数据类型有例外:日期
型、时间型和数字型字段不接受空字
符串。 如果你尝试将一个空字符串
插入日期型或整数型字段,你可能会
得到数据库返回的错误,这取决于那
个数据库的类型。 (PostgreSQL比较
严禁,会抛出一个异常;MySQL可能
会也可能不会接受,这取决于你使用
的版本和运气了。)在这种情况下,
NULL是唯一指定空值的方法。 在
Django模块中,你可以通过添加
null=True来指定一个字段允许为
NULL。
因此,这说起来有点复杂: 如果你
想允许一个日期型(DateField、
TimeField、DateTimeField)或数字型
(
IntegerField、DecimalField、
FloatField)字段为空,你需要使用
nul l =Tr ue 和 blank=True。
为了举例说明,让我们把Book模块修
改成允许 publication_date为空。修改
后的代码如下:
class Book(models.Model):
title = models.CharField(max_len
gth=100)
authors = models.ManyToManyField
(Author)
publisher = models.ForeignKey(Pu
blisher)
publication_date = models.DateFi
eld(blank=True, null=True )
添加null=True比添加blank=True复
杂。因为null=True改变了数据的语
义,即改变了CREATE TABLE语句,
把publication_date字段上的
NOT NULL删除了。 要完成这些改
动,我们还需要更新数据库。
出于某种原因,Django不会尝试自动
更新数据库结构。所以你必须执行
ALTER TABLE语句将模块的改动更
新至数据库。 像先前那样,你可以
使用manage.py dbshell进入数据库服
务环境。 以下是在这个特殊情况下
如何删除NOT NULL:
ALTER TABLE books_book ALTER COLUMN
publication_date DROP NOT NULL;
(注意:以下SQL语法是PostgreSQL
特有的。)
我们将在第十章详细讲述数据库结构
更改。
现在让我们回到管理工具,添加book
的编辑页面允许输入一个空的
publication date。
自定义字段标签
在编辑页面中,每个字段的标签都是
从模块的字段名称生成的。 规则很
简单: 用空格替换下划线;首字母
大写。例如:Book模块中
publication_date的标签是Publication
date。
然而,字段名称并不总是贴切的。有
些情况下,你可能想自定义一个标
签。 你只需在模块中指定
verbose_name。
举个例子,说明如何将Author.email的
标签改为e-mail,中间有个横线。
class Author(models.Model):
first_name = models.CharField(ma
x_length=30)
last_name = models.CharField(max
_
length=40)
email = models.EmailField(blank=
True, verbose_name='e-mail' )
修改后重启服务器,你会在author编
辑页面中看到这个新标签。
请注意,你不必把verbose_name的首
字母大写,除非是连续大写
(
如:"USA state")。Django会自动
适时将首字母大写,并且在其它不需
要大写的地方使用verbose_name的精
确值。
最后还需注意的是,为了使语法简
洁,你可以把它当作固定位置的参数
传递。 这个例子与上面那个的效果
相同。
class Author(models.Model):
first_name = models.CharField(ma
x_length=30)
last_name = models.CharField(max
_
length=40)
email = models.EmailField('e-mai
l', blank=True)
但这不适用于ManyToManyFi el d 和
ForeignKey字段,因为它们第一个参
数必须是模块类。 那种情形,必须
显式使用verbose_name这个参数名
称。
自定义ModelAdmi 类
迄今为止,我们做的blank=True、
null=True和verbose_name修改其实是
模块级别,而不是管理级别的。 也
就是说,这些修改实质上是构成模块
的一部分,并且正好被管理工具使
用,而不是专门针对管理工具的。
除了这些,Django还提供了大量选项
让你针对特别的模块自定义管理工
具。 这些选项都在_ModelAdmin
classes_里面,这些类包含了管理工
具中针对特别模块的配置。
自定义列表
让我们更深一步:自定义Author模块
的列表中的显示字段。 列表默认地
显示查询结果中对象的unicode()。 在
第五章中,我们定义Author对象的
unicode()方法,用以同时显示作者的
姓和名。
class Author(models.Model):
first_name = models.CharField(ma
x_length=30)
last_name = models.CharField(max
length=40)
email = models.EmailField(blank=
_
True, verbose_name='e-mail')
def __unicode__(self):
return u'%s %s' % (self.firs
t_name, self.last_name)
结果正如图6-7所示,列表中显示的
是每个作者的姓名。
图 6-7. 作者列表
我们可以在这基础上改进,添加其它
字段,从而改变列表的显示。 这个
页面应该提供便利,比如说:在这个
列表中可以看到作者的邮箱地址。如
果能按照姓氏或名字来排序,那就更
好了。
为了达到这个目的,我们将为Author
模块定义一个ModelAdmin类。 这个
类是自定义管理工具的关键,其中最
基本的一件事情是允许你指定列表中
的字段。 打开admin.py并修改:
from django.contrib import admin
from mysite.books.models import Publ
isher, Author, Book
class AuthorAdmin(admin.ModelAdmin):
list_display = ('first_name', 'l
ast_name', 'email')
admin.site.register(Publisher)
admin.site.register(Author, AuthorAd
min)
admin.site.register(Book)
解释一下代码:
我们新建了一个类AuthorAdmin,
它是从
django.contrib.admin.ModelAdmin派
生出来的子类,保存着一个类的
自定义配置,以供管理工具使
用。 我们只自定义了一项:
list_display, 它是一个字段名称的
元组,用于列表显示。 当然,这
些字段名称必须是模块中有的。
我们修改了admin.site.register()调
用,在Author后面添加了
AuthorAdmin。你可以这样理解:
用AuthorAdmin选项注册Author模
块。
admin.site.register()函数接受一个
ModelAdmin子类作为第二个参
数。 如果你忽略第二个参数,
Django将使用默认的选项。
Publisher和Book的注册就属于这种
情况。
弄好了这个东东,再刷新author列表
页面,你会看到列表中有三列:姓
氏、名字和邮箱地址。 另外,点击
每个列的列头可以对那列进行排序。
(参见图 6-8)
图 6-8. 修改后的author列表页面
接下来,让我们添加一个快速查询
栏。 向AuthorAdmin追加
search_fields,如:
class AuthorAdmin(admin.ModelAdmin):
list_display = ('first_name', 'l
ast_name', 'email')
search_fields = ('first_name', '
last_name')
刷新浏览器,你会在页面顶端看到一
个查询栏。 (见图6-9.)我们刚才所
作的修改列表页面,添加了一个根据
姓名查询的查询框。 正如用户所希
望的那样,它是大小写敏感,并且对
两个字段检索的查询框。如果查
询"bar",那么名字中含有Barney和姓
氏中含有Hobarson的作者记录将被检
索出来。
图 6-9. 含search_fields的author列表页
面
接下来,让我们为Book列表页添加一
些过滤器。
from django.contrib import admin
from mysite.books.models import Publ
isher, Author, Book
class AuthorAdmin(admin.ModelAdmin):
list_display = ('first_name', 'l
ast_name', 'email')
search_fields = ('first_name', '
last_name')
class BookAdmin(admin.ModelAdmin):
list_display = ('title', 'publis
her', 'publication_date')
list_filter = ('publication_date
'
,)
admin.site.register(Publisher)
admin.site.register(Author, AuthorAd
min)
admin.site.register(Book, BookAdmin)
由于我们要处理一系列选项,因此我
们创建了一个单独的ModelAdmin
类:BookAdmin。首先,我们定义一
个list_display,以使得页面好看些。
然后,我们用list_filter这个字段元组
创建过滤器,它位于列表页面的右
边。 Django为日期型字段提供了快捷
过滤方式,它包含:今天、过往七
天、当月和今年。这些是开发人员经
常用到的。 图 6-10显示了修改后的
页面。
图 6-10. 含过滤器的book列表页面
过滤器 同样适用于其它类型的字
段,而不单是 日期型 (请在 布尔型
和 外键 字段上试试)。当有两个以
上值时,过滤器就会显示。
另外一种过滤日期的方式是使用
date_hierarchy选项,如:
class BookAdmin(admin.ModelAdmin):
list_display = ('title', 'publis
her', 'publication_date')
list_filter = ('publication_date
date_hierarchy = 'publication_da
'
,)
te'
修改好后,页面中的列表顶端会有一
个逐层深入的导航条,效果如图 6-
11. 它从可用的年份开始,然后逐层
细分到月乃至日。
图 6-11. 含date_hierarchy的book列表
页面
请注意,date_hierarchy接受的是字符
串 ,而不是元组。因为只能对一个
日期型字段进行层次划分。
最后,让我们改变默认的排序方式,
按publication date降序排列。 列表页
面默认按照模块class Meta(详见第
五章)中的ordering所指的列排序。
但目前没有指定ordering值,所以当
前排序是没有定义的。
class BookAdmin(admin.ModelAdmin):
list_display = ('title', 'publis
her', 'publication_date')
list_filter = ('publication_date
'
,)
date_hierarchy = 'publication_da
ordering = ('-publication_date',)
te'
这个ordering选项基本像模块中
class Meta的ordering那样工作,除了
它只用列表中的第一个字段名。 如
果要实现降序,仅需在传入的列表或
元组的字段前加上一个减号(-)。
刷新book列表页面观看实际效果。
注意Publication date列头现在有一个
小箭头显示排序。 (见图 6-12.)
图 6-12 含排序的book列表页面
我们已经学习了主要的选项。 通过
使用它们,你可以仅需几行代码就能
创建一个功能强大、随时上线的数据
编辑界面。
自定义编辑表单
正如自定义列表那样,编辑表单多方
面也能自定义。
首先,我们先自定义字段顺序。 默
认地,表单中的字段顺序是与模块中
定义是一致的。 我们可以通过使用
ModelAdmin子类中的fields选项来改
变它:
class BookAdmin(admin.ModelAdmin):
list_display = ('title', 'publis
her', 'publication_date')
list_filter = ('publication_date
'
,)
date_hierarchy = 'publication_da
ordering = ('-publication_date',
fields = ('title', 'authors', 'p
te'
)
ublisher', 'publication_date')
完成之后,编辑表单将按照指定的顺
序显示各字段。 它看起来自然多了
——作者排在书名之后。 字段顺序
当然是与数据条目录入顺序有关,
每个表单都不一样。
通过fields这个选项,你可以排除一
些不想被其他人编辑的fields 只要不
选上不想被编辑的field(s)即可。 当
你的admi用户只是被信任可以更改你
的某一部分数据时,或者,你的数据
被一些外部的程序自动处理而改变了
了,你就可以用这个功能。 例如,
在book数据库中,我们可以隐藏
publication_date,以防止它被编辑。
class BookAdmin(admin.ModelAdmin):
list_display = ('title', 'publis
her', 'publication_date')
list_filter = ('publication_date
'
,)
date_hierarchy = 'publication_da
ordering = ('-publication_date',
te'
)
fields = ('title', 'authors', 'p
ublisher')
这样,在编辑页面就无法对
publication date进行改动。 如果你是
一个编辑,不希望作者推迟出版日期
的话,这个功能就很有用。 (当
然,这纯粹是一个假设的例子。)
当一个用户用这个不包含完整信息的
表单添加一本新书时,Django会简单
地将publication_date设置为None,以
确保这个字段满足null=True的条件。
另一个常用的编辑页面自定义是针对
多对多字段的。 真如我们在book编
辑页面看到的那样, 多对多字段 被展
现成多选框。虽然多选框在逻辑上是
最适合的HTML控件,但它却不那么
好用。 如果你想选择多项,你必须
还要按下Ctrl键(苹果机是c omma nd
键)。 虽然管理工具因此添加了注
释(help_text),但是当它有几百个
选项时,它依然显得笨拙。
更好的办法是使用filter_horizontal。
让我们把它添加到BookAdmin中,然
后看看它的效果。
class BookAdmin(admin.ModelAdmin):
list_display = ('title', 'publis
her', 'publication_date')
list_filter = ('publication_date
'
,)
date_hierarchy = 'publication_da
ordering = ('-publication_date',
filter_horizontal = ('authors',)
te'
)
(如果你一着跟着做练习,请注意移
除fields选项,以使得编辑页面包含
所有字段。)
刷新book编辑页面,你会看到Author
区中有一个精巧的JavaScript过滤
器,它允许你检索选项,然后将选中
的authors从Available框移到Chosen
框,还可以移回来。
图 6-13. 含filter_horizontal的book编辑
页面
我们强烈建议针对那些拥有十个以上
选项的 多对多字段 使用
filter_horizontal。 这比多选框好用多
了。 你可以在多个字段上使用
filter_horizontal,只需在这个元组中
指定每个字段的名字。
ModelAdmin类还支持filter_vertical选
项。 它像filter_horizontal那样工作,
除了控件都是垂直排列,而不是水平
排列的。 至于使用哪个,只是个人
喜好问题。
filter_horizontal和filter_vertical选项只
能用在多对多字段 上, 而不能用
于 ForeignKey字段。 默认地,管理工
具使用 下拉框 来展现 外键 字段。但
是,正如 多对多字段 那样,有时候你
不想忍受因装载并显示这些选项而产
生的大量开销。 例如,我们的book
数据库膨胀到拥有数千条publishers的
记录,以致于book的添加页面装载时
间较久,因为它必须把每一个
publishe都装载并显示在 下拉框 中。
解决这个问题的办法是使用
raw_id_fields 选项。它是一个包
含外键字段名称的元组,它包含的字
段将被展现成 文本框 ,而不再是
下拉框 。见图 6-14。
class BookAdmin(admin.ModelAdmin):
list_display = ('title', 'publis
her', 'publication_date')
list_filter = ('publication_date
'
,)
date_hierarchy = 'publication_da
ordering = ('-publication_date',
te'
)
filter_horizontal = ('authors',)
raw_id_fields = ('publisher',)
图 6-14. 含raw_id_fields的book编辑页
面
在这个输入框中,你输入什么呢?
publisher的数据库ID号。 考虑到人们
通常不会记住这些数据库ID,管理工
具提供了一个放大镜图标方便你输
入。点击那个图标将会弹出一个窗
口,在那里你可以选择想要添加的
publishe。
用户、用户组和权限
因为你是用超级用户登录的,你可以
创建,编辑和删除任何对像。 然
而,不同的环境要求有不同的权限,
系统不允许所有人都是超级用户。
管理工具有一个用户权限系统,通过
它你可以根据用户的需要来指定他们
的权限,从而达到部分访问系统的目
的。
用户帐号应该是通用的、独立于管理
界面以外仍可以使用。但我们现在把
它看作是管理界面的一部分。 在第
十四章,我们将讲述如何把用户帐号
与你的网站(不仅仅是管理工具)集
成在一起。
你通过管理界面编辑用户及其许可就
像你编辑别的对象一样。 我们在本
章的前面,浏览用户和用户组区域的
时候已经见过这些了。 如你所想,
用户对象有标准的用户名、密码、邮
箱地址和真实姓名,同时它还有关于
使用管理界面的权限定义。 首先,
这有一组三个布尔型标记:
活动标志,它用来控制用户是否
已经激活。 如果一个用户帐号的
这个标记是关闭状态,而用户又
尝试用它登录时,即使密码正
确,他也无法登录系统。
成员标志,它用来控制这个用户
是否可以登录管理界面(即:这
个用户是不是你们组织里的成
员) 由于用户系统可以被用于控
制公众页面(即:非管理页面)
的访问权限(详见第十四章),
这个标志可用来区分公众用户和
管理用户。
超级用户标志,它赋予用户在管
理界面中添加、修改和删除任何
项目的权限。 如果一个用户帐号
有这个标志,那么所有权限设置
(
即使没有)都会被忽略。
普通的活跃,非超级用户的管理用户
可以根据一套设定好的许可进入。
管理界面中每种可编辑的对象(如:
books、authors、publishers)都有三
种权限: 创建 许可, 编辑 许可
和 删除 许可。 给一个用户授权许可
也就表明该用户可以进行许可描述的
操作。
当你创建一个用户时,它没有任何权
限,该有什么权限是由你决定的。
例如,你可以给一个用户添加和修改
publishers的权限,而不给他删除的权
限。 请注意,这些权限是定义在模
块级别上,而不是对象级别上的。据
个例子,你可以让小强修改任何图
书,但是不能让他仅修改由机械工业
出版社出版的图书。 后面这种基于
对象级别的权限设置比较复杂,并且
超出了本书的覆盖范围,但你可以在
Django documentation中寻找答案。
注释
权限管理系统也控制编辑用户和权
限。 如果你给某人编辑用户的权
限,他可以编辑自己的权限,这种能
力可能不是你希望的。 赋予一个用
户修改用户的权限,本质上说就是把
他变成一个超级用户。
你也可以给组中分配用户。 一
个 组 简化了给组中所有成员应用一
套许可的动作。 组在给大量用户特
定权限的时候很有用。
何时、为什么使用管
理界面?何时又不使
用呢?
经过这一章的学习,你应该对Django
管理工具有所认识。 但是我们需要
表明一个观点:什么时候 、为什么
用,以及什么时候又不 用。
Django的管理界面对非技术用户要输
入他们的数据时特别有用;事实上这
个特性就是专门为这个 实现的。 在
Django最开始开发的新闻报道的行业
应用中,有一个典型的在线自来水的
水质专题报道 应用,它的实现流程
是这样的:
负责这个报道的记者和要处理数
据的开发者碰头,提供一些数据
给开发者。
开发者围绕这些数据设计模型然
后配置一个管理界面给记者。
记者检查管理界面,尽早指出缺
少或多余的字段。 开发者来回地
修改模块。
当模块认可后,记者就开始用管
理界面输入数据。 同时,程序员
可以专注于开发公众访问视图和
模板(有趣的部分)。
换句话说,Django的管理界面为内容
输入人员和编程人员都提供了便利的
工具。
当然,除了数据输入方面,我们发现
管理界面在下面这些情景中也是很有
用的:
检查模块 :当你定义好了若干个
模块,在管理页面中把他们调出
来然后输入一些虚假的数据,这
是相当有用的。 有时候,它能显
示数据建模的错误或者模块中其
它问题。
管理既得数据 :如果你的应用程
序依赖外部数据(来自用户输入
或网络爬虫),管理界面提供了
一个便捷的途径,让你检查和编
辑那些数据。 你可以把它看作是
一个功能不那么强大,但是很方
便的数据库命令行工具。
临时的数据管理程序 :你可以用
管理工具建立自己的轻量级数据
管理程序,比如说开销记录。 如
果你正在根据自己的,而不是公
众的需要开发些什么,那么管理
界面可以带给你很大的帮助。 从
这个意义上讲,你可以把它看作
是一个增强的关系型电子表格。
最后一点要澄清的是: 管理界面不
是终结者。 过往许多年间,我们看
到它被拆分、修改成若干个功能模
块,而这些功能不是它所支持的。
它不应成为一个公众 数据访问接
口,也不应允许对你的数据进行复杂
的排序和查询。 正如本章开头所
说,它仅提供给可信任的管理员。
请记住这一点,它是有效使用管理界
面的钥匙。
下一章
到现在,我们已经创建了一些模块,
并且为编辑数据配置了一个优秀的界
面。 下一章,我们将转入到网站开
发中最重要的部分: 表单的创建和
处理。
从Google的简朴的单个搜索框,到常
见的Blog评论提交表单,再到复杂的
自定义数据输入接口,HTML表单一
直是交互性网站的支柱。 本章介绍
如何用Django对用户通过表单提交的
数据进行访问、有效性检查以及其它
处理。 与此同时,我们将介绍
HttpRequest对象和Form对象。
从Request对象中获取
数据
我们在第三章讲述View的函数时已经
介绍过HttpRequest对象了,但当时并
没有讲太多。 让我们回忆下:每个
view函数的第一个参数是一个
HttpRequest对象,就像下面这个
hello()函数:
from django.http import HttpResponse
def hello(request):
return HttpResponse("Hello world
"
)
HttpRequest对象,比如上面代码里的
request变量,会有一些有趣的、你必
须让自己熟悉的属性和方法,以便知
道能拿它们来做些什么。 在view函
数的执行过程中,你可以用这些属性
来获取当前request的一些信息(比
如,你正在加载这个页面的用户是
谁,或者用的是什么浏览器)。
URL相关信息
HttpRequest对象包含当前请求URL的
一些信息:
属性/方法
说明
举例
除域名
以外的
请求路
request.path
"/h
径,以
正斜杠
开头
主机名
(比
如,通
常所说
的域
request.get_host()
"12
名)
请求路
径,可
request.get_full_path() 能包含 "/h
查询字
符串
如果通
过
HTTPS
访问,
则此方
法返回
Tr ue,
否则返
回
request.is_secure()
Tru
False
在view函数里,要始终用这个属性或
方法来得到URL,而不要手动输入。
这会使得代码更加灵活,以便在其它
地方重用。 下面是一个简单的例
子:
#
BAD!
def current_url_view_bad(request):
return HttpResponse("Welcome to
the page at /current/")
GOOD
#
def current_url_view_good(request):
return HttpResponse("Welcome to
the page at %s" % request.path)
有关request的其它信息
request.META 是一个Python字典,包
含了所有本次HTTP请求的Header信
息,比如用户IP地址和用户Agent(通
常是浏览器的名称和版本号)。 注
意,Header信息的完整列表取决于用
户所发送的Header信息和服务器端设
置的Header信息。 这个字典中几个常
见的键值有:
HTTP_REFERER,进站前链接网
页,如果有的话。 (请注意,它
是REFERRER的笔误。)
HTTP_USER_AGENT,用户浏览
器的user-agent字符串,如果有的
话。 例
如:"Mozilla/5.0 (X11; U; Linux i68
6; fr-
FR; rv:1.8.1.17) Gecko/20080829 Fi
refox/2.0.0.17" .
REMOTE_ADDR 客户端IP,
如:"12.345.67.89" 。(如果申请是
经过代理服务器的话,那么它可
能是以逗号分割的多个IP地址,
如:"12.345.67.89,23.456.78.90"
。)
注意,因为 request.META 是一个普
通的Python字典,因此当你试图访问
一个不存在的键时,会触发一个
KeyError异常。 (HTTP header信息
是由用户的浏览器所提交的、不应该
给予信任的“额外”数据,因此你总是
应该好好设计你的应用以便当一个特
定的Header数据不存在时,给出一个
优雅的回应。)你应该用 try/except
语句,或者用Python字典的 get() 方法
来处理这些“可能不存在的键”:
#
BAD!
def ua_display_bad(request):
ua = request.META['HTTP_USER_AGE
NT'] # Might raise KeyError!
return HttpResponse("Your browse
r is %s" % ua)
#
GOOD (VERSION 1)
def ua_display_good1(request):
try:
ua = request.META['HTTP_USER
_
AGENT']
except KeyError:
ua = 'unknown'
return HttpResponse("Your browse
r is %s" % ua)
GOOD (VERSION 2)
#
def ua_display_good2(request):
ua = request.META.get('HTTP_USER
_
AGENT', 'unknown')
return HttpResponse("Your browse
r is %s" % ua)
我们鼓励你动手写一个简单的view函
数来显示 request.META 的所有数
据,这样你就知道里面有什么了。
这个view函数可能是这样的:
def display_meta(request):
values = request.META.items()
values.sort()
html = []
for k, v in values:
html.append('<tr><td>%s</td>
<
td>%s</td></tr>' % (k, v))
return HttpResponse('<table>%s</
table>' % '\n'.join(html))
做为一个练习,看你自己能不能把上
面这个view函数改用Django模板系统
来实现,而不是上面这样来手动输入
HTML代码。 也可以试着把前面提到
的 request.path 方法或 HttpRequest 对
象的其它方法加进去。
提交的数据信息
除了基本的元数据,HttpRequest对象
还有两个属性包含了用户所提交的信
息: request.GET 和 request.POST。
二者都是类字典对象,你可以通过它
们来访问GET和POST数据。
类字典对象
我们说“request.GET和request.POST是
类字典对象”,意思是他们的行为像
Python里标准的字典对象,但在技术
底层上他们不是标准字典对象。 比
如说,request.GET和request.POST都
有get()、keys()和values()方法,你可
以用用 for key in request.GET 获取所
有的键。
那到底有什么区别呢? 因为
request.GET和request.POST拥有一些
普通的字典对象所没有的方法。 我
们会稍后讲到。
你可能以前遇到过相似的名字:类文
件对象,这些Python对象有一些基本
的方法,如read(),用来做真正的
Python文件对象的代用品。
POST数据是来自HTML中的〈for m〉
标签提交的,而GET数据可能来自
〈for m〉提交也可能是URL中的查询
字符串(the query string)。
一个简单的表单处理
示例
继续本书一直进行的关于书籍、作
者、出版社的例子,我们现在来创建
一个简单的view函数以便让用户可以
通过书名从数据库中查找书籍。
通常,表单开发分为两个部分: 前
端HTML页面用户接口和后台view函
数对所提交数据的处理过程。 第一
部分很简单;现在我们来建立个view
来显示一个搜索表单:
from django.shortcuts import render_
to_response
def search_form(request):
return render_to_response('searc
h_form.html')
在第三章已经学过,这个view函数可
以放到Python的搜索路径的任何位
置。 为了便于讨论,咱们将它放在
books/views.py 里。
这个 search_form.html 模板,可能看
起来是这样的:
<
<
html>
head>
<
title>Search</title>
<
<
/head>
body>
<
form action="/search/" method="
get">
<
<
input type="text" name="q">
input type="submit" value="
Search">
<
/form>
<
<
/body>
/html>
而 urls.py 中的 URLpattern 可能是这
样的:
from mysite.books import views
urlpatterns = patterns('',
#
...
(r'^search-form/$', views.search
form),
...
_
)
#
(注意,我们直接将views模块import
进来了,而不是用类似 from
mysite.views import search_form 这样
的语句,因为前者看起来更简洁。
我们将在第8章讲述更多的关于
import的用法。)
现在,如果你运行 runserver 命令,
然后访问http://127.0.0.1:8000/search-
form/,你会看到搜索界面。 非常简
单。
不过,当你通过这个for m提交数据
时,你会得到一个Django 404错误。
这个Form指向的URL /search/ 还没有
被实现。 让我们添加第二个视图函
数并设置URL:
#
urls.py
urlpatterns = patterns('',
...
(r'^search-form/$', views.search
form),
(r'^search/$', views.search),
#
_
#
...
)
#
views.py
def search(request):
if 'q' in request.GET:
message = 'You searched for:
r' % request.GET['q']
else:
message = 'You submitted an
empty form.'
return HttpResponse(message)
%
暂时先只显示用户搜索的字词,以确
定搜索数据被正确地提交给了
Django,这样你就会知道搜索数据是
如何在这个系统中传递的。 简而言
之:
1
. 在HTML里我们定义了一个变量
q。当提交表单时,变量q的值通
过GET(method=”get”)附加在URL
/
search/上。
2
. 处理/search/(search())的视图通
过request.GET来获取q的值。
需要注意的是在这里明确地判断q是
否包含在request.GET中。就像上面
r equest.META小节里面提到,对于用
户提交过来的数据,甚至是正确的数
据,都需要进行过滤。 在这里若没
有进行检测,那么用户提交一个空的
表单将引发KeyError异常:
#
BAD!
def bad_search(request):
#
The following line will raise
KeyError if 'q' hasn't
#
been submitted!
message = 'You searched for: %r'
request.GET['q']
%
return HttpResponse(message)
查询字符串参数
因为使用GET方法的数据是通过查询
字符串的方式传递的(例如/search/?
q=django),所以我们可以使用
requet.GET来获取这些数据。 第三章
介绍Django的URLconf系统时我们比
较了Django的简洁的URL与PHP/Java
传统的URL,我们提到将在第七章讲
述如何使用传统的URL。通过刚才的
介绍,我们知道在视图里可以使用
request.GET来获取传统URL里的查询
字符串(例如hours=3)。
获取使用POST方法的数据与GET的
相似,只是使用request.POST代替了
request.GET。那么,POST与GET之
间有什么不同?当我们提交表单仅仅
需要获取数据时就可以用GET; 而当
我们提交表单时需要更改服务器数据
的状态,或者说发送e-mail,或者其
他不仅仅是获取并显示数据的时候就
使用POST。 在这个搜索书籍的例子
里,我们使用GET,因为这个查询不
会更改服务器数据的状态。 (如果你
有兴趣了解更多关于GET和POST的
知识,可以参见
http://www.w3.org/2001/tag/doc/whenT
oUseGet.html。)
既然已经确认用户所提交的数据是有
效的,那么接下来就可以从数据库中
查询这个有效的数据(同样,在
views.py里操作):
from django.http import HttpResponse
from django.shortcuts import render_
to_response
from mysite.books.models import Book
def search(request):
if 'q' in request.GET and reques
t.GET['q']:
q = request.GET['q']
books = Book.objects.filter(
title__icontains=q)
return render_to_response('s
earch_results.html',
{
'books': books, 'query'
:
q})
else:
return HttpResponse('Please
submit a search term.')
让我们来分析一下上面的代码:
除了检查q是否存在于request.GET
之外,我们还检查来
reuqest.GET[‘q’]的值是否为空。
我们使用
Book.objects.filter(title__icontains=q
)
获取数据库中标题包含q的书籍。
icontains是一个查询关键字(参看
第五章和附录B)。这个语句可以
理解为获取标题里包含q的书籍,
不区分大小写。
这是实现书籍查询的一个很简单
的方法。 我们不推荐在一个包含
大量产品的数据库中使用icontains
查询,因为那会很慢。 (在真实
的案例中,我们可以使用以某种
分类的自定义查询系统。 在网上
搜索“开源 全文搜索”看看是否有
好的方法)
最后,我们给模板传递来books,
一个包含Book对象的列表。 查询
结果的显示模板search_results.html
如下所示:
<
p>You searched for: <strong>{{ quer
y }}</strong></p>
{
{
% if books %}
<
p>Found {{ books|length }} book
{ books|pluralize }}.</p>
<
ul>
{
<
{
% for book in books %}
li>{{ book.title }}</li>
% endfor %}
<
/ul>
{
% else %}
p>No books matched your search
criteria.</p>
% endif %}
<
{
注意这里pluralize的使用,这个过
滤器在适当的时候会输出s(例如
找到多本书籍)。
改进表单
同上一章一样,我们先从最为简单、
有效的例子开始。 现在我们再来找
出这个简单的例子中的不足,然后改
进他们。
首先,search()视图对于空字符串的
处理相当薄弱——仅显示一条”Please
submi t a search term.”的提示信息。 若
用户要重新填写表单必须自行点
击“后退”按钮, 这种做法既糟糕又不
专业。如果在现实的案例中,我们这
样子编写,那么Django的优势将荡然
无存。
在检测到空字符串时更好的解决方法
是重新显示表单,并在表单上面给出
错误提示以便用户立刻重新填写。
最简单的实现方法既是添加else分句
重新显示表单,代码如下:
from django.http import HttpResponse
from django.shortcuts import render_
to_response
from mysite.books.models import Book
def search_form(request):
return render_to_response('searc
h_form.html')
def search(request):
if 'q' in request.GET and reques
t.GET['q']:
q = request.GET['q']
books = Book.objects.filter(
title__icontains=q)
return render_to_response('s
earch_results.html',
{
'books': books, 'query'
:
q})
else:
return render_to_response('s
earch_form.html', {'error': True})
(注意,将search_form()视图也包含
进来以便查看)
这段代码里,我们改进来search()视
图:在字符串为空时重新显示
search_form.html。 并且给这个模板传
递了一个变量error,记录着错误提示
信息。 现在我们编辑一下
search_form.html,检测变量error:
<
<
html>
head>
<
title>Search</title>
<
<
/head>
body>
{
% if error %}
p style="color: red;">Pleas
e submit a search term.</p>
<
{
<
% endif %}
form action="/search/" method="
get">
<
<
input type="text" name="q">
input type="submit" value="
Search">
<
/form>
<
<
/body>
/html>
我们修改了search_form()视图所使用
的模板,因为search_form()视图没有
传递error变量,所以在条用
search_form视图时不会显示错误信
息。
通过上面的一些修改,现在程序变的
好多了,但是现在出现一个问题:
是否有必要专门编写search_form()来
显示表单? 按实际情况来说,当一
个请求发送至/search/(未包含GET的
数据)后将会显示一个空的表单(带
有错误信息)。 所以,只要我们改
变search()视图:当用户访问/search/
并未提交任何数据时就隐藏错误信
息,这样就移去search_form()视图以
及对应的URLpattern。
def search(request):
error = False
if 'q' in request.GET:
q = request.GET['q']
if not q:
error = True
else:
books = Book.objects.fil
ter(title__icontains=q)
return render_to_respons
e('search_results.html',
{
'books': books, 'qu
ery': q})
return render_to_response('searc
h_form.html',
'error': error})
{
在改进后的视图中,若用户访
问/search/并且没有带有GET数据,那
么他将看到一个没有错误信息的表
单; 如果用户提交了一个空表单,
那么它将看到错误提示信息,还有表
单; 最后,若用户提交了一个非空
的值,那么他将看到搜索结果。
最后,我们再稍微改进一下这个表
单,去掉冗余的部分。 既然已经将
两个视图与URLs合并起来,/search/
视图管理着表单的显示以及结果的显
示,那么在search_form.html里表单的
action值就没有必要硬编码的指定
URL。 原先的代码是这样:
<
form action="/search/" method="get">
现在改成这样:
<
form action="" method="get">
action=”“意味着表单将提交给与当前
页面相同的URL。 这样修改之后,如
果search()视图不指向其它页面的
话,你将不必再修改action。
简单的验证
我们的搜索示例仍然相当地简单,特
别从数据验证方面来讲;我们仅仅只
验证搜索关键值是否为空。 然后许
多HTML表单包含着比检测值是否为
空更为复杂的验证。 我们都有在网
站上见过类似以下的错误提示信息:
请输入一个有效的email地址,
foo’ 并不是一个有效的e-mail地
址。
请输入5位数的U. S 邮政编码,
23并非是一个有效的邮政编码。
1
请输入YYYY- MM- DD格式的日
期。
请输入8位数以上并至少包含一个
数字的密码。
关于JavaScript验证
可以使用Javascript在客户端浏览器里
对数据进行验证,这些知识已超出本
书范围。 要注意: 即使在客户端已
经做了验证,但是服务器端仍必须再
验证一次。 因为有些用户会将
JavaScript关闭掉,并且还有一些怀
有恶意的用户会尝试提交非法的数据
来探测是否有可以攻击的机会。
除了在服务器端对用户提交的数据进
行验证(例如在视图里验证),我们
没有其他办法。 JavaScript验证可以
看作是额外的功能,但不能作为唯一
的验证功能。
我们来调整一下search()视图,让她
能够验证搜索关键词是否小于或等于
2
0个字符。 (为来让例子更为显
著,我们假设如果关键词超过20个字
符将导致查询十分缓慢)。那么该如
何实现呢? 最简单的方式就是将逻
辑处理直接嵌入到视图里,就像这
样:
def search(request):
error = False
if 'q' in request.GET:
q = request.GET['q']
if not q:
error = True
elif len(q) > 20:
error = True
else:
books = Book.objects.fil
ter(title__icontains=q)
return render_to_respons
e('search_results.html',
{
'books': books, 'qu
ery': q})
return render_to_response('searc
h_form.html',
'error': error})
{
现在,如果尝试着提交一个超过20个
字符的搜索关键词,系统不会执行搜
索操作,而是显示一条错误提示信
息。 但是,search_form.html里的这条
提示信息是:”Please submi t a search
term.”,这显然是错误的, 所以我们
需要更精确的提示信息:
<
<
html>
head>
<
title>Search</title>
<
<
/head>
body>
{
% if error %}
<
p style="color: red;">Pleas
e submit a search term 20 characters
or shorter.</p>
{
<
% endif %}
form action="/search/" method="
get">
<
<
input type="text" name="q">
input type="submit" value="
Search">
<
/form>
<
<
/body>
/html>
但像这样修改之后仍有一些问题。
我们包含万象的提示信息很容易使人
产生困惑: 提交一个空表单怎么会
出现一个关于20个字符限制的提示?
所以,提示信息必须是详细的,明确
的,不会产生疑议。
问题的实质在于我们只使用来一个布
尔类型的变量来检测是否出错,而不
是使用一个列表来记录相应的错误信
息。 我们需要做如下的调整:
def search(request):
errors = []
if 'q' in request.GET:
q = request.GET['q']
if not q:
errors.append('Enter a s
earch term.')
elif len(q) > 20:
errors.append('Please en
ter at most 20 characters.')
else:
books = Book.objects.fil
ter(title__icontains=q)
return render_to_respons
e('search_results.html',
'books': books, 'qu
{
ery': q})
return render_to_response('searc
h_form.html',
'errors': errors })
{
接着,我们要修改一下
search_form.html模板,现在需要显示
一个errors列表而不是一个布尔判
断。
<
<
html>
head>
<
title>Search</title>
<
<
/head>
body>
{
% if errors %}
ul>
<
{
% for error in errors %
}
<
{
li>{{ error }}</li>
% endfor %}
<
/ul>
{
<
% endif %}
form action="/search/" method="
get">
<
<
input type="text" name="q">
input type="submit" value="
Search">
<
/form>
<
<
/body>
/html>
编写Contact表单
虽然我们一直使用书籍搜索的示例表
单,并将起改进的很完美,但是这还
是相当的简陋: 只包含一个字段,
q。这简单的例子,我们不需要使用
Django表单库来处理。 但是复杂一点
的表单就需要多方面的处理,我们现
在来一下一个较为复杂的例子: 站
点联系表单。
这个表单包括用户提交的反馈信息,
一个可选的e-mail回信地址。 当这个
表单提交并且数据通过验证后,系统
将自动发送一封包含题用户提交的信
息的e-mail给站点工作人员。
我们从contact_form.html模板入手:
<
<
html>
head>
<
title>Contact us</title>
<
<
/head>
body>
<
h1>Contact us</h1>
{
% if errors %}
<
<
ul>
{
% for error in errors %
}
<
{
li>{{ error }}</li>
% endfor %}
/ul>
{
<
% endif %}
form action="/contact/" method=
"
post">
<
p>Subject: <input type="tex
t" name="subject"></p>
<
p>Your e-mail (optional): <
input type="text" name="email"></p>
<
p>Message: <textarea name="
message" rows="10" cols="50"></texta
rea></p>
<
input type="submit" value="
Submit">
/form>
<
<
<
/body>
/html>
我们定义了三个字段: 主题,e-mail
和反馈信息。 除了e-mail字段为可
选,其他两个字段都是必填项。 注
意,这里我们使用method=”post”而非
method=”get”,因为这个表单会有一
个服务器端的操作:发送一封e-
mail。 并且,我们复制了前一个模板
search_form.html中错误信息显示的代
码。
如果我们顺着上一节编写search()视
图的思路,那么一个contact()视图代
码应该像这样:
from django.core.mail import send_ma
il
from django.http import HttpResponse
Redirect
from django.shortcuts import render_
to_response
def contact(request):
errors = []
if request.method == 'POST':
if not request.POST.get('sub
ject', ''):
errors.append('Enter a s
ubject.')
if not request.POST.get('mes
sage', ''):
errors.append('Enter a m
essage.')
if request.POST.get('email')
and '@' not in request.POST['email'
]
:
errors.append('Enter a v
alid e-mail address.')
if not errors:
send_mail(
request.POST['subjec
t'],
e'],
request.POST['messag
request.POST.get('em
ail', 'noreply@example.com'),
'siteowner@example.
[
com'],
)
return HttpResponseRedir
ect('/contact/thanks/')
return render_to_response('conta
ct_form.html',
{
'errors': errors})
(如果按照书中的示例做下来,这这
里可能乎产生一个疑问:contact()视
图是否要放在books/views.py这个文
件里。 但是contact()视图与books应用
没有任何关联,那么这个视图应该可
以放在别的地方? 这毫无紧要,只
要在URLconf里正确设置URL与视图
之间的映射,Django会正确处理的。
笔者个人喜欢创建一个contact的文件
夹,与books文件夹同级。这个文件
夹中包括空的init.py和views.py两个文
件。
现在来分析一下以上的代码:
确认request.method的值
是’POST’。用户浏览表单时这个
值并不存在,当且仅当表单被提
交时这个值才出现。 (在后面的
例子中,request.method将会设置
为’GET’,因为在普通的网页浏览
中,浏览器都使用GET,而非
POST)。判断request.method的值
很好地帮助我们将表单显示与表
单处理隔离开来。
我们使用request.POST代替
request.GET来获取提交过来的数
据。 这是必须的,因为
contact_form.html里表单使用的是
method=”post”。如果在视图里通过
POST获取数据,那么request.GET
将为空。
这里,有两个必填项,subject 和
message,所以需要对这两个进行
验证。 注意,我们使用
request.POST.get()方法,并提供一
个空的字符串作为默认值;这个
方法很好的解决了键丢失与空数
据问题。
虽然email非必填项,但如果有提
交她的值则我们也需进行验证。
我们的验证算法相当的薄弱,仅
验证值是否包含@字符。 在实际
应用中,需要更为健壮的验证机
制(Django提供这些验证机制,稍
候我们就会看到)。
我们使用了
django.core.mail.send_mail函数来发
送e-mail。 这个函数有四个必选参
数: 主题,正文,寄信人和收件
人列表。 send_mail是Django的
EmailMessage类的一个方便的包
装,EmailMessage类提供了更高级
的方法,比如附件,多部分邮
件,以及对于邮件头部的完整控
制。
注意,若要使用send_mail()函数来
发送邮件,那么服务器需要配置
成能够对外发送邮件,并且在
Django中设置出站服务器地址。
参见规
范:http://docs.djangoproject.com/e
n/dev/topics/email/
当邮件发送成功之后,我们使用
HttpResponseRedirect对象将网页重
定向至一个包含成功信息的页
面。 包含成功信息的页面这里留
给读者去编写(很简单 一个视
图/ URL映射/一份模板即可),但
是我们要解释一下为何重定向至
新的页面,而不是在模板中直接
调用render_to_response()来输出。
原因就是: 若用户刷新一个包含
POST表单的页面,那么请求将会
重新发送造成重复。 这通常会造
成非期望的结果,比如说重复的
数据库记录;在我们的例子中,
将导致发送两封同样的邮件。 如
果用户在POST表单之后被重定向
至另外的页面,就不会造成重复
的请求了。
我们应每次都给成功的POST请求
做重定向。 这就是web开发的最佳
实践。
contact()视图可以正常工作,但是她
的验证功能有些复杂。 想象一下假
如一个表单包含一打字段,我们真的
将必须去编写每个域对应的if判断语
句?
另外一个问题是表单的重新显示。若
数据验证失败后,返回客户端的表单
中各字段最好是填有原来提交的数
据,以便用户查看哪里出现错误(用
户也不需再次填写正确的字段值)。
我们可以手动地将原来的提交数据返
回给模板,并且必须编辑HTML里的
各字段来填充原来的值。
#
views.py
def contact(request):
errors = []
if request.method == 'POST':
if not request.POST.get('sub
ject', ''):
errors.append('Enter a s
ubject.')
if not request.POST.get('mes
sage', ''):
errors.append('Enter a m
essage.')
if request.POST.get('email')
and '@' not in request.POST['email'
:
]
errors.append('Enter a v
alid e-mail address.')
if not errors:
send_mail(
request.POST['subjec
t'],
e'],
request.POST['messag
request.POST.get('em
ail', `'noreply@example.com`_'),
`'siteowner@example
[
.
com`_'],
)
return HttpResponseRedir
ect('/contact/thanks/')
return render_to_response('conta
ct_form.html', {
'
'
errors': errors,
subject': request.POST.get(
'
subject', ''),
'
message': request.POST.get(
message', ''),
email': request.POST.get('e
mail', ''),
'
'
}
)
#
contact_form.html
<
<
html>
head>
<
title>Contact us</title>
<
<
/head>
body>
<
h1>Contact us</h1>
{
% if errors %}
<
<
ul>
{
% for error in errors %
}
<
{
li>{{ error }}</li>
% endfor %}
/ul>
{
<
% endif %}
form action="/contact/" method=
"
post">
<
p>Subject: <input type="tex
t" name="subject" value="{{ subject
}
}" ></p>
<
p>Your e-mail (optional): <
input type="text" name="email" value
"{{ email }}" ></p>
p>Message: <textarea name="
=
<
message" rows="10" cols="50">{{ mess
age }}</textarea></p>
<
input type="submit" value="
Submit">
<
/form>
<
<
/body>
/html>
这看起来杂乱,且写的时候容易出
错。 希望你开始明白使用高级库的
用意——负责处理表单及相关校验任
务。
第一个Form 类
Django带有一个for m库,称为
django.forms,这个库可以处理我们本
章所提到的包括HTML表单显示以及
验证。 接下来我们来深入了解一下
for m库,并使用她来重写contact表单
应用。
Django的new forms库
在Django社区上会经常看到
django.newforms这个词语。当人们讨
论django.newforms,其实就是我们本
章里面介绍的django.forms。
改名其实有历史原因的。 当Django一
次向公众发行时,它有一个复杂难懂
的表单系统:django.forms。后来它被
完全重写了,新的版本改叫作:
django.newforms,这样人们还可以通
过名称,使用旧版本。 当Django 1.0
发布时,旧版本django.forms就不再使
用了,而django.newforms也终于可以
名正言顺的叫做:django.forms。
表单框架最主要的用法是,为每一个
将要处理的HTML的
`` 定义一个Form类。 在这个例子中,我们
,
因此我们只需定义一个Form类。 这个类可
views.py
文件里也行,但是社区的惯例是把Form类都放
views.py`` 的目录中,创建这个文
件,然后输入:
from django import forms
class ContactForm(forms.Form):
subject = forms.CharField()
email = forms.EmailField(require
d=False)
message = forms.CharField()
这看上去简单易懂,并且很像在模块
中使用的语法。 表单中的每一个字
段(域)作为Form类的属性,被展现
成Field类。这里只用到CharField和
EmailField类型。 每一个字段都默认
是必填。要使email成为可选项,我
们需要指定required=False。
让我们钻研到Python解释器里面看看
这个类做了些什么。 它做的第一件
事是将自己显示成HTML:
>
>> from contact.forms import Contac
tForm
>
>
<
>> f = ContactForm()
>> print f
tr><th><label for="id_subject">Subj
ect:</label></th><td><input type="te
xt" name="subject" id="id_subject" /
>
<
<
</td></tr>
tr><th><label for="id_email">Email:
/label></th><td><input type="text"
name="email" id="id_email" /></td></
tr>
<
tr><th><label for="id_message">Mess
age:</label></th><td><input type="te
xt" name="message" id="id_message
为了便于访问,Django用 标志,
为每一个字段添加了标签。 这个做
法使默认行为尽可能合适。
默认输出按照HTML的格式,另外有
一些其它格式的输出:
>
<
<
>> print f.as_ul()
li><label for="id_subject">Subject:
/label> <input type="text" name="su
bject" id="id_subject" /></li>
<
li><label for="id_email">Email:</la
bel> <input type="text" name="email"
id="id_email" /></li>
<
<
li><label for="id_message">Message:
/label> <input type="text" name="me
ssage" id="id_message" /></li>
>
<
/
>> print f.as_p()
p><label for="id_subject">Subject:<
label> <input type="text" name="sub
ject" id="id_subject" /></p>
<
p><label for="id_email">Email:</lab
el> <input type="text" name="email"
id="id_email" /></p>
<
/
p><label for="id_message">Message:<
label> <input type="text" name="mes
sage" id="id_message" /></p>
请注意,标签、、的开闭合标记没有
包含于输出当中,这样你就可以添加
额外的行或者自定义格式。
这些类方法只是一般情况下用于快捷
显示完整表单的方法。 你同样可以
用HTML显示个别字段:
>
<
=
>
<
=
>> print f['subject']
input type="text" name="subject" id
"id_subject" />
>> print f['message']
input type="text" name="message" id
"id_message" />
Form对象做的第二件事是校验数据。
为了校验数据,我们创建一个新的对
Form象,并且传入一个与定义匹配的
字典类型数据:
>
>> f = ContactForm({'subject': 'Hel
lo', 'email': 'adrian@example.com',
message': 'Nice site!'})
'
一旦你对一个Form实体赋值,你就得
到了一个绑定for m:
>
>> f.is_bound
True
调用任何绑定for m的is_valid()方法,
就可以知道它的数据是否合法。 我
们已经为每个字段传入了值,因此整
个Form是合法的:
>
>> f.is_valid()
True
如果我们不传入email值,它依然是
合法的。因为我们指定这个字段的属
性required=False:
>
>> f = ContactForm({'subject': 'Hel
lo', 'message': 'Nice site!'})
>> f.is_valid()
>
True
但是,如果留空subject或message,整
个Form就不再合法了:
>
>> f = ContactForm({'subject': 'Hel
lo'})
>> f.is_valid()
False
>> f = ContactForm({'subject': 'Hel
lo', 'message': ''})
>> f.is_valid()
False
>
>
>
你可以逐一查看每个字段的出错消
息:
>
>> f = ContactForm({'subject': 'Hel
lo', 'message': ''})
>
[
>
[
>
[
>> f['message'].errors
u'This field is required.']
>> f['subject'].errors
]
>> f['email'].errors
]
每一个邦定Form实体都有一个errors
属性,它为你提供了一个字段与错误
消息相映射的字典表。
>
>> f = ContactForm({'subject': 'Hel
lo', 'message': ''})
>> f.errors
>
{
'message': [u'This field is require
d.']}
最终,如果一个Form实体的数据是合
法的,它就会有一个可用的
cleaned_data属性。 这是一个包含干
净的提交数据的字典。 Django的form
框架不但校验数据,它还会把它们转
换成相应的Python类型数据,这叫做
清理数据。
>
,
:
>
>> f = ContactForm({subject': Hello
email: adrian@example.com, message
Nice site!})
>> f.is_valid()
True
>>> f.cleaned_data
{
message': uNice site!, email: uadri
an@example.com, subject: uHello}
我们的contact for m只涉及字符串类
型,它们会被清理成Unicode对象。
如果我们使用整数型或日期型,form
框架会确保方法使用合适的Python整
数型或datetime.date型对象。
在视图中使用Form对
象
在学习了关于Form类的基本知识后,
你会看到我们如何把它用到视图中,
取代contact()代码中不整齐的部分。
一下示例说明了我们如何用forms框
架重写contact() :
#
views.py
from django.shortcuts import render_
to_response
from mysite.contact.forms import Con
tactForm
def contact(request):
if request.method == 'POST':
form = ContactForm(request.P
OST)
if form.is_valid():
cd = form.cleaned_data
send_mail(
cd['subject'],
cd['message'],
cd.get('email', 'nor
eply@example.com'),
[
'siteowner@example.
com'],
)
return HttpResponseRedir
ect('/contact/thanks/')
else:
form = ContactForm()
return render_to_response('conta
ct_form.html', {'form': form})
#
contact_form.html
<
<
html>
head>
<
title>Contact us</title>
<
<
/head>
body>
<
h1>Contact us</h1>
{
% if form.errors %}
<
p style="color: red;">
Please correct the error
{
{ form.errors|pluralize }} below.
<
/p>
{
<
% endif %}
form action="" method="post">
<
table>
{
{ form.as_table }}
<
<
/table>
input type="submit" value="
Submit">
/form>
<
<
<
/body>
/html>
看看,我们能移除这么多不整齐的代
码! Django的forms框架处理HTML显
示、数据校验、数据清理和表单错误
重现。
尝试在本地运行。 装载表单,先留
空所有字段提交空表单;继而填写一
个错误的邮箱地址再尝试提交表单;
最后再用正确数据提交表单。 (根
据服务器的设置,当send_mail()被调
用时,你将得到一个错误提示。而这
是另一个问题。)
改变字段显示
你可能首先注意到:当你在本地显示
这个表单的时,message字段被显示
成 input type=”text” ,而它应该
被显示成。我们可以通过设置 widget
来修改它:
from django import forms
class ContactForm(forms.Form):
subject = forms.CharField()
email = forms.EmailField(require
d=False)
message = forms.CharField(widget
forms.Textarea )
=
forms框架把每一个字段的显示逻辑
分离到一组部件(widget)中。 每一
个字段类型都拥有一个默认的部件,
我们也可以容易地替换掉默认的部
件,或者提供一个自定义的部件。
考虑一下Field类表现 校验逻辑 ,而
部件表现 显示逻辑 。
设置最大长度
一个最经常使用的校验要求是检查字
段长度。 另外,我们应该改进
ContactForm,使subject限制在100个
字符以内。 为此,仅需为CharField
提供max_length参数,像这样:
from django import forms
class ContactForm(forms.Form):
subject = forms.CharField(max_le
ngth=100 )
email = forms.EmailField(require
d=False)
message = forms.CharField(widget
=
forms.Textarea)
选项mi n_l ength参数同样可用。
设置初始值
让我们再改进一下这个表单:为字
subject段添加 初始值
:
"I love your site!" (一点建议,但
没坏处。)为此,我们可以在创建
Form实体时,使用initial参数:
def contact(request):
if request.method == 'POST':
form = ContactForm(request.P
OST)
if form.is_valid():
cd = form.cleaned_data
send_mail(
cd['subject'],
cd['message'],
cd.get('email', `'no
reply@example.com`_'),
[
`'siteowner@example
.
com`_'],
)
return HttpResponseRedir
ect('/contact/thanks/')
else:
form = ContactForm(
initial={'subject': 'I l
ove your site!'}
)
return render_to_response('conta
ct_form.html', {'form': form})
现在,subject字段将被那个句子填
充。
请注意,传入 初始值 数据和传入数
据以 绑定 表单是有区别的。 最大的
区别是,如果仅传入 初始值 数据,
表单是_unbound_的,那意味着它没
有错误消息。
自定义校验规则
假设我们已经发布了反馈页面了,
email已经开始源源不断地涌入了。
这里有一个问题: 一些提交的消息
只有一两个字,我们无法得知详细的
信息。 所以我们决定增加一条新的
校验: 来点专业精神,最起码写四
个字,拜托。
我们有很多的方法把我们的自定义校
验挂在Django的for m上。 如果我们的
规则会被一次又一次的使用,我们可
以创建一个自定义的字段类型。 大
多数的自定义校验都是一次性的,可
以直接绑定到for m类.
我们希望 message 字段有一个额外
的校验,我们增加一个
clean_message() 方法到 Form
类:
from django import forms
class ContactForm(forms.Form):
subject = forms.CharField(max_le
ngth=100)
email = forms.EmailField(require
d=False)
message = forms.CharField(widget
forms.Textarea)
=
def clean_message(self):
message = self.cleaned_data[
message']
'
num_words = len(message.spli
t())
if num_words < 4:
raise forms.ValidationEr
ror("Not enough words!")
return message
Django的for m系统自动寻找匹配的函
数方法,该方法名称以clean_开头,
并以字段名称结束。 如果有这样的
方法,它将在校验时被调用。
特别地,clean_message()方法将在指
定字段的默认校验逻辑执行 之后 被
调用。(本例中,在必填CharField这
个校验逻辑之后。)因为字段数据已
经被部分处理,所以它被从
self.cleaned_data中提取出来了。同
样,我们不必担心数据是否为空,因
为它已经被校验过了。
我们简单地使用了len()和split()的组
合来计算单词的数量。 如果用户输
入字数不足,我们抛出一个
forms.ValidationError型异常。这个异
常的描述会被作为错误列表中的一项
显示给用户。
在函数的末尾显式地返回字段的值非
常重要。 我们可以在我们自定义的
校验方法中修改它的值(或者把它转
换成另一种Python类型)。 如果我们
忘记了这一步,None值就会返回,原
始的数据就丢失掉了。
指定标签
HTML表单中自动生成的标签默认是
按照规则生成的:用空格代替下划
线,首字母大写。如email的标签
是"Email" 。(好像在哪听到过? 是
的,同样的逻辑被用于模块
(model)中字段的verbose_name值。
我们在第五章谈到过。)
像在模块中做过的那样,我们同样可
以自定义字段的标签。 仅需使用
label,像这样:
class ContactForm(forms.Form):
subject = forms.CharField(max_le
ngth=100)
email = forms.EmailField(require
d=False, label='Your e-mail address'
)
message = forms.CharField(widget
=
forms.Textarea)
定制Form设计
在上面的 contact_form.html 模板
中我们使用 {{form.as_table}} 显
示表单,不过我们可以使用其他更精
确控制表单显示的方法。
修改for m的显示的最快捷的方式是使
用CSS。 尤其是错误列表,可以增强
视觉效果。自动生成的错误列表精确
的使用 <ul class=”errorlist”> ,
这样,我们就可以针对它们使用
CSS。 下面的CSS让错误更加醒目
了:
<
style type="text/css">
ul.errorlist {
margin: 0;
padding: 0;
}
.
errorlist li {
background-color: red;
color: white;
display: block;
font-size: 10px;
margin: 0 0 3px;
padding: 4px 5px;
}
<
/style>
虽然,自动生成HTML是很方便的,
但是在某些时候,你会想覆盖默认的
显示。 {{form.as_table}}和其它的方
法在开发的时候是一个快捷的方式,
for m的显示方式也可以在for m中被方
便地重写。
每一个字段部件(, , , 或者类似)都可
以通过访问{{form.字段名}}进行单独
的渲染。
<
<
html>
head>
<
title>Contact us</title>
<
<
/head>
body>
<
h1>Contact us</h1>
{
% if form.errors %}
p style="color: red;">
<
Please correct the error
{
}
{ form.errors|pluralize }} below.
<
/p>
{
<
% endif %}
form action="" method="post">
<
div class="field">
{
{ form.subject.errors }
<
label for="id_subject">
Subject:</label>
{
{ form.subject }}
<
<
/div>
div class="field">
{
<
{ form.email.errors }}
label for="id_email">Yo
ur e-mail address:</label>
{
{ form.email }}
<
<
/div>
div class="field">
{
{ form.message.errors }
}
<
label for="id_message">
Message:</label>
{
{ form.message }}
<
<
/div>
input type="submit" value="
Submit">
<
/form>
<
<
/body>
/html>
{
{ form.message.errors }} 会
在 class="errorlist"> 里面显示,如果
字段是合法的,或者for m没有被绑
定,就显示一个空字符串。 我们还
可以把 form.message.errors 当作一个
布尔值或者当它是list在上面做迭
代, 例如:
<
div class="field{% if form.message.
errors %} errors{% endif %}">
% if form.message.errors %}
{
<
{
ul>
% for error in form.message
.
/
errors %}
strong></li>
<
li><strong>{{ error }}<
{
<
% endfor %}
/ul>
{
<
% endif %}
label for="id_message">Message:
<
<
/label>
{
{ form.message }}
/div>
在校验失败的情况下, 这段代码会在
包含错误字段的div的class属性中增
加一个”errors”,在一个有序列表中
显示错误信息。
下一章
这一章总结了本书的介绍材料,即所
谓“核心教程”。 后面部分,从第八章
到第十二章,将详细讲述高级(进
阶)使用,包括如何配置一个Django
应用程序(第十二章)。
在学习本书的前面七章后,我们终于
对于使用Django构建自己的网站已经
知道的够多了, 本书中剩余的材料
将在你需要的时候帮助你补遗。
第八章我们将回头、并深入地讲解
视图和URLconfs(第三章已简单介
绍)。
在第三章,我们已经对基本的Django
视图和URL配置做了介绍。 在这一
章,将进一步说明框架中这两个部分
的高级机能。
URLconf 技巧
URLconf没什么特别的,就象 Django
中其它东西一样,它们只是 Python 代
码。 你可以在几方面从中得到好
处,正如下面所描述的。
流线型化(Streamlining)函
数导入
看下这个 URLconf,它是建立在第三
章的例子上 :
from django.conf.urls.defaults impor
t *
from mysite.views import hello, curr
ent_datetime, hours_ahead
urlpatterns = patterns('',
(r'^hello/$', hello),
(r'^time/$', current_datetime),
(r'^time/plus/(\d{1,2})/$', hour
s_ahead),
)
正如第三章中所解释的,在 URLconf
中的每一个入口包括了它所关联的视
图函数,直接传入了一个函数对象。
这就意味着需要在模块开始处导入视
图函数。
但随着 Django 应用变得复杂,它的
URLconf 也在增长,并且维护这些导
入可能使得管理变麻烦。 (对每个新
的view函数,你不得不记住要导入
它,并且采用这种方法会使导入语句
将变得相当长。)可以通过导入 views
模块本身来避免这个麻烦。 下面例
子的URLconf与前一个等价:
from django.conf.urls.defaults impor
t *
from mysite import views
urlpatterns = patterns('',
(r'^hello/$', views.hello ),
(r'^time/$', views.current_datet
ime ),
(r'^time/plus/(d{1,2})/$', views
.
)
hours_ahead ),
Django 还提供了另一种方法可以在
URLconf 中为某个特别的模式指定视
图函数: 你可以传入一个包含模块
名和函数名的字符串,而不是函数对
象本身。 继续示例:
from django.conf.urls.defaults impor
t *
urlpatterns = patterns('',
(r'^hello/$', 'mysite.views.hell
o' ),
(r'^time/$', 'mysite.views.curre
nt_datetime' ),
(r'^time/plus/(d{1,2})/$', 'mysi
te.views.hours_ahead' ),
)
(注意视图名前后的引号。 应该使用
带引号
的 'mysite.views.current_datetime' 而不
是mysite.views.current_datetime 。)
使用这个技术,就不必导入视图函数
了;Django 会在第一次需要它时根据
字符串所描述的视图函数的名字和路
径,导入合适的视图函数。
当使用字符串技术时,你可以采用更
简化的方式:提取出一个公共视图前
缀。 在我们的URLconf例子中,每个
视图字符串的开始部分都是\,造成
重复输入。 我们可以把公共的前缀
提取出来,作为第一个参数传给函
数:
System Message: WARNING/ 2 (, line
9
9); backlink
Inline literal start-string without end-
string.
from django.conf.urls.defaults impor
t *
urlpatterns = patterns('mysite.views
'
,
(r'^hello/$', 'hello' ),
(r'^time/$', 'current_datetime'
)
,
(r'^time/plus/(d{1,2})/$', 'hour
s_ahead' ),
)
注意既不要在前缀后面跟着一个点号
("." ),也不要在视图字符串前面放一
个点号。 Django 会自动处理它们。
牢记这两种方法,哪种更好一些呢?
这取决于你的个人编码习惯和需要。
字符串方法的好处如下 :
更紧凑,因为不需要你导入视图
函数。
如果你的视图函数存在于几个不
同的 Python 模块的话,它可以使
得 URLconf 更易读和管理。
函数对象方法的好处如下 :
更容易对视图函数进行包装
(wrap)。 参见本章后面的《包装
视图函数》一节。
更 Pythonic,就是说,更符合
Python 的传统,如把函数当成对
象传递。
两个方法都是有效的,甚至你可以在
同一个 URLconf 中混用它们。 决定
权在你。
使用多个视图前缀
在实践中,如果你使用字符串技术,
特别是当你的 URLconf 中没有一个公
共前缀时,你最终可能混合视图。
然而,你仍然可以利用视图前缀的简
便方式来减少重复。 只要增加多
个 patterns() 对象,象这样:
旧的 :
from django.conf.urls.defaults impor
t *
urlpatterns = patterns('',
(r'^hello/$', 'mysite.views.hell
o'),
(r'^time/$', 'mysite.views.curre
nt_datetime'),
(r'^time/plus/(\d{1,2})/$', 'mys
ite.views.hours_ahead'),
(r'^tag/(\w+)/$', 'weblog.views.
tag'),
)
新的 :
from django.conf.urls.defaults impor
t *
urlpatterns = patterns('mysite.views
'
,
(r'^hello/$', 'hello'),
(r'^time/$', 'current_datetime')
,
(r'^time/plus/(\d{1,2})/$', 'hou
rs_ahead'),
)
urlpatterns += patterns('weblog.view
s',
(r'^tag/(\w+)/$', 'tag'),
)
整个框架关注的是存在一个名
为 urlpatterns 的模块级别的变量。如
上例,这个变量可以动态生成。 这
里我们要特别说明一下,patterns()返
回的对象是可相加的,这个特性可能
是大家没有想到的。
调试模式中的特例
说到动态构建 urlpatterns,你可能想
利用这一技术,在 Django 的调试模
式下修改 URLconf 的行为。 为了做
到这一点,只要在运行时检
查 DEBUG 配置项的值即可,如:
from django.conf import settings
from django.conf.urls.defaults impor
t *
from mysite import views
urlpatterns = patterns('',
(r'^$', views.homepage),
(r'^(\d{4})/([a-z]{3})/$', views
.
)
archive_month),
if settings.DEBUG:
urlpatterns += patterns('',
(r'^debuginfo/$', views.debu
g),
)
在这个例子中,URL链
接/debuginfo/ 只在你的 DEBUG 配置
项设为 Tr ue 时才有效。
使用命名组
在目前为止的所有 URLconf 例子中,
我们使用简单的无命名 正则表达式
组,即,在我们想要捕获的URL部分
上加上小括号,Django 会将捕获的文
本作为位置参数传递给视图函数。
在更高级的用法中,还可以使用 命
名 正则表达式组来捕获URL,并且将
其作为 关键字 参数传给视图。
关键字参数 对比 位置参数
一个 Python 函数可以使用关键字参数
或位置参数来调用,在某些情况下,
可以同时进行使用。 在关键字参数
调用中,你要指定参数的名字和传入
的值。 在位置参数调用中,你只需
传入参数,不需要明确指明哪个参数
与哪个值对应,它们的对应关系隐含
在参数的顺序中。
例如,考虑这个简单的函数 :
def sell(item, price, quantity):
print "Selling %s unit(s) of %s
at %s" % (quantity, item, price)
为了使用位置参数来调用它,你要按
照在函数定义中的顺序来指定参数。
sell('Socks', '$2.50', 6)
为了使用关键字参数来调用它,你要
指定参数名和值。 下面的语句是等
价的 :
sell(item='Socks', price='$2.50', qu
antity=6)
sell(item='Socks', quantity=6, price
=
'$2.50')
sell(price='$2.50', item='Socks', qu
antity=6)
sell(price='$2.50', quantity=6, item
=
'Socks')
sell(quantity=6, item='Socks', price
'$2.50')
sell(quantity=6, price='$2.50', item
'Socks')
=
=
最后,你可以混合关键字和位置参
数,只要所有的位置参数列在关键字
参数之前。 下面的语句与前面的例
子是等价 :
sell('Socks', '$2.50', quantity=6)
sell('Socks', price='$2.50', quantit
y=6)
sell('Socks', quantity=6, price='$2.
5
0')
在 Python 正则表达式中,命名的正则
表达式组的语法是 (?Ppattern) ,这
里 na me 是组的名字,而pattern 是匹
配的某个模式。
下面是一个使用无名组的 URLconf 的
例子 :
from django.conf.urls.defaults impor
t *
from mysite import views
urlpatterns = patterns('',
(r'^articles/(\d{4})/$', views.y
ear_archive),
(r'^articles/(\d{4})/(\d{2})/$',
views.month_archive),
)
下面是相同的 URLconf,使用命名组
进行了重写 :
from django.conf.urls.defaults impor
t *
from mysite import views
urlpatterns = patterns('',
(r'^articles/(?P<year>\d{4})/$',
views.year_archive),
(r'^articles/(?P<year>\d{4})/(?P
<
month>\d{2})/$', views.month_archiv
e),
)
这段代码和前面的功能完全一样,只
有一个细微的差别: 取的值是以关
键字参数的方式而不是以位置参数的
方式传递给视图函数的。
例如,如果不带命名组,请
求 /articles/2006/03/ 将会等同于这样
的函数调用:
month_archive(request, '2006', '03')
而带命名组,同样的请求就会变成这
样的函数调用:
month_archive(request, year='2006',
month='03')
使用命名组可以让你的URLconfs更加
清晰,减少搞混参数次序的潜在
BUG,还可以让你在函数定义中对参
数重新排序。 接着上面这个例子,
如果我们想修改URL把月份放到 年份
的 前面 ,而不使用命名组的话,我
们就不得不去修改视
图 month_archive 的参数次序。 如果
我们使用命名组的话,修改URL里提
取参数的次序对视图没有影响。
当然,命名组的代价就是失去了简洁
性: 一些开发者觉得命名组的语法
丑陋和显得冗余。 命名组的另一个
好处就是可读性强。
理解匹配/分组算法
需要注意的是如果在URLconf中使用
命名组,那么命名组和非命名组是不
能同时存在于同一个URLconf的模式
中的。 如果你这样做,Django不会抛
出任何错误,但你可能会发现你的
URL并没有像你预想的那样匹配正
确。 具体地,以下是URLconf解释器
有关正则表达式中命名组和 非命名
组所遵循的算法 :
如果有任何命名的组,Django会
忽略非命名组而直接使用命名
组。
否则,Django会把所有非命名组
以位置参数的形式传递。
在以上的两种情况,Django同时
会以关键字参数的方式传递一些
额外参数。 更具体的信息可参考
下一节。
传递额外的参数到视图函
数中
有时你会发现你写的视图函数是十分
类似的,只有一点点的不同。 比如
说,你有两个视图,它们的内容是一
致的,除了它们所用的模板不太一
样:
#
urls.py
from django.conf.urls.defaults impor
t *
from mysite import views
urlpatterns = patterns('',
(r'^foo/$', views.foo_view),
(r'^bar/$', views.bar_view),
)
#
views.py
from django.shortcuts import render_
to_response
from mysite.models import MyModel
def foo_view(request):
m_list = MyModel.objects.filter(
is_new=True)
return render_to_response('templ
ate1.html', {'m_list': m_list})
def bar_view(request):
m_list = MyModel.objects.filter(
is_new=True)
return render_to_response('templ
ate2.html', {'m_list': m_list})
我们在这代码里面做了重复的工作,
不够简练。 起初你可能会想,通过
对两个URL都使用同样的视图,在
URL中使用括号捕捉请求,然后在视
图中检查并决定使用哪个模板来去除
代码的冗余,就像这样:
#
urls.py
from django.conf.urls.defaults impor
t *
from mysite import views
urlpatterns = patterns('',
(r'^(foo)/$', views.foobar_view)
(r'^(bar)/$', views.foobar_view)
,
,
)
#
views.py
from django.shortcuts import render_
to_response
from mysite.models import MyModel
def foobar_view(request, url):
m_list = MyModel.objects.filter(
is_new=True)
if url == 'foo':
template_name = 'template1.h
tml'
elif url == 'bar':
template_name = 'template2.h
tml'
return render_to_response(templa
te_name, {'m_list': m_list})
这种解决方案的问题还是老缺点,就
是把你的URL耦合进你的代码里面
了。 如果你打算把 /foo/ 改成 /fooey/
的话,那么你就得记住要去改变视图
里面的代码。
对一个可选URL配置参数的优雅解决
方法: URLconf里面的每一个模式都
可以包含第三个数据: 一个关键字
参数的字典:
有了这个概念以后,我们就可以把我
们现在的例子改写成这样:
#
urls.py
from django.conf.urls.defaults impor
t *
from mysite import views
urlpatterns = patterns('',
(r'^foo/$', views.foobar_view, {
'
template_name': 'template1.html'}),
(r'^bar/$', views.foobar_view, {
template_name': 'template2.html'}),
'
)
#
views.py
from django.shortcuts import render_
to_response
from mysite.models import MyModel
def foobar_view(request, template_na
me):
m_list = MyModel.objects.filter(
is_new=True)
return render_to_response(templa
te_name, {'m_list': m_list})
如你所见,这个例子中,URLconf指
定了 template_name 。 而视图函数会
把它当成另一个参数。
这种使用额外的URLconf参数的技术
以最小的代价给你提供了向视图函数
传递额外信息的一个好方法。 正因
如此,这技术已被很多Django的捆绑
应用使用,其中以我们将在第11章讨
论的通用视图系统最为明显。
下面的几节里面有一些关于你可以怎
样把额外URLconf参数技术应用到你
自己的工程的建议。
伪造捕捉到的URLconf值
比如说你有匹配某个模式的一堆视
图,以及一个并不匹配这个模式但视
图逻辑是一样的URL。 这种情况下,
你可以通过向同一个视图传递额外
URLconf参数来伪造URL值的捕捉。
例如,你可能有一个显示某一个特定
日子的某些数据的应用,URL类似这
样的:
/
/
/
#
mydata/jan/01/
mydata/jan/02/
mydata/jan/03/
...
/
/
mydata/dec/30/
mydata/dec/31/
这太简单了,你可以在一个URLconf
中捕捉这些值,像这样(使用命名组
的方法):
urlpatterns = patterns('',
(r'^mydata/(?P<month>\w{3})/(?P<
day>\d\d)/$', views.my_view),
)
然后视图函数的原型看起来会是:
def my_view(request, month, day):
#
....
这种解决方案很直接,没有用到什么
你没见过的技术。 当你想添加另外
一个使用 my_vi ew 视图但不包含
month和/或者day的URL时,问题就出
现了。
比如你可能会想增加这样一个
URL, /mydata/birthday/ , 这个URL
等价于 /mydata/jan/06/ 。这时你可以
这样利用额外URLconf参数:
urlpatterns = patterns('',
(r'^mydata/birthday/$', views.my
_
)
view, {'month': 'jan', 'day': '06'}
,
(r'^mydata/(?P<month>\w{3})/(?P<
day>\d\d)/$', views.my_view),
)
在这里最帅的地方莫过于你根本不用
改变你的视图函数。 视图函数只会
关心它 获得 了 参数,它不会去管这
些参数到底是捕捉回来的还是被额外
提供的。month和day
创建一个通用视图
抽取出我们代码中共性的东西是一个
很好的编程习惯。 比如,像以下的
两个Python函数:
def say_hello(person_name):
print 'Hello, %s' % person_name
def say_goodbye(person_name):
print 'Goodbye, %s' % person_name
我们可以把问候语提取出来变成一个
参数:
def greet(person_name, greeting):
print '%s, %s' % (greeting, pers
on_name)
通过使用额外的URLconf参数,你可
以把同样的思想应用到Django的视图
中。
了解这个以后,你可以开始创作高抽
象的视图。 更具体地说,比如这个
视图显示一系列的 Event 对象,那个
视图显示一系列的 BlogEntry 对象,
并意识到它们都是一个用来显示一系
列对象的视图的特例,而对象的类型
其实就是一个变量。
以这段代码作为例子:
#
urls.py
from django.conf.urls.defaults impor
t *
from mysite import views
urlpatterns = patterns('',
(r'^events/$', views.event_list)
(r'^blog/entries/$', views.entry
,
_
)
list),
#
views.py
from django.shortcuts import render_
to_response
from mysite.models import Event, Blo
gEntry
def event_list(request):
obj_list = Event.objects.all()
return render_to_response('mysit
e/event_list.html', {'event_list': o
bj_list})
def entry_list(request):
obj_list = BlogEntry.objects.all
()
return render_to_response('mysit
e/blogentry_list.html', {'entry_list
'
: obj_list})
这两个视图做的事情实质上是一样
的: 显示一系列的对象。 让我们把
它们显示的对象的类型抽象出来:
#
urls.py
from django.conf.urls.defaults impor
t *
from mysite import models, views
urlpatterns = patterns('',
(r'^events/$', views.object_list
,
{'model': models.Event}),
(r'^blog/entries/$', views.objec
t_list, {'model': models.BlogEntry})
,
)
#
views.py
from django.shortcuts import render_
to_response
def object_list(request, model):
obj_list = model.objects.all()
template_name = 'mysite/%s_list.
html' % model.__name__.lower()
return render_to_response(templa
te_name, {'object_list': obj_list})
就这样小小的改动,我们突然发现我
们有了一个可复用的,模型无关的视
图! 从现在开始,当我们需要一个
视图来显示一系列的对象时,我们可
以简简单单的重用这一
个 object_list 视图,而无须另外写视
图代码了。 以下是我们做过的事
情:
我们通过 model 参数直接传递了
模型类。 额外URLconf参数的字
典是可以传递任何类型的对象,
而不仅仅只是字符串。
这一行: model.objects.all() 是 鸭
子界定 (原文:
我们使用 model.name.lower() 来决
定模板的名字。 每个Python的类
都有一个 na me 属性返回类名。
这特性在当我们直到运行时刻才
知道对象类型的这种情况下很有
用。 比如, BlogEntry 类的
na me 就是字符串 'BlogEntry' 。
这个例子与前面的例子稍有不
同,我们传递了一个通用的变量
名给模板。 当然我们可以轻易的
把这个变量名改
成 blogentry_list 或者 event_list ,
不过我们打算把这当作练习留给
读者。
因为数据库驱动的网站都有一些通用
的模式,Django提供了一个通用视图
的集合,使用它可以节省你的时间。
我们将会在下一章讲讲Django的内置
通用视图。
提供视图配置选项
如果你发布一个Django的应用,你的
用户可能会希望配置上能有些自由
度。 这种情况下,为你认为用户可
能希望改变的配置选项添加一些钩子
到你的视图中会是一个很好的主意。
你可以用额外URLconf参数实现。
一个应用中比较常见的可供配置代码
是模板名字:
def my_view(request, template_name):
var = do_something()
return render_to_response(templa
te_name, {'var': var})
了解捕捉值和额外参数之
间的优先级 额外的选项
当冲突出现的时候,额外URLconf参
数优先于捕捉值。 也就是说,如果
URLconf捕捉到的一个命名组变量和
一个额外URLconf参数包含的变量同
名时,额外URLconf参数的值会被使
用。
例如,下面这个URLconf:
from django.conf.urls.defaults impor
t *
from mysite import views
urlpatterns = patterns('',
(r'^mydata/(?P<id>\d+)/$', views
.
)
my_view, {'id': 3}),
这里,正则表达式和额外字典都包含
了一个 id 。硬编码的(额外字典
的) id 将优先使用。 就是说任何请
求(比如, /mydata/2/ 或
者 /mydata/432432/ )都会作 id 设置
为 3 对待,不管URL里面能捕捉到什
么样的值。
聪明的读者会发现在这种情况下,在
正则表达式里面写上捕捉是浪费时间
的,因为 id 的值总是会被字典中的
值覆盖。 没错,我们说这个的目的
只是为了让你不要犯这样的错误。
使用缺省视图参数
另外一个方便的特性是你可以给一个
视图指定默认的参数。 这样,当没
有给这个参数赋值的时候将会使用默
认的值。
例子:
#
urls.py
from django.conf.urls.defaults impor
t *
from mysite import views
urlpatterns = patterns('',
(r'^blog/$', views.page),
(r'^blog/page(?P<num>\d+)/$', vi
ews.page),
)
#
views.py
def page(request, num='1'):
Output the appropriate page of
blog entries, according to num.
...
#
#
在这里,两个URL表达式都指向了同
一个视图 views.page ,但是第一个表
达式没有传递任何参数。 如果匹配
到了第一个样式, page() 函数将会对
参数 num 使用默认值 "1" ,如果第二
个表达式匹配成功, page() 函数将使
用正则表达式传递过来的num的值。
(
注:我们已经注意到设置默认参数
值是字符串 ‘1’ ,不是整数 1 。为
了保持一致,因为捕捉给 num 的值
总是字符串。
就像前面解释的一样,这种技术与配
置选项的联用是很普遍的。 以下这
个例子比提供视图配置选项一节中的
例子有些许的改进。
def my_view(request, template_name='
mysite/my_view.html'):
var = do_something()
return render_to_response(templa
te_name, {'var': var})
特殊情况下的视图
有时你有一个模式来处理在你的
URLconf中的一系列URL,但是有时
候需要特别处理其中的某个URL。 在
这种情况下,要使用将URLconf中把
特殊情况放在首位的线性处理方式
。
比方说,你可以考虑通过下面这个
URLpattern所描述的方式来向Django
的管理站点添加一个目标页面
urlpatterns = patterns('',
#
...
('^([^/]+)/([^/]+)/add/$', views
add_stage),
...
.
)
#
这将匹配
像 /myblog/entries/add/ 和 /auth/groups/
add/ 这样的URL。然而,对于用户对
象的添加页面(/auth/user/add/ )是个
特殊情况,因为它不会显示所有的表
单域,它显示两个密码域等等。 我
们 可以 在视图中特别指出以解决这
种情况:
def add_stage(request, app_label, mo
del_name):
if app_label == 'auth' and model
_
name == 'user':
#
else:
#
do special-case code
do normal code
不过,就如我们多次在这章提到的,
这样做并不优雅: 因为它把URL逻辑
放在了视图中。 更优雅的解决方法
是,我们要利用URLconf从顶向下的
解析顺序这个特点:
urlpatterns = patterns('',
#
...
('^auth/user/add/$', views.user_
add_stage),
('^([^/]+)/([^/]+)/add/$', views
add_stage),
...
.
)
#
在这种情况下,象 /auth/user/add/ 的
请求将会被 user_add_stage 视图处
理。 尽管URL也匹配第二种模式,它
会先匹配上面的模式。 (这是短路
逻辑。)
从URL中捕获文本
每个被捕获的参数将被作为纯Python
字符串来发送,而不管正则表达式中
的格式。 举个例子,在这行URLConf
中:
(r'^articles/(?P<year>\d{4})/$', vie
ws.year_archive),
尽管 \d{4} 将只匹配整数的字符串,
但是参数 year 是作为字符串传
至 views.year_archive() 的,而不是整
型。
当你在写视图代码时记住这点很重
要,许多Python内建的方法对于接受
的对象的类型很讲究。 许多内置
Python函数是挑剔的(这是理所当然
的)只接受特定类型的对象。 一个
典型的的错误就是用字符串值而不是
整数值来创建 datetime.date 对象:
>
>
>> import datetime
>> datetime.date('1993', '7', '9')
Traceback (most recent call last):
.
..
TypeError: an integer is required
>> datetime.date(1993, 7, 9)
>
datetime.date(1993, 7, 9)
回到URLconf和视图处,错误看起来
很可能是这样:
#
urls.py
from django.conf.urls.defaults impor
t *
from mysite import views
urlpatterns = patterns('',
(r'^articles/(\d{4})/(\d{2})/(\d
{
)
2})/$', views.day_archive),
#
views.py
import datetime
def day_archive(request, year, month
,
,
day):
#
The following statement raises
a TypeError!
date = datetime.date(year, month
day)
因此, day_archive() 应该这样写才是
正确的:
def day_archive(request, year, month
,
day):
date = datetime.date(int(year),
int(month), int(day))
注意,当你传递了一个并不完全包含
数字的字符串时, int() 会抛
出 ValueError 的异常,不过我们已经
避免了这个错误,因为在URLconf的
正则表达式中已经确保只有包含数字
的字符串才会传到这个视图函数中。
决定URLconf搜索的东西
当一个请求进来时,Django试着将请
求的URL作为一个普通Python字符串
进行URLconf模式匹配(而不是作为
一个Unicode字符串)。 这并不包
括 GET 或 POST 参数或域名。 它也
不包括第一个斜杠,因为每个URL必
定有一个斜杠。
例如,在
向 http://www.example.com/myapp/ 的
请求中,Django将试着去匹
配 myapp/ 。在向
http://www.example.com/myapp/?
page=3 的请求中,Django同样会去匹
配 myapp/ 。
在解析URLconf时,请求方法(例
如, POST , GET , HEAD )并 不
会 被考虑。 换而言之,对于相同的
URL的所有请求方法将被导向到相同
的函数中。 因此根据请求方法来处
理分支是视图函数的责任。
视图函数的高级概念
说到关于请求方法的分支,让我们来
看一下可以用什么好的方法来实现
它。 考虑这个 URLconf/view 设计:
#
urls.py
from django.conf.urls.defaults impor
t *
from mysite import views
urlpatterns = patterns('',
#
...
(r'^somepage/$', views.some_page
)
)
#
,
#
...
views.py
from django.http import Http404, Htt
pResponseRedirect
from django.shortcuts import render_
to_response
def some_page(request):
if request.method == 'POST':
do_something_for_post()
return HttpResponseRedirect(
'
/someurl/')
elif request.method == 'GET':
do_something_for_get()
return render_to_response('p
age.html')
else:
raise Http404()
在这个示例中, some_page() 视图
函数对 POST 和 GET 这两种请求方
法的处理完全不同。 它们唯一的共
同点是共享一个URL地址:
/
somepage/. 正如大家所看到的,在
同一个视图函数中对 POST 和 GET
进行处理是一种很初级也很粗糙的做
法。 一个比较好的设计习惯应该
是,用两个分开的视图函数——一个
处理 POST 请求,另一个处理 GET
请求,然后在相应的地方分别进行调
用。
我们可以像这样做:先写一个视图函
数然后由它来具体分派其它的视图,
在之前或之后可以执行一些我们自定
的程序逻辑。 下边的示例展示了这
个技术是如何帮我们改进前边那个简
单的 some_page() 视图的:
#
views.py
from django.http import Http404, Htt
pResponseRedirect
from django.shortcuts import render_
to_response
def method_splitter(request, GET=Non
e, POST=None):
if request.method == 'GET' and G
ET is not None:
return GET(request)
elif request.method == 'POST' an
d POST is not None:
return POST(request)
raise Http404
def some_page_get(request):
assert request.method == 'GET'
do_something_for_get()
return render_to_response('page.
html')
def some_page_post(request):
assert request.method == 'POST'
do_something_for_post()
return HttpResponseRedirect('/so
meurl/')
#
urls.py
from django.conf.urls.defaults impor
t *
from mysite import views
urlpatterns = patterns('',
#
...
(r'^somepage/$', views.method_sp
litter, {'GET': views.some_page_get,
'
POST': views.some_page_post}),
...
#
)
让我们从头看一下代码是如何工作
的:
我们写了一个新的视图,
method_splitter() ,它根据
request.method 返回的值来调用
相应的视图。可以看到它带有两
个关键参数, GET 和 POST ,也
许应该是 视图函数 。如果
request.method 返回 GET ,那
它就会自动调用 GET 视图。 如果
request.method 返回的是 POST
,那它调用的就是 POST 视图。
如果 request.method 返回的是其
它值(如: HEAD ),或者是没有
把 GET 或 POST 提交给此函数,
那它就会抛出一个 Http404 错
误。
在URLconf中,我们把
/
somepage/ 指到
method_splitter() 函数,并把
视图函数额外需要用到的 GET 和
POST 参数传递给它。
最终,我们把 some_page() 视图
分解到两个视图函数中
some_page_get() 和
some_page_post() 。这比把所有
逻辑都挤到一个单一视图的做法
要优雅得多。
注意,在技术上这些视图函数就
不用再去检查 request.method
了,因为 method_splitter() 已
经替它们做了。 (比如,
some_page_post() 被调用的时
候,我们可以确信
request.method 返回的值是
post 。)当然,这样做不止更安
全也能更好的将代码文档化,这
里我们做了一个假定,就是
request.method 能象我们所期望
的那样工作。
现在我们就拥有了一个不错的,可以
通用的视图函数了,里边封装着由
request.method 的返回值来分派不
同的视图的程序。关于
method_splitter() 就不说什么
了,当然,我们可以把它们重用到其
它项目中。
然而,当我们做到这一步时,我们仍
然可以改进 method_splitter 。从
代码我们可以看到,它假设 Get 和
POST 视图除了 request 之外不需
要任何其他的参数。那么,假如我们
想要使用 method_splitter 与那种
会从URL里捕捉字符,或者会接收一
些可选参数的视图一起工作时该怎么
办呢?
为了实现这个,我们可以使用Python
中一个优雅的特性 带星号的可变参
数 我们先展示这些例子,接着再进
行解释
def method_splitter(request, *args,
*
*kwargs):
get_view = kwargs.pop('GET', Non
e)
post_view = kwargs.pop('POST', N
if request.method == 'GET' and g
one)
et_view is not None:
return get_view(request, *ar
gs, **kwargs)
elif request.method == 'POST' an
d post_view is not None:
return post_view(request, *a
rgs, **kwargs)
raise Http404
这里,我们重构method_splitter(),去掉
了GET和POST两个关键字参数,改而
支持使用*args和和*kwargs(注意号)
这是一个Python特性,允许函数接受
动态的、可变数量的、参数名只在运
行时可知的参数。 如果你在函数定
义时,只在参数前面加一个号,所有传
递给函数的参数将会保存为一个元
组. 如果你在函数定义时,在参数前面
加两个号,所有传递给函数的关键字
参数,将会保存为一个字典
例如,对于这个函数
def foo(*args, **kwargs):
print "Positional arguments are:
"
print args
print "Keyword arguments are:"
print kwargs
看一下它是怎么工作的
>
>> foo(1, 2, 3)
Positional arguments are:
(1, 2, 3)
Keyword arguments are:
{
>
}
>> foo(1, 2, name='Adrian', framewo
rk='Django')
Positional arguments are:
(1, 2)
Keyword arguments are:
{
'framework': 'Django', 'name': 'Adr
ian'}
回过头来看,你能发现我们用
method_splitter()和*args接受**kw ar gs
函数参数并把它们传递到正确的视
图。any 但是在我们这样做之前,我
们要调用两次获得参数
kwargs.pop()GETPOST,如果它们合
法的话。 (我们通过指定pop的缺省值
为None,来避免由于一个或者多个关
键字缺失带来的KeyError)
包装视图函数
我们最终的视图技巧利用了一个高级
python技术。 假设你发现自己在各个
不同视图里重复了大量代码,就像
这个例子:
def my_view1(request):
if not request.user.is_authentic
ated():
return HttpResponseRedirect(
'
/accounts/login/')
...
#
return render_to_response('templ
ate1.html')
def my_view2(request):
if not request.user.is_authentic
ated():
return HttpResponseRedirect(
'
/accounts/login/')
...
return render_to_response('templ
#
ate2.html')
def my_view3(request):
if not request.user.is_authentic
ated():
return HttpResponseRedirect(
'
/accounts/login/')
...
return render_to_response('templ
#
ate3.html')
这里,每一个视图开始都检查
request.user是否是已经认证的,是的
话,当前用户已经成功登陆站点否则
就重定向/accounts/login/ (注意,虽然我
们还没有讲到request.user,但是14章将
要讲到它.就如你所想像
的,request.user描述当前用户是登陆的
还是匿名 )
如果我们能够丛每个视图里移除那些
重复代,并且只在需要认证的时候指
明它们,那就完美了。 我们能够通
过使用一个视图包装达到目的。 花
点时间来看看这个:
def requires_login(view):
def new_view(request, *args, **k
wargs):
if not request.user.is_authe
nticated():
return HttpResponseRedir
ect('/accounts/login/')
return view(request, *args,
*
*kwargs)
return new_view
函数requires_login,传入一个视图函数
view,然后返回一个新的视图函数
new_view.这个新的视图函数
new_view在函数requires_login内定义
处理request.user.is_authenticated()这个
验证,从而决定是否执行原来的view
函数
现在,我们可以从views中去掉if not
request.user.is_authenticated()验证.我
们可以在URLconf中很容易的用
requires_login来包装实现.
from django.conf.urls.defaults impor
t *
from mysite.views import requires_lo
gin, my_view1, my_view2, my_view3
urlpatterns = patterns('',
(r'^view1/$', requires_login(my_
view1)),
(r'^view2/$', requires_login(my_
view2)),
(r'^view3/$', requires_login(my_
view3)),
)
优化后的代码和前面的功能一样,但
是减少了代码冗余 现在我们建立了
一个漂亮,通用的函数requires_login()
来帮助我们修饰所有需要它来验证的
视图
包含其他URLconf
如果你试图让你的代码用在多个基于
Django的站点上,你应该考虑将你的
URLconf以包含的方式来处理。
在任何时候,你的URLconf都可以包
含其他URLconf模块。 对于根目录是
基于一系列URL的站点来说,这是必
要的。 例如下面的,URLconf包含了
其他URLConf :
from django.conf.urls.defaults impor
t *
urlpatterns = patterns('',
(r'^weblog/', include('mysite.bl
og.urls')),
(r'^photos/', include('mysite.ph
otos.urls')),
(r'^about/$', 'mysite.views.abou
t'),
)
在前面第6章介绍Django的admi n模块
时我们曾经见过include. admi n模块有
他自己的URLconf,你仅仅只需要在你
自己的代码中加入include就可以了.
这里有个很重要的地方: 例子中的
指向 include() 的正则表达式并 不 包
含一个 $ (字符串结尾匹配符),但
是包含了一个斜杆。 每当Django遇
到 include() 时,它将截断匹配的
URL,并把剩余的字符串发往包含的
URLconf作进一步处理。
继续看这个例子,这里就是被包含的
URLconf mysite.blog.urls :
from django.conf.urls.defaults impor
t *
urlpatterns = patterns('',
(r'^(\d\d\d\d)/$', 'mysite.blog.
views.year_detail'),
(r'^(\d\d\d\d)/(\d\d)/$', 'mysit
e.blog.views.month_detail'),
)
通过这两个URLconf,下面是一些处
理请求的例子:
/
weblog/2007/ :在第一个URLconf
中,模式 r'^weblog/' 被匹配。 因
为它是一个 include() ,Django将
截掉所有匹配的文本,在这里
是 'weblog/' 。URL剩余的部分
是 2007/ , 将
在 mysite.blog.urls 这个URLconf的
第一行中被匹配到。 URL仍存在
的部分为 2007/ ,与第一行
的 mysite.blog.urlsURL设置相匹
配。
/
weblog//2007/(包含两个斜杠) 在
第一个URLconf中,r ’^weblog/’匹配
因为它有一个include(),django去掉
了匹配的部,在这个例子中匹配的
部分是’weblog/’ 剩下的部分
是/2007/ (最前面有一个斜杠),不
匹配mysite.blog.urls中的任何一行.
about/ : 这个匹配第一个URLconf
/
中的 mysite.views.about 视图。
捕获的参数如何和include()
协同工作
一个被包含的URLconf接收任何来自
parent URLconfs的被捕获的参数,比
如:
#
root urls.py
from django.conf.urls.defaults impor
t *
urlpatterns = patterns('',
(r'^(?P<username>\w+)/blog/', in
clude('foo.urls.blog')),
)
#
foo/urls/blog.py
from django.conf.urls.defaults impor
t *
urlpatterns = patterns('',
(r'^$', 'foo.views.blog_index'),
(r'^archive/$', 'foo.views.blog_
archive'),
)
)
),
在这个例子中,被捕获
的 user name 变量将传递给被包含的
URLconf,进而传递给那个URLconf中
的 每一个 视图函数。
注意,这个被捕获的参数 总是 传递
到被包含的URLconf中的 每一 行,不
管那些行对应的视图是否需要这些参
数。 因此,这个技术只有在你确实
需要那个被传递的参数的时候才显得
有用。
额外的URLconf如何和
include()协同工作
相似的,你可以传递额外的URLconf
选项到 include() , 就像你可以通过字
典传递额外的URLconf选项到普通的
视图。 当你这样做的时候,被包含
URLconf的 每一 行都会收到那些额外
的参数。
比如,下面的两个URLconf在功能上
是相等的。
第一个:
#
urls.py
from django.conf.urls.defaults impor
t *
urlpatterns = patterns('',
(r'^blog/', include('inner'), {'
blogid': 3}),
)
#
inner.py
from django.conf.urls.defaults impor
t *
urlpatterns = patterns('',
(r'^archive/$', 'mysite.views.ar
chive'),
(r'^about/$', 'mysite.views.abou
t'),
(r'^rss/$', 'mysite.views.rss'),
)
第二个
#
urls.py
from django.conf.urls.defaults impor
t *
urlpatterns = patterns('',
(r'^blog/', include('inner')),
)
#
inner.py
from django.conf.urls.defaults impor
t *
urlpatterns = patterns('',
(r'^archive/$', 'mysite.views.ar
chive', {'blogid': 3}),
(r'^about/$', 'mysite.views.abou
t', {'blogid': 3}),
(r'^rss/$', 'mysite.views.rss',
{
)
'blogid': 3}),
这个例子和前面关于被捕获的参数一
样(在上一节就解释过这一点),额
外的选项将 总是 被传递到被包含的
URLconf中的 每一 行,不管那一行对
应的视图是否确实作为有效参数接收
这些选项,因此,这个技术只有在你
确实需要那个被传递的额外参数的时
候才显得有用。 因为这个原因,这
种技术仅当你确信在涉及到的接受到
额外你给出的选项的每个URLconf时
有用的才奏效。
下一章
这一章提供了很多高级视图和
URLconfs的小提示和技巧。 接下
来,在Chapter 9,我们将会将这个先进
的处理方案带给djangos模板系统。
虽然大多数和Django模板语言的交互
都是模板作者的工作,但你可能想定
制和扩展模板引擎,让它做一些它不
能做的事情,或者是以其他方式让你
的工作更轻松。
本章深入探讨Django的模板系统。 如
果你想扩展模板系统或者只是对它的
工作原理感觉到好奇,本章涉及了你
需要了解的东西。 它也包含一个自
动转意特征,如果你继续使用
django,随着时间的推移你一定会注
意这个安全考虑。
如果你想把Django的模版系统作为另
外一个应用程序的一部分(就是说,
仅使用Django的模板系统而不使用
Django框架的其他部分),那你一定
要读一下“配置独立模式下的模版系
统”这一节。
模板语言回顾
首先,让我们快速回顾一下第四章介
绍的若干专业术语:
模板 是一个纯文本文件,或是一
个用Django模板语言标记过的普通
的Python字符串。 模板可以包含模
板标签和变量。
模板标签 是在一个模板里面起作
用的的标记。 这个定义故意搞得
模糊不清。 例如,一个模版标签
能够产生作为控制结构的内容
(
一个 if语句或for 循环), 可以获
取数据库内容,或者访问其他的
模板标签。
区块标签被 {% 和 %} 包围:
{
{
{
% if is_logged_in %}
Thanks for logging in!
% else %}
Please log in.
% endif %}
变量 是一个在模板里用来输出值
的标记。
变量标签被 {{ 和 }} 包围:
My first name is {{ first_name }}. M
y last name is {{ last_name }}.
context 是一个传递给模板的名称
到值的映射(类似Python字典)。
模板 渲染 就是是通过从context获
取值来替换模板中变量并执行所
有的模板标签。
关于这些基本概念更详细的内容,请
参考第四章。
本章的其余部分讨论了扩展模板引擎
的方法。 首先,我们快速的看一下
第四章遗留的内容。
RequestContext和
Context处理器
你需要一段context来解析模板。 一般
情况下,这是一
个 django.template.Context 的实例,不
过在Django中还可以用一个特殊的子
类, django.template.RequestContext ,
这个用起来稍微有些不
同。 RequestContext默认地在模板
context中加入了一些变量,
如 HttpRequest 对象或当前登录用户
的相关信息。
当你不想在一系例模板中都明确指定
一些相同的变量时,你应该使
用 RequestContext 。 例如,考虑这两
个视图:
from django.template import loader,
Context
def view_1(request):
#
...
t = loader.get_template('templat
e1.html')
c = Context({
'
'
'
app': 'My app',
user': request.user,
ip_address': request.META['
REMOTE_ADDR'],
message': 'I am view 1.'
'
}
)
return t.render(c)
def view_2(request):
#
...
t = loader.get_template('templat
e2.html')
c = Context({
app': 'My app',
'
'
'
user': request.user,
ip_address': request.META['
REMOTE_ADDR'],
'
message': 'I am the second
view.'
}
)
return t.render(c)
(
注意,在这些例子中,我们故
意 不 使用 render_to_response() 这个
快捷方法,而选择手动载入模板,手
动构造context对象然后渲染模板。 是
为了能够清晰的说明所有步骤。)
每个视图都给模板传入了三个相同的
变量:app、user和ip_address。 如果
我们把这些冗余去掉会不会更好?
创建 RequestContext 和 context处理
器 就是为了解决这个问题。 Context
处理器允许你设置一些变量,它们会
在每个context中自动被设置好,而不
必每次调用 render_to_response() 时都
指定。 要点就是,当你渲染模板
时,你要用 RequestContext 而不
是 Context 。
最直接的做法是用context处理器来创
建一些处理器并传递
给 RequestContext 。上面的例子可以
用context processors改写如下:
from django.template import loader,
RequestContext
def custom_proc(request):
"
A context processor that provid
es 'app', 'user' and 'ip_address'."
return {
'
'
'
app': 'My app',
user': request.user,
ip_address': request.META['
REMOTE_ADDR']
}
def view_1(request):
#
...
t = loader.get_template('templat
e1.html')
c = RequestContext(request, {'me
ssage': 'I am view 1.'},
processors=[custom_proc]
)
return t.render(c)
def view_2(request):
#
...
t = loader.get_template('templat
e2.html')
c = RequestContext(request, {'me
ssage': 'I am the second view.'},
processors=[custom_proc]
)
return t.render(c)
我们来通读一下代码:
首先,我们定义一个函
数 custom_proc 。这是一个context
处理器,它接收一
个 HttpRequest 对象,然后返回一
个字典,这个字典中包含了可以
在模板context中使用的变量。 它
就做了这么多。
我们在这两个视图函数中
用 RequestContext 代替
了 Context 。在context对象的构建
上有两个不同点。
一, RequestContext 的第一个参数
需要传递一个 HttpRequest 对象,
就是传递给视图函数的第一个参
数( request )。
二, RequestContext 有一个可选的
参数 processors ,这是一个包含
context处理器函数的列表或者元
组。 在这里,我们传递了我们之
前定义的处理器函
数 curstom_proc 。
每个视图的context结构里不再包
含 app 、 user 、 ip_address 等变
量,因为这些由 custom_proc 函数
提供了。
每个视图 仍然 具有很大的灵活
性,可以引入我们需要的任何模
板变量。 在这个例子
中, message 模板变量在每个视
图中都不一样。
在第四章,我们介绍
了 render_to_response() 这个快捷方
式,它可以简化调
用 loader.get_template() ,然后创建一
个 Context 对象,最后再调用模板对
象的 render()过程。 为了讲解context
处理器底层是如何工作的,在上面的
例子中我们没有使
用 render_to_response() 。但是建议选
择 render_to_response() 作为context的
处理器。这就需要用到
context_instance参数:
from django.shortcuts import render_
to_response
from django.template import RequestC
ontext
def custom_proc(request):
"
A context processor that provid
es 'app', 'user' and 'ip_address'."
return {
'
'
'
app': 'My app',
user': request.user,
ip_address': request.META['
REMOTE_ADDR']
}
def view_1(request):
#
...
return render_to_response('templ
ate1.html',
'message': 'I am view 1.'},
{
context_instance=RequestCont
ext(request, processors=[custom_proc
]
))
def view_2(request):
...
return render_to_response('templ
ate2.html',
#
{
'message': 'I am the second
view.'},
context_instance=RequestCont
ext(request, processors=[custom_proc
]
))
在这,我们将每个视图的模板渲染代
码写成了一个单行。
虽然这是一种改进,但是,请考虑一
下这段代码的简洁性,我们现在不得
不承认的是在 另外 一方面有些过分
了。 我们以代码冗余
(
在 processors 调用中)的代价消除
了数据上的冗余(我们的模板变
量)。 由于你不得不一直键
入 processors ,所以使用context处理
器并没有减少太多的输入量。
Django因此提供对 全局 context处理器
的支
持。 TEMPLATE_CONTEXT_PROCE
SSORS 指定了哪些context processors_
总是_默认被使用。这样就省去了每
次使用 RequestContext 都指
定 processors 的麻烦。
默认情况
下, TEMPLATE_CONTEXT_PROCE
SSORS 设置如下:
TEMPLATE_CONTEXT_PROCESSORS = (
'
django.core.context_processors.
auth',
'
django.core.context_processors.
debug',
'
django.core.context_processors.
i18n',
'
media',
)
django.core.context_processors.
这个设置项是一个可调用函数的元
组,其中的每个函数使用了和上文中
我们的 custom_proc 相同的接口,它
们以request对象作为参数,返回一个
会被合并传给context的字典: 接收一
个request对象作为参数,返回一个包
含了将被合并到context中的项的字
典。
每个处理器将会按照顺序应用。 也
就是说如果你在第一个处理器里面向
context添加了一个变量,而第二个处
理器添加了同样名字的变量,那么第
二个将会覆盖第一个。
Django提供了几个简单的context处理
器,有些在默认情况下被启用的。
django.core.context_process
ors.auth
如
果 TEMPLATE_CONTEXT_PROCESS
ORS 包含了这个处理器,那么每
个 RequestContext 将包含这些变量:
user :一
个 django.contrib.auth.models.User
实例,描述了当前登录用户(或
者一个 AnonymousUser 实例,如
果客户端没有登录)。
messages :一个当前登录用户的
消息列表(字符串)。 在后台,
对每一个请求,这个变量都调用
request.user.get_and_delete_message
s() 方法。 这个方法收集用户的消
息然后把它们从数据库中删除。
perms : django.core.context_proces
sors.PermWrapper 的一个实例,包
含了当前登录用户有哪些权限。
关于users、permissions和messages的
更多内容请参考第14章。
django.core.context_process
ors.debug
这个处理器把调试信息发送到模板
层。 如果
TEMPLATE_CONTEXT_PROCESSOR
S包含这个处理器,每一个
RequestContext将包含这些变量:
debug :你设置的 DEBUG 的值
(
Tr ue 或 False )。你可以在模
板里面用这个变量测试是否处在
debug模式下。
sql_queries :包含类似于 ``{‘sql’:
…, ‘time’: `` 的字典的一个列表,
记录了这个请求期间的每个SQL
查询以及查询所耗费的时间。 这
个列表是按照请求顺序进行排列
的。
由于调试信息比较敏感,所以这个
context处理器只有当同时满足下面两
个条件的时候才有效:
DEBUG 参数设置为 Tr ue 。
请求的ip应该包含
在 INTERNAL_IPS 的设置里面。
细心的读者可能会注意到debug模板
变量的值永远不可能为False,因为如
果DEBUG是False,那么debug模板变
量一开始就不会被RequestContext所包
含。
django.core.context_process
ors.i18n
如果这个处理器启用,每
个 RequestContext 将包含下面的变
量:
LANGUAGES : LANGUAGES 选
项的值。
LANGUAGE_CODE :如
果 request.LANGUAGE_CODE 存
在,就等于它;否则,等同
于 LANGUAGE_CODE 设置。
附录E提供了有关这两个设置的更多
的信息。
django.core.context_process
ors.request
如果启用这个处理器,每
个 RequestContext 将包含变
量 request , 也就是当前
的 HttpRequest 对象。 注意这个处理
器默认是不启用的,你需要激活它。
如果你发现你的模板需要访问当前的
HttpRequest你就需要使用它:
{
{ request.REMOTE_ADDR }}
写Context处理器的一些建
议
编写处理器的一些建议:
使每个context处理器完成尽可能
小的功能。 使用多个处理器是很
容易的,所以你可以根据逻辑块
来分解功能以便将来复用。
要注
意 TEMPLATE_CONTEXT_PROC
ESSORS 里的context processor 将
会在基于这个settings.py的每个 模
板中有效,所以变量的命名不要
和模板的变量冲突。 变量名是大
小写敏感的,所以processor的变
量全用大写是个不错的主意。
不论它们存放在哪个物理路径
下,只要在你的Python搜索路径
中,你就可以在
TEMPLATE_CONTEXT_PROCESS
ORS 设置里指向它们。 建议你把
它们放在应用或者工程目录下名
为context_processors.py 的文件
里。
html自动转义
从模板生成html的时候,总是有一个
风险——变量包了含会影响结果html
的字符。 例如,考虑这个模板片
段:
Hello, {{ name }}.
一开始,这看起来是显示用户名的一
个无害的途径,但是考虑如果用户输
入如下的名字将会发生什么:
<
script>alert('hello')</script>
用这个用户名,模板将被渲染成:
Hello, <script>alert('hello')</scrip
t>
这意味着浏览器将弹出JavaScript警
告框!
类似的,如果用户名包含小于符号,
就像这样:
<
b>username
那样的话模板结果被翻译成这样:
Hello, <b>username
页面的剩余部分变成了粗体!
显然,用户提交的数据不应该被盲目
信任,直接插入到你的页面中。因为
一个潜在的恶意的用户能够利用这类
漏洞做坏事。 这类漏洞称为被跨域
脚本 (XSS) 攻击。 关于安全的更多
内容,请看20章
为了避免这个问题,你有两个选择:
一是你可以确保每一个不被信任
的变量都被escape过滤器处理一
遍,把潜在有害的html字符转换为
无害的。 这是最初几年Django的
默认方案,但是这样做的问题是
它把责任推给你(开发者、模版
作者)自己,来确保把所有东西
转意。 很容易就忘记转意数据。
二是,你可以利用Django的自动
html转意。 这一章的剩余部分描
述自动转意是如何工作的。
在django里默认情况下,每一个模板
自动转意每一个变量标签的输出。
尤其是这五个字符。
<
自动转换为 < ;
自动转换为 > ;
'
(单引号) 自动转换为 ' ;
(双引号) 自动转换为 " ;
"
&
自动转换为 & ;
另外,我强调一下这个行为默认是开
启的。 如果你正在使用django的模板
系统,那么你是被保护的。
如何关闭它
如果你不想数据被自动转意,在每一
站点级别、每一模板级别或者每一变
量级别你都有几种方法来关闭它。
为什么要关闭它? 因为有时候模板
变量包含了一些原始html数据,在这
种情况下我们不想它们的内容被转
意。 例如,你可能在数据库里存储
了一段被信任的html代码,并且你想
直接把它嵌入到你的模板里。 或
者,你可能正在使用Django的模板系
统生成非html文本,比如一封e-
mail。
对于单独的变量
用safe过滤器为单独的变量关闭自动
转意:
This will be escaped: {{ data }}
This will not be escaped: {{ data|sa
fe }}
你可以把_safe_当做_safe from further
escaping_的简写,或者当做可以被直
接译成HTML的内容。在这个例子
里,如果数据包含'',那么输出会变
成:
This will be escaped: <b>
This will not be escaped:
对于模板块
为了控制模板的自动转意,用标签
autoescape来包装整个模板(或者模板
中常用的部分),就像这样:
{
{
% autoescape off %}
Hello {{ name }}
% endautoescape %}
autoescape 标签有两个参数on和off 有
时,你可能想阻止一部分自动转意,对
另一部分自动转意。 这是一个模板
的例子:
Auto-escaping is on by default. Hell
o {{ name }}
{
{
% autoescape off %}
This will not be auto-escaped: {
data }}.
Nor this: {{ other_data }}
{
% autoescape on %}
Auto-escaping applies again:
{
{ name }}
% endautoescape %}
% endautoescape %}
{
{
auto-escaping 标签的作用域不仅可以
影响到当前模板还可以通过include标
签作用到其他标签,就像block标签一
样。 例如:
#
base.html
{
<
% autoescape off %}
h1>{% block title %}{% endblock %}<
/
{
{
{
h1>
% block content %}
% endblock %}
% endautoescape %}
#
child.html
{
{
% extends "base.html" %}
% block title %}This & that{% endbl
ock %}
% block content %}{{ greeting }}{%
{
endblock %}
由于在base模板中自动转意被关闭,所
以在child模板中自动转意也会关闭.
因此,在下面一段HTML被提交时,变
量greeting的值就为字符串Hello!
<
<
h1>This & that</h1>
b>Hello!</b>
备注
通常,模板作者没必要为自动转意担
心. 基于Pyhton的开发者(编写VIEWS
视图和自定义过滤器)只需要考虑哪
些数据不需要被转意,适时的标记数
据,就可以让它们在模板中工作。
如果你正在编写一个模板而不知道是
否要关闭自动转意,那就为所有需要
转意的变量添加一个escape过滤器。
当自动转意开启时,使用escape过滤
器似乎会两次转意数据,但其实没有
任何危险。因为escape过滤器不作用
于被转意过的变量。
过滤器参数里的字符串常
量的自动转义
就像我们前面提到的,过滤器也可以
是字符串 .
{
{ data|default:"This is a string li
teral." }}
所有字符常量没有经过转义就被插入
模板,就如同它们都经过了safe过滤。
这是由于字符常量完全由模板作者决
定,因此编写模板的时候他们会确保
文本的正确性。
这意味着你必须这样写
{
{ data|default:"3 < 2" }}
而不是这样
{
{ data|default:"3 < 2" }} <-- Bad!
Don't do this.
这点对来自变量本身的数据不起作
用。 如果必要,变量内容会自动转义,
因为它们不在模板作者的控制下。
模板加载的内幕
一般说来,你会把模板以文件的方式
存储在文件系统中,但是你也可以使
用自定义的 template loaders 从其他
来源加载模板。
Django有两种方法加载模板
django.template.loader.get_template(
template_name) : get_template 根
据给定的模板名称返回一个已编
译的模板(一个 Templ ate 对
象)。 如果模板不存在,就触
发 Templ ateDoesNotExi st 的异常。
django.template.loader.select_templa
te(template_name_list) : select_tem
plate 很像get_template ,不过它是
以模板名称的列表作为参数的。
它会返回列表中存在的第一个模
板。 如果模板都不存在,将会触
发TemplateDoesNotExist异常。
正如在第四章中所提到的,默认情况
下这些函数使用 TEMPLATE_DIRS 的
设置来载入模板。 但是,在内部这
些函数可以指定一个模板加载器来完
成这些繁重的任务。
一些加载器默认被禁用,但是你可以
通过编辑 TEMPLATE_LOADERS 设
置来激活它
们。 TEMPLATE_LOADERS 应当是
一个字符串的元组,其中每个字符串
都表示一个模板加载器。 这些模板
加载器随Django一起发布。
django.template.loaders.filesystem.lo
ad_template_source : 这个加载器根
据 TEMPLATE_DIRS 的设置从文
件系统加载模板。它默认是可用
的。
django.template.loaders.app_director
ies.load_template_source : 这个加
载器从文件系统上的Django应用中
加载模板。
对 INSTALLED_APPS 中的每个应
用,这个加载器会查找
templates 子目录。 如果这个目录
存在,Django就在那里寻找模板。
这意味着你可以把模板和你的应
用一起保存,从而使得Django应用
更容易和默认模板一起发布。 例
如,如果 INSTALLED_APPS 包
含 ('myproject.polls','myproject.musi
c') ,那
么 get_template('foo.html') 会按这个
顺序查找模板:
/
path/to/myproject/polls/template
s/foo.html
path/to/myproject/music/template
s/foo.html
/
请注意加载器在首次被导入的时
候会执行一个优化: 它会缓存一
个列表,这个列表包含
了 INSTALLED_APPS中带
有 templates 子目录的包。
这个加载器默认启用。
django.template.loaders.eggs.load_te
mplate_source : 这个加载器类
似 app_directories ,只不过它从
Python eggs而不是文件系统中加载
模板。 这个加载器默认被禁用;
如果你使用eggs来发布你的应用,
那么你就需要启用它。 Python eggs
可以将Python代码压缩到一个文件
中。
Django按
照 TEMPLATE_LOADERS 设置中的
顺序使用模板加载器。 它逐个使用
每个加载器直至找到一个匹配的模
板。
扩展模板系统
既然你已经对模板系统的内幕多了一
些了解,让我们来看看如何使用自定
义的代码来扩展这个系统吧。
绝大部分的模板定制是以自定义标
签/过滤器的方式来完成的。 尽管
Django模板语言自带了许多内建标签
和过滤器,但是你可能还是需要组建
你自己的标签和过滤器库来满足你的
需要。 幸运的是,定义你自己的功
能非常容易。
创建一个模板库
不管是写自定义标签还是过滤器,第
一件要做的事是创建模板库(Django
能够导入的基本结构)。
创建一个模板库分两步走:
第一,决定模板库应该放在哪个
Django应用下。 如果你通
过 manage.py startapp 创建了一个
应用,你可以把它放在那里,或
者你可以为模板库单独创建一个
应用。 我们更推荐使用后者,因
为你的filter可能在后来的工程中有
用。
无论你采用何种方式,请确保把
你的应用添加
到 INSTALLED_APPS 中。 我们稍
后会解释这一点。
第二,在适当的Django应用包里创
建一个 templatetags 目录。 这个目
录应当和 models.py 、 views.py 等
处于同一层次。 例如:
books/
_
_init__.py
models.py
templatetags/
views.py
在 templatetags 中创建两个空文
件: 一个 init.py (告诉Python这是
一个包含了Python代码的包)和一
个用来存放你自定义的标签/过滤
器定义的文件。 第二个文件的名
字稍后将用来加载标签。 例如,
如果你的自定义标签/过滤器在一
个叫作 poll_extras.py 的文件中,
你需要在模板中写入如下内容:
{
% load poll_extras %}
{
% load %} 标签检
查 INSTALLED_APPS 中的设置,
仅允许加载已安装的Django应用程
序中的模板库。 这是一个安全特
性;它可以让你在一台电脑上部
署很多的模板库的代码,而又不
用把它们暴露给每一个Django安
装。
如果你写了一个不和任何特定模型 /
视图关联的模板库,那么得到一个仅
包含 templatetags 包的Django应用程
序包是完全正常的。 对于
在 templatetags 包中放置多少个模块
没有做任何的限制。 需要了解的
是:{%load%}语句是通过指定的
Python模块名而不是应用名来加载标
签/过滤器的。
一旦创建了Python模块,你只需根据
是要编写过滤器还是标签来相应的编
写一些Python代码。
作为合法的标签库,模块需要包含一
个名为register的模块级变量。这个变
量是template.Library的实例,是所有
注册标签和过滤器的数据结构。 所
以,请在你的模块的顶部插入如下语
句:
from django import template
register = template.Library()
注意
请阅读Django默认的过滤器和标签的
源码,那里有大量的例子。 他们分
别为:
django/template/defaultfilters.py 和
django/template/defaulttags.py 。
django.contrib中的某些应用程序也包
含模板库。
创建 register 变量后,你就可以使用
它来创建模板的过滤器和标签了。
自定义模板过滤器
自定义过滤器就是有一个或两个参数
的Python函数:
(输入)变量的值
参数的值, 可以是默认值或者完
全留空
例如,在过滤器 {{ var|foo:"bar" }} 中
,
过滤器 foo 会被传入变量 var 和默
认参数 bar 。
过滤器函数应该总有返回值。 而且
不能触发异常,它们都应该静静地失
败。 如果出现错误,应该返回一个
原始输入或者空字符串,这会更有意
义。
这里是一些定义过滤器的例子:
def cut(value, arg):
"
Removes all values of arg from
the given string"
return value.replace(arg, '')
下面是一个可以用来去掉变量值空格
的过滤器例子:
{
{ somevariable|cut:" " }}
大多数过滤器并不需要参数。 下面
的例子把参数从你的函数中拿掉了:
def lower(value): # Only one argumen
t.
"
Converts a string into all lowe
rcase"
return value.lower()
当你定义完过滤器后,你需要
用 Library 实例来注册它,这样就能
通过Django的模板语言来使用了:
register.filter('cut', cut)
register.filter('lower', lower)
Library.filter() 方法需要两个参数:
过滤器的名称(一个字串)
过滤器函数本身
如果你使用的是Python 2.4或者更新的
版本,你可以使用装饰器
register.filter():
@
register.filter(name='cut')
def cut(value, arg):
return value.replace(arg, '')
@
register.filter
def lower(value):
return value.lower()
如果你想第二个例子那样不使
用 na me 参数,那么Django会把函数
名当作过滤器的名字。
下面是一个完整的模板库的例子,它
包含一个 cut 过滤器:
from django import template
register = template.Library()
@
register.filter(name='cut')
def cut(value, arg):
return value.replace(arg, '')
自定义模板标签
标签要比过滤器复杂些,因为标签几
乎能做任何事情。
第四章描述了模板系统的两步处理过
程: 编译和呈现。 为了自定义一个
模板标签,你需要告诉Django当遇到
你的标签时怎样进行这个过程。
当Django编译一个模板时,它将原始
模板分成一个个 节点 。每个节点都
是 django.template.Node 的一个实例,
并且具备 render() 方法。 于是,一个
已编译的模板就是 节点 对象的一个
列表。 例如,看看这个模板:
Hello, {{ person.name }}.
{
{
% ifequal name.birthday today %}
Happy birthday!
% else %}
Be sure to come back on your bir
thday
for a splendid surprise message.
% endifequal %}
{
被编译的模板表现为节点列表的形
式:
文本节点: "Hello, "
变量节点: person.name
文本节点: ".\n\n"
IfEqual节点: name.birthday和today
当你调用一个已编译模板
的 render() 方法时,模板就会用给定
的context来调用每个在它的节点列表
上的所有节点的 render() 方法。 这些
渲染的结果合并起来,形成了模板的
输出。 因此,要自定义模板标签,
你需要指明原始模板标签如何转换成
节点(编译函数)和节点的render()
方法完成的功能 。
在下面的章节中,我们将详细解说写
一个自定义标签时的所有步骤。
编写编译函数
当遇到一个模板标签(template tag)
时,模板解析器就会把标签包含的内
容,以及模板解析器自己作为参数调
用一个python函数。 这个函数负责返
回一个和当前模板标签内容相对应的
节点(Node)的实例。
例如,写一个显示当前日期的模板标
签:{% current_time %}。该标签会根
据参数指定的 strftime 格式(参见:
http://www.djangoproject.com/r/python/
标签的语法是个好主意。 在这个例
子里,标签应该这样使用:
The time is {% current_time "%Y-%m-%
d %I:%M %p" %}.
注意
没错, 这个模板标签是多余的,
Django默认的 {% now %} 用更简单
的语法完成了同样的工作。 这个模
板标签在这里只是作为一个例子。
这个函数的分析器会获取参数并创建
一个 Node 对象:
from django import template
register = template.Library()
def do_current_time(parser, token):
try:
#
split_contents() knows not
to split quoted strings.
tag_name, format_string = to
ken.split_contents()
except ValueError:
msg = '%r tag requires a sin
gle argument' % token.split_contents
()[0]
raise template.TemplateSynta
xError(msg)
return CurrentTimeNode(format_st
ring[1:-1])
这里需要说明的地方很多:
每个标签编译函数有两个参数,
parser和token。parser是模板解析
器对象。 我们在这个例子中并不
使用它。 token是正在被解析的语
句。
token.contents 是包含有标签原始
内容的字符串。 在我们的例子
中,它是'current_time "%Y-%m-
%
d %I:%M %p"' 。
token.split_contents() 方法按空格拆
分参数同时保证引号中的字符串
不拆分。 应该避免使用
token.contents.split() (仅使用
Python的标准字符串拆分)。 它
不够健壮,因为它只是简单的按
照所有空格进行拆分,包括那些
引号引起来的字符串中的空格。
这个函数可以抛
出 django.template.TemplateSyntaxE
rror ,这个异常提供所有语法错
误的有用信息。
不要把标签名称硬编码在你的错
误信息中,因为这样会把标签名
称和你的函数耦合在一起。
token.split_contents()[0]_总是_记录
标签的名字,就算标签没有任何
参数。
这个函数返回一
个 CurrentTimeNode (稍后我们将
创建它),它包含了节点需要知
道的关于这个标签的全部信息。
在这个例子中,它只是传递了参
数 "%Y-%m-%d %I:%M %p" 。模
板标签开头和结尾的引号使用
format_string[1:-1] 除去。
模板标签编译函数 必须 返回一
个 Node 子类,返回其它值都是错
的。
编写模板节点
编写自定义标签的第二步就是定义一
个拥有 render() 方法的 Node 子类。
继续前面的例子,我们需要定义
CurrentTimeNode :
import datetime
class CurrentTimeNode(template.Node)
:
def __init__(self, format_string
)
:
self.format_string = str(for
mat_string)
def render(self, context):
now = datetime.datetime.now(
)
return now.strftime(self.for
mat_string)
这两个函数( init() 和 render() )与
模板处理中的两步(编译与渲染)直
接对应。 这样,初始化函数仅仅需
要存储后面要用到的格式字符串,
而 render() 函数才做真正的工作。
与模板过滤器一样,这些渲染函数应
该静静地捕获错误,而不是抛出错
误。 模板标签只允许在编译的时候
抛出错误。
注册标签
最后,你需要用你模块的Library 实
例注册这个标签。 注册自定义标签
与注册自定义过滤器非常类似(如前
文所述)。 只需实例化一
个 template.Library 实例然后调用它
的 tag() 方法。 例如:
register.tag('current_time', do_curr
ent_time)
tag() 方法需要两个参数:
模板标签的名字(字符串)。
编译函数。
和注册过滤器类似,也可以在
Python2.4及其以上版本中使
用 register.tag装饰器:
@
register.tag(name="current_time")
def do_current_time(parser, token):
...
#
@
register.tag
def shout(parser, token):
...
#
如果你像在第二个例子中那样忽
略 na me 参数的话,Django会使用函
数名称作为标签名称。
在上下文中设置变量
前一节的例子只是简单的返回一个
值。 很多时候设置一个模板变量而
非返回值也很有用。 那样,模板作
者就只能使用你的模板标签所设置的
变量。
要在上下文中设置变量,
在 render() 函数的context对象上使用
字典赋值。 这里是一个修改过的
CurrentTimeNode ,其中设定了一个
模板变量 current_time ,并没有返回
它:
class CurrentTimeNode2(template.Node
)
:
def __init__(self, format_string
)
:
self.format_string = str(for
mat_string)
def render(self, context):
now = datetime.datetime.now(
)
context['current_time'] = no
w.strftime(self.format_string)
return ''
(我们把创建函数do_current_time2和
注册给current_time2模板标签的工作
留作读者练习。 )
注意 render() 返回了一个空字符
串。 render() 应当总是返回一个字符
串,所以如果模板标签只是要设置变
量, render() 就应该返回一个空字符
串。
你应该这样使用这个新版本的标签:
{
% current_time2 "%Y-%M-%d %I:%M %p"
%
}
The time is {{ current_time }}.
但是 CurrentTimeNode2 有一个问题:
变量名 current_time 是硬编码的。 这
意味着你必须确定你的模板在其它任
何地方都不使用 {{ current_time }} ,
因为 {% current_time2 %} 会盲目的覆
盖该变量的值。
一种更简洁的方案是由模板标签来指
定需要设定的变量的名称,就像这
样:
{
%
% get_current_time "%Y-%M-%d %I:%M
p" as my_current_time %}
The current time is {{ my_current_ti
me }}.
为此,你需要重构编译函数
和 Node 类,如下所示:
import re
class CurrentTimeNode3(template.Node
)
:
def __init__(self, format_string
var_name):
,
self.format_string = str(for
mat_string)
self.var_name = var_name
def render(self, context):
now = datetime.datetime.now(
)
.
context[self.var_name] = now
strftime(self.format_string)
return ''
def do_current_time(parser, token):
This version uses a regular ex
#
pression to parse tag contents.
try:
#
Splitting by None == split
ting by spaces.
tag_name, arg = token.conten
ts.split(None, 1)
except ValueError:
msg = '%r tag requires argum
ents' % token.contents[0]
raise template.TemplateSynta
xError(msg)
m = re.search(r'(.*?) as (\w+)',
arg)
if m:
fmt, var_name = m.groups()
else:
msg = '%r tag had invalid ar
guments' % tag_name
raise template.TemplateSynta
xError(msg)
if not (fmt[0] == fmt[-1] and fm
t[0] in ('"', "'")):
msg = "%r tag's argument sho
uld be in quotes" % tag_name
raise template.TemplateSynta
xError(msg)
return CurrentTimeNode3(fmt[1:-1
, var_name)
]
现在 do_current_time() 把格式字符串
和变量名传递给 CurrentTimeNode3 。
分析直至另一个模板标签
模板标签可以像包含其它标签的块一
样工作(想
想 {% if %} 、 {% for %} 等)。 要
创建一个这样的模板标签,在你的编
译函数中使用 parser.parse() 。
标准的 {% comment %} 标签是这样
实现的:
def do_comment(parser, token):
nodelist = parser.parse(('endcom
ment',))
parser.delete_first_token()
return CommentNode()
class CommentNode(template.Node):
def render(self, context):
return ''
parser.parse() 接收一个包含了需要分
析的模板标签名的元组作为参数。
它返回一个django.template.NodeList实
例,它是一个包含了所有_Node_对象
的列表,这些对象是解析器在解析到
任一元组中指定的标签之前遇到的内
容.
因此在前面的例子中, nodelist 是
在 {% c omme nt %} 和 {% endcomment
%
} 之间所有节点的列表,不包括
{
}
% c omme nt %} 和 {% endcomment %
自身。
在 parser.parse() 被调用之后,分析器
还没有清除 {% endcomment %} 标
签,因此代码需要显式地调用
parser.delete_first_token() 来防止该标
签被处理两次。
之后 CommentNode.render() 只是简单
地返回一个空字符串。
在 {% c omme nt %} 和 {% endcomment
%
} 之间的所有内容都被忽略。
分析直至另外一个模板标
签并保存内容
在前一个例子中, do_comment() 抛
弃了
{
}
% c omme nt %} 和 {% endcomment %
之间的所有内容。当然也可以修改
和利用下标签之间的这些内容。
例如,这个自定义模板标签
{
{
% upper %},它会把它自己和
% endupper %}之间的内容变成大
写:
{
% upper %}
This will appear in uppercase, {
user_name }}.
% endupper %}
{
{
就像前面的例子一样,我们将使
用 parser.parse() 。这次,我们将产生
的 nodelist 传递给 Node :
def do_upper(parser, token):
nodelist = parser.parse(('endupp
er',))
parser.delete_first_token()
return UpperNode(nodelist)
class UpperNode(template.Node):
def __init__(self, nodelist):
self.nodelist = nodelist
def render(self, context):
output = self.nodelist.rende
r(context)
return output.upper()
这里唯一的一个新概念
是 UpperNode.render() 中
的 self.nodelist.render(context) 。它对
节点列表中的每个Node 简单的调
用 render() 。
更多的复杂渲染示例请查
看 django/template/defaulttags.py 中
的 {% if %} 、 {% for %} 、 {% ifequ
al %}和 {% ifchanged %} 的代码。
简单标签的快捷方式
许多模板标签接收单一的字符串参数
或者一个模板变量引用,然后独立地
根据输入变量和一些其它外部信息进
行处理并返回一个字符串。 例如,
我们先前写的current_time标签就是这
样一个例子。 我们给定了一个格式
化字符串,然后它返回一个字符串形
式的时间。
为了简化这类标签,Django提供了一
个帮助函数simple_tag。这个函数是
django.template.Library的一个方法,
它接受一个只有一个参数的函数作参
数,把它包装在render函数和之前提
及过的其他的必要单位中,然后通过
模板系统注册标签。
我们之前的的 current_time 函数于是
可以写成这样:
def current_time(format_string):
try:
return datetime.datetime.now
().strftime(str(format_string))
except UnicodeEncodeError:
return ''
register.simple_tag(current_time)
在Python 2.4中,也可以使用装饰器语
法:
@
register.simple_tag
def current_time(token):
#
...
有关 simple_tag 辅助函数,需要注意
下面一些事情:
传递给我们的函数的只有(单
个)参数。
在我们的函数被调用的时候,检
查必需参数个数的工作已经完成
了,所以我们不需要再做这个工
作。
参数两边的引号(如果有的话)
已经被截掉了,所以我们会接收
到一个普通Unicode字符串。
包含标签
另外一类常用的模板标签是通过渲
染 其他 模板显示数据的。 比如说,
Django的后台管理界面,它使用了自
定义的模板标签来显示新增/编辑表
单页面下部的按钮。 那些按钮看起
来总是一样的,但是链接却随着所编
辑的对象的不同而改变。 这就是一
个使用小模板很好的例子,这些小模
板就是当前对象的详细信息。
这些排序标签被称为 包含标签 。如
何写包含标签最好通过举例来说明。
让我们来写一个能够产生指定作者对
象的书籍清单的标签。 我们将这样
利用标签:
{
% books_for_author author %}
结果将会像下面这样:
<
<
ul>
<
<
<
li>The Cat In The Hat</li>
li>Hop On Pop</li>
li>Green Eggs And Ham</li>
/ul>
首先,我们定义一个函数,通过给定
的参数生成一个字典形式的结果。
需要注意的是,我们只需要返回字典
类型的结果就行了,不需要返回更复
杂的东西。 这将被用来作为模板片
段的内容:
def books_for_author(author):
books = Book.objects.filter(auth
ors__id=author.id)
return {'books': books}
接下来,我们创建用于渲染标签输出
的模板。 在我们的例子中,模板很
简单:
<
{
ul>
% for book in books %}
<
li>{{ book.title }}</li>
{
<
% endfor %}
/ul>
最后,我们通过对一个 Library 对象
使用 inclusion_tag() 方法来创建并注
册这个包含标签。
在我们的例子中,如果先前的模板
在 polls/result_snippet.html 文件中,
那么我们这样注册标签:
register.inclusion_tag('book_snippet
.
html')(books_for_author)
Python 2.4装饰器语法也能正常工作,
所以我们可以这样写:
@
register.inclusion_tag('book_snippe
t.html')
def books_for_author(author):
#
...
有时候,你的包含标签需要访问父模
板的context。 为了解决这个问题,
Django为包含标签提供了一个
takes_context 选项。 如果你在创建模
板标签时,指明了这个选项,这个标
签就不需要参数,并且下面的Python
函数会带一个参数: 就是当这个标
签被调用时的模板context。
例如,你正在写一个包含标签,该标
签包含有指向主页
的 home_l i nk 和 home_title 变量。
Python函数会像这样:
@
register.inclusion_tag('link.html',
takes_context=True)
def jump_link(context):
return {
'
link': context['home_link']
,
'
'
title': context['home_title
],
}
(
注意函数的第一个参数 必
须 是 context 。)
模板 l i nk.html 可能包含下面的东西:
Jump directly to <a href="{{ link }}
"
>{{ title }}</a>.
然后您想使用自定义标签时,就可以
加载它的库,然后不带参数地调用
它,就像这样:
{
% jump_link %}
编写自定义模板加载
器
Djangos 内置的模板加载器(在先前
的模板加载内幕章节有叙述)通常会
满足你的所有的模板加载需求,但是
如果你有特殊的加载需求的话,编写
自己的模板加载器也会相当简单。
比如:你可以从数据库中,或者利用
Python的绑定直接从Subversion库中,
更或者从一个ZIP文档中加载模板。
模板加载器,也就
是 TEMPLATE_LOADERS 中的每一
项,都要能被下面这个接口调用:
load_template_source(template_name,
template_dirs=None)
参数 template_name 是所加载模板的
名称 (和传递
给 loader.get_template() 或
者 loader.select_template()一样),
而 template_dirs 是一个可选的代替
TEMPLATE_DIRS的搜索目录列表。
如果加载器能够成功加载一个模板 ,
它应当返回一个元
组: (template_source, template_path)
。
在这里的template_source 就是将被
模板引擎编译的的模板字符串,
而 template_path 是被加载的模板的路
径。 由于那个路径可能会出于调试
目的显示给用户,因此它应当很快的
指明模板从哪里加载。
如果加载器加载模板失败,那么就会
触
发 django.template.TemplateDoesNotEx
ist 异常。
每个加载函数都应该有一个名
为 is_usable 的函数属性。 这个属性
是一个布尔值,用于告知模板引擎这
个加载器是否在当前安装的Python中
可用。 例如,如果 pkg_resources 模
块没有安装的话,eggs加载器(它能
够从python eggs中加载模板)就应该
把 is_usable 设为 False ,因为必须通
过 pkg_resources 才能从eggs中读取数
据。
一个例子可以清晰地阐明一切。 这
儿是一个模板加载函数,它可以从
ZIP文件中加载模板。 它使用了自定
义的设置 TEMPLATE_ZIP_FILES 来
取代了 TEMPLATE_DIRS 用作查找路
径,并且它假设在此路径上的每一个
文件都是包含模板的ZIP文件:
from django.conf import settings
from django.template import Template
DoesNotExist
import zipfile
def load_template_source(template_na
me, template_dirs=None):
"
Template loader that loads temp
lates from a ZIP file."
template_zipfiles = getattr(sett
ings, "TEMPLATE_ZIP_FILES", [])
#
Try each ZIP file in TEMPLATE_
ZIP_FILES.
for fname in template_zipfiles:
try:
z = zipfile.ZipFile(fnam
e)
source = z.read(template
_
name)
except (IOError, KeyError):
continue
z.close()
#
We found a template, so re
turn the source.
template_path = "%s:%s" % (f
name, template_name)
return (source, template_pat
h)
#
If we reach here, the template
couldn't be loaded
raise TemplateDoesNotExist(templ
ate_name)
#
This loader is always usable (sinc
e zipfile is included with Python)
load_template_source.is_usable = True
我们要想使用它,还差最后一步,就
是把它加入
到 TEMPLATE_LOADERS 。 如果我
们将这个代码放入一个叫
mysite.zip_loader的包中,那么我们要
把
mysite.zip_loader.load_template_source
加到TEMPLATE_LOADERS中。
配置独立模式下的模
板系统
注意:
这部分只针对于对在其他应用中使用
模版系统作为输出组件感兴趣的人。
如果你是在Django应用中使用模版系
统,请略过此部分。
通常,Django会从它的默认配置文件
和
由 DJANGO_SETTINGS_MODULE 环
境变量所指定的模块中加载它需要的
所有配置信息。 (这点在第四章
的”特殊的Python命令提示行”一节解
释过。)但是当你想在非Django应用
中使用模版系统的时候,采用环境变
量并不方便,因为你可能更想同其余
的应用一起配置你的模板系统,而不
是处理配置文件并通过环境变量指向
他们。
为了解决这个问题,你需要使用附录
D中所描述的手动配置选项。概括的
说,你需要导入正确的模板中的片
段,然后在你访问任一个模板函数之
前,首先用你想指定的配置访问
Django.conf.settings.configure()。
你可能会考虑至少要设
置 TEMPLATE_DIRS (如果你打算使
用模板加载
器), DEFAULT_CHARSET (尽管
默认的utf-8 编码相当好用),以
及 TEMPLATE_DEBUG 。所有可用
的选项在附录D中都有详细描述,所
有以 TEMPLATE_开头的选项都可能
使你感兴趣。
接下来做什么?
延续本章的高级话题,下一章 会继
续讨论Django模型的高级用法。
在第5章里,我们介绍了Django的数
据层如何定义数据模型以及如何使用
数据库API来创建、检索、更新以及
删除记录 在这章里,我们将向你介
绍Django在这方面的一些更高级功
能。
相关对象
先让我们回忆一下在第五章里的关于
书本(book)的数据模型:
from django.db import models
class Publisher(models.Model):
name = models.CharField(max_leng
th=30)
address = models.CharField(max_l
ength=50)
city = models.CharField(max_leng
th=60)
state_province = models.CharFiel
d(max_length=30)
country = models.CharField(max_l
ength=50)
website = models.URLField()
def __unicode__(self):
return self.name
class Author(models.Model):
first_name = models.CharField(ma
x_length=30)
last_name = models.CharField(max
_
length=40)
email = models.EmailField()
def __unicode__(self):
return u'%s %s' % (self.firs
t_name, self.last_name)
class Book(models.Model):
title = models.CharField(max_len
gth=100)
authors = models.ManyToManyField
(Author)
publisher = models.ForeignKey(Pu
blisher)
publication_date = models.DateFi
eld()
def __unicode__(self):
return self.title
如我们在第5章的讲解,获取数据库对
象的特定字段的值只需直接使用属
性。 例如,要确定ID为50的书本的标
题,我们这样做:
>
>> from mysite.books.models import
Book
>
>
>> b = Book.objects.get(id=50)
>> b.title
u'The Django Book'
但是,在之前有一件我们没提及到的
是表现为
ForeignKey 或 ManyToManyFi el d的关
联对象字段,它们的作用稍有不同。
访问外键(Forei gn Key)值
当你获取一个ForeignKey 字段时,你会
得到相关的数据模型对象。 例如:
>
>
>> b = Book.objects.get(id=50)
>> b.publisher
>
>> b.publisher.website
u'http://www.apress.com/'
对于用 ForeignKey 来定义的关系来
说,在关系的另一端也能反向的追溯
回来,只不过由于不对称性的关系而
稍有不同。 通过一个 publisher 对
象,直接获取 books ,用
publisher.book_set.all() ,如下:
>
>> p = Publisher.objects.get(name='
Apress Publishing')
>
[
>> p.book_set.all()
<Book: The Django Book>, <Book: Div
e Into Python>, ...]
实际上,book_set 只是一
个 QuerySet(参考第5章的介绍),
所以它可以像QuerySet一样,能实现数
据过滤和分切,例如:
>
>> p = Publisher.objects.get(name='
Apress Publishing')
>> p.book_set.filter(name__icontain
s='django')
>
[
<Book: The Django Book>, <Book: Pro
Django>]
属性名称book_set是由模型名称的小
写(如book)加_set组成的。
访问多对多值(Many-to-
Many Values)
多对多和外键工作方式相同,只不过
我们处理的是QuerySet而不是模型实
例。 例如,这里是如何查看书籍的作
者:
>
>
[
>> b = Book.objects.get(id=50)
>> b.authors.all()
<Author: Adrian Holovaty>, <Author:
Jacob Kaplan-Moss>]
>
>> b.authors.filter(first_name='Adr
ian')
[
>
<Author: Adrian Holovaty>]
>> b.authors.filter(first_name='Ada
m')
[
]
反向查询也可以。 要查看一个作者
的所有书籍,使用author.book_set ,就如
这样 :
>
>> a = Author.objects.get(first_nam
e='Adrian', last_name='Holovaty')
>
[
>> a.book_set.all()
<Book: The Django Book>, <Book: Adr
ian's Other Book>]
这里,就像使用 ForeignKey字段一样,
属性名book_set是在数据模型(model)
名后追加_set。
更改数据库模式
(Database Schema)
在我们在第5章介绍 syncdb 这个命令
时, 我们注意到 syncdb仅仅创建数据
库里还没有的表,它 并不 对你数据
模型的修改进行同步,也不处理数据
模型的删除。 如果你新增或修改数
据模型里的字段,或是删除了一个数
据模型,你需要手动在数据库里进行
相应的修改。 这段将解释了具体怎
么做:
当处理模型修改的时候,将Django的
数据库层的工作流程铭记于心是很重
要的。
如果模型包含一个未曾在数据库
里建立的字段,Django会报出错
信息。 当你第一次用Django的数
据库API请求表中不存在的字段时
会导致错误(就是说,它会在运
行时出错,而不是编译时)。
Django_不_关心数据库表中是否
存在未在模型中定义的列。
Django_不_关心数据库中是否存
在未被模型表示的表格。
改变模型的模式架构意味着需要按照
顺序更改Python代码和数据库。
添加字段
当要向一个产品设置表(或者说是
model)添加一个字段的时候,要使用
的技巧是利用Django不关心表里是否
包含model里所没有的列的特性。 策
略就是现在数据库里加入字段,然后
同步Django的模型以包含新字段。
然而 这里有一个鸡生蛋蛋生鸡的问
题 ,由于要想了解新增列的SQL语
句,你需要使用Django的
manage.py sqlall命令进行查看 ,而这又
需要字段已经在模型里存在了。 (注
意:你并 _不是非得使用_与Django相
同的SQL语句创建新的字段,但是这
样做确实是一个好主意 ,它能让一切
都保持同步。 )
这个鸡-蛋的问题的解决方法是在开
发者环境里而不是发布环境里实现这
个变化。 (你_正_使用的是测试/开发
环境,对吧?)下面是具体的实施步
骤。
首先,进入开发环境(也就是说,不
是在发布环境里):
1. 在你的模型里添加字段。
2
. 运行 manage.py sqlall [yourapp] 来
测试模型新的 CREATE TABLE 语
句。 注意为新字段的列定义。
3
. 开启你的数据库的交互命令界面
(比如, psql 或mys ql , 或者可以使
用 manage.py dbshell )。 执行
ALTER TABLE 语句来添加新列。
4
. 使用Python的manage.py shell,通
过导入模型和选中表单(例
如, MyModel.objects.all()[:5] )来
验证新的字段是否被正确的添加 ,
如果一切顺利,所有的语句都不会
报错。
然后在你的产品服务器上再实施一遍
这些步骤。
1. 启动数据库的交互界面。
2
. 执行在开发环境步骤中,第三步
的ALTER TABLE语句。
3. 将新的字段加入到模型中。 如果
你使用了某种版本控制工具,并
且在第一步中,已经提交了你在
开发环境上的修改,现在,可以
在生产环境中更新你的代码了
(
例如,如果你使用Subversion,
执行svn update。
4
. 重新启动We b server,使修改生
效。
让我们实践下,比如添加一个
num_pages字段到第五章中Book模
型。首先,我们会把开发环境中的模
型改成如下形式:
class Book(models.Model):
title = models.CharField(max_len
gth=100)
authors = models.ManyToManyField
(Author)
publisher = models.ForeignKey(Pu
blisher)
publication_date = models.DateFi
eld()
num_pages = models.IntegerField(
blank=True, null=True)
def __unicode__(self):
return self.title
(注意 阅读第六章的“设置可选字
段”以及本章下面的“添加非空列”小
节以了解我们在这里添加blank=True
和null=True的原因。)
然后,我们运行命令manage.py sqlall
books 来查看CREATE TABLE语句。
语句的具体内容取决与你所使用的数
据库, 大概是这个样子:
CREATE TABLE "books_book" (
"
id" serial NOT NULL PRIMARY KEY
,
"
"
title" varchar(100) NOT NULL,
publisher_id" integer NOT NULL
REFERENCES "books_publisher" ("id"),
"
publication_date" date NOT NULL
,
)
"
num_pages" integer NULL
;
新加的字段被这样表示:
"
num_pages" integer NULL
接下来,我们要在开发环境上运行数
据库客户端,如果是PostgreSQL,运
行 psql,,然后,我执行如下语句。
ALTER TABLE books_book ADD COLUMN nu
m_pages integer;
添加 非NULL字段
这里有个微妙之处值得一提。 在我
们添加字段num_pages的时候,我们
使用了 blank=True 和 nul l =Tr ue 选
项。 这是因为在我们第一次创建它
的时候,这个数据库字段会含有空
值。
然而,想要添加不能含有空值的字段
也是可以的。 要想实现这样的效
果,你必须先创建 NULL型的字段,
然后将该字段的值填充为某个默认
值,然后再将该字段改为 NOT NULL
型。 例如:
BEGIN;
ALTER TABLE books_book ADD COLUMN nu
m_pages integer;
UPDATE books_book SET num_pages=0;
ALTER TABLE books_book ALTER COLUMN
num_pages SET NOT NULL;
COMMIT;
如果你这样做,记得你不要在模型中
添加 blank=True 和 nul l =Tr ue 选项。
执行ALTER TABLE之后,我们要验
证一下修改结果是否正确。启动
python并执行下面的代码:
>
>> from mysite.books.models import
Book
>
>> Book.objects.all()[:5]
如果没有异常发生,我们将切换到生
产服务器,然后在生产环境的数据库
中执行命令ALTER TABLE 然后我们
更新生产环境中的模型,最后重启
web服务器。
删除字段
从Model中删除一个字段要比添加容
易得多。 删除字段,仅仅只要以下
几个步骤:
删除字段,然后重新启动你的web
服务器。
用以下命令从数据库中删除字
段:
ALTER TABLE books_book DROP COLUMN n
um_pages;
请保证操作的顺序正确。 如果你先
从数据库中删除字段,Django将会立
即抛出异常。
删除多对多关联字段
由于多对多关联字段不同于普通字
段,所以删除操作是不同的。
从你的模型中删除
ManyToManyFi el d,然后重启web
服务器。
用下面的命令从数据库删除关联
表:
DROP TABLE books_book_authors;
像上面一样,注意操作的顺序。
删除模型
删除整个模型要比删除一个字段容
易。 删除一个模型只要以下几个步
骤:
从文件中删除你想要删除的模
型,然后重启web 服务器
models.py
然后用以下命令从数据库中删除
表:
DROP TABLE books_book;
当你需要从数据库中删除任何有
依赖的表时要注意(也就是任何
与表books_book有外键的表 )。
正如在前面部分,一定要按这样的顺
序做。
Managers
在语句Book.objects.all()中,objects是
一个特殊的属性,需要通过它查询数
据库。 在第5章,我们只是简要地说
这是模块的manager 。现在是时候深
入了解managers是什么和如何使用
了。
总之,模块manager是一个对象,
Django模块通过它进行数据库查询。
每个Django模块至少有一个manager,
你可以创建自定义manager以定制数
据库访问。
下面是你创建自定义manager的两个
原因: 增加额外的manager方法,和/
或修manager返回的初始QuerySet。
增加额外的Manager方法
增加额外的manager方法是为模块添
加表级功能的首选办法。 (至于行
级功能,也就是只作用于模型对象实
例的函数,一会儿将在本章后面解
释。)
例如,我们为Book模型定义了一个
title_count()方法,它需要一个关键
字,返回包含这个关键字的书的数
量。 (这个例子有点牵强,不过它
可以说明managers如何工作。)
#
models.py
from django.db import models
... Author and Publisher models he
#
re ...
class BookManager(models.Manager):
def title_count(self, keyword):
return self.filter(title__ic
ontains=keyword).count()
class Book(models.Model):
title = models.CharField(max_len
gth=100)
authors = models.ManyToManyField
(Author)
publisher = models.ForeignKey(Pu
blisher)
publication_date = models.DateFi
eld()
num_pages = models.IntegerField(
blank=True, null=True)
objects = BookManager()
def __unicode__(self):
return self.title
有了这个manager,我们现在可以这
样做:
>
'
4
>
'
>> Book.objects.title_count('django
)
>> Book.objects.title_count('python
)
1
8
下面是编码该注意的一些地方:
我们建立了一个BookManager类,
它继承了
django.db.models.Manager。这个类
只有一个title_count()方法,用来
做统计。 注意,这个方法使用了
self.filter(),此处self指manager本
身。
我们把BookManager()赋值给模型
的objects属性。 它将取代模型的
默认manager(objects)如果我们
没有特别定义,它将会被自动创
建。 我们把它命名为objects,这
是为了与自动创建的manager保持
一致。
为什么我们要添加一个title_count()方
法呢?是为了将经常使用的查询进行
封装,这样我们就不必重复编码了。
修改初始Manager
QuerySets
manager的基本QuerySet返回系统中的
所有对象。 例如,
Book.objects.all() 返回数据库
book中的所有书本。
我们可以通过覆盖
Manager.get_query_set()方法来重写
manager的基本QuerySet。
get_query_set()按照你的要求返回一个
QuerySet。
例如,下面的模型有 两个 manager。一
个返回所有对像,另一个只返回作者
是Roald Dahl的书。
from django.db import models
#
.
First, define the Manager subclass
class DahlBookManager(models.Manager
)
:
def get_query_set(self):
return super(DahlBookManager
self).get_query_set().filter(autho
,
r='Roald Dahl')
#
Then hook it into the Book model e
xplicitly.
class Book(models.Model):
title = models.CharField(max_len
gth=100)
author = models.CharField(max_le
ngth=50)
#
...
objects = models.Manager() # The
default manager.
dahl_objects = DahlBookManager()
#
The Dahl-specific manager.
在这个示例模型中,Book.objects.all()
返回了数据库中的所有书本,而
Book.dahl_objects.all()只返回了一本.
注意我们明确地将objects设置成
manager的实例,因为如果我们不这
么做,那么唯一可用的manager就将
是dah1_objects。
当然,由于get_query_set()返回的是一
个QuerySet对象,所以我们可以使用
filter(),exclude()和其他一切QuerySet
的方法。 像这些语法都是正确的:
Book.dahl_objects.all()
Book.dahl_objects.filter(title='Mati
lda')
Book.dahl_objects.count()
这个例子也指出了其他有趣的技术:
在同一个模型中使用多个manager。
只要你愿意,你可以为你的模型添加
多个manager()实例。 这是一个为模
型添加通用滤器的简单方法。
例如 :
class MaleManager(models.Manager):
def get_query_set(self):
return super(MaleManager, se
lf).get_query_set().filter(sex='M')
class FemaleManager(models.Manager):
def get_query_set(self):
return super(FemaleManager,
self).get_query_set().filter(sex='F'
)
class Person(models.Model):
first_name = models.CharField(ma
x_length=50)
last_name = models.CharField(max
_
length=50)
sex = models.CharField(max_lengt
h=1, choices=(('M', 'Male'), ('F', '
Female')))
people = models.Manager()
men = MaleManager()
women = FemaleManager()
这个例子允许你执行
Person.men.all() ,
Person.women.all() ,
Person.people.all() 查询,生成
你想要的结果。
如果你使用自定义的Manager对象,
请注意,Django遇到的第一个
Manager(以它在模型中被定义的位置
为准)会有一个特殊状态。 Django将
会把第一个Manager 定义为默认
Manager ,Django的许多部分(但是不
包括admi n应用)将会明确地为模型使
用这个manager。 结论是,你应该小
心地选择你的默认manager。因为覆
盖get_query_set() 了,你可能接受到
一个无用的返回对像,你必须避免这
种情况。
模型方法
为了给你的对像添加一个行级功能,
那就定义一个自定义方法。 有鉴于
manager经常被用来用一些整表操作
(
table-wide),模型方法应该只对
特殊模型实例起作用。
这是一项在模型的一个地方集中业务
逻辑的技术。
最好用例子来解释一下。 这个模型
有一些自定义方法:
from django.contrib.localflavor.us.m
odels import USStateField
from django.db import models
class Person(models.Model):
first_name = models.CharField(ma
x_length=50)
last_name = models.CharField(max
_
length=50)
birth_date = models.DateField()
address = models.CharField(max_l
ength=100)
city = models.CharField(max_leng
th=50)
state = USStateField() # Yes, th
is is U.S.-centric...
def baby_boomer_status(self):
"
Returns the person's baby-b
oomer status."
import datetime
if datetime.date(1945, 8, 1)
<
= self.birth_date <= datetime.date
(1964, 12, 31):
return "Baby boomer"
if self.birth_date < datetim
e.date(1945, 8, 1):
return "Pre-boomer"
return "Post-boomer"
def is_midwestern(self):
"
Returns True if this person
is from the Midwest."
return self.state in ('IL',
'
WI', 'MI', 'IN', 'OH', 'IA', 'MO')
def _get_full_name(self):
"
Returns the person's full n
ame."
return u'%s %s' % (self.firs
t_name, self.last_name)
full_name = property(_get_full_n
ame)
例子中的最后一个方法是一个
property。 想了解更多关于属性的信
息请访问
http://www.python.org/download/releas
es/2.2/descrintro/#property
这是用法的实例:
>
>> p = Person.objects.get(first_nam
e='Barack', last_name='Obama')
>> p.birth_date
datetime.date(1961, 8, 4)
>
>
'
>
>> p.baby_boomer_status()
Baby boomer'
>> p.is_midwestern()
True
>> p.full_name # Note this isn't a
method -- it's treated as an attrib
>
ute
u'Barack Obama'
执行原始SQL查询
有时候你会发现Django数据库API带
给你的也只有这么多,那你可以为你
的数据库写一些自定义SQL查询。 你
可以通过导入django.db.connection对
像来轻松实现,它代表当前数据库连
接。 要使用它,需要通过
connection.cursor()得到一个游标对
像。 然后,使用
cursor.execute(sql, [params])来执行
SQL语句,使用cursor.fetchone()或者
cursor.fetchall()来返回记录集。 例如:
>
>
>
.
>> from django.db import connection
>> cursor = connection.cursor()
>> cursor.execute("""
..
SELECT DISTINCT first_name
.
.
..
..
FROM people_person
WHERE last_name = %s""", ['Le
nnon'])
>
>
[
>> row = cursor.fetchone()
>> print row
'John']
connection和cursor几乎实现了标准
Python DB-API,你可以访问
http://www.python.org/peps/pep-
_
_来获取更多信息。 如果你对Python
DB-API不熟悉,请注意在
cursor.execute() 的SQL语句中使用
“
%s” ,而不要在SQL内直接添加参
数。 如果你使用这项技术,数据库
基础库将会自动添加引号,同时在必
要的情况下转意你的参数。
不要把你的视图代码和
django.db.connection语句混杂在一
起,把它们放在自定义模型或者自定
义manager方法中是个不错的主意。
比如,上面的例子可以被整合成一个
自定义manager方法,就像这样:
from django.db import connection, mo
dels
class PersonManager(models.Manager):
def first_names(self, last_name)
:
cursor = connection.cursor()
cursor.execute("""
SELECT DISTINCT first_na
me
[
FROM people_person
WHERE last_name = %s""",
last_name])
return [row[0] for row in cu
rsor.fetchone()]
class Person(models.Model):
first_name = models.CharField(ma
x_length=50)
last_name = models.CharField(max
_
length=50)
objects = PersonManager()
然后这样使用 :
>
>> Person.objects.first_names('Lenn
on')
[
'John', 'Cynthia']
接下来做什么?
在下一章 我们将讲解Django的通用视
图框架,使用它创建常见的网站可以
节省时间。
这里需要再次回到本书的主题: 在
最坏的情况下, Web 开发是一项无
聊而且单调的工作。 到目前为止,
我们已经介绍了 Django 怎样在模型
和模板的层面上减小开发的单调性,
但是 We b 开发在视图的层面上,也
经历着这种令人厌倦的事情。
Django的通用视图 可以减少这些痛
苦。 它抽象出一些在视图开发中常
用的代码和模式,这样就可以在无需
编写大量代码的情况下,快速编写出
常用的数据视图。 事实上,前面章
节中的几乎所有视图的示例都可以在
通用视图的帮助下重写。
在第八章简单的向大家介绍了怎样使
视图更加的“通用”。 回顾一下,我们
会发现一些比较常见的任务,比如显
示一系列对象,写一段代码来显
示 任何 对象内容。 解决办法就是传
递一个额外的参数到URLConf。
Django内建通用视图可以实现如下功
能:
完成常用的简单任务: 重定向到
另一个页面以及渲染一个指定的
模板。
显示列表和某个特定对象的详细
内容页面。 第8章中提到
的 event_list 和 entry_list 视图就是
列表视图的一个例子。 一个单一
的 event 页面就是我们所说的详细
内容页面。
呈现基于日期的数据的年/月/日归
档页面,关联的详情页面,最新
页面。 Django Weblogs
(http://www.djangoproject.com/web
用通用视图 架构的,就像是典型
的新闻报纸归档。
综上所述,这些视图为开发者日常开
发中常见的任务提供了易用的接口。
使用通用视图
使用通用视图的方法是在URLconf文
件中创建配置字典,然后把这些字典
作为URLconf元组的第三个成员。
(对于这个技巧的应用可以参看第八
章向视图传递额外选项。)
例如,下面是一个呈现静态“关于”页
面的URLconf :
from django.conf.urls.defaults impor
t *
from django.views.generic.simple imp
ort 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 impor
t *
from django.views.generic.simple imp
ort 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 Template
DoesNotExist
from django.views.generic.simple imp
ort direct_to_template
def about_pages(request, page):
try:
return direct_to_template(re
quest, template="about/%s.html" % pa
ge)
except TemplateDoesNotExist:
raise Http404()
在这里我们象使用其他函数一样使
用 direct_to_template 。 因为它返回一
个HttpResponse对象,我们只需要简
单的返回它就好了。 这里唯一有点
棘手的事情是要处理找不到模板的情
况。 我们不希望一个不存在的模板
导致一个服务端错误,所以我们捕获
TemplateDoesNotExist异常并且返回
404错误来作为替代。
这里有没有安全性问题?
眼尖的读者可能已经注意到一个
可能的安全漏洞: 我们直接使用
从客户端浏览器得到的数据构造
模板名称
(template="about/%s.html" % page )
。
乍看起来,这像是一个经典
的 目录跨越(directory
traversal) 攻击(详情请看第20
章)。 事实真是这样吗?
完全不是。 是的,一个恶意
的 page 值可以导致目录跨越,但
是尽管 page 是 从请求的URL中获
取的,但并不是所有的值都会被
接受。 这就是URL配置的关键所
在: 我们使用正则表达式 \w+ 来
从URL里匹配 page ,而 \w 只接受
字符和数字。 因此,任何恶意的
字符 (例如在这里是点 . 和正斜
线 / )将在URL解析时被拒绝,根
本不会传递给视图函数。
对象的通用视图
direct_to_template 毫无疑问是非常有
用的,但Django通用视图最有用的地
方是呈现数据库中的数据。 因为这
个应用实在太普遍了,Django带有很
多内建的通用视图来帮助你很容易
地生成对象的列表和明细视图。
让我们先看看其中的一个通用视图:
对象列表视图。 我们使用第五章中
的 Publisher 来举例:
class Publisher(models.Model):
name = models.CharField(max_leng
th=30)
address = models.CharField(max_l
ength=50)
city = models.CharField(max_leng
th=60)
state_province = models.CharFiel
d(max_length=30)
country = models.CharField(max_l
ength=50)
website = models.URLField()
def __unicode__(self):
return self.name
class Meta:
ordering = ['name']
要为所有的出版商创建一个列表页
面,我们使用下面的URL配置:
from django.conf.urls.defaults impor
t *
from django.views.generic import lis
t_detail
from mysite.books.models import Publ
isher
publisher_info = {
queryset': Publisher.objects.al
'
l(),
}
urlpatterns = patterns('',
(r'^publishers/$', list_detail.o
bject_list, publisher_info)
)
这就是所要编写的所有Python代码。
当然,我们还需要编写一个模板。
我们可以通过在额外参数字典中包含
一个template_name键来显式地告诉
object_list视图使用哪个模板:
from django.conf.urls.defaults impor
t *
from django.views.generic import lis
t_detail
from mysite.books.models import Publ
isher
publisher_info = {
'
queryset': Publisher.objects.al
l(),
'
template_name': 'publisher_list
_
}
page.html',
urlpatterns = patterns('',
(r'^publishers/$', list_detail.o
bject_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_l
ist %}
/li>
<
li>{{ publisher.name }}
<
{
% endfor %}
<
/ul>
{
% endblock %}
(注意,这里我们假定存在一个
base.html模板,它和我们第四章中的
一样。)
这就是所有要做的事。 要使用通用
视图酷酷的特性只需要修改参数字典
并传递给通用视图函数。 附录D是通
用视图的完全参考资料;本章接下来
的章节将讲到自定义和扩展通用视图
的一些方法。
扩展通用视图
毫无疑问,使用通用视图可以充分加
快开发速度。 然而,在多数的工程
中,也会出现通用视图不能 满足需
求的情况。 实际上,刚接触Django的
开发者最常见的问题就是怎样使用通
用视图来处理更多的情况。
幸运的是,几乎每种情况都有相应的
方法来简易地扩展通用视图以处理这
些情况。 这时总是使用下面的 这些
方法。
制作友好的模板Context
你也许已经注意到范例中的出版商列
表模板在变量 object_list 里保存所有
的书籍。这个方法工作的很好,只是
对编写模板的人不太友好。 他们必
须知道这里正在处理的是书籍。 更
好的变量名应该是publisher_list,这
样变量所代表的内容就显而易见了。
我们可以很容易地像下面这样修
改 template_object_name 参数的名
称:
from django.conf.urls.defaults impor
t *
from django.views.generic import lis
t_detail
from mysite.books.models import Publ
isher
publisher_info = {
'
queryset': Publisher.objects.al
l(),
'
template_name': 'publisher_list
_
page.html',
template_object_name': 'publish
'
er',
}
urlpatterns = patterns('',
(r'^publishers/$', list_detail.o
bject_list, publisher_info)
)
在模板中,通用视图会通过在
template_object_name后追加一个_list
的方式来创建一个表示列表项目的变
量名。
使用有用的 template_object_name 总
是个好想法。 你的设计模板的合作
伙伴会感谢你的。
添加额外的Context
你常常需要呈现比通用视图提供的更
多的额外信息。 例如,考虑一下在
每个出版商的详细页面显示所有其他
出版商列表。 object_detail 通用视图
为context提供了出版商信息,但是看
起来没有办法在模板中 获取 所有 出
版商列表。
这是解决方法: 所有的通用视图都
有一个额外的可选参
数 extra_context 。这个参数是一个字
典数据类型,包含要添加到模板的
context中的额外的对象。 所以要给视
图提供所有出版商的列表,我们就用
这样的info字典:
publisher_info = {
'
queryset': Publisher.objects.al
l(),
er',
'
template_object_name': 'publish
'
extra_context': {'book_list': B
ook.objects.all()}
}
这样就把一个 {{ book_list }} 变量放
到模板的context中。 这个方法可以用
来传递任意数据 到通用视图模板中
去,非常方便。 这是非常方便的
不过,这里有一个很隐蔽的BUG,不
知道你发现了没有?
我们现在来看一下, extra_context 里
包含数据库查询的问题。 因为在这
个例子中,我们把
Publisher.objects.all() 放在URLconf
中,它只会执行一次(当URLconf第
一次加载的时候)。 当你添加或删
除出版商,你会发现在重启Web服务
器之前,通用视图不会反映出这些修
改(有关QuerySet何时被缓存和赋值
的更多信息请参考附录C中“缓存与查
询集”一节)。
备注
这个问题不适用于通用视图
的 queryset 参数。 因为Django知道
有些特别的 QuerySet 永远不能 被
缓存,通用视图在渲染前都做了
缓存清除工作。
解决这个问题的办法是
在 _extracontext 中用一个回调
(callback)来代替使用一个变量。
任何传递给extra_context的可调用对
象(例如一个函数)都会在每次视图
渲染前执行(而不是只执行一次)。
你可以象这样定义一个函数:
def get_books():
return Book.objects.all()
publisher_info = {
'
'
'
queryset': Publisher.objects.al
l(),
er',
template_object_name': 'publish
extra_context': {'book_list': g
et_books}
}
或者你可以使用另一个不是那么清晰
但是很简短的方法,事实
上 Publisher.objects.all 本身就是可以
调用的:
publisher_info = {
'
'
'
queryset': Publisher.objects.al
template_object_name': 'publish
extra_context': {'book_list': B
l(),
er',
ook.objects.all}
}
注意 Book.objects.all 后面没有括号;
这表示这是一个函数的引用,并没有
真正调用它(通用视图将会在渲染时
调用它)。
显示对象的子集
现在让我们来仔细看看这
个 queryset 。 大多数通用视图有一个
queryset参数,这个参数告诉视图要
显示对象的集合 (有关QuerySet的解
释请看第五章的 “选择对象”章节,详
细资料请参看附录B)。
举一个简单的例子,我们打算对书籍
列表按出版日期排序,最近的排在最
前:
book_info = {
'
queryset': Book.objects.order_b
y('-publication_date'),
}
urlpatterns = patterns('',
(r'^publishers/$', list_detail.o
bject_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_l
ist.html'
}
urlpatterns = patterns('',
(r'^publishers/$', list_detail.o
bject_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.o
bject_list, publisher_info),
(r'^books/(\w+)/$', books_by_pub
lisher),
)
接下来,我们
写 books_by_publisher 这个视图:
from django.shortcuts import get_obj
ect_or_404
from django.views.generic import lis
t_detail
from mysite.books.models import Book
,
Publisher
def books_by_publisher(request, name
)
:
#
Look up the publisher (and rai
se a 404 if it can't be found).
publisher = get_object_or_404(Pu
blisher, name__iexact=name)
Use the object_list view for t
#
he heavy lifting.
return list_detail.object_list(
request,
queryset = Book.objects.filt
er(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 autho
r_detail
urlpatterns = patterns('',
#
...
(r'^authors/(?P<author_id>\d+)/$
, author_detail),
'
)
#
...
接下来写包装函数:
import datetime
from django.shortcuts import get_obj
ect_or_404
from django.views.generic import lis
t_detail
from mysite.books.models import Auth
or
def author_detail(request, author_id
)
:
#
Delegate to the generic view a
nd get an HttpResponse.
response = list_detail.object_de
tail(
request,
queryset = Author.objects.al
l(),
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 exi
sts. (If the author doesn't exist,
object_detail() will raise Htt
#
#
#
p404, 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_li
st(
request,
queryset = Author.objects.al
l(),
mimetype = 'text/plain',
template_name = 'books/autho
r_list.txt'
)
response["Content-Disposition"]
=
"attachment; filename=authors.txt"
return response
这个方法之所以工作是因为通用视图
返回的 HttpResponse 对象可以象一个
字典 一样的设置HTTP的头部。 随便
说一下,这个 Content-Disposition 的
含义是 告诉浏览器下载并保存这个
页面,而不是在浏览器中显示它。
下一章
在这一章我们只讲了Django带的通用
视图其中一部分,不过这些方法也适
用于其他的 通用视图。 附录C详细地
介绍了所有可用的视图,如果你想了
解这些强大的特性,推荐你阅读一
下。
这本书的高级语法部分到此结束。
在下一章, 我们讲解了Django应用的
部署。
本章包含创建一个django程序最必不
可少的步骤 在服务器上部署它
如果你一直跟着我们的例子做,你可
能正在用runserver 但是runserver 要部
署你的django程序,你需要挂接到工
业用的服务器 如:Apache 在本章,
我们将展示如何做,但是,在做之前
我们要给你一个(要做的事的)清单.
准备你的代码库
很幸运,runserver 但是,在开始前,
有一些**
关闭Debug模式.
我们在第2章,用命令 django-
admin.py startproject创建了一个项目 ,
其中创建的 settings.py 文件
的 DEBUG设置默认为 Tr ue . django会
根据这个设置来改变他们的行为,
如果 DEBUG 模式被开启. 例如, 如
果 DEBUG 被设置成 Tr ue , 那么:
所有的数据库查询将被保存在内
存中,
以 django.db.connection.queries 的
形式. 你可以想象,这个吃内存!
任何404错误都将呈现django的特
殊的404页面(第3章有)而不是普通
的404页面。 这个页面包含潜在的
敏感信息,但是不会暴露在公共
互联网。
你的应用中任何未捕获的异常,
从基本的python语法错误到数据库
错误以及模板语法错误都会返回
漂亮的Django错误页面。 这个页
面包含了比404错误页面更多的敏
感信息,所以这个页面绝对不要
公开暴露。
简单的说,把 DEBUG 设置成 True
相当于告诉Django你的网站只会被可
信任的开发人员使用。 Internet里充
满了不可信赖的事物,当你准备部署
你的应用时,首要的事情就是把
DEBUG 设置为 False 。
来关闭模板Debug模式。
类似地,你应该在生产环境中把
TEMPLATE_DEBUGFalse 如果这个设
为 True ,为了在那个好看的错误页
面上显示足够的东西,Django的模版
系统就会为每一个模版保存一些额外
的信息。
实现一个404模板
如果 DEBUG 设置为 True ,Django会
显示那个自带的404错误页面。 但如
果 DEBUG 被设置成 False ,那它的
行为就不一样了: 他会显示一个在
你的模版根目录中名字叫 404.html
的模版 所以,当你准备部署你的应
用时,你会需要创建这个模版并在里
面放一些有意义的“页面未找到”的信
息
这里有一个 404.html 的示例,你可
以从它开始。 假定你使用的模板继
承并定义一个 base.html ,该页面由
title和content两块组成。
{
{
% extends "base.html" %}
% block title %}Page not found{% en
dblock %}
{
<
% block content %}
h1>Page not found</h1>
<
p>Sorry, but the requested page cou
ld not be found.</p>
% endblock %}
{
要测试你的404.html页面是否正常工
作,仅仅需要将DEBUG 设置为
False ,并且访问一个并不存在的
URL。 (它将在 sunserver 上工作
的和开发服务器上一样好)
实现一个500模板
类似的,如果 DEBUG 设置为 False
,Djang不再会显示它自带的应对未
处理的Python异常的错误反馈页面。
作为代替,它会查找一个名为
5
4
00.html 的模板并且显示它。 像
04.html 一样,这个模板应该被放
置在你的模板根目录下。
这里有一个关于500.html的比较棘手
的问题。你永远不能确定 为什么 会显
示这个模板,所以它不应该做任何需
要连接数据库,或者依赖任何可能被
破坏的基础构件的事情。 (例如:
它不应该使用自定义模板标签。)如
果它用到了模板继承,那么父模板也
就不应该依赖可能被破坏的基础构
件。 因此,最好的方法就是避免模
板继承,并且用一些非常简单的东
西。 这是一个 500.html 的例子,可
以把它作为一个起点:
<
!DOCTYPE html PUBLIC "-//W3C//DTD H
TML 4.01//EN"
"
http://www.w3.org/TR/html4/stri
ct.dtd">
<
<
html lang="en">
head>
<
title>Page unavailable</title>
<
<
/head>
body>
<
h1>Page unavailable</h1>
<
p>Sorry, but the requested page
is unavailable due to a
server hiccup.</p>
<
p>Our engineers have been notif
ied, so check back later.</p>
<
<
/body>
/html>
设置错误警告
当你使用Django制作的网站运行中出
现了异常,你会希望去了解以便于修
正它。 默认情况下,Django在你的代
码引发未处理的异常时,将会发送一
封Email至开发者团队。但你需要去
做两件事来设置这种行为。
首先,改变你的ADMINS设置用来引
入你的E-mail地址,以及那些任何需
要被注意的联系人的E-mail地址。 这
个设置采用了类似于(姓名, Email)元
组,像这样:
ADMINS = (
('John Lennon', 'jlennon@example
.
com'),
('Paul McCartney', 'pmacca@examp
le.com'),
)
第二,确保你的服务器配置为发送电
子邮件。 设置好postfix,sendmail或其
他本书范围之外但是与Django设置相
关的邮件服务器,你需要将将
EMAIL_HOST设置为你的邮件服务器
的正确的主机名. 默认模式下是设置
为’localhost’, 这个设置对大多数的共
享主机系统环境适用. 取决于你的安
排的复杂性,你可能还需要设置
EMAIL_HOST_USER,EMAIL_HOST_
PASSWORD,EMAIL_PORT或
EMAIL_USE_TLS。
你还可以设置
EMAIL_SUBJECT_PREFIX以控制
Django使用的 error e-mail的前缀。 默
认情况下它被设置为'[Django] '
设置连接中断警报
如果你安装有CommonMiddleware(比
如,你的MIDDLEWARE_CLASSES设
置包含
了’dj ango.mi ddl ew are.common.Commo
nMiddleware’的情况下,默认就安装
了CommonMiddleware),你就具有了设
置这个选项的能力:有人在访问你的
Django网站的一个非空的链接而导致
一个404错误的发生和连接中断的情
况,你将收到一封邮件. 如果你想激
活这个特性,设置
SEND_BROKEN_LINK_EMAILS 为
True(默认为False),并设置你的
MANAGERS为某个人或某些人的邮
件地址,这些邮件地址将会收到报告
连接中断错误的邮件. MANAGERS使
用和ADMINS 同样的语法.例如:
MANAGERS = (
('George Harrison', 'gharrison@e
xample.com'),
('Ringo Starr', 'ringo@example.c
om'),
)
请注意,错误的Email会令人感到反
感,对于任何人来说都是这样。
使用针对产品的不同
的设置
在此书中,我们仅仅处理一个单一的
设置文件 settings.py文件由django-
admin.py startproject命令生成。但是
当你准备要进行配置的时候,你将发
现你需要多个配置文件以使你的开发
环境和产品环境相独立。 比如,你
可能不想每次在本地机器上测试代码
改变的时候将DEBUG从False 改为
Tr ue。Django通过使用多个配置文件
而使得这种情况很容易得到避免。
如果你想把你的配置文件按照产品设
置和开发设置组织起来,你可以通过
下面三种方法的其中一种达到这个目
的。
设置成两个全面的,彼此独立的
配置文件
设置一个基本的配置文件(比
如,为了开发)和第二个(为了产
品)配置文件,第二个配置文件仅
仅从基本的那个配置文件导入配
置,并对需要定义的进行复写.
使用一个单独的配置文件,此配
置文件包含一个Python的逻辑判断
根据上下文环境改变设置。
我们将会在依次解释这几种方式
首先,最基本的方法是定义两个单独
的配置文件。 如果你是跟随之前的
例子做下来的,那么你已经有了一个
settings.py了,现在你只需要将它复制
一份并命名为
settings_production.py(文件名可以按
照你自己的喜好定义),在这个新文件
中改变DEBUG等设置。
第二种方法比较类似,但是减少了许
多冗余。 作为使用两个内容大部分
相同的配置文件的替代方式,你可以
使用一个文件为基本文件,另外一个
文件从基本文件中导入相关设定。
例如
#
settings.py
DEBUG = True
TEMPLATE_DEBUG = DEBUG
DATABASE_ENGINE = 'postgresql_psycop
g2'
DATABASE_NAME = 'devdb'
DATABASE_USER = ''
DATABASE_PASSWORD = ''
DATABASE_PORT = ''
#
#
...
settings_production.py
from settings import *
DEBUG = TEMPLATE_DEBUG = False
DATABASE_NAME = 'production'
DATABASE_USER = 'app'
DATABASE_PASSWORD = 'letmein'
此处,settings_production.py 从
settings.py 导入所有的设定,仅仅只
是重新定义了产品模式下需要特殊处
理的设置。 在这个案例中,DEBUG
被设置为False,但是我们已经对产品
模式设置了不同的数据库访问参数。
(后者将向你演示你可以重新定义
任何 设置,并不只是象 DEBUG 这样
的基本设置。)
最终,最精简的达到两个配置环境设
定的方案是使用一个配置文件,在此
配置文件中根据不同的环境进行设
置。 一个达到这个目的的方法是检
查当前的主机名。 例如:
#
settings.py
import socket
if socket.gethostname() == 'my-lapto
p':
DEBUG = TEMPLATE_DEBUG = True
else:
DEBUG = TEMPLATE_DEBUG = False
#
...
在这里,我们从python标准库导入了
socket 模块,使用它来检查当前系统
的主机名。 我们可以通过检查主机
名来确认代码是否运行在产品服务器
上。
一个关键是配置文件仅仅是包含
python代码的文件。你可以从其他文
件导入这些python代码,可以通过这
些代码执行任意的逻辑判断等操作。
如果你打算按照这种方案走下去,请
确定这些配置文件中的代码是足够安
全(防弹)的。 如果这个配置文件抛
出任何的异常,Django都有可能会发
生很严重的崩溃。
重命名settings.py
随便将你的settings.py重命名为
settings_dev.py或settings/dev.py或
foobar.py,Django 并不在乎你的配置
文件取什么名字,只要你告诉它你使
用的哪个配置文件就可以了。
但是如果你真的重命名了由django-
admin.py startproject 命令创建的
settings.py文件,你会发现manage.py
会给出一个错误信息说找不到配置文
件。 那是由于它尝试从这个文件中
导入一个叫做settings的模块,你可以
通过修改manage.py 文件,将 import
settings 语句改为导入你自己的模
块,或者使用django-admin.py而不是
使用manage.py,在后一种方式中你需
要设置
DJANGO_SETTINGS_MODULE 环境
变量为你的配置文件所在的python 路
径.(比如’mysite.settings’)。
DJANGO_SETTINGS
_
MODULE
通过这种方式的代码改变后,本章的
下一部分将集中在对具体环境(比如
Apache)的发布所需要的指令上。 这
些指令针对每一种环境都不同,但是
有一件事情是相同的。 在每一种环
境中,你都需要告诉Web服务器你的
DJANGO_SETTINGS_MODULE是什
么,这是你的Django应用程序的进入
点。 DJANGO_SETTINGS_MODULE
指向你的配置文件,在你的配置文件
中指向你的ROOT_URLCONF,在
ROOT_URLCONF中指向了你的视图
以及其他的部分。
DJANGO_SETTINGS_MODULE是你
的配置文件的python的路径 比如,假
设mysi te是在你的Python路径中,
DJANGO_SETTINGS_MODULE对于
我们正在进行的例子就
是’mysite.settings’。
用Apache和
mod_python来部署
Django
服务器上部署Django的最健壮搭配。
mod_python
(http://www.djangoproject.com/r/mod_p
ython/)是一个在Apache中嵌入Python
的Apache插件,它在服务器启动时将
Python代码加载到内存中。 (译注:
Django 需要Apaceh 2.x 和mod_python
3.x支持。
备注
如何配置Apache超出了本书的范
围,因此下面将只简单介绍必要
的细节。 幸运的是,如果需要进
一步学习Apache的相关知识,可
以找到相当多的绝佳资源。 我们
喜欢去的几个地方:
开源的Apache在线文档,位
于 http://www.djangoproject.com
/
r/apache/docs/
Pro Apache,第三版 (Apress,
004),作者Peter Wainwright, 位
2
于
http://www.djangoproject.com/r/
books/pro-apache/
Apache: The Definitive Guide, 第
三版 (OReilly, 2002),作者Ben
Laurie和Peter Laurie, 位于
http://www.djangoproject.com/r/
books/apache-pra/
基本配置
为了配置基于 mod_python 的
Django,首先要安装有可用的
mod_python 模块的 Apache。 这通常
意味着应该有一个 LoadModule 指令
在 Apache 配置文件中。 它看起来就
像是这样:
LoadModule python_module /usr/lib/ap
ache2/modules/mod_python.so
然后,编辑你的Apache配置文件添加
一个重定向,例如:
<
Location "/">
SetHandler python-program
PythonHandler django.core.handle
rs.modpython
SetEnv DJANGO_SETTINGS_MODULE my
site.settings
PythonDebug Off
/Location>
<
要确保
把 DJANGO_SETTINGS_MODULE 中
的 mysite.settings 项目换成与你的站
点相应的内容。
它告诉 Apache,任何在 / 这个路径之
后的 URL都使用 Django 的
mod_python 来处理。 它 将
DJANGO_SETTINGS_MODULE 的值
传递过去,使得 mod_python 知道这
时应该使用哪个配置。
注意这里使用 <Location> 指令而不
是 <Directory> 。 后者用于指向你
的文件系统中的一个位置,然而 指
向一个 We b 站点的 URL位置。
Apache 可能不但会运行在你正常登
录的环境中,也会运行在其它不同的
用户环境中;也可能会有不同的文件
路径或 sys.path。 你需要告诉
mod_python 如何去寻找你的项目及
Django 的位置。
PythonPath "['/path/to/project', '/p
ath/to/django'] + sys.path"
你也可以加入一些其它指令,比
如 PythonAutoReload Off 以提升性
能。 查看 mod_python 文档获得详细
的指令列表。
注意,你应该在成品服务器上设
置 PythonDebug Off 。如果你使
用 PythonDebug On 的话,在程序产生
错误时,你的用户会看到难看的(并
且是暴露的) Python 回溯信息。 如
果你把 PythonDebug 置 On,当
mod_python出现某些错误,你的用户会
看到丑陋的(也会暴露某些信
息)Python的对错误的追踪的信息。
重启 Apache 之后所有对你的站点的
请求(或者是当你用了 指令后则是
虚拟主机)都会由 Djanog 来处理。
在同一个 Apache 的实例中
运行多个 Django 程序
在同一个 Apache 实例中运行多个
Django 程序是完全可能的。 当你是
一个独立的 Web 开发人员并有多个
不同的客户时,你可能会想这么做。
只要像下面这样使用 VirtualHost 你可
以实现:
NameVirtualHost *
<
VirtualHost *>
ServerName www.example.com
#
...
SetEnv DJANGO_SETTINGS_MODULE my
site.settings
<
/VirtualHost>
<
VirtualHost *>
ServerName www2.example.com
#
...
SetEnv DJANGO_SETTINGS_MODULE my
site.other_settings
/VirtualHost>
<
如果你需要在同一个 VirtualHost 中运
行两个 Django 程序,你需要特别留
意一下以 确保 mod_python 的代码缓
存不被弄得乱七八糟。 使
用 PythonInterpreter 指令来将不 同
的 指令分别解释:
<
VirtualHost *>
ServerName www.example.com
#
<
...
Location "/something">
SetEnv DJANGO_SETTINGS_MODUL
E mysite.settings
PythonInterpreter mysite
/Location>
<
<
Location "/otherthing">
SetEnv DJANGO_SETTINGS_MODUL
E mysite.other_settings
PythonInterpreter mysite_oth
er
<
/Location>
<
/VirtualHost>
这个 PythonInterpreter 中的值不重
要,只要它们在两个 Location 块中不
同。
用 mod_python 运行一个开
发服务器
因为 mod_python 缓存预载入了
Python 的代码,当在 mod_python 上
发布 Django 站点时,你每 改动了一
次代码都要需要重启 Apache 一次。
这还真是件麻烦事,所以这有个办法
来避免它: 只要 加入
MaxRequestsPerChild 1 到配置文件中
强制 Apache 在每个请求时都重新载
入所有的 代码。 但是不要在产品服
务器上使用这个指令,这会撤销
Django 的特权。
如果你是一个用分散的 print 语句
(我们就是这样)来调试的程序员,
注意这 print 语 句在 mod_python 中是
无效的;它不会像你希望的那样产生
一个 Apache 日志。 如果你需要在
mod_python 中打印调试信息,可能需
要用到 Python 标准日志包(Pythons
standard logging package)。 更多的
信息请参见
http://docs.python.org/lib/module-
logging.html 。另一个选择是在模板页
面中加入调试信息。
使用相同的Apache实例来
服务Django和Media文件
Django本身不用来服务media文件;
应该把这项工作留给你选择的网络服
务器。 我们推荐使用一个单独的网
络服务器(即没有运行Django的一
个)来服务media。 想了解更多信
息,看下面的章节。
不过,如果你没有其他选择,所以只
能在同Django一样的
Apache VirtualHost 上服务media文
件,这里你可以针对这个站点的特定
部分关闭mod_python:
<
<
Location "/media/">
SetHandler None
/Location>
将 Location 改成你的media文件所处
的根目录。
你也可以使用 来匹配正则表达式。
比如,下面的写法将Django定义到网
站的根目录,并且显式地将 media 子
目录以及任何以 .jpg , .gif , 或
者 .png 结尾的URL屏蔽掉:
<
Location "/">
SetHandler python-program
PythonHandler django.core.handle
rs.modpython
SetEnv DJANGO_SETTINGS_MODULE my
site.settings
<
<
<
<
<
/Location>
Location "/media/">
SetHandler None
/Location>
LocationMatch "\.(jpg|gif|png)$">
SetHandler None
/LocationMatch>
在所有这些例子中,你必须设
置 DocumentRoot ,这样apache才能知
道你存放静态文件的位置。
错误处理
当你使用 Apache/mod_python 时,错
误会被 Django 捕捉,它们不会传播
到 Apache 那里,也不会出现在
Apache 的 错误日志 中。
除非你的 Django 设置的确出了问
题。 在这种情况下,你会在浏览器
上看到一个 内部服务器错误的页
面,并在 Apache 的 错误日志 中看到
Python 的完整回溯信息。 错误日
志 的回溯信息有多行。 当然,这些
信息是难看且难以阅读的。
处理段错误
有时候,Apache会在你安装Django的
时候发生段错误。 这时,基本上 总
是 有以下两个与Django本身无关的原
因其中之一所造成:
有可能是因为,你使用
了 pyexpat 模块(进行XML解析)
并且与Apache内置的版本相冲
突。 详情请见
http://www.djangoproject.com/r/arti
cles/expat-apache-crash/.
也有可能是在同一个Apache进程
中,同时使用了mod_python 和
mod_php,而且都使用MySQL作为
数据库后端。 在有些情况下,这
会造成PHP和Python的MySQL模块
的版本冲突。 在mod_python的
FAQ中有更详细的解释。
如果还有安装mod_python的问题,有
一个好的建议,就是先只运行
mod_python站点,而不使用Django框
架。 这是区分mod_python特定问题的
好方法。 下面的这篇文章给出了更
详细的解
释。http://www.djangoproject.com/r/art
icles/getting-modpython-working/.
下一个步骤应该是编辑一段测试代
码,把你所有django相关代码import
进去,你的
views,models,URLconf,RSS配置,等
等。 把这些imports放进你的handler
函数中,然后从浏览器进入你的
URL。 如果这些导致了crash,你就
可以确定是import的django代码引起
了问题。 逐个去掉这些imports,直
到不再冲突,这样就能找到引起问题
的那个模块。 深入了解各模块,看
看它们的imports。 要想获得更多帮
助,像l i nux的ldconfig,Mac OS的
otool和windows的ListDLLs(form
sysInternals)都可以帮你识别共享依
赖和可能的版本冲突。
一种替代方案: mod_wsgi
模块
作为一个mod_python模块的替代,你
可以考虑使用mod_w sgi模块
(http://code.google.com/p/modwsgi/),此
模块开发的时间比mod_python的开发
时间离现在更近一些,在Django社区
已有一些使用。 一个完整的概述超
出了本书的范围,你可以从官方的
Django文档查看到更多的信息。
使用FastCGI部署
Django应用
尽管将使用Apache和mod_python搭建
Django环境是最具鲁棒性的,但在很
多虚拟主机平台上,往往只能使用
FastCGI
此外,在很多情况下,FastCGI能够
提供比mod_python更为优越的安全性
和效能。 针对小型站点,相对于
Apache来说FastCGI更为轻量级。
FastCGI 简介
如何能够由一个外部的应用程序很有
效解释WEB 服务器上的动态页面请
求呢? 答案就是使用FastCGI! 它的工
作步骤简单的描述起来是这样的:
和mod_python一样,FastCGI也是驻留
在内存里为客户请求返回动态信息 ,
而且也免掉了像传统的CGI一样启动
进程时候的时间花销。 但于
mod_python不同之处是它并不是作为
模块运行在web服务器同一进程内
的,而是有自己的独立进程。
为什么要在一个独立的进程中运行代
码?
在以传统的方式的几种以mod_*方式
嵌入到Apache的脚本语言中(常见的
例如: PHP,Python/mod_python和
Perl/mod_perl),他们都是以apache
扩展模块的方式将自身嵌入到Apache
进程中的。
每一个Apache进程都是一个Apache引
擎的副本,它完全包括了所有Apache
所具有的一切功能特性(哪怕是对
Django毫无好处的东西也一并加载进
来)。 而FastCGI就不一样了,它仅
仅把Python和Django等必备的东东弄
到内存中。
依据FastCGI自身的特点可以看到,
FastCGI进程可以与Web服务器的进程
分别运行在不同的用户权限下。 对
于一个多人共用的系统来说,这个特
性对于安全性是非常有好处的,因为
你可以安全的于别人分享和重用代码
了。
如果你希望你的Django以FastCGI的方
式运行,那么你还必须安装 flup 这个
Python库,这个库就是用于处理
FastCGI的。 很多用户都抱怨 flup 的
发布版太久了,老是不更新。 其实
不是的,他们一直在努力的工作着,
这是没有放出来而已。
运行你的 FastCGI 服务器
FastCGI是以客户机/服务器方式运行
的,并且在很多情况下,你得自己去
启动FastCGI的服务进程。 Web服务
器(例如Apache,lighttpd等等)仅仅
在有动态页面访问请求的时候才会去
与你的Django-FastCGI进程交互。 因
为Fast-CGI已经一直驻留在内存里面
了的,所以它响应起来也是很快的。
记录
在虚拟主机上使用的话,你可能
会被强制的使用We b server-
managed FastCGI进程。 在这样的
情况下,请参阅下面的“在Apache
共享主机里运行Django”这一小
节。
web服务器有两种方式于FastCGI进程
交互: 使用Uni x domain socket(在
win32里面是 命名管道 )或者使用TCP
socket.具体使用哪一个,那就根据你
的偏好而定了,但是TCP socket弄不
好的话往往会发生一些权限上的问
题。 What you choose is a ma nne r of
preference; a TCP socket is usually
easier due to permissions issues.
开始你的服务器项目,首先进入你的
项目目录下(你的 manage.py 文件所
在之处),然后使用
manage.py runfcgi 命令:
.
/manage.py runfcgi [options]
想了解如何使用 runfcgi ,输
入 manage.py runfcgi help 命令。
你可以指定 socket 或者同时指
定 host 和 port 。当你要创建Web服务
器时,你只需要将服务器指向当你在
启动FastCGI服务器时确定的socket或
者host/port。
范例:
在TCP端口上运行一个线程服务
器:
.
/manage.py runfcgi method=threaded
host=127.0.0.1 port=3033
在Uni x socket上运行prefork服务
器:
.
/manage.py runfcgi method=prefork s
ocket=/home/user/mysite.sock pidfile
django.pid
=
启动,但不作为后台进程(在调
试时比较方便):
.
/manage.py runfcgi daemonize=false
socket=/tmp/mysite.sock
停止FastCGI的行程
如果你的FastCGI是在前台运行的,
那么只需按Ctrl+C就可以很方便的停
止这个进程了。 但如果是在后台运
行的话,你就要使用Uni x的 kill 命令
来杀掉它。 然而,当你正在处理后
台进程时,你会需要将其付诸于
Unixkill的命令
如果你在 manage.py runfcgi 中指定
了 pidfile 这个选项,那么你可以这样
来杀死这个FastCGI后台进程:
kill `cat $PIDFILE`
$PIDFILE 就是你在 pidfile 指定的那
个。
你可以使用下面这个脚本方便地重启
Uni x里的FastCGI守护进程:
#
#
!/bin/bash
Replace these three settings.
PROJDIR="/home/user/myproject"
PIDFILE="$PROJDIR/mysite.pid"
SOCKET="$PROJDIR/mysite.sock"
cd $PROJDIR
if [ -f $PIDFILE ]; then
kill `cat -- $PIDFILE`
rm -f -- $PIDFILE
fi
exec /usr/bin/env - PYTHONPATH="..
python:.." ./manage.py runfcgi so
/
cket=$SOCKET pidfile=$PIDFILE
在Apache中以FastCGI的方
式使用Django
在Apache和FastCGI上使用Django,你
需要安装和配置Apache,并且安装
mod_fastcgi。 请参见Apache和
mod_fastcgi文
档: http://www.djangoproject.com/r/m
od_fastcgi/ 。
当完成了安装,通
过 httpd.conf (Apache的配置文件)
来让Apache和Django FastCGI互相通
信。 你需要做两件事:
使用 FastCGIExternalServer 指明
FastCGI的位置。
使用 mod_rewrite 为FastCGI指定
合适的URL。
指定 FastCGI Server 的位
置
FastCGIExternalServer 告诉Apache如
何找到FastCGI服务器。 按照
FastCGIExternalServer 文档
(
http://www.djangoproject.com/r/mod
_
fastcgi/FastCGIExternalServer/ ),
你可以指明 socket 或者 host。以下是
两个例子:
Connect to FastCGI
via a socket/named
pipe:
FastCGIExternalServer
/
home/user/public_html/mysite.fcgi -
socket /home/user/mysite.sock
Connect to FastCGI
via a TCP host/port:
FastCGIExternalServer
/
home/user/public_html/mysite.fcgi -host
1
27.0.0.1:3033
在这两个例子中,
home/user/public_html/ 目录必须存
在,
/
而 /home/user/public_html/mysite.fcgi
文件不一定存在。 它仅仅是一个We b
服务器内部使用的接口,这个URL决
定了对于哪些URL的请求会被FastCGI
处理(下一部分详细讨论)。 (下
一章将会有更多有关于此的介绍)
使用mod_rewri te为FastCGI
指定URL
第二步是告诉Apache为符合一定模式
的URL使用FastCGI。 为了实现这一
点,请使用mod_rewrite 模块,并将
这些URL重定向到 mysite.fcgi (或者
正如在前文中描述的那样,使用任何
在 FastCGIExternalServer 指定的内
容)。
在这个例子里面,我们告诉Apache使
用FastCGI来处理那些在文件系统上
不提供文件(译者注:
<
l
VirtualHost 12.34.56.78>
ServerName example.com
DocumentRoot /home/user/public_htm
Alias /media /home/user/python/dja
ngo/contrib/admin/media
RewriteEngine On
RewriteRule ^/(media.*)$ /$1 [QSA,
L]
RewriteCond %{REQUEST_FILENAME} !-
f
RewriteRule ^/(.*)$ /mysite.fcgi/$
1
<
[QSA,L]
/VirtualHost>
FastCGI 和 lighttpd
lighttpd
(http://www.djangoproject.com/r/lighttp
d/) 是一个轻量级的Web服务器,通常
被用来提供静态页面的访问。 它天
生支持FastCGI,因此除非你的站点
需要一些Apache特有的特性,否则,
lighttpd对于静态和动态页面来说都是
理想的选择。
确保 mod_fastcgi 在模块列表中,它
需要出现
在 mod_rewrite 和 mod_access ,但是
要在 mod_accesslog 之前。
将下面的内容添加到你的lighttpd的配
置文件中:
server.document-root = "/home/user/p
ublic_html"
fastcgi.server = (
"
/mysite.fcgi" => (
main" => (
Use host / port instea
d of socket for TCP fastcgi
"
#
#
"host" => "127.0.0.1",
#
"port" => 3033,
"
mysite.sock",
"
socket" => "/home/user/
check-local" => "disabl
e",
)
)
)
,
alias.url = (
"
/media/" => "/home/user/django/
contrib/admin/media/",
)
url.rewrite-once = (
"
"
^(/media.*)$" => "$1",
^/favicon\.ico$" => "/media/fav
icon.ico",
"
^(/.*)$" => "/mysite.fcgi$1",
)
在一个lighttpd进程中运行
多个Django站点
lighttpd允许你使用条件配置来为每个
站点分别提供设置。 为了支持
FastCGI的多站点,只需要在FastCGI
的配置文件中,为每个站点分别建立
条件配置项:
#
If the hostname is 'www.example1.c
om'...
$
{
HTTP["host"] == "www.example1.com"
server.document-root = "/foo/sit
e1"
fastcgi.server = (
..
.
)
.
..
}
#
If the hostname is 'www.example2.c
om'...
$
{
HTTP["host"] == "www.example2.com"
server.document-root = "/foo/sit
e2"
fastcgi.server = (
.
..
)
.
..
}
你也可以通过 fastcgi.server 中指定多
个入口,在同一个站点上实现多个
Django安装。 请为每一个安装指定一
个FastCGI主机。
在使用Apache的共享主机
服务商处运行Django
许多共享主机的服务提供商不允许运
行你自己的服务进程,也不允许修
改 httpd.conf 文件。 尽管如此,仍然
有可能通过Web服务器产生的子进程
来运行Django 。
记录
如果你要使用服务器的子进程,
你没有必要自己去启动FastCGI服
务器。 Apache会自动产生一些子
进程,产生的数量按照需求和配
置会有所不同。
在你的Web根目录下,将下面的内容
增加到 .htaccess 文件中:
AddHandler fastcgi-script .fcgi
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ mysite.fcgi/$1 [Q
SA,L]
接着,创建一个脚本,告知Apache如
何运行你的FastCGI程序。 创建一
个 mysite.fcgi 文件,并把它放在你的
Web目录中,打开可执行权限。
#
!/usr/bin/python
import sys, os
#
Add a custom Python path.
sys.path.insert(0, "/home/user/pytho
n")
#
Switch to the directory of your pr
oject. (Optional.)
#
os.chdir("/home/user/myproject")
Set the DJANGO_SETTINGS_MODULE env
#
ironment variable.
os.environ['DJANGO_SETTINGS_MODULE']
=
"myproject.settings"
from django.core.servers.fastcgi imp
ort runfastcgi
runfastcgi(method="threaded", daemon
ize="false")
重启新产生的进程服务器
如果你改变了站点上任何的python代
码,你需要告知FastCGI。 但是,这
不需要重启Apache,而只需要重新上
传 mysite.fcgi 或者编辑改文件,使得
修改时间发生了变化,它会自动帮你
重启Django应用。 你可以重新上传
mysite.fcgi或者编辑这个文件以改变
该文件的时间戳。 当阿帕奇服务器
发现文档被更新了,它将会为你重启
你的Django应用。
如果你拥有Uni x系统命令行的可执行
权限,只需要简单地使用 touch 命
令:
touch mysite.fcgi
可扩展性
既然你已经知道如何在一台服务器上
运行Django,让我们来研究一下,如
何扩展我们的Django安装。 这一部分
我们将讨论,如何把一台服务器扩展
为一个大规模的服务器集群,这样就
能满足每小时上百万的点击率。
有一点很重要,每一个大型的站点大
的形式和规模不同,因此可扩展性其
实并不是一种千篇一律的行为。 以
下部分会涉及到一些通用的原则,并
且会指出一些不同选择。
首先,我们来做一个大的假设,只集
中地讨论在Apache和mod_python下的
可扩展性问题。 尽管我们也知道一
些成功的中型和大型的FastCGI策
略,但是我们更加熟悉Apache。
运行在一台单机服务器上
大多数的站点一开始都运行在单机服
务器上,看起来像图20-1这样的构
架。
图 20-1: 一个单服务器的Django安
装。
这对于小型和中型的站点来说还不
错,并且也很便宜,一般来说,你可
以在3000美元以下就搞定一切。
然而,当流量增加的时候,你会迅速
陷入不同软件的 资源争夺 之中。 数
据库服务器和Web服务器都 喜欢 自
己拥有整个服务器资源,因此当被安
装在单机上时,它们总会争夺相同的
资源(RAM, CPU),它们更愿意独
享资源。
通过把数据库服务器搬移到第二台主
机上,可以很容易地解决这个问题。
分离出数据库服务器
对于Django来说,把数据库服务器分
离开来很容易: 只需要简单地修
改 DATABASE_HOST ,设置为新的
数据库服务器的IP地址或者DNS域
名。 设置为IP地址总是一个好主意,
因为使用DNS域名,还要牵涉到DNS
服务器的可靠性连接问题。
使用了一个独立的数据库服务器以
后,我们的构架变成了图20-2。
图 20-2: 将数据库移到单独的服务
器上。
这里,我们开始步入 n-tier 构架。 不
要被这个词所吓坏,它只是说明了
Web栈的不同部分,被分离到了不同
的物理机器上。
我们再来看,如果发现需要不止一台
的数据库服务器,考虑使用连接池和
数据库备份将是一个好主意。 不幸
的是,本书没有足够的时间来讨论这
个问题,所以你参考数据库文档或者
向社区求助。
运行一个独立的媒体服务
器
使用单机服务器仍然留下了一个大问
题: 处理动态内容的媒体资源,也
是在同一台机器上完成的。
这两个活动是在不同的条件下进行
的,因此把它们强行凑和在同一台机
器上,你不可能获得很好的性能。
下一步,我们要把媒体资源(任
何 不是 由Django视图产生的东西)
分离到别的服务器上(请看图20-
3
)。
图 20-3: 分离出媒体服务器。
理想的情况是,这个媒体服务器是一
个定制的Web服务器,为传送静态媒
体资源做了优化。 lighttpd和tux
(http://www.djangoproject.com/r/tux/)
都是极佳的选择,当然瘦身的Apache
服务器也可以工作的很好。
对于拥有大量静态内容(照片、视频
等)的站点来说,将媒体服务器分离
出去显然有着更加重要的意义,而且
应该是扩大规模的时候所要采取
的 第一步措施 。
这一步需要一点点技巧,Django的
admi n管理接口需要能够获得足够的
权限来处理上传的媒体(通过设置
MEDIA_ROOT )。如果媒体资源在
另外的一台服务器上,你需要获得通
过网络写操作的权限。 如果你的应
用牵涉到文件上载,Django需要能够
面向媒体服务器撰写上载媒体 如果
媒体是在另外一台服务器上的,你需
要部署一种方法使得Django可以通过
网络去写这些媒体。
实现负担均衡和数据冗余
备份
现在,我们已经尽可能地进行了分
解。 这种三台服务器的构架可以承
受很大的流量,比如每天1000万的点
击率。
这是个好主意。 请看图 20-3,一旦
三个服务器中的任何一个发生了故
障,你就得关闭整个站点。 因此在
引入冗余备份的时候,你并不只是增
加了容量,同时也增加了可靠性。
我们首先来考虑Web服务器的点击
量。 把同一个Django的站点复制多
份,在多台机器上同时运行很容易,
我们也只需要同时运行多台机器上的
Apache服务器。
你还需要另一个软件来帮助你在多台
服务器之间均衡网络流量: 流量均
衡器(load balancer) 。你可以购买
昂贵的专有的硬件均衡器,当然也有
一些高质量的开源的软件均衡器可供
选择。
Apaches 的 mod_proxy 是一个可以考
虑的选择,但另一个配置更棒的选择
是: memcached是同一个团队的人写
的一个负载均衡和反向代理的程序 .
(见第15章)
记录
如果你使用FastCGI,你同样可以
分离前台的web服务器,并在多台
其他机器上运行FastCGI服务器来
实现相同的负载均衡的功能。 前
台的服务器就相当于是一个均衡
器,而后台的FastCGI服务进程代
替了Apache/mod_python/Django 服
务器。
现在我们拥有了服务器集群,我们的
构架慢慢演化,越来越复杂,如图
2
0-4。
图 20-4: 负载均衡的服务器设置。
值得一提的是,在图中,Web服务器
指的是一个集群,来表示许多数量的
服务器。 一旦你拥有了一个前台的
均衡器,你就可以很方便地增加和删
除后台的Web服务器,而且不会造成
任何网站不可用的时间。
慢慢变大
下面的这些步骤都是上面最后一个的
变体:
当你需要更好的数据库性能,你
可能需要增加数据库的冗余服务
器。 MySQL内置了备份功能;
PostgreSQL应该看一下Slony
(http://www.djangoproject.com/r/slo
ny/) 和 pgpool
(http://www.djangoproject.com/r/pg
pool/) ,这两个分别是数据库备份
和连接池的工具。
如果单个均衡器不能达到要求,
你可以增加更多的均衡器,并且
使用轮训(round-robin)DNS来实
现分布访问。
如果单台媒体服务器不够用,你
可以增加更多的媒体服务器,并
通过集群来分布流量。
如果你需要更多的高速缓存
(
cache),你可以增加cache服务
器。
在任何情况下,只要集群工作性
能不好,你都可以往上增加服务
器。
重复了几次以后,一个大规模的构架
会像图20-5 。
图 20-5。 大规模的Django安装。
尽管我们只是在每一层上展示了两到
三台服务器,你可以在上面随意地增
加更多。
性能优化
如果你有大笔大笔的钱,遇到扩展性
问题时,你可以简单地投资硬件。
对于剩下的人来说,性能优化就是必
须要做的一件事。
注意
顺便提一句,谁要是有大笔大笔
的钞票,请捐助一点Django项目。
我们也接受未切割的钻石和金
币。
不幸的是,性能优化比起科学来说更
像是一种艺术,并且这比扩展性更难
描述。 如果你真想要构建一个大规
模的Django应用,你需要花大量的时
间和精力学习如何优化构架中的每一
部分。
以下部分总结了多年以来的经验,是
一些专属于Django的优化技巧。
RAM怎么也不嫌多
最近即使那些昂贵的RAM也相对来
说可以负担的起了。 购买尽可能多
的RAM,再在别的上面投资一点
点。
高速的处理器并不会大幅度地提高性
能;大多数的Web服务器90%的时间
都浪费在了硬盘IO上。 当硬盘上的
数据开始交换,性能就急剧下降。
更快速的硬盘可以改善这个问题,但
是比起RAM来说,那太贵了。
如果你拥有多台服务器,首要的是要
在数据库服务器上增加内存。 如果
你能负担得起,把你整个数据库都放
入到内存中。 这应该不是很困难,
我们已经开发过一个站点上面有多于
一百万条报刊文章,这个站点使用了
不到2GB的空间。
下一步,最大化Web服务器上的内
存。 最理想的情况是,没有一台服
务器进行磁盘交换。 如果你达到了
这个水平,你就能应付大多数正常的
流量。
禁用 Keep-Alive
Keep-Alive 是HTTP提供的功能之
一,它的目的是允许多个HTTP请求
复用一个TCP连接,也就是允许在同
一个TCP连接上发起多个HTTP请
求,这样有效的避免了每个HTTP请
求都重新建立自己的TCP连接的开
销。
这一眼看上去是好事,但它足以杀死
Django站点的性能。 如果你从单独的
媒体服务器上向用户提供服务,每个
光顾你站点的用户都大约10秒钟左右
发出一次请求。 这就使得HTTP服务
器一直在等待下一次keep-alive 的请
求,空闲的HTTP服务器和工作时消
耗一样多的内存。
使用 memcached
尽管Django支持多种不同的cache后台
机制,没有一种的性能可以 接
近 memcached。 如果你有一个高流量
的站点,不要犹豫,直接选择
memcached。
经常使用memcached
当然,选择了memcached而不去使用
它,你不会从中获得任何性能上的提
升。 Chapter 15 is your best friend
here: 学习如何使用Django的cache框
架,并且尽可能地使用它。 大量的
可抢占式的高速缓存通常是一个站点
在大流量下正常工作的唯一瓶颈。
参加讨论
Django相关的每一个部分,从Linux到
Apache到PostgreSQL或者MySQL背
后,都有一个非常棒的社区支持。
如果你真想从你的服务器上榨干最后
1
%的性能,加入开源社区寻求帮
助。 多数的自由软件社区成员都会
很乐意地提供帮助。
别忘了Django社区。 这本书谦逊的作
者只是Django开发团队中的两位成
员。 我们的社区有大量的经验可以
提供。
下一章
下面的章节集中在其他的一些Django
特性上,你是否需要它们取决于你的
应用项目。 可以自由选择阅读。
通常当我们谈到开发网站时,主要谈
论的是HTML。 当然,Web远不只有
HTML,我们在Web上用多种格式来
发布数据: RSS、PDF、图片等。
到目前为止,我们的注意力都是放在
常见 HTML代码生成上,但是在这一
章中,我们将会对使用 Django 生成
其它格式的内容进行简要介绍。
Django拥有一些便利的内建工具帮助
你生成常见的非HTML内容:
RSS/Atom 聚合文件
站点地图 (一个XML格式文件,
最初由Google开发,用于给搜索
引擎提示线索)
我们稍后会逐一研究这些工具,不过
首先让我们来了解些基础原理。
基础: 视图和MIME
类型
回顾一下第三章,视图函数只是一个
以Web请求为参数并返回Web响应的
Python函数。 这个响应可以是一个
Web页面的HTML内容,或者一个跳
转,或者一个404 错误,或者一个
XML文档,或者一幅图片,或者映射
到任何东西上。
更正式的说,一个Django视图函数 必
须
接受一个 HttpRequest 实例作为它
的第一个参数
返回一个 HttpResponse 实例
从一个视图返回一个非 HTML内容的
关键是在构造一个 HttpResponse 类
时,需要指定 mi metype 参数。 通过
改变 MIME 类型,我们可以通知浏览
器将要返回的数据是另一种类型。
下面我们以返回一张PNG图片的视图
为例。 为了使事情能尽可能的简
单,我们只是读入一张存储在磁盘上
的图片:
from django.http import HttpResponse
def my_image(request):
image_data = open("/path/to/my/i
mage.png", "rb").read()
return HttpResponse(image_data,
mimetype="image/png")
就是这么简单。 如果改变 open() 中
的图片路径为一张真实图片的路径,
那么就可以使用这个十分简单的视图
来提供一张图片,并且浏览器可以正
确显示它。
另外我们必须了解的是HttpResponse
对象实现了Python标准的文件应用程
序接口(API)。 这就是说你可以在
Python(或第三方库)任何用到文件
的地方使用”HttpResponse”实例。
下面将用 Django 生成 CSV 文件为
例,说明它的工作原理。
生成 CSV 文件
CSV 是一种简单的数据格式,通常为
电子表格软件所使用。 它主要是由
一系列的表格行组成,每行中单元格
之间使用逗号(CSV 是 逗号分隔数值
(comma-separated values) 的缩写)隔
开。例如,下面是CSV格式的“不守
规矩”的飞机乘客表。
Year,Unruly Airline Passengers
1
1
995,146
996,184
1
1
1
2
2
2
2
2
2
2
2
997,235
998,200
999,226
000,251
001,299
002,273
003,281
004,304
005,203
006,134
007,147
备注
前面的列表包含真实数据。 这些
数据来自美国 联邦航空管理局。
CSV格式尽管看起来简单,却是全球
通用的。 但是不同的软件会生成和
使用不同的 CSV 的变种,在使用上
会有一些不便。 幸运的是, Python
使用的是标准 CSV 库, csv ,所以
它更通用。
因为 csv 模块操作的是类似文件的对
象,所以可以使用 HttpResponse 替
换:
import csv
from django.http import HttpResponse
#
Number of unruly passengers each y
ear 1995 - 2005\. In a real applicat
ion
#
this would likely come from a data
base or some other back-end data sto
re.
UNRULY_PASSENGERS = [146,184,235,200
,226,251,299,273,281,304,203]
def unruly_passengers_csv(request):
Create the HttpResponse object
#
with the appropriate CSV header.
response = HttpResponse(mimetype
'text/csv')
response['Content-Disposition']
'attachment; filename=unruly.csv'
=
=
#
Create the CSV writer using th
e HttpResponse as the "file."
writer = csv.writer(response)
writer.writerow(['Year', 'Unruly
Airline Passengers'])
for (year, num) in zip(range(199
5
, 2006), UNRULY_PASSENGERS):
writer.writerow([year, num])
return response
代码和注释可以说是很清楚,但还有
一些事情需要特别注意:
响应返回的是 text/csv MIME类型
(而非默认的 text/html )。这会告
诉浏览器,返回的文档是CSV文
件。
响应会有一个附加的 Content-
Disposition 头部,它包含有CSV文
件的文件名。 这个头部(或者
说,附加部分)会指示浏览器弹
出对话框询问文件存放的位置
(而不仅仅是显示)。 这个文件
名是任意的。 它会显示在浏览器
的另存为对话框中。
要在HttpResponse指定头部信息,
只需把HttpResponse当做字典使用
就可以了。
与创建CSV的应用程序界面
(
API)挂接是很容易的: 只需
将 response 作为第一个变量传递
给 csv.writer。 csv.writer 函数需要
一个文件类的对
象, HttpResponse 正好能达成这
个目的。
调用 writer.writerow ,并且传递给
它一个类似 list 或者 tuple 的可迭
代对象,就可以在 CSV 文件中写
入一行。
CSV 模块考虑到了引用的问题,
所以您不用担心逸出字符串中引
号和逗号。 只要把信息传递
给 writerow(),它会处理好所有的
事情。
在任何需要返回非 HTML内容的时
候,都需要经过以下几步: 创建一
个 HttpResponse 响应对象(需要指定
特殊的 MIME 类型),它它传给需要
处理文件的函数,然后返回这个响应
对象。
下面是一些其它的例子。
生成 PDF 文件
便携文档格式 (PDF) 是由 Adobe 开发
的格式,主要用于呈现可打印的文
档,其中包含有 pixel-perfect 格式,
嵌入字体以及2D矢量图像。 Yo u can
think of a PDF document as the digital
equivalent of a printed document;
indeed, PDFs are often used in
distributing documents for the purpose of
printing them.
可以方便的使用 Python 和 Django 生
成 PDF 文档需要归功于一个出色的
开源库, ReportLab
(http://www.reportlab.org/rl_toolkit.html
)
。动态生成 PDF 文件的好处是在不
同的情况下,如不同的用户或者不同
的内容,可以按需生成不同的 PDF
文件。 The advantage of generating
PDF files dynamically is that you can
create customized PDFs for different
purposes say, for different users or
different pieces of content.
下面的例子是使用 Django 和
ReportLab 在 KUSports.com 上生成个
性化的可打印的 NCAA 赛程表
(tournament brackets) 。
安装 ReportLab
在生成 PDF 文件之前,需要安装
ReportLab 库。这通常是个很简单的
过程: Its usually simple: just
download and install the library
from http://www.reportlab.org/downloa
ds.html .
Note
如果使用的是一些新的 Linux 发行
版,则在安装前可以先检查包管
理软件。 多数软件包仓库中都加
入了 ReportLab 。
比如,如果使用(杰出的) Ubuntu
发行版,只需要简单的 apt-
get install python-reportlab 一行命令即
可完成安装。
使用手册(原始的只有 PDF 格式)
可以
从 http://www.reportlab.org/rsrc/usergu
ide.pdf 下载,其中包含有一些其它的
安装指南。
在 Python 交互环境中导入这个软件包
以检查安装是否成功。
>
>> import reportlab
如果刚才那条命令没有出现任何错
误,则表明安装成功。
编写视图
和 CSV 类似,由 Django 动态生成
PDF 文件很简单,因为 ReportLab
API 同样可以使用类似文件对象。
下面是一个 Hello World 的示例:
from reportlab.pdfgen import canvas
from django.http import HttpResponse
def hello_pdf(request):
#
Create the HttpResponse object
with the appropriate PDF headers.
response = HttpResponse(mimetype
=
=
'application/pdf')
response['Content-Disposition']
'attachment; filename=hello.pdf'
#
Create the PDF object, using t
he response object as its "file."
p = canvas.Canvas(response)
#
Draw things on the PDF. Here's
where the PDF generation happens.
See the ReportLab documentatio
#
n for the full list of functionality
.
p.drawString(100, 100, "Hello wo
rld.")
#
Close the PDF object cleanly,
and we're done.
p.showPage()
p.save()
return response
需要注意以下几点:
这里我们使用的 MIME 类型
是 application/pdf 。这会告诉浏览
器这个文档是一个 PDF 文档,而
不是 HTML文档。 如果忽略了这
个参数,浏览器可能会把这个文
件看成 HTML文档,这会使浏览
器的窗口中出现很奇怪的文字。
If you leave off this information,
browsers will probably interpret the
response as HTML, which will
result in scary gobbledygook in the
browser window.
使用 ReportLab 的 API 很简单:
只需要将 response 对象作
为 canvas.Canvas 的第一个参数传
入。
所有后续的 PDF 生成方法需要由
PDF 对象调用(在本例中
是 p ),而不是 response 对象。
最后需要对 PDF 文件调
用 showPage() 和 save() 方法(否
则你会得到一个损坏的 PDF 文
件)。
复杂的 PDF 文件
如果您在创建一个复杂的 PDF 文档
(或者任何较大的数据块),请使
用 cStringIO 库存放临时生成的 PDF
文件。 cStringIO 提供了一个用 C 编
写的类似文件对象的接口,从而可以
使系统的效率最高。
下面是使用 cStringIO 重写的 Hello
World 例子:
from cStringIO import StringIO
from reportlab.pdfgen import canvas
from django.http import HttpResponse
def hello_pdf(request):
#
Create the HttpResponse object
with the appropriate PDF headers.
response = HttpResponse(mimetype
'application/pdf')
response['Content-Disposition']
'attachment; filename=hello.pdf'
=
=
temp = StringIO()
#
Create the PDF object, using t
he StringIO object as its "file."
p = canvas.Canvas(temp)
#
Draw things on the PDF. Here's
where the PDF generation happens.
See the ReportLab documentatio
#
n for the full list of functionality
.
p.drawString(100, 100, "Hello wo
rld.")
#
Close the PDF object cleanly.
p.showPage()
p.save()
#
Get the value of the StringIO
buffer and write it to the response.
response.write(temp.getvalue())
return response
其它的可能性
使用 Python 可以生成许多其它类型的
内容,下面介绍的是一些其它的想法
和一些可以用以实现它们的库。
Here are a few more ideas and some
pointers to libraries you could use to
i mpl ement them:
ZIP 文件 :Python 标准库中包含
有 zipfile 模块,它可以读和写压
缩的 ZIP 文件。 它可以用于按需
生成一些文件的压缩包,或者在
需要时压缩大的文档。 如果是
TA R 文件则可以使用标准
库 tarfile 模块。
动态图片 : Python 图片处理库
(PIL; http://www.pythonware.com/pr
oducts/pil/) 是极好的生成图片
(PNG, JPEG, GIF 以及其它许多格
式)的工具。 它可以用于自动为图
片生成缩略图,将多张图片压缩
到单独的框架中,或者是做基于
Web 的图片处理。
图表 : Python 有许多出色并且强
大的图表库用以绘制图表,按需
地图,表格等。 我们不可能将它
们全部列出,所以下面列出的是
个中的翘楚。
matplotlib (http://matplotlib.sourc
eforge.net/) 可以用于生成通常
是由 matl ab 或者 Mathematica
生成的高质量图表。
pygraphviz (https://networkx.lanl.
gov/wiki/pygraphviz) 是一个
Graphviz 图形布局的工具
(http://graphviz.org/) 的 Python
接口,可以用于生成结构化的
图表和网络。
总之,所有可以写文件的库都可以与
Django 同时使用。 The possibilities
are i mmense.
我们已经了解了生成“非HTML”内容
的基本知识,让我们进一步总结一
下。 Django拥有很多用以生成各
类“非HTML”内容的内置工具。
内容聚合器应用框架
Django带来了一个高级的聚合生成框
架,它使得创建RSS和Atom feeds变
得非常容易。
什么是RSS? 什么是Atom?
RSS和Atom都是基于XML的格
式,你可以用它来提供有关你站
点内容的自动更新的feed。 了解更
多关于RSS的可以访
问 http://www.whatisrss.com/, 更多
Atom的信息可以访
问 http://www.atomenabled.org/.
想创建一个联合供稿的源(syndication
feed),所需要做的只是写一个简短的
python类。 你可以创建任意多的源
(feed)。
高级feed生成框架是一个默认绑定
到/feeds/的视图,Django使用URL的
其它部分(在/feeds/之后的任何东西)
来决定输出 哪个feed Django uses the
remainder of the URL (everything
after /feeds/ ) to determine which feed to
return.
要创建一个 sitemap,你只需要写一
个 Sitemap 类然后配置你的URLconf
指向它。
初始化
为了在您的Django站点中激活
syndication feeds, 添加如下的
URLconf:
(r'^feeds/(?P<url>.*)/$', 'django.co
ntrib.syndication.views.feed',
{
'feed_dict': feeds}
)
,
这一行告诉Django使用RSS框架处理
所有的以 "feeds/" 开头的URL. ( 你可
以修改 "feeds/" 前缀以满足您自己的
要求. )
URLConf里有一行参
数: {'feed_dict': feeds},这个参数可
以把对应URL需要发布的feed内容传
递给 syndication framework
特别的,feed_dict应该是一个映射
feed的slug(简短URL标签)到它的Feed
类的字典 你可以在URL配置本身里定
义feed_dict,这里是一个完整的例子
You can define the feed_dict in the
URLconf itself. Here’s a full exampl e
URLconf:
from django.conf.urls.defaults impor
t *
from mysite.feeds import LatestEntri
es, LatestEntriesByCategory
feeds = {
'
'
latest': LatestEntries,
categories': LatestEntriesByCat
egory,
}
urlpatterns = patterns('',
#
...
(r'^feeds/(?P<url>.*)/$', 'djang
o.contrib.syndication.views.feed',
{
'feed_dict': feeds}),
#
...
)
前面的例子注册了两个feed:
LatestEntries 表示的内容将对应到
feeds/latest/ .
LatestEntriesByCategory
的内容将对应到 feeds/categories/ .
以上的设定完成之后,接下来需要自
己定义 Feed 类
一个 Feed 类是一个简单的python类,
用来表示一个syndication feed. 一个
feed可能是简单的 (例如一个站点新
闻feed,或者最基本的,显示一个
blog的最新条目),也可能更加复杂
(例如一个显示blog某一类别下所有条
目的feed。 这里类别 category 是个变
量).
Feed类必须继承
django.contrib.syndication.feeds.Feed,
它们可以在你的代码树的任何位置
一个简单的Feed
This simple example describes a feed
of the latest five blog entries for
a given blog:
from django.contrib.syndication.feed
s import Feed
from mysite.blog.models import Entry
class LatestEntries(Feed):
title = "My Blog"
link = "/archive/"
description = "The latest news a
bout stuff."
def items(self):
return Entry.objects.order_b
y('-pub_date')[:5]
要注意的重要的事情如下所示 :
子
类 django.contrib.syndication.feeds.
Feed .
title , link , 和 description 对应一个
标准 RSS 里的 , , 和 标签.
items() 是一个方法,返回一个用
以包含在包含在feed的 元素里的
list 虽然例子里用Djangos database
API返回的 NewsItem 对
象, items() 不一定必须返回 model
的实例 Although this exampl e
returns Entry objects usi ng Django’s
database API, items() doesn’t have
to return model instances.
还有一个步骤,在一个RSS feed里,
每个(item)有一个(title),(link)和
(description),我们需要告诉框架 把
数据放到这些元素中 In an RSS feed,
each has a , , and . We need to tell the
framework what data to put into those
elements.
如果要指定 和 ,可以建立一个
Django模板(见Chapter 4)名字叫
feeds/latest_title.html 和 feeds/latest
_
description.html ,后者是URLConf
里为对应feed指定的slug 。注
意 .html 后缀是必须的。 Note that
the .html extension is required.
RSS系统模板渲染每一个条目,需
要给传递2个参数给模板上下文变
量:
obj : 当前对象 ( 返回
到 items() 任意对象之一 )。
site : 一个表示当前站点
的 django.models.core.sites.Site
对象。 这对
于 {{ site.domain }} 或者
{
{ site.name }} 很有用。
如果你在创建模板的时候,没有
指明标题或者描述信息,框架会
默认使用 "{{ obj }}" ,对象的字
符串表示。 (For model objects, this
will be the unicode() method.
你也可以通过修改 Feed 类中的两
个属
性 title_template 和 description_tem
plate 来改变这两个模板的名字。
你有两种方法来指定 的内容。
Django 首先执行 items() 中每一项
的 get_absolute_url() 方法。 如果
该方法不存在,就会尝试执
行 Feed 类中的 item_link() 方法,
并将自身作为 item 参数传递进
去。
get_absolute_url() 和 item_link() 都
应该以Python字符串形式返回
URL。
对于前面提到的 LatestEntries 例
子,我们可以实现一个简单的feed
模板。 latest_title.html 包括:
{
{ obj.title }}
并且 latest_description.html 包含:
{
{ obj.description }}
这真是 太 简单了!
一个更复杂的Feed
框架通过参数支持更加复杂的feeds。
For exampl e, say your blog offers an
RSS feed for every distinct tag you’ve
used to categorize your entries. 如果为
每一个单独的区域建立一个 Feed 类
就显得很不明智。
取而代之的方法是,使用聚合框架来
产生一个通用的源,使其可以根据
feeds URL返回相应的信息。
Yo ur tag-specific feeds could use URLs
like this:
http://example.com/feeds/tags/python/
:
Returns recent entries tagged with
python
http://example.com/feeds/tags/cats/ :
Returns recent entries tagged with
cats
固定的那一部分是 "beats" (区
域)。
举个例子会澄清一切。 下面是每个
地区特定的feeds:
from django.core.exceptions import O
bjectDoesNotExist
from mysite.blog.models import Entry
,
Tag
class TagFeed(Feed):
def get_object(self, bits):
#
In case of "/feeds/tags/ca
ts/dogs/mice/", or other such
#
clutter, check that bits h
as only one member.
if len(bits) != 1:
raise ObjectDoesNotExist
return Tag.objects.get(tag=b
its[0])
def title(self, obj):
return "My Blog: Entries tag
ged with %s" % obj.tag
def link(self, obj):
return obj.get_absolute_url(
)
def description(self, obj):
return "Entries tagged with
%
s" % obj.tag
def items(self, obj):
entries = Entry.objects.filt
er(tags__id__exact=obj.id)
return entries.order_by('-pu
b_date')[:30]
以下是RSS框架的基本算法,我们假
设通过URL /rss/beats/0613/ 来访问这
个类:
框架获得了URL /rss/beats/0613/ 并
且注意到URL中的sl ug部分后面含
有更多的信息。 它将斜杠("/" )作
为分隔符,把剩余的字符串分割
开作为参数,调用 Feed 类
的 get_object() 方法。
在这个例子中,添加的信息
是 ['0613'] 。对
于 /rss/beats/0613/foo/bar/ 的一个
URL请求, 这些信息就
是 ['0613', 'foo', 'bar'] 。
get_object() 就根据给定的 bits 值
来返回区域信息。
In this case, it uses the Django
database API to retrieve the Ta g .
Note that get_object() should
raisedjango.core.exceptions.ObjectD
oesNotExist if given invalid
parameters. 在 Beat.objects.get() 调
用中也没有出现 try /except 代码
块。 函数在出错时抛
出 Beat.DoesNotExist 异常,
而 Beat.DoesNotExist
是 ObjectDoesNotExist 异常的一个
子类型。
为产生 , , 和 的feeds, Django
使用 title() , link() , 和
description() 方法。 在上面的例子
中,它们都是简单的字符串类型
的类属性,而这个例子表明,它
们既可以是字符串, 也可以是 方
法。 对于每一
个 title , link 和 description 的组
合,Django使用以下的算法:
1
. 试图调用一个函数,并且
以 get_object() 返回的对象作为
参数传递给 obj 参数。
2
3
. 如果没有成功,则不带参数调
用一个方法。
. 还不成功,则使用类属性。
最后,值得注意的是,这个例子
中的 items() 使用 obj 参数。 对
于 i tems 的算法就如同上面第一步
所描述的那样,首先尝
试 items(obj) , 然后是 items() ,
最后是 i tems 类属性(必须是一个
列表)。
Feed 类所有方法和属性的完整文
档,请参考官方的Django文档
(http://www.djangoproject.com/docume
ntation/0.96/syndication_feeds/) 。
指定Feed的类型
默认情况下, 聚合框架生成RSS 2.0.
要改变这样的情况, 在 Feed 类中添加
一个 feed_type 属性. To change that,
add a feed_type attribute to
your Feed class:
from django.utils.feedgenerator impo
rt Atom1Feed
class MyFeed(Feed):
feed_type = Atom1Feed
注意你把 feed_type 赋值成一个类对
象,而不是类实例。 目前合法的
Feed类型如表11-1所示。
表 11-1. Feed 类型
django.utils.feedgenerator.Rss201rev2F
django.utils.feedgenerator.RssUserland0
django.utils.feedgenerator.Atom1Feed
闭包
为了指定闭包(例如,与feed项比方
说MP3 feeds相关联的媒体资源信
息),使用 item_enclosure_url ,
item_enclosure_length , 以
及 item_enclosure_mime_type ,比如
from myproject.models import Song
class MyFeedWithEnclosures(Feed):
title = "Example feed with enclo
sures"
link = "/feeds/example-with-encl
osures/"
def items(self):
return Song.objects.all()[:3
0
]
def item_enclosure_url(self, ite
return item.song_url
m):
def item_enclosure_length(self,
item):
return item.song_length
item_enclosure_mime_type = "audi
o/mpeg"
当然,你首先要创建一个包含
有 song_url 和 song_length (比如按照
字节计算的长度)域的 Song 对象。
语言
聚合框架自动创建的Feed包含适当
的 标签(RSS 2.0) 或 xml:lang 属性
(Atom). 他直接来自于您的
LANGUAGE_CODE 设置. This comes
directly from
your LANGUAGE_CODE setting.
URLs
link 方法/属性可以以绝对URL的形式
(
例如, "/blog/" )或者指定协议和
域名的URL的形式返回(例
如"http://www.example.com/blog/" )
。
如果 link 没有返回域名,聚合框架
会根据 SITE_ID 设置,自动的插入当
前站点的域信息。 (See Chapter 16 for
more on SITE_ID and the sites
framework.)
Atom feeds需要 rel="self"> 指明
feeds现在的位置。 The syndication
framework populates this automatically.
同时发布Atom and RSS
一些开发人员想 同时 支持Atom和
RSS。 这在Django中很容易实现: 只
需创建一个你的 feed 类的子类,然
后修改 feed_type ,并且更新URLconf
内容。 下面是一个完整的例子:
Here’s a full exampl e:
from django.contrib.syndication.feed
s import Feed
from django.utils.feedgenerator impo
rt Atom1Feed
from mysite.blog.models import Entry
class RssLatestEntries(Feed):
title = "My Blog"
link = "/archive/"
description = "The latest news a
bout stuff."
def items(self):
return Entry.objects.order_b
y('-pub_date')[:5]
class AtomLatestEntries(RssLatestEnt
ries):
feed_type = Atom1Feed
这是与之相对应那个的URLconf:
from django.conf.urls.defaults import *
from myproject.feeds import
RssLatestEntries, AtomLatestEntries
feeds = {
'
'
rss': RssLatestEntries,
atom' : AtomLatestEntries,
}
urlpatterns = patterns('',
...
(r'^feeds/(?P.*)//pre>, 'django.cont
rib.syndication.views.feed',
{
'feed_dict': feeds}),
#
...
)
Sitemap 框架
sitemap 是你服务器上的一个XML文
件,它告诉搜索引擎你的页面的更新
频率和某些页面相对于其它页面的重
要性。 这个信息会帮助搜索引擎索
引你的网站。
例如,这是 Django 网站
(http://www.djangoproject.com/sitemap.
xml)sitemap的一部分:
http://www.djangoproject.com/documen
tation/
weekly
0
.5
http://www.djangoproject.com/documen
tation/0_90/
never
0
.1
.
..
需要了解更多有关 sitemaps 的信息,
Django sitemap 框架允许你用 Python
代码来表述这些信息,从而自动创建
这个XML文件。 要创建一个站点地
图,你只需要写一个 Sitemap 类,
并且在URLconf中指向它。
安装
要安装 sitemap 应用程序, 按下面的步
骤进行 :
1
. 将 'django.contrib.sitemaps' 添加到
您的 INSTALLED_APPS 设置中.
2
. 确
保 'django.template.loaders.app_dire
ctories.load_template_source' 在您
的 TEMPLATE_LOADERS 设置
中。 默认情况下它在那里, 所以,
如果你已经改变了那个设置的话,
只需要改回来即可。
3. 确定您已经安装了 sites 框架 (参
见第14章).
Note
sitemap 应用程序没有安装任何数
据库表. 它需要加入
到 INSTALLED_APPS 中的唯一原
因是: 这样load_template_source 模
板加载器可以找到默认的模板.
The only reason it needs to go
intoINSTALLED_APPS is so
the load_template_source template
loader can find the default templates.
Initialization
要在您的Django站点中激活sitemap生
成, 请在您的 URLconf 中添加这一行:
(r'^sitemap.xml/pre>,
'
django.contrib.sitemaps.views.sitemap',
{
'sitemaps': sitemaps})
This line tells Django to build a sitemap
when a client accesses / si temap.xml .
Note that the dot character
in si temap.xml is escaped with a
backslash, because dots have a special
meani ng in regular expressions.
sitemap文件的名字无关紧要,但是它
在服务器上的位置却很重要。 搜索
引擎只索引你的sitemap中当前URL级
别及其以下级别的链接。 用一个实
例来说,如果 si temap.xml 位于你的
根目录,那么它将引用任何的URL。
然而,如果你的sitemap位
于 /content/sitemap.xml ,那么它只引
用以 /content/ 打头的URL。
sitemap视图需要一个额外的必须的参
数: {'sitemaps': sitemaps} . sitemaps s
hould be a dictionary that maps a short
section label (e.g., blog or news ) to
its Sitemap class
(e.g., BlogSitemap or NewsSitemap ). It
may also map to an instance of
a Sitemap class
(e.g., BlogSitemap(some_var) ).
Sitemap 类
Sitemap 类展示了一个进入地图站点
简单的Python类片断.例如,一
个 Sitemap 类能展现所有日志入口,
而另外一个能够调度所有的日历事
件。 For exampl e, one Sitemap class
could represent all the entries of your
weblog, while another could represent
all of the events in your events calendar.
在最简单的例子中,所有部分可以全
部包含在一个 si temap.xml 中,也可
以使用框架来产生一个站点地图,为
每一个独立的部分产生一个单独的站
点文件。
Sitemap 类必须
是 django.contrib.sitemaps.Sitemap 的
子类. 他们可以存在于您的代码树的
任何地方。
例如假设你有一个blog系统,有一
个 Entry 的model,并且你希望你的站
点地图包含所有连到你的blog入口的
超链接。 你的 Sitemap 类很可能是这
样的:
from django.contrib.sitemaps import
Sitemap
from mysite.blog.models import Entry
class BlogSitemap(Sitemap):
changefreq = "never"
priority = 0.5
def items(self):
return Entry.objects.filter(
is_draft=False)
def lastmod(self, obj):
return obj.pub_date
声明一个 Sitemap 和声明一个 Feed 看
起来很类似;这都是预先设计好的。
如同 Feed 类一样, Sitemap 成员也既
可以是方法,也可以是属性。 想要
知道更详细的内容,请参见上文
《一个复杂的例子》章节。
一个 Sitemap 类可以定义如下 方法/
属性 :
i tems (必需 ):提供对象列表。 框
架并不关心对象的 类型 ;唯一关
心的是这些对象会传递
给 location(), lastmod() , changef
req() ,和 priority() 方法。
location (可选): 给定对象的绝
对URL。 绝对URL不包含协议名称
和域名。 下面是一些例子:
好的: '/foo/bar/' '/foo/bar/'
差
的: 'example.com/foo/bar/' 'exam
ple.com/foo/bar/'
Bad: 'http://example.com/foo/bar/
'
如果没有提供 location , 框架将会
在每个 items() 返回的对象上调
用 get_absolute_url() 方法.
lastmod (可选): 对象的最后修改日
期, 作为一个Python datetime 对象.
The object’s last modification date,
as a Python datetime object.
changefreq (可选): 对象变更的
频率。 可选的值如下(详见
Sitemaps文档):
'
'
'
'
'
'
always'
hourly'
daily'
weekly'
monthly'
yearly'
'
never'
priority (可选): 取值范围
在 0.0 and 1.0 之间,用来表明优先
级。
快捷方式
sitemap框架提供了一些常用的类。
在下一部分中会看到。
FlatPageSitemap
django.contrib.sitemaps.FlatPageSitema
p 类涉及到站点中所有的flat page,并
在sitemap中建立一个入口。 但仅仅
只包含 location 属性,不支
持 lastmod , changefreq ,或
者 priority 。
参见第16章获取有关flat page的更多
的内容 .
GenericSitemap
GenericSitemap 与所有的通用视图一
同工作(详见第9章)。
你可以如下使用它,创建一个实例,
并通过 info_dict 传递给通用视图。
唯一的要求是字典包含 queryset 这一
项。 也可以用 date_field 来指明
从 queryset 中取回的对象的日期域。
这会被用作站点地图中的 lastmod属
性。
下面是一个使
用 FlatPageSitemap and GenericSiteMa
p (包括前面所假定的 Entry 对象)
的URLconf:
from django.conf.urls.defaults import *
from django.contrib.sitemaps import
FlatPageSitemap, GenericSitemap
from mysite.blog.models import Entry
info_dict = {
'
'
queryset': Entry.objects.all(),
date_field': 'pub_date',
}
sitemaps = {
'
'
flatpages': FlatPageSitemap,
blog': GenericSitemap(info_dict,
priority=0.6),
}
urlpatterns = patterns('',
some generic view
using info_dict
#
#
...
the sitemap
(r'^sitemap\.xml/pre>,
django.contrib.sitemaps.views.site
map',
'sitemaps': sitemaps})
'
{
)
创建一个Sitemap索引
sitemap框架同样可以根据 sitemaps 字
典中定义的单独的sitemap文件来建立
索引。 用法区别如下:
您在您的URLconf 中使用了两个
视
图: django.contrib.sitemaps.views.in
dex 和
django.contrib.sitemaps.views.sitem
ap .
django.contrib.sitemaps.views
和
django.contrib.sitemaps.views
django.contrib.sitemaps.views.sitem
ap 视图需要带一个 section 关键字
参数.
这里是前面的例子的相关的 URLconf
行看起来的样子 :
(r'^sitemap.xml/pre>,
'
django.contrib.sitemaps.views.index',
{
'sitemaps': sitemaps}),
(r'^sitemap-(?P.+).xml/pre>,
django.contrib.sitemaps.views.sitemap',
'sitemaps': sitemaps})
'
{
这将自动生成一个 si temap.xml 文件,
它同时引用 sitemap-
flatpages.xml 和 sitemap-
bl og.xml . Sitemap 类和sitemaps 目录
根本没有更改 .
通知Google
当你的sitemap变化的时候,你会想通
知Google,以便让它知道对你的站点
进行重新索引。 框架就提供了这样
的一个函
数: django.contrib.sitemaps.ping_goog
le() 。
ping_google() 有一个可选的参
数 sitemap_url ,它应该是你的站点地
图的URL绝对地址(例如:
如果不能够确定你的sitemap
URL, ping_google() 会引
发 django.contrib.sitemaps.SitemapNotF
ound 异常。
我们可以通过模型中的 save() 方法来
调用 ping_google() :
from django.contrib.sitemaps import
ping_google
class Entry(models.Model):
#
...
def save(self, *args, **kwargs):
super(Entry, self).save(*arg
s, **kwargs)
try:
ping_google()
except Exception:
#
Bare 'except' because
we could get a variety
#
of HTTP-related except
ions.
pass
一个更有效的解决方案是用 cron 脚
本或任务调度表来调
用 ping_google() ,该方法使用Http直
接请求Google服务器,从而减少每次
调用 save() 时占用的网络带宽。 The
function makes an HTTP request to
Google’s servers, so you may not want
to introduce that network overhead each
ti me you call save().
Finally, if 'django.contrib.sitemaps' is in
your INSTALLED_APPS , then
your manage.py will include a new
command, ping_google . This is useful
for command-l i ne access to pinging. For
exampl e:
python manage.py ping_google
/
si temap.xml
下一章
下面, 我们要继续深入挖掘所有的
Django给你的很好的内置工具。 第十
四章查看创建用户自定义站点需要的
工具 sessions, users 和authentication.
是时候承认了: 我们有意的避开了
Web开发中极其重要的方面。 到目前
为止,我们都在假定,网站流量是大
量的匿名用户带来的。
这当然不对。 浏览器的背后都是活
生生的人(至少某些时候是)。 这忽略
了重要的一点: 互联网服务于人而
不是机器。 要开发一个真正令人心
动的网站,我们必须面对浏览器后面
活生生的人。
很不幸,这并不容易。 HTTP被设计
为”无状态”,每次请求都处于相同的
空间中。 在一次请求和下一次请求
之间没有任何状态保持,我们无法根
据请求的任何方面(IP地址,用户代
理等)来识别来自同一人的连续请
求。
在本章中你将学会如何搞定状态的问
题。 好了,我们会从较低的层次
(cookies)开始,然后过渡到用高层的
工具来搞定会话,用户和注册的问
题。
Cookies
浏览器的开发者在很早的时候就已经
意识到, HTTP’s 的无状态会对We b
开发者带来很大的问题,于是
(cookies)应运而生。 cookies 是浏览
器为 We b 服务器存储的一小段信
息。 每次浏览器从某个服务器请求
页面时,它向服务器回送之前收到的
cookies
来看看它是怎么工作的。 当你打开
浏览器并访问 google.com ,你的浏览
器会给Google发送一个HTTP请求,
起始部分就象这样:
GET / HTTP/1.1
Host: google.com
.
..
当 Google响应时,HTTP的响应是这
样的:
HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: PREF=ID=5b14f22bdaf1e81c
:
TM=1167000671:LM=1167000671;
expires=Sun, 17-Jan-2038
9:14:07 GMT;
path=/; domain=.google.c
1
om
Server: GWS/2.1
..
.
注意 Set-Cookie 的头部。 你的浏览
器会存储cookie值
(PREF=ID=5b14f22bdaf1e81c:TM=116
7000671:LM=1167000671 ) ,而且每
次访问google 站点都会回送这个
cookie值。 因此当你下次访问Google
时,你的浏览器会发送像这样的请
求:
GET / HTTP/1.1
Host: google.com
Cookie: PREF=ID=5b14f22bdaf1e81c:TM=
1
.
167000671:LM=1167000671
..
于是 Cookies 的值会告诉Google,你
就是早些时候访问过Google网站的
人。 这个值可能是数据库中存储用
户信息的key,可以用它在页面上显
示你的用户名。 Google会(以及目
前)使用它在网页上显示你账号的用
户名。
存取Cookies
在Django中处理持久化,大部分时候
你会更愿意用高层些的session 和/或
后面要讨论的user 框架。 但在此之
前,我们需要停下来在底层看看如何
读写cookies。 这会帮助你理解本章
节后面要讨论的工具是如何工作的,
而且如果你需要自己操作cookies,这
也会有所帮助。
读取已经设置好的cookies极其简单。
每一个 HttpRequest 对象都有一个
COOKIES 对象,该对象的行为类似
一个字典,你可以使用它读取任何浏
览器发送给视图(view)的cookies。
def show_color(request):
if "favorite_color" in request.C
OOKIES:
return HttpResponse("Your fa
vorite color is %s" % re
quest.COOKIES["favorite_color"])
else:
return HttpResponse("You don
t have a favorite color.")
'
写cookies稍微复杂点。 你需要使
用 HttpResponse对象的 set_cookie()方
法。 这儿有个基于 GET 参数来设置
favorite_color
cookie的例子:
def set_color(request):
if "favorite_color" in request.G
ET:
#
Create an HttpResponse obj
ect...
response = HttpResponse("You
r favorite color is now %s" %
request.GET["favorite_color"])
#
... and set a cookie on th
e response
response.set_cookie("favorit
e_color",
request.
GET["favorite_color"])
return response
else:
return HttpResponse("You did
n't give a favorite color.")
你可以给 response.set_cookie() 传递
一些可选的参数来控制cookie的行
为,详见表14-1。
System Message: ERROR/3 (, line 145)
Error parsing content block for the
table” directive: exactly one table
expected.
“
.
. table:: 表 14-1: Cookie 选项
缺省
值
参数
描述
cookie需要延续的
max_age None 位) 如果参数是
会延续到浏览器关
cookie失效的实际
格式必须是:
expires
None "Wdy, DD-Mth-YY
。如果给出了这个
max_age 参数。
cookie生效的路径
把cookie回传给带
这样你可以避免将
的其他的应用。当
点的顶层时,这样
path
"/"
这个cookie有效的
这个参数设置一个
domai n)的cookie
domain=".exampl
一个在 www.examp
www2.example.co
an.other.sub.do
站点下都可读到的
参数被设成 None
设置它的站点下可
domain
False
None
False
如果设置为 True
HTTPS来回传coo
好坏参半的Cookies
也许你已经注意到了,cookies的工作
方式可能导致的问题。 让我们看一
下其中一些比较重要的问题:
cookie的存储是自愿的,一个客户
端不一定要去接受或存储cookie。
事实上,所有的浏览器都让用户
自己控制 是否接受cookies。 如果
你想知道cookies对于Web应用有多
重要,你可以试着打开这个浏览
器的 选项:
尽管cookies广为使用,但仍被认
为是不可靠的的。 这意味着,开
发者使用cookies之前必须 检查用
户是否可以接收cookie。
Cookie(特别是那些没通过HTTPS
传输的)是非常不安全的。 因为
HTTP数据是以明文发送的,所以
特别容易受到嗅探攻击。 也就是
说,嗅探攻击者可以在网络中拦
截并读取cookies,因此你要 绝对
避免在cookies中存储敏感信息。
这就意味着您不应该使用cookie来
在存储任何敏感信息。
还有一种被称为”中间人”的攻击更
阴险,攻击者拦截一个cookie并将
其用于另一个用户。 第19章将深
入讨论这种攻击的本质以及如何
避免。
即使从预想中的接收者返回的
cookie也是不安全的。 在大多数浏
览器中您可以非常容易地修改
cookies中的信息。有经验的用户
甚至可以通过像
mechani ze(http://wwwsearch.sourcef
orge.net/mechanize/) 这样的工具手
工构造一个HTTP请求。
因此不能在cookies中存储可能会
被篡改的敏感数据。 在cookies中
存储 IsLoggedIn=1 ,以标识用户已
经登录。 犯这类错误的站点数量
多的令人难以置信; 绕过这些网
站的安全系统也是易如反掌。
Django的 Session 框
架
由于存在的限制与安全漏洞,cookies
和持续性会话已经成为Web开发中令
人头疼的典范。 好消息是,Django的
目标正是高效的“头疼杀手”,它自带
的session框架会帮你搞定这些问题。
你可以用session 框架来存取每个访问
者任意数据, 这些数据在服务器端
存储,并对cookie的收发进行了抽
象。 Cookies只存储数据的哈希会话
ID,而不是数据本身,从而避免了大
部分的常见cookie问题。
下面我们来看看如何打开session功
能,并在视图中使用它。
打开 Sessions功能
Sessions 功能是通过一个中间件(参见
第17章)和一个模型(model)来实现
的。 要打开sessions功能,需要以下
几步操作:
1. 编辑 MIDDLEWARE_CLASSES 配
置,确
保 MIDDLEWARE_CLASSES 中包
含'django.contrib.sessions.middlewa
re.SessionMiddleware' 。
2
. 确认 INSTALLED_APPS 中
有 'django.contrib.sessions' (如果你
是刚打开这个应用,别忘了运行
manage.py syncdb )
如果项目是用 startproject 来创建的,
配置文件中都已经安装了这些东西,
除非你自己删除,正常情况下,你无
需任何设置就可以使用session功能。
如果不需要session功能,你可以删
除 MIDDLEWARE_CLASSES 设置中
的 SessionMiddleware 和 INSTALLED
APPS 设置中
_
的 'django.contrib.sessions' 。虽然这只
会节省很少的开销,但积少成多啊。
在视图中使用Session
SessionMiddleware 激活后,每个传
给视图(view)函数的第一个参数
HttpRequest 对象都有一
个 session 属性,这是一个字典型的
对象。 你可以象用普通字典一样来
用它。 例如,在视图(view)中你可以
这样用:
#
Set a session value:
request.session["fav_color"] = "blue
"
#
Get a session value -- this could
be called in a different view,
or many requests later (or both):
#
fav_color = request.session["fav_col
or"]
#
Clear an item from the session:
del request.session["fav_color"]
#
Check if the session has a given k
ey:
if "fav_color" in request.session:
.
..
其他的映射方法,
如 keys() 和 items() 对 request.session
同样有效:
下面是一些有效使用Django sessions
的简单规则:
用正常的字符串作为key来访问字
典 request.session , 而不是整数、
对象或其它什么的。
Session字典中以下划线开头的key
值是Django内部保留key值。 框架
只会用很少的几个下划线 开头的
session变量,除非你知道他们的具
体含义,而且愿意跟上Django的变
化,否则,最好 不要用这些下划
线开头的变量,它们会让Django搅
乱你的应用。
比如,不要象这样使用
_
fav_color 会话密钥(session
key):
request.session['_fav_color'] = 'blu
e' # Don't do this!
不要用一个新对象来替换
掉 request.session ,也不要存取其
属性。 可以像Python中的字典那样
使用。 例如:
request.session = some_other_object
#
Don't do this!
request.session.foo = 'bar' # Don't
do this!
我们来看个简单的例子。 这是个简
单到不能再简单的例子:在用户发了
一次评论后将has_commented设置为
Tr ue。 这是个简单(但不很安全)
的、防止用户多次评论的方法。
def post_comment(request):
if request.method != 'POST':
raise Http404('Only POSTs ar
e allowed')
if 'comment' not in request.POST
:
raise Http404('Comment not s
ubmitted')
if request.session.get('has_comm
ented', False):
return HttpResponse("You've
already commented.")
c = comments.Comment(comment=req
uest.POST['comment'])
c.save()
request.session['has_commented']
=
True
return HttpResponse('Thanks for
your comment!')
下面是一个很简单的站点登录视图
(view):
def login(request):
if request.method != 'POST':
raise Http404('Only POSTs ar
e allowed')
try:
m = Member.objects.get(usern
ame=request.POST['username'])
if m.password == request.POS
T['password']:
request.session['member_
id'] = m.id
return HttpResponseRedir
ect('/you-are-logged-in/')
except Member.DoesNotExist:
return HttpResponse("Your us
ername and password didn't match.")
下面的例子将登出一个在上面已通过
login() 登录的用户:
def logout(request):
try:
del request.session['member_
id']
except KeyError:
pass
return HttpResponse("You're logg
ed out.")
注意
在实践中,这是很烂的用户登录
方式,稍后讨论的认证
(authentication )框架会帮你以更健
壮和有利的方式来处理这些问
题。 这些非常简单的例子只是想
让你知道这一切是如何工作的。
这些实例尽量简单,这样你可以
更容易看到发生了什么。
设置测试Cookies
就像前面提到的,你不能指望所有的
浏览器都可以接受cookie。 因此,为
了使用方便,Django提供了一个简单
的方法来测试用户的浏览器是否接受
cookie。 你只需在视图(view)中调
用 request.session.set_test_cookie() ,
并在后续的视图(view)、而不是当前
的视图(view)中检
查 request.session.test_cookie_worked(
)
。
虽然
把 set_test_cookie() 和 test_cookie_wor
ked() 分开的做法看起来有些笨拙,
但由于cookie的工作方式,这无可避
免。 当设置一个cookie时候,只能等
浏览器下次访问的时候,你才能知道
浏览器是否接受cookie。
检查cookie是否可以正常工作后,你
得自己用 delete_test_cookie() 来清除
它,这是个好习惯。 在你证实了测
试cookie已工作了之后这样操作。
这是个典型例子:
def login(request):
#
If we submitted the form...
if request.method == 'POST':
#
Check that the test cookie
worked (we set it below):
if request.session.test_cook
ie_worked():
#
The test cookie worked
so delete it.
request.session.delete_t
,
est_cookie()
#
In practice, we'd need
some logic to check username/passwo
rd
#
here, but since this i
s an example...
return HttpResponse("You
re logged in.")
'
#
The test cookie failed, so
display an error message. If this
were a real site, we'd wan
#
t to display a friendlier message.
else:
return HttpResponse("Ple
ase enable cookies and try again.")
#
If we didn't post, send the te
st cookie along with the login form.
request.session.set_test_cookie(
)
return render_to_response('foo/l
ogin_form.html')
注意
再次强调,内置的认证函数会帮
你做检查的。
在视图(View)外使用Session
从内部来看,每个session都只是一个
普通的Django
model(在 django.contrib.sessions.mod
els 中定义)。每个session都由一个随
机的32字节哈希串来标识,并存储于
cookie中。 因为它是一个标准的模
型,所以你可以使用Django数据库
API来存取session。
>
>> from django.contrib.sessions.mod
els import Session
>
8
>
>> s = Session.objects.get(pk='2b11
9a188b44ad18c35e113ac6ceead')
>> s.expire_date
datetime.datetime(2005, 8, 20, 13, 3
5
, 12)
你需要使用get_decoded() 来读取实际
的session数据。 这是必需的,因为字
典存储为一种特定的编码格式。
>
'
>> s.session_data
KGRwMQpTJ19hdXRoX3VzZXJfaWQnCnAyCkk
xCnMuMTExY2ZjODI2Yj...'
>
{
>> s.get_decoded()
'user_id': 42}
何时保存Session
缺省的情况下,Django只会在session
发生变化的时候才会存入数据库,比
如说,字典赋值或删除。
#
Session is modified.
request.session['foo'] = 'bar'
#
Session is modified.
del request.session['foo']
#
Session is modified.
request.session['foo'] = {}
#
Gotcha: Session is NOT modified, b
ecause this alters
request.session['foo'] instead of
#
request.session.
request.session['foo']['bar'] = 'baz'
你可以设
置 SESSION_SAVE_EVERY_REQUES
T 为 Tr ue 来改变这一缺省行为。如果
置为Tr ue的话,Django会在每次收到
请求的时候保存session,即使没发生
变化。
注意,会话cookie只会在创建和修改
的时候才会送出。 但如
果 SESSION_SAVE_EVERY_REQUES
T 设置为 Tr ue ,会话cookie在每次请
求的时候都会送出。 同时,每次会
话cookie送出的时候,其 expires 参数
都会更新。
浏览器关闭即失效会话 vs
持久会话
你可能注意到了,Google给我们发送
的cookie中有 expires=Sun, 17-Jan-
2
038 19:14:07 GMT; cookie可以有过
期时间,这样浏览器就知道什么时候
可以删除cookie了。 如果cookie没有
设置过期时间,当用户关闭浏览器的
时候,cookie就自动过期了。 你可以
改
变 SESSION_EXPIRE_AT_BROWSER
_
CLOSE 的设置来控制session框架的
这一行为。
缺省情况
下, SESSION_EXPIRE_AT_BROWS
ER_CLOSE 设置为 False ,这样,会
话cookie可以在用户浏览器中保持有
效达 SESSION_COOKIE_AGE 秒(缺
省设置是两周,即1,209,600 秒)。
如果你不想用户每次打开浏览器都必
须重新登陆的话,用这个参数来帮
你。
如
果 SESSION_EXPIRE_AT_BROWSER
_
CLOSE 设置为 Tr ue ,当浏览器关闭
时,Django会使cookie失效。
其他的Session设置
除了上面提到的设置,还有一些其他
的设置可以影响Django session框架如
何使用cookie,详见表 14-2.
表 14-2. 影响cookie行为的
设置
设置
使用
cook
cook
点。
个字
象
SESSION_COOKIE_DOMAIN
“.e
以用
(
cr
的co
Non
个站
会话
cook
它可
字符
SESSION_COOKIE_NAME
是否
使用
如果
SESSION_COOKIE_SECURE cook
安全
cook
HTT
技术细节
如果你还是好奇的话,下面是一
些关于session框架内部工作方式的
技术细节:
session 字典接受任何支持序列化
的Python对象。 参考Python内建模
块pickle的文档以获取更多信息。
Session 数据存在数据库
表 django_session 中
Session 数据在需要的时候才会读
取。 如果你从不使
用 request.session , Django不会动
相关数据库表的一根毛。
Django 只在需要的时候才送出
cookie。 如果你压根儿就没有设置
任何会话数据,它不会 送出会话
cookie(除
非 SESSION_SAVE_EVERY_REQU
EST 设置为 Tr ue )。
Django session 框架完全而且只能
基于cookie。 它不会后退到把会话
ID编码在URL中(像某些工具
(PHP,JSP)那样)。
这是一个有意而为之的设计。 把
session放在URL中不只是难看,更
重要的是这让你的站点 很容易受
到攻击——通过 Referer header进
行session ID”窃听”而实施的攻
击。
如果你还是好奇,阅读源代码是最直
接办法,详
见 django.contrib.sessions 。
用户与Authentication
通过session,我们可以在多次浏览器
请求中保持数据, 接下来的部分就
是用session来处理用户登录了。 当
然,不能仅凭用户的一面之词,我们
就相信,所以我们需要认证。
当然了,Django 也提供了工具来处理
这样的常见任务(就像其他常见任务
一样)。 Django 用户认证系统处理
用户帐号,组,权限以及基于cookie
的用户会话。 这个系统一般被称
为 auth/auth (认证与授权)系统。 这
个系统的名称同时也表明了用户常见
的两步处理。 我们需要
1
. 验证 (认证) 用户是否是他所宣称
的用户(一般通过查询数据库验证
其用户名和密码)
2
. 验证用户是否拥有执行某种操作
的 授权 (通常会通过检查一个权
限表来确认)
根据这些需求,Django 认证/授权 系
统会包含以下的部分:
用户 : 在网站注册的人
权限 : 用于标识用户是否可以执
行某种操作的二进制(yes/no)标志
组 :一种可以将标记和权限应用于
多个用户的常用方法
Messages : 向用户显示队列式的系
统消息的常用方法
如果你已经用了admi n工具(详见第6
章),就会看见这些工具的大部分。
如果你在admi n工具中编辑过用户或
组,那么实际上你已经编辑过授权系
统的数据库表了。
打开认证支持
像session工具一样,认证支持也是一
个Django应用,放
在 django.contrib 中,所以也需要安
装。 与session系统相似,它也是缺省
安装的,但如果它已经被删除了,通
过以下步骤也能重新安装上:
1
. 根据本章早前的部分确认已经安
装了session 框架。 需要确认用户
使用cookie,这样sesson 框架才能
正常使用。
2
. 将 'django.contrib.auth' 放在你
的 INSTALLED_APPS 设置中,然
后运行 manage.py syncdb以创建对
应的数据库表。
3
. 确认 SessionMiddleware 后面
的 MIDDLEWARE_CLASSES 设置
中包
含'django.contrib.auth.middleware.A
uthenticationMiddleware' SessionMi
ddleware 。
这样安装后,我们就可以在视图
(view)的函数中处理user了。 在视图
中存取users,主要用 request.user ;
这个对象表示当前已登录的用户。
如果用户还没登录,这就是一个
AnonymousUser对象(细节见下)。
你可以很容易地通
过 is_authenticated() 方法来判断一个
用户是否已经登录了:
if request.user.is_authenticated():
#
Do something for authenticated
users.
else:
#
Do something for anonymous use
rs.
使用User对象
User 实例一般从 request.user ,或是
其他下面即将要讨论到的方法取得,
它有很多属性和方法。
AnonymousUser 对象模拟了 部分 的
接口,但不是全部,在把它当成真正
的user对象 使用前,你得检查一下
user.is_authenticated() 表14-3和14-4分
别列出了 User 对象中的属性
(
fields)和方法。
表 14-
3. User 对
描
述
属性
象属性
必需的,不能
多于30个字
符。 仅用字母
数字式字符
(字母、数字
和下划线)。
user name
可选; 少于等
于30字符。
first_name
last_name
可选; 少于等
于30字符。
可选。 邮件地
址。
必需的。 密码
的哈希值
(Django不储
存原始密
password
码)。 See the
Passwords
section for
more about this
value.
布尔值。 用户
是否拥有网站
的管理权限。
is_staff
布尔值. 设置
该账户是否可
以登录。 把该
标志位置为
is_active
False而不是直
接删除账户。
布尔值 标识用
户是否拥有所
is_superuser 有权限,无需
显式地权限分
配定义。
用户上次登录
的时间日期。
它被默认设置
为当前的日期/
时间。
last_login
账号被创建的
日期时间 当账
号被创建时,
它被默认设置
为当前的日期/
时间。
date_joined
.
. table:: 表 14-4. User 对象方法
方法
描述
对于真
象,总
。这是
是否已
法。
is_authenticated()
何权限
户是否
它仅说
成功鉴
对于
对象返
于真实
返回
is_anonymous()
来说,
法,你
用
is_a
方法。
返回
上
get_full_name()
la
插入一
设定用
字符串
哈希串
有保存
set_password(passwd)
如果指
用户密
True
check_password(passwd)
get_group_permissions()
get_all_permissions()
用密码
返回一
所属组
符串列
返回一
所属组
所获得
列表。
如果用
限,则
此时
has_perm(perm)
"pac
。如果
动,此
Fals
如果用
指定权
True
has_perms(perm_list)
不活动
总是返
如果用
app_
权限,
has_module_perms(app_label)
。如果
动,这
回 Fa
返回一
的 Me
表,并
get_and_delete_messages()
email_user(subj, ms g)
些消息
向用户
邮件。
是从
DEFA
设置的
你还可
三参数
,以覆
送地址
最后, User 对象有两个many-to-
many属性。 groups 和
permissions 。正如其他的many-to-
many属性使用的方法一样, User 对
象可以获得它们相关的对象:
#
Set a user's groups:
myuser.groups = group_list
#
Add a user to some groups:
myuser.groups.add(group1, group2,...
)
#
Remove a user from some groups:
myuser.groups.remove(group1, group2,
.
#
..)
Remove a user from all groups:
myuser.groups.clear()
#
Permissions work the same way
myuser.permissions = permission_list
myuser.permissions.add(permission1,
permission2, ...)
myuser.permissions.remove(permission
1
, permission2, ...)
myuser.permissions.clear()
登录和退出
Django 提供内置的视图(view)函数用
于处理登录和退出 (以及其他奇技淫
巧),但在开始前,我们来看看如何
手工登录和退出。 Django提供两个函
数来执行django.contrib.auth\中的动
作 : authenticate()和login()。
认证给出的用户名和密码,使
用 authenticate() 函数。它接受两个参
数,用户名 user name 和 密码
password ,并在密码对给出的用户名
合法的情况下返回一个 User 对象。
如果密码不合法,authenticate()返回
None。
>
>
>> from django.contrib import auth
>> user = auth.authenticate(usernam
e='john', password='secret')
>
.
.
.
>> if user is not None:
.. print "Correct!"
.. else:
.. print "Invalid password."
authenticate() 只是验证一个用户的证
书而已。 而要登录一个用户,使
用 login() 。该函数接受一个
HttpRequest 对象和一个 User 对象作
为参数并使用Django的会话
(
session )框架把用户的ID保存在
该会话中。
下面的例子演示了如何在一个视图中
同时使用 authenticate() 和 login() 函
数:
from django.contrib import auth
def login_view(request):
username = request.POST.get('use
rname', '')
password = request.POST.get('pas
sword', '')
user = auth.authenticate(usernam
e=username, password=password)
if user is not None and user.is_
active:
#
Correct password, and the
user is marked "active"
auth.login(request, user)
Redirect to a success page
#
.
return HttpResponseRedirect(
/account/loggedin/")
"
else:
#
Show an error page
return HttpResponseRedirect(
/account/invalid/")
"
注销一个用户,在你的视图中使
用 django.contrib.auth.logout() 。 它接
受一个HttpRequest对象并且没有返回
值。
from django.contrib import auth
def logout_view(request):
auth.logout(request)
#
Redirect to a success page.
return HttpResponseRedirect("/ac
count/loggedout/")
注意,即使用户没有登
录, logout() 也不会抛出任何异常。
在实际中,你一般不需要自己写登
录/登出的函数;认证系统提供了一
系例视图用来处理登录和登出。 使
用认证视图的第一步是把它们写在你
的URLconf中。 你需要这样写:
from django.contrib.auth.views impor
t login, logout
urlpatterns = patterns('',
#
existing patterns here...
(r'^accounts/login/$', login),
(r'^accounts/logout/$', logout),
)
/
accounts/login/ 和 /accounts/logout/ 是
Django提供的视图的默认URL。
缺省情况下, login 视图渲
染 registragiton/login.html 模板(可以通
过视图的额外参数 template_name 修
改这个模板名称)。 这个表单必须包
含 user name 和 password 域。如下示
例: 一个简单的 template 看起来是这
样的
{
{
% extends "base.html" %}
% block content %}
{
% if form.errors %}
p class="error">Sorry, that's n
ot a valid username or password</p>
<
{
% endif %}
<
form action="" method="post">
<
label for="username">User name:
/label>
input type="text" name="usernam
e" value="" id="username">
label for="password">Password:<
label>
input type="password" name="pas
sword" value="" id="password">
<
<
<
/
<
<
n" />
<
input type="submit" value="logi
input type="hidden" name="next"
value="{{ next|escape }}" />
</form>
{
% endblock %}
如果用户登录成功,缺省会重定向
到 /accounts/profile 。 你可以提供一
个保存登录后重定向URL的next隐藏
域来重载它的行为。 也可以把值以
GET参数的形式发送给视图函数,它
会以变量next的形式保存在上下文
中,这样你就可以把它用在隐藏域上
了。
logout视图有一些不同。 默认情况下
它渲染 registration/logged_out.html 模
板(这个视图一般包含你已经成功退
出的信息)。 视图中还可以包含一
个参数 next_page 用于退出后重定
向。
限制已登录用户的访问
有很多原因需要控制用户访问站点的
某部分。
一个简单原始的限制方法是检
查 request.user.is_authenticated() ,然后
重定向到登陆页面:
from django.http import HttpResponse
Redirect
def my_view(request):
if not request.user.is_authentic
ated():
return HttpResponseRedirect(
'
.
/accounts/login/?next=%s' % request
path)
#
...
或者显示一个出错信息:
def my_view(request):
if not request.user.is_authentic
ated():
return render_to_response('m
yapp/login_error.html')
#
...
作为一个快捷方式, 你可以使用便捷
的 login_required 修饰符:
from django.contrib.auth.decorators
import login_required
@
login_required
def my_view(request):
...
#
login_required 做下面的事情:
如果用户没有登录, 重定向
到 /accounts/login/ , 把当前绝对
URL作为 next 在查询字符串中传
递过去, 例如: /accounts/login/?
next=/polls/3/ 。
如果用户已经登录, 正常地执行视
图函数。 视图代码就可以假定用
户已经登录了。
对通过测试的用户限制访
问
限制访问可以基于某种权限,某些检
查或者为login视图提供不同的位置,
这些实现方式大致相同。
一般的方法是直接在视图
的 request.user 上运行检查。 例如,
下面视图确认用户登录并是否有
polls.can_vote权限:
def vote(request):
if request.user.is_authenticated
() and request.user.has_perm('polls.
can_vote')):
#
vote here
else:
return HttpResponse("You can
t vote in this poll.")
'
并且Django有一个称
为 user_passes_test 的简洁方式。它接
受参数然后为你指定的情况生成装饰
器。
def user_can_vote(user):
return user.is_authenticated() a
nd user.has_perm("polls.can_vote")
@
user_passes_test(user_can_vote, log
in_url="/login/")
def vote(request):
#
Code here can assume a logged-
in user with the correct permission.
.
..
user_passes_test 使用一个必需的参
数: 一个可调用的方法,当存
在 User 对象并当此用户允许查看该
页面时返回Tr ue 。 注意
user_passes_test 不会自动检查 User是
否认证,你应该自己做这件事。
例子中我们也展示了第二个可选的参
数 login_url ,它让你指定你的登录页
面的URL(默认
为/accounts/login/ )。 如果用户没有
通过测试,那么user_passes_test将把
用户重定向到login_url
既然检查用户是否有一个特殊权限是
相对常见的任务,Django为这种情形
提供了一个捷径:
permission_required() 装饰器。 使用
这个装饰器,前面的例子可以改写
为:
from django.contrib.auth.decorators
import permission_required
@
'
permission_required('polls.can_vote
, login_url="/login/")
def vote(request):
#
...
注意, permission_required() 也有一个
可选的 login_url 参数, 这个参数默认
为 '/accounts/login/' 。
限制通用视图的访问
在Django用户邮件列表中问到最多的
问题是关于对通用视图的限制性访
问。 为实现这个功能,你需要自己
包装视图,并且在URLconf中,将你
自己的版本替换通用视图:
from django.contrib.auth.decorators
import login_required
from django.views.generic.date_based
import object_detail
@
login_required
def limited_object_detail(*args, **k
wargs):
return object_detail(*args, **kw
args)
当然, 你可以用任何其他限定修饰符
来替换 login_required 。
管理 Users, Permissions 和
Grou ps
管理认证系统最简单的方法是通过管
理界面。 第六章讨论了怎样使用
Django的管理界面来编辑用户和控制
他们的权限和可访问性,并且大多数
时间你使用这个界面就可以了。
然而,当你需要绝对的控制权的时
候,有一些低层 API 需要深入专研,
我们将在下面的章节中讨论它们。
创建用户
使用 create_user 辅助函数创建用户:
>
>> from django.contrib.auth.models
import User
>> user = User.objects.create_user(
>
username='john',
..
email='jlennon@beatles.com',
..
.
.
password='glass onion')
在这里, user 是 User 类的一个实
例,准备用于向数据库中存储数据。
(create_user()实际上没有调用
save())。 create_user() 函数并没有
在数据库中创建记录,在保存数据之
前,你仍然可以继续修改它的属性
值。
>
>
>> user.is_staff = True
>> user.save()
修改密码
你可以使用 set_password() 来修改密
码:
>
=
>
>> user = User.objects.get(username
'john')
>> user.set_password('goo goo goo j
oob')
>>> user.save()
除非你清楚的知道自己在做什么,否
则不要直接修改 password 属性。 其
中保存的是密码的 加入salt的hash
值,所以不能直接编辑。
一般来说, User 对象的 password 属
性是一个字符串,格式如下:
hashtype$salt$hash
这是哈希类型,salt和哈希本身,用
美元符号($)分隔。
hashtype 是 sha1 (默认)或者 md5 ,
它是用来处理单向密码哈希的算法。
Salt是一个用来加密原始密码以创建
哈希的随机字符串,例如 :
sha1$a1976$a36cc8cbf81742a8fb52e221a
aeab48ed7f58ab4
User.set_password() 和 User.check_pas
sword() 函数在后台处理和检查这些
值。
salt化得哈希值
一次 哈希 是一次单向的加密过
程,你能容易地计算出一个给定
值的哈希码,但是几乎不可能从
一个哈希码解出它的原值。
如果我们以普通文本存储密码,任
何能进入数据库的人都能轻易的
获取每个人的密码。 使用哈希方
式来存储密码相应的减少了数据
库泄露密码的可能。
然而,攻击者仍然可以使用 暴力
破解 使用上百万个密码与存储的
值对比来获取数据库密码。 这需
要花一些时间,但是智能电脑惊
人的速度超出了你的想象。
更糟糕的是我们可以公开地得
到 rainbow tables (一种暴力密码
破解表)或预备有上百万哈希密
码值的数据库。 使用rainbow
tables可以在几秒之内就能搞定最
复杂的一个密码。
在存储的hash值的基础上,加
入 salt 值(一个随机值),增加
了密码的强度,使得破解更加困
难。 因为每个密码的salt值都不相
同,这也限制了rainbow table的使
用,使得攻击者只能使用最原始
的暴力破解方法。
加入salt值得hash并不是绝对安全
的存储密码的方法,然而却是安
全和方便之间很好的折衷。
处理注册
我们可以使用这些底层工具来创建允
许用户注册的视图。 最近每个开发
人员都希望实现各自不同的注册方
法,所以Django把写注册视图的工作
留给了你。 幸运的是,这很容易。
作为这个事情的最简化处理, 我们可
以提供一个小视图, 提示一些必须的
用户信息并创建这些用户。 Django为
此提供了可用的内置表单, 下面这个
例子就使用了这个表单 :
from django import forms
from django.contrib.auth.forms impor
t UserCreationForm
from django.http import HttpResponse
Redirect
from django.shortcuts import render_
to_response
def register(request):
if request.method == 'POST':
form = UserCreationForm(requ
est.POST)
if form.is_valid():
new_user = form.save()
return HttpResponseRedir
ect("/books/")
else:
form = UserCreationForm()
return render_to_response("regis
tration/register.html", {
'
form': form,
}
)
这个表单需要一个
叫 registration/register.html 的模板。
这个模板可能是这样的:
{
{
% extends "base.html" %}
% block title %}Create an account{%
endblock %}
{
% block content %}
<
h1>Create an account</h1>
<
form action="" method="post">
{
<
{ form.as_p }}
input type="submit" value="Cr
eate the account">
/form>
% endblock %}
<
{
在模板中使用认证数据
当前登入的用户以及他(她)的权限
可以通过 RequestContext 在模板的
context中使用(详见第9章)。
注意
从技术上来说,只有当你使用
了 RequestContext这些变量才可
用。 _并且
_
TEMPLATE_CONTEXT_PROCES
SORS 设置包含了
“
”
django.core.context_processors.auth
(默认情况就是如此)时,这些
变量才能在模板context中使
用。 TEMPLATE_CONTEXT_PRO
CESSORS 设置包含
了 "django.core.context_processors.a
uth" (默认情况就是如此)时,这
些变量才能在模板context中使用。
当使用 RequestContext 时, 当前用户
(是一个 User 实例或一
个 AnonymousUser 实例) 存储在模板
变量{{ user }} 中:
{
% if user.is_authenticated %}
p>Welcome, {{ user.username }}. T
hanks for logging in.</p>
% else %}
<
{
<
p>Welcome, new user. Please log i
n.</p>
% endif %}
{
这些用户的权限信息存储
在 {{ perms }} 模板变量中。
你有两种方式来使用 perms 对象。 你
可以使用类似于 {{ perms.polls }} 的
形式来检查,对于某个特定的应用,
一个用户是否具有 任意 权限;你也
可以使
用 {{ perms.polls.can_vote }} 这样的
形式,来检查一个用户是否拥有特定
的权限。
这样你就可以在模板中
的 {% if %} 语句中检查权限:
{
% if perms.polls %}
p>You have permission to do somet
hing in the polls app.</p>
<
{
% if perms.polls.can_vote %}
p>You can vote!</p>
% endif %}
<
{
{
{
% else %}
p>You don't have permission to do
anything in the polls app.</p>
% endif %}
<
权限、组和消息
在认证框架中还有其他的一些功能。
我们会在接下来的几个部分中进一步
地了解它们。
权限
权限可以很方便地标识用户和用户组
可以执行的操作。 它们被Django的
admi n管理站点所使用,你也可以在
你自己的代码中使用它们。
Django的admi n站点如下使用权限:
只有设置了 add 权限的用户才能
使用添加表单,添加对象的视
图。
只有设置了 change 权限的用户才
能使用变更列表,变更表格,变
更对象的视图。
只有设置了 delete 权限的用户才
能删除一个对象。
权限是根据每一个类型的对象而设置
的,并不具体到对象的特定实例。
例如,我们可以允许Mary改变新故
事,但是目前还不允许设置Mary只能
改变自己创建的新故事,或者根据给
定的状态,出版日期或者ID号来选择
权限。
会自动为每一个Django模型创建三个
基本权限:增加、改变和删除。 当
你运行manage.py syncdb命令时,这些
权限被添加到auth_permission数据库
表中。
权限以 "._" 的形式出现。
就跟用户一样,权限也就是Django模
型中的 django.contrib.auth.models 。因
此如果你愿意,你也可以通过Django
的数据库API直接操作权限。
组
组提供了一种通用的方式来让你按照
一定的权限规则和其他标签将用户分
类。 一个用户可以隶属于任何数量
的组。
在一个组中的用户自动获得了赋予该
组的权限。 例如, Site editors 组拥
有 can_edit_home_page 权限,任何在
该组中的用户都拥有这个权限。
组也可以通过给定一些用户特殊的标
记,来扩展功能。 例如,你创建了
一个 'Special users' 组,并且允许组
中的用户访问站点的一些VIP部分,
或者发送VIP的邮件消息。
和用户管理一样,admi n接口是管理
组的最简单的方法。 然而,组也就
是Django模型
django.contrib.auth.models ,因此你可
以使用Django的数据库API,在底层
访问这些组。
消息
消息系统会为给定的用户接收消息。
每个消息都和一个 User 相关联。
在每个成功的操作以后,Django的
admi n管理接口就会使用消息机制。
例如,当你创建了一个对象,你会在
admi n页面的顶上看
到 The object was created successfully
的消息。
你也可以使用相同的API在你自己的
应用中排队接收和显示消息。 API非
常地简单:
要创建一条新的消息,使
用 user.message_set.create(message
'message_text') 。
=
要获得/删除消息,使
用 user.get_and_delete_messages()
,
这会返回一个 Message 对象的
列表,并且从队列中删除返回的
项。
在例子视图中,系统在创建了播放单
(
playlist)以后,为用户保存了一条
消息。
def create_playlist(request, songs):
Create the playlist with the g
#
iven songs.
...
#
request.user.message_set.create(
message="Your playlist was a
dded successfully."
)
return render_to_response("playl
ists/create.html",
context_instance=RequestCont
ext(request))
当使用 RequestContext ,当前登录的
用户以及他(她)的消息,就会以模
板变量 {{ messages }} 出现在模板的
context中。
{
<
% if messages %}
ul>
{
<
{
% for message in messages %}
li>{{ message }}</li>
% endfor %}
<
{
/ul>
% endif %}
需要注意的是 RequestContext 会在后
台调用 get_and_delete_messages ,因
此即使你没有显示它们,它们也会被
删除掉。
最后注意,这个消息框架只能服务于
在用户数据库中存在的用户。 如果
要向匿名用户发送消息,请直接使用
会话框架。
下一章
是的,会话和认证系统有太多的东西
要学。 大多数情况下,你并不需要
本章所提到的所有功能。在下一章,
我们会看一下Django的缓存机制,这
是一个提高你的网页应用性能的便利
的办法。
动态网站的问题就在于它是动态的。
也就是说每次用户访问一个页面,服
务器要执行数据库查询,启动模板,
执行业务逻辑以及最终生成一个你所
看到的网页,这一切都是动态即时生
成的。 从处理器资源的角度来看,
这是比较昂贵的。
对于大多数网络应用来说,过载并不
是大问题。 因为大多数网络应用并
不是washingtonpost.com或Slashdot;
它们通常是很小很简单,或者是中等
规模的站点,只有很少的流量。 但
是对于中等至大规模流量的站点来
说,尽可能地解决过载问题是非常必
要的。
这就需要用到缓存了。
缓存的目的是为了避免重复计算,特
别是对一些比较耗时间、资源的计
算。 下面的伪代码演示了如何对动
态页面的结果进行缓存。
given a URL, try finding that page i
n the cache
if the page is in the cache:
return the cached page
else:
generate the page
save the generated page in the c
ache (for next time)
return the generated page
为此,Django提供了一个稳定的缓存
系统让你缓存动态页面的结果,这样
在接下来有相同的请求就可以直接使
用缓存中的数据,避免不必要的重复
计算。 另外Django还提供了不同粒度
数据的缓存,例如: 你可以缓存整
个页面,也可以缓存某个部分,甚至
缓存整个网站。
Django也和”上游”缓存工作的很好,
例如Squid(http://www.squid-cache.org)
和基于浏览器的缓存。 这些类型的
缓存你不直接控制,但是你可以提供
关于你的站点哪部分应该被缓存和怎
样缓存的线索(通过HTTP头部)给它们
设定缓存
缓存系统需要一些少量的设定工作。
也就是说,你必须告诉它缓存的数据
应该放在哪里,在数据库中,在文件
系统,或直接在内存中。 这是一个
重要的决定,影响您的高速缓存的性
能,是的,有些类型的缓存比其它类
型快。
缓存设置在settings文件
的 CACHE_BACKEND中。 这里是一
个CACHE_BACKEND所有可用值的
解释。
内存缓冲
Memcached是迄今为止可用于Django
的最快,最有效的缓存类型,
Memcached是完全基于内存的缓存框
架,最初开发它是用以处理高负荷的
LiveJournal.com随后由Danga
Interactive公司开源。 它被用于一些
站点,例如Facebook和维基百科网
站,以减少数据库访问,并大幅提高
站点的性能。
Memcached是免费的
(
http://danga.com/memcached)。它
定数量的内存。 它只是提供了添
加,检索和删除缓存中的任意数据的
高速接口。 所有数据都直接存储在
内存中,所以没有对使用的数据库或
文件系统的开销。
在安装了Memcached本身之后,你将
需要安装Memcached Python绑定,它
没有直接和Django绑定。 这两个可用
版本。 选择和安装以下模块之一:
最快的可用选项是一个模块,称
为cmemcache,在
http://gijsbert.org/cmemcache 。
如果您无法安装cmemcache,您可
以安装python - Memcached,在
ftp: / / ftp.tummy.com/ pub/ python-
memcached/。如果该网址已不再
有效,只要到Memcached的网站
http://www.danga.com/memcached/
),并从客户端API完成Python绑
定。
若要使用Memcached的Django,设置
CACHE_BACKEND到memcached:/ /
IP:port/,其中IP是Memcached的守
护进程的IP地址,port是Memcached
运行的端口。
在这个例子中,Memcached运行在本
地主机 (127.0.0.1)上,端口为11211:
CACHE_BACKEND = 'memcached://127.0.0
.1:11211/'
Memcached的一个极好的特性是它在
多个服务器间分享缓存的能力。 这
意味着您可以在多台机器上运行
Memcached的守护进程,该程序会把
这些机器当成一个单一缓存,而无需
重复每台机器上的缓存值。 要充分
利用此功能,请在
CACHE_BACKEND里引入所有服务
器的地址,用分号分隔。
这个例子中,缓存在运行在IP地址为
172.19.26.240和172.19.26.242,端口
号为11211的Memcached实例间分享:
CACHE_BACKEND = 'memcached://172.19.
2
6.240:11211;172.19.26.242:11211/'
这个例子中,缓存在运行在
172.19.26.240(端口11211),
172.19.26.242(端口11212),
172.19.26.244(端口11213)的
Memcached实例间分享:
CACHE_BACKEND = 'memcached://172.19.
2
.
6.240:11211;172.19.26.242:11212;172
19.26.244:11213/'
最后有关Memcached的一点是,基于
内存的缓存有一个重大的缺点。 由
于缓存的数据存储在内存中,所以如
果您的服务器崩溃,数据将会消失。
显然,内存不是用来持久化数据的,
因此不要把基于内存的缓存作为您唯
一的存储数据缓存。 毫无疑问,在
Django的缓存后端不应该用于持久
化,它们本来就被设计成缓存的解决
方案。但我们仍然指出此点,这里是
因为基于内存的缓存是暂时的。
数据库缓存
为了使用数据库表作为缓存后端,首
先在数据库中运行这个命令以创建缓
存表:
python manage.py createcachetable [c
ache_table_name]
这里的[cache_table_name]是要创建的
数据库表名。 (这个名字随你的
便,只要它是一个有效的表名,而且
不是已经在您的数据库中使用的表
名。)这个命令以Django的数据库缓
存系统所期望的格式创建一个表。
一旦你创建了数据库表,把你的
CACHE_BACKEND设置
为”db://tablename”,这里的tablename
是数据库表的名字,在这个例子中,
缓存表名为my_cache_table: 在这个例
子中,高速缓存表的名字是
my_cache_table:
CACHE_BACKEND = 'db://my_cache_table'
数据库缓存后端使用你的settings文件
指定的同一数据库。 你不能为你的
缓存表使用不同的数据库后端 .
如果你已经有了一个快速,良好的索
引数据库服务器,那么数据库缓存的
效果最明显。
文件系统缓存
要把缓存项目放在文件系统上,请为
CACHE_BACKEND使用”file://“的缓
存类型。例如,要把缓存数据存储
在/var/tmp/django_cache上,请使用此
设置:
CACHE_BACKEND = 'file:///var/tmp/dja
ngo_cache'
注意例子中开头有三个斜线。 头两
项是file://,第三个是第一个字符的
目录路径,/var/tmp/django_cache。如
果你使用的是Windows,在file://之后
加上文件的驱动器号:
file://c:/foo/bar
目录路径应该是绝对路径,即应该以
你的文件系统的根开始。 在设置的
结尾放置斜线与否无关紧要。
确认该设置指向的目录存在并且你的
Web服务器运行的系统的用户可以读
写该目录。 继续上面的例子,如果
你的服务器以用户apache运行,确
认/var/tmp/django_cache存在并且用户
apache可以读写/var/tmp/django_cache
目录。
每个缓存值将被存储为单独的文件,
其内容是Python的pickle模块以序列化
(“pickled”)形式保存的缓存数据。 每
个文件的名称是缓存键,以规避开安
全文件系统的使用。
本地内存缓存
如果你想利用内存缓存的速度优势,
但又不能使用Memcached,可以考虑
使用本地存储器缓存后端。 此缓存
的多进程和线程安全。 设
置 CACHE_BACKEND 为 locmem:///
来使用它,例如 :
CACHE_BACKEND = 'locmem:///'
请注意,每个进程都有自己私有的缓
存实例,这意味着跨进程缓存是不可
能的。 这显然也意味着本地内存缓
存效率并不是特别高,所以对产品环
境来说它可能不是一个好选择。 对
开发来说还不错。
仿缓存(供开发时使用)
最后,Django提供了一个假缓存(只
是实现了缓存接口,实际上什么都不
做)。
假如你有一个产品站点,在许多地方
使用高度缓存,但在开发/测试环境
中,你不想缓存,也不想改变代码,
这就非常有用了。 要激活虚拟缓
存,就像这样设置
CACHE_BACKEND:
CACHE_BACKEND = 'dummy:///'
使用自定义缓存后端
尽管Django包含对许多缓存后端的支
持,在某些情况下,你仍然想使用自
定义缓存后端。 要让Django使用外部
缓存后端,需要使用一个Python
import路径作为的CACHE_BACKEND
URI的(第一个冒号前的部分),像
这样:
CACHE_BACKEND = 'path.to.backend://'
如果您构建自己的后端,你可以参考
标准缓存后端的实现。 源代码在
Django的代码目录的
django/core/cache/backends/下。
注意 如果没有一个真正令人信服的
理由,比如主机不支持,你就应该坚
持使用Django包含的缓存后端。 它们
经过大量测试,并且易于使用。
CACHE_BACKEND参数
每个缓存后端都可能使用参数。 它
们在CACHE_BACKEND设置中以查
询字符串形式给出。 有效参数如
下:
ti meout:用于缓存的过期时间,以
秒为单位。 这个参数默认被设置
为300秒(五分钟)。
max_entries:对于内存,文件系统
和数据库后端,高速缓存允许的
最大条目数,超出这个数则旧值
将被删除。 这个参数默认是300。
cull_percentage :当达
到 max_entries 的时候,被删除的条
目比率。 实际的比率
是 1/cull_percentage ,所以设置
cull_frequency=2就是在达
到 max_entries 的时候去除一半数
量的缓存。
把 cull_frequency 的值设置为 0 意
味着当达到 max_entries 时,缓存将
被清空。 这将以很多缓存丢失为
代价,大大提高接受访问的速度。
在这个例子中, ti meout 被设成 60
CACHE_BACKEND = "memcached://127.0.0
.
1:11211/?timeout=60"
而在这个例子中, ti meout 设
为 30 而 max_entries 为 400 :
CACHE_BACKEND = "locmem:///?timeout=
3
0&max_entries=400"
其中,非法的参数与非法的参数值都
将被忽略。
站点级 Cache
一旦高速缓存设置,最简单的方法是
使用缓存缓存整个网站。 您 需要添
加’django.middleware.cache.UpdateCac
heMiddleware’和
‘
django.middleware.cache.FetchFromCa
cheMiddleware’到您的
MIDDLEWARE_CLASSES设置中,在
这个例子中是:
MIDDLEWARE_CLASSES = (
'
django.middleware.cache.UpdateC
acheMiddleware',
'
django.middleware.common.Common
Middleware',
'
django.middleware.cache.FetchFr
omCacheMiddleware',
)
注意:
不,这里并没有排版错误: 修改
的中间件,必须放在列表的开始
位置,而fectch中间件,必须放在
最后。 细节有点费解,如果您想
了解完整内幕请参看下面的
MIDDLEWARE_CLASSES顺序。
然后,在你的Django settings文件里加
入下面所需的设置:
CACHE_MIDDLEWARE_SECOND
S :每个页面应该被缓存的秒数。
CACHE_MIDDLEWARE_KEY_PR
EFIX :如果缓存被多个使用相同
Django安装的网站所共享,那么
把这个值设成当前网站名,或其
他能代表这个Django实例的唯一
字符串,以避免key发生冲突。 如
果你不在意的话可以设成空字符
串。
缓存中间件缓存每个没有GET或者
POST参数的页面。 或者,如果
CACHE_MIDDLEWARE_ANONYMO
US_ONLY设置为Tr ue,只有匿名请求
(即不是由登录的用户)将被缓存。
如果想取消用户相关页面(user-
specific pages)的缓存,例如Djangos
的管理界面,这是一种既简单又有效
的方法。
CACHE_MIDDLEWARE_ANONYMO
US_ONLY,你应该确保你已经启动
AuthenticationMiddleware。
此外,缓存中间件为每个
HttpResponse自动设置了几个头部信
息:
当一个新(没缓存的)版本的页面被
请求时设置Last-Modified头部为当
前日期/时间。
设置Expires头部为当前日期/时间
加上定义的
CACHE_MIDDLEWARE_SECOND
S。
设置Cache-Control头部来给页面
一个最长的有效期,值来自于
CACHE_MIDDLEWARE_SECOND
S设置。
参阅更多的中间件第17章。
如果视图设置自己的缓存到期时间
(即 它有一个最大年龄在头部信息
的Cache-Control中),那么页面将缓
存直到过期,而不是
CACHE_MIDDLEWARE_SECONDS
。使用django.views.decorators.cache
装饰器,您可以轻松地设置视图的到
期时间(使用cache_control装饰器)
或禁用缓存视图(使用never_cache装
饰器)。 请参阅下面的”使用其他头
部信息“小节以了解装饰器的更多信
息。
视图级缓存
更加颗粒级的缓存框架使用方法是对
单个视图的输出进行缓
存。 django.views.decorators.cache 定
义了一个自动缓存视图响应的
cache_page装饰器。 他是很容易使用
的:
from django.views.decorators.cache i
mport cache_page
def my_view(request):
#
...
my_view = cache_page(my_view, 60 * 1
5
)
也可以使用Python2.4的装饰器语法:
@
cache_page(60 * 15)
def my_view(request):
...
#
cache_page 只接受一个参数: 以秒计
的缓存超时时间。 在前例中,
“
my_view()” 视图的结果将被缓存 15
分钟。 (注意: 为了提高可读性,
该参数被书写为 60 15 。 60 15 将被
计算为 900 ,也就是说15 分钟乘以
每分钟 60 秒。)
和站点缓存一样,视图缓存与 URL
无关。 如果多个 URL指向同一视
图,每个视图将会分别缓存。 继续
my_vi ew 范例,如果 URLconf 如下所
示:
urlpatterns = ('',
(r'^foo/(\d{1,2})/$', my_view),
)
那么正如你所期待的那样,发送
到 /foo/1/ 和 /foo/23/ 的请求将会分别
缓存。 但一旦发出了特定的请求
(如: /foo/23/ ),之后再度发出的
指向该 URL的请求将使用缓存。
在 URLconf 中指定视图缓
存
前一节中的范例将视图硬编码为使用
缓存,因为 cache_page 在适当的位置
对 my_vi ew 函数进行了转换。 该方
法将视图与缓存系统进行了耦合,从
几个方面来说并不理想。 例如,你
可能想在某个无缓存的站点中重用该
视图函数,或者你可能想将该视图发
布给那些不想通过缓存使用它们的
人。 解决这些问题的方法是在
URLconf 中指定视图缓存,而不是紧
挨着这些视图函数本身来指定。
完成这项工作非常简单: 在 URLconf
中用到这些视图函数的时候简单地包
裹一个 cache_page 。以下是刚才用到
过的 URLconf : 这是之前的URLconf:
urlpatterns = ('',
(r'^foo/(\d{1,2})/$', my_view),
)
以下是同一个 URLconf ,不过
用 cache_page 包裹了 my_vi ew :
from django.views.decorators.cache i
mport cache_page
urlpatterns = ('',
(r'^foo/(\d{1,2})/$', cache_page
(my_view, 60 * 15)),
)
如果采取这种方法, 不要忘记在
URLconf 中导入 cache_page。
模板碎片缓存
你同样可以使用cache标签来缓存模
板片段。 在模板的顶端附近加入
{
% load cache %}以通知模板存取缓
存标签。
模板标签{% cache %}在给定的时间
内缓存了块的内容。 它至少需要两
个参数: 缓存超时时间(以秒计)和
指定缓存片段的名称。 示例:
{
{
% load cache %}
% cache 500 sidebar %}
.
. sidebar ..
{
% endcache %}
有时你可能想缓存基于片段的动态内
容的多份拷贝。 比如,你想为上一
个例子的每个用户分别缓存侧边栏。
这样只需要给{% cache %}传递额外
的参数以标识缓存片段。
{
{
% load cache %}
% cache 500 sidebar request.user.us
ername %}
.
. sidebar for logged in user ..
{
% endcache %}
传递不止一个参数也是可行的。 简
单地把参数传给{% cache %}。
缓存超时时间可以作为模板变量,只
要它可以解析为整数值。 例如,如
果模板变量my_ti me out值为600,那么
以下两个例子是等价的。
{
% cache 600 sidebar %} ... {% endca
che %}
{
%
% cache my_timeout sidebar %} ... {
endcache %}
这个特性在避免模板重复方面非常有
用。 可以把超时时间保存在变量
里,然后在别的地方复用。
低层次缓存API
有些时候,对整个经解析的页面进行
缓存并不会给你带来太多好处,事实
上可能会过犹不及。
比如说,也许你的站点所包含的一个
视图依赖几个费时的查询,每隔一段
时间结果就会发生变化。 在这种情
况下,使用站点级缓存或者视图级缓
存策略所提供的整页缓存并不是最理
想的,因为你可能不会想对整个结果
进行缓存(因为一些数据经常变
化),但你仍然会想对很少变化的部
分进行缓存。
针对这样的情况,Django提供了简单
低级的缓存API。 你可以通过这个
API,以任何你需要的粒度来缓存对
象。 你可以对所有能够安全进行
pickle 处理的 Python 对象进行缓存:
字符串、字典和模型对象列表等等。
(查阅 Python 文档可以了解到更多关
于 pickling 的信息。)
缓存模块django.core.cache拥有一个自
动依据CACHE_BACKEND设置创建
的django.core.cache对象。
>
>> from django.core.cache import ca
che
基本的接口
是 set(key, value, timeout_seconds) 和 g
et(key) :
>
>> cache.set('my_key', 'hello, worl
d!', 30)
>
'
>> cache.get('my_key')
hello, world!'
timeout_seconds 参数是可选的, 并且
默认为前面讲过
的 CACHE_BACKEND 设置中
的 ti meout 参数.
如果缓存中不存在该对象,那么
cache.get()会返回None。
#
Wait 30 seconds for 'my_key' to ex
pire...
>
>> cache.get('my_key')
None
我们不建议在缓存中保存 None 常
量,因为你将无法区分你保存
的 None 变量及由返回值 None 所标
识的缓存未命中。
cache.get() 接受一个 缺省 参数。 它
指定了当缓存中不存在该对象时所返
回的值:
>
'
'
>> cache.get('my_key', 'has expired
)
has expired'
使用add()方法来新增一个原来没有的
键值。 它接受的参数和set()一样,但
是并不去尝试更新已经存在的键值。
>
>> cache.set('add_key', 'Initial va
lue')
>
)
>
'
>> cache.add('add_key', 'New value'
>> cache.get('add_key')
Initial value'
如果想确定add()是否成功添加了缓存
值,你应该测试返回值。 成功返回
Tr ue,失败返回False。
还有个get_many() 接
口。 get_many() 所返回的字典包括了
你所请求的存在于缓存中且未超时的
所有键值。
>
>
>
>
{
>> cache.set('a', 1)
>> cache.set('b', 2)
>> cache.set('c', 3)
>> cache.get_many(['a', 'b', 'c'])
'a': 1, 'b': 2, 'c': 3}
最后,你可以用 cache.delete() 显式地
删除关键字。
>
>> cache.delete('a')
也可以使用incr()或者decr()来增加或
者减少已经存在的键值。 默认情况
下,增加或减少的值是1。可以用参
数来制定其他值。 如果尝试增减不
存在的键值会抛出ValueError。
>
>
2
>
1
>> cache.set('num', 1)
>> cache.incr('num')
>> cache.incr('num', 10)
2
>
1
>
6
>> cache.decr('num')
1
>> cache.decr('num', 5)
注意
incr()/decr()方法不是原子操作。
在支持原子增减的缓存后端上
(最著名的是memcached),增减
操作才是原子的。 然而,如果后
端并不原生支持增减操作,也可
以通过取值/更新两步操作来实
现。
上游缓存
目前为止,本章的焦点一直是对
你 自己的 数据进行缓存。 但还有一
种与 We b 开发相关的缓存: 上游缓
存。 有一些系统甚至在请求到达站
点之前就为用户进行页面缓存。
下面是上游缓存的几个例子:
你的 ISP (互联网服务商)可能会对
特定的页面进行缓存,因此如果
你向 http://example.com/ 请求一个
页面,你的 ISP 可能无需直接访
问 example.com 就能将页面发送给
你。 而 example.com 的维护者们
却无从得知这种缓存,ISP 位于
example.com 和你的网页浏览器之
间,透明地处理所有的缓存。
你的 Django 网站可能位于某个 代
理缓存 之后,例如 Squid 网页代
理缓存 (http://www.squid-
对页面进行缓存。 在此情况下 ,
每个请求将首先由代理服务器进
行处理,然后仅在需要的情况下
才被传递至你的应用程序。
你的网页浏览器也对页面进行缓
存。 如果某网页送出了相应的头
部,你的浏览器将在为对该网页
的后续的访问请求使用本地缓存
的拷贝,甚至不会再次联系该网
页查看是否发生了变化。
上游缓存将会产生非常明显的效率提
升,但也存在一定风险。 许多网页
的内容依据身份验证以及许多其他变
量的情况发生变化,缓存系统仅盲目
地根据 URL保存页面,可能会向这
些页面的后续访问者暴露不正确或者
敏感的数据。
举个例子,假定你在使用网页电邮系
统,显然收件箱页面的内容取决于登
录的是哪个用户。 如果 ISP 盲目地缓
存了该站点,那么第一个用户通过该
ISP 登录之后,他(或她)的用户收
件箱页面将会缓存给后续的访问者。
这一点也不好玩。
幸运的是, HTTP 提供了解决该问题
的方案。 已有一些 HTTP 头标用于指
引上游缓存根据指定变量来区分缓存
内容,并通知缓存机制不对特定页面
进行缓存。 我们将在本节后续部分
将对这些头标进行阐述。
使用 Vary头部
Vary 头部定义了缓存机制在构建其缓
存键值时应当将哪个请求头标考虑在
内。 例如,如果网页的内容取决于
用户的语言偏好,该页面被称为根据
语言而不同。
缺省情况下,Django 的缓存系统使用
所请求的路径(比
如:"/stories/2005/jun/23/bank_robbed
/
" )来创建其缓存键。这意味着每次
请求都会使用同样的缓存版本,不考
虑才客户端cookie和语言配置的不
同。 除非你使用Vary头部通知缓存机
制页面输出要依据请求头里的
cookie,语言等的设置而不同。
要在 Django 完成这项工作,可使用
便利的 vary_on_headers 视图装饰
器,如下所示:
from django.views.decorators.vary im
port vary_on_headers
#
Python 2.3 syntax.
def my_view(request):
...
#
my_view = vary_on_headers(my_view, '
User-Agent')
#
@
Python 2.4+ decorator syntax.
vary_on_headers('User-Agent')
def my_view(request):
...
#
在这种情况下,缓存机制(如 Django
自己的缓存中间件)将会为每一个单
独的用户浏览器缓存一个独立的页面
版本。
使用 vary_on_headers 装饰器而不是
手动设置 Vary 头部(使用
像 response['Vary'] = 'user-agent' 之类
的代码)的好处是修饰器在(可能已
经存在的) Vary 之上进行 添加 ,而
不是从零开始设置,且可能覆盖该处
已经存在的设置。
你可以向 vary_on_headers() 传入多个
头标:
@
vary_on_headers('User-Agent', 'Cook
ie')
def my_view(request):
...
#
该段代码通知上游缓存对 两者 都进
行不同操作,也就是说 user-agent 和
cookie 的每种组合都应获取自己的缓
存值。 举例来说,使用 Mozilla 作为
user-agent 而 foo=bar 作为 cookie 值的
请求应该和使用 Mozilla 作为 user-
agent 而 foo=ham 的请求应该被视为
不同请求。
由于根据 cookie 而区分对待是很常见
的情况,因此有 vary_on_cookie 装饰
器。 以下两个视图是等效的:
@
vary_on_cookie
def my_view(request):
...
#
@
vary_on_headers('Cookie')
def my_view(request):
...
#
传入 vary_on_headers 头标是大小写
不敏感的; "User-Agent" 与 "user-
agent" 完全相同。
你也可以直接使用帮助函数:
django.utils.cache.patch_vary_headers
。
该函数设置或增加 Vary header ,
例如:
from django.utils.cache import patch
_vary_headers
def my_view(request):
...
#
response = render_to_response('t
emplate_name', context)
patch_vary_headers(response, ['C
ookie'])
return response
patch_vary_headers 以一
个 HttpResponse 实例为第一个参数,
以一个大小写不敏感的头标名称列表
或元组为第二个参数。
控制缓存: 使用其它
头部
关于缓存剩下的问题是数据的隐私性
以及在级联缓存中数据应该在何处储
存的问题。
通常用户将会面对两种缓存: 他或
她自己的浏览器缓存(私有缓存)以
及他或她的提供者缓存(公共缓
存)。 公共缓存由多个用户使用,
而受其他某人的控制。 这就产生了
你不想遇到的敏感数据的问题,比如
说你的银行账号被存储在公众缓存
中。 因此,We b 应用程序需要以某
种方式告诉缓存那些数据是私有的,
哪些是公共的。
解决方案是标示出某个页面缓存应当
是私有的。 要在 Django 中完成此项
工作,可使用 cache_control 视图修饰
器: 例如:
from django.views.decorators.cache i
mport cache_control
@
cache_control(private=True)
def my_view(request):
...
#
该修饰器负责在后台发送相应的
HTTP 头部。
还有一些其他方法可以控制缓存参
数。 例如, HTTP 允许应用程序执行
如下操作 :
定义页面可以被缓存的最大时
间。
指定某个缓存是否总是检查较新
版本,仅当无更新时才传递所缓
存内容。 (一些缓存即便在服务
器页面发生变化的情况下仍然会
传送所缓存的内容,只因为缓存
拷贝没有过期。)
在 Django 中,可使
用 cache_control 视图修饰器指定这些
缓存参数。 在本例
中, cache_control 告诉缓存对每次访
问都重新验证缓存并在最长 3600 秒
内保存所缓存版本:
from django.views.decorators.cache i
mport cache_control
@
cache_control(must_revalidate=True,
max_age=3600)
def my_view(request):
#
...
在 cache_control() 中,任何合法的
Cache-Control HTTP 指令都是有效
的。下面是完整列表:
public=True
private=True
no_cache=True
no_transform=True
must_revalidate=True
proxy_revalidate=True
max_age=num_seconds
s_maxage=num_seconds
缓存中间件已经使
用 CACHE_MIDDLEWARE_SETTING
S 设置设定了缓存头部 max- age 。 如
果你在cache_control修饰器中使用了
自定义的max_age,该修饰器将会取
得优先权,该头部的值将被正确地被
合并。
如果你想用头部完全禁掉缓存,
django.views.decorators.cache.never_ca
che装饰器可以添加确保响应不被缓
存的头部信息。 例如:
from django.views.decorators.cache i
mport never_cache
@
never_cache
def myview(request):
...
#
其他优化
Django 带有一些其它中间件可帮助您
优化应用程序的性能 :
django.middleware.http.Conditional
GetMiddleware 为现代浏览器增加
了有条件的,基于 ETag 和Last-
Modified 头标的GET响应的相关
支持。
django.middleware.gzip.GZipMiddle
ware 为所有现代浏览器压缩响应
内容,以节省带宽和传送时间。
MIDDLEWARE_CLA
SSES 的顺序
如果使用缓存中间件,注意在
MIDDLEWARE_CLASSES设置中正确
配置。 因为缓存中间件需要知道哪
些头部信息由哪些缓存区来区分。
中间件总是尽可能得想Vary响应头中
添加信息。
UpdateCacheMiddleware在相应阶段运
行。因为中间件是以相反顺序运行
的,所有列表顶部的中间件反而
_
last_在相应阶段的最后运行。 所
有,你需要确保
UpdateCacheMiddleware排在任何可能
往_Vary_头部添加信息的中间件之
前。 下面的中间件模块就是这样
的:
添加 Cookie 的 SessionMiddleware
添加 Accept-
Encoding 的 GZipMiddleware
添加Accept-Language 的
LocaleMiddleware
另一方面,
FetchFromCacheMiddleware在请求阶
段运行,这时中间件循序执行,所以
列表顶端的项目会_首先_执
行。 FetchFromCacheMiddleware也需
要在会修改Vary头部的中间件之后运
行,所以FetchFromCacheMiddleware
必须放在它们后面。
下一章
Django捆绑了一系列可选的方便特
性。 我们已经介绍了一些: admi n站
点(第六章)和session/user框架(第
十四章)。 下一章中,我们将讲述
Django中其他的子框架。
Python有众多优点,其中之一就是“开
机即用”原则: 安装Python的同时会
安装好大量的标准软件包,这样 你
可以立即使用而不用自己去下载。
Django也遵循这个原则,它同样包含
了自己的标准库。 这一章就来讲 这
些集成的子框架。
Django标准库
Django的标准库存放
在 django.contrib 包中。每个子包都
是一个独立的附加功能包。 这些子
包一般是互相独立的,不过有些
django.contrib子包需要依赖其他子
包。
在 django.contrib 中对函数的类型并
没有强制要求 。其中一些包中带有
模型(因此需要你在数据库中安装对
应的数据表),但其它一些由独立的
中间件及模板标签组成。
django.contrib 开发包共有的特性是:
就算你将整个django.contrib开发包删
除,你依然可以使用 Django 的基础
功能而不会遇到任何问题。 当
Django 开发者向框架增加新功能的
时,他们会严格根据这一原则来决定
是否把新功能放入django.contrib中。
django.contrib 由以下开发包组成:
admin : 自动化的站点管理工具。
请查看第6章。
admindocs:为Django admi n站点提
供自动文档。 本书没有介绍这方
面的知识;详情请参阅Django官
方文档。
auth : Django的用户验证框架。 参
见第十四章。
c omme nts : 一个评论应用,目前,
这个应用正在紧张的开发中,因
此在本书出版的时候还不能给出
一个完整的说明,关于这个应用
的更多信息请参见Django的官方
网站. 本书没有介绍这方面的知
识;详情请参阅Django官方文
档。
contenttypes : 这是一个用于引入文
档类型的框架,每个安装的
Django模块作为一种独立的文档
类型。 这个框架主要在Django内
部被其他应用使用,它主要面向
Django的高级开发者。 可以通过
阅读源码来了解关于这个框架的
更多信息,源码的位置
在 django/contrib/contenttypes/ 。
csrf : 这个模块用来防御跨站请求
伪造(CSRF)。参 见后面标题
为”CSRF 防御”的小节。
databrowse:帮助你浏览数据的
Django应用。 本书没有介绍这方
面的知识;详情请参阅Django官
方文档。
flatpages : 一个在数据库中管理单
一HTML内容的模块。 参见后面
标题为“Flatpages”的小节。
formtools:一些列处理表单通用
模式的高级库。 本书没有介绍这
方面的知识;详情请参阅Django
官方文档。
gis:为Django提供
GIS(Geographic Information
Systems)支持的扩展。 举个例
子,它允许你的Django模型保存
地理学数据并执行地理学查询。
这个库比较复杂,本书不详细介
绍。 请参看http://geodjango.org/上
的文档。
humanize : 一系列 Django 模块过
滤器,用于增加数据的人性化。
参阅稍后的章节《人性化数
据》。
localflavor:针对不同国家和文化
的混杂代码段。 例如,它包含了
验证美国的邮编 以及爱尔兰的身
份证号的方法。
ma r kup : 一系列的 Django 模板过
滤器,用于实现一些常用标记语
言。 参阅后续章节《标记过滤
器》。
redirects : 用来管理重定向的框
架。 参看后面的“重定向”小节。
sessions : Django 的会话框架。 参
见14章。
sitemaps : 用来生成网站地图的
XML文件的框架。 参见13章。
sites : 一个让你可以在同一个数据
库与 Django 安装中管理多个网站
的框架。 参见下一节:
syndication : 一个用 RSS 和 Atom
来生成聚合订阅源的的框架。 参
见13章。
webdesign:对设计者非常有用的
Django扩展。 到编写此文时,它
只包含一个模板标签
% lorem %}。详情参阅Django文
{
档。
本章接下来将详细描述前面没有介绍
过的 django.contrib 开发包内容。
多个站点
Django 的多站点系统是一种通用框
架,它让你可以在同一个数据库和同
一个Django项目下操作多个网站。 这
是一个抽象概念,理解起来可能有点
困难,因此我们从几个让它能派上用
场的实际情景入手。
情景1:多站点间复用数据
正如我们在第一章里所讲,Django 构
建的网站 LJWorld.com 和
Lawrance.com 是用由同一个新闻组织
控制的: 肯萨斯州劳伦斯市的 劳伦
斯日报世界 报纸。 LJWorld.com 主要
做新闻,而 Lawrence.com 关注本地
娱乐。 然而有时,编辑可能需要把
一篇文章发布到 两个 网站上。
解决此问题的死脑筋方法可能是使用
每个站点分别使用不同的数据库,然
后要求站点维护者把同一篇文章发布
两次: 一次为 LJWorld.com,另一次
为Lawrence.com。 但这对站点管理员
来说是低效率的,而且为同一篇文章
在数据库里保留多个副本也显得多
余。
更好的解决方案? 两个网站用的是
同一个文章数据库,并将每一篇文章
与一个或多个站点用多对多关系关联
起来。 Django 站点框架提供数据库
表来记载哪些文章可以被关联。 它
是一个把数据与一个或多个站点关联
起来的钩子。
情景2:把网站的名字/域名
保存在一个地方
LJWorld.com 和 Lawrence.com 都有邮
件提醒功能,使读者注册后可以在新
闻发生后立即收到通知。 这是一种
完美的的机制: 某读者提交了注册
表单,然后马上就受到一封内容
是“感谢您的注册”的邮件。
把这个注册过程的代码实现两遍显然
是低效、多余的,因此两个站点在后
台使用相同的代码。 但感谢注册的
通知在两个网站中需要不同。 通过
使用 Site 对象,我们通过使用当前站
点的 na me (例如 'LJWorld.com' )和
domain (例如 'www.ljworld.com' )可以
把感谢通知抽提出来。
Django 的多站点框架为你提供了一个
位置来存储 Django 项目中每个站点
的 na me 和 domain ,这意味着你可以
用同样的方法来重用这些值。
如何使用多站点框架
多站点框架与其说是一个框架,不如
说是一系列约定。 所有的一切都基
于两个简单的概念:
位于 django.contrib.sites 的 Site 模
型有 domain 和 na me 两个字段。
SITE_ID 设置指定了与特定配置
文件相关联的 Site 对象之数据库
ID 。
如何运用这两个概念由你决定,但
Django 是通过几个简单的约定自动使
用的。
安装多站点应用要执行以下几个步
骤:
1
. 将 'django.contrib.sites' 加入
到 INSTALLED_APPS 中。
2
. 运行 manage.py syncdb 命令
将 django_site 表安装到数据库
中。 这样也会建立默认的站点对
象,域名为 exampl e.com。
3. 把exampl e.com改成你自己的域
名,然后通过Django admi n站点或
Python API来添加其他Site对象。
为该 Django 项目支撑的每个站
(或域)创建一个 Site 对象。
4
. 在每个设置文件中定义一
个 SITE_ID 变量。 该变量值应当
是该设置文件所支撑的站点
Site 对象的数据库 ID。
多站点框架的功能
下面几节讲述的是用多站点框架能够
完成的几项工作。
多个站点的数据重用
正如在情景一中所解释的,要在多个
站点间重用数据,仅需在模型中
为 Site 添加一个 多对多字段 即可,
例如:
from django.db import models
from django.contrib.sites.models imp
ort Site
class Article(models.Model):
headline = models.CharField(max_
length=200)
#
...
sites = models.ManyToManyField(S
ite)
这是在数据库中为多个站点进行文章
关联操作的基础步骤。 在适当的位
置使用该技术,你可以在多个站点中
重复使用同一段 Django 视图代码。
继续 Article 模型范例,下面是一个
可能的 article_detail 视图:
from django.conf import settings
from django.shortcuts import get_obj
ect_or_404
from mysite.articles.models import A
rticle
def article_detail(request, article_
id):
a = get_object_or_404(Article, i
d=article_id, sites__id=settings.SIT
E_ID)
#
...
该视图方法是可重用的,因为它根
据 SITE_ID 设置的值动态检查
articles 站点。
例如, LJWorld.coms 设置文件中有
有个 SITE_ID 设置为 1 ,而
Lawrence.coms 设置文件中有
个 SITE_ID 设置为 2 。如果该视图在
LJWorld.coms 处于激活状态时被调
用,那么它将把查找范围局限于站点
列表包括 LJWorld.com 在内的文章。
将内容与单一站点相关联
同样,你也可以使用 外键 在多对一
关系中将一个模型关联到 Site 模型。
举例来说,如果某篇文章仅仅能够出
现在一个站点上,你可以使用下面这
样的模型:
from django.db import models
from django.contrib.sites.models imp
ort Site
class Article(models.Model):
headline = models.CharField(max_
length=200)
#
...
site = models.ForeignKey(Site)
这与前一节中介绍的一样有益。
从视图钩挂当前站点
在底层,通过在 Django 视图中使用
多站点框架,你可以让视图根据调用
站点不同而完成不同的工作,例如:
from django.conf import settings
def my_view(request):
if settings.SITE_ID == 3:
#
else:
#
Do something.
Do something else.
当然,像那样对站点 ID 进行硬编码
是比较难看的。 略为简洁的完成方
式是查看当前的站点域:
from django.conf import settings
from django.contrib.sites.models imp
ort Site
def my_view(request):
current_site = Site.objects.get(
id=settings.SITE_ID)
if current_site.domain == 'foo.c
om':
#
else:
#
Do something
Do something else.
从 Site 对象中获
取 settings.SITE_ID 值的做法比较常
见,因此 Site 模型管理器
(Site.objects ) 具备一个
get_current() 方法。 下面的例子与前
一个是等效的:
from django.contrib.sites.models imp
ort Site
def my_view(request):
current_site = Site.objects.get_
current()
if current_site.domain == 'foo.c
om':
#
else:
#
Do something
Do something else.
注意
在这个最后的例子里,你不用导
入 django.conf.settings 。
获取当前域用于呈现
正如情景二中所解释的那样,依据
DRY原则(不做重复工作),你只需在
一个位置储存站名和域名,然后引用
当前Site 对象的 na me 和 domain 。例
如: 例如:
from django.contrib.sites.models imp
ort Site
from django.core.mail import send_ma
il
def register_for_newsletter(request)
:
#
Check form values, etc., and s
ubscribe the user.
...
current_site = Site.objects.get_
current()
#
send_mail('Thanks for subscribin
g to %s alerts' % current_site.name,
'
Thanks for your subscriptio
n. We appreciate it.\n\n-The %s team
.
' % current_site.name,
'
editor@%s' % current_site.d
omain,
#
[
...
user_email])
继续我们正在讨论的 LJWorld.com 和
Lawrence.com 例子,在Lawrence.com
该邮件的标题行是“感谢注册
Lawrence.com 提醒信件”。 在
LJWorld.com ,该邮件标题行是“感谢
注册 LJWorld.com 提醒信件”。 这种
站点关联行为方式对邮件信息主体也
同样适用。
完成这项工作的一种更加灵活(但更
重量级)的方法是使用 Django 的模
板系统。 假定 Lawrence.com 和
LJWorld.com 各自拥有不同的模板目
录( TEMPLATE_DIRS ),你可将工
作轻松地转交给模板系统,如下所
示:
from django.core.mail import send_ma
il
from django.template import loader,
Context
def register_for_newsletter(request)
:
#
Check form values, etc., and s
ubscribe the user.
...
#
subject = loader.get_template('a
lerts/subject.txt').render(Context({
}
))
message = loader.get_template('a
lerts/message.txt').render(Context({
))
}
send_mail(subject, message, 'do-
not-reply@example.com', [user_email]
)
#
...
本例中,你不得不在 LJWorld.com 和
Lawrence.com 的模板目录中都创建一
份 subject.txt 和 message.txt模板。 正
如之前所说,该方法带来了更大的灵
活性,但也带来了更多复杂性。
尽可能多的利用 Site 对象是减少不必
要的复杂、冗余工作的好办法。
当前站点管理器
如果 站点 在你的应用中扮演很重要
的角色,请考虑在你的模型中使用方
便的 CurrentSiteManager 。 这是一个
模型管理器(见第十章),它会自动
过滤使其只包含与当前站点相关联的
对象。
通过显示地将 CurrentSiteManager 加
入模型中以使用它。 例如:
from django.db import models
from django.contrib.sites.models imp
ort Site
from django.contrib.sites.managers i
mport CurrentSiteManager
class Photo(models.Model):
photo = models.FileField(upload_
to='/home/photos')
photographer_name = models.CharF
ield(max_length=100)
pub_date = models.DateField()
site = models.ForeignKey(Site)
objects = models.Manager()
on_site = CurrentSiteManager()
通过该模型, Photo.objects.all() 将返
回数据库中所有的 Photo 对象,
而 Photo.on_site.all() 仅根据
SITE_ID 设置返回与当前站点相关联
的 Photo 对象。
换言之,以下两条语句是等效的:
Photo.objects.filter(site=settings.S
ITE_ID)
Photo.on_site.all()
CurrentSiteManager 是如何知
道 Photo 的哪个字段是 Site 呢?缺省
情况下,它会查找一个叫做 site 的字
段。如果你的模型包含了名字不是
site的_外键_或者多对多关联,你需
要把它作为参数传给
CurrentSiteManager以显示指明。下面
的模型拥有一个publish_on字段:
from django.db import models
from django.contrib.sites.models imp
ort Site
from django.contrib.sites.managers i
mport CurrentSiteManager
class Photo(models.Model):
photo = models.FileField(upload_
to='/home/photos')
photographer_name = models.CharF
ield(max_length=100)
pub_date = models.DateField()
publish_on = models.ForeignKey(S
ite)
objects = models.Manager()
on_site = CurrentSiteManager('pu
blish_on')
如果试图使用 CurrentSiteManager 并
传入一个不存在的字段名, Django
将引发一个 ValueError 异常。
注意
即便是已经使用
了 CurrentSiteManager ,你也许还
想在模型中拥有一个正常的(非
站点相关)的 管理器 。正如在附
录 B 中所解释的,如果你手动定
义了一个管理器,那么 Django 不
会为你创建全自动的
objects = models.Manager() 管理
器。
同样,Django 的特定部分(即
Django 超级管理站点和通用视图)使
用在模型中定义 的_第一个_管理
器,因此如果希望管理站点能够访问
所有对象(而不是仅仅站点特有对
象),请于定
义 CurrentSiteManager 之前在模型中
放入 objects = models.Manager() 。
Django如何使用多站点框
架
尽管并不是必须的,我们还是强烈建
议使用多站点框架,因为 Django 在
几个地方利用了它。 即使只用
Django 来支持单个网站,你也应该花
一点时间用 domain 和 na me 来创建站
点对象,并将 SITE_ID 设置指向它的
ID 。
以下讲述的是 Django 如何使用多站
点框架:
在重定向框架中(见后面的重定
向一节),每一个重定向对象都
与一个特定站点关联。 当 Django
搜索重定向的时候,它会考虑当
前的 SITE_ID 。
在注册框架中,每个注释都与特
定站点相关。 每个注释被显示
时,其 site 被设置为当前
的 SITE_ID ,而当通过适当的模
板标签列出注释时,只有当前站
点的注释将会显示。
在 flatpages 框架中 (参见后面的
Flatpages 一节),每个 flatpage 都
与特定的站点相关联。 创建
flatpage 时,你都将指定它
的 site ,而 flatpage 中间件在获取
flatpage 以显示它的过程中,将查
看当前的 SITE_ID 。
在 syndication 框架中(参阅第 13
章), title 和 description 的模板
会自动访问变量 {{ site }} ,它其
实是代表当前站点的 Site 对象。
而且,如果你不指定一个合格的
domai n的话,提供目录URL的钩子
将会使用当前“Site”对象的
domai n。
在权限框架中(参见十四章),
视图django.contrib.auth.views.login
把当前Site名字和对象分别以
{
{ site_name }}和{{ site }}的形式
传给了模板。
Flatpages(简单页面)
尽管通常情况下总是搭建运行数据库
驱动的 We b 应用,有时你还是需要
添加一两张一次性的静态页面,例
如“关于”页面,或者“隐私策略”页面
等等。 可以用像 Apache 这样的标准
Web服务器来处理这些静态页面,但
却会给应用带来一些额外的复杂性,
因为你必须操心怎么配置 Apache,
还要设置权限让整个团队可以修改编
辑这些文件,而且你还不能使用
Django 模板系统来统一这些页面的风
格。
这个问题的解决方案是使用位
于 django.contrib.flatpages 开发包中的
Django 简单页面(flatpages)应用程
序。该应用让你能够通过 Django 管
理站点来管理这些一次性的页面,还
可以让你使用 Django 模板系统指定
它们使用哪个模板。 它在后台使用
Django模型,这意味着它把页面项别
的数据一样保存在数据库中,也就是
说你可以使用标准Django数据库API
来存取页面。
简单页面以它们的 URL和站点为键
值。 当创建简单页面时,你指定它
与哪个URL以及和哪个站点相关联 。
(有关站点的更多信息,请查阅”多
站点“一节。)
使用简单页面
安装简单页面应用程序必须按照下面
的步骤:
1. 添
加 'django.contrib.flatpages' 到 INS
TALLED_APPS 设置。
django.contrib.flatpages依赖
django.contrib.sites,所以确保它们
都在INSTALLED_APPS里。
2.
将 'django.contrib.flatpages.middlew
are.FlatpageFallbackMiddleware' 添
加到 MIDDLEWARE_CLASSES 设
置中。
3
. 运行 manage.py syncdb 命令在数据
库中创建必需的两个表。
简单页面应用程序在数据库中创建两
个
表: django_flatpage 和 django_flatpag
e_sites 。 django_flatpage只是将 URL
映射到标题和一段文本内
容。 django_flatpage_sites 是一个多对
多表,用于关联某个简单页面以及一
个或多个站点。
该应用捆绑的 FlatPage 模型
在 django/contrib/flatpages/models.py
进行定义,如下所示:
from django.db import models
from django.contrib.sites.models imp
ort Site
class FlatPage(models.Model):
url = models.CharField(max_lengt
h=100, db_index=True)
title = models.CharField(max_len
gth=200)
content = models.TextField(blank
=
True)
enable_comments = models.Boolean
Field()
template_name = models.CharField
(max_length=70, blank=True)
registration_required = models.B
ooleanField()
sites = models.ManyToManyField(S
ite)
让我们逐项看看这些字段的含义:
url : 该简单页面所处的 URL,不
包括域名,但是包含前导斜杠 (例
如 /about/contact/ )。
title : 简单页面的标题。 框架不对
它作任何特殊处理。 由你通过模
板来显示它。
content : 简单页面的内容 (即
HTML页面)。 框架不对它作任何
特殊处理。 由你负责使用模板来
显示。
enable_comments : 是否允许该简
单页面使用评论。 框架不对它作
任何特殊处理。 你可在模板中检
查该值并根据需要显示评论窗
体。
template_name : 用来解析该简单页
面的模板名称。 这是一个可选
项;如果未指定模板或该模板不
存在,系统会退而使用默认模
板 flatpages/default.html 。
registration_required : 是否注册用
户才能查看此简单页面。 该设置
项集成了 Djangos 验证/用户框
架,该框架于第十四章详述。
sites : 该简单页面放置的站点。
该项设置集成了 Django 多站点框
架,该框架在本章的“多站点”一
节中有所阐述。
你可以通过 Django 超级管理界面或
者 Django 数据库 API 来创建简单页
面。 要了解更多内容,请查阅“添
加、修改和删除简单页面”一节。
一旦简单页面创建完
成, FlatpageFallbackMiddleware 将
完成(剩下)所有的工作。 每当
Django 引发 404 错误,作为最后的办
法,该中间件将根据所请求的 URL
检查简单页面数据库。 确切地说,
它将使用所指定的 URL以
及 SITE_ID 设置对应的站点 ID 查找
一个简单页面。
如果找到一个匹配项,它将载入该简
单页面的模板(如果没有指定的话,
将使用默认模板
flatpages/default.html )。 同时,它把
一个简单的上下文变量flatpage(一个
简单页面对象)传递给模板。 模板
解析过程中,它实际用的是
RequestContext。
如果 FlatpageFallbackMiddleware 没
有找到匹配项,该请求继续如常处
理。
注意
该中间件仅在发生 404 (页面未找
到)错误时被激活,而不会在 500
(服务器错误)或其他错误响应时被
激活。 还要注意的是必须考
虑 MIDDLEWARE_CLASSES 的顺序
问题。 通常,你可以
把 FlatpageFallbackMiddleware放在列
表最后,因为它是最后的办法。
添加、修改和删除简单页
面
可以用两种方式增加、变更或删除简
单页面:
通过超级管理界面
如果已经激活了自动的 Django 超级
管理界面,你将会在超级管理页面的
首页看到有个 Flatpages 区域。 你可
以像编辑系统中其它对象那样编辑简
单页面。
通过 Python API
前面已经提到,简单页面表现
为 django/contrib/flatpages/models.py
中的标准 Django 模型。这样,你就
可以使用Django数据库API来存取简
单页面对象,例如:
>
>> from django.contrib.flatpages.mo
dels import FlatPage
>
>> from django.contrib.sites.models
import Site
>
.
.
.
,
.
.
.
.
>
=
>
/
>> fp = FlatPage.objects.create(
..
..
..
url='/about/',
title='About',
content='About this site...'
..
..
enable_comments=False,
template_name='',
..
registration_required=False,
.. )
>> fp.sites.add(Site.objects.get(id
1))
>> FlatPage.objects.get(url='/about
')
使用简单页面模板
缺省情况下,系统使用模
板 flatpages/default.html 来解析简单页
面,但你也可以通过设定 FlatPage 对
象的template_name 字段来更改特定简
单页面的模板。
你必须自己创
建 flatpages/default.html 模板。 只需
要在模板目录创建一个 flatpages 目
录,并把default.html 文件置于其中。
简单页面模板只接受有一个上下文变
量—— flatpage ,也就是该简单页面
对象。
以下是一个 flatpages/default.html 模板
范例 :
<
!DOCTYPE HTML PUBLIC "-//W3C//DTD H
TML 4.0 Transitional//EN"
http://www.w3.org/TR/REC-html40
"
/
<
<
<
<
<
{
<
<
loose.dtd">
html>
head>
title>{{ flatpage.title }}</title>
/head>
body>
{ flatpage.content|safe }}
/body>
/html>
注意我们使用了safe模板过滤器来允
许flatpage.content引入原始HTML而不
必转义。
重定向
通过将重定向存储在数据库中并将其
视为 Django 模型对象,Django 重定
向框架让你能够轻松地管理它们。
比如说,你可以通过重定向框架告诉
Django,把任何指向 /music/ 的请求
重定向到 /sections/arts/music/ 。当你
需要在站点中移动一些东西时,这项
功能就派上用场了——网站开发者应
该穷尽一切办法避免出现坏链接。
使用重定向框架
安装重定向应用程序必须遵循以下步
骤:
1
. 将 'django.contrib.redirects' 添加
到 INSTALLED_APPS 设置中。
2
.
将 'django.contrib.redirects.middlew
are.RedirectFallbackMiddleware' 添
加到 MIDDLEWARE_CLASSES 设
置中。
3
. 运行 manage.py syncdb 命令将所需
的表添加到数据库中。
manage.py syncdb 在数据库中创建了
一个 django_redirect 表。 这是一个简
单的查询表,只有site_id、old_path和
new_path三个字段。
你可以通过 Django 超级管理界面或
者 Django 数据库 API 来创建重定
向。 要了解更多信息,请参阅“增
加、变更和删除重定向”一节。
一旦创建了重定
向, RedirectFallbackMiddleware 类
将完成所有的工作。 每当 Django 应
用引发一个 404 错误,作为终极手
段,该中间件将为所请求的 URL在
重定向数据库中进行查找。 确切地
说,它将使用给定的old_path 以
及 SITE_ID 设置对应的站点 ID 查找
重定向设置。 (查阅前面的“多站
点”一节可了解关于SITE_ID 和多站
点框架的更多细节) 然后,它将执
行以下两个步骤:
如果找到了匹配项,并
且 new_path 非空,它将重定向
到 new_path 。
如果找到了匹配项,
但 new_path 为空,它将发送一个
4
10 (Gone) HTTP 头信息以及一个
空(无内容)响应。
如果未找到匹配项,该请求将如
常处理。
该中间件仅为 404 错误激活,而不会
为 500 错误或其他任何状态码的响应
所激活。
注意必须考
虑 MIDDLEWARE_CLASSES 的顺
序。 通常,你可以
将 RedirectFallbackMiddleware 放置
在列表的最后,因为它是一种终极手
段。
注意
如果同时使用重定向和简单页面
回退中间件, 必须考虑先检查其
中的哪一个(重定向或简单页
面)。 我们建议将简单页面放在
重定向之前(因此将简单页面中
间件放置在重定向中间件之
前),但你可能有不同想法。
增加、变更和删除重定向
你可以两种方式增加、变更和删除重
定向:
通过管理界面
如果已经激活了全自动的 Django 超
级管理界面,你应该能够在超级管理
首页看到重定向区域。 可以像编辑
系统中其它对象一样编辑重定向。
同过Python API
重定向表现为
django/contrib/redirects/models.py 中的
一个标准 Django 模型。因此,你可
以通过Django数据库API来存取重定
向对象,例如:
>
>> from django.contrib.redirects.mo
dels import Redirect
>
>> from django.contrib.sites.models
import Site
>
.
>> red = Redirect.objects.create(
..
site=Site.objects.get(id=1),
.
.
..
..
old_path='/music/',
new_path='/sections/arts/mus
ic/',
.
>
.. )
>> Redirect.objects.get(old_path='/
music/')
/
sections/arts/music/>
CSRF 防护
django.contrib.csrf 开发包能够防止遭
受跨站请求伪造攻击 (CSRF).
CSRF, 又叫会话跳转,是一种网站安
全攻击技术。 当某个恶意网站在用
户未察觉的情况下将其从一个已经通
过身份验证的站点诱骗至一个新的
URL时,这种攻击就发生了,因此它
可以利用用户已经通过身份验证的状
态。 乍一看,要理解这种攻击技术
比较困难,因此我们在本节将使用两
个例子来说明。
一个简单的 CSRF 例子
假定你已经登录到 example.com 的网
页邮件账号。该网站有一个指向
example.com/logout的注销按钮。就是
说,注销其实就是访问
example.com/logout。
通过在(恶意)网页上用隐藏一个指
向 URL example.com/logout 的 ,恶意
网站可以强迫你访问该 URL。因
此,如果你登录 example.com 的网页
邮件账号之后,访问了带有指
向 example.com/logout 之 的恶意站
点,访问该恶意页面的动作将使你登
出 example.com 。 Thus, if you’re
logged in to the example.comw ebmail
account and visit the malicious page that
has an to example.com/logout , the act
of visiting the malicious page will log
you out from example.com .
很明显,登出一个邮件网站也不是什
么严重的安全问题。但是同样的攻击
可能针对任何相信用户的站点,比如
在线银行和电子商务网站。这样的话
可能在用户不知情的情况下就下订单
付款了。
稍微复杂一点的CSRF例子
在上一个例子中, example.com 应该
负部分责任,因为它允许通过
HTTP GET 方法进行状态变更(即登
入和登出)。 如果对服务器的状态
变更要求使用 HTTP POST 方法,情
况就好得多了。 但是,即便是强制
要求使用POST 方法进行状态变更操
作也易受到 CSRF 攻击。
假设 example.com 对登出功能进行了
升级,登出 按钮是通过一个指向
URL example.com/logout 的 POST动作
完成,同时在 中加入了以下隐藏的
字段:
<
input type="hidden" name="confirm"
value="true">
这就确保了用简单的指向
example.com/logout的POST 不会让用
户登出;要让用户登出,用户必须通
过 POST 向example.com/logout 发送请
求 并且发送一个值为’true’的POST变
量。 confi rm。
尽管增加了额外的安全机制,这种设
计仍然会遭到 CSRF 的攻击——恶意
页面仅需一点点改进而已。 攻击者
可以针对你的站点设计整个表单,并
将其藏身于一个不可见的 中,然后
使用 Javascript 自动提交该表单。
防止 CSRF
那么,是否可以让站点免受这种攻击
呢? 第一步,首先确保所有 GET 方
法没有副作用。 这样以来,如果某
个恶意站点将你的页面包含为 ,它
将不会产生负面效果。
该技术没有考虑 POST 请求。 第二步
就是给所有 POST 的for m标签一个隐
藏字段,它的值是保密的并根据用户
进程的 ID 生成。 这样,从服务器端
访问表单时,可以检查该保密的字
段。不吻合时可以引发一个错误。
这正是 Django CSRF 防护层完成的工
作,正如下面的小节所介绍的。
使用CSRF中间件
django.contrib.csrf 开发包只有一个模
块: middleware.py 。该模块包含了
一个 Django 中间件类——
CsrfMiddleware ,该类实现了 CSRF
防护功能。
在设置文件中
将 'django.contrib.csrf.middleware.Csrf
Middleware' 添加
到 MIDDLEWARE_CLASSES 设置中
可激活 CSRF 防护。 该中间件必须
在 SessionMiddleware 之后 执行,因
此在列表中 CsrfMiddleware 必须出现
在SessionMiddleware 之前 (因为响
应中间件是自后向前执行的)。 同
时,它也必须在响应被压缩或解压之
前对响应结果进行处理,因
此 CsrfMiddleware 必须
在 GZipMiddleware 之后执行。一旦
将它添加到MIDDLEWARE_CLASSES
设置中,你就完成了工作。 参见第
十五章的“MIDDLEWARE_CLASSES
顺序”小节以了解更多。
如果感兴趣的话,下面
是 CsrfMiddleware 的工作模式。 它
完成以下两项工作:
1
. 它修改当前处理的请求,向所有
的 POST 表单增添一个隐藏的表
单字段,使用名称
是 csrfmiddlewaretoken,值为当前
会话 ID 加上一个密钥的散列值。
如果未设置会话 ID ,该中间件
将 不会 修改响应结果,因此对于
未使用会话的请求来说性能损失
是可以忽略的。
2
. 对于所有含会话 cookie 集合的传
入 POST 请求,它将检查是否存
在 csrfmiddlewaretoken 及其是否
正确。 如果不是的话,用户将会
收到一个 403 HTTP 错误。 403 错
误页面的内容是检测到了跨域请
求伪装。 终止请求。
该步骤确保只有源自你的站点的表单
才能将数据 POST 回来。
该中间件特意只针对 HTTP POST 请
求(以及对应的 POST 表单)。 如我
们所解释的,永远不应该因为使用了
GET 请求而产生负面效应,你必须自
己来确保这一点。
未使用会话 cookie 的 POST 请求无法
受到保护,但它们也不 需要 受到保
护,因为恶意网站可用任意方法来制
造这种请求。
为了避免转换非 HTML请求,中间件
在编辑响应结果之前对它的 Content-
Typ e 头标进行检查。 只有标记为
text/html 或 application/xml+xhtml 的页
面才会被修改。
CSRF中间件的局限性
CsrfMiddleware 的运行需要 Django
的会话框架。 (参阅第 14 章了解更
多关于会话的内容。)如果你使用了
自定义会话或者身份验证框架手动管
理会话 cookies,该中间件将帮不上
你的忙。
如果你的应用程序以某种非常规的方
法创建 HTML页面(例如:在
Javascript 的document.write语句中发
送 HTML片段),你可能会绕开了向
表单添加隐藏字段的过滤器。 在此
情况下,表单提交永远无法成功。
(这是因为在页面发送到客户端之
前,CsrfMiddleware使用正则表达式
来添加csrfmiddlewaretoken字段到你
的HTML中,而正则表达式不能处理
不规范的HTML。)如果你怀疑出现
了这样的问题。使用你浏览器的查看
源代码功能以确定
csrfmiddlewaretoken是否插入到了表
单中。
想了解更多关于 CSRF 的信息和例子
的话,可以访
问 http://en.wikipedia.org/wiki/CSRF
。
人性化数据
包django.contrib.humanize包含了一些
是数据更人性化的模板过滤器。 要
激活这些过滤器,请
把'django.contrib.humanize'加入到你的
INSTALLED_APPS中。完成之后,向
模版了加入{% load humanize %}就可
以使用下面的过滤器了。
apnumber
对于 1 到 9 的数字,该过滤器返回了
数字的拼写形式。 否则,它将返回
数字。 这遵循的是美联社风格。
举例:
1
2
1
变成 one 。
变成 two 。
0 变成 10 。
你可以传入一个整数或者表示整数的
字符串。
intcomma
该过滤器将整数转换为每三个数字用
一个逗号分隔的字符串。
例子:
4
4
4
4
500 变成 4,500 。
5000 变成 45,000 。
50000 变成 450,000 。
500000 变成 4,500,000 。
可以传入整数或者表示整数的字符
串。
i ntword
该过滤器将一个很大的整数转换成友
好的文本表示方式。 它对于超过一
百万的数字最好用。
例子:
1
1
1
000000 变成 1.0 million 。
200000 变成 1.2 million 。
200000000 变成 1.2 billion 。
最大支持不超过一千的五次方
(1,000,000,000,000,000)。
可以传入整数或者表示整数的字符
串。
ordinal
该过滤器将整数转换为序数词的字符
串形式。
例子:
1
2
3
2
变成 1st 。
变成 2nd 。
变成 3rd 。
54变成254th。
可以传入整数或者表示整数的字符
串。
标记过滤器
包django.contrib.markup包含了一些列
Django模板过滤器,每一个都实现了
一中通用的标记语言。
textile : 实现了 Textile
(http://en.wikipedia.org/wiki/Textile
_
%28markup_language%29)
markdow n : 实现了 Markdown
(http://en.wikipedia.org/wiki/Markd
own)
restructuredtext : 实现了
ReStructured Te xt
(http://en.wikipedia.org/wiki/ReStru
ctur edText)
每种情形下,过滤器都期望字符串形
式的格式化标记,并返回表示标记文
本的字符串。 例如:textile过滤器吧
Textile格式的文本转换为HTML。
{
{
% load markup %}
{ object.content|textile }}
要激活这些过滤器,仅需
将 'django.contrib.markup' 添加
到 INSTALLED_APPS 设置中。 一旦
完成了该项工作,在模板中通
过 {% load ma r kup %} 就能使用这些
过滤器。 要想掌握更多信息的话,
可阅读
django/contrib/markup/templatetags/mar
kup.py. 内的源代码。
下一章
这些继承框架(CSRF、身份验证系
统等等)通过提供 中间件 来实现其
奇妙的功能。中间件是在请求之前 /
后执行的可以修改请求和响应的代
码,它扩展了框架。 在下一章,我
们将介绍Django的中间件并解释怎样
写出自己的中间件。
在有些场合,需要对Django处理的每
个request都执行某段代码。 这类代码
可能是在view处理之前修改传入的
request,或者记录日志信息以便于调
试,等等。
这类功能可以用Django的中间件框架
来实现,该框架由切入到Django的
request/response处理过程中的钩子集
合组成。 这个轻量级低层次的plug-in
系统,能用于全面的修改Django的输
入和输出。
每个中间件组件都用于某个特定的功
能。 如果你是顺着这本书读下来的
话,你应该已经多次见到“中间件”了
第12章中所有的session和user工具
都籍由一小簇中间件实现(例如,
由中间件设定view中可见的
request.session 和 request.user )。
第13章讨论的站点范围cache实际
上也是由一个中间件实现,一旦
该中间件发现与view相应的
response已在缓存中,就不再调用
对应的view函数。
第14章所介绍
的 flatpages , redirects , 和 csrf 等
应用也都是通过中间件组件来完
成其魔法般的功能。
这一章将深入到中间件及其工作机制
中,并阐述如何自行编写中间件。
什么是中间件
我们从一个简单的例子开始。
高流量的站点通常需要将Django部署
在负载平衡proxy(参见第20章)之后。
这种方式将带来一些复杂性,其一就
是每个request中的远程IP地址
(request.META["REMOTE_IP"])将指
向该负载平衡proxy,而不是发起这
个request的实际IP。 负载平衡proxy
处理这个问题的方法在特殊的 X-
Forwarded-For 中设置实际发起请求
的IP。
因此,需要一个小小的中间件来确保
运行在proxy之后的站点也能够在
request.META["REMOTE_ADDR"] 中
得到正确的IP地址:
class SetRemoteAddrFromForwardedFor(
object):
def process_request(self, reques
t):
try:
real_ip = request.META['
HTTP_X_FORWARDED_FOR']
except KeyError:
pass
else:
#
HTTP_X_FORWARDED_FOR c
an be a comma-separated list of IPs.
Take just the first on
#
e.
real_ip = real_ip.split(
request.META['REMOTE_ADD
"
,")[0]
R'] = real_ip
(Note: Although the HTTP header is
called X-Forwarded-For , Django
makes it available
asrequest.META[' HTTP_X_FORWARD
EDFOR'] . With the exception
of content-length and content-type ,
any HTTP headers in the request are
converted to request.META keys by
converting all characters to uppercase,
replacing any hyphens with
underscores and adding
an HTTP prefix to the name.)
一旦安装了该中间件(参见下一节),
每个request中的 X-Forwarded-For 值
都会被自动插入到
request.META['REMOTE_ADDR'] 中
。这样,Django应用就不需要关心自
己是否位于负载平衡proxy之后;简
单读
取 request.META['REMOTE_ADDR']
的方式在是否有proxy的情形下都将
正常工作。
实际上,为针对这个非常常见的情
形,Django已将该中间件内置。 它位
于 django.middleware.http 中, 下一节
将给出这个中间件相关的更多细节。
安装中间件
如果按顺序阅读本书,应当已经看到
涉及到中间件安装的多个示例,因为
前面章节的许多例子都需要某些特定
的中间件。 出于完整性考虑,下面
介绍如何安装中间件。
要启用一个中间件,只需将其添加到
配置模块
的 MIDDLEWARE_CLASSES 元组
中。
在 MIDDLEWARE_CLASSES 中,中
间件组件用字符串表示: 指向中间
件类名的完整Python路径。 例如,下
面是 django-admin.py startproject创建
的缺省 MIDDLEWARE_CLASSES :
MIDDLEWARE_CLASSES = (
'
django.middleware.common.Common
Middleware',
'
django.contrib.sessions.middlew
are.SessionMiddleware',
'
django.contrib.auth.middleware.
AuthenticationMiddleware',
)
Django项目的安装并不强制要求任何
中间件,如果你愿
意, MIDDLEWARE_CLASSES 可以
为空。
这里中间件出现的顺序非常重要。
在request和view的处理阶段,Django
按照 MIDDLEWARE_CLASSES 中出
现的顺序来应用中间件,而在
response和异常处理阶段,Django则
按逆序来调用它们。 也就是说,
Django将
MIDDLEWARE_CLASSES 视为view
函数外层的顺序包装子: 在request阶
段按顺序从上到下穿过,而在
response则反过来。
中间件方法
现在,我们已经知道什么是中间件和
怎么安装它,下面将介绍中间件类中
可以定义的所有方法。
Initializer: init(self)
init(self)「初始化]
在中间件类中, init() 方法用于执行
系统范围的设置。
出于性能的考虑,每个已启用的中间
件在每个服务器进程中只初始
化 一 次。 也就是说 init() 仅在服务
进程启动的时候调用,而在针对单个
request处理时并不执行。
对一个middleware而言,定
义 init() 方法的通常原因是检查自身
的必要性。 如果 init() 抛出异常
django.core.exceptions.MiddlewareNot
Used ,则Django将从middleware栈中移
出该middleware。 可以用这个机制来
检查middleware依赖的软件是否存
在、服务是否运行于调试模式、以及
任何其它环境因素。
在中间件中定义 init() 方法时,除了
标准的 self 参数之外,不应定义任何
其它参数。
Request预处理函数:
process_request(self,
request)
process_request(self,
request)
这个方法的调用时机在Django接收到
request之后,但仍未解析URL以确定
应当运行的view之前。 Django向它传
入相应的 HttpRequest 对象,以便在
方法中修改。
process_request() 应当返
回 None 或 HttpResponse 对象.
如果返回 None , Django将继续处
理这个request,执行后续的中间
件, 然后调用相应的view.
如果返回 HttpResponse 对象,
Django 将不再执行 任何 其它的中
间件(而无视其种类)以及相应的
view。 Django将立即返回
该 HttpResponse .
Vi ew预处理函数:
process_vi ew(sel f, request,
view, args, k wargs)
process_vi ew(sel f, request,
view, args, k wargs)
这个方法的调用时机在Django执行完
request预处理函数并确定待执行的
view之后,但在view函数实际执行之
前。
表15-1列出了传入到这个View预处理
函数的参数。
表 15-1. 传入
process_view() 参数
的参数
request
The HttpRequest obj
The Python function
Django will call to
handle this request.
This is the actual
function object itself
not the na me of the
view
function as a string.
将传入view的位置
数列表,但不包括
request 参数(它通常
传 入view的第一个
数)
args
将传入view的关键
参数字典.
kw args
Just
like process_request() , process_view()
should return either None or
an HttpResponse object.
If it returns None , Django will
continue processing this request,
executing any other middleware and
then the appropriate view.
If it returns an HttpResponse object,
Django won’t bother
calling any other middleware (of any
type) or the appropriate view.
Django will immediately return
that HttpResponse .
Response后处理函数:
process_response(self,
request, response)
process_response(self,
request, response)
这个方法的调用时机在Django执行
view函数并生成response之后。 Here,
the processor can modi fy the content of a
response. One obvious use case is
content compression, such as gzipping of
the request’s HTML.
这个方法的参数相当直观: request 是
request对象,而 response 则是从view
中返回的response对象。 requestis the
request object, and response is the
response object returned from the view.
不同可能返回 None 的request和view
预处理函数, process_response() 必
须 返回 HttpResponse 对象. 这个
response对象可以是传入函数的那一
个原始对象(通常已被修改),也可以
是全新生成的。 That response could
be the original one passed into the
function (possibly modified) or a brand-
new one.
Exception后处理函数:
process_exception(self,
request, exception)
process_exception(self,
request, exception)
这个方法只有在request处理过程中出
了问题并且view函数抛出了一个未捕
获的异常时才会被调用。 这个钩子
可以用来发送错误通知,将现场相关
信息输出到日志文件, 或者甚至尝试
从错误中自动恢复。
这个函数的参数除了一贯
的 request 对象之外,还包括view函
数抛出的实际的异常对
象 exception 。
process_exception() 应当返回 None 或
HttpResponse 对象.
如果返回 None , Django将用框架
内置的异常处理机制继续处理相
应request 。
如果返回 HttpResponse 对象,
Django 将使用该response对象,而
短路框架内置的异常处理机制。
备注
Django自带了相当数量的中间件类
(将在随后章节介绍),它们都是相当
好的范例。 阅读这些代码将使你对
在Djangos wiki上也可以找到大量的
社区贡献的中间件范
例:http://code.djangoproject.com/wiki/
ContributedMiddlewarehttp://code.djan
goproject.com/wiki/ContributedMiddle
ware
内置的中间件
Django自带若干内置中间件以处理常
见问题,将从下一节开始讨论。
认证支持中间件
中间件
类: django.contrib.auth.middleware.Aut
henticationMiddleware .django.contrib.a
uth.middleware.AuthenticationMiddlew
are .
这个中间件激活认证支持功能. 它在
每个传入的 HttpRequest 对象中添加
代表当前登录用户的 request.user 属
性。 It adds the request.user attribute,
representing the currently logged-in user,
to every incomingHttpRequest object.
完整的细节请参见第12章。
通用中间件
Middleware
class: django.middleware.common.Com
monMiddleware .
这个中间件为完美主义者提供了一些
便利 :
_
禁止 DISALLOWED_USER_AGENTS
列表中所设置的user agent访问_ :
一旦提供,这一列表应当由已编
译的正则表达式对象组成,这些
对象用于匹配传入的request请求头
中的user-agent域。 下面这个例子
来自某个配置文件片段:
import re
DISALLOWED_USER_AGENTS = (
re.compile(r'^OmniExplorer_Bot')
re.compile(r'^Googlebot')
,
)
请注意 import re ,因
为 DISALLOWED_USER_AGENTS
要求其值为已编译的正则表达式
(也就是 re.compile()的返回值)。
_
依据 APPEND_SLASH 和
PREPEND_WWW 的设置执行URL重
写_ :如
果 APPEND_SLASH 为 Tr ue , 那些
尾部没有斜杠的URL将被重定向到
添加了斜杠的相应URL,除非path
的最末组成部分包含点号。 因
此,foo.com/bar 会被重定向
到 foo.com/bar/ , 但
是 foo.com/bar/file.txt 将以不变形
式通过。
如果 PREPEND_WWW 为 Tr ue , 那
些缺少先导www.的URLs将会被重
定向到含有先导www.的相应URL
上。 will be redirected to the same
URL with a leading www..
这两个选项都是为了规范化URL。
其后的哲学是每个URL都应且只应
当存在于一处。 技术上来说,
URLexampl e.com/ bar 与 example.co
m/bar/ 及 www.example.com/bar/ 都
互不相同。
_依据 USE_ETAGS 的设置处理
Etag_ : ETags 是HTTP级别上按条
件缓存页面的优化机制。 如果
USE_ETAGS 为 Tr ue ,Django针对
每个请求以MD5算法处理页面内
容,从而得到Etag, 在此基础上,
Django将在适当情形下处理并返
回 Not Modified 回应(译注:
请注意,还有一个条件化
的 GET 中间件, 处理Etags并干得
更多,下面马上就会提及。
压缩中间件
中间件
类 django.middleware.gzip.GZipMiddle
ware .
这个中间件自动为能处理gzip压缩(包
括所有的现代浏览器)的浏览器自动
压缩返回]内容。 这将极大地减少
Web服务器所耗用的带宽。 代价是压
缩页面需要一些额外的处理时间。
相对于带宽,人们一般更青睐于速
度,但是如果你的情形正好相反,尽
可启用这个中间件。
条件化的GET中间件
Middleware
class: django.middleware.http.Condition
alGetMiddleware .
这个中间件对条件化 GET 操作提供
支持。 如果response头中包括 Last-
Modified 或 ETag 域,并且request头
中包含 If-None-Match 或 If-Modified-
Since 域,且两者一致,则该response
将被response 304(Not modified)取
代。 对 ETag 的支持依赖
于 USE_ETAGS 配置及事先在
response头中设置 ETag 域。稍前所讨
论的通用中间件可用于设置response
中的 ETag 域。 As discussed above,
the ETag header is set by the Common
middleware.
此外,它也将删除处理 HEAD request
时所生成的response中的任何内容,
并在所有request的response头中设
置 Date 和 Content-Length 域。
反向代理支持 (X-
Forwarded-For中间件)
Middleware
class: django.middleware.http.SetRemot
eAddrFromForwardedFor .
这是我们在 什么是中间件 这一节中
所举的例子。
在 request.META[' HTTP_X_FORWAR
DED_FOR'] 存在的前提下,它根据
其值来设
置 request.META['REMOTE_ADDR']
。在站点位于某个反向代理之后的、
每个request的REMOTE_ADDR 都被
指向 127.0.0.1 的情形下,这一功能
将非常有用。 It
sets request.META['REMOTE_ADDR']
based
on request.META[' HTTP_X_FORWAR
DED_FOR'] , if the latter is set. This is
useful if you’re sitting behind a reverse
proxy that causes each
request’s REMOTE_ADDR to be set
to 127.0.0.1 .
红色警告!
这个middleware并 不 验
证 HTTP_X_FORWARDED_FOR
的合法性。
如果站点并不位于自动设
置 HTTP_X_FORWARDED_FOR 的反
向代理之后,请不要使用这个中间
件。 否则,因为任何人都能够伪
造 HTTP_X_FORWARDED_FOR 值,
而 REMOTE_ADDR 又是依
据 HTTP_X_FORWARDED_FOR 来设
置,这就意味着任何人都能够伪造IP
地址。
只有当能够绝对信
任 HTTP_X_FORWARDED_FOR 值得
时候才能够使用这个中间件。
会话支持中间件
Middleware
class: django.contrib.sessions.middlewa
re.SessionMiddleware .
这个中间件激活会话支持功能. 细节
请参见第12章。 See Chapter 14 for
details.
站点缓存中间件
Middleware
classes: django.middleware.cache.Updat
eCacheMiddleware anddjango.middlew
are.cache.FetchFromCacheMiddleware .
这些中间件互相配合以缓存每个基于
Django的页面。 已在第13章中详细讨
论。
事务处理中间件
Middleware
class: django.middleware.transaction.Tr
ansactionMiddleware .
这个中间件将数据库
的 COMMIT 或 ROLLBACK 绑定到
request/response处理阶段。 如果view
函数成功执行,则发出 COMMIT 指
令。 如果view函数抛出异常,则发
出 ROLLBACK 指令。
这个中间件在栈中的顺序非常重要。
其外层的中间件模块运行在Django缺
省的 保存-提交 行为模式下。 而其内
层中间件(在栈中的其后位置出现)将
置于与view函数一致的事务机制的控
制下。
关于数据库事务处理的更多信息,请
参见附录C。
Django最适合于所谓的green-field开
发,即从头开始的一个项目,正如你
在一块还长着青草的未开垦的土地上
从零开始建造一栋建筑一般。 然
而,尽管Django偏爱从头开始的项
目,将这个框架和以前遗留的数据库
和应用相整合仍然是可能的。 本章
就将介绍一些整合的技巧。
与遗留数据库整合
Django的数据库层从Python代码生成
SQL schemas—但是对于遗留数据
库,你已经拥有SQL schemas. 这种情
况,你需要为已经存在的数据表创建
model. 为此,Django自带了一个可以通
过读取您的数据表结构来生成model
的工具. 该辅助工具称为inspectdb,你
可以通过执行 manage.py inspectdb
来调用它 .
使用 inspectdb
inspectdb工具自省你配置文件指向的
数据库,针对每一个表生成一个
Django模型,然后将这些Python模型
的代码显示在系统的标准输出里面。
下面是一个从头开始的针对一个典型
的遗留数据库的整合过程。 两个前
提条件是安装了Django和一个传统数
据库。
通过运行django-admin.py
startproject mysi te (这里 mysi te 是你
的项目的名字)建立一个Django项
目。 好的,那我们在这个例子中
就用这个 mysi te 作为项目的名
字。
编辑项目中的配置文
件, mysite/settings.py ,告诉Django你
的数据库连接参数和数据库名。
具体的说,要提
供 DATABASE_NAME , DATABAS
E_ENGINE , DATABASE_USER , D
ATABASE_PASSWORD , DATABA
SE_HOST , 和
DATABASE_PORT 这些配置信
息.。 (请注意其中的一些设置是可
选的。 更多信息参见第5章)
通过运
行 python mysi te/ manage.py startapp
mya pp (这里 mya pp 是你的应用的
名字)创建一个Django应用。 这里
我们使用mya pp 做为应用名。
运行命
令 python mysi te/ manage.py inspectd
b。这将检查
DATABASE_NAME 数据库中所有
的表并打印出为每张表生成的模
型类。 看一看输出结果以了解
inspectdb能做些什么。
将标准shell的输出重定向,保存输
出到你的应用的 models.py 文件
里:
python mysi te/ manage.py inspectdb >
mysite/myapp/models.py
编辑 mysite/myapp/models.py 文件
以清理生成的 models 并且做一些
必要的自定义。 针对这个,下一
个节有些好的建议。
清理生成的Models
如你可能会预料到的,数据库自省不
是完美的,你需要对产生的模型代码
做些许清理。 这里提醒一点关于处
理生成 models 的要点:
数据库的每一个表都会被转化为
一个model类 (也就是说,数据库
的表和model 类之间是一对一的映
射)。 这意味着你需要为多对多连
接的表,重构其models
为 ManyToManyFi el d 的对象。
所生成的每一个model中的每个字
段都拥有自己的属性,包括id主键
字段。 但是,请注意,如果某个
model没有主键的话,那么Django
会自动为其增加一个id主键字段。
这样一来,你也许希望移除这样
的代码行。
id = models.IntegerField(primary_key
=
True)
这样做并不是仅仅因为这些行是
冗余的,而且如果当你的应用需
要向这些表中增加新记录时,这
些行会导致某些问题。
每一个字段类型,如CharField、
DateField, 是通过查找数据库列
类型如VARCHAR,DATE来确定
的。如果inspectdb无法把某个数据
库字段映射到model字段上,它会
使用TextField字段进行代替,并且
会在所生成model字段后面加入
Python注释“该字段类型是猜的”。
对这要当心,如果必要的话,更
改字段类型。
如果你的数据库中的某个字段在
Django中找不到合适的对应物,你
可以放心的略过它。 Django模型
层不要求必须导入你数据库表中
的每个列。
如果数据库中某个列的名字是
Python的保留字(比如pass、class
或者for等),inspectdb会在每个属
性名后附加上_field,并将
db_column属性设置为真实的字段
名(也就是pass,class或者for
等)。
例如,某张表中包含一个INT类型
的列,其列名为for,那么所生成
的model将会包含如下所示的一个
字段:
for_field = models.IntegerField(db_c
olumn='for')
inspectdb 会在该字段后加注 ‘字段
重命名,因为它是一个Python保留
字’ 。
如果数据库中某张表引用了其他
表(正如大多数数据库系统所做
的那样),你需要适当的修改所
生成model的顺序,以使得这种引
用能够正确映射。 例如,model
Book拥有一个针对于model Author
的外键,那么后者应该先于前者
被定义。如果你想创建一个指向
尚未定义的model的关系,那么可
以使用包含model名的字符串,而
不是model对象本身。
对于PostgreSQL,MySQL和SQLite数
据库系统,inspectdb能够自动检测
出主键关系。 也就是说,它会在
合适的位置插入
primary_key=True。 而对于其他数
据库系统,你必须为每一个model
中至少一个字段插入这样的语
句,因为Django的model要求必须
拥有一个primary_key=True的字
段。
外键检测仅对PostgreSQL,还有
MySQL表中的某些特定类型生
效。 至于其他数据库,外键字段
将在假定其为INT列的情况下被自
动生成为IntegerField。
与认证系统的整合
将Django与其他现有认证系统的用户
名和密码或者认证方法进行整合是可
以办到的。
例如,你所在的公司也许已经安装了
LDAP,并且为每一个员工都存储了
相应的用户名和密码。 如果用户在
LDAP和基于Django的应用上拥有独
立的账号,那么这时无论对于网络管
理员还是用户自己来说,都是一件很
令人头痛的事儿。
为了解决这样的问题,Django认证系
统能让您以插件方式与其他认证资源
进行交互。 您可以覆盖Diango默认的
基于数据库的模式,您还可以使用默
认的系统与其他系统进行交互。
指定认证后台
在后台,Django维护了一个用于检查
认证的后台列表。 当某个人调
用 django.contrib.auth.authenticate()(如
14章中所述)时,Django会尝试对其
认证后台进行遍历认证。 如果第一
个认证方法失败,Django会尝试认证
第二个,以此类推,一直到尝试完。
认证后台列表在
AUTHENTICATION_BACKENDS设置
中进行指定。 它应该是指向知道如
何认证的Python类的Python路径的名
字数组。 这些类可以在你Python路径
的任何位置。
默认情况下,
AUTHENTICATION_BACKENDS被设
置为如下:
('django.contrib.auth.backends.Model
Backend',)
那就是检测Django用户数据库的基本
认证模式。
AUTHENTICATION_BACKENDS的顺
序很重要,如果用户名和密码在多个
后台中都是有效的,那么Django将会
在第一个正确匹配后停止进一步的处
理。
编写认证后台
一个认证后台其实就是一个实现了如
下两个方法的
类: get_user(id) 和 authenticate(**cre
dentials) 。
方法 get_user 需要一个参数 id ,这
个 id 可以是用户名,数据库ID或者
其他任何数值,该方法会返回一个
User 对象。
方法 authenticate 使用证书作为关键参
数。 大多数情况下,该方法看起来
如下:
class MyBackend(object):
def authenticate(self, username=
None, password=None):
#
Check the username/passwor
d and return a User.
但是有时候它也可以认证某个短语,
例如:
class MyBackend(object):
def authenticate(self, token=Non
e):
#
Check the token and return
a User.
每一个方法中, authenticate 都应该检
测它所获取的证书,并且当证书有效
时,返回一个匹配于该证书的User 对
象,如果证书无效那么返回 None 。
如果它们不合法,就返回None。
如14章中所述,Django管理系统紧密
连接于其自己后台数据库的 User 对
象。 实现这个功能的最好办法就是
为您的后台数据库(如LDAP目录,
外部SQL数据库等)中的每个用户都
创建一个对应的Django User对象。
您可以提前写一个脚本来完成这个工
作,也可以在某个用户第一次登陆的
时候在 authenticate 方法中进行实现。
以下是一个示例后台程序,该后台用
于认证定义在 setting.py 文件中的
username和password变量,并且在该
用户第一次认证的时候创建一个相应
的Django User 对象。
from django.conf import settings
from django.contrib.auth.models impo
rt User, check_password
class SettingsBackend(object):
"
""
Authenticate against the setting
s ADMIN_LOGIN and ADMIN_PASSWORD.
Use the login name, and a hash o
f the password. For example:
ADMIN_LOGIN = 'admin'
ADMIN_PASSWORD = 'sha1$4e987$afb
cf42e21bd417fb71db8c66b321e9fc33051d
e'
"
""
def authenticate(self, username=
None, password=None):
login_valid = (settings.ADMI
N_LOGIN == username)
pwd_valid = check_password(p
assword, settings.ADMIN_PASSWORD)
if login_valid and pwd_valid
:
try:
user = User.objects.
get(username=username)
except User.DoesNotExist
:
#
Create a new user.
Note that we can set password
#
to anything, becau
se it won't be checked; the password
#
from settings.py w
ill.
user = User(username
username, password='get from settin
=
gs.py')
user.is_staff = True
user.is_superuser =
True
user.save()
return user
return None
def get_user(self, user_id):
try:
return User.objects.get(
pk=user_id)
except User.DoesNotExist:
return None
更多认证模块的后台, 参考Django文
档。
和遗留Web应用集成
同由其他技术驱动的应用一样,在相
同的Web服务器上运行Django应用也
是可行的。 最简单直接的办法就是
利用Apaches配置文件httpd.conf,将
不同的URL类型分发至不同的技术。
(请注意,第12章包含了在
Apache/mod_python上配置Django的相
关内容,因此在尝试本章集成之前花
些时间去仔细阅读第12章或许是值得
的。 )
关键在于只有在您的httpd.conf文件中
进行了相关定义,Django对某个特定
的URL类型的驱动才会被激活。 在第
12章中解释的缺省部署方案假定您需
要Django去驱动某个特定域上的每一
个页面。
<
Location "/">
SetHandler python-program
PythonHandler django.core.handle
rs.modpython
SetEnv DJANGO_SETTINGS_MODULE my
site.settings
PythonDebug On
/Location>
<
这里, <Location "/"> 这一行表示
用Django处理每个以根开头的URL.
精妙之处在于Django将指令值限定于
一个特定的目录树上。 举个例子,
比如说您有一个在某个域中驱动大多
数页面的遗留PHP应用,并且您希望
不中断PHP代码的运行而在../admin/
位置安装一个Django域。 要做到这一
点,您只需将值设置为/admin/即可。
<
Location "/admin/">
SetHandler python-program
PythonHandler django.core.handle
rs.modpython
SetEnv DJANGO_SETTINGS_MODULE my
site.settings
PythonDebug On
/Location>
<
有了这样的设置,只有那些以/admin/
开头的URL地址才会触发Django去进
行处理。 其他页面会使用已存在的
设置。
请注意,把Diango绑定到的合格的
URL(比如在本章例子中的
/
admin/ )并不会影响其对URL的
解析。 绝对路径对Django才是有效的
(例如
/
admin/people/person/add/ ),
而非截断后的URL(例如
/
people/person/add/ )。这意味
着你的根URLconf必须包含前缀
/
admin/ 。
下一章
如果你的母语是英语, 你可能就不会
注意到许多Django admi n网站中最酷
的特性功能。 它支持超过50种语言!
Django 的国际化框架使其成为可能(
还有Django志愿翻译者的努力 ) 下一
章介绍如何使用这个框架来提供本地
化的Django网站。
Django诞生于美国中部堪萨斯的劳伦
斯,距美国的地理中心不到40英里。
像大多数开源项目一样,Djano社区
逐渐开始包括来自全球各地的许多参
与者。 鉴于Django社区逐渐变的多样
性,_国际化_和_本地化_逐渐变得很
重要。 由于很多开发者对这些措辞
比较困惑,所以我们将简明的定义一
下它们。
国际化* 是指为了该软件在任何地
区的潜在使用而进行程序设计的
过程。 它包括了为将来翻译而标
记的文本(比如用户界面要素和
错误信息等)、日期和时间的抽
象显示以便保证不同地区的标准
得到遵循、为不同时区提供支
持,并且一般确保代码中不会存
在关于使用者所在地区的假设。
您会经常看到国际化被缩写
为“I18N”(18表示Internationlization
这个单词首字母I和结尾字母N之
间的字母有18个)。
本地化* 是指使一个国际化的程序
为了在某个特定地区使用而进行
实际翻译的过程。 有时,本地化
缩写为L10N 。
Django本身是完全国际化了的,所有
的字符串均因翻译所需而被标记,并
且设定了与地域无关的显示控制值,
如时间和日期。 Django是带着50个不
同的本地化文件发行的。 即使您的
母语不是英语,Django也很有可能已
经被翻译为您的母语了。
这些本地化文件所使用的国际化框架
同样也可以被用在您自己的代码和模
板中。
您只需要添加少量的挂接代码到您的
Python代码和模板中。 这些挂接代码
被称为 翻译字符串 。它们告诉
Django:如果这段文本的译文可用的
话,它应被翻译为终端用户指定的语
言。
Django会根据用户的语言偏好,在线
地运用这些挂接指令去翻译Web应用
程序。
本质上来说,Django做两件事情:
它让开发者和模板的作者指定他
们的应用程序的哪些部分应该被
翻译。
Django根据用户的语言偏好来翻
译Web应用程序。
备注 :
Django的翻译机制是使用
GNU gettext (http://www.gnu.org/softwa
模块 gettext 。
如果您不需要国际化 :
Django的国际化挂接是默认开启的,
这可能会给Django的运行增加一点点
开销。 如果您不需要国际化支持,
那么您可以在您的设置文件中设
置 USE_I18N = False 。 如
果 USE_I18N 被设为 False ,那么
Django会进行一些优化,而不加载国
际化支持机制。
您也可以从您
的 TEMPLATE_CONTEXT_PROCESS
ORS 设置中移
除 'django.core.context_processors.i18n'
。
对你的Django应用进行国际化的三个
步骤 :
1
2
3
. 第一步:在你的Python代码和模板
中嵌入待翻译的字符串。
. 第二步:把那些字符串翻译成你
要支持的语言。
. 第三步:在你的Django settings文
件中激活本地中间件。
我们将详细地对以上步骤逐一进行描
述。
1
、如何指定待翻译字
符串
翻译字符串指定这段需要被翻译的文
本。 这些字符串可以出现在您的
Python代码和模板中。 而标记出这些
翻译字符串则是您的责任;系统仅能
翻译出它所知道的东西。
在Python 代码中
标准翻译
使用函数 ugettext() 来指定一个翻译
字符串。 作为惯例,使用短别
名 _ 来引入这个函数以节省键入时
间.
在下面这个例子中,文
本 "Welcome to my site" 被标记为待翻
译字符串:
from django.utils.translation import
ugettext as _
def my_view(request):
output = _("Welcome to my site."
)
return HttpResponse(output)
显然,你也可以不使用别名来编码。
下面这个例子和前面两个例子相同:
from django.utils.translation import
ugettext
def my_view(request):
output = ugettext("Welcome to my
site.")
return HttpResponse(output)
翻译字符串对于计算出来的值同样有
效。 下面这个例子等同前面一种:
def my_view(request):
words = ['Welcome', 'to', 'my',
'
site.']
output = _(' '.join(words))
return HttpResponse(output)
翻译对变量也同样有效。 这里是一
个同样的例子:
def my_view(request):
sentence = 'Welcome to my site.'
output = _(sentence)
return HttpResponse(output)
(
以上两个例子中,对于使用变量或
计算值,需要注意的一点是Django的
待翻译字符串检测工具,make-
messages.py ,将不能找到这些字符
串。 稍后,在 makemessages 中会有
更多讨论。)
你传递给 _() 或 gettext() 的字
符串可以接受占位符,由Python标准
命名字符串插入句法指定的。 例
如:
def my_view(request, m, d):
output = _('Today is %(month)s %
(day)s.') % {'month': m, 'day': d}
return HttpResponse(output)
这项技术使得特定语言的译文可以对
这段文本进行重新排序。 比如,一
段英语译文可能
是"Today is November 26." ,而一段
西班牙语译文会
是 "Hoy es 26 de Noviembre." 使用占
位符(月份和日期)交换它们的位
置。
由于这个原因,无论何时当你有多于
一个单一参数时,你应当使用命名字
符串插入(例如: %(day)s )来替代
位置插入(例如: %s or %d )。 如
果你使用位置插入的话,翻译动作将
不能重新排序占位符文本。
标记字符串为不操作
使
用 django.utils.translation.gettext_noop(
)
函数来标记一个不需要立即翻译的
字符串。 这个串会稍后从变量翻
译。
使用这种方法的环境是,有字符串必
须以原始语言的形式存储(如储存在
数据库中的字符串)而在最后需要被
翻译出来(如显示给用户时)。
惰性翻译
使
用 django.utils.translation.gettext_lazy()
函数,使得其中的值只有在访问时才
会被翻译,而不是在gettext_lazy() 被
调用时翻译。
例如:要翻译一个模型的 help_text,
按以下进行:
from django.utils.translation import
ugettext_lazy
class MyThing(models.Model):
name = models.CharField(help_tex
t=ugettext_lazy('This is the help te
xt'))
在这个例子中, ugettext_lazy() 将字
符串作为惰性参照存储,而不是实际
翻译。 翻译工作将在字符串在字符
串上下文中被用到时进行,比如在
Django管理页面提交模板时。
在Python中,无论何处你要使用一个
unicode 字符串(一个unicode 类型的
对象),您都可以使用一个
ugettext_lazy() 调用的结果。 一个
ugettext_lazy()对象并不知道如何把它
自己转换成一个字节串。如果你尝试
在一个需要字节串的地方使用它,事
情将不会如你期待的那样。 同样,
你也不能在一个字节串中使用一个
unicode 字符串。所以,这同常规的
Python行为是一致的。 例如:
#
This is fine: putting a unicode pr
oxy into a unicode string.
u"Hello %s" % ugettext_lazy("people"
)
#
This will not work, since you cann
ot insert a unicode object
into a bytestring (nor can you ins
ert our unicode proxy there)
Hello %s" % ugettext_lazy("people")
#
"
如果你曾经见到到像"hello"这样的输
出,你就可能在一个字节串中插入了
ugettext_lazy()的结果。 在您的代码
中,那是一个漏洞。
如果觉得 gettextlazy 太过冗长,可以
用 (下划线)作为别名,就像这
样:
from django.utils.translation import
ugettext_lazy as _
class MyThing(models.Model):
name = models.CharField(help_tex
t=_('This is the help text'))
在Django模型中总是无一例外的使用
惰性翻译。 为了翻译,字段名和表
名应该被标记。(否则的话,在管理
界面中它们将不会被翻译) 这意味
着在Meta类中显式地编写
verbose_nane和verbose_name_plural选
项,而不是依赖于Django默认的
verbose_name和
verbose_name_plural(通过检查model
的类名得到)。
from django.utils.translation import
ugettext_lazy as _
class MyThing(models.Model):
name = models.CharField(_('name'
)
, help_text=_('This is the help tex
t'))
class Meta:
verbose_name = _('my thing')
verbose_name_plural = _('myt
hings')
复数的处理
使用django.utils.translation.ungettext()
来指定以复数形式表示的消息。 例
如:
from django.utils.translation import
ungettext
def hello_world(request, count):
page = ungettext('there is %(cou
nt)d object',
'
there are %(count)d objects
'
, count) % {
'
count': count,
}
return HttpResponse(page)
ngettext 函数包括三个参数: 单数形
式的翻译字符串,复数形式的翻译字
符串,和对象的个数(将以 count 变
量传递给需要翻译的语言)。
模板代码
Django模板使用两种模板标签,且语
法格式与Python代码有些许不同。 为
了使得模板访问到标签,需要将
{
% load i18n %} 放在模板最前面。
这个{% trans %}模板标记翻译一个常
量字符串 (括以单或双引号) 或 可变
内容:
{
{
% trans "This is the title." %}
% trans myvar %}
如果有noop 选项,变量查询还是有
效但翻译会跳过。 当空缺内容要求
将来再翻译时,这很有用。
{
% trans "myvar" noop %}
在一个带 {% trans %} 的字符串中,
混进一个模板变量是不可能的。如果
你的译文要求字符串带有变量(占位
符placeholders),请使
用 {% blocktrans %} :
{
% blocktrans %}This string will hav
e {{ value }} inside.{% endblocktran
s %}
使用模板过滤器来翻译一个模板表达
式,需要在翻译的这段文本中将表达
式绑定到一个本地变量中:
{
% blocktrans with value|filter as m
yvar %}
This will have {{ myvar }} inside.
{
% endblocktrans %}
如果需要在 blocktrans 标签内绑定多
个表达式,可以用 and 来分隔:
{
% blocktrans with book|title as boo
k_t and author|title as author_t %}
This is {{ book_t }} by {{ author_t
}
{
}
% endblocktrans %}
为了表示单复数相关的内容,需要
在 {% blocktrans %} 和 {% endblocktr
ans %} 之间使用 {% plural %} 标签
来指定单复数形式,例如:
{
% blocktrans count list|length as c
ounter %}
There is only one {{ name }} object.
{
% plural %}
There are {{ counter }} {{ name }} o
bjects.
{
% endblocktrans %}
其内在机制是,所有的块和内嵌翻译
调用相应的 gettext 或 ngettext 。
每一个RequestContext可以访问三个指
定翻译变量:
{
{ LANGUAGES }} 是一系列元组
组成的列表,每个元组的第一个
元素是语言代码,第二个元素是
用该语言表示的语言名称。
作为一二字符串,
LANGUAGE_CODE是当前用户的
优先语言。 例如: en-us。(请参
见下面的Django如何发现语言偏
好)
LANGUAGE_BIDI就是当前地域的
说明。 如果为真(Tr ue),它就
是从右向左书写的语言,例如:
希伯来语,阿拉伯语。 如果为假
(
False),它就是从左到右书写
的语言,如: 英语,法语,德语
等。
如果你不用这个RequestContext扩展,
你可以用3个标记到那些值:
{
% get_current_language as LANGUAGE_
CODE %}
% get_available_languages as LANGUA
GES %}
% get_current_language_bidi as LANG
{
{
UAGE_BIDI %}
这些标记亦要求一
个 {% load i18n %} 。
翻译的hook在任何接受常量字符串的
模板块标签内也是可以使用的。 此
时,使用 _() 表达式来指定翻译字符
串,例如:
{
% some_special_tag _("Page not foun
d") value|yesno:_("yes,no") %}
在这种情况下,标记和过滤器两个都
会看到已经翻译的字符串,所有它们
并不需要提防翻译操作。
备注 :
在这个例子中,翻译结构将放过字符
串"yes,no",而不是单独的字符
串"yes"和"no"。翻译的字符串将需要
包括逗号以便过滤器解析代码明白如
何分割参数。 例如, 一个德语翻译
器可能会翻译字符
串 "yes,no" 为"ja,nein" (保持逗号原封
不动)。
与惰性翻译对象一道工作
在模型和公用函数中,使用
ugettext_lazy()和ungettext_lazy()来标记
字符串是很普遍的操作。 当你在你
的代码中其它地方使用这些对象时,
你应当确定你不会意外地转换它们成
一个字符串,因为它们应被尽量晚地
转换(以便正确的地域生效) 这需
要使用及个帮助函数。
拼接字符串:
string_concat()
标准Python字符串拼接(''.join([...]) )
将不会工作在包括惰性翻译对象的列
表上。 作为替代,你可以使用
django.utils.translation.string_concat()
,
这个函数创建了一个惰性对象,
其连接起它的内容 并且 仅当结果被
包括在一个字符串中时转换它们为字
符串 。 例如:
from django.utils.translation import
string_concat
#
...
name = ugettext_lazy(u'John Lennon')
instrument = ugettext_lazy(u'guitar'
)
result = string_concat([name, ': ',
instrument])
在这种情况下,当result 自己被用与
一个字符串时, result 中的惰性翻译
将仅被转换为字符串(通常在模板渲
染时间)。
al l ow_l azy() 修饰符
Django提供很多功能函数(如:取一
个字符串作为他们的第一个参数并且
对那个字符串做些什么)。(尤其在
django.utils 中) 这些函数被模板过滤
器像在其他代码中一样直接使用。
如果你写你自己的类似函数并且与翻
译打交道,当第一个参数是惰性翻译
对象时,你会面临“做什么”的难题。
因为你可能在视图之外使用这个函数
(
并且因此当前线程的本地设置将会
不正确),所以你不想立即转换其为
一个字符串。
象这种情况,请使
用 django.utils.functional.allow_lazy()
修饰符。 它修改这个函数以便 _假如
_第一个参数是一个惰性翻译, 这个
函数的赋值会被延后直到它需要被转
化为一个字符串为止。
例如:
from django.utils.functional import
allow_lazy
def fancy_utility_function(s, ...):
#
Do some conversion on string '
s'
#
...
fancy_utility_function = allow_lazy(
fancy_utility_function, unicode)
allow_lazy() 装饰符 采用了另外的函
数来装饰,以及一定量的,原始函数
可以返回的特定类型的额外参数
(*args ) 。 通常,在这里包
括 unicode 就足够了并且确定你的函
数将仅返回Unicode字符串。
使用这个修饰符意味着你能写你的函
数并且假设输入是合适的字符串,然
后在末尾添加对惰性翻译对象的支
持。
2
、如何创建语言文件
当你标记了翻译字符串,你就需要写
出(或获取已有的)对应的语言翻译
信息。 这里就是它如何工作的。
地域限制
Django不支持把你的应用本地化到一
个连它自己都还没被翻译的地域。
在这种情况下,它将忽略你的翻译文
件。 如果你想尝试这个并且Django支
持它,你会不可避免地见到这样一个
混合体––参杂着你的译文和来自
Django自己的英文。 如果你的应用需
要你支持一个Django中没有的地域,
你将至少需要做一个Django core的最
小翻译。
消息文件
第一步,就是为一种语言创建一个信
息文件。 信息文件是包含了某一语
言翻译字符串和对这些字符串的翻译
的一个文本文件。 信息文件以 .po 为
后缀名。
Django中带有一个工具, bi n/ make-
messages.py ,它完成了这些文件的创
建和维护工作。 运行以下命令来创
建或更新一个信息文件:
django-admin.py makemessages -l de
其中 de 是所创建的信息文件的语言
代码。 在这里,语言代码是以本地
格式给出的。 例如,巴西地区的葡
萄牙语为 pt_BR ,澳大利亚地区的德
语为 de_AT 。
这段脚本应该在三处之一运行:
Django项目根目录。
您Django应用的根目录。
django 根目录(不是Subversion检
出目录,而是通
过 $PYTHONPATH 链接或位于该
路径的某处)。 这仅和你为
Django自己创建一个翻译时有
关。
这段脚本遍历你的项目源树或你的应
用程序源树并且提取出所有为翻译而
被标记的字符串。 它在
locale/LANG/LC_MESSAGES 目录下
创建(或更新)了一个信息文件。针
对上面的de,应该是
locale/de/LC_MESSAGES/django.po。
作为默认, django-
admin.py makemessages 检测每一个
有 .html 扩展名的文件。 以备你要重
载缺省值,使用--extension 或 -e 选项
指定文件扩展名来检测。
django-admin.py makemessages -l de -
e txt
用逗号和(或)使用-e或--extension
来分隔多项扩展名:
django-admin.py makemessages -l de -
e html,txt -e xml
当创建JavaScript翻译目录时,你需
要使用特殊的Django域:not -e js 。
没有gettext?
如果没有安装 gettext 组件, make-
messages.py 将会创建空白文件。 这
种情况下,安装 gettext 组件或只是复
制英语信息文件
( conf/locale/en/LC_MESSAGES/djang
o.po )来作为一个起点;只是一个空
白的翻译信息文件而已。
工作在Windows上么?
如果你正在使用Windows,且需要安
装GNU gettext共用程序以便 django-
admin makemessages 可以工作,请参
看下面Windows小节中gettext部分以
获得更多信息。
.
po 文件格式很直观。 每个 .po 文件
包含一小部分的元数据,比如翻译维
护人员的联系信息,而文件的大部分
内容是简单的翻译字符串和对应语言
翻译结果的映射关系的列表。
举个例子,如果Django应用程序包括
一个 "Welcome to my site." 的待翻译
字符串 ,像这样:
_
("Welcome to my site.")
则django-admin.py makemessages将创
建一个 .po 文件来包含以下片段的消
息:
#
: path/to/python/module.py:23
msgid "Welcome to my site."
msgstr ""
快速解释:
msgi d 是在源文件中出现的翻译字
符串。 不要做改动。
ms gs tr 是相应语言的翻译结果。
刚创建时它只是空字符串,此时
就需要你来完成它。 注意不要丢
掉语句前后的引号。
作为方便之处,每一个消息都包
括:以 # 为前缀的一个注释行并
且定位上边的msgi d 行,文件名和
行号。
对于比较长的信息也有其处理方
法。 ms gs tr (或 msgi d )后紧跟着的
字符串为一个空字符串。 然后真正
的内容在其下面的几行。 这些字符
串会被直接连在一起。 同时,不要
忘了字符串末尾的空格,因为它们会
不加空格地连到一起。
若要对新创建的翻译字符串校验所有
的源代码和模板,并且更新所有语言
的信息文件,可以运行以下命令:
django-admin.py makemessages -a
编译信息文件
创建信息文件之后,每次对其做了修
改,都需要将它重新编译成一种更有
效率的形式,供 gettext 使用。可以使
用django-admin.py compilemessages完
成。
这个工具作用于所有有效的 .po 文
件,创建优化过的二进制 .mo 文件
供 gettext 使用。在你可以运行django-
admin.py makemessages的目录下,运
行django-admin.py compilemessages:
django-admin.py compilemessages
就是这样了。 你的翻译成果已经可
以使用了。
Django如何处理语言
偏好
一旦你准备好了翻译,如果希望在
Django中使用,那么只需要激活这些
翻译即可。
在这些功能背后,Django拥有一个灵
活的模型来确定在安装和使用应用程
序的过程中选择使用的语言。
要设定一个安装阶段的语种偏好,请
设定LANGUAGE_CODE。如果其他
翻译器没有找到一个译文,Django将
使用这个语种作为缺省的翻译最终尝
试。
如果你只是想要用本地语言来运行
Django,并且该语言的语言文件存
在,只需要简单地设
置 LANGUAGE_CODE 即可。
如果要让每一个使用者各自指定语言
偏好,就需要使
用 LocaleMiddleware 。 LocaleMiddle
ware 使得Django基于请求的数据进行
语言选择,从而为每一位用户定制内
容。 它为每一个用户定制内容。
使用 LocaleMiddleware 需要
在 MIDDLEWARE_CLASSES 设置中
增
加'django.middleware.locale.LocaleMid
dleware' 。 中间件的顺序是有影响
的,最好按照依照以下要求:
保证它是第一批安装的中间件
类。
因为 LocalMiddleware 要用到
session数据,所以需要放
在 SessionMiddleware 之后。
如果你使用CacheMiddleware,把
LocaleMiddleware放在它后面。
例如, MIDDLE_CLASSES 可能会是
如此:
MIDDLEWARE_CLASSES = (
django.contrib.sessions.middleware.Ses
sionMiddleware',
django.middleware.locale.LocaleMiddl
eware',
dj ango.mi ddl ew are.common.CommonM
'
'
'
iddleware',
)
(
更多关于中间件的内容,请参阅第
1
7章)
LocaleMiddleware 按照如下算法确定
用户的语言 :
首先,在当前用户的 session 的中
查找django_language键;
如未找到,它会找寻一个cookie
还找不到的话,它会在 HTTP 请
求头部里查找Accept-Language,
该头部是你的浏览器发送的,并
且按优先顺序告诉服务器你的语
言偏好。 Django会尝试头部中的
每一个语种直到它发现一个可用
的翻译。
以上都失败了的话, 就使用全局
的 LANGUAGE_CODE 设定值。
备注:
在上述每一处,语种偏好应作为
字符串,以标准的语种格式出
现。 例如,巴西葡萄牙语是pt-br
如果一个基本语种存在而亚语种
没有指定,Django将使用基本语
种。 比如,如果用户指定了 de-
at (澳式德语)但Django只有针
对 de 的翻译,那么 de 会被选用。
只有在 LANGUAGES 设置中列出
的语言才能被选用。 若希望将语
言限制为所提供语言中的某些
(因为应用程序并不提供所有语
言的表示),则
将 LANGUAGES 设置为所希望提
供语言的列表,例如: 例如:
LANGUAGES = (
('de', _('German')),
('en', _('English')),
)
上面这个例子限制了语言偏好只
能是德语和英语(包括它们的子
语言,如 de-ch 和 en-us )。
如果自定义了 LANGUAGES ,将
语言标记为翻译字符串是可以
的,但是,请不要使用
django.utils.translation 中
的 gettext() (决不要在settings文件
中导入 django.utils.translation ,因
为这个模块本身是依赖于settings,
这样做会导致无限循环),而是
使用一个“虚构的” gettext() 。
解决方案就是使用一个“虚假
的” gettext() 。以 下是一个settings
文件的例子:
ugettext = lambda s: s
LANGUAGES = (
('de', ugettext('German')),
('en', ugettext('English')),
)
这样做的话, make-messages.py 仍
会寻找并标记出将要被翻译的这
些字符串,但翻译不会在运行时
进行,故而需要在任何使
用 LANGUAGES 的代码中用“真实
的” ugettext() 。
LocaleMiddleware 只能选择那些
Django已经提供了基础翻译的语
言。 如果想要在应用程序中对
Django中还没有基础翻译的语言提
供翻译,那么必须至少先提供该
语言的基本的翻译。 例如,
Django使用特定的信息ID来翻译日
期和时间格式,故要让系统正常
工作,至少要提供这些基本的翻
译。
以英语的 .po 文件为基础,翻译其
中的技术相关的信息,可能还包
括一些使之生效的信息。
技术相关的信息ID很容易被认出
来:它们都是大写的。 这些信息
ID的翻译与其他信息不同:你需要
提供其对应的本地化内容。 例
如,对于 DATETIME_FORMAT
(或 DATE_FORMAT 、
TIME_FORMAT ),应该提供希
望在该语言中使用的格式化字符
串。 格式被模板标签now用来识别
格式字符串。
一旦LocaleMiddleware决定用户的偏
好,它会让这个偏好作为
request.LANGUAGE_CODE对每一个
HttpRequest有效。请随意在你的视图
代码中读一读这个值。 以下是一个
简单的例子:
def hello_world(request):
if request.LANGUAGE_CODE == 'de-
at':
return HttpResponse("You pre
fer to read Austrian German.")
else:
return HttpResponse("You pre
fer to read another language.")
注意,对于静态翻译(无中间件)而
言,此语言在
settings.LANGUAGE_CODE中,而对
于动态翻译(中间件),它在
request.LANGUAGE_CODE中。
在你自己的项目中使
用翻译
Django使用以下算法寻找翻译:
首先,Django在该视图所在的应
用程序文件夹中寻找 locale 目
录。 若找到所选语言的翻译,则
加载该翻译。
第二步,Django在项目目录中寻
找 locale 目录。 若找到翻译,则
加载该翻译。
最后,Django使
用 django/conf/locale 目录中的基
本翻译。
以这种方式,你可以创建包含独立翻
译的应用程序,可以覆盖项目中的基
本翻译。 或者,你可以创建一个包
含几个应用程序的大项目,并将所有
需要的翻译放在一个大的项目信息文
件中。 决定权在你手中。
所有的信息文件库都是以同样方式组
织的: 它们是:
$
APPPATH/ l ocal e/ / LC_MESSAGE
S/django.(po|mo)
$
PROJECTPATH/ l ocal e/ / LC_MESS
AGES/django.(po|mo)
所有在settings文件
中 LOCALE_ PATHS 中列出的路径
以其列出的顺序搜
索/LC_MESSAGES/django.(po|mo)
$PYTHONPATH/ dj ango/ conf/ l ocal e
/
/LC_MESSAGES/django.(po|mo)
要创建信息文件,也是使用 django-
admin.py makemessages.py 工具,和
Django信息文件一样。 需要做的就是
进入正确的目录—— conf/locale (在
源码树的情况下)或者 locale/ (在
应用程序信息或项目信息的情况下)
所在的目录下。 同样地,使
用 compile-messages.py 生成 gettext 需
要使用的二进制 django.mo 文件。
您亦可运行django-
admin.py compilemessages --
settings=path.to.settings 来使编译器处
理所有存在于您LOCALE_ PATHS 设
置中的目录。
应用程序信息文件稍微难以发现——
因为它们需要 LocaleMiddle 。如果不
使用中间件,Django只会处理Django
的信息文件和项目的信息文件。
最后,需要考虑一下翻译文件的结
构。 若应用程序要发放给其他用
户,应用到其它项目中,可能需要使
用应用程序相关的翻译。 但是,使
用应用程序相关的翻译和项目翻译在
使用 make-messages 时会产生古怪的
问题。它会遍历当前路径下所有的文
件夹,这样可能会把应用消息文件里
存在的消息ID重复放入项目消息文件
中。
最容易的解决方法就是将不属于项目
的应用程序(因此附带着本身的翻
译)存储在项目树之外。 这样做的
话,项目级的 make-messages 将只会
翻译与项目精确相关的,而不包括那
些独立发布的应用程序中的字符串。
set_language 重定向
视图
方便起见,Django自带了一
个 django.views.i18n.set_language 视
图,作用是设置用户语言偏好并重定
向返回到前一页面。
在URLconf中加入下面这行代码来激
活这个视图:
(r'^i18n/', include('django.conf.url
s.i18n')),
(
注意这个例子使得这个视图
在 /i18n/setlang/ 中有效。)
这个视图是通过 GET 方法调用的,
在请求中包含了 language 参数。 如
果session已启用,这个视图会将语言
选择保存在用户的session中。 否则,
它会以缺省名django_language在
cookie中保存这个语言选择。(这个名
字可以通过
LANGUAGE_COOKIE_NAME设置来
改变 )
保存了语言选择后,Django根据以下
算法来重定向页面:
Django 在 POST 数据中寻找一
个 下一个 参数。
如果 next 参数不存在或为空,
Django尝试重定向页面为HTML头
部信息中 Referer 的值。
如果 Referer 也是空的,即该用户
的浏览器并不发送 Referer 头信
息,则页面将重定向到 / (页面
根目录)。
这是一个HTML模板代码的例子:
<
=
<
form action="/i18n/setlang/" method
"post">
input name="next" type="hidden" val
ue="/next/page/" />
select name="language">
<
{
<
% for lang in LANGUAGES %}
option value="{{ lang.0 }}">{{
lang.1 }}</option>
{
% endfor %}
<
<
/select>
input type="submit" value="Go" />
<
/form>
翻译与JavaScript
将翻译添加到JavaScript会引起一些
问题:
JavaScript代码无法访问一
个 gettext 的实现。
JavaScript 代码并不访问 .po或 .mo
文件;它们需要由服务器分发。
针对JavaScript的翻译目录应尽量
小。
Django已经提供了一个集成解决方
案: 它会将翻译传递给JavaScript,
因此就可以在JavaScript中调用
gettext 之类的代码。
javascript_catalog视图
这些问题的主要解决方案就
是 javascript_catalog 视图。该视图生
成一个JavaScript代码库,包括模仿
gettext 接口的函数,和翻译字符串的
数组。 这些翻译字符串来自于你在
info_dict或URl中指定的应用,工程或
Django内核。
像这样使用:
js_info_dict = {
'
packages': ('your.app.package',
)
}
,
urlpatterns = patterns('',
(r'^jsi18n//pre>, 'django.views.
i18n.javascript_catalog', js_info_di
ct),
)
packages 里的每个字符串应该是
Python中的点分割的包的表达式形式
(
和在 INSTALLED_APPS 中的字符
串相同的格式),而且应指向包
含 locale 目录的包。 如果指定了多
个包,所有的目录会合并成一个目
录。 如果有用到来自不同应用程序
的字符串的JavaScript,这种机制会
很有帮助。
你可以动态使用视图,将包放在
urlpatterns里:
urlpatterns = patterns('',
(r'^jsi18n/(?P<packages>\S+)/$',
'
django.views.i18n.javascript_catal
og'),
)
这样的话,就可以在URL中指定由加
号( + )分隔包名的包了。 如果页
面使用来自不同应用程序的代码,且
经常改变,还不想将其放在一个大的
目录文件中,对于这些情况,显然这
是很有用的。 出于安全考虑,这些
值只能
是 django.conf 或 INSTALLED_APPS
设置中的包。
使用JavaScript翻译目录
要使用这个目录,只要这样引入动态
生成的脚本:
<
/
script type="text/javascript" src="
path/to/jsi18n/"></script>
这就是管理页面如何从服务器获取翻
译目录。 当目录加载后,JavaScript
代码就能通过标准的 gettext 接口进行
访问:
document.write(gettext('this is to b
e translated'));
也有一个ngettext接口:
var object_cnt = 1 // or 0, or 2, or
3
, ...
s = ngettext('literal for the singul
ar case',
'
literal for the plural case
'
, object_cnt);
甚至有一个字符串插入函数:
function interpolate(fmt, obj, named
)
;
插入句法是从Python借用的,所以
interpolate 函数对位置和命名插入均
提供支持:
位置插入 obj包括一个JavaScript数
组对象,元素值在它们对应于fmt
的占位符中以它们出现的相同次
序顺序插值 。 例如:
fmts = ngettext('There is %s object.
Remaining: %s',
'
There are %s objects. Remai
ning: %s', 11);
s = interpolate(fmts, [11, 20]);
/
/ s is 'There are 11 objects. Remai
ning: 20'
命名插入 通过传送为真(TRUE)
的布尔参数name来选择这个模
式。 obj包括一个 JavaScript 对象
或相关数组。 例如:
d = {
count: 10
total: 50
}
;
fmts = ngettext('Total: %(total)s, t
here is %(count)s object',
'
there are %(count)s of a total of %
(total)s objects', d.count);
s = interpolate(fmts, d, true);
但是,你不应重复编写字符串插值:
这还是JavaScript,所以这段代码不
得不重复做正则表达式置换。 它不
会和Python中的字符串插补一样快,
因此只有真正需要的时候再使用它
(
例如,利用 ngettext 生成合适的复
数形式)。
创建JavaScript翻译目录
你可以创建和更改翻译目录,就像其
他
Django翻译目录一样,使用django-
admin.py makemessages 工具。 唯一的
差别是需要提供一个-d djangojs 的参
数,就像这样:
django-admin.py makemessages -d djan
gojs -l de
这样来创建或更新JavaScript的德语
翻译目录。 和普通的Django翻译目录
一样,更新了翻译目录后,运行
compile-messages.py 即可。
熟悉 gettext 用户的注
意事项
如果你了解 gettext ,你可能会发现
Django进行翻译时的一些特殊的东
西:
字符串域为 django 或 djangojs 。
字符串域是用来区别将数据存储
在同一信息文件库(一般
是/usr/share/locale/ )的不同程
序。django 域是为Python和模板翻
译字符串服务的,被加载到全局
翻译目录。 djangojs 域只是用来
尽可能缩小JavaScript翻译的体
积。
Django不单独使用 xgettext , 而是
经过Python包装后的xgettext和
msgfmt。这主要是为了方便。
Windo ws下的gettext
对于那些要提取消息或编译消息文件
的人们来说,需要的只有这么多。翻
译工作本身仅仅包含编辑这个类型的
现存文件,但如果你要创建你自己的
消息文件,或想要测试或编译一个更
改过的消息文件,你将需要这个
gettext公用程序。
从
http://sourceforge.net/projects/get
text下载以下zip文件
gettext-runtime-X.bin.woe32.zip
gettext-tools-X.bin.woe32.zip
libiconv-X.bin.woe32.zip
在同一文件夹下展开这3个文件。
(
也就是 C:\Program Files\gettext-
utils )
更新系统路径:
控制面板 > 系统> 高级 > 环境
变量
在系统变量列表中,点击
Path,点击Edit
把;C:\Program Files\gettext-
utils\bin加到变量值字段的末
尾。
只要 xgettext --version 命令正常
工作,你亦可使用从别处获得的
gettext的二进制代码。 有些版本的
0
.14.4二进制代码被发现不支持这个
命令。 不要试图与Django公用程序一
起使用一个gettext。在一个windows
命令提示窗口输入命令
xgettext --version 将导致出现一
个错误弹出窗口–“xgettext.exe产生错
误并且将被windows关闭”。
System Message: WARNING/ 2 (, line
1
346); backlink
Inline literal start-string without end-
string.
Internet并不安全。
现如今,每天都会出现新的安全问
题。 我们目睹过病毒飞速地蔓延,
大量被控制的肉鸡作为武器来攻击其
他人,与垃圾邮件的永无止境的军备
竞赛,以及许许多多站点被黑的报
告。
作为Web开发人员,我们有责任来对
抗这些黑暗的力量。 每一个Web开发
者都应该把安全看成是Web编程中的
基础部分。 不幸的是,要实现安全
是困难的。
Django试图减轻这种难度。 它被设计
为自动帮你避免一些web开发新手
(甚至是老手)经常会犯的错误。
尽管如此,需要弄清楚,Django如何
保护我们,以及我们可以采取哪些重
要的方法来使得我们的代码更加安
全。
首先,一个重要的前提: 我们并不
打算给出web安全的一个详尽的说
明,因此我们也不会详细地解释每一
个薄弱环节。 在这里,我们会给出
Django所面临的安全问题的一个大
概。
Web安全现状
如果你从这章中只学到了一件事情,
那么它会是:
在任何条件下都不要相信浏览器端提
交的数据。
你从不会知道HTTP连接的另一端会
是谁。 可能是一个正常的用户,但
是同样可能是一个寻找漏洞的邪恶的
骇客。
从浏览器传过来的任何性质的数据,
都需要近乎狂热地接受检查。 这包
括用户数据(比如Web表单提交的内
容)和带外数据(比如,HTTP头、
cookies以及其他信息)。 要修改那
些浏览器自动添加的元数据,是一件
很容易的事。
在这一章所提到的所有的安全隐患都
直接源自对传入数据的信任,并且在
使用前不加处理。 你需要不断地问
自己,这些数据从何而来。
SQL注入
SQL注入 是一个很常见的形式,在
SQL注入中,攻击者改变web网页的
参数(例如 GET /POST 数据或者
URL地址),加入一些其他的SQL片
段。 未加处理的网站会将这些信息
在后台数据库直接运行。
这种危险通常在由用户输入构造SQL
语句时产生。 例如,假设我们要写
一个函数,用来从通信录搜索页面收
集一系列的联系信息。 为防止垃圾
邮件发送器阅读系统中的email,我
们将在提供email地址以前,首先强
制用户输入用户名。
def user_contacts(request):
user = request.GET['username']
sql = "SELECT * FROM user_contac
ts WHERE username = '%s';" % usernam
e
#
execute the SQL here...
备注
在这个例子中,以及在以下所有
的“不要这样做”的例子里,我们都去
除了大量的代码,避免这些函数可以
正常工作。 我们可不想这些例子被
拿出去使用。
尽管,一眼看上去,这一点都不危
险,实际上却不尽然。
首先,我们对于保护email列表所采
取的措施,遇到精心构造的查询语句
就会失效。 想象一下,如果攻击者
在查询框中输入 "' OR 'a'='a" 。
此时,查询的字符串会构造如下:
SELECT * FROM user_contacts WHERE us
ername = '' OR 'a' = 'a';
由于我们允许不安全的SQL语句出现
在字符串中,攻击者加入 OR 子句,
使得每一行数据都被返回。
事实上,这是最温和的攻击方式。
如果攻击者提交了
"
'; DELETE FROM user_contacts WH
,
我们最终将得到这样的查询:
SELECT * FROM user_contacts WHERE us
ername = ''; DELETE FROM user_contac
ts WHERE 'a' = 'a';
哦!我们整个通信录名单去哪儿了?
我们整个通讯录会被立即删除
解决方案
尽管这个问题很阴险,并且有时很难
发现,解决方法却很简单: 绝不信
任用户提交的数据,并且在传递给
SQL语句时,总是转义它。
Django的数据库API帮你做了。 它会
根据你所使用的数据库服务器(例如
PostSQL或者MySQL)的转换规则,
自动转义特殊的SQL参数。
举个例子,在下面这个API调用中:
foo.get_list(bar__exact="' OR 1=1")
Django会自动进行转义,得到如下表
达:
SELECT * FROM foos WHERE bar = '\' O
R 1=1'
完全无害。
这被运用到了整个Django的数据库
API中,只有一些例外:
传给 extra() 方法的 where 参数。
(参考 附录 C。) 这个参数故意设
计成可以接受原始的SQL。
使用底层数据库API的查询。 (详
见第十章)
以上列举的每一个示例都能够很容易
的让您的应用得到保护。 在每一个
示例中,为了避免字符串被篡改而使
用绑定参数 来代替。这样,本节开
始的例子应该写成这样:
from django.db import connection
def user_contacts(request):
user = request.GET['username']
sql = "SELECT * FROM user_contac
ts WHERE username = %s"
cursor = connection.cursor()
cursor.execute(sql, [user])
#
... do something with the resu
lts
底层 execute 方法采用了一个SQL字
符串作为其第二个参数,这个SQL字
符串包含若干’%s’占位符,execute方
法能够自动对传入列表中的参数进行
转义和插入。 你应该用 always 这种
方式构造自定义的SQL。
不幸的是,您并不是在SQL中能够处
处都使用绑定参数,绑定参数不能够
作为标识符(如表或列名等)。 因
此,如果您需要这样做—我是说—动
态构建 POST 变量中的数据库表的列
表的话,您需要在您的代码中来对这
些数据库表的名字进行转义。 Django
提供了一个函
数, django.db.backend.quote_name ,
这个函数能够根据当前数据库引用结
构对这些标识符进行转义。
跨站点脚本 (XSS)
在Web应用中, 跨站点脚本 (XSS)有
时在被渲染成HTML之前,不能恰当
地对用户提交的内容进行转义。 这
使得攻击者能够向你的网站页面插入
通常以 标签形式的任意HTML代
码。
攻击者通常利用XSS攻击来窃取
cookie和会话信息,或者诱骗用户将
其私密信息透漏给被人(又称 钓
鱼 )。
这种类型的攻击能够采用多种不同的
方式,并且拥有几乎无限的变体,因
此我们还是只关注某个典型的例子
吧。 让我们来想想这样一个极度简
单的Hello World视图:
from django.http import HttpResponse
def say_hello(request):
name = request.GET.get('name', '
world')
return HttpResponse('<h1>Hello,
%
s!</h1>' % name)
这个视图只是简单的从GET参数中读
取姓名然后将姓名传递给hel l o.html模
板。 因此,如果我们访问
http://example.com/hello/?name=J
,被呈现的页面将会包含一以下这
些:
<
h1>Hello, Jacob!</h1>
但是,等等,如果我们访问
http://example.com/hello/?name=J
时又会发生什么呢?
<
h1>Hello, <i>Jacob</i>!</h1>
当然,一个攻击者不会使用标签开始
的类似代码,他可能会用任意内容去
包含一个完整的HTML集来劫持您的
页面。 这种类型的攻击已经运用于
虚假银行站点以诱骗用户输入个人信
息,事实上这就是一种劫持XSS的形
式,用以使用户向攻击者提供他们的
银行帐户信息。
如果您将这些数据保存在数据库中,
然后将其显示在您的站点上,那么问
题就变得更严重了。 例如,一旦
MySpace被发现这样的特点而能够轻
易的被XSS攻击,后果不堪设想。 某
个用户向他的简介中插入
JavaScript,使得您在访问他的简介
页面时自动将其加为您的好友,这样
在几天之内,这个人就能拥有上百万
的好友。 在几天的时间里,他拥有
了数以百万的朋友。
现在,这种后果听起来还不那么恶
劣,但是您要清楚——这个攻击者正
设法将 他 的代码而不是MySpace的
代码运行在 您 的计算机上。 这显然
违背了假定信任——所有运行在
MySpace上的代码应该都是MySpace
编写的,而事实上却不如此。
MySpace是极度幸运的,因为这些恶
意代码并没有自动删除访问者的帐
户,没有修改他们的密码,也并没有
使整个站点一团糟,或者出现其他因
为这个弱点而导致的其他噩梦。
解决方案
解决方案是简单的: 总是转义可能
来自某个用户的任何内容。
为了防止这种情况,Django的模板系
统自动转义所有的变量值。 让我们
来看看如果我们使用模板系统重写我
们的例子会发生什么
#
views.py
from django.shortcuts import render_
to_response
def say_hello(request):
name = request.GET.get('name', '
world')
return render_to_response('hello
.
#
<
html', {'name': name})
hello.html
h1>Hello, {{ name }}!</h1>
这样,一个到
http://example.com/hello/name=Ja
的请求将导致下面的页面:
<
<
h1>Hello, <i>Jacob</i>!
/h1>
我们在第四章涵盖了Django的自动转
义,一起想办法将其关闭。 甚至,
如果Django真的新增了这些特性,您
也应该习惯性的问自己,一直以来,
这些数据都来自于哪里呢? 没有哪
个自动解决方案能够永远保护您的站
点百分之百的不会受到XSS攻击。
伪造跨站点请求
伪造跨站点请求(CSRF)发生在当某个
恶意Web站点诱骗用户不知不觉的从
一个信任站点下载某个URL之时,这
个信任站点已经被通过信任验证,因
此恶意站点就利用了这个被信任状
态。
Django拥有内建工具来防止这种攻
击。 包括攻击本身及其使用的工具
都在有详细介绍。16章
会话伪造/劫持
这不是某个特定的攻击,而是对用户
会话数据的通用类攻击。 这种攻击
可以采取多种形式:
中间人 攻击:检索所在有线(无
线)网络,监听会话数据。
伪造会话 :攻击者利用会话
ID(可能是通过中间人攻击来获
得)将自己伪装成另一个用户。
这两种攻击的一个例子可以是在
一间咖啡店里的某个攻击者利用
店内的无线网络来捕获某个会话
cookie,然后她就可以利用那个
cookie来假冒原始用户。 她便可以
使该cookie来模拟原始用户。
伪造cookie :就是指某个攻击者覆
盖了在某个cookie中本应该是只读
的数据。 第十四章 __ 详细介绍了
cookies如何工作,以及要点之一
的是,它在你不知道的情况下无
视浏览器和恶意用户私自改变
cookies。
Web站点以 IsLoggedIn=1 或
者 LoggedInAsUser=jacob 这样的方
式来保存cookie由来已久,使用这
样的cookie是再简单不过的了。
一个更微妙的层面上,然而,相
信在cookies中存储的任意信息绝
对不是一个好主意。 你永远不知
道谁一直在作怪。
会话滞留 :攻击者诱骗用户设置
或者重设置该用户的会话ID。
例如,PHP允许在
URL(如 http://example.com/?
1
374c510d7a32 等)中传递会话标
识符。攻击者欺骗用户点击一个
硬编码会话ID的链接,这回导致
用户转到那个会话。
会话滞留已经运用在钓鱼攻击
中,以诱骗用户在攻击者拥有的
账号里输入其个人信息。 他可以
稍后登陆账户并且检索数据。
会话中毒 :攻击者通过用户提交
设置会话数据的Web表单向该用户
会话中注入潜在危险数据。
一个经典的例子就是一个站点在
某个cookie中存储了简单的用户偏
好(比如一个页面背景颜色)。
攻击者可以诱骗用户点击一个链
接来提交背景颜色,实际上包含
了一个XSS攻击。 如果颜色没有
转义,那么就可以再把恶意代码
注入到用户环境中。
解决方案
有许多基本准则能够保护您不受到这
些攻击:
不要在URL中包含任何session信
息。
Django的session框架(参见
第十四章 __ )根本不会容许
session包含在URL中。
不要直接在cookie中保存数据。 相
反,存储一个在后台映射到session
数据存储的session ID。
如果使用Django内置的session框架
(即 request.session ),它会自动
进行处理。 这个session框架仅在
cookie中存储一个session ID,所有
的session数据将会被存储在数据库
中。
如果需要在模板中显示session数
据,要记得对其进行转义。 可参
考之前的XSS部分,对所有用户提
交的数据和浏览器提交的数据进
行转义。 对于session信息,应该
像用户提交的数据一样对其进行
处理。
任何可能的地方都要防止攻击者
进行session欺骗。
尽管去探测究竟是谁劫持了会话
ID是几乎不可能的事儿,Django还
是内置了保护措施来抵御暴力会
话攻击。 会话ID被存在哈希表里
(取代了序列数字),这样就阻
止了暴力攻击,并且如果一个用
户去尝试一个不存在的会话那么
她总是会得到一个新的会话ID,
这样就阻止了会话滞留。
请注意,以上没有一种准则和工具能
够阻止中间人攻击。 这些类型的攻
击是几乎不可能被探测的。 如果你
的站点允许登陆用户去查看任意敏感
数据的话,你应该 总是 通过HTTPS
来提供网站服务。 此外,如果你的
站点使用SSL,你应该
将 SESSION_COOKIE_SECURE 设置
为 Tr ue ,这样就能够使Django只通过
HTTPS发送会话cookie。
邮件头部注入
邮件头部注入 :SQL注入的兄弟,是
一种通过劫持发送邮件的Web表单的
攻击方式。 攻击者能够利用这种技
术来通过你的邮件服务器发送垃圾邮
件。 在这种攻击面前,任何方式的
来自Web表单数据的邮件头部构筑都
是非常脆弱的。
让我们看看在我们许多网站中发现的
这种攻击的形式。 通常这种攻击会
向硬编码邮件地址发送一个消息,因
此,第一眼看上去并不显得像面对垃
圾邮件那么脆弱。
但是,大多数表单都允许用户输入自
己的邮件主题(同时还有fr om地址,
邮件体,有时还有部分其他字段)。
这个主题字段被用来构建邮件消息的
主题头部。
如果那个邮件头部在构建邮件信息时
没有被转义,那么攻击者可以提交类
似"hello\ncc:spamvictim@example.com
"
(这里的 "\n" 是换行符)的东西。
这有可能使得所构建的邮件头部变
成:
To: hardcoded@example.com
Subject: hello
cc: spamvictim@example.com
就像SQL注入那样,如果我们信任了
用户提供的主题行,那样同样也会允
许他构建一个头部恶意集,他也就能
够利用联系人表单来发送垃圾邮件。
解决方案
我们能够采用与阻止SQL注入相同的
方式来阻止这种攻击: 总是校验或
者转义用户提交的内容。
Django内建邮件功能
(在 django.core.mail 中)根本不允许
在用来构建邮件头部的字段中存在换
行符(表单,收件地址,还有主
题)。 如果您试图使
用 django.core.mail.send_mail 来处理
包含换行符的主题时,Django将会抛
出BadHeaderError异常。
如果你没有使用Django内建邮件功能
来发送邮件,那么你需要确保包含在
邮件头部的换行符能够引发错误或者
被去掉。 你或许想仔细阅
读 django.core.mail 中
的 SateMIMEText 类来看看Django是
如何做到这一点的。
目录遍历
目录遍历 :是另外一种注入方式的
攻击,在这种攻击中,恶意用户诱骗
文件系统代码对Web服务器不应该访
问的文件进行读取和/或写入操作。
例子可以是这样的,某个视图试图在
没有仔细对文件进行防毒处理的情况
下从磁盘上读取文件:
def dump_file(request):
filename = request.GET["filename
"
]
filename = os.path.join(BASE_PAT
H, filename)
content = open(filename).read()
#
...
尽管一眼看上去,视图通
过 BASE_PATH (通过使
用 os.path.join )限制了对于文件的访
问,但如果攻击者使用了包含 .. (两
个句号,父目录的一种简写形式)的
文件名,她就能够访问
到 BASE_PATH 目录结构以上的文
件。对她来说,发现究竟使用几个点
号只是时间问题,比如这样:
.
./../../../../etc/passwd 。
任何不做适当转义地读取文件操作,
都可能导致这样的问题。 允许 写 操
作的视图同样容易发生问题,而且结
果往往更加可怕。
这个问题的另一种表现形式,出现在
根据URL和其他的请求信息动态地加
载模块。 一个众所周知的例子来自
于Ruby on Rails。 在2006年上半年之
前,Rails使用类似
于 http://example.com/person/poke/1 这
样的URL直接加载模块和调用函数。
结果是,精心构造的URL,可以自动
地调用任意的代码,包括数据库的清
空脚本。
解决方案
如果你的代码需要根据用户的输入来
读写文件,你就需要确保,攻击者不
能访问你所禁止访问的目录。
备注
不用多说,你 永远 不要在编写可以
读取任何位置上的文件的代码!
Django内置的静态内容视图是做转义
的一个好的示例
(在 django.views.static 中)。这是相
关代码:
import os
import posixpath
#
...
path = posixpath.normpath(urllib.unq
uote(path))
newpath = ''
for part in path.split('/'):
if not part:
#
strip empty path component
s
continue
drive, part = os.path.splitdrive
(part)
head, part = os.path.split(part)
if part in (os.curdir, os.pardir
)
:
#
strip '.' and '..' in path
continue
newpath = os.path.join(newpath,
part).replace('\\', '/')
Django不读取文件(除非你使
用 static.serve 函数,但也受到了上面
这段代码的保护),因此这种危险对
于核心代码的影响就要小得多。
更进一步,URLconf抽象层的使用,
意味着不经过你明确的指定,
Django 决不会 装载代码。 通过创建
一个URL来让Django装载没有在
URLconf中出现的东西,是不可能发
生的。
暴露错误消息
在开发过程中,通过浏览器检查错误
和跟踪异常是非常有用的。 Django提
供了漂亮且详细的debug信息,使得
调试过程更加容易。
然而,一旦在站点上线以后,这些消
息仍然被显示,它们就可能暴露你的
代码或者是配置文件内容给攻击者。
还有,错误和调试消息对于最终用户
而言是毫无用处的。 Django的理念
是,站点的访问者永远不应该看到与
应用相关的出错消息。 如果你的代
码抛出了一个没有处理的异常,网站
访问者不应该看到调试信息或者 _任
何_代码片段或者Python(面向开发
者)出错消息。 访问者应该只看到
友好的无法访问的页面。
当然,开发者需要在debug时看到调
试信息。 因此,框架就要将这些出
错消息显示给受信任的网站开发者,
而要向公众隐藏。
解决方案
正如我们在第12章所提到的,Django
的 DEBUG 设置控制这些错误信息的
显示。 当你准备部署时请确认把这
个设置为: False 。
在Apache和mod_python下开发的人
员,还要保证在Apache的配置文件中
关闭 PythonDebug Off 选项,这个会
在Django被加载以前去除出错消息。
安全领域的总结
我们希望关于安全问题的讨论,不会
太让你感到恐慌。 Web是一个处处布
满陷阱的世界,但是只要有一些远
见,你就能拥有安全的站点。
永远记住,Web安全是一个不断发展
的领域。如果你正在阅读这本书的停
止维护的那些版本,请阅读最新版本
的这个部分来检查最新发现的漏洞。
事实上,每周或者每月花点时间挖掘
Web应用安全,并且跟上最新的动态
是一个很好的主意。 花费很少,但
是对你网站和用户的保护确是无价
的。
接下来?
你已经完成了我们安排的程序。 以
下的附录内容中包含了可能在你的
Djang项目中用得上的引用资源.
在运行你的Django网站时,无论是为
你或几个朋友的小网站,或者是下一
个google,我们祝你好运。




