Python有众多优点,其中之一就是“开机即用”原则:安装Python的同时安装好大量的标准软件包,这样你可以立即使用而不用自己去下载。Django也遵循这个原则,它同样包含了自己的标准库。这一章就来讲这些集成的子框架。
Django标准库
Django的标准库存放在django.contrib包中。每个子包都是一个独立的附加功能包。它们互相之间一般没有必然的关联,但是有些django.contrib子包可能依赖其他的包。
在django.contrib中对函数的类型并没有强制要求。其中一些包中带有模型(因此需要你在数据库中安装对应的数据表),但其它一些由独立的中间件及模板标签组成。
django.contrib开发包共有的特性是:就算你将整个django.contrib开发包删除,你依然可以使用
Django 的基础功能而不会遇到任何问题。当 Django开发者向框架增加新功能的时,他们会严格根据这一教条来决定是否把新功能放入django.contrib中。
django.contrib由以下开发包组成:
§ admin:自动化的站点管理工具。请查看Django管理站点
§ auth: Django的用户验证框架。请查会话、用户和注册
§ comments:一个评论应用,目前,这个应用正在紧张的开发中,因此在本书出版的时候还不能给出一个完整的说明,关于这个应用的更多信息请参见Django的官方网站.
§ contenttypes:这是一个用于文档类型钩子的框架,每个安装的Django模块作为一种独立的文档类型。这个框架主要在Django内部被其他应用使用,它主要面向Django的高级开发者。可以通过阅读源码来了解关于这个框架的更多信息,源码的位置在django/contrib/contenttypes/.
§ csrf:这个模块用来防御跨站请求伪造(CSRF).参见后面标题为”CSRF防御”的小节。
§ flatpages:一个在数据库中管理单一HTML内容的模块,参见后面标题为“Flatpages”的小节。
§ humanize:一系列 Django
模块过滤器,用于增加数据的人性化。
§ markup:一系列的 Django
模板过滤器,用于实现一些常用标记语言。
§ redirects:用来管理重定向的框架。
§ sessions: Django的会话框架,参见会话、用户和注册。
§ sitemaps:用来生成网站地图的 XML
文件的框架。参见Django输出非HTML内容。
§ sites:一个让你可以在同一个数据库与 Django安装中管理多个网站的框架。
§ syndication:一个用 RSS
和 Atom 来生成聚合订阅源的的框架。参阅Django输出非HTML内容。
本章接下来将详细描述前面没有介绍过的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对象,我们通过使用当前站点的name(例如'LJWorld.com')和domain(例如'www.ljworld.com')可以把感谢通知抽提出来。
Django 的多站点框架为你提供了一个位置来存储 Django项目中每个站点的name和domain,这意味着你可以用同样的方法来重用这些值。
如何使用多站点框架
多站点框架与其说是一个框架,不如说是一系列约定。所有的一切都基于两个简单的概念:
§ 位于django.contrib.sites的Site模型有domain和name两个字段。
§ SITE_ID设置指定了与特定配置文件相关联的Site对象之数据库 ID。
如何运用这两个概念由你决定,但 Django是通过几个简单的约定自动使用的。
安装多站点应用要执行以下几个步骤:
1. 将'django.contrib.sites'加入到INSTALLED_APPS中。
2. 运行 manage.py syncdb命令将 django_site表安装到数据库中。
3. 通过 Django管理后台或通过 Python API添加一个或者多个
‘Site’对象。为该 Django
项目支撑的每个站(或域)创建一个Site对象。
4. 在每个设置文件中定义一个SITE_ID变量。该变量值应当是该设置文件所支撑的站点之Site对象的数据库
ID 。
多站点框架的功能
下面几节讲述的是用多站点框架能够完成的几项工作。
多个站点的数据重用
正如在情景一中所解释的,要在多个站点间重用数据,仅需在模型中为Site添加一个多对多字段即可,例如:
from django.db import models from django.contrib.sites.models import Site class Article(models.Model): headline = models.CharField(maxlength=200) # ... sites = models.ManyToManyField(Site)
这是在数据库中为多个站点进行文章关联操作的基础步骤。在适当的位置使用该技术,你可以在多个站点中重复使用同一段 Django视图代码。继续Article模型范例,下面是一个可能的article_detail视图:
from django.conf import settings def article_detail(request, article_id): try: a = Article.objects.get(id=article_id, sites__id=settings.SITE_ID) except Article.DoesNotExist: raise Http404
# ...
该视图方法是可重用的,因为它根据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 import Site class Article(models.Model): headline = models.CharField(maxlength=200) # ... site = models.ForeignKey(Site)
这与前一节中介绍的一样有益。
从视图钩挂当前站点
在底层,通过在 Django视图中使用多站点框架,你可以让视图根据调用站点不同而完成不同的工作,例如:
from django.conf import settings def my_view(request): if settings.SITE_ID == 3: # Do something. else: # Do something else.
当然,像那样对站点 ID进行硬编码是比较难看的。略为简洁的完成方式是查看当前的站点域:
from django.conf import settings from django.contrib.sites.models import Site def my_view(request): current_site = Site.objects.get(id=settings.SITE_ID) if current_site.domain == 'foo.com': # Do something else: # Do something else.
从Site对象中获取settings.SITE_ID值的做法比较常见,因此Site模型管理器
(Site.objects)具备一个get_current()方法。下面的例子与前一个是等效的:
from django.contrib.sites.models import Site def my_view(request): current_site = Site.objects.get_current() if current_site.domain == 'foo.com': # Do something else: # Do something else.
注意
在这个最后的例子里,你不用导入django.conf.settings。
获取当前域用于呈现
正如情景二中所解释的那样,对于储存站名和域名的 DRY (Dont Repeat Yourself)方法(在一个位置储存站名和域名)来说,只需引用当前Site对象的name和domain。例如:
from django.contrib.sites.models import Site from django.core.mail import send_mail def register_for_newsletter(request): # Check form values, etc., and subscribe the user. # ... current_site = Site.objects.get_current() send_mail('Thanks for subscribing to %s alerts' % current_site.name, 'Thanks for your subscription. We appreciate it./n/n-The %s team.' % current_site.name, 'editor@%s' % current_site.domain, [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_mail from django.template import loader, Context def register_for_newsletter(request): # Check form values, etc., and subscribe the user. # ... subject = loader.get_template('alerts/subject.txt').render(Context({})) message = loader.get_template('alerts/message.txt').render(Context({})) send_mail(subject, message, 'do-not-reply@example.com', [user_email]) # ...
本例中,你不得不在 LJWorld.com和 Lawrence.com
的模板目录中都创建一份subject.txt和message.txt模板。正如之前所说,该方法带来了更大的灵活性,但也带来了更多复杂性。
尽可能多的利用Site对象是减少不必要的复杂、冗余工作的好办法。
获取当前域的完整 URL
Django 的get_absolute_url()约定对与获取不带域名的对象 URL非常理想,但在某些情形下,你可能想显示某个对象带有http://和域名以及所有部分的完整
URL。要完成此工作,你可以使用多站点框架。下面是个简单的例子:
>>> from django.contrib.sites.models import Site >>> obj = MyModel.objects.get(id=3) >>> obj.get_absolute_url() '/mymodel/objects/3/' >>> Site.objects.get_current().domain 'example.com' >>> 'http://%s%s' % (Site.objects.get_current().domain, obj.get_absolute_url())
'http://example.com/mymodel/objects/3/'
当前站点管理器
如果站点在你的应用中扮演很重要的角色,请考虑在你的模型中使用方便的CurrentSiteManager。这是一个模型管理器(见附录B),它会自动过滤使其只包含与当前站点相关联的对象。
通过显示地将CurrentSiteManager加入模型中以使用它。例如:
from django.db import models from django.contrib.sites.models import Site from django.contrib.sites.managers import CurrentSiteManager class Photo(models.Model): photo = models.FileField(upload_to='/home/photos') photographer_name = models.CharField(maxlength=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.SITE_ID)
Photo.on_site.all()
CurrentSiteManager是如何知道Photo的哪个字段是Site呢?缺省情况下,它会查找一个叫做site的字段。如果模型中有个外键或多对多字段叫做site之外的名字,你必须显示地将它作为参数传递给CurrentSiteManager。下面的模型中有个叫做publish_on的字段,如下所示:
from django.db import models from django.contrib.sites.models import Site from django.contrib.sites.managers import CurrentSiteManager class Photo(models.Model): photo = models.FileField(upload_to='/home/photos') photographer_name = models.CharField(maxlength=100) pub_date = models.DateField() publish_on = models.ForeignKey(Site) objects = models.Manager() on_site = CurrentSiteManager('publish_on')
如果试图使用CurrentSiteManager并传入一个不存在的字段名, Django将引发一个ValueError异常。
注意事项
即便是已经使用了CurrentSiteManager,你也许还想在模型中拥有一个正常的(非站点相关)的管理器。正如在附录
B 中所解释的,如果你手动定义了一个管理器,那么 Django不会为你创建全自动的objects = models.Manager()管理器。
同样,Django的特定部分——即 Django超级管理站点和通用视图——使用的管理器首先在模型中定义,因此如果希望超级管理站点能够访问所有对象(而不是仅仅站点特有对象),请于定义CurrentSiteManager之前在模型中放入objects
= models.Manager()。
Django如何使用多站点框架
尽管并不是必须的,我们还是强烈建议使用多站点框架,因为 Django在几个地方利用了它。即使只用 Django来支持单个网站,你也应该花一点时间用domain和name来创建站点对象,并将SITE_ID设置指向它的
ID 。
以下讲述的是 Django如何使用多站点框架:
§ 在重定向框架中(见后面的重定向一节),每一个重定向对象都与一个特定站点关联。当 Django搜索重定向的时候,它会考虑当前的SITE_ID。
§ 在注册框架中,每个注释都与特定站点相关。每个注释被张贴时,其site被设置为当前的SITE_ID,而当通过适当的模板标签列出注释时,只有当前站点的注释将会显示。
§ 在 flatpages框架中 (参见后面的
Flatpages一节),每个 flatpage都与特定的站点相关联。创建 flatpage时,你都将指定它的site,而
flatpage中间件在获取 flatpage以显示它的过程中,将查看当前的SITE_ID。
§ 在 syndication框架中(参阅Django输出非HTML内容),title和description的模板自动访问变量{{
site }},它就是代表当前着桨的Site对象。Also, the hook for providing item URLs will use thedomainfrom
the currentSiteobject if you dont specify a fully qualified domain.
§ 在身份验证框架(参见Django会话、用户和注册)中,django.contrib.auth.views.login视图将当前Site名称作为{{
site_name }}传递给模板。
Flatpages - 简单页面
尽管通常情况下总是建造和运行数据库驱动的 Web应用,你还是会需要添加一两张一次性的静态页面,例如“关于”页面,或者“隐私策略”页面等等。可以用像
Apache 这样的标准Web服务器来处理这些静态页面,但却会给应用带来一些额外的复杂性,因为你必须操心怎么配置 Apache,还要设置权限让整个团队可以修改编辑这些文件,而且你还不能使用
Django 模板系统来统一这些页面的风格。
这个问题的解决方案是使用位于django.contrib.flatpages开发包中的 Django简单页面(flatpages)应用程序。该应用让你能够通过
Django超级管理站点来管理这些一次性的页面,还可以让你使用 Django模板系统指定它们使用哪个模板。它在后台使用了 Django模型,也就是说它将页面存放在数据库中,你也可以像对待其他数据一样用标准
Django数据库 API
存取简单页面。
简单页面以它们的 URL和站点为键值。当创建简单页面时,你指定它与哪个URL以及和哪个站点相关联。
使用简单页面
安装平页面应用程序必须按照下面的步骤:
1. 添加'django.contrib.flatpages'到INSTALLED_APPS设置。django.contrib.flatpages依赖于django.contrib.sites,所以确保这两个开发包都包括在``INSTALLED_APPS``设置中。
2. 将'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware'添加到MIDDLEWARE_CLASSES设置中。
3. 运行manage.py syncdb命令在数据库中创建必需的两个表。
简单页面应用程序在数据库中创建两个表:django_flatpage和django_flatpage_sites。django_flatpage只是将
URL 映射到到标题和一段文本内容。django_flatpage_sites是一个多对多表,用于关联某个简单页面以及一个或多个站点。
该应用所带来的FlatPage模型在django/contrib/flatpages/models.py进行定义,如下所示:
from django.db import models from django.contrib.sites.models import Site class FlatPage(models.Model): url = models.CharField(maxlength=100) title = models.CharField(maxlength=200) content = models.TextField() enable_comments = models.BooleanField() template_name = models.CharField(maxlength=70, blank=True) registration_required = models.BooleanField() sites = models.ManyToManyField(Site)
让我们逐项看看这些字段的含义:
§ 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.models import FlatPage >>> from django.contrib.sites.models import Site >>> fp = FlatPage( ... url='/about/', ... title='About', ... content='<p>About this site...</p>', ... enable_comments=False, ... template_name='', ... registration_required=False, ... ) >>> fp.save() >>> fp.sites.add(Site.objects.get(id=1)) >>> FlatPage.objects.get(url='/about/') <FlatPage: /about/ -- About>
使用简单页面模板
缺省情况下,系统使用模板flatpages/default.html来解析简单页面,但你也可以通过设定FlatPage对象的template_name字段来覆盖特定简单页面的模板。
你必须自己创建flatpages/default.html模板。只需要在模板目录创建一个flatpages目录,并把default.html文件置于其中。
简单页面模板只接受有一个上下文变量——flatpage,也就是该简单页面对象。
以下是一个flatpages/default.html模板范例:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"> <html> <head> <title>{{ flatpage.title }}</title> </head> <body> {{ flatpage.content }} </body> </html>
重定向
通过将重定向存储在数据库中并将其视为 Django模型对象,Django
重定向框架让你能够轻松地管理它们。比如说,你可以通过重定向框架告诉Django,把任何指向/music/的请求重定向到/sections/arts/music/。当你需要在站点中移动一些东西时,这项功能就派上用场了——网站开发者应该穷尽一切办法避免出现坏链接。
使用重定向框架
安装重定向应用程序必须遵循以下步骤:
1. 将'django.contrib.redirects'添加到INSTALLED_APPS设置中。
2. 将'django.contrib.redirects.middleware.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为空,它将发送一个 410 (Gone) HTTP头信息以及一个空(无内容)响应。
§ 如果未找到匹配项,该请求将如常处理。
该中间件仅为 404错误激活,而不会为 500
错误或其他任何状态码的响应所激活。
注意必须考虑MIDDLEWARE_CLASSES的顺序。通常,你可以将RedirectFallbackMiddleware放置在列表的最后,因为它是一种终极手段。
注意
如果同时使用重定向和简单页面回退中间件,必须考虑先检查其中的哪一个(重定向或简单页面)。我们建议将简单页面放在重定向之前(因此将简单页面中间件放置在重定向中间件之前),但你可能有不同想法。
增加、变更和删除重定向
你可以两种方式增加、变更和删除重定向:
通过超级管理界面
如果已经激活了全自动的 Django超级管理界面,你应该能够在超级管理首页看到重定向区域。可以像编辑系统中其它对象一样编辑重定向。
通过 Python API
django/contrib/redirects/models.py中的一个标准 Django模型代表了重定向。因此,你可以通过 Django数据库
API 来存取重定向对象,例如:
>>> from django.contrib.redirects.models import Redirect >>> from django.contrib.sites.models import Site >>> red = Redirect( ... site=Site.objects.get(id=1), ... old_path='/music/', ... new_path='/sections/arts/music/', ... ) >>> red.save() >>> Redirect.objects.get(old_path='/music/') <Redirect: /music/ ---> /sections/arts/music/>
CSRF 防护
django.contrib.csrf开发包能够防止遭受跨站请求伪造攻击 (CSRF).
CSRF, 又叫进程跳转,是一种网站安全攻击技术。当某个恶意网站在用户未察觉的情况下将其从一个已经通过身份验证的站点诱骗至一个新的 URL时,这种攻击就发生了,因此它可以利用用户已经通过身份验证的状态。开始的时候,要理解这种攻击技术比较困难,因此我们在本节将使用两个例子来说明。
一个简单的 CSRF例子
假定你已经登录到example.com的网页邮件账号。该网页邮件站点上有一个登出按钮指向了 URLexample.com/logout,换句话说,要登出的话,需要做的唯一动作就是访问
URL :example.com/logout。
通过在(恶意)网页上用隐藏一个指向 URLexample.com/logout的
诚然,对你而言登出一个网页邮件站点并不会构成多大的安全破坏,但同样的攻击可能发生在任何信任用户的站点之上,比如在线银行网站或者电子商务网站。
稍微复杂一点的CSRF例子
在上一个例子中,example.com应该负部分责任,因为它允许通过 HTTPGET方法进行状态变更(即登入和登出)。如果对服务器的状态变更要求使用
HTTPPOST方法,情况就好得多了。但是,即便是强制要求使用POST方法进行状态变更操作也易受到 CSRF攻击。
假设example.com对登出功能进行了升级,登出