精通-Django-全-

精通 Django(全)

原文:zh.annas-archive.org/md5/0D7AA9BDBF4A402F69CD832FB5D17FA6

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

您需要为本书做好准备

所需的编程知识

本书的读者应该了解过程化和面向对象编程的基础知识:控制结构(如 if、while 或 for)、数据结构(列表、哈希/字典)、变量、类和对象。网页开发经验,正如您可能期望的那样,非常有帮助,但不是阅读本书的必要条件。在整本书中,我试图为缺乏这方面经验的读者推广网页开发的最佳实践。

所需的 Python 知识

在其核心,Django 只是用 Python 编写的一组库。要使用 Django 开发网站,您需要编写使用这些库的 Python 代码。因此,学习 Django 实际上就是学习如何在 Python 中编程以及理解 Django 库的工作原理。如果您有 Python 编程经验,那么您应该可以轻松上手。总的来说,Django 代码并不执行很多魔术(即,编程技巧,其实现很难解释或理解)。对您来说,学习 Django 将是学习 Django 的惯例和 API 的问题。

如果您没有 Python 编程经验,您将会有所收获。它很容易学习,也很愉快使用!尽管本书不包括完整的 Python 教程,但它会在适当的时候突出 Python 的特性和功能,特别是当代码不立即让人明白时。不过,我建议您阅读官方的 Python 教程(有关更多信息,请访问docs.python.org/tut/)。我还推荐 Mark Pilgrim 的免费书籍Dive Into Python,可在线获取www.diveintopython.net/,并由 Apress 出版。

所需的 Django 版本

本书涵盖 Django 1.8 LTS。这是 Django 的长期支持版本,将至少在 2018 年 4 月之前得到全面支持。

如果您使用的是 Django 的早期版本,建议您升级到最新版本的 Django 1.8 LTS。在印刷时(2016 年 7 月),Django 1.8 LTS 的最新生产版本是 1.8.13。

如果您安装了 Django 的较新版本,请注意,尽管 Django 的开发人员尽可能保持向后兼容性,但偶尔会引入一些向后不兼容的更改。每个版本的更改都总是在发布说明中进行了解,您可以在docs.djangoproject.com/en/dev/releases/找到。

有任何疑问,请访问:masteringdjango.com

这本书是为谁准备的

本书假设您对互联网和编程有基本的了解。有 Python 或 Django 的经验会是一个优势,但不是必需的。这本书非常适合初学者和中级程序员,他们正在寻找一个快速、安全、可扩展和可维护的替代网页开发平台,而不是基于 PHP、Java 和 dotNET 的平台。

惯例

在本书中,您会发现一些区分不同信息类型的文本样式。以下是一些这些样式的示例及其含义的解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:“在命令提示符(或在Applications/Utilities/Terminal中,OS X 中)键入python。”

代码块设置如下:

from django.http import HttpResponse
def hello(request):
return HttpResponse("Hello world")

任何命令行输入或输出都以以下方式编写:

Python 2.7.5 (default, June 27 2015, 13:20:20)
[GCC x.x.x] on xxx
Type "help", "copyright", "credits" or "license" for more information.
>>>

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这样的方式显示在文本中:“您应该看到文本Hello world-这是您的 Django 视图的输出(图 2-1)。”

注意

警告或重要说明会以这样的方式显示在框中。

提示

提示和技巧是这样显示的。

第一章:Django 简介和入门

介绍 Django

几乎所有优秀的开源软件都是因为一个或多个聪明的开发人员有问题需要解决,而没有可行或成本效益的解决方案。Django 也不例外。Adrian 和 Jacob 早已从项目中退休,但是驱使他们创建 Django 的基本原因仍然存在。正是这种扎实的实际经验基础使 Django 如此成功。为了表彰他们的贡献,我认为最好让他们用自己的话(从原书中编辑和重新格式化)介绍 Django。

Adrian Holovaty 和 Jacob Kaplan-Moss-2009 年 12 月

在早期,网页开发人员手工编写每个页面。更新网站意味着编辑 HTML;重新设计涉及逐个重新制作每个页面。随着网站的增长和变得更加雄心勃勃,很快就显而易见,这种方法是乏味、耗时且最终是不可持续的。

国家超级计算应用中心NCSA,开发了第一个图形化网页浏览器 Mosaic 的地方)的一群有进取心的黑客解决了这个问题,让 Web 服务器生成可以动态生成 HTML 的外部程序。他们称这个协议为通用网关接口CGI),它彻底改变了 Web。现在很难想象 CGI 必须是多么大的突破:CGI 允许你将 HTML 页面视为根据需要动态生成的资源,而不是简单的磁盘文件。

CGI 的发展开创了动态网站的第一代。然而,CGI 也有它的问题:CGI 脚本需要包含大量重复的样板代码,它们使代码重用变得困难,对于初学者来说编写和理解也很困难。

PHP 解决了许多这些问题,并风靡全球——它现在是用于创建动态网站的最流行工具,并且有数十种类似的语言(如 ASP、JSP 等)都紧随 PHP 的设计。PHP 的主要创新在于它的易用性:PHP 代码简单地嵌入到普通 HTML 中;对于已经了解 HTML 的人来说,学习曲线极其浅。

但是 PHP 也有自己的问题;它非常容易使用,鼓励编写松散、重复、考虑不周的代码。更糟糕的是,PHP 几乎没有保护程序员免受安全漏洞的影响,因此许多 PHP 开发人员发现自己只有在为时已晚时才学习安全知识。

这些和类似的挫折直接导致了当前一批第三代Web 开发框架的发展。随着这一新的 Web 开发潮流的兴起,人们对 Web 开发人员的期望也在不断增加。

Django 的发展是为了满足这些新的期望。

Django 的历史

Django 是在美国堪萨斯州劳伦斯的 Web 开发团队编写的真实应用程序的基础上有机地发展起来的。它诞生于 2003 年秋天,当时《劳伦斯日报》报纸的 Web 程序员 Adrian Holovaty 和 Simon Willison 开始使用 Python 构建应用程序。

负责制作和维护几个本地新闻网站的 World Online 团队在由新闻截止日期决定的开发环境中蓬勃发展。对于包括 LJWorld.com、Lawrence.com 和 KUsports.com 在内的网站,记者(和管理层)要求在非常紧迫的时间表下添加功能和构建整个应用程序,通常只有一天或一小时的通知时间。因此,Simon 和 Adrian 出于必要性开发了一个节省时间的 Web 开发框架——这是他们在极端截止日期下构建可维护应用程序的唯一方法。

在 2005 年夏天,经过将这个框架开发到能够有效地为 World Online 的大部分网站提供动力的程度后,包括 Jacob Kaplan-Moss 在内的团队决定将该框架作为开源软件发布。他们于 2005 年 7 月发布了它,并将其命名为 Django,以纪念爵士吉他手 Django Reinhardt。

这段历史很重要,因为它有助于解释两个关键问题。首先是 Django 的“甜蜜点”。因为 Django 诞生于新闻环境中,它提供了一些功能(比如它的管理站点,在第五章中介绍的The * Django * Admin * Site*),特别适合像Amazon.comcraigslist.orgwashingtonpost.com这样提供动态和数据库驱动信息的“内容”网站。

不要让这使你失去兴趣,尽管 Django 特别适合开发这类网站,但这并不排除它成为构建任何类型动态网站的有效工具。(在某些方面特别有效和在其他方面无效之间存在区别。)

第二个需要注意的事情是 Django 的起源如何塑造了其开源社区的文化。因为 Django 是从现实世界的代码中提取出来的,而不是学术练习或商业产品,它专注于解决 Django 开发人员自己曾经面对过的问题,而且仍在面对。因此,Django 本身几乎每天都在积极改进。该框架的维护者有兴趣确保 Django 节省开发人员的时间,生成易于维护并在负载下表现良好的应用程序。

Django 可以让您在极短的时间内构建深度、动态、有趣的网站。Django 旨在让您专注于工作中有趣的部分,同时减轻重复部分的痛苦。通过这样做,它提供了常见 Web 开发模式的高级抽象,频繁编程任务的快捷方式,并明确了解决问题的约定。与此同时,Django 试图不干扰您的工作,让您根据需要在框架范围之外工作。

我们写这本书是因为我们坚信 Django 可以使 Web 开发变得更好。它旨在快速让您开始自己的 Django 项目,然后最终教会您成功设计、开发和部署一个令您自豪的网站所需的一切知识。

入门

要开始使用 Django,您需要做两件非常重要的事情:

  1. 安装 Django(显然);和

  2. 深入了解模型-视图-控制器MVC)设计模式。

首先,安装 Django 非常简单,并且在本章的第一部分中有详细介绍。其次同样重要,特别是如果您是新程序员或者从使用不清晰地将网站的数据和逻辑与其显示方式分离的编程语言转换而来。Django 的理念基于松耦合,这是 MVC 的基本理念。随着我们的学习,我们将更详细地讨论松耦合和 MVC,但如果您对 MVC 了解不多,最好不要跳过本章的后半部分,因为了解 MVC 将使理解 Django 变得更加容易。

安装 Django

在学习如何使用 Django 之前,您必须先在计算机上安装一些软件。幸运的是,这是一个简单的三个步骤过程:

  1. 安装 Python。

  2. 安装 Python 虚拟环境。

  3. 安装 Django。

如果这对您来说不熟悉,不用担心,在本章中,让我们假设您以前从未从命令行安装过软件,并将逐步引导您完成。

我为那些使用 Windows 的人编写了这一部分。虽然 Django 在*nix 和 OSX 用户群体中有很强的基础,但大多数新用户都在 Windows 上。如果您使用 Mac 或 Linux,互联网上有大量资源;最好的起点是 Django 自己的安装说明。有关更多信息,请访问docs.djangoproject.com/en/1.8/topics/install/

对于 Windows 用户,您的计算机可以运行任何最近的 Windows 版本(Vista,7,8.1 或 10)。本章还假设您正在桌面或笔记本电脑上安装 Django,并将使用开发服务器和 SQLite 来运行本书中的所有示例代码。这绝对是您刚开始时设置 Django 的最简单和最好的方法。

如果您确实想要进行更高级的 Django 安装,您的选项在第十三章*,部署 Django*,第二十章*,更多关于安装 Django和第二十一章,高级数据库管理*中都有涵盖。

注意

如果您使用的是 Windows,我建议您尝试使用 Visual Studio 进行所有 Django 开发。微软已经在为 Python 和 Django 程序员提供支持方面进行了重大投资。这包括对 Python/Django 的完整 IntelliSense 支持,并将 Django 的所有命令行工具整合到 VS IDE 中。

最重要的是,这完全免费。我知道,谁会想到 M$会提供免费服务??但这是真的!

有关 Visual Studio Community 2015 的完整安装指南,请参阅附录 G*,使用 Visual Studio 开发 Django*,以及在 Windows 中开发 Django 的一些建议。

安装 Python

Django 本身纯粹是用 Python 编写的,因此安装框架的第一步是确保您已安装 Python。

Python 版本

Django 1.8 LTS 版本与 Python 2.7、3.3、3.4 和 3.5 兼容。对于每个 Python 版本,只支持最新的微版本(A.B.C)。

如果您只是试用 Django,无论您使用 Python 2 还是 Python 3 都无所谓。但是,如果您打算最终将代码部署到实时网站,Python 3 应该是您的首选。Python 维基(有关更多信息,请访问wiki.python.org/moin/Python2orPython3)非常简洁地解释了这背后的原因:

简短版本:Python 2.x 是遗留版本,Python 3.x 是语言的现在和未来

除非您有非常好的理由使用 Python 2(例如,遗留库),否则 Python 3 是最佳选择。

提示

注意:本书中的所有代码示例都是用 Python 3 编写的

安装

如果您使用的是 Linux 或 Mac OS X,您可能已经安装了 Python。在命令提示符(或在 OS X 中的Applications/Utilities/Terminal)中输入python。如果看到类似以下内容,则表示已安装 Python:

Python 2.7.5 (default, June 27 2015, 13:20:20)
[GCC x.x.x] on xxx
Type "help", "copyright", "credits" or "license" for more 
    information.

注意

您可以看到,在前面的示例中,Python 交互模式正在运行 Python 2.7。这是对经验不足的用户的陷阱。在 Linux 和 Mac OS X 机器上,通常会安装 Python 2 和 Python 3。如果您的系统是这样的,您需要在所有命令前面输入python3,而不是 python 来运行 Python 3 的 Django。

假设您的系统尚未安装 Python,我们首先需要获取安装程序。转到www.python.org/downloads/,并单击大黄色按钮,上面写着下载 Python 3.x.x

在撰写本文时,最新版本的 Python 是 3.5.1,但在您阅读本文时可能已经更新,因此数字可能略有不同。

不要下载 2.7.x 版本,因为这是 Python 的旧版本。本书中的所有代码都是用 Python 3 编写的,因此如果尝试在 Python 2 上运行代码,将会出现编译错误。

下载 Python 安装程序后,转到您的Downloads文件夹,双击文件python-3.x.x.msi运行安装程序。安装过程与任何其他 Windows 程序相同,因此如果您之前安装过软件,这里应该没有问题,但是,有一个非常重要的自定义您必须进行。

注意

不要忘记下一步,因为它将解决由于在 Windows 中不正确映射pythonpath(Python 安装的重要变量)而引起的大多数问题。

默认情况下,Python 可执行文件不会添加到 Windows PATH 语句中。为了使 Django 正常工作,Python 必须在 PATH 语句中列出。幸运的是,这很容易纠正:

  • 在 Python 3.4.x 中,当安装程序打开自定义窗口时,选项将 python.exe 添加到 Path未被选中,您必须将其更改为将安装在本地硬盘上,如图 1.1所示。安装

图 1.1:将 Python 添加到 PATH(版本 3.4.x)。

  • 在 Python 3.5.x 中,确保在安装之前选中将 Python 3.5 添加到 PATH图 1.2)。安装

图 1.2:将 Python 添加到 PATH(版本 3.5.x)。

安装 Python 后,您应该能够重新打开命令窗口并在命令提示符下键入 python,然后会得到类似于这样的输出:

Python 3.5.1 (v3.5.1:37a07cee5969, Dec  6 2015, 01:38:48) 
    [MSC v.1900 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more 
    information.
>>>

在此期间,还有一件重要的事情要做。使用CTRL+C退出 Python。在命令提示符下键入以下内容并按 Enter:

python-m pip install-U pip

输出将类似于这样:

C:\Users\nigel>python -m pip install -U pip
Collecting pip
 Downloading pip-8.1.2-py2.py3-none-any.whl (1.2MB)
 100% |################################| 1.2MB 198kB/s
Installing collected packages: pip
Found existing installation: pip 7.1.2
Uninstalling pip-7.1.2:
Successfully uninstalled pip-7.1.2
Successfully installed pip-8.1.2

您现在不需要完全了解这个命令的作用;简而言之,pip是 Python 软件包管理器。它用于安装 Python 软件包:pip实际上是 Pip Installs Packages 的递归缩写。Pip 对我们安装过程的下一阶段非常重要,但首先,我们需要确保我们正在运行最新版本的 pip(在撰写本文时为 8.1.2),这正是这个命令所做的。

安装 Python 虚拟环境

注意

如果您要使用 Microsoft Visual Studio(VS),您可以在这里停下来并跳转到附录 G,使用 Visual Studio 开发 Django。VS 只需要您安装 Python,其他所有操作都可以在集成开发环境(IDE)内完成。

计算机上的所有软件都是相互依存的 - 每个程序都有其他软件依赖的软件部分(称为依赖项)和需要找到文件和其他软件运行所需的设置(称为环境变量)。

当您编写新的软件程序时,可能(并且经常)会修改其他软件依赖的依赖项和环境变量。这可能会导致许多问题,因此应该避免。

Python 虚拟环境通过将新软件所需的所有依赖项和环境变量包装到与计算机上其余软件分开的文件系统中来解决此问题。

注意

一些查看其他教程的人可能会注意到,这一步通常被描述为可选的。这不是我支持的观点,也不是一些 Django 核心开发人员支持的观点。

注意

在虚拟环境中开发 Python 应用程序(其中包括 Django)的优势是显而易见的,这里不值得一提。作为初学者,您只需要相信我 - 运行 Django 开发的虚拟环境是不可选的。

Python 中的虚拟环境工具称为virtualenv,我们使用pip从命令行安装它:

pip install virtualenv

您的命令窗口的输出应该类似于这样:

C:\Users\nigel>pip install virtualenv
 Collecting virtualenv
 Downloading virtualenv-15.0.2-py2.py3-none-any.whl (1.8MB)
100% |################################| 1.8MB 323kB/s
Installing collected packages: virtualenv
Successfully installed virtualenv-15.0.2

一旦安装了virtualenv,您需要通过输入以下命令为您的项目创建一个虚拟环境:

virtualenv env_mysite

注意

互联网上的大多数示例使用env作为您的环境名称。这是不好的;主要是因为通常会安装几个虚拟环境来测试不同的配置,而env并不是非常描述性的。例如,您可能正在开发一个必须在 Python 2.7 和 Python 3.4 上运行的应用程序。命名为env_someapp_python27env_someapp_python34的环境将比如果您将它们命名为envenv1更容易区分。

在这个例子中,我保持了简单,因为我们只会使用一个虚拟环境来进行我们的项目,所以我使用了env_mysite。您的命令的输出应该看起来像这样:

C:\Users\nigel>virtualenv env_mysite
Using base prefix 
 'c:\\users\\nigel\\appdata\\local\\programs\\python\\python35-32'
New python executable in 
    C:\Users\nigel\env_mysite\Scripts\python.exe
Installing setuptools, pip, wheel...done.

一旦virtualenv完成设置新虚拟环境的工作,打开 Windows 资源管理器,看看virtualenv为您创建了什么。在您的主目录中,现在会看到一个名为\env_mysite的文件夹(或者您给虚拟环境的任何名称)。如果您打开文件夹,您会看到以下内容:

\Include 
\Lib 
\Scripts 
\src 

virtualenv为您创建了一个完整的 Python 安装,与您的其他软件分开,因此您可以在不影响系统上的任何其他软件的情况下工作。

要使用这个新的 Python 虚拟环境,我们必须激活它,所以让我们回到命令提示符并输入以下内容:

 env_mysite\scripts\activate

这将在您的虚拟环境的\scripts文件夹中运行激活脚本。您会注意到您的命令提示现在已经改变:

 (env_mysite) C:\Users\nigel>

命令提示符开头的(env_mysite)让您知道您正在虚拟环境中运行。我们的下一步是安装 Django。

安装 Django

既然我们已经安装了 Python 并运行了一个虚拟环境,安装 Django 就非常容易了,只需输入以下命令:

 pip install django==1.8.13

这将指示 pip 将 Django 安装到您的虚拟环境中。您的命令输出应该如下所示:

 (env_mysite) C:\Users\nigel>pip install django==1.8.13
 Collecting django==1.8.13
 Downloading Django-1.8.13-py2.py3-none-any.whl (6.2MB)
 100% |################################| 6.2MB 107kB/s
 Installing collected packages: django
 Successfully installed django-1.8.13

在这种情况下,我们明确告诉 pip 安装 Django 1.8.13,这是撰写本文时 Django 1.8 LTS 的最新版本。如果要安装 Django,最好查看 Django 项目网站以获取 Django 1.8 LTS 的最新版本。

注意

如果您想知道,输入pip install django将安装 Django 的最新稳定版本。如果您想获取有关安装 Django 最新开发版本的信息,请参阅第二十章, 更多 * 关于安装 * Django

为了获得一些安装后的积极反馈,请花点时间测试安装是否成功。在您的虚拟环境命令提示符下,输入python并按回车键启动 Python 交互解释器。如果安装成功,您应该能够导入模块django

 (env_mysite) C:\Users\nigel>python
 Python 3.5.1 (v3.5.1:37a07cee5969, Dec  6 2015, 01:38:48) 
 [MSC v.1900 32 bit (Intel)] on win32
 Type "help", "copyright", "credits" or "license" for more 
    information.
 >>> import django
 >>> django.get_version()
 1.8.13'

设置数据库

这一步并不是为了完成本书中的任何示例而必需的。Django 默认安装了 SQLite。SQLite 无需您进行任何配置。如果您想使用像 PostgreSQL、MySQL 或 Oracle 这样的大型数据库引擎,请参阅第二十一章, 高级数据库管理

开始一个项目

一旦安装了 Python、Django 和(可选)数据库server/library,您可以通过创建一个项目来开始开发 Django 应用程序。

项目是 Django 实例的一组设置。如果这是您第一次使用 Django,您需要进行一些初始设置。换句话说,您需要自动生成一些代码来建立一个 Django 项目:Django 实例的一组设置,包括数据库配置、Django 特定选项和应用程序特定设置。

我假设在这个阶段,您仍然在运行上一个安装步骤中的虚拟环境。如果没有,您将不得不重新开始:

 env_mysite\scripts\activate\

从您的虚拟环境命令行中,运行以下命令:

 django-admin startproject mysite

这将在当前目录(在本例中为\env_mysite\)中创建一个mysite目录。如果您想要在根目录之外的其他目录中创建项目,您可以创建一个新目录,切换到该目录并从那里运行startproject命令。

注意

警告!

您需要避免将项目命名为内置的 Python 或 Django 组件。特别是,这意味着您应该避免使用诸如"django"(这将与 Django 本身冲突)或"test"(这与内置的 Python 包冲突)等名称。

让我们看看startproject创建了什么:

mysite/ 
  manage.py 
  mysite/ 
    __init__.py 
    settings.py 
    urls.py 
    wsgi.py 

这些文件是:

  • 外部的mysite/根目录。这只是您项目的一个容器。对 Django 来说,它的名称并不重要;您可以将其重命名为任何您喜欢的名称。

  • manage.py,一个命令行实用程序,让您以各种方式与您的 Django 项目进行交互。您可以在 Django 项目网站上阅读有关manage.py的所有详细信息(有关更多信息,请访问docs.djangoproject.com/en/1.8/ref/django-admin/)。

  • 内部的mysite/目录。这是您项目的 Python 包。这是您用来导入其中任何内容的名称(例如,mysite.urls)。

  • mysite/__init__.py,一个空文件,告诉 Python 这个目录应该被视为 Python 包。 (如果你是 Python 初学者,请阅读官方 Python 文档中关于包的更多信息docs.python.org/tutorial/modules.html#packages。)

  • mysite/settings.py,这个 Django 项目的设置/配置。附录 D设置将告诉您有关设置如何工作的所有信息。

  • mysite/urls.py,这个 Django 项目的 URL 声明;你的 Django 网站的目录。您可以在第二章视图和 URLconfs和第七章高级视图和 URLconfs中了解更多关于 URL 的信息。

  • mysite/wsgi.py,WSGI 兼容的 Web 服务器为您的项目提供服务的入口点。有关更多详细信息,请参阅第十三章部署 Django

Django 设置

现在,编辑mysite/settings.py。这是一个普通的 Python 模块,其中包含表示 Django 设置的模块级变量。在编辑settings.py时的第一步是将TIME_ZONE设置为您的时区。请注意文件顶部的INSTALLED_APPS设置。它包含了在此 Django 实例中激活的所有 Django 应用程序的名称。应用程序可以在多个项目中使用,并且您可以将它们打包和分发给其他人在他们的项目中使用。默认情况下,INSTALLED_APPS包含以下应用程序,这些应用程序都是 Django 自带的:

  • django.contrib.admin:管理站点。

  • django.contrib.auth:身份验证系统。

  • django.contrib.contenttypes:内容类型框架。

  • django.contrib.sessions:会话框架。

  • django.contrib.messages:消息框架。

  • django.contrib.staticfiles:用于管理静态文件的框架。

这些应用程序默认包含,以方便常见情况。其中一些应用程序至少使用了一个数据库表,因此我们需要在使用它们之前在数据库中创建这些表。要做到这一点,请运行以下命令:

 python manage.py migrate 

migrate命令查看INSTALLED_APPS设置,并根据settings.py文件中的数据库设置和应用程序附带的数据库迁移创建任何必要的数据库表(我们稍后会涵盖这些)。它将为每个应用程序应用的每个迁移显示一条消息。

开发服务器

让我们验证一下你的 Django 项目是否正常工作。如果还没有,请切换到外部的mysite目录,并运行以下命令:

python manage.py runserver

您将在命令行上看到以下输出:

Performing system checks... 0 errors found
June 12, 2016-08:48:58
Django version 1.8.13, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

您已经启动了 Django 开发服务器,这是一个纯粹用 Python 编写的轻量级 Web 服务器。我们已经将其与 Django 一起提供,这样您就可以在准备投入生产之前快速开发,而无需处理配置生产服务器(如 Apache)的问题。

现在是一个很好的时机来注意:不要在任何类似生产环境的地方使用这个服务器。它只用于开发时使用。

现在服务器正在运行,请使用您的 Web 浏览器访问http://127.0.0.1:8000/。您将在愉快的浅蓝色(图 1.3)中看到一个“欢迎来到 Django”的页面。它成功了!

注意

runserver 的自动重新加载

开发服务器会根据需要自动重新加载每个请求的 Python 代码。您无需重新启动服务器即可使代码更改生效。但是,某些操作(例如添加文件)不会触发重新启动,因此在这些情况下,您将不得不重新启动服务器。

开发服务器

Django 的欢迎页面

模型-视图-控制器(MVC)设计模式

MVC 作为一个概念已经存在很长时间,但自从互联网出现以来,它已经呈指数级增长,因为它是设计客户端-服务器应用程序的最佳方式。所有最好的 Web 框架都是围绕 MVC 概念构建的。冒着引发战争的风险,我认为如果你不使用 MVC 来设计 Web 应用程序,那么你就错了。作为一个概念,MVC 设计模式真的很容易理解:

  • **模型(M)**是您的数据的模型或表示。它不是实际数据,而是数据的接口。模型允许您从数据库中提取数据,而无需了解底层数据库的复杂性。模型通常还提供了一个抽象层与您的数据库,以便您可以在多个数据库中使用相同的模型。

  • **视图(V)**是你所看到的。它是模型的表示层。在你的计算机上,视图是 Web 应用程序中浏览器中所看到的内容,或者是桌面应用程序的用户界面。视图还提供了一个接口来收集用户输入。

  • **控制器(C)**控制信息在模型和视图之间的流动。它使用编程逻辑来决定从模型中提取哪些信息,并将哪些信息传递给视图。它还通过视图从用户那里获取信息,并实现业务逻辑:通过更改视图,或通过模型修改数据,或两者兼而有之。

在每一层发生的事情的不同解释是困难的地方-不同的框架以不同的方式实现相同的功能。一个框架专家可能会说某个函数属于视图,而另一个可能会坚决地主张它应该在控制器上。

作为一个有远见的程序员,你不必关心这一点,因为最终这并不重要。只要你理解 Django 如何实现 MVC 模式,你就可以自由地继续并完成一些真正的工作。尽管在评论线程中观看战争可能是一种极具娱乐性的分心……

Django 紧密遵循 MVC 模式,但它在实现中使用了自己的逻辑。因为C由框架本身处理,而 Django 中的大部分工作发生在模型、模板和视图中,因此 Django 通常被称为MTV 框架。在 MTV 开发模式中:

  • **M 代表“模型”,**数据访问层。这一层包含关于数据的一切:如何访问它,如何验证它,它具有哪些行为,以及数据之间的关系。我们将在第四章中仔细研究 Django 的模型,模型

  • T 代表“模板”,表示层。这一层包含与表示相关的决策:在网页或其他类型的文档上如何显示某些内容。我们将在第三章中探讨 Django 的模板,“模板”。

  • V 代表“视图”,业务逻辑层。这一层包含访问模型并转到适当模板的逻辑。你可以把它看作是模型和模板之间的桥梁。我们将在下一章中查看 Django 的视图。

这可能是 Django 中唯一不太幸运的命名,因为 Django 的视图更像是 MVC 中的控制器,而 MVC 的视图实际上在 Django 中是一个模板。起初可能有点混淆,但作为一个完成工作的程序员,你真的不会长时间在意。这只是对于我们这些需要教授它的人来说是个问题。哦,当然还有那些喷子。

接下来呢?

现在你已经安装了所有东西并且开发服务器正在运行,你已经准备好继续学习 Django 的视图,并学习使用 Django 提供网页的基础知识。

第二章:视图和 URLconfs

在上一章中,我解释了如何设置 Django 项目并运行 Django 开发服务器。在本章中,你将学习使用 Django 创建动态网页的基础知识。

你的第一个 Django 网页:Hello World

作为我们的第一个目标,让我们创建一个网页,输出那个著名的例子消息:Hello World。如果你要发布一个简单的Hello World网页而没有使用 Web 框架,你只需在一个文本文件中输入 Hello world,将其命名为 hello.html,然后上传到 Web 服务器的某个目录中。请注意,在这个过程中,你已经指定了关于该网页的两个关键信息:它的内容(字符串 Hello world)和它的 URL(例如 http://www.example.com/hello.html)。使用 Django,你以不同的方式指定相同的两个内容。页面的内容由视图函数生成,URL 在URLconf中指定。首先,让我们编写我们的 Hello World 视图函数。

你的第一个视图

在我们在上一章中创建的mysite目录中,创建一个名为views.py的空文件。这个 Python 模块将包含本章的视图。我们的 Hello World 视图很简单。以下是整个函数以及导入语句,你应该将其输入到views.py文件中:

from django.http import HttpResponse 

def hello(request): 
    return HttpResponse("Hello world") 

让我们逐行分析这段代码:

  • 首先,我们导入了django.http模块中的HttpResponse类。我们需要导入这个类,因为它在我们的代码中稍后会用到。

  • 接下来,我们定义一个名为 hello 的函数-视图函数。

每个视图函数至少需要一个参数,按照惯例称为request。这是一个包含有关触发此视图的当前 Web 请求的信息的对象,是django.http.HttpRequest类的实例。

在这个例子中,我们并没有对 request 做任何操作,但它仍然必须是视图的第一个参数。请注意,视图函数的名称并不重要;它不必以某种方式命名,以便 Django 识别它。我们在这里称它为 hello,因为这个名称清楚地表示了视图的要点,但它也可以被命名为 hello_wonderful_beautiful_world,或者其他同样令人讨厌的名称。接下来的部分,“你的第一个 URLconf”,将解释 Django 如何找到这个函数。

这个函数是一个简单的一行代码:它只是返回一个用文本 Hello world 实例化的 HttpResponse 对象。

这里的主要教训是:视图只是一个以HttpRequest作为第一个参数并返回HttpResponse实例的 Python 函数。为了使 Python 函数成为 Django 视图,它必须做这两件事。(有例外情况,但我们稍后会讨论。)

你的第一个 URLconf

如果此时再次运行 python manage.py runserver,你仍然会看到欢迎使用 Django的消息,但没有我们的 Hello World 视图的任何痕迹。这是因为我们的mysite项目还不知道hello视图;我们需要明确告诉 Django 我们正在激活这个视图的特定 URL。继续我们之前关于发布静态 HTML 文件的类比,此时我们已经创建了 HTML 文件,但还没有将其上传到服务器上的目录中。

要将视图函数与 Django 中的特定 URL 挂钩,我们使用 URLconf。URLconf 就像是 Django 网站的目录。基本上,它是 URL 和应该为这些 URL 调用的视图函数之间的映射。这是告诉 Django 的方式,对于这个 URL,调用这段代码,对于那个 URL,调用那段代码

例如,当有人访问 URL /foo/ 时,调用视图函数 foo_view(),它位于 Python 模块 views.py 中。在上一章中执行 django-admin startproject 时,脚本会自动为您创建一个 URLconf:文件 urls.py

默认情况下,它看起来像这样:

"""mysite URL Configuration 
 The urlpatterns list routes URLs to views. For more information please 
 see:
     https://docs.djangoproject.com/en/1.8/topics/http/urls/ 
Examples:
Function views
     1\. Add an import:  from my_app import views
     2\. Add a URL to urlpatterns:  url(r'^$', views.home, name='home') Class-based views
     1\. Add an import:  from other_app.views import Home
     2\. Add a URL to urlpatterns:  url(r'^$', Home.as_view(), name='home')
Including another URLconf
     1\. Add an import:  from blog import urls as blog_urls
     2\. Add a URL to urlpatterns:  url(r'^blog/', include(blog_urls)) 
""" 
from django.conf.urls import include, url
from django.contrib import admin

 urlpatterns = [
     url(r'^admin/', include(admin.site.urls)),
 ] 

如果我们忽略文件顶部的文档注释,这就是一个 URLconf 的本质:

from django.conf.urls import include, url
from django.contrib import admin 

urlpatterns = [
     url(r'^admin/', include(admin.site.urls)),
 ] 

让我们逐行分析这段代码:

  • 第一行从django.conf.urls模块中导入了两个函数:include允许你包含另一个 URLconf 模块的完整 Python 导入路径,url使用正则表达式将浏览器中的 URL 模式匹配到 Django 项目中的模块。

  • 第二行调用了django.contrib模块中的admin函数。这个函数是由include函数调用的,用于加载 Django 管理站点的 URL。

  • 第三行是urlpatterns-一个简单的url()实例列表。

这里需要注意的主要是变量urlpatterns,Django 希望在你的 URLconf 模块中找到它。这个变量定义了 URL 和处理这些 URL 的代码之间的映射关系。要向 URLconf 添加 URL 和视图,只需在 URL 模式和视图函数之间添加映射。下面是如何连接我们的hello视图:

from django.conf.urls import include, url 
from django.contrib import admin 
from mysite.views import hello 

urlpatterns = [
     url(r'^admin/', include(admin.site.urls)),
     url(r'^hello/$', hello), 
] 

我们在这里做了两个更改:

  • 首先,我们从模块mysite/views.py中导入了hello视图,这在 Python 导入语法中转换为mysite.views。(这假设mysite/views.py在你的 Python 路径上。)

  • 接下来,我们在urlpatterns中添加了一行url(r'^hello/$', hello),。这行被称为 URLpattern。url()函数告诉 Django 如何处理你正在配置的 URL。第一个参数是一个模式匹配字符串(一个正则表达式;稍后会详细介绍),第二个参数是用于该模式的视图函数。url()还可以接受其他可选参数,我们将在第七章中更深入地介绍,高级视图和 URLconfs

我们在这里引入的另一个重要细节是正则表达式字符串前面的r字符。这告诉 Python 该字符串是一个原始字符串-它的内容不应解释反斜杠。

在普通的 Python 字符串中,反斜杠用于转义特殊字符,比如字符串\n,它是一个包含换行符的单字符字符串。当你添加r使其成为原始字符串时,Python 不会应用反斜杠转义,因此r'\n'是一个包含字面反斜杠和小写n的两个字符字符串。

Python 的反斜杠用法与正则表达式中的反斜杠之间存在自然冲突,因此最好在 Django 中定义正则表达式时始终使用原始字符串。

简而言之,我们只是告诉 Django,任何对 URL/hello/的请求都应该由hello视图函数处理。

值得讨论的是这个 URLpattern 的语法,因为它可能不是立即显而易见的。虽然我们想匹配 URL/hello/,但模式看起来与此有些不同。原因如下:

  • Django 在检查 URLpatterns 之前会从每个传入的 URL 中删除斜杠。这意味着我们的 URLpattern 不包括/hello/中的前导斜杠。起初,这可能看起来有点不直观,但这个要求简化了一些事情,比如在其他 URLconfs 中包含 URLconfs,我们将在第七章中介绍,高级视图和 URLconfs

  • 模式包括插入符(^)和美元符号($)。这些是具有特殊含义的正则表达式字符:插入符表示要求模式与字符串的开头匹配,美元符号表示要求模式与字符串的结尾匹配

这个概念最好通过例子来解释。如果我们使用的是模式^hello/(末尾没有美元符号),那么任何以/hello/开头的 URL 都会匹配,比如/hello/foo/hello/bar,而不仅仅是/hello/

同样,如果我们省略了初始的插入符号(即hello/$),Django 将匹配任何以hello/结尾的 URL,比如/foo/bar/hello/

如果我们只是使用了hello/,没有插入符号或美元符号,那么包含hello/的任何 URL 都会匹配,比如/foo/hello/bar

因此,我们同时使用插入符号和美元符号来确保只有 URL/hello/匹配-没有多余,也没有少了。大多数 URLpatterns 将以插入符号开头,并以美元符号结尾,但具有执行更复杂匹配的灵活性也是很好的。

也许你会想知道如果有人请求 URL/hello(即没有尾随斜杠),会发生什么。因为我们的 URLpattern 需要一个尾随斜杠,那个 URL 就不会匹配。然而,默认情况下,任何不匹配 URLpattern 并且不以斜杠结尾的 URL 请求都会被重定向到相同的 URL,但是以斜杠结尾(这由APPEND_SLASH Django 设置规定,详见附录 D,设置)。

关于这个 URLconf 的另一件事是,我们将hello视图函数作为对象传递而不调用函数。这是 Python(和其他动态语言)的一个关键特性:函数是一级对象,这意味着你可以像任何其他变量一样传递它们。很酷,对吧?

为了测试我们对 URLconf 的更改,启动 Django 开发服务器,就像你在第一章 Django 简介和入门中所做的那样,通过运行命令python manage.py runserver。(如果你让它保持运行状态,也没关系。开发服务器会自动检测 Python 代码的更改并在必要时重新加载,因此你不必在更改之间重新启动服务器。)服务器正在运行在地址http://127.0.0.1:8000/,所以打开一个网络浏览器,转到http://127.0.0.1:8000/hello/。你应该会看到文本Hello World-这是你的 Django 视图的输出(图 2.1)。

你的第一个 URLconf

图 2.1:耶!你的第一个 Django 视图

正则表达式

正则表达式(或 regexes)是一种在文本中指定模式的简洁方式。虽然 Django 的 URLconfs 允许任意的正则表达式进行强大的 URL 匹配,但实际上你可能只会使用一些正则表达式符号。表 2.1列出了一些常见符号。

表 2.1:常见的正则表达式符号

符号 匹配
.(点) 任意单个字符
\d 任意单个数字
[A-Z] AZ之间的任何字符(大写)
[a-z] az之间的任何字符(小写)
[A-Za-z] az之间的任何字符(不区分大小写)
+ 前一个表达式的一个或多个(例如,\d+匹配一个或多个数字)
[^/]+ 一个或多个字符,直到(但不包括)斜杠
? 前一个表达式的零个或一个(例如,\d?匹配零个或一个数字)
* 前一个表达式的零个或多个(例如,\d*匹配零个、一个或多个数字)
{1,3} 前一个表达式的一个到三个(包括)(例如,\d{1,3}匹配一个、两个或三个数字)

有关正则表达式的更多信息,请参阅 Python 正则表达式文档,访问docs.python.org/3.4/library/re.html

关于 404 错误的快速说明

此时,我们的 URLconf 只定义了一个 URLpattern:处理 URL/hello/的 URLpattern。当你请求不同的 URL 时会发生什么?要找出来,尝试运行 Django 开发服务器,并访问诸如http://127.0.0.1:8000/goodbye/之类的页面。

你应该会看到一个页面未找到的消息(图 2.2)。Django 显示这个消息是因为你请求了一个在你的 URLconf 中没有定义的 URL。

关于 404 错误的快速说明

图 2.2:Django 的 404 页面

这个页面的实用性不仅仅体现在基本的 404 错误消息上。它还会告诉您 Django 使用了哪个 URLconf 以及该 URLconf 中的每个模式。通过这些信息,您应该能够知道为什么请求的 URL 会引发 404 错误。

当您首次创建 Django 项目时,每个 Django 项目都处于调试模式。如果项目不处于调试模式,Django 会输出不同的 404 响应。这是敏感信息,仅供您作为 Web 开发人员使用。如果这是一个部署在互联网上的生产站点,您不希望将这些信息暴露给公众。因此,只有在您的 Django 项目处于调试模式时才会显示Page not found页面。

我将在后面解释如何关闭调试模式。现在只需知道每个 Django 项目在创建时都处于调试模式,如果项目不处于调试模式,Django 会输出不同的 404 响应。

关于站点根目录的一点说明

在上一节中已经解释过,如果您查看站点根目录http://127.0.0.1:8000/,您将看到一个 404 错误消息。Django 不会在站点根目录自动添加任何内容;该 URL 并没有特殊处理。

这取决于您将其分配给一个 URL 模式,就像 URLconf 中的每个其他条目一样。匹配站点根目录的 URL 模式有点不直观,因此值得一提。

当您准备为站点根目录实现一个视图时,使用 URL 模式^$,它匹配空字符串。例如:

from mysite.views import hello, my_homepage_view

 urlpatterns = [
     url(r'^$', my_homepage_view),
     # ... 

Django 如何处理请求

在继续我们的第二个视图函数之前,让我们暂停一下,了解一下 Django 是如何工作的。具体来说,当您在 Web 浏览器中访问http://127.0.0.1:8000/hello/以查看您的Hello World消息时,Django 在幕后做了什么?一切都始于settings 文件

当您运行python manage.py runserver时,脚本会在内部mysite目录中查找名为settings.py的文件。该文件包含了这个特定 Django 项目的各种配置,全部都是大写的:TEMPLATE_DIRSDATABASES等等。最重要的设置叫做ROOT_URLCONFROOT_URLCONF告诉 Django 应该使用哪个 Python 模块作为这个网站的 URLconf。

记得django-admin startproject创建了settings.pyurls.py文件吗?自动生成的settings.py包含一个指向自动生成的urls.pyROOT_URLCONF设置。打开settings.py文件,您会看到它应该是这样的。

ROOT_URLCONF = 'mysite.urls'

这对应于文件mysite/urls.py。当请求特定 URL(比如请求/hello/)时,Django 加载ROOT_URLCONF设置指向的 URLconf。然后,它按顺序检查该 URLconf 中的每个 URL 模式,逐个将请求的 URL 与模式进行比较,直到找到一个匹配的模式。

当找到匹配的 URL 模式时,它调用与该模式相关联的视图函数,并将HttpRequest对象作为第一个参数传递给它(我们将在后面介绍HttpRequest的具体内容)。正如我们在第一个视图示例中看到的,视图函数必须返回一个HttpResponse

一旦这样做,Django 就会完成剩下的工作,将 Python 对象转换为适当的 Web 响应,包括适当的 HTTP 标头和正文(即网页内容)。总之:

  • 一个请求进入/hello/

  • Django 通过查看ROOT_URLCONF设置来确定根 URLconf。

  • Django 查看 URLconf 中的所有 URL 模式,找到第一个与/hello/匹配的模式。

  • 如果找到匹配项,它会调用相关的视图函数。

  • 视图函数返回一个HttpResponse

  • Django 将HttpResponse转换为适当的 HTTP 响应,从而生成一个网页。

现在您已经了解了如何制作 Django 网页的基础知识。实际上很简单,只需编写视图函数并通过 URLconf 将其映射到 URL。

您的第二个视图:动态内容

我们的 Hello World 视图在演示 Django 工作基础方面很有启发性,但它并不是动态网页的一个例子,因为页面的内容始终相同。每次查看/hello/时,您都会看到相同的内容;它可能就像是一个静态 HTML 文件。

对于我们的第二个视图,让我们创建一个更加动态的东西-一个显示当前日期和时间的网页。这是一个不错的、简单的下一步,因为它不涉及数据库或任何用户输入-只涉及服务器内部时钟的输出。它只比 Hello World 有点更有趣,但它将演示一些新概念。这个视图需要做两件事:计算当前日期和时间,并返回包含该值的HttpResponse。如果您有 Python 经验,您就会知道 Python 包括一个用于计算日期的datetime模块。以下是如何使用它:

>>> import datetime 
>>> now = datetime.datetime.now() 
>>> now 
datetime.datetime(2015, 7, 15, 18, 12, 39, 2731) 
>>> print (now) 
2015-07-15 18:12:39.002731 

这很简单,与 Django 无关。这只是 Python 代码。(我们想强调的是,您应该知道哪些代码只是 Python 代码,哪些是特定于 Django 的代码。当您学习 Django 时,我们希望您能够将您的知识应用到其他不一定使用 Django 的 Python 项目中。)要创建一个显示当前日期和时间的 Django 视图,我们只需要将datetime.datetime.now()语句连接到一个视图并返回一个HttpResponse。更新后的views.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所做的更改,以适应current_datetime视图。

  • 我们在模块顶部添加了import datetime,这样我们就可以计算日期了。

  • 新的current_datetime函数计算当前日期和时间,作为datetime.datetime对象,并将其存储为本地变量now

  • 视图中的第二行代码使用 Python 的格式化字符串功能构造了一个 HTML 响应。字符串中的%s是一个占位符,字符串后面的百分号表示用变量now的值替换后面字符串中的%snow变量在技术上是一个datetime.datetime对象,而不是一个字符串,但%s格式字符将其转换为其字符串表示形式,类似于"2015-07-15 18:12:39.002731"。这将导致一个 HTML 字符串,如"<html><body>现在是 2015-07-15 18:12:39.002731。</body></html>"

  • 最后,视图返回一个包含生成的响应的HttpResponse对象-就像我们在hello中做的那样。

views.py中添加了这个之后,将 URL 模式添加到urls.py中,告诉 Django 哪个 URL 应该处理这个视图。类似/time/这样的东西会有意义:

from django.conf.urls import include, url 
from django.contrib import admin 
from mysite.views import hello, current_datetime

     urlpatterns = [
         url(r'^admin/', include(admin.site.urls)),
         url(r'^hello/$', hello),
         url(r'^time/$', current_datetime),
     ] 

我们在这里做了两个更改。首先,在顶部导入了current_datetime函数。其次,更重要的是,我们添加了一个 URL 模式,将 URL/time/映射到这个新视图。掌握了吗?视图编写和 URLconf 更新后,启动runserver并在浏览器中访问http://127.0.0.1:8000/time/。您应该看到当前的日期和时间。如果您没有看到本地时间,很可能是因为您的settings.py中的默认时区设置为UTC

URLconfs 和松散耦合

现在是时候强调 URLconfs 和 Django 背后的一个关键理念了:松散耦合的原则。简单地说,松散耦合是一种软件开发方法,它重视使各个部分可互换的重要性。如果两个代码片段松散耦合,那么对其中一个片段的更改对另一个片段几乎没有影响。

Django 的 URLconfs 是这个原则在实践中的一个很好的例子。在 Django Web 应用程序中,URL 定义和它们调用的视图函数是松散耦合的;也就是说,对于给定函数的 URL 应该是什么的决定,以及函数本身的实现,存在于两个不同的地方。

例如,考虑我们的current_datetime视图。如果我们想要更改应用程序的 URL——比如,将其从/time/移动到/current-time/——我们可以快速更改 URLconf,而不必担心视图本身。同样,如果我们想要更改视图函数——以某种方式改变其逻辑——我们可以这样做,而不会影响函数绑定的 URL。此外,如果我们想要在几个 URL 上公开当前日期功能,我们可以通过编辑 URLconf 轻松处理,而不必触及视图代码。

在这个例子中,我们的current_datetime可以通过两个 URL 访问。这是一个刻意制造的例子,但这种技术可能会派上用场:

urlpatterns = [
       url(r'^admin/', include(admin.site.urls)),
       url(r'^hello/$', hello),
       url(r'^time/$', current_datetime),
       url(r'^another-time-page/$', current_datetime),
 ] 

URLconfs 和视图是松散耦合的实践。我将在整本书中继续指出这一重要的哲学。

你的第三个视图:动态 URL

在我们的current_datetime视图中,页面的内容——当前日期/时间——是动态的,但 URL(/time/)是静态的。然而,在大多数动态网络应用中,URL 包含影响页面输出的参数。例如,一个在线书店可能为每本书提供自己的 URL,如/books/243//books/81196/。让我们创建一个第三个视图,显示当前日期和时间偏移了一定数量的小时。目标是以这样的方式设计网站,使得页面/time/plus/1/显示未来一小时的日期/时间,页面/time/plus/2/显示未来两小时的日期/时间,页面/time/plus/3/显示未来三小时的日期/时间,依此类推。一个新手可能会想要为每个小时偏移编写一个单独的视图函数,这可能会导致这样的 URLconf:

urlpatterns = [
     url(r'^time/$', current_datetime),
     url(r'^time/plus/1/$', one_hour_ahead),
     url(r'^time/plus/2/$', two_hours_ahead),
     url(r'^time/plus/3/$', three_hours_ahead),
] 

显然,这种想法是有缺陷的。这不仅会导致冗余的视图函数,而且应用程序基本上只能支持预定义的小时范围——一、两或三个小时。

如果我们决定创建一个显示未来四小时时间的页面,我们将不得不为此创建一个单独的视图和 URLconf 行,进一步增加了重复。

那么,我们如何设计我们的应用程序来处理任意小时偏移?关键是使用通配符 URLpatterns。正如我之前提到的,URLpattern 是一个正则表达式;因此,我们可以使用正则表达式模式\d+来匹配一个或多个数字:

urlpatterns = [
     # ...
     url(r'^time/plus/\d+/$', hours_ahead),
     # ... 
] 

(我使用# ...来暗示可能已经从这个例子中删除了其他 URLpatterns。)这个新的 URLpattern 将匹配任何 URL,比如/time/plus/2//time/plus/25/,甚至/time/plus/100000000000/。想想看,让我们限制一下,使得最大允许的偏移量是合理的。

在这个例子中,我们将通过只允许一位或两位数字来设置最大的 99 小时——在正则表达式语法中,这相当于\d{1,2}

url(r'^time/plus/\d{1,2}/$', hours_ahead), 

既然我们已经为 URL 指定了一个通配符,我们需要一种方法将通配符数据传递给视图函数,这样我们就可以对任意小时偏移使用一个视图函数。我们通过在 URLpattern 中希望保存的数据周围放置括号来实现这一点。在我们的例子中,我们希望保存在 URL 中输入的任何数字,所以让我们在\d{1,2}周围放上括号,就像这样:

url(r'^time/plus/(\d{1,2})/$', hours_ahead), 

如果你熟悉正则表达式,你会在这里感到很舒适;我们使用括号来从匹配的文本中捕获数据。最终的 URLconf,包括我们之前的两个视图,看起来像这样:

from django.conf.urls import include, url from django.contrib import admin from mysite.views import hello, current_datetime, hours_ahead 

urlpatterns = [
     url(r'^admin/', include(admin.site.urls)),
     url(r'^hello/$', hello),
     url(r'^time/$', current_datetime),
     url(r'^time/plus/(\d{1,2})/$', hours_ahead),
 ] 

注意

如果您在其他 Web 开发平台上有经验,您可能会想:“嘿,让我们使用查询字符串参数!”-类似于/time/plus?hours=3,其中小时将由 URL 查询字符串('?'后面的部分)中的hours参数指定。您可以在 Django 中这样做(我将在第七章中告诉您如何做),但 Django 的核心理念之一是 URL 应该是美观的。URL/time/plus/3/比其查询字符串对应项更清晰、更简单、更可读、更容易大声朗读,而且更漂亮。美观的 URL 是高质量 Web 应用的特征。

Django 的 URLconf 系统鼓励使用美观的 URL,因为使用美观的 URL 比不使用更容易。

处理完这些后,让我们编写hours_ahead视图。hours_ahead与我们之前编写的current_datetime视图非常相似,但有一个关键区别:它接受一个额外的参数,即偏移的小时数。以下是视图代码:

from django.http import Http404, HttpResponse 
import datetime 

def hours_ahead(request, offset):
     try:
         offset = int(offset)
     except ValueError:
         raise Http404()
     dt = datetime.datetime.now() + datetime.timedelta(hours=offset)
     html = "<html><body>In %s hour(s), it will be  %s.
             </body></html>" % (offset, dt)
     return HttpResponse(html) 

让我们仔细看看这段代码。

视图函数hours_ahead接受两个参数:requestoffset

  • request是一个HttpRequest对象,就像hellocurrent_datetime中一样。我再说一遍:每个视图总是以HttpRequest对象作为其第一个参数。

  • offset是 URLpattern 中括号捕获的字符串。例如,如果请求的 URL 是/time/plus/3/,那么offset将是字符串'3'。如果请求的 URL 是/time/plus/21/,那么offset将是字符串'21'。请注意,捕获的值将始终是 Unicode 对象,而不是整数,即使字符串只由数字组成,比如'21'。

我决定将变量称为offset,但只要它是有效的 Python 标识符,您可以将其命名为任何您喜欢的名称。变量名并不重要;重要的是它是request之后的函数的第二个参数。(在 URLconf 中也可以使用关键字参数,而不是位置参数。我将在第七章中介绍这一点。)

在函数内部,我们首先对offset调用int()。这将把 Unicode 字符串值转换为整数。

请注意,如果您对无法转换为整数的值(如字符串foo)调用int(),Python 将引发ValueError异常。在这个例子中,如果我们遇到ValueError,我们会引发异常django.http.Http404,这将导致404页面未找到错误。

敏锐的读者会想:无论如何,我们怎么会到达ValueError的情况呢?因为我们 URLpattern-(\d{1,2})中的正则表达式只捕获数字,因此offset将始终是由数字组成的字符串?答案是,我们不会,因为 URLpattern 提供了适度但有用的输入验证级别,但我们仍然检查ValueError,以防这个视图函数以其他方式被调用。

实现视图函数时最好不要对其参数做任何假设。记住松耦合。

在函数的下一行中,我们计算当前的日期/时间,并添加适当数量的小时。我们已经从current_datetime视图中看到了datetime.datetime.now();这里的新概念是,您可以通过创建datetime.timedelta对象并添加到datetime.datetime对象来执行日期/时间算术。我们的结果存储在变量dt中。

这行还显示了为什么我们对offset调用了int()-datetime.timedelta函数要求hours参数是一个整数。

接下来,我们构建这个视图函数的 HTML 输出,就像我们在current_datetime中所做的那样。这一行与上一行的一个小区别是,它使用了 Python 的格式化字符串功能,而不仅仅是一个。因此,在字符串中有两个%s符号和一个要插入的值的元组:(offset, dt)

最后,我们返回一个 HTML 的HttpResponse

有了这个视图函数和 URLconf 的编写,启动 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/来验证您的 URLconf 中的模式只接受一位或两位数字;在这种情况下,Django 应该显示Page not found错误,就像我们在关于 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() + datetime.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 hours component: str ( 图 2.3).

Django 的漂亮错误页面

图 2.3:Django 的错误页面

发生了什么?嗯,datetime.timedelta函数期望hours参数是一个整数,而我们注释掉了将offset转换为整数的代码。这导致datetime.timedelta引发了TypeError。这是每个程序员在某个时候都会遇到的典型小错误。这个例子的目的是演示 Django 的错误页面。花点时间来探索错误页面,并了解它提供的各种信息。以下是一些需要注意的事项:

  • 在页面顶部,您将获得有关异常的关键信息:异常的类型,异常的任何参数(在这种情况下是unsupported type消息),引发异常的文件以及有问题的行号。

  • 在关键异常信息下面,页面显示了此异常的完整 Python 回溯。这类似于您在 Python 命令行解释器中获得的标准回溯,只是更加交互式。对于堆栈中的每个级别(帧),Django 显示文件的名称,函数/方法名称,行号以及该行的源代码。

  • 单击源代码行(深灰色),您将看到错误行之前和之后的几行代码,以便为您提供上下文。单击堆栈中任何帧下的Local vars,以查看该帧中所有局部变量及其值的表,即在引发异常的代码的确切点上。这些调试信息可以提供很大的帮助。

  • 注意切换到复制和粘贴视图文本下的Traceback标题。单击这些单词,回溯将切换到一个可以轻松复制和粘贴的备用版本。当您想与其他人分享您的异常回溯以获得技术支持时,可以使用此功能-例如 Django IRC 聊天室中的友好人士或 Django 用户邮件列表中的人士。

  • 在下面,在公共网站上分享此回溯按钮将在单击一次后为您完成此工作。单击它以将回溯发布到 dpaste(有关更多信息,请访问www.dpaste.com/),在那里您将获得一个独特的 URL,可以与其他人分享。

  • 接下来,请求信息部分包括关于产生错误的传入 web 请求的丰富信息:GETPOST信息,cookie 值和元信息,比如 CGI 头。附录 F,请求和响应对象,包含了请求对象包含的所有信息的完整参考。

  • 请求信息部分之后,设置部分列出了此特定 Django 安装的所有设置。所有可用的设置都在附录 D,设置中有详细介绍。

在某些特殊情况下,Django 错误页面能够显示更多信息,比如模板语法错误的情况。我们稍后会讨论 Django 模板系统时再谈论这些。现在,取消注释offset = int(offset)行,以使视图函数再次正常工作。

如果您是那种喜欢通过精心放置print语句来调试的程序员,Django 错误页面也非常有用。

在视图的任何位置,临时插入assert False来触发错误页面。然后,您可以查看程序的本地变量和状态。以下是一个示例,使用hours_ahead视图:

def hours_ahead(request, offset):
     try:
         offset = int(offset)
     except ValueError:
         raise Http404()
     dt = datetime.datetime.now() + datetime.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 配置的内部,并且在公共互联网上显示这些信息是愚蠢的。恶意人士可以利用它来尝试反向工程您的 Web 应用程序并做坏事。因此,只有当您的 Django 项目处于调试模式时,才会显示 Django 错误页面。我将在第十三章 部署 Django中解释如何停用调试模式。现在,只需知道每个 Django 项目在启动时都会自动处于调试模式即可。(听起来很熟悉吗?本章前面描述的页面未找到错误也是同样的工作方式。)

接下来呢?

到目前为止,我们一直在 Python 代码中直接编写 HTML 硬编码的视图函数。我这样做是为了在演示核心概念时保持简单,但在现实世界中,这几乎总是一个坏主意。Django 附带了一个简单而强大的模板引擎,允许您将页面的设计与底层代码分离。我们将在下一章深入探讨 Django 的模板引擎。

第三章:模板

在上一章中,您可能已经注意到我们在示例视图中返回文本的方式有些奇怪。换句话说,HTML 直接硬编码在我们的 Python 代码中,就像这样:

def current_datetime(request): 
    now = datetime.datetime.now() 
    html = "It is now %s." % now 
    return HttpResponse(html) 

尽管这种技术对于解释视图如何工作的目的很方便,但直接在视图中硬编码 HTML 并不是一个好主意。原因如下:

  • 对页面设计的任何更改都需要对 Python 代码进行更改。网站的设计往往比底层 Python 代码更频繁地发生变化,因此如果设计可以在不需要修改 Python 代码的情况下进行更改,那将是很方便的。

  • 这只是一个非常简单的例子。一个常见的网页模板有数百行 HTML 和脚本。从这个混乱中解开和排除程序代码是一场噩梦(咳嗽-PHP-咳嗽)。

  • 编写 Python 代码和设计 HTML 是两种不同的学科,大多数专业的 Web 开发环境将这些责任分开给不同的人(甚至是不同的部门)。设计师和 HTML/CSS 编码人员不应该被要求编辑 Python 代码来完成他们的工作。

  • 如果程序员和设计师可以同时工作在 Python 代码和模板上,而不是一个人等待另一个人完成编辑包含 Python 和 HTML 的单个文件,那将是最有效的。

出于这些原因,将页面的设计与 Python 代码本身分开会更加清晰和易于维护。我们可以通过 Django 的模板系统来实现这一点,这是我们在本章中讨论的内容。

模板系统基础知识

Django 模板是一串文本,旨在将文档的呈现与其数据分离。模板定义了占位符和各种基本逻辑(模板标签),规定文档应该如何显示。通常,模板用于生成 HTML,但 Django 模板同样能够生成任何基于文本的格式。

注意

Django 模板背后的哲学

如果您有编程背景,或者习惯于将编程代码直接嵌入 HTML 的语言,您需要记住 Django 模板系统不仅仅是 Python 嵌入到 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 will 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 working.</p> 
{% endif %} 

<p>Sincerely,<br />{{ company }}</p> 

</body> 
</html> 

这个模板是基本的 HTML,其中包含一些变量和模板标签。让我们逐步进行:

  • 任何被一对大括号包围的文本(例如,{{ person_name }})都是变量。这意味着“插入具有给定名称的变量的值”。我们如何指定变量的值?我们马上就会讨论到。任何被大括号和百分号包围的文本(例如,{% if ordered_warranty %})都是模板标签。标签的定义非常广泛:标签只是告诉模板系统“做某事”。

  • 这个示例模板包含一个for标签({% for item in item_list %})和一个if标签({% if ordered_warranty %})。for标签的工作方式与 Python 中的for语句非常相似,让您可以循环遍历序列中的每个项目。

  • 一个if标签,正如您可能期望的那样,充当逻辑 if 语句。在这种特殊情况下,标签检查ordered_warranty变量的值是否评估为True。如果是,模板系统将显示{% if ordered_warranty %}{% else %}之间的所有内容。如果不是,模板系统将显示{% else %}{% endif %}之间的所有内容。请注意,{% else %}是可选的。

  • 最后,这个模板的第二段包含了一个filter的例子,这是改变变量格式的最方便的方法。在这个例子中,{{ ship_date|date:"F j, Y" }},我们将ship_date变量传递给date过滤器,并给date过滤器传递参数"F j, Y"date过滤器根据该参数指定的格式格式化日期。过滤器使用管道字符(|)进行连接,作为对 Unix 管道的引用。

每个 Django 模板都可以访问多个内置标签和过滤器,其中许多在接下来的章节中讨论。附录 E,内置模板标签和过滤器,包含了标签和过滤器的完整列表,熟悉该列表是一个好主意,这样您就知道可能发生什么。还可以创建自己的过滤器和标签;我们将在第八章,高级模板中进行介绍。

使用模板系统

Django 项目可以配置一个或多个模板引擎(甚至可以不使用模板)。Django 自带了一个用于其自己模板系统的内置后端-Django 模板语言DTL)。Django 1.8 还包括对流行的替代品 Jinja2 的支持(有关更多信息,请访问jinja.pocoo.org/)。如果没有紧迫的理由选择其他后端,应该使用 DTL-特别是如果您正在编写可插拔应用程序并且打算分发模板。Django 的contrib应用程序包括模板,如django.contrib.admin,使用 DTL。本章中的所有示例都将使用 DTL。有关更高级的模板主题,包括配置第三方模板引擎,请参阅第八章,高级模板。在您的视图中实现 Django 模板之前,让我们先深入了解 DTL,以便您了解其工作原理。以下是您可以在 Python 代码中使用 Django 模板系统的最基本方式:

  1. 通过提供原始模板代码作为字符串来创建Template对象。

  2. 使用给定的一组变量(上下文)调用Template对象的render()方法。这将根据上下文返回一个完全呈现的模板字符串,其中所有变量和模板标签都根据上下文进行评估。

在代码中,它看起来像这样:

>>> from django import template 
>>> t = template.Template('My name is {{ name }}.') 
>>> c = template.Context({'name': 'Nige'}) 
>>> print (t.render(c)) 
My name is Nige. 
>>> c = template.Context({'name': 'Barry'}) 
>>> print (t.render(c)) 
My name is Barry. 

以下各节将更详细地描述每个步骤。

创建模板对象

创建Template对象的最简单方法是直接实例化它。Template类位于django.template模块中,构造函数接受一个参数,即原始模板代码。让我们进入 Python 交互式解释器,看看这在代码中是如何工作的。从您在第一章中创建的mysite项目目录中,键入python manage.py shell以启动交互式解释器。

让我们来看一些模板系统的基础知识:

>>> from django.template import Template 
>>> t = Template('My name is {{ name }}.') 
>>> print (t) 

如果您正在交互式地跟随,您会看到类似于这样的内容:

<django.template.base.Template object at 0x030396B0> 

0x030396B0每次都会不同,这并不重要;这是一个 Python 的东西(如果你一定要知道的话,这是Template对象的 Python“标识”)。

当您创建一个Template对象时,模板系统会将原始模板代码编译成内部优化形式,准备好进行呈现。但是,如果您的模板代码包含任何语法错误,对Template()的调用将引发TemplateSyntaxError异常:

>>> from django.template import Template 
>>> t = Template('{% notatag %}') 
Traceback (most recent call last): 
  File "", line 1, in ? 
  ... 
django.template.base.TemplateSyntaxError: Invalid block tag: 'notatag' 

这里的“块标签”是指{% notatag %}。 “块标签”和“模板标签”是同义词。系统对以下任何情况都会引发TemplateSyntaxError异常:

  • 无效标签

  • 对有效标签的无效参数

  • 无效的过滤器

  • 对有效过滤器的无效参数

  • 无效的模板语法

  • 未关闭的标签(对于需要关闭标签的标签)

呈现模板

一旦你有了Template对象,你可以通过给它上下文来传递数据。上下文只是一组模板变量名及其关联的值。模板使用这个来填充它的变量并评估它的标记。在 Django 中,上下文由Context类表示,它位于django.template模块中。它的构造函数接受一个可选参数:将变量名映射到变量值的字典。

使用上下文调用Template对象的render()方法来填充模板:

>>> from django.template import Context, Template 
>>> t = Template('My name is {{ name }}.') 
>>> c = Context({'name': 'Stephane'}) 
>>> t.render(c) 
'My name is Stephane.' 

注意

一个特殊的 Python 提示

如果你以前使用过 Python,你可能会想知道为什么我们要运行 python manage.py shell 而不是只运行 python(或 python3)。这两个命令都会启动交互式解释器,但manage.py shell 命令有一个关键的区别:在启动解释器之前,它会告诉 Django 要使用哪个设置文件。Django 的许多部分,包括模板系统,都依赖于你的设置,除非框架知道要使用哪些设置,否则你将无法使用它们。

如果你感兴趣,这是它在幕后是如何工作的。Django 会查找一个名为 DJANGO_SETTINGS_MODULE 的环境变量,它应该设置为你的 settings.py 的导入路径。例如,DJANGO_SETTINGS_MODULE 可能设置为'mysite.settings',假设 mysite 在你的 Python 路径上。

当你运行 python manage.py shell 时,该命令会为你设置 DJANGO_SETTINGS_MODULE。在这些示例中,你需要使用 python manage.py shell,否则 Django 会抛出异常。

字典和上下文

Python 字典是已知键和变量值之间的映射。Context类似于字典,但Context提供了额外的功能,如第八章中所述的高级模板

变量名必须以字母(A-Z 或 a-z)开头,可以包含更多的字母、数字、下划线和点。(点是一个我们马上会讨论的特殊情况。)变量名是区分大小写的。以下是使用类似本章开头示例的模板进行编译和渲染的示例:

>>> from django.template import Template, Context 
>>> raw_template = """<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> 
... 
... {% if ordered_warranty %} 
... <p>Your warranty information will 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 working.</p> 
... {% endif %} 
... 
... <p>Sincerely,<br />{{ company }}</p>""" 
>>> t = Template(raw_template) 
>>> import datetime 
>>> c = Context({'person_name': 'John Smith', 
...     'company': 'Outdoor Equipment', 
...     'ship_date': datetime.date(2015, 7, 2), 
...     'ordered_warranty': False}) 
>>> t.render(c) 
u"<p>Dear John Smith,</p>\n\n<p>Thanks for placing an order from Outdoor 
Equipment. It's scheduled to\nship on July 2, 2015.</p>\n\n\n<p>You 
didn't order a warranty, so you're on your own when\nthe products 
inevitably stop working.</p>\n\n\n<p>Sincerely,<br />Outdoor Equipment 
</p>" 

  • 首先,我们导入TemplateContext类,它们都位于django.template模块中。

  • 我们将模板的原始文本保存到变量raw_template中。请注意,我们使用三引号来指定字符串,因为它跨越多行;相比之下,单引号内的字符串不能跨越多行。

  • 接下来,我们通过将raw_template传递给Template类的构造函数来创建一个模板对象t

  • 我们从 Python 的标准库中导入datetime模块,因为我们在下面的语句中会用到它。

  • 然后,我们创建一个Context对象cContext构造函数接受一个 Python 字典,它将变量名映射到值。在这里,例如,我们指定person_name是"John Smith",company是"Outdoor Equipment",等等。

  • 最后,我们在模板对象上调用render()方法,将上下文传递给它。这将返回渲染后的模板-也就是说,它用变量的实际值替换模板变量,并执行任何模板标记。

请注意,您没有订购保修段落被显示,因为ordered_warranty变量评估为False。还请注意日期2015 年 7 月 2 日,它根据格式字符串"F j, Y"显示。(我们稍后会解释date过滤器的格式字符串。)

如果你是 Python 的新手,你可能会想为什么这个输出包含换行符("\n")而不是显示换行。这是因为 Python 交互式解释器中的一个微妙之处:对 t.render(c) 的调用返回一个字符串,默认情况下交互式解释器显示字符串的表示形式,而不是字符串的打印值。如果你想看到带有换行符的字符串显示为真正的换行而不是 "\n" 字符,使用 print 函数:print (t.render(c))

这些是使用 Django 模板系统的基础知识:只需编写一个模板字符串,创建一个 Template 对象,创建一个 Context,然后调用 render() 方法。

多个上下文,同一个模板

一旦你有了一个 Template 对象,你可以通过它渲染多个上下文。例如:

>>> from django.template import Template, 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 

当你使用相同的模板源来渲染多个上下文时,最好只创建一次 Template 对象,然后多次调用 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 的模板渲染引擎慢几个数量级。

上下文变量查找

到目前为止的例子中,我们在上下文中传递了简单的值-大多是字符串,还有一个 datetime.date 的例子。然而,模板系统优雅地处理了更复杂的数据结构,如列表、字典和自定义对象。在 Django 模板中遍历复杂数据结构的关键是点字符(“.”)。

使用点来访问对象的字典键、属性、方法或索引。这最好通过一些例子来说明。例如,假设你要将一个 Python 字典传递给模板。要通过字典键访问该字典的值,使用一个点:

>>> from django.template import Template, Context 
>>> person = {'name': 'Sally', 'age': '43'} 
>>> t = Template('{{ person.name }} is {{ person.age }} years old.') 
>>> c = Context({'person': person}) 
>>> t.render(c) 
'Sally is 43 years old.' 

同样,点也允许访问对象的属性。例如,Python 的 datetime.date 对象具有 yearmonthday 属性,你可以使用点来在 Django 模板中访问这些属性:

>>> from django.template import Template, Context 
>>> import datetime 
>>> d = datetime.date(1993, 5, 2) 
>>> d.year 
1993 
>>> d.month 
5 
>>> d.day 
2 
>>> t = Template('The month is {{ date.month }} and the year is {{ date.year }}.') 
>>> c = Context({'date': d}) 
>>> t.render(c) 
'The month is 5 and the year is 1993.' 

这个例子使用了一个自定义类,演示了变量点也允许在任意对象上进行属性访问:

>>> from django.template import Template, Context 
>>> class Person(object): 
...     def __init__(self, first_name, last_name): 
...         self.first_name, self.last_name = first_name, last_name 
>>> t = Template('Hello, {{ person.first_name }} {{ person.last_name }}.') 
>>> c = Context({'person': Person('John', 'Smith')}) 
>>> t.render(c) 
'Hello, John Smith.' 

点也可以指代对象的方法。例如,每个 Python 字符串都有 upper()isdigit() 方法,你可以在 Django 模板中使用相同的点语法调用这些方法:

>>> from django.template import Template, Context 
>>> t = Template('{{ var }} -- {{ var.upper }} -- {{ var.isdigit }}') 
>>> t.render(Context({'var': 'hello'})) 
'hello -- HELLO -- False' 
>>> t.render(Context({'var': '123'})) 
'123 -- 123 -- True' 

请注意,在方法调用中不要包括括号。而且,不可能向方法传递参数;你只能调用没有必需参数的方法(我们稍后在本章中解释这个理念)。最后,点也用于访问列表索引,例如:

>>> from django.template import Template, Context 
>>> t = Template('Item 2 is {{ items.2 }}.') 
>>> c = Context({'items': ['apples', 'bananas', 'carrots']}) 
>>> t.render(c) 
'Item 2 is carrots.' 

不允许负列表索引。例如,模板变量

{{ items.-1 }} 会导致 TemplateSyntaxError

注意

Python 列表

提醒:Python 列表是从 0 开始的索引。第一个项目在索引 0 处,第二个在索引 1 处,依此类推。

点查找可以总结如下:当模板系统在变量名称中遇到一个点时,它按照以下顺序尝试以下查找:

  • 字典查找(例如,foo["bar"]

  • 属性查找(例如,foo.bar

  • 方法调用(例如,foo.bar()

  • 列表索引查找(例如,foo[2]

系统使用第一个有效的查找类型。这是短路逻辑。点查找可以嵌套多层深。例如,以下示例使用 {{ person.name.upper }},它转换为字典查找 (person['name']),然后是方法调用 (upper()):

>>> from django.template import Template, Context 
>>> person = {'name': 'Sally', 'age': '43'} 
>>> t = Template('{{ person.name.upper }} is {{ person.age }} years old.') 
>>> c = Context({'person': person}) 
>>> t.render(c) 
'SALLY is 43 years old.' 

方法调用行为

方法调用比其他查找类型稍微复杂一些。以下是一些需要记住的事项:

  • 如果在方法查找期间,方法引发异常,异常将被传播,除非异常具有一个值为 Truesilent_variable_failure 属性。如果异常确实具有 silent_variable_failure 属性,则变量将呈现为引擎的 string_if_invalid 配置选项的值(默认情况下为空字符串)。例如:
        >>> t = Template("My name is {{ person.first_name }}.") 
        >>> class PersonClass3: 
        ...     def first_name(self): 
        ...         raise AssertionError("foo") 
        >>> p = PersonClass3() 
        >>> t.render(Context({"person": p})) 
        Traceback (most recent call last): 
        ... 
        AssertionError: foo 

        >>> class SilentAssertionError(Exception): 
        ...     silent_variable_failure = True 
        >>> class PersonClass4: 
        ...     def first_name(self): 
        ...         raise SilentAssertionError 
        >>> p = PersonClass4() 
        >>> t.render(Context({"person": p})) 
        'My name is .' 

  • 只有当方法没有必需的参数时,方法调用才能正常工作。否则,系统将转到下一个查找类型(列表索引查找)。

  • 按设计,Django 有意限制了模板中可用的逻辑处理的数量,因此无法向从模板中访问的方法调用传递参数。数据应该在视图中计算,然后传递给模板进行显示。

  • 显然,一些方法具有副作用,允许模板系统访问它们将是愚蠢的,甚至可能是一个安全漏洞。

  • 比如,你有一个 BankAccount 对象,它有一个 delete() 方法。如果模板包含类似 {{ account.delete }} 的内容,其中 account 是一个 BankAccount 对象,那么当模板被渲染时,对象将被删除!为了防止这种情况发生,在方法上设置函数属性 alters_data

        def delete(self): 
        # Delete the account 
        delete.alters_data = True 

  • 模板系统不会执行以这种方式标记的任何方法。继续上面的例子,如果模板包含 {{ account.delete }},并且 delete() 方法具有 alters_data=True,那么在模板被渲染时,delete() 方法将不会被执行,引擎将用 string_if_invalid 替换变量。

  • 注意: Django 模型对象上动态生成的 delete()save() 方法会自动设置 alters_data=true

如何处理无效变量

通常,如果变量不存在,模板系统会插入引擎的 string_if_invalid 配置选项的值,默认情况下为空字符串。例如:

>>> from django.template import Template, Context 
>>> t = Template('Your name is {{ name }}.') 
>>> t.render(Context()) 
'Your name is .' 
>>> t.render(Context({'var': 'hello'})) 
'Your name is .' 
>>> t.render(Context({'NAME': 'hello'})) 
'Your name is .' 
>>> t.render(Context({'Name': 'hello'})) 
'Your name is .' 

这种行为比引发异常更好,因为它旨在对人为错误具有弹性。在这种情况下,所有的查找都失败了,因为变量名的大小写或名称错误。在现实世界中,由于小的模板语法错误导致网站无法访问是不可接受的。

基本模板标签和过滤器

正如我们已经提到的,模板系统附带了内置的标签和过滤器。接下来的部分将介绍最常见的标签和过滤器。

标签

if/else

{% if %} 标签评估一个变量,如果该变量为 True(即存在,不为空,并且不是 false 布尔值),系统将显示 {% 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 %} 

if 标签也可以接受一个或多个 {% elif %} 子句:

{% if athlete_list %} 
    Number of athletes: {{ athlete_list|length }} 
{% elif athlete_in_locker_room_list %} 
    <p>Athletes should be out of the locker room soon! </p> 
{% elif ... 
    ... 
{% else %} 
    <p>No athletes. </p> 
{% endif %} 

{% if %} 标签接受 and、or 或 not 用于测试多个变量,或者对给定变量取反。例如:

{% if athlete_list and coach_list %} 
    <p>Both athletes and coaches are available. </p> 
{% endif %} 

{% if not athlete_list %} 
    <p>There are no athletes. </p> 
{% endif %} 

{% if athlete_list or coach_list %} 
    <p>There are some athletes or some coaches. </p> 
{% endif %} 

{% if not athlete_list or coach_list %} 
    <p>There are no athletes or there are some coaches. </p> 
{% endif %} 

{% if athlete_list and not coach_list %} 
    <p>There are some athletes and absolutely no coaches. </p> 
{% endif %} 

在同一个标签中使用 andor 子句是允许的,其中 and 的优先级高于 or,例如:

{% if athlete_list and coach_list or cheerleader_list %} 

将被解释为:

if (athlete_list and coach_list) or cheerleader_list 

提示

注意:在 if 标签中使用实际括号是无效的语法。

如果需要使用括号表示优先级,应该使用嵌套的 if 标签。不支持使用括号来控制操作的顺序。如果发现自己需要括号,考虑在模板外执行逻辑,并将结果作为专用模板变量传递。或者,只需使用嵌套的 {% if %} 标签,就像这样:

 {% if athlete_list %} 
     {% if coach_list or cheerleader_list %} 
         <p>We have athletes, and either coaches or cheerleaders! </p> 
     {% endif %} 
 {% endif %} 

同一个逻辑运算符的多次使用是可以的,但不能组合不同的运算符。例如,这是有效的:

{% if athlete_list or coach_list or parent_list or teacher_list %} 

确保用 {% endif %} 来关闭每个 {% if %}。否则,Django 将抛出 TemplateSyntaxError

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 reversed %} 
... 
{% endfor %} 

可以嵌套{% for %}标签:

{% for athlete in athlete_list %} 
    <h1>{{ athlete.name }}</h1> 
    <ul> 
    {% for sport in athlete.sports_played %} 
        <li>{{ sport }}</li> 
    {% endfor %} 
    </ul> 
{% endfor %} 

如果需要循环遍历一个列表的列表,可以将每个子列表中的值解压缩为单独的变量。

例如,如果您的上下文包含一个名为points的(x,y)坐标列表,则可以使用以下内容输出点列表:

{% for x, y in points %} 
    <p>There is a point at {{ x }},{{ y }}</p> 
{% endfor %} 

如果需要访问字典中的项目,则这也可能很有用。 例如,如果您的上下文包含一个名为data的字典,则以下内容将显示字典的键和值:

{% for key, value in data.items %} 
    {{ key }}: {{ value }} 
{% endfor %} 

在循环之前检查列表的大小并在列表为空时输出一些特殊文本是一种常见模式:

{% if athlete_list %} 

  {% for athlete in athlete_list %} 
      <p>{{ athlete.name }}</p> 
  {% endfor %} 

{% else %} 
    <p>There are no athletes. Only computer programmers.</p> 
{% endif %} 

由于这种模式很常见,for标签支持一个可选的{% empty %}子句,让您定义列表为空时要输出的内容。 此示例等效于上一个示例:

{% for athlete in athlete_list %} 
    <p>{{ athlete.name }}</p> 
{% empty %} 
    <p>There are no athletes. Only computer programmers.</p> 
{% endfor %} 

没有支持在循环完成之前中断循环。 如果要实现此目的,请更改要循环遍历的变量,以便仅包括要循环遍历的值。

同样,不支持continue语句,该语句将指示循环处理器立即返回到循环的开头。 (有关此设计决定背后的原因,请参见本章后面的哲学和限制部分。)

在每个{% for %}循环中,您可以访问名为forloop的模板变量。 此变量具有一些属性,可为您提供有关循环进度的信息:

  • forloop.counter始终设置为表示循环已输入的次数的整数。 这是从 1 开始索引的,因此第一次循环时,forloop.counter将设置为1。 以下是一个示例:
        {% for item in todo_list %} 
            <p>{{ forloop.counter }}: {{ item }}</p> 
        {% endfor %} 

  • forloop.counter0类似于forloop.counter,只是它是从零开始索引的。 它的值将在第一次循环时设置为0

  • forloop.revcounter始终设置为表示循环中剩余项目数的整数。 第一次循环时,forloop.revcounter将设置为您正在遍历的序列中项目的总数。 最后一次循环时,forloop.revcounter将设置为1

  • forloop.revcounter0类似于forloop.revcounter,只是它是从零开始索引的。 第一次循环时,forloop.revcounter0将设置为序列中的元素数减去1。 最后一次循环时,它将设置为0

  • forloop.first是一个布尔值,如果这是第一次循环,则设置为True。 这对于特殊情况很方便:

        {% for object in objects %} 
            {% if forloop.first %}<li class="first">
{% else %}<li>{% endif %} 
            {{ object }} 
            </li> 
        {% endfor %} 

  • forloop.last是一个布尔值,如果这是最后一次循环,则设置为True。 这的一个常见用法是在链接列表之间放置管道字符:
        {% for link in links %} 
            {{ link }}{% if not forloop.last %} | {% endif %} 
        {% endfor %} 

  • 前面的模板代码可能会输出类似于以下内容:
        Link1 | Link2 | Link3 | Link4 

  • 这种模式的另一个常见用法是在列表中的单词之间放置逗号:
        Favorite places: 
        {% for p in places %}{{ p }}{% if not forloop.last %}, 
          {% endif %} 
        {% endfor %} 

  • forloop.parentloop是对父循环的forloop对象的引用,以防嵌套循环。 以下是一个示例:
        {% for country in countries %} 
            <table> 
            {% for city in country.city_list %} 
                <tr> 
                <td>Country #{{ forloop.parentloop.counter }}</td> 
                <td>City #{{ forloop.counter }}</td> 
                <td>{{ city }}</td> 
                </tr> 
            {% endfor %} 
            </table> 
        {% endfor %} 

forloop变量仅在循环内部可用。 模板解析器达到{% endfor %}后,forloop将消失。

注意

上下文和 forloop 变量

在{% for %}块内,现有的变量被移出以避免覆盖forloop变量。Django 在forloop.parentloop中公开了这个移动的上下文。通常情况下,您不需要担心这一点,但如果您提供了一个名为forloop的模板变量(尽管我们建议不要这样做),它将在{% for %}块内被命名为forloop.parentloop

ifequal/ifnotequal

Django 模板系统不是一个完整的编程语言,因此不允许执行任意的 Python 语句。(有关这个想法的更多信息,请参见哲学和限制部分)。

但是,比较两个值并在它们相等时显示某些内容是一个常见的模板要求,Django 提供了一个{% ifequal %}标签来实现这个目的。

{% ifequal %}标签比较两个值,并显示两者之间的所有内容

{% ifequal %}{% endifequal %}如果值相等。此示例比较模板变量usercurrentuser

{% 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 %}

ifequal标签的替代方法是使用if标签和"=="运算符。

{% ifnotequal %}标签与ifequal标签相同,只是它测试两个参数是否不相等。ifnotequal标签的替代方法是使用if标签和"!="运算符。

评论

就像在 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 }} 

这将显示经过lower过滤器过滤后的{{ name }}变量的值,该过滤器将文本转换为小写。过滤器可以链接-也就是说,它们可以串联使用,以便将一个过滤器的输出应用于下一个过滤器。

以下是一个示例,它获取列表中的第一个元素并将其转换为大写:

 {{ my_list|first|upper }} 

一些过滤器需要参数。过滤器参数在冒号后面,总是用双引号括起来。例如:

 {{ bio|truncatewords:"30" }} 

这将显示bio变量的前 30 个单词。

以下是一些最重要的过滤器。附录 E,内置模板标签和过滤器涵盖了其余部分。

  • addslashes:在任何反斜杠、单引号或双引号之前添加一个反斜杠。这对于转义字符串很有用。例如:
        {{ value|addslashes }} 

  • date:根据参数中给定的格式字符串格式化datedatetime对象,例如:
        {{ pub_date|date:"F j, Y" }} 

  • 格式字符串在附录 E 中定义,内置模板标签和过滤器

  • length:返回值的长度。对于列表,这将返回元素的数量。对于字符串,这将返回字符的数量。如果变量未定义,length返回0

哲学和限制

现在你对Django 模板语言(DTL)有了一定的了解,现在可能是时候解释 DTL 背后的基本设计理念了。首先,DTL 的限制是有意的。

Django 是在在线新闻编辑室这样一个高频率、不断变化的环境中开发的。Django 的原始创作者在创建 DTL 时有一套非常明确的哲学。

这些理念至今仍然是 Django 的核心。它们是:

  1. 将逻辑与呈现分开

  2. 防止冗余

  3. 与 HTML 解耦

  4. XML 很糟糕

  5. 假设设计师有能力

  6. 显而易见地处理空格

  7. 不要发明一种编程语言

  8. 确保安全性

  9. 可扩展

以下是对此的解释:

  1. 将逻辑与呈现分开

模板系统是控制呈现和与呈现相关逻辑的工具——仅此而已。模板系统不应该支持超出这一基本目标的功能。

  1. 防止冗余

大多数动态网站使用某种常见的站点范围设计——共同的页眉、页脚、导航栏等等。Django 模板系统应该能够轻松地将这些元素存储在一个地方,消除重复的代码。这就是模板继承背后的哲学。

  1. 与 HTML 解耦

模板系统不应该被设计成只输出 HTML。它应该同样擅长生成其他基于文本的格式,或者纯文本。

  1. 不应该使用 XML 作为模板语言

使用 XML 引擎解析模板会在编辑模板时引入一整套新的人为错误,并且在模板处理中产生不可接受的开销。

  1. 假设设计师有能力

模板系统不应该设计成模板必须在诸如 Dreamweaver 之类的所见即所得编辑器中显示得很好。这太严重了,不会允许语法像现在这样好。

Django 期望模板作者能够舒适地直接编辑 HTML。

  1. 显而易见地处理空格

模板系统不应该对空格做魔术。如果模板包含空格,系统应该像对待文本一样对待空格——只是显示它。任何不在模板标记中的空格都应该显示出来。

  1. 不要发明一种编程语言

模板系统有意不允许以下情况:

  • 变量赋值

  • 高级逻辑

目标不是发明一种编程语言。目标是提供足够的编程式功能,如分支和循环,这对于做出与呈现相关的决策至关重要。

Django 模板系统认识到模板通常是由设计师而不是程序员编写的,因此不应假设有 Python 知识。

  1. 安全性

模板系统应该默认禁止包含恶意代码,比如删除数据库记录的命令。这也是模板系统不允许任意 Python 代码的另一个原因。

  1. 可扩展性

模板系统应该认识到高级模板作者可能想要扩展其技术。这就是自定义模板标记和过滤器背后的哲学。

多年来我使用过许多不同的模板系统,我完全支持这种方法——DTL 及其设计方式是 Django 框架的主要优点之一。

当压力来临,需要完成任务时,你既有设计师又有程序员试图沟通并完成所有最后一分钟的任务时,Django 只是让每个团队专注于他们擅长的事情。

一旦你通过实际实践发现了这一点,你会很快发现为什么 Django 真的是完美主义者的截止日期框架

考虑到这一切,Django 是灵活的——它不要求你使用 DTL。与 Web 应用程序的任何其他组件相比,模板语法是高度主观的,程序员的观点差异很大。Python 本身有数十,甚至数百个开源模板语言实现,这一点得到了支持。每一个可能都是因为其开发者认为所有现有的模板语言都不够好而创建的。

因为 Django 旨在成为一个提供所有必要组件的全栈 Web 框架,以使 Web 开发人员能够高效工作,所以大多数情况下更方便使用 DTL,但这并不是严格的要求。

在视图中使用模板

你已经学会了使用模板系统的基础知识;现在让我们利用这些知识来创建一个视图。

回想一下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({'current_date': now})) 
    return HttpResponse(html) 

当然,这使用了模板系统,但它并没有解决我们在本章开头指出的问题。也就是说,模板仍然嵌入在 Python 代码中,因此没有真正实现数据和呈现的分离。让我们通过将模板放在一个单独的文件中来解决这个问题,这个视图将会加载。

你可能首先考虑将模板保存在文件系统的某个位置,并使用 Python 的内置文件打开功能来读取模板的内容。假设模板保存为文件/home/djangouser/templates/mytemplate.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/templates/mytemplate.html') 
    t = Template(fp.read()) 
    fp.close() 

    html = t.render(Context({'current_date': now})) 
    return HttpResponse(html) 

然而,这种方法是不够优雅的,原因如下:

  • 它没有处理文件丢失的情况。如果文件mytemplate.html不存在或不可读,open()调用将引发IOError异常。

  • 它会将模板位置硬编码。如果你要为每个视图函数使用这种技术,你将会重复模板位置。更不用说这需要大量的输入!

  • 它包含了大量乏味的样板代码。你有更好的事情要做,而不是每次加载模板时都写open()fp.read()fp.close()的调用。

为了解决这些问题,我们将使用模板加载和模板目录。

模板加载

Django 提供了一个方便而强大的 API,用于从文件系统加载模板,目的是消除模板加载调用和模板本身中的冗余。为了使用这个模板加载 API,首先你需要告诉框架你存储模板的位置。这个地方就是你的设置文件——我在上一章中提到的settings.py文件。如果你在跟着做,打开你的settings.py文件,找到TEMPLATES设置。这是一个配置列表,每个引擎一个:

TEMPLATES = [ 
    { 
        'BACKEND': 'django.template.backends.django.DjangoTemplates', 
        'DIRS': [], 
        'APP_DIRS': True, 
        'OPTIONS': { 
            # ... some options here ... 
        }, 
    }, 
] 

BACKEND是一个点分隔的 Python 路径,指向实现 Django 模板后端 API 的模板引擎类。内置的后端是django.template.backends.django.DjangoTemplatesdjango.template.backends.jinja2.Jinja2。由于大多数引擎从文件加载模板,每个引擎的顶级配置包含三个常见的设置:

  • DIRS定义了引擎应该在其中查找模板源文件的目录列表,按搜索顺序排列。

  • APP_DIRS告诉引擎是否应该在已安装的应用程序内查找模板。按照惯例,当APPS_DIRS设置为True时,DjangoTemplates会在每个INSTALLED_APPS的"templates"子目录中查找。这允许模板引擎即使DIRS为空也能找到应用程序模板。

  • OPTIONS包含特定于后端的设置。

虽然不常见,但是可以配置多个具有不同选项的相同后端实例。在这种情况下,你应该为每个引擎定义一个唯一的NAME

模板目录

默认情况下,DIRS是一个空列表。要告诉 Django 的模板加载机制在哪里查找模板,选择一个您想要存储模板的目录,并将其添加到DIRS中,如下所示:

'DIRS': [ 
           '/home/html/example.com', 
           '/home/html/default', 
       ], 

有几件事情需要注意:

  • 除非您正在构建一个没有应用程序的非常简单的程序,否则最好将DIRS留空。默认设置文件将APP_DIRS配置为True,因此最好在 Django 应用程序中有一个templates子目录。

  • 如果您想在项目根目录下拥有一组主模板,例如mysite/templates,您确实需要设置DIRS,如下所示:

  • 'DIRS': [os.path.join(BASE_DIR, 'templates')],

  • 顺便说一句,您的模板目录不一定要被称为'templates',Django 对您使用的名称没有任何限制,但是如果您遵循惯例,您的项目结构会更容易理解。

  • 如果您不想使用默认设置,或者由于某些原因无法使用默认设置,您可以指定任何您想要的目录,只要该目录和该目录中的模板可被您的 Web 服务器运行的用户帐户读取。

  • 如果您使用 Windows,请包括您的驱动器号,并使用 Unix 风格的正斜杠而不是反斜杠,如下所示:

        'DIRS': [
        'C:/www/django/templates',
        ]

由于我们还没有创建 Django 应用程序,因此您必须根据上面的示例将DIRS设置为[os.path.join(BASE_DIR, 'templates')],以使下面的代码按预期工作。设置了DIRS之后,下一步是更改视图代码,使用 Django 的模板加载功能而不是硬编码模板路径。回到我们的current_datetime视图,让我们像这样进行更改:

from django.template.loader import get_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_datetime.html') 
    html = t.render(Context({'current_date': now})) 
    return HttpResponse(html) 

在这个例子中,我们使用了函数django.template.loader.get_template()而不是手动从文件系统加载模板。get_template()函数以模板名称作为参数,找出模板在文件系统上的位置,打开该文件,并返回一个编译的Template对象。在这个例子中,我们的模板是current_datetime.html,但.html扩展名并没有什么特别之处。您可以为您的应用程序指定任何扩展名,或者完全不使用扩展名。为了确定模板在文件系统上的位置,get_template()将按顺序查找:

  • 如果APP_DIRS设置为True,并且假设您正在使用 DTL,它将在当前应用程序中查找templates目录。

  • 如果它在当前应用程序中找不到您的模板,get_template()将从DIRS中组合您传递给get_template()的模板名称,并按顺序逐个查找,直到找到您的模板。例如,如果您的DIRS中的第一个条目设置为'/home/django/mysite/templates',那么前面的get_template()调用将查找模板/home/django/mysite/templates/current_datetime.html

  • 如果get_template()找不到给定名称的模板,它会引发TemplateDoesNotExist异常。

要查看模板异常的样子,再次启动 Django 开发服务器,方法是在 Django 项目目录中运行python manage.py runserver。然后,将浏览器指向激活current_datetime视图的页面(例如http://127.0.0.1:8000/time/)。假设您的DEBUG设置为True,并且您还没有创建current_datetime.html模板,您应该会看到一个 Django 错误页面,突出显示TemplateDoesNotExist错误(图 3.1)。

模板目录

图 3.1:缺少模板错误页面。

这个错误页面与我在第二章中解释的类似,视图和 URLconfs,只是增加了一个额外的调试信息部分:模板加载器事后调查部分。该部分告诉您 Django 尝试加载的模板,以及每次尝试失败的原因(例如,文件不存在)。当您尝试调试模板加载错误时,这些信息是非常宝贵的。接下来,使用以下模板代码创建current_datetime.html文件:

It is now {{ current_date }}. 

将此文件保存到mysite/templates(如果尚未创建templates目录,则创建该目录)。刷新您的网络浏览器页面,您应该看到完全呈现的页面。

render()

到目前为止,我们已经向您展示了如何加载模板,填充Context并返回一个包含呈现模板结果的HttpResponse对象。下一步是优化它,使用get_template()代替硬编码模板和模板路径。我带您通过这个过程是为了确保您了解 Django 模板是如何加载和呈现到您的浏览器的。

实际上,Django 提供了一个更简单的方法来做到这一点。Django 的开发人员意识到,因为这是一个常见的习语,Django 需要一个快捷方式,可以在一行代码中完成所有这些。这个快捷方式是一个名为render()的函数,它位于模块django.shortcuts中。

大多数情况下,您将使用render()而不是手动加载模板和创建ContextHttpResponse对象-除非您的雇主根据编写的代码总行数来评判您的工作。

以下是使用render()重写的持续current_datetime示例:

from django.shortcuts import render 
import datetime 

def current_datetime(request): 
    now = datetime.datetime.now() 
    return render(request, 'current_datetime.html',  
                  {'current_date': now}) 

有何不同!让我们逐步了解代码更改:

  • 我们不再需要导入get_templateTemplateContextHttpResponse。相反,我们导入django.shortcuts.renderimport datetime保持不变。

  • current_datetime函数中,我们仍然计算now,但模板加载、上下文创建、模板渲染和HttpResponse创建都由render()调用处理。因为render()返回一个HttpResponse对象,所以我们可以在视图中简单地return该值。

render()的第一个参数是请求,第二个是要使用的模板的名称。如果给出第三个参数,应该是用于为该模板创建Context的字典。如果不提供第三个参数,render()将使用一个空字典。

模板子目录

将所有模板存储在单个目录中可能会变得难以管理。您可能希望将模板存储在模板目录的子目录中,这也是可以的。

事实上,我建议这样做;一些更高级的 Django 功能(例如通用视图系统,我们在第十章中介绍,通用视图)期望这种模板布局作为默认约定。

在模板目录的子目录中存储模板很容易。在对get_template()的调用中,只需包括子目录名称和模板名称之前的斜杠,就像这样:

t = get_template('dateapp/current_datetime.html') 

因为render()是围绕get_template()的一个小包装器,你可以用render()的第二个参数做同样的事情,就像这样:

return render(request, 'dateapp/current_datetime.html',  
              {'current_date': now}) 

您的子目录树的深度没有限制。随意使用尽可能多的子目录。

注意

Windows 用户,请确保使用正斜杠而不是反斜杠。get_template()假定 Unix 风格的文件名指定。

包含模板标签

现在我们已经介绍了模板加载机制,我们可以介绍一个利用它的内置模板标签:{% include %}。此标签允许您包含另一个模板的内容。标签的参数应该是要包含的模板的名称,模板名称可以是变量,也可以是硬编码(带引号)的字符串,可以是单引号或双引号。

每当您在多个模板中有相同的代码时,请考虑使用{% include %}来消除重复。这两个示例包括模板nav.html的内容。这两个示例是等效的,并且说明单引号和双引号都是允许的:

{% include 'nav.html' %} 
{% include "nav.html" %} 

此示例包括模板includes/nav.html的内容:

{% include 'includes/nav.html' %} 

此示例包括变量template_name中包含的模板的内容:

{% include template_name %} 

get_template()一样,模板的文件名是通过将当前 Django 应用程序中的templates目录的路径添加到模板名称(如果APPS_DIRTrue)或将DIRS中的模板目录添加到请求的模板名称来确定的。包含的模板将使用包含它们的模板的上下文进行评估。

例如,考虑这两个模板:

# 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,那么该变量将在included模板中可用,就像您期望的那样。

如果在{% include %}标记中找不到给定名称的模板,Django 将执行以下两种操作之一:

  • 如果DEBUG设置为True,您将在 Django 错误页面上看到TemplateDoesNotExist异常。

  • 如果DEBUG设置为False,标记将会静默失败,在标记的位置显示空白。

注意

包含的模板之间没有共享状态-每个包含都是完全独立的渲染过程。

块在被包含之前被评估。这意味着包含另一个模板的模板将包含已经被评估和渲染的块,而不是可以被另一个扩展模板覆盖的块。

模板继承

到目前为止,我们的模板示例都是小型的 HTML 片段,但在现实世界中,您将使用 Django 的模板系统来创建整个 HTML 页面。这导致了一个常见的 Web 开发问题:在整个网站中,如何减少常见页面区域的重复和冗余,比如整个站点的导航?

解决这个问题的经典方法是使用服务器端包含,您可以在 HTML 页面中嵌入的指令来包含一个网页在另一个网页中。事实上,Django 支持这种方法,刚刚描述的{% include %}模板标记。

但是,使用 Django 解决这个问题的首选方法是使用一种更优雅的策略,称为模板继承。实质上,模板继承允许您构建一个包含站点所有常见部分并定义子模板可以覆盖的“块”的基本“骨架”模板。让我们通过编辑current_datetime.html文件来看一个更完整的模板示例,为我们的current_datetime视图创建一个更完整的模板:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"> 
<html lang="en"> 
<head> 
    <title>The current time</title> 
</head> 
<body> 
    <h1>My helpful timestamp site</h1> 
    <p>It is now {{ current_date }}.</p> 

    <hr> 
    <p>Thanks for visiting my site.</p> 
</body> 
</html> 

看起来很好,但是当我们想要为另一个视图创建一个模板时会发生什么-比如,来自第二章的hours_ahead视图,视图和 URLconfs?如果我们再次想要创建一个漂亮的有效的完整 HTML 模板,我们会创建类似于以下内容:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"> 
<html lang="en"> 

<head> 
    <title>Future time</title> 
</head> 

<body> 
    <h1>My helpful timestamp site</h1> 
    <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。

解决这个问题的服务器端包含解决方案是将两个模板中的共同部分分解出来,并将它们保存在单独的模板片段中,然后在每个模板中包含它们。也许您会将模板的顶部部分存储在名为header.html的文件中:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"> 
<html lang="en"> 
<head> 

也许您会将底部部分存储在名为footer.html的文件中:

    <hr> 
    <p>Thanks for visiting my site.</p> 
</body> 
</html> 

使用基于包含的策略,标题和页脚很容易。中间部分很混乱。在这个示例中,两个页面都有一个标题-我的有用的时间戳站-但是这个标题无法放入header.html,因为两个页面上的标题是不同的。如果我们在头部包含 h1,我们就必须包含标题,这样就无法根据页面自定义它。

Django 的模板继承系统解决了这些问题。您可以将其视为服务器端包含的内部版本。您不是定义常见的片段,而是定义不同的片段。

第一步是定义一个基本模板-稍后子模板将填写的页面骨架。以下是我们正在进行的示例的基本模板:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"> 
<html lang="en"> 

<head> 
    <title>{% block title %}{% endblock %}</title> 
</head> 

<body> 
    <h1>My helpful timestamp site</h1> 
    {% block content %}{% endblock %} 
    {% block footer %} 
    <hr> 
    <p>Thanks for visiting my site.</p> 
    {% endblock %} 
</body> 
</html> 

这个模板,我们将其称为base.html,定义了一个简单的 HTML 骨架文档,我们将用于站点上的所有页面。

子模板的工作是覆盖、添加或保留块的内容。 (如果您在跟踪,请将此文件保存到模板目录中,命名为base.html。)

我们在这里使用了一个您以前没有见过的模板标记:{% 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视图创建一个模板。(如果您正在使用代码进行跟踪,我将让您自己决定将hours_ahead更改为使用模板系统而不是硬编码的 HTML。)以下是可能的样子:

{% extends "base.html" %} 

{% block title %}Future time{% endblock %} 

{% 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 title %}中定义的标题将被使用,{% block content %}也将被使用。

请注意,由于子模板未定义页脚块,模板系统将使用父模板中的值。在

父模板中的{% block %}标记始终用作备用。

继承不会影响模板上下文。换句话说,继承树中的任何模板都可以访问上下文中的每个模板变量。您可以使用所需的任意级别的继承。使用继承的一种常见方式是以下三级方法:

  1. 创建一个包含站点主要外观和感觉的base.html模板。这通常是很少或几乎不会更改的东西。

  2. 为站点的每个部分创建一个base_SECTION.html模板(例如,base_photos.htmlbase_forum.html)。这些模板扩展base.html并包括特定于部分的样式/设计。

  3. 为每种类型的页面创建单独的模板,例如论坛页面或照片库。这些模板扩展适当的部分模板。

这种方法最大程度地提高了代码的重用性,并使向共享区域添加项目变得容易,比如整个部分的导航。以下是一些使用模板继承的指导方针:

  • 如果您在模板中使用{% extends %},它必须是该模板中的第一个模板标记。否则,模板继承将无法工作。

  • 通常,基本模板中有更多的{% block %}标记,越好。请记住,子模板不必定义所有父块,因此您可以在许多块中填写合理的默认值,然后仅在子模板中定义您需要的块。拥有更多的钩子比拥有更少的钩子更好。

  • 如果您发现自己在许多模板中重复使用代码,这可能意味着您应该将该代码移动到父模板中的{% block %}中。

  • 如果您需要从父模板获取块的内容,请使用 {{ block.super }},这是一个提供父模板呈现文本的 "魔术" 变量。如果您想要添加到父块的内容而不是完全覆盖它,这将非常有用。

  • 您可能不会在同一个模板中定义多个具有相同名称的 {% block %} 标签。这种限制存在是因为块标签在 "两个" 方向上起作用。也就是说,块标签不仅提供要填充的空白,还定义了填充父级空白的内容。如果模板中有两个类似命名的 {% block %} 标签,那么该模板的父级将不知道使用哪个块的内容。

  • 您传递给 {% extends %} 的模板名称是使用 get_template() 使用的相同方法加载的。也就是说,模板名称将附加到您的 DIRS 设置,或者当前 Django 应用程序中的 "templates" 文件夹。

  • 在大多数情况下,{% extends %} 的参数将是一个字符串,但它也可以是一个变量,如果您直到运行时才知道父模板的名称。这让您可以做一些很酷的、动态的事情。

接下来是什么?

现在您已经掌握了 Django 模板系统的基础知识。接下来呢?大多数现代网站都是数据库驱动的:网站的内容存储在关系数据库中。这允许对数据和逻辑进行清晰的分离(就像视图和模板允许逻辑和显示的分离一样)。下一章介绍了 Django 提供的与数据库交互的工具。

第四章:模型

在第二章视图和 URLconfs中,我们介绍了使用 Django 构建动态网站的基础知识:设置视图和 URLconfs。正如我们所解释的,视图负责执行一些任意逻辑,然后返回一个响应。在其中一个示例中,我们的任意逻辑是计算当前日期和时间。

在现代 Web 应用程序中,任意逻辑通常涉及与数据库的交互。在幕后,一个数据库驱动的网站连接到数据库服务器,从中检索一些数据,并在网页上显示这些数据。该网站还可能提供访问者自行填充数据库的方式。

许多复杂的网站提供了这两种方式的组合。例如,www.amazon.com就是一个数据库驱动的网站的绝佳例子。每个产品页面本质上都是对亚马逊产品数据库的查询,格式化为 HTML,当您发布客户评论时,它会被插入到评论数据库中。

Django 非常适合制作数据库驱动的网站,因为它提供了使用 Python 执行数据库查询的简单而强大的工具。本章解释了这个功能:Django 的数据库层。

注意

虽然不是必须要了解基本的关系数据库理论和 SQL 才能使用 Django 的数据库层,但强烈建议这样做。这本书不涉及这些概念的介绍,但即使你是数据库新手,继续阅读也是有可能跟上并理解基于上下文的概念。

在视图中进行数据库查询的“愚蠢”方法

正如第二章视图和 URLconfs中详细介绍了在视图中生成输出的“愚蠢”方法(通过在视图中直接硬编码文本),在视图中从数据库中检索数据也有一个“愚蠢”的方法。很简单:只需使用任何现有的 Python 库来执行 SQL 查询并对结果进行处理。在这个示例视图中,我们使用MySQLdb库连接到 MySQL 数据库,检索一些记录,并将它们传递给模板以在网页上显示:

from django.shortcuts import render 
import MySQLdb 

def book_list(request): 
    db = MySQLdb.connect(user='me', db='mydb',  passwd='secret', host='localhost') 
    cursor = db.cursor() 
    cursor.execute('SELECT name FROM books ORDER BY name') 
    names = [row[0] for row in cursor.fetchall()] 
    db.close() 
    return render(request, 'book_list.html', {'names': names}) 

这种方法可以工作,但是一些问题应该立即引起您的注意:

  • 我们在硬编码数据库连接参数。理想情况下,这些参数应该存储在 Django 配置中。

  • 我们不得不写相当多的样板代码:创建连接,创建游标,执行语句,关闭连接。理想情况下,我们只需要指定我们想要的结果。

  • 它将我们与 MySQL 绑定。如果将来我们从 MySQL 切换到 PostgreSQL,我们很可能需要重写大量代码。理想情况下,我们使用的数据库服务器应该被抽象化,这样数据库服务器的更改可以在一个地方进行。 (如果您正在构建一个希望尽可能多的人使用的开源 Django 应用程序,这个功能尤其重要。)

正如您所期望的,Django 的数据库层解决了这些问题。

配置数据库

考虑到所有这些理念,让我们开始探索 Django 的数据库层。首先,让我们探索在创建应用程序时添加到settings.py的初始配置。

# Database 
#  
DATABASES = { 
    'default': { 
        'ENGINE': 'django.db.backends.sqlite3', 
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 
    } 
} 

默认设置非常简单。以下是每个设置的概述。

  • ENGINE:它告诉 Django 使用哪个数据库引擎。在本书的示例中,我们使用 SQLite,所以将其保留为默认的django.db.backends.sqlite3

  • NAME:它告诉 Django 你的数据库的名称。例如:'NAME': 'mydb',

由于我们使用的是 SQLite,startproject为我们创建了数据库文件的完整文件系统路径。

这就是默认设置-你不需要改变任何东西来运行本书中的代码,我包含这个只是为了让你了解在 Django 中配置数据库是多么简单。有关如何设置 Django 支持的各种数据库的详细描述,请参见第二十一章, 高级数据库管理

你的第一个应用程序

现在你已经验证了连接是否正常工作,是时候创建一个Django 应用程序了-一个包含模型和视图的 Django 代码包,它们一起存在于一个单独的 Python 包中,代表一个完整的 Django 应用程序。这里值得解释一下术语,因为这往往会让初学者困惑。我们已经在第一章中创建了一个项目,Django 简介和入门,那么项目应用程序之间有什么区别呢?区别在于配置和代码:

  • 项目是一组 Django 应用程序的实例,以及这些应用程序的配置。从技术上讲,项目的唯一要求是提供一个设置文件,其中定义了数据库连接信息、已安装应用程序的列表、DIRS等。

  • 应用程序是一组可移植的 Django 功能,通常包括模型和视图,它们一起存在于一个单独的 Python 包中。

例如,Django 自带了许多应用程序,比如自动管理界面。关于这些应用程序的一个关键点是它们是可移植的,可以在多个项目中重复使用。

关于如何将 Django 代码适应这个方案,几乎没有硬性规定。如果你正在构建一个简单的网站,可能只使用一个应用程序。如果你正在构建一个包括电子商务系统和留言板等多个不相关部分的复杂网站,你可能希望将它们拆分成单独的应用程序,以便将来可以单独重用它们。

事实上,你并不一定需要创建应用程序,正如我们在本书中迄今为止创建的示例视图函数所证明的那样。在这些情况下,我们只需创建一个名为views.py的文件,填充它以视图函数,并将我们的 URLconf 指向这些函数。不需要应用程序。

然而,关于应用程序约定有一个要求:如果你正在使用 Django 的数据库层(模型),你必须创建一个 Django 应用程序。模型必须存在于应用程序中。因此,为了开始编写我们的模型,我们需要创建一个新的应用程序。

mysite项目目录中(这是你的manage.py文件所在的目录,而不是mysite应用程序目录),输入以下命令来创建一个books应用程序:

python manage.py startapp books

这个命令不会产生任何输出,但它会在mysite目录中创建一个books目录。让我们看看该目录的内容:

books/ 
    /migrations 
    __init__.py 
    admin.py 
    models.py 
    tests.py 
    views.py 

这些文件将包含此应用程序的模型和视图。在你喜欢的文本编辑器中查看models.pyviews.py。这两个文件都是空的,除了注释和models.py中的导入。这是你的 Django 应用程序的空白板。

在 Python 中定义模型

正如我们在第一章中讨论的那样,MTV 中的 M 代表模型。Django 模型是对数据库中数据的描述,表示为 Python 代码。它是你的数据布局-相当于你的 SQL CREATE TABLE语句-只不过它是用 Python 而不是 SQL 编写的,并且包括的不仅仅是数据库列定义。

Django 使用模型在后台执行 SQL 代码,并返回表示数据库表中行的方便的 Python 数据结构。Django 还使用模型来表示 SQL 不能必然处理的更高级概念。

如果你熟悉数据库,你可能会立刻想到,“在 Python 中定义数据模型而不是在 SQL 中定义,这不是多余的吗?” Django 之所以采用这种方式有几个原因:

  • 内省需要额外开销,而且并不完美。为了提供方便的数据访问 API,Django 需要以某种方式了解数据库布局,有两种方法可以实现这一点。第一种方法是在 Python 中明确描述数据,第二种方法是在运行时内省数据库以确定数据模型。

  • 这第二种方法看起来更干净,因为关于你的表的元数据只存在一个地方,但它引入了一些问题。首先,在运行时内省数据库显然需要开销。如果框架每次处理请求时,甚至只在 Web 服务器初始化时都需要内省数据库,这将产生无法接受的开销。(虽然有些人认为这种开销是可以接受的,但 Django 的开发人员的目标是尽量减少框架的开销。)其次,一些数据库,特别是较旧版本的 MySQL,没有存储足够的元数据来进行准确和完整的内省。

  • 编写 Python 很有趣,而且将所有东西都放在 Python 中可以减少你的大脑进行“上下文切换”的次数。如果你尽可能长时间地保持在一个编程环境/思维方式中,这有助于提高生产率。不得不先写 SQL,然后写 Python,再写 SQL 是会打断思维的。

  • 将数据模型存储为代码而不是在数据库中,可以更容易地将模型纳入版本控制。这样,你可以轻松跟踪对数据布局的更改。

  • SQL 只允许对数据布局进行一定级别的元数据。例如,大多数数据库系统并没有提供专门的数据类型来表示电子邮件地址或 URL。但 Django 模型有。更高级别的数据类型的优势在于更高的生产率和更可重用的代码。

  • SQL 在不同的数据库平台上是不一致的。例如,如果你要分发一个网络应用程序,更实际的做法是分发一个描述数据布局的 Python 模块,而不是针对 MySQL、PostgreSQL 和 SQLite 分别创建CREATE TABLE语句的集合。

然而,这种方法的一个缺点是,Python 代码可能与实际数据库中的内容不同步。如果你对 Django 模型进行更改,你需要在数据库内做相同的更改,以保持数据库与模型一致。在本章后面讨论迁移时,我将向你展示如何处理这个问题。

最后,你应该注意到 Django 包括一个实用程序,可以通过内省现有数据库来生成模型。这对于快速启动和运行遗留数据非常有用。我们将在第二十一章中介绍这个内容,高级数据库管理

你的第一个模型

作为本章和下一章的一个持续的例子,我将专注于一个基本的书籍/作者/出版商数据布局。我选择这个作为例子,因为书籍、作者和出版商之间的概念关系是众所周知的,这是初级 SQL 教科书中常用的数据布局。你也正在阅读一本由作者撰写并由出版商出版的书籍!

我假设以下概念、字段和关系:

  • 作者有名字、姓氏和电子邮件地址。

  • 出版商有一个名称、街道地址、城市、州/省、国家和网站。

  • 一本书有一个标题和出版日期。它还有一个或多个作者(与作者之间是多对多的关系)和一个出版商(一对多的关系,也就是外键到出版商)。

在 Django 中使用这个数据库布局的第一步是将其表达为 Python 代码。在由startapp命令创建的models.py文件中输入以下内容:

from django.db import models 

class Publisher(models.Model): 
    name = models.CharField(max_length=30) 
    address = models.CharField(max_length=50) 
    city = models.CharField(max_length=60) 
    state_province = models.CharField(max_length=30) 
    country = models.CharField(max_length=50) 
    website = models.URLField() 

class Author(models.Model): 
    first_name = models.CharField(max_length=30) 
    last_name = models.CharField(max_length=40) 
    email = models.EmailField() 

class Book(models.Model): 
    title = models.CharField(max_length=100) 
    authors = models.ManyToManyField(Author) 
    publisher = models.ForeignKey(Publisher) 
    publication_date = models.DateField() 

让我们快速检查这段代码,以涵盖基础知识。首先要注意的是,每个模型都由一个 Python 类表示,该类是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有一个名为authorsManyToManyField。这表示一本书有一个或多个作者,但Book数据库表不会得到一个authors列。相反,Django 会创建一个额外的表-一个多对多的连接表-来处理书籍到作者的映射。

对于字段类型和模型语法选项的完整列表,请参见附录 B, 数据库 API 参考。最后,请注意,我们没有在任何这些模型中明确定义主键。除非您另有指示,否则 Django 会自动为每个模型提供一个自增的整数主键字段,称为id。每个 Django 模型都需要有一个单列主键。

安装模型

我们已经编写了代码;现在让我们在数据库中创建表。为了做到这一点,第一步是在我们的 Django 项目中激活这些模型。我们通过将books应用程序添加到设置文件中的已安装应用程序列表中来实现这一点。再次编辑settings.py文件,并查找INSTALLED_APPS设置。INSTALLED_APPS告诉 Django 为给定项目激活了哪些应用程序。默认情况下,它看起来像这样:

INSTALLED_APPS = ( 
'django.contrib.admin', 
'django.contrib.auth', 
'django.contrib.contenttypes', 
'django.contrib.sessions', 
'django.contrib.messages', 
'django.contrib.staticfiles', 
) 

要注册我们的books应用程序,请将'books'添加到INSTALLED_APPS中,以便设置最终看起来像这样('books'指的是我们正在使用的books应用程序):

INSTALLED_APPS = ( 
'django.contrib.admin', 
'django.contrib.auth', 
'django.contrib.contenttypes', 
'django.contrib.sessions', 
'django.contrib.messages', 
'django.contrib.staticfiles', 
'books', 
) 

INSTALLED_APPS中的每个应用程序都由其完整的 Python 路径表示-即,由点分隔的导致应用程序包的路径。现在 Django 应用程序已在设置文件中激活,我们可以在数据库中创建数据库表。首先,让我们通过运行此命令来验证模型:

python manage.py check

check命令运行 Django 系统检查框架-一组用于验证 Django 项目的静态检查。如果一切正常,您将看到消息System check identified no issues (0 silenced)。如果没有,请确保您正确输入了模型代码。错误输出应该为您提供有关代码错误的有用信息。每当您认为模型存在问题时,请运行python manage.py check。它往往会捕捉到所有常见的模型问题。

如果您的模型有效,请运行以下命令告诉 Django 您对模型进行了一些更改(在本例中,您创建了一个新模型):

python manage.py makemigrations books 

您应该看到类似以下内容的东西:

Migrations for 'books': 
  0001_initial.py: 
   -Create model Author 
   -Create model Book 
   -Create model Publisher 
   -Add field publisher to book 

迁移是 Django 存储对模型的更改(因此是数据库模式)的方式-它们只是磁盘上的文件。在这种情况下,您将在books应用程序的migrations文件夹中找到名为0001_initial.py的文件。migrate命令将获取您的最新迁移文件并自动更新您的数据库模式,但首先让我们看看该迁移将运行的 SQL。sqlmigrate命令获取迁移名称并返回它们的 SQL:

python manage.py sqlmigrate books 0001

你应该看到类似以下的内容(为了可读性重新格式化):

BEGIN; 

CREATE TABLE "books_author" ( 
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 
    "first_name" varchar(30) NOT NULL, 
    "last_name" varchar(40) NOT NULL, 
    "email" varchar(254) NOT NULL 
); 
CREATE TABLE "books_book" ( 
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 
    "title" varchar(100) NOT NULL, 
    "publication_date" date NOT NULL 
); 
CREATE TABLE "books_book_authors" ( 
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 
    "book_id" integer NOT NULL REFERENCES "books_book" ("id"), 
    "author_id" integer NOT NULL REFERENCES "books_author" ("id"), 
    UNIQUE ("book_id", "author_id") 
); 
CREATE TABLE "books_publisher" ( 
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 
    "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_book__new" ( 
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 
    "title" varchar(100) NOT NULL, 
    "publication_date" date NOT NULL, 
    "publisher_id" integer NOT NULL REFERENCES 
    "books_publisher" ("id") 
); 

INSERT INTO "books_book__new" ("id", "publisher_id", "title", 
"publication_date") SELECT "id", NULL, "title", "publication_date" FROM 
"books_book"; 

DROP TABLE "books_book"; 

ALTER TABLE "books_book__new" RENAME TO "books_book"; 

CREATE INDEX "books_book_2604cbea" ON "books_book" ("publisher_id"); 

COMMIT; 

请注意以下内容:

  • 表名是通过组合应用程序的名称(books)和模型的小写名称(publisherbookauthor)自动生成的。你可以覆盖这种行为,详细信息请参见附录 B,数据库 API 参考

  • 正如我们之前提到的,Django 会自动为每个表添加一个主键-id字段。你也可以覆盖这一点。按照惯例,Django 会将"_id"附加到外键字段名称。你可能已经猜到,你也可以覆盖这种行为。

  • 外键关系通过REFERENCES语句明确表示。

这些CREATE TABLE语句是针对你正在使用的数据库定制的,因此数据库特定的字段类型,如auto_increment(MySQL),serial(PostgreSQL)或integer primary key(SQLite)都会自动处理。列名的引用也是一样的(例如,使用双引号或单引号)。这个示例输出是以 PostgreSQL 语法为例。

sqlmigrate命令实际上并不会创建表或者对数据库进行任何操作,它只是在屏幕上打印输出,这样你就可以看到如果要求 Django 执行的 SQL 是什么。如果你愿意,你可以将这些 SQL 复制粘贴到你的数据库客户端中,然而,Django 提供了一个更简单的方法将 SQL 提交到数据库:migrate命令:

python manage.py migrate

运行该命令,你会看到类似以下的内容:

Operations to perform:
 Apply all migrations: books
Running migrations:
 Rendering model states... DONE
 # ...
 Applying books.0001_initial... OK
 # ...

如果你想知道所有这些额外的内容是什么(在上面被注释掉的),第一次运行 migrate 时,Django 还会创建 Django 内置应用所需的所有系统表。迁移是 Django 传播你对模型所做更改(添加字段、删除模型等)到数据库模式的方式。它们被设计为大部分是自动的,但是也有一些注意事项。有关迁移的更多信息,请参见第二十一章,高级数据库管理

基本数据访问

一旦你创建了一个模型,Django 会自动为这些模型提供一个高级别的 Python API。通过运行python manage.py shell并输入以下内容来尝试一下:

>>> from books.models import Publisher 
>>> p1 = Publisher(name='Apress', address='2855 Telegraph Avenue', 
...     city='Berkeley', state_province='CA', country='U.S.A.', 
...     website='http://www.apress.com/') 
>>> p1.save() 
>>> p2 = Publisher(name="O'Reilly", address='10 Fawcett St.', 
...     city='Cambridge', state_province='MA', country='U.S.A.', 
...     website='http://www.oreilly.com/') 
>>> p2.save() 
>>> publisher_list = Publisher.objects.all() 
>>> publisher_list 
[<Publisher: Publisher object>, <Publisher: Publisher object>] 

这几行代码完成了很多事情。以下是重点:

  • 首先,我们导入我们的Publisher模型类。这让我们可以与包含出版商的数据库表进行交互。

  • 我们通过为每个字段实例化一个Publisher对象来创建一个Publisher对象-nameaddress等等。

  • 要将对象保存到数据库中,请调用其save()方法。在幕后,Django 在这里执行了一个 SQL INSERT语句。

  • 要从数据库中检索出出版商,使用属性Publisher.objects,你可以将其视为所有出版商的集合。使用语句Publisher.objects.all()获取数据库中所有Publisher对象的列表。在幕后,Django 在这里执行了一个 SQL SELECT语句。

有一件事值得一提,以防这个例子没有清楚地表明。当你使用 Django 模型 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(name='Apress', 
...     address='2855 Telegraph Avenue', 
...     city='Berkeley', state_province='CA', country='U.S.A.', 
...     website='http://www.apress.com/') 
>>> p2 = Publisher.objects.create(name="O'Reilly", 
...     address='10 Fawcett St.', city='Cambridge', 
...     state_province='MA', country='U.S.A.', 
...     website='http://www.oreilly.com/') 
>>> publisher_list = Publisher.objects.all() 
>>> publisher_list 
[<Publisher: Publisher object>, <Publisher: Publisher object>] 

当然,你可以使用 Django 数据库 API 做很多事情,但首先,让我们解决一个小烦恼。

添加模型字符串表示

当我们打印出出版商列表时,我们得到的只是这种不太有用的显示,这使得很难区分Publisher对象:

[<Publisher: Publisher object>, <Publisher: Publisher object>] 

我们可以通过在Publisher类中添加一个名为__str__()的方法来轻松解决这个问题。__str__()方法告诉 Python 如何显示对象的可读表示。通过为这三个模型添加__str__()方法,你可以看到它的作用。

from django.db import models 

class Publisher(models.Model): 
    name = models.CharField(max_length=30) 
    address = models.CharField(max_length=50) 
    city = models.CharField(max_length=60) 
    state_province = models.CharField(max_length=30) 
    country = models.CharField(max_length=50) 
    website = models.URLField() 

 def __str__(self): 
 return self.name 

class Author(models.Model): 
    first_name = models.CharField(max_length=30) 
    last_name = models.CharField(max_length=40) 
    email = models.EmailField() 

 def __str__(self):
 return u'%s %s' % 
                                (self.first_name, self.last_name) 

class Book(models.Model): 
    title = models.CharField(max_length=100) 
    authors = models.ManyToManyField(Author) 
    publisher = models.ForeignKey(Publisher) 
    publication_date = models.DateField() 

 def __str__(self):
 return self.title

如您所见,__str__()方法可以根据需要执行任何操作,以返回对象的表示。在这里,PublisherBook__str__()方法分别返回对象的名称和标题,但Author__str__()方法稍微复杂一些-它将first_namelast_name字段拼接在一起,用空格分隔。__str__()的唯一要求是返回一个字符串对象。如果__str__()没有返回一个字符串对象-如果它返回了一个整数-那么 Python 将引发一个类似于以下的TypeError消息:

TypeError: __str__ returned non-string (type int). 

要使__str__()的更改生效,请退出 Python shell,然后使用python manage.py shell再次进入。 (这是使代码更改生效的最简单方法。)现在Publisher对象的列表更容易理解了:

>>> from books.models import Publisher 
>>> publisher_list = Publisher.objects.all() 
>>> publisher_list 
[<Publisher: Apress>, <Publisher: O'Reilly>] 

确保您定义的任何模型都有一个__str__()方法-不仅是为了在使用交互式解释器时方便您自己,而且还因为 Django 在需要显示对象时使用__str__()的输出。最后,请注意,__str__()是向模型添加行为的一个很好的例子。Django 模型描述了对象的数据库表布局,还描述了对象知道如何执行的任何功能。__str__()就是这种功能的一个例子-模型知道如何显示自己。

插入和更新数据

您已经看到了这个操作:要向数据库插入一行数据,首先使用关键字参数创建模型的实例,如下所示:

>>> p = Publisher(name='Apress', 
...         address='2855 Telegraph Ave.', 
...         city='Berkeley', 
...         state_province='CA', 
...         country='U.S.A.', 
...         website='http://www.apress.com/') 

正如我们上面所指出的,实例化模型类的行为并不会触及数据库。直到您调用save(),记录才会保存到数据库中,就像这样:

>>> p.save() 

在 SQL 中,这大致可以翻译为以下内容:

INSERT INTO books_publisher 
    (name, address, city, state_province, country, website) 
VALUES 
    ('Apress', '2855 Telegraph Ave.', 'Berkeley', 'CA', 
     'U.S.A.', 'http://www.apress.com/'); 

因为Publisher模型使用自增主键id,对save()的初始调用还做了一件事:它计算了记录的主键值,并将其设置为实例的id属性:

>>> p.id 
52    # this will differ based on your own data 

save()的后续调用将在原地保存记录,而不是创建新记录(即执行 SQL 的UPDATE语句而不是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; 

选择对象

了解如何创建和更新数据库记录是至关重要的,但很有可能您构建的 Web 应用程序将更多地查询现有对象,而不是创建新对象。我们已经看到了检索给定模型的每条记录的方法:

>>> Publisher.objects.all() 
[<Publisher: Apress>, <Publisher: O'Reilly>] 

这大致对应于以下 SQL:

SELECT id, name, address, city, state_province, country, website 
FROM books_publisher; 

注意

请注意,Django 在查找数据时不使用SELECT *,而是明确列出所有字段。这是有意设计的:在某些情况下,SELECT *可能会更慢,并且(更重要的是)列出字段更贴近 Python 之禅的一个原则:明确胜于隐晦。有关 Python 之禅的更多信息,请尝试在 Python 提示符下输入import this

让我们仔细看看Publisher.objects.all()这行的每个部分:

  • 首先,我们有我们定义的模型Publisher。这里没有什么意外:当您想要查找数据时,您使用该数据的模型。

  • 接下来,我们有objects属性。这被称为管理器。管理器在第九章高级模型中有详细讨论。现在,您需要知道的是,管理器负责处理数据的所有表级操作,包括最重要的数据查找。所有模型都会自动获得一个objects管理器;每当您想要查找模型实例时,都会使用它。

  • 最后,我们有all()。这是objects管理器上的一个方法,它返回数据库中的所有行。虽然这个对象看起来像一个列表,但它实际上是一个QuerySet-一个表示数据库中特定一组行的对象。附录 C,通用视图参考,详细介绍了 QuerySets。在本章的其余部分,我们将把它们当作它们模拟的列表来处理。

任何数据库查找都会遵循这个一般模式-我们将在我们想要查询的模型上调用附加的管理器的方法。

过滤数据

自然地,很少有人希望一次从数据库中选择所有内容;在大多数情况下,您将希望处理您数据的一个子集。在 Django API 中,您可以使用filter()方法过滤您的数据:

>>> Publisher.objects.filter(name='Apress') 
[<Publisher: Apress>] 

filter()接受关键字参数,这些参数被转换为适当的 SQL WHERE子句。前面的例子将被转换为类似于这样的东西:

SELECT id, name, address, city, state_province, country, website 
FROM books_publisher 
WHERE name = 'Apress'; 

您可以将多个参数传递给filter()以进一步缩小范围:

>>> Publisher.objects.filter(country="U.S.A.", state_province="CA") 
[<Publisher: Apress>] 

这些多个参数被转换为 SQL AND子句。因此,代码片段中的示例被转换为以下内容:

SELECT id, name, address, city, state_province, country, website 
FROM books_publisher 
WHERE country = 'U.S.A.' 
AND state_province = 'CA'; 

请注意,默认情况下,查找使用 SQL =运算符进行精确匹配查找。其他查找类型也是可用的:

>>> Publisher.objects.filter(name__contains="press") 
[<Publisher: Apress>] 

namecontains之间有一个双下划线。像 Python 本身一样,Django 使用双下划线来表示发生了一些魔术-这里,__contains部分被 Django 转换为 SQL LIKE语句:

SELECT id, name, address, city, state_province, country, website 
FROM books_publisher 
WHERE name LIKE '%press%'; 

还有许多其他类型的查找可用,包括icontains(不区分大小写的LIKE)、startswithendswith,以及range(SQL BETWEEN查询)。附录 C,通用视图参考,详细描述了所有这些查找类型。

检索单个对象

上面的所有filter()示例都返回了一个QuerySet,您可以像对待列表一样对待它。有时,只获取单个对象比获取列表更方便。这就是get()方法的用途:

>>> Publisher.objects.get(name="Apress") 
<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="Penguin") 
Traceback (most recent call last): 
    ... 
DoesNotExist: Publisher matching query does not exist. 

DoesNotExist异常是模型类Publisher.DoesNotExist的属性。在您的应用程序中,您将希望捕获这些异常,就像这样:

try: 
    p = Publisher.objects.get(name='Apress') 
except Publisher.DoesNotExist: 
    print ("Apress isn't in the database 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, state_province, country, website 
FROM books_publisher 
ORDER BY name; 

您可以按任何您喜欢的字段排序:

>>> Publisher.objects.order_by("address") 
 [<Publisher: O'Reilly>, <Publisher: Apress>] 

>>> Publisher.objects.order_by("state_province") 
 [<Publisher: Apress>, <Publisher: O'Reilly>] 

要按多个字段排序(其中第二个字段用于消除第一个字段相同时的排序),请使用多个参数:

>>> Publisher.objects.order_by("state_province", "address") 
 [<Publisher: Apress>, <Publisher: O'Reilly>] 

您还可以通过在字段名前加上“-”(减号)来指定反向排序:

>>> Publisher.objects.order_by("-name") 
[<Publisher: O'Reilly>, <Publisher: Apress>] 

虽然这种灵活性很有用,但是一直使用order_by()可能会相当重复。大多数情况下,您通常会有一个特定的字段,您希望按照它进行排序。在这些情况下,Django 允许您在模型中指定默认排序:

class Publisher(models.Model): 
    name = models.CharField(max_length=30) 
    address = models.CharField(max_length=50) 
    city = models.CharField(max_length=60) 
    state_province = models.CharField(max_length=30) 
    country = models.CharField(max_length=50) 
    website = models.URLField() 

    def __str__(self): 
        return self.name 

    class Meta:
 ordering = ['name']

在这里,我们介绍了一个新概念:class Meta,它是嵌入在Publisher类定义中的类(也就是说,它是缩进在class Publisher内部的)。您可以在任何模型上使用这个Meta类来指定各种特定于模型的选项。Meta选项的完整参考可在附录 B 中找到,但现在我们关注的是排序选项。如果您指定了这个选项,它告诉 Django,除非使用order_by()明确给出排序,否则所有Publisher对象在使用 Django 数据库 API 检索时都应该按name字段排序。

链接查找

您已经看到了如何过滤数据,也看到了如何对其进行排序。当然,通常情况下,您需要同时做这两件事。在这些情况下,您只需将查找链接在一起:

>>> Publisher.objects.filter(country="U.S.A.").order_by("-name") 
[<Publisher: O'Reilly>, <Publisher: Apress>] 

正如您所期望的,这会转换为一个同时具有WHEREORDER BY的 SQL 查询:

SELECT id, name, address, city, state_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, state_province, country, website 
FROM books_publisher 
ORDER BY name 
LIMIT 1; 

类似地,您可以使用 Python 的范围切片语法检索特定的数据子集:

>>> Publisher.objects.order_by('name')[0:2] 

这返回两个对象,大致翻译为:

SELECT id, name, address, city, state_province, country, website 
FROM books_publisher 
ORDER BY name 
OFFSET 0 LIMIT 2; 

请注意,不支持负切片:

>>> Publisher.objects.order_by('name')[-1] 
Traceback (most recent call last): 
  ... 
AssertionError: Negative indexing is not supported. 

不过,这很容易解决。只需更改order_by()语句,就像这样:

>>> Publisher.objects.order_by('-name')[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, state_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 为52。)您可以在此示例中看到,Django 的save()方法设置了所有列的值,而不仅仅是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(country='USA') 
2 

update()方法有一个返回值-表示更改了多少条记录的整数。在上面的例子中,我们得到了2

删除对象

要从数据库中删除对象,只需调用对象的delete()方法:

>>> p = Publisher.objects.get(name="O'Reilly") 
>>> p.delete() 
>>> Publisher.objects.all() 
[<Publisher: Apress Publishing>] 

您还可以通过在任何QuerySet的结果上调用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 "", line 1, in  
AttributeError: 'Manager' object has no attribute 'delete' 

但如果添加all()方法,它将起作用:

>>> Publisher.objects.all().delete() 

如果您只是删除数据的一个子集,您不需要包括all()。重复之前的例子:

>>> Publisher.objects.filter(country='USA').delete() 

接下来是什么?

阅读完本章后,您已经掌握了足够的 Django 模型知识,可以编写基本的数据库应用程序。第九章,“高级模型”,将提供有关 Django 数据库层更高级用法的一些信息。一旦您定义了模型,下一步就是向数据库填充数据。您可能有遗留数据,这种情况下第二十一章,“高级数据库管理”,将为您提供有关与遗留数据库集成的建议。您可能依赖站点用户提供数据,这种情况下第六章,“表单”,将教您如何处理用户提交的表单数据。但在某些情况下,您或您的团队可能需要手动输入数据,这种情况下拥有一个基于 Web 的界面来输入和管理数据将非常有帮助。下一章将介绍 Django 的管理界面,它正是为了这个目的而存在的。

第五章:Django 管理站点

对于大多数现代网站,管理界面是基础设施的一个重要部分。这是一个基于 web 的界面,仅限于受信任的站点管理员,它使得可以添加、编辑和删除站点内容。一些常见的例子:你用来发布博客的界面,后端站点管理员用来审核用户生成的评论的界面,你的客户用来更新你为他们建立的网站上的新闻稿的工具。

不过,管理界面存在一个问题:构建它们很无聊。当你开发面向公众的功能时,web 开发是很有趣的,但构建管理界面总是一样的。你必须验证用户、显示和处理表单、验证输入等等。这很无聊,也很重复。

那么 Django 对于这些无聊、重复的任务的处理方式是什么呢?它会为你处理一切。

使用 Django,构建管理界面是一个已解决的问题。在本章中,我们将探索 Django 的自动管理界面:看看它如何为我们的模型提供方便的界面,以及我们可以用它做的一些其他有用的事情。

使用管理站点

当你在第一章中运行了django-admin startproject mysite时,Django 为你创建并配置了默认的管理站点。你所需要做的就是创建一个管理用户(超级用户),然后你就可以登录管理站点了。

注意

如果你使用的是 Visual Studio,你不需要在命令行中完成下一步,你可以直接在 Visual Studio 的项目菜单选项卡中添加一个超级用户。

要创建一个管理用户,运行以下命令:

python manage.py createsuperuser

输入你想要的用户名并按回车。

Username: admin

然后你将被提示输入你想要的电子邮件地址:

Email address: admin@example.com

最后一步是输入密码。你将被要求两次输入密码,第二次是对第一次的确认。

Password: **********
Password (again): *********
Superuser created successfully.

启动开发服务器

在 Django 1.8 中,默认情况下激活了 django 管理站点。让我们启动开发服务器并进行探索。回想一下之前的章节,你可以这样启动开发服务器:

python manage.py runserver

现在,打开一个网页浏览器,转到本地域的/admin/,例如,http://127.0.0.1:8000/admin/。你应该会看到管理员的登录界面(图 5.1)。

由于默认情况下已经启用了翻译,登录界面可能会显示为你自己的语言,这取决于你的浏览器设置以及 Django 是否为这种语言提供了翻译。

进入管理站点

现在,尝试使用你在上一步中创建的超级用户账户登录。你应该会看到Django 管理员首页(图 5.2)。

你应该会看到两种可编辑的内容:组和用户。它们由django.contrib.auth提供,这是 Django 提供的身份验证框架。管理站点旨在供非技术用户使用,因此它应该相当容易理解。尽管如此,我们还是会快速介绍一下基本功能。

进入管理站点

图 5.1:Django 管理员登录界面

进入管理站点

图 5.2:Django 管理员首页

Django 管理站点中的每种数据都有一个更改列表和一个编辑表单。更改列表会显示数据库中所有可用的对象,而编辑表单则允许你添加、更改或删除数据库中的特定记录。点击用户行中的更改链接,加载用户的更改列表页面(图 5.3)。

进入管理站点

图 5.3:用户更改列表页面

这个页面显示了数据库中的所有用户;您可以将其视为SELECT * FROM auth_user; SQL 查询的网页版本。如果您正在跟随我们的示例,假设您只看到一个用户,那么一旦您有了更多的用户,您可能会发现过滤、排序和搜索选项很有用。

过滤选项在右侧,点击列标题可进行排序,顶部的搜索框可让您按用户名搜索。点击您创建的用户的用户名,您将看到该用户的编辑表单(图 5.4)。

这个页面允许您更改用户的属性,比如名字和各种权限。请注意,要更改用户的密码,您应该点击密码字段下的更改密码表单,而不是编辑哈希代码。

另一个需要注意的是,不同类型的字段会得到不同的小部件-例如,日期/时间字段有日历控件,布尔字段有复选框,字符字段有简单的文本输入字段。

进入管理站点

图 5.4:用户编辑表单

您可以通过在其编辑表单的左下角点击删除按钮来删除记录。这将带您到一个确认页面,在某些情况下,它将显示将被删除的任何相关对象(例如,如果您删除一个出版商,那么任何与该出版商有关的书籍也将被删除!)

您可以通过在管理主页的适当列中点击添加来添加记录。这将为您提供一个空白版本的编辑页面,准备让您填写。

您还会注意到,管理界面还为您处理输入验证。尝试将必填字段留空或在日期字段中输入无效日期,当您尝试保存时,您将看到这些错误,就像图 5.5中显示的那样。

当您编辑现有对象时,您会注意到窗口右上角有一个“历史”链接。通过管理界面进行的每一次更改都会被记录下来,您可以通过单击“历史”链接来查看这个日志(见图 5.6)。

进入管理站点

图 5.5:显示错误的编辑表单

进入管理站点

图 5.6:对象历史页面

注意

管理站点的工作原理

在幕后,管理站点是如何工作的?这相当简单。当 Django 在服务器启动时加载时,它会运行admin.autodiscover()函数。在 Django 的早期版本中,您需要从urls.py中调用这个函数,但现在 Django 会自动运行它。这个函数会遍历您的INSTALLED_APPS设置,并在每个已安装的应用程序中查找名为admin.py的文件。如果给定的应用程序中存在admin.py,它将执行该文件中的代码。

在我们的books应用程序的admin.py中,每次调用admin.site.register()都会简单地向管理站点注册给定的模型。管理站点只会为已经明确注册的模型显示编辑/更改界面。应用程序django.contrib.auth包括自己的admin.py,这就是为什么用户和组自动显示在管理中的原因。其他django.contrib应用程序,比如django.contrib.redirects,也会将自己添加到管理中,许多您从网上下载的第三方 Django 应用程序也会这样做。

除此之外,Django 管理站点只是一个 Django 应用程序,有自己的模型、模板、视图和 URLpatterns。您可以通过将其连接到您的 URLconf 来将其添加到您的应用程序中,就像您连接自己的视图一样。您可以在 Django 代码库的django/contrib/admin中查看其模板、视图和 URLpatterns,但不要尝试直接更改其中的任何内容,因为有很多钩子可以让您自定义管理站点的工作方式。

如果您决定在 Django 管理应用程序中进行探索,请记住,它在读取有关模型的元数据时会执行一些相当复杂的操作,因此可能需要大量时间来阅读和理解代码。

将您的模型添加到管理站点

有一个至关重要的部分我们还没有做。让我们将我们自己的模型添加到管理站点,这样我们就可以使用这个不错的界面向我们的自定义数据库表中添加、更改和删除对象。我们将继续第四章 模型中的books示例,我们在其中定义了三个模型:出版商、作者和书籍。在books目录(mysite/books)中,如果startapp没有创建一个名为admin.py的文件,那么您可以自己创建一个,并输入以下代码:

from django.contrib import admin 
from .models import Publisher, Author, Book 

admin.site.register(Publisher) 
admin.site.register(Author) 
admin.site.register(Book) 

这段代码告诉 Django 管理站点为每个模型提供界面。完成后,转到您的网页浏览器中的管理主页(http://127.0.0.1:8000/admin/),您应该会看到一个Books部分,其中包含有关作者、书籍和出版商的链接。(您可能需要停止并重新启动开发服务器以使更改生效。)现在,您已经为这三个模型中的每一个拥有了一个完全功能的管理界面。这很容易!

花一些时间添加和更改记录,用一些数据填充您的数据库。如果您遵循第四章 模型,创建Publisher对象的示例(并且您没有删除它们),您已经可以在出版商更改列表页面上看到这些记录了。

这里值得一提的一个功能是管理站点对外键和多对多关系的处理,这两者都出现在Book模型中。作为提醒,这是Book模型的样子:

class Book(models.Model): 
    title = models.CharField(max_length=100) 
    authors = models.ManyToManyField(Author) 
    publisher = models.ForeignKey(Publisher) 
    publication_date = models.DateField() 

    def __str__(self): 
        return self.title 

在 Django 管理站点的添加书籍页面(http://127.0.0.1:8000/admin/books/book/add/

出版商(ForeignKey)由一个下拉框表示,作者字段(ManyToManyField)由一个多选框表示。这两个字段旁边有一个绿色加号图标,让您可以添加相关类型的记录。

例如,如果您点击出版商字段旁边的绿色加号,您将会得到一个弹出窗口,让您可以添加一个出版商。在弹出窗口中成功创建出版商后,添加书籍表单将会更新,显示新创建的出版商。很棒。

使字段变为可选

在管理站点玩一段时间后,您可能会注意到一个限制-编辑表单要求填写每个字段,而在许多情况下,您可能希望某些字段是可选的。例如,我们希望Author模型的email字段是可选的-也就是说,允许空字符串。在现实世界中,您可能并不为每个作者都有电子邮件地址。

要指定email字段是可选的,请编辑Author模型(正如您从第四章 模型中记得的那样,它位于mysite/books/models.py中)。只需向email字段添加blank=True,如下所示:

class Author(models.Model): 
    first_name = models.CharField(max_length=30) 
    last_name = models.CharField(max_length=40) 
    email = models.EmailField(blank=True)

这段代码告诉 Django 空值确实允许作者的电子邮件地址。默认情况下,所有字段都具有blank=False,这意味着不允许空值。

这里发生了一些有趣的事情。到目前为止,除了__str__()方法之外,我们的模型一直作为数据库表的定义-基本上是 SQL CREATE TABLE语句的 Python 表达式。通过添加blank=True,我们已经开始扩展我们的模型,超出了对数据库表的简单定义。

现在,我们的模型类开始成为关于Author对象是什么以及它们能做什么的更丰富的知识集合。email字段不仅在数据库中表示为VARCHAR列;在诸如 Django 管理站点之类的上下文中,它也是一个可选字段。

一旦添加了blank=True,重新加载添加作者编辑表单(http://127.0.0.1:8000/admin/books/author/add/),您会注意到字段的标签-电子邮件-不再是粗体。这表示它不是必填字段。现在您可以添加作者而无需提供电子邮件地址;如果字段提交为空,您将不再收到响亮的红色此字段是必填的消息。

使日期和数字字段变为可选

blank=True相关的一个常见陷阱与日期和数字字段有关,但它需要相当多的背景解释。SQL 有自己指定空值的方式-一个称为NULL的特殊值。NULL可能意味着“未知”、“无效”或其他一些特定于应用程序的含义。在 SQL 中,NULL的值与空字符串不同,就像特殊的 Python 对象None与空的 Python 字符串("")不同。

这意味着特定字符字段(例如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 NULL, 
    "last_name" varchar(40) NOT NULL, 
    "email" varchar(75) NOT NULL 
); 

在大多数情况下,这种默认行为对于您的应用程序来说是最佳的,并且会避免数据不一致的问题。它与 Django 的其余部分很好地配合,比如 Django 管理站点,在您留空字符字段时会插入一个空字符串(而不是NULL值)。

但是,对于不接受空字符串作为有效值的数据库列类型,例如日期、时间和数字,有一个例外。如果您尝试将空字符串插入日期或整数列,根据您使用的数据库,您可能会收到数据库错误(PostgreSQL 是严格的,在这里会引发异常;MySQL 可能会接受它,也可能不会,这取决于您使用的版本、时间和月相)。

在这种情况下,NULL是指定空值的唯一方法。在 Django 模型中,您可以通过向字段添加null=True来指定允许NULL。这就是说:如果您想在日期字段(例如DateFieldTimeFieldDateTimeField)或数字字段(例如IntegerFieldDecimalFieldFloatField)中允许空值,您将需要同时使用null=Trueblank=True

举例来说,让我们将我们的Book模型更改为允许空白的publication_date。以下是修改后的代码:

class Book(models.Model): 
    title = models.CharField(max_length=100) 
    authors = models.ManyToManyField(Author) 
    publisher = models.ForeignKey(Publisher) 
    publication_date = models.DateField(blank=True, null=True)

添加null=True比添加blank=True更复杂,因为null=True会改变数据库的语义-也就是说,它会从publication_date字段的CREATE TABLE语句中删除NOT NULL。要完成此更改,我们需要更新数据库。出于许多原因,Django 不尝试自动更改数据库模式,因此您需要在对模型进行此类更改时执行python manage.py migrate命令。回到管理站点,现在添加书籍编辑表单应该允许空的出版日期值。

自定义字段标签

在管理站点的编辑表单上,每个字段的标签都是从其模型字段名称生成的。算法很简单:Django 只是用空格替换下划线,并将第一个字符大写,因此,例如,Book模型的publication_date字段的标签是出版日期

然而,字段名称并不总是适合作为管理员字段标签,因此在某些情况下,您可能希望自定义标签。您可以通过在适当的模型字段中指定verbose_name来实现这一点。例如,这是我们如何将Author.email字段的标签更改为e-mail,并加上连字符:

class Author(models.Model): 
    first_name = models.CharField(max_length=30) 
    last_name = models.CharField(max_length=40) 
 email = models.EmailField(blank=True, verbose_name ='e-mail')

进行这些更改并重新加载服务器,您应该在作者编辑表单上看到字段的新标签。请注意,除非始终应该大写(例如"USA state"),否则不要大写verbose_name的第一个字母。Django 将在需要时自动将其大写,并且在不需要大写的其他地方使用确切的verbose_name值。

自定义模型管理员类

到目前为止我们所做的更改-blank=Truenull=Trueverbose_name-实际上是模型级别的更改,而不是管理员级别的更改。也就是说,这些更改基本上是模型的一部分,只是碰巧被管理员站点使用;它们与管理员无关。

除此之外,Django 管理员站点提供了丰富的选项,让您可以自定义管理员站点如何为特定模型工作。这些选项存在于ModelAdmin 类中,这些类包含了特定模型在特定管理员站点实例中的配置。

自定义更改列表

让我们通过指定在我们的Author模型的更改列表上显示的字段来深入研究管理员自定义。默认情况下,更改列表显示每个对象的__str__()的结果。在第四章模型中,我们为Author对象定义了__str__()方法,以显示名字和姓氏:

class Author(models.Model): 
    first_name = models.CharField(max_length=30) 
    last_name = models.CharField(max_length=40) 
    email = models.EmailField(blank=True, verbose_name ='e-mail') 

    def __str__(self): 
        return u'%s %s' % (self.first_name, self.last_name) 

结果是,Author对象的更改列表显示了每个人的名字和姓氏,就像图 5.7中所示的那样。

自定义更改列表

图 5.7:作者更改列表页面

我们可以通过向更改列表显示添加一些其他字段来改进这种默认行为。例如,在此列表中看到每个作者的电子邮件地址会很方便,而且能够按名字和姓氏排序也很好。为了实现这一点,我们将为Author模型定义一个ModelAdmin类。这个类是自定义管理员的关键,它让您可以做的最基本的事情之一就是指定要在更改列表页面上显示的字段列表。编辑admin.py以进行这些更改:

from django.contrib import admin 
from mysite.books.models import Publisher, Author, Book 

class AuthorAdmin(admin.ModelAdmin):
 list_display = ('first_name', 'last_name', 'email') 

admin.site.register(Publisher) 
admin.site.register(Author, AuthorAdmin) 
admin.site.register(Book) 

我们所做的是:

  • 我们创建了AuthorAdmin类。这个类是django.contrib.admin.ModelAdmin的子类,保存了特定管理员模型的自定义配置。我们只指定了一个自定义选项-list_display,它设置为要在更改列表页面上显示的字段名称的元组。当然,这些字段名称必须存在于模型中。

  • 我们修改了admin.site.register()调用,将AuthorAdmin添加到Author之后。您可以这样理解:使用AuthorAdmin选项注册Author模型。

  • admin.site.register()函数接受ModelAdmin子类作为可选的第二个参数。如果不指定第二个参数(就像PublisherBook的情况一样),Django 将使用该模型的默认管理员选项。

进行了这些调整后,重新加载作者更改列表页面,您会看到现在显示了三列-名字、姓氏和电子邮件地址。此外,每列都可以通过单击列标题进行排序。(见图 5.8。)

自定义更改列表

图 5.8:添加list_display后的作者更改列表页面

接下来,让我们添加一个简单的搜索栏。像这样在AuthorAdmin中添加search_fields

class AuthorAdmin(admin.ModelAdmin): 
    list_display = ('first_name', 'last_name', 'email') 
 search_fields = ('first_name', 'last_name')

在浏览器中重新加载页面,您应该会看到顶部有一个搜索栏(见 图 5.9)。我们刚刚告诉管理员更改列表页面包括一个搜索栏,可以搜索 first_namelast_name 字段。正如用户所期望的那样,这是不区分大小写的,并且搜索两个字段,因此搜索字符串 bar 将找到名为 Barney 的作者和姓为 Hobarson 的作者。

自定义更改列表

图 5.9:search_fields 添加后的作者更改列表页面

接下来,让我们在我们的 Book 模型的更改列表页面上添加一些日期过滤器:

from django.contrib import admin 
from mysite.books.models import Publisher, Author, Book 

class AuthorAdmin(admin.ModelAdmin): 
    list_display = ('first_name', 'last_name', 'email') 
    search_fields = ('first_name', 'last_name') 

class BookAdmin(admin.ModelAdmin):
 list_display = ('title', 'publisher', 'publication_date')
 list_filter = ('publication_date',) 

admin.site.register(Publisher) 
admin.site.register(Author, AuthorAdmin) 
admin.site.register(Book, BookAdmin)

在这里,因为我们正在处理不同的选项集,我们创建了一个单独的 ModelAdmin 类-BookAdmin。首先,我们定义了一个 list_display,只是为了让更改列表看起来更好一些。然后,我们使用了 list_filter,它设置为一个字段元组,用于在更改列表页面的右侧创建过滤器。对于日期字段,Django 提供了快捷方式来过滤列表,包括今天过去 7 天本月今年-这些是 Django 开发人员发现的常见日期过滤情况的快捷方式。图 5.10 显示了它的样子。

自定义更改列表

图 5.10:list_filter 后的书籍更改列表页面

list_filter 也适用于其他类型的字段,不仅仅是 DateField。(例如,尝试使用 BooleanFieldForeignKey 字段。)只要有至少两个可选择的值,过滤器就会显示出来。另一种提供日期过滤器的方法是使用 date_hierarchy 管理选项,就像这样:

class BookAdmin(admin.ModelAdmin): 
    list_display = ('title', 'publisher','publication_date') 
    list_filter = ('publication_date',) 
 date_hierarchy = 'publication_date'

有了这个设置,更改列表页面顶部会出现一个日期钻取导航栏,如 图 5.11 所示。它从可用年份列表开始,然后进入月份和具体日期。

自定义更改列表

图 5.11:date_hierarchy 后的书籍更改列表页面

请注意,date_hierarchy 接受一个字符串,而不是元组,因为只能使用一个日期字段来创建层次结构。最后,让我们更改默认排序,使得更改列表页面上的书籍总是按照它们的出版日期降序排序。默认情况下,更改列表根据其模型的 class Meta 中的 ordering 对象进行排序(我们在第四章中介绍过,模型)-但如果您没有指定这个 ordering 值,那么排序是未定义的。

class BookAdmin(admin.ModelAdmin): 
    list_display = ('title', 'publisher','publication_date') 
    list_filter = ('publication_date',) 
    date_hierarchy = 'publication_date' 
 ordering = ('-publication_date',)

这个管理员 ordering 选项与模型的 class Meta 中的 ordering 完全相同,只是它只使用列表中的第一个字段名。只需传递一个字段名的列表或元组,并在字段前加上减号以使用降序排序。重新加载书籍更改列表,以查看它的效果。请注意,出版日期 标头现在包含一个小箭头,指示记录的排序方式(见 图 5.12)。

自定义更改列表

图 5.12:排序后的书籍更改列表页面

我们在这里介绍了主要的更改列表选项。使用这些选项,您可以只用几行代码就可以创建一个非常强大的、适用于生产的数据编辑界面。

自定义编辑表单

就像更改列表可以自定义一样,编辑表单也可以以多种方式自定义。首先,让我们自定义字段的排序方式。默认情况下,编辑表单中字段的顺序与模型中定义的顺序相对应。我们可以使用我们的 ModelAdmin 子类中的 fields 选项来更改这一点:

class BookAdmin(admin.ModelAdmin): 
    list_display = ('title', 'publisher', 'publication_date') 
    list_filter = ('publication_date',) 
    date_hierarchy = 'publication_date' 
    ordering = ('-publication_date',) 
 fields = ('title', 'authors', 'publisher', publication_date')

在这个更改之后,书籍的编辑表单将使用给定的字段排序。将作者放在书名后面会更自然一些。当然,字段顺序应该取决于您的数据输入工作流程。每个表单都是不同的。

fields选项让你可以做的另一件有用的事情是完全排除某些字段的编辑。只需省略你想要排除的字段。如果你的管理员用户只被信任编辑数据的某个部分,或者你的某些字段是由外部自动化流程改变的,你可能会用到这个功能。

例如,在我们的书籍数据库中,我们可以隐藏publication_date字段,使其不可编辑:

class BookAdmin(admin.ModelAdmin): 
    list_display = ('title', 'publisher','publication_date') 
    list_filter = ('publication_date',) 
    date_hierarchy = 'publication_date' 
    ordering = ('-publication_date',) 
 fields = ('title', 'authors', 'publisher')

因此,书籍的编辑表单没有提供指定出版日期的方法。这可能很有用,比如,如果你是一个编辑,你希望作者不要推迟出版日期。(当然,这只是一个假设的例子。)当用户使用这个不完整的表单添加新书时,Django 将简单地将publication_date设置为None-所以确保该字段具有null=True

另一个常用的编辑表单定制与多对多字段有关。正如我们在书籍的编辑表单上看到的,管理员站点将每个ManyToManyField表示为多选框,这是最合乎逻辑的 HTML 输入小部件使用方式,但多选框可能难以使用。如果你想选择多个项目,你必须按住控制键,或者在 Mac 上按住命令键。

管理员站点贴心地插入了一些解释这一点的文本,但是当你的字段包含数百个选项时,它仍然变得笨拙。管理员站点的解决方案是filter_horizontal。让我们将其添加到BookAdmin中,看看它的作用。

class BookAdmin(admin.ModelAdmin): 
    list_display = ('title', 'publisher','publication_date') 
    list_filter = ('publication_date',) 
    date_hierarchy = 'publication_date' 
    ordering = ('-publication_date',) 
 filter_horizontal = ('authors',)

(如果你在跟着做,注意我们也已经移除了fields选项来显示编辑表单中的所有字段。)重新加载书籍的编辑表单,你会看到作者部分现在使用了一个花哨的 JavaScript 过滤界面,让你可以动态搜索选项并将特定作者从可用作者移动到已选作者框中,反之亦然。

自定义编辑表单

图 5.13:添加 filter_horizontal 后的书籍编辑表单

我强烈建议对于有超过十个项目的ManyToManyField使用filter_horizontal。它比简单的多选小部件更容易使用。另外,请注意你可以对多个字段使用filter_horizontal-只需在元组中指定每个名称。

ModelAdmin类也支持filter_vertical选项。这与filter_horizontal的工作方式完全相同,但是生成的 JavaScript 界面将两个框垂直堆叠而不是水平堆叠。这是个人品味的问题。

filter_horizontalfilter_vertical只对ManyToManyField字段起作用,而不对ForeignKey字段起作用。默认情况下,管理员站点对ForeignKey字段使用简单的<select>框,但是,就像对于ManyToManyField一样,有时你不想承担选择所有相关对象以在下拉框中显示的开销。

例如,如果我们的书籍数据库增长到包括成千上万的出版商,添加书籍表单可能需要一段时间才能加载,因为它需要加载每个出版商以在<select>框中显示。

修复这个问题的方法是使用一个叫做raw_id_fields的选项:

class BookAdmin(admin.ModelAdmin): 
    list_display = ('title', 'publisher','publication_date') 
    list_filter = ('publication_date',) 
    date_hierarchy = 'publication_date' 
    ordering = ('-publication_date',) 
    filter_horizontal = ('authors',) 
 raw_id_fields = ('publisher',)

将其设置为ForeignKey字段名称的元组,这些字段将在管理员中显示为一个简单的文本输入框(<input type="text">),而不是一个<select>。见图 5.14

自定义编辑表单

图 5.14:添加raw_id_fields后的书籍编辑表单

你在这个输入框中输入什么?出版商的数据库 ID。鉴于人类通常不会记住数据库 ID,还有一个放大镜图标,你可以点击它弹出一个窗口,从中选择要添加的出版商。

用户、组和权限

因为您以超级用户身份登录,您可以访问创建、编辑和删除任何对象。不同的环境需要不同的权限系统-并非每个人都可以或应该成为超级用户。Django 的管理员站点使用了一个权限系统,您可以使用它来仅给特定用户访问他们需要的界面部分。这些用户帐户的设计是足够通用,可以在管理员界面之外使用,但我们现在将它们视为管理员用户帐户。

在第十一章,“Django 中的用户认证”中,我们将介绍如何使用 Django 的认证系统在整个站点上管理用户(即不仅仅是管理员站点)。您可以像编辑任何其他对象一样,通过管理员界面编辑用户和权限。我们在本章的前面看到了这一点,当时我们在管理员的用户和组部分玩耍。

用户对象具有标准的用户名、密码、电子邮件和真实姓名字段,以及一组定义用户在管理员界面中允许做什么的字段。首先,有一组三个布尔标志:

  • active 标志控制用户是否活跃。如果这个标志关闭,用户尝试登录时,即使有有效密码,也不会被允许登录。

  • staff 标志控制用户是否被允许登录到管理员界面(也就是说,该用户是否被认为是您组织中的工作人员)。由于这个相同的用户系统可以用来控制对公共(即非管理员)站点的访问(参见第十一章,“Django 中的用户认证”),这个标志区分了公共用户和管理员。

  • 超级用户标志给予用户在管理员界面中添加、创建和删除任何项目的完全访问权限。如果用户设置了这个标志,那么所有常规权限(或缺乏权限)对该用户都将被忽略。

普通管理员用户-也就是活跃的、非超级用户的工作人员-通过分配的权限获得管理员访问权限。通过管理员界面可编辑的每个对象(例如书籍、作者、出版商)都有三个权限:创建权限、编辑权限和删除权限。将权限分配给用户将授予用户执行这些权限描述的操作的访问权限。当您创建用户时,该用户没有任何权限,您需要为用户分配特定的权限。

例如,您可以给用户添加和更改出版商的权限,但不给予删除的权限。请注意,这些权限是针对模型定义的,而不是针对对象定义的-因此它们让您说“约翰可以对任何书进行更改”,但不让您说“约翰可以对 Apress 出版的任何书进行更改”。后者的功能,即对象级权限,有点复杂,超出了本书的范围,但在 Django 文档中有介绍。

注意

警告!

对编辑用户和权限的访问也受到这个权限系统的控制。如果您给某人编辑用户的权限,他们将能够编辑自己的权限,这可能不是您想要的!给用户编辑用户的权限实质上是将用户变成超级用户。

您还可以将用户分配到组。组只是一组权限,适用于该组的所有成员。组对于授予一部分用户相同的权限非常有用。

何时以及为什么使用管理员界面,以及何时不要使用

通过本章的学习,您应该对如何使用 Django 的管理员站点有一个很好的了解。但我想强调一下何时以及为什么您可能想要使用它,以及何时不要使用它。

当非技术用户需要输入数据时,Django 的管理站点尤其突出;毕竟,这就是该功能的目的。在 Django 首次开发的报纸上,开发典型的在线功能(比如市政供水水质特别报告)的开发过程大致如下:

  • 负责项目的记者与其中一名开发人员会面,并描述可用的数据。

  • 开发人员设计 Django 模型以适应这些数据,然后向记者打开管理站点。

  • 记者检查管理站点以指出任何缺失或多余的字段-现在指出比以后好。开发人员迭代更改模型。

  • 当模型达成一致后,记者开始使用管理站点输入数据。与此同时,程序员可以专注于开发公开可访问的视图/模板(这是有趣的部分!)。

换句话说,Django 的管理界面的存在意义是促进内容生产者和程序员的同时工作。然而,除了这些明显的数据输入任务之外,管理站点在一些其他情况下也很有用:

  • 检查数据模型:一旦定义了一些模型,通过在管理界面中调用它们并输入一些虚拟数据,这可能会揭示数据建模错误或模型的其他问题。

  • 管理获取的数据:对于依赖来自外部来源的数据的应用程序(例如用户或网络爬虫),管理站点为您提供了一种轻松的方式来检查或编辑这些数据。您可以将其视为数据库命令行实用程序的功能较弱但更方便的版本。

  • 快速而简单的数据管理应用程序:您可以使用管理站点来构建一个非常轻量级的数据管理应用程序,比如用于跟踪开支。如果您只是为自己的需求构建某些东西,而不是为公众消费,管理站点可以帮助您走得更远。在这种意义上,您可以将其视为增强版的关系型电子表格。

然而,管理站点并不是万能的。它不打算成为数据的公共接口,也不打算允许对数据进行复杂的排序和搜索。正如本章早期所说,它是为受信任的站点管理员而设计的。牢记这一甜蜜点是有效使用管理站点的关键。

接下来呢?

到目前为止,我们已经创建了一些模型,并配置了一个一流的界面来编辑数据。在下一章中,我们将继续进行真正的网页开发:表单创建和处理。

第六章:表单

HTML 表单是交互式网站的支柱,从谷歌的单个搜索框的简单性到无处不在的博客评论提交表单到复杂的自定义数据输入界面。

本章涵盖了如何使用 Django 访问用户提交的表单数据,对其进行验证并执行某些操作。在此过程中,我们将涵盖HttpRequestForm对象。

从请求对象获取数据

我在第二章中介绍了HttpRequest对象,视图和 URLconfs,当时我们首次涵盖了视图函数,但那时我对它们没有太多可说的。回想一下,每个视图函数都以HttpRequest对象作为其第一个参数,就像我们的hello()视图一样:

from django.http import HttpResponse 

def hello(request): 
    return HttpResponse("Hello world") 

HttpRequest对象,比如这里的变量request,有许多有趣的属性和方法,您应该熟悉它们,以便了解可能发生的情况。您可以使用这些属性来获取有关当前请求的信息(即加载 Django 站点上当前页面的用户/网络浏览器)在执行视图函数时。

关于 URL 的信息

HttpRequest对象包含有关当前请求的 URL 的几个信息(表 6.1)。

属性/方法 描述 示例
request.path 完整路径,不包括域名,但包括前导斜杠。 "/hello/"
request.get_host() 主机(即俗称的“域名”)。 "127.0.0.1:8000""www.example.com"
request.get_full_path() path,加上查询字符串(如果有的话)。 "/hello/?print=true"
request.is_secure() 如果请求是通过 HTTPS 进行的,则为True。否则为False TrueFalse

表 6.1:HttpRequest 方法和属性

始终使用这些属性/方法,而不是在视图中硬编码 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.META是一个 Python 字典,包含给定请求的所有可用 HTTP 标头-包括用户的 IP 地址和用户代理(通常是 Web 浏览器的名称和版本)。请注意,可用标头的完整列表取决于用户发送了哪些标头以及您的 Web 服务器设置了哪些标头。该字典中一些常用的键是:

  • HTTP_REFERER:引用的 URL,如果有的话。(请注意REFERER的拼写错误)。

  • HTTP_USER_AGENT:用户的浏览器的用户代理字符串,如果有的话。它看起来像这样:"Mozilla/5.0 (X11; U; Linux i686; fr-FR; rv:1.8.1.17) Gecko/20080829 Firefox/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 标头是外部数据-即它们是由您的用户的浏览器提交的-所以不应该信任它们,您应该始终设计您的应用程序,以便在特定标头为空或不存在时优雅地失败。)您应该使用try/except子句或get()方法来处理未定义键的情况:

# BAD! 
def ua_display_bad(request): 
    ua = request.META['HTTP_USER_AGENT']  # Might raise KeyError! 
    return HttpResponse("Your browser 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 browser is %s" % ua) 

# GOOD (VERSION 2) 
def ua_display_good2(request): 
    ua = request.META.get('HTTP_USER_AGENT', 'unknown') 
    return HttpResponse("Your browser is %s" % ua) 

我鼓励您编写一个小视图,显示所有request.META数据,以便了解其中的内容。以下是该视图的样子:

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)) 

查看请求对象包含的信息的另一种好方法是仔细查看 Django 错误页面,当您使系统崩溃时-那里有大量有用的信息,包括所有 HTTP 标头和其他请求对象(例如request.path)。

有关提交数据的信息

关于请求的基本元数据之外,HttpRequest对象有两个属性,包含用户提交的信息:request.GETrequest.POST。这两个都是类似字典的对象,可以访问GETPOST数据。

POST数据通常是从 HTML <form>提交的,而GET数据可以来自页面 URL 中的<form>或查询字符串。

注意

类似字典的对象

当我们说request.GETrequest.POST类似字典的对象时,我们的意思是它们的行为类似于标准的 Python 字典,但在技术上并不是字典。例如,request.GETrequest.POST都有get()keys()values()方法,您可以通过for key in request.GET来遍历键。那么为什么要区分呢?因为request.GETrequest.POST都有标准字典没有的额外方法。我们将在短时间内介绍这些方法。您可能遇到过类似的术语类似文件的对象-具有一些基本方法(如read())的 Python 对象,让它们可以充当"真实"文件对象的替代品。

一个简单的表单处理示例

继续图书、作者和出版商的示例,让我们创建一个简单的视图,让用户通过标题搜索我们的图书数据库。通常,开发表单有两个部分:HTML 用户界面和处理提交数据的后端视图代码。第一部分很容易;让我们设置一个显示搜索表单的视图:


from django.shortcuts import render 

def search_form(request): 
    return render(request, 'search_form.html') 

正如您在第三章中学到的,这个视图可以存在于 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> 

将此文件保存到您在第三章中创建的mysite/templates目录中,模板,或者您可以创建一个新的文件夹books/templates。只需确保您的设置文件中的'APP_DIRS'设置为Trueurls.py中的 URL 模式可能如下所示:

from books import views 

urlpatterns = [ 
    # ... 
    url(r'^search-form/$', views.search_form), 
    # ... 
] 

(请注意,我们直接导入views模块,而不是像from mysite.views import search_form这样的方式,因为前者更简洁。我们将在第七章中更详细地介绍这种导入方法,高级视图和 URLconfs)。现在,如果您运行开发服务器并访问http://127.0.0.1:8000/search-form/,您将看到搜索界面。足够简单。不过,尝试提交表单,您将收到 Django 404 错误。表单指向 URL/search/,但尚未实现。让我们用第二个视图函数来修复这个问题:

# urls.py 

urlpatterns = [ 
    # ... 
    url(r'^search-form/$', views.search_form), 
    url(r'^search/$', views.search), 
    # ... 
] 

# books/views.py 

from django.http import HttpResponse 

# ... 

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,并且您可以感受搜索词是如何在系统中流动的。简而言之:

  • HTML <form>定义了一个变量q。当提交时,q的值通过GETmethod="get")发送到 URL/search/

  • 处理 URL/search/search())的 Django 视图可以访问request.GET中的q值。

这里要指出的一个重要事情是,我们明确检查request.GET中是否存在'q'。正如我在前面的request.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),您可以使用request.GET来访问查询字符串变量。在第二章中,视图和 URLconfs,介绍了 Django 的 URLconf 系统,我将 Django 的美观 URL 与更传统的 PHP/Java URL 进行了比较,例如/time/plus?hours=3,并说我会在第六章中向您展示如何做后者。现在您知道如何在视图中访问查询字符串参数(例如在这个示例中的hours=3)-使用request.GET

POST数据的工作方式与GET数据相同-只需使用request.POST而不是request.GETGETPOST之间有什么区别?当提交表单的行为只是获取数据时使用GET。当提交表单的行为会产生一些副作用-更改数据、发送电子邮件或其他超出简单数据显示的操作时使用POST。在我们的图书搜索示例中,我们使用GET,因为查询不会改变服务器上的任何数据。(如果您想了解更多关于GETPOST的信息,请参阅 http://www.w3.org/2001/tag/doc/whenToUseGet.html 网站。)现在我们已经验证了request.GET是否被正确传递,让我们将用户的搜索查询连接到我们的图书数据库中(同样是在views.py中):

from django.http import HttpResponse 
from django.shortcuts import render 
from books.models import Book 

def search(request): 
    if 'q' in request.GET and request.GET['q']: 
        q = request.GET['q'] 
        books = Book.objects.filter(title__icontains=q) 
        return render(request, 'search_results.html', 
                      {'books': books, 'query': q}) 
    else: 
        return HttpResponse('Please submit a search term.') 

关于我们在这里所做的一些说明:

  • 除了检查'q'是否存在于request.GET中,我们还确保在将其传递给数据库查询之前,request.GET['q']是一个非空值。

  • 我们使用Book.objects.filter(title__icontains=q)来查询我们的图书表,找到标题包含给定提交的所有书籍。icontains是一种查找类型(如第四章和附录 B 中所解释的那样),该语句可以粗略地翻译为“获取标题包含q的书籍,而不区分大小写。”

  • 这是一个非常简单的图书搜索方法。我们不建议在大型生产数据库上使用简单的icontains查询,因为它可能会很慢。(在现实世界中,您可能希望使用某种自定义搜索系统。搜索网络以获取开源全文搜索的可能性。)

  • 我们将books,一个Book对象的列表,传递给模板。search_results.html文件可能包括类似以下内容:

         <html> 
          <head> 
              <title>Book Search</title> 
          </head> 
          <body> 
            <p>You searched for: <strong>{{ query }}</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 %} 

          </body> 
        </html> 

注意使用pluralize模板过滤器,根据找到的书籍数量输出“s”。

改进我们简单的表单处理示例

与以前的章节一样,我向您展示了可能起作用的最简单的方法。现在我将指出一些问题,并向您展示如何改进它。首先,我们的search()视图对空查询的处理很差-我们只显示一个**请提交搜索词。**消息,要求用户点击浏览器的返回按钮。

这是可怕的,不专业的,如果您真的在实际中实现了这样的东西,您的 Django 权限将被撤销。更好的方法是重新显示表单,并在其前面显示一个错误,这样用户可以立即重试。最简单的方法是再次渲染模板,就像这样:

from django.http import HttpResponse 
from django.shortcuts import render 
from books.models import Book 

def search_form(request): 
    return render(request, 'search_form.html') 

def search(request): 
    if 'q' in request.GET and request.GET['q']: 
        q = request.GET['q'] 
        books = Book.objects.filter(title__icontains=q) 
        return render(request, 'search_results.html', 
                      {'books': books, 'query': q}) 
    else: 
 return render
           (request, 'search_form.html', {'error': True})

(请注意,我在这里包括了search_form(),这样您就可以在一个地方看到两个视图。)在这里,我们改进了search(),如果查询为空,就重新渲染search_form.html模板。因为我们需要在该模板中显示错误消息,所以我们传递了一个模板变量。现在我们可以编辑search_form.html来检查error变量:

<html> 
<head> 
    <title>Search</title> 
</head> 
<body> 
 {% if error %} 
 <p style="color: red;">Please 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()视图?

目前,对 URL/search/(没有任何GET参数)的请求将显示空表单(但带有错误)。只要我们在没有GET参数的情况下访问/search/,就可以删除search_form()视图及其相关的 URLpattern,同时将search()更改为在有人访问/search/时隐藏错误消息:

def search(request): 
    error = False 
    if 'q' in request.GET: 
        q = request.GET['q'] 
if not q: 
 error = True 
 else: 
            books = Book.objects.filter(title__icontains=q) 
            return render(request, 'search_results.html', 
                          {'books': books, 'query': q}) 
 return render(request, 'search_form.html', 
 {'error': error})

在这个更新的视图中,如果用户在没有GET参数的情况下访问/search/,他们将看到没有错误消息的搜索表单。如果用户提交了一个空值的'q',他们将看到带有错误消息的搜索表单。最后,如果用户提交了一个非空值的'q',他们将看到搜索结果。

我们可以对此应用进行最后一次改进,以消除一些冗余。现在我们已经将两个视图和 URL 合并为一个,并且/search/处理搜索表单显示和结果显示,search_form.html中的 HTML<form>不必硬编码 URL。而不是这样:

<form action="/search/" method="get"> 

可以更改为这样:

<form action="" method="get"> 

action="" 表示将表单提交到与当前页面相同的 URL。有了这个改变,如果您将search()视图连接到另一个 URL,您就不必记得更改action

简单验证

我们的搜索示例仍然相当简单,特别是在数据验证方面;我们只是检查确保搜索查询不为空。许多 HTML 表单包括比确保值非空更复杂的验证级别。我们都在网站上看到过错误消息:

  • 请输入一个有效的电子邮件地址。'foo'不是一个电子邮件地址。

  • 请输入一个有效的五位数字的美国邮政编码。'123'不是一个邮政编码。

  • 请输入格式为 YYYY-MM-DD 的有效日期。

  • 请输入至少 8 个字符长且至少包含一个数字的密码。

让我们调整我们的search()视图,以验证搜索词是否少于或等于 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.filter(title__icontains=q) 
            return render(request, 'search_results.html', 
                          {'books': books, 'query': q}) 
    return render(request, 'search_form.html', 
        {'error': error}) 

现在,如果您尝试提交一个超过 20 个字符长的搜索查询,它将不允许您进行搜索;您将收到一个错误消息。但是search_form.html中的错误消息目前说:“请提交搜索词”。-所以我们必须更改它以适应两种情况:

<html> 
<head> 
    <title>Search</title> 
</head> 
<body> 
    {% if error %} 
 <p style="color: red;"> 
 Please 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 个字符的限制?

错误消息应该是具体的、明确的,不应该令人困惑。问题在于我们使用了一个简单的布尔值error,而我们应该使用一个错误消息字符串列表。以下是我们可能如何修复它:

def search(request): 
    errors = [] 
    if 'q' in request.GET: 
        q = request.GET['q'] 
        if not q: 
 errors.append('Enter a search term.') 
        elif len(q) > 20: 
 errors.append('Please enter at most 20 characters.') 
        else: 
            books = Book.objects.filter(title__icontains=q) 
            return render(request, 'search_results.html', 
                          {'books': books, 'query': q}) 
    return render(request, 'search_form.html', 
                  {'errors': errors}) 

然后,我们需要对search_form.html模板进行小的调整,以反映它现在传递了一个errors列表,而不是一个error布尔值:

<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> 

创建联系表单

尽管我们多次迭代了图书搜索表单示例并对其进行了良好的改进,但它仍然基本上很简单:只有一个字段'q'。随着表单变得更加复杂,我们必须一遍又一遍地重复前面的步骤,为我们使用的每个表单字段重复这些步骤。这引入了很多废料和很多人为错误的机会。幸运的是,Django 的开发人员考虑到了这一点,并在 Django 中构建了一个处理表单和验证相关任务的更高级别库。

您的第一个表单类

Django 带有一个表单库,称为django.forms,它处理了本章中我们探讨的许多问题-从 HTML 表单显示到验证。让我们深入研究并使用 Django 表单框架重新设计我们的联系表单应用程序。

使用表单框架的主要方法是为您处理的每个 HTML <form>定义一个Form类。在我们的情况下,我们只有一个<form>,所以我们将有一个Form类。这个类可以放在任何您想要的地方,包括直接放在您的views.py文件中,但社区约定是将Form类放在一个名为forms.py的单独文件中。

在与您的mysite/views.py相同的目录中创建此文件,并输入以下内容:

from django import forms 

class ContactForm(forms.Form): 
    subject = forms.CharField() 
    email = forms.EmailField(required=False) 
    message = forms.CharField() 

这是非常直观的,类似于 Django 的模型语法。表单中的每个字段都由Field类的一种类型表示-这里只使用CharFieldEmailField作为Form类的属性。默认情况下,每个字段都是必需的,因此要使email可选,我们指定required=False。让我们进入 Python 交互解释器,看看这个类能做什么。它能做的第一件事是将自己显示为 HTML:

>>> from mysite.forms import ContactForm 
>>> f = ContactForm() 
>>> print(f) 
<tr><th><label for="id_subject">Subject:</label></th><td><input type="text" 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">Message:</label></th><td><input type="text" name="message" id="id_message" /></td></tr> 

Django 为每个字段添加了标签,以及用于辅助功能的<label>标签。其目的是使默认行为尽可能优化。此默认输出采用 HTML <table>格式,但还有其他几种内置输出:

>>> print(f.as_ul()) 
<li><label for="id_subject">Subject:</label> <input type="text" name="subject" id="id_subject" /></li> 
<li><label for="id_email">Email:</label> <input type="text" name="email" id="id_email" /></li> 
<li><label for="id_message">Message:</label> <input type="text" name="message" id="id_message" /></li> 

>>> print(f.as_p()) 
<p><label for="id_subject">Subject:</label> <input type="text" name="subject" id="id_subject" /></p> 
<p><label for="id_email">Email:</label> <input type="text" name="email" id="id_email" /></p> 
<p><label for="id_message">Message:</label> <input type="text" name="message" id="id_message" /></p> 

请注意,输出中不包括开放和关闭的<table><ul><form>标签,因此您可以根据需要添加任何额外的行和自定义。这些方法只是常见情况下的快捷方式,即“显示整个表单”。您还可以显示特定字段的 HTML:

>>> print(f['subject']) 
<input id="id_subject" name="subject" type="text" /> 
>>> print f['message'] 
<input id="id_message" name="message" type="text" /> 

Form对象的第二个功能是验证数据。要验证数据,请创建一个新的Form对象,并将数据字典传递给它,将字段名称映射到数据:

>>> f = ContactForm({'subject': 'Hello', 'email': 'adrian@example.com', 'message': 'Nice site!'}) 

一旦您将数据与Form实例关联起来,就创建了一个绑定表单:

>>> f.is_bound 
True 

对任何绑定的Form调用is_valid()方法,以了解其数据是否有效。我们已为每个字段传递了有效值,因此整个Form都是有效的:

>>> f.is_valid() 
True 

如果我们不传递email字段,它仍然有效,因为我们已经为该字段指定了required=False

>>> f = ContactForm({'subject': 'Hello', 'message': 'Nice site!'}) 
>>> f.is_valid() 
True 

但是,如果我们省略subjectmessage中的任何一个,Form将不再有效:

>>> f = ContactForm({'subject': 'Hello'}) 
>>> f.is_valid() 
False 
>>> f = ContactForm({'subject': 'Hello', 'message': ''}) 
>>> f.is_valid() 
False 

您可以深入了解特定字段的错误消息:

>>> f = ContactForm({'subject': 'Hello', 'message': ''}) 
>>> f['message'].errors 
['This field is required.'] 
>>> f['subject'].errors 
[] 
>>> f['email'].errors 
[] 

每个绑定的Form实例都有一个errors属性,该属性为您提供了一个将字段名称映射到错误消息列表的字典:

>>> f = ContactForm({'subject': 'Hello', 'message': ''}) 
>>> f.errors 
{'message': ['This field is required.']} 

最后,对于数据已被发现有效的Form实例,将提供cleaned_data属性。这是提交的数据的“清理”。Django 的表单框架不仅验证数据;它通过将值转换为适当的 Python 类型来清理数据:

>>> f = ContactForm({'subject': 'Hello', 'email': 'adrian@example.com', 
'message': 'Nice site!'}) 
>>> f.is_valid() True 
>>> f.cleaned_data 
{'message': 'Nice site!', 'email': 'adrian@example.com', 'subject': 
'Hello'} 

我们的联系表单只处理字符串,这些字符串被“清理”为字符串对象-但是,如果我们使用IntegerFieldDateField,表单框架将确保cleaned_data使用适当的 Python 整数或datetime.date对象来表示给定字段。

将表单对象与视图绑定

除非我们有一种方法将其显示给用户,否则我们的联系表单对我们来说没有太大用处。为此,我们首先需要更新我们的mysite/views

# views.py 

from django.shortcuts import render 
from mysite.forms import ContactForm 
from django.http import HttpResponseRedirect 
from django.core.mail import send_mail 

# ... 

def contact(request): 
    if request.method == 'POST': 
        form = ContactForm(request.POST) 
        if form.is_valid(): 
            cd = form.cleaned_data 
            send_mail( 
                cd['subject'], 
                cd['message'], 
                cd.get('email', 'noreply@example.com'), 
                ['siteowner@example.com'], 
            ) 
            return HttpResponseRedirect('/contact/thanks/') 
    else: 
        form = ContactForm() 
    return render(request, 'contact_form.html', {'form': form}) 

接下来,我们必须创建我们的联系表单(保存到mysite/templates):

# 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> 
        {% csrf_token %} 
        <input type="submit" value="Submit"> 
    </form> 
</body> 
</html> 

最后,我们需要更改我们的urls.py,以便在/contact/处显示我们的联系表单:

 # ... 
from mysite.views import hello, current_datetime, hours_ahead, contact 

 urlpatterns = [ 

     # ... 

     url(r'^contact/$', contact), 
] 

由于我们正在创建一个POST表单(可能会导致修改数据的效果),我们需要担心跨站点请求伪造。幸运的是,您不必太担心,因为 Django 带有一个非常易于使用的系统来防止它。简而言之,所有针对内部 URL 的POST表单都应使用{% csrf_token %}模板标记。更多细节

{% csrf_token %}可以在第十九章Django 中的安全性中找到。

尝试在本地运行此代码。加载表单,提交表单时没有填写任何字段,使用无效的电子邮件地址提交表单,最后使用有效数据提交表单。(当调用send_mail()时,除非您配置了邮件服务器,否则会收到ConnectionRefusedError。)

更改字段呈现方式

当您在本地呈现此表单时,您可能首先注意到的是message字段显示为<input type="text">,而应该是<textarea>。我们可以通过设置字段的小部件来解决这个问题:

from django import forms 

class ContactForm(forms.Form): 
    subject = forms.CharField() 
    email = forms.EmailField(required=False) 
    message = forms.CharField(widget=forms.Textarea)

表单框架将每个字段的呈现逻辑分离为一组小部件。每种字段类型都有一个默认小部件,但您可以轻松地覆盖默认值,或者提供自定义小部件。将Field类视为验证逻辑,而小部件表示呈现逻辑

设置最大长度

最常见的验证需求之一是检查字段的大小。为了好玩,我们应该改进我们的ContactForm以将subject限制为 100 个字符。要做到这一点,只需向CharField提供max_length,如下所示:

from django import forms 

class ContactForm(forms.Form): 
    subject = forms.CharField(max_length=100) 
    email = forms.EmailField(required=False) 
    message = forms.CharField(widget=forms.Textarea) 

还可以使用可选的min_length参数。

设置初始值

作为对这个表单的改进,让我们为subject字段添加一个初始值:I love your site!(一点点建议的力量不会有害)。为此,我们可以在创建Form实例时使用initial参数:

def contact(request): 
    if request.method == 'POST': 
        form = ContactForm(request.POST) 
        if form.is_valid(): 
            cd = form.cleaned_data 
            send_mail( 
                cd['subject'], 
                cd['message'], 
                cd.get('email', 'noreply@example.com'), 
['siteowner@example.com'], 
            ) 
            return HttpResponseRedirect('/contact/thanks/') 
    else: 
        form = ContactForm( 
            initial={'subject': 'I love your site!'} 
        ) 
    return render(request, 'contact_form.html', {'form':form}) 

现在,subject字段将显示为预填充了这种陈述。请注意,传递初始数据和绑定表单的数据之间存在差异。最大的区别在于,如果你只是传递初始数据,那么表单将是未绑定的,这意味着它不会有任何错误消息。

自定义验证规则

想象一下,我们已经推出了我们的反馈表单,电子邮件已经开始涌入。只有一个问题:一些提交的消息只有一两个单词,这对我们来说不够长。我们决定采用一个新的验证策略:请至少四个单词。

有许多方法可以将自定义验证集成到 Django 表单中。如果我们的规则是我们将一遍又一遍地重用的,我们可以创建一个自定义字段类型。大多数自定义验证都是一次性的事务,可以直接绑定到Form类。我们想要在message字段上进行额外的验证,因此我们在Form类中添加了一个clean_message()方法:

from django import forms 

class ContactForm(forms.Form): 
    subject = forms.CharField(max_length=100) 
    email = forms.EmailField(required=False) 
    message = forms.CharField(widget=forms.Textarea) 

    def clean_message(self): 
 message = self.cleaned_data['message'] 
 num_words = len(message.split()) 
 if num_words < 4: 
 raise forms.ValidationError("Not enough words!") 
 return message

Django 的表单系统会自动查找任何以clean_开头并以字段名称结尾的方法。如果存在这样的方法,它将在验证期间被调用。具体来说,clean_message()方法将在给定字段的默认验证逻辑之后被调用(在本例中,是必需的CharField的验证逻辑)。

因为字段数据已经部分处理,我们从self.cleaned_data中提取它。此外,我们不必担心检查该值是否存在且非空;这是默认验证器完成的。我们天真地使用len()split()的组合来计算单词的数量。如果用户输入的单词太少,我们会引发一个forms.ValidationError

附加到此异常的字符串将显示为错误列表中的一项。重要的是我们明确地在方法的最后返回字段的清理值。这允许我们在自定义验证方法中修改值(或将其转换为不同的 Python 类型)。如果我们忘记了返回语句,那么将返回None,并且原始值将丢失。

指定标签

默认情况下,Django 自动生成的表单 HTML 上的标签是通过用空格替换下划线并大写第一个字母来创建的-因此email字段的标签是"Email"。(听起来熟悉吗?这是 Django 模型用于计算字段默认verbose_name值的相同简单算法。我们在第四章中介绍过这一点,模型)。但是,与 Django 的模型一样,我们可以自定义给定字段的标签。只需使用label,如下所示:

class ContactForm(forms.Form): 
    subject = forms.CharField(max_length=100) 
 email = forms.EmailField(required=False,
        label='Your e-mail address') 
    message = forms.CharField(widget=forms.Textarea)

自定义表单设计

我们的contact_form.html模板使用{{ form.as_table }}来显示表单,但我们可以以其他方式显示表单,以便更精细地控制显示。自定义表单的呈现方式最快的方法是使用 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 }}和其他方法在开发应用程序时是有用的快捷方式,但表单的显示方式可以被覆盖,主要是在模板本身内部,您可能会发现自己这样做。

每个字段的小部件(<input type="text"><select><textarea>等)可以通过在模板中访问{{ form.fieldname }}来单独呈现,并且与字段相关的任何错误都可以作为{{ form.fieldname.errors }}获得。

考虑到这一点,我们可以使用以下模板代码为我们的联系表单构建一个自定义模板:

<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">Your e-mail address:</label> 
            {{ form.email }} 
        </div> 
        <div class="field"> 
            {{ form.message.errors }} 
            <label for="id_message">Message:</label> 
            {{ form.message }} 
        </div> 
        {% csrf_token %} 
        <input type="submit" value="Submit"> 
    </form> 
</body> 
</html> 

如果存在错误,{{ form.message.errors }}会显示一个<ul class="errorlist">,如果字段有效(或表单未绑定),则显示一个空字符串。我们还可以将form.message.errors视为布尔值,甚至可以将其作为列表进行迭代。例如:

<div class="field{% if form.message.errors %} errors{% endif %}"> 
    {% if form.message.errors %} 
        <ul> 
        {% for error in form.message.errors %} 
            <li><strong>{{ error }}</strong></li> 
        {% endfor %} 
        </ul> 
    {% endif %} 
    <label for="id_message">Message:</label> 
    {{ form.message }} 
</div> 

在验证错误的情况下,这将在包含的<div>中添加一个“errors”类,并在无序列表中显示错误列表。

接下来呢?

本章结束了本书的介绍性材料-所谓的核心课程 本书的下一部分,第七章,高级视图和 URLconfs,到第十三章,部署 Django,将更详细地介绍高级 Django 用法,包括如何部署 Django 应用程序(第十三章,部署 Django)。在这七章之后,你应该已经了解足够的知识来开始编写自己的 Django 项目。本书中的其余材料将帮助您填补需要的空白。我们将从第七章开始,高级视图和 URLconfs,通过回顾并更仔细地查看视图和 URLconfs(首次介绍于第二章,视图和 URLconfs)。

第七章:高级视图和 URLconfs

在第二章视图和 URLconfs中,我们解释了 Django 的视图函数和 URLconfs 的基础知识。本章将更详细地介绍框架中这两个部分的高级功能。

URLconf 提示和技巧

URLconfs 没有什么特别的-就像 Django 中的其他任何东西一样,它们只是 Python 代码。您可以以几种方式利用这一点,如下面的部分所述。

简化函数导入

考虑这个 URLconf,它基于第二章视图和 URLconfs中的示例构建:

from django.conf.urls import include, url 
from django.contrib import admin 
from mysite.views import hello, current_datetime, hours_ahead 

urlpatterns = [ 
      url(r'^admin/', include(admin.site.urls)), 
      url(r'^hello/$', hello), 
      url(r'^time/$', current_datetime), 
      url(r'^time/plus/(\d{1,2})/$', hours_ahead), 
      ] 

如第二章视图和 URLconfs中所述,URLconf 中的每个条目都包括其关联的视图函数,直接作为函数对象传递。这意味着需要在模块顶部导入视图函数。

但是随着 Django 应用程序的复杂性增加,其 URLconf 也会增加,并且保持这些导入可能很繁琐。 (对于每个新的视图函数,您必须记住导入它,并且如果使用这种方法,导入语句往往会变得过长。)

可以通过导入views模块本身来避免这种单调。这个示例 URLconf 等同于前一个:

from django.conf.urls import include, url 
from . import views 

urlpatterns = [ 
         url(r'^hello/$', views.hello), 
         url(r'^time/$', views.current_datetime), 
         url(r'^time/plus/(d{1,2})/$', views.hours_ahead), 
] 

在调试模式下特殊处理 URL

说到动态构建urlpatterns,您可能希望利用这种技术来在 Django 的调试模式下更改 URLconf 的行为。为此,只需在运行时检查DEBUG设置的值,如下所示:

from django.conf import settings 
from django.conf.urls import url 
from . import views 

urlpatterns = [ 
    url(r'^$', views.homepage), 
    url(r'^(\d{4})/([a-z]{3})/$', views.archive_month), 
] 

if settings.DEBUG: 
 urlpatterns += [url(r'^debuginfo/$', views.debug),]

在这个例子中,只有当您的DEBUG设置为True时,URL/debuginfo/才可用。

命名组预览

上面的示例使用简单的非命名正则表达式组(通过括号)来捕获 URL 的部分并将它们作为位置参数传递给视图。

在更高级的用法中,可以使用命名的正则表达式组来捕获 URL 部分并将它们作为关键字参数传递给视图。

在 Python 正则表达式中,命名正则表达式组的语法是(?P<name>pattern),其中name是组的名称,pattern是要匹配的某个模式。

例如,假设我们在我们的书籍网站上有一系列书评,并且我们想要检索特定日期或日期范围的书评。

这是一个示例 URLconf:

from django.conf.urls import url 

from . import views 

urlpatterns = [ 
    url(r'^reviews/2003/$', views.special_case_2003), 
    url(r'^reviews/([0-9]{4})/$', views.year_archive), 
    url(r'^reviews/([0-9]{4})/([0-9]{2})/$', views.month_archive), 
    url(r'^reviews/([0-9]{4})/([0-9]{2})/([0-9]+)/$', views.review_detail), 
] 

提示

注意:

要从 URL 中捕获一个值,只需在其周围加括号。不需要添加一个前导斜杠,因为每个 URL 都有。例如,它是^reviews,而不是^/reviews

每个正则表达式字符串前面的'r'是可选的,但建议使用。它告诉 Python 字符串是原始的,字符串中的任何内容都不应该被转义。

示例请求:

  • /reviews/2005/03/的请求将匹配列表中的第三个条目。Django 将调用函数views.month_archive(request,``'2005',``'03')

  • /reviews/2005/3/不会匹配任何 URL 模式,因为列表中的第三个条目要求月份需要两位数字。

  • /reviews/2003/将匹配列表中的第一个模式,而不是第二个模式,因为模式是按顺序测试的,第一个模式是第一个通过的测试。可以随意利用排序来插入这样的特殊情况。

  • /reviews/2003不会匹配这些模式中的任何一个,因为每个模式都要求 URL 以斜杠结尾。

  • /reviews/2003/03/03/将匹配最终模式。Django 将调用函数views.review_detail(request,``'2003',``'03',``'03')

以下是上面的示例 URLconf,重写以使用命名组:

from django.conf.urls import url 

from . import views 

urlpatterns = [ 
    url(r'^reviews/2003/$', views.special_case_2003), 
    url(r'^reviews/(?P<year>[0-9]{4})/$', views.year_archive), 
    url(r'^reviews/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.month_archive), 
    url(r'^reviews/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/(?P<day>[0-9]{2})/$', views.review_detail), 
] 

这与前面的示例完全相同,只有一个细微的区别:捕获的值作为关键字参数传递给视图函数,而不是作为位置参数。例如:

  • /reviews/2005/03/的请求将调用函数views.month_archive(request,``year='2005',``month='03'),而不是views.month_archive(request,``'2005',``'03')

  • /reviews/2003/03/03/的请求将调用函数views.review_detail(request,``year='2003',``month='03',``day='03')

实际上,这意味着您的 URLconf 更加明确,不太容易出现参数顺序错误-您可以重新排列视图函数定义中的参数。当然,这些好处是以简洁为代价的;一些开发人员认为命名组语法难看且过于冗长。

匹配/分组算法

以下是 URLconf 解析器遵循的算法,关于正则表达式中的命名组与非命名组:

  1. 如果有任何命名参数,它将使用这些参数,忽略非命名参数。

  2. 否则,它将把所有非命名参数作为位置参数传递。

在这两种情况下,任何给定的额外关键字参数也将传递给视图。

URLconf 搜索的内容

URLconf 会针对请求的 URL 进行搜索,作为普通的 Python 字符串。这不包括GETPOST参数,也不包括域名。例如,在对http://www.example.com/myapp/的请求中,URLconf 将查找myapp/。在对http://www.example.com/myapp/?page=3的请求中,URLconf 将查找myapp/。URLconf 不会查看请求方法。换句话说,所有请求方法-POSTGETHEAD等等-都将被路由到相同的函数以处理相同的 URL。

捕获的参数始终是字符串

每个捕获的参数都作为普通的 Python 字符串发送到视图中,无论正则表达式的匹配类型如何。例如,在这个 URLconf 行中:

url(r'^reviews/(?P<year>[0-9]{4})/$', views.year_archive), 

...views.year_archive()year参数将是一个字符串,而不是一个整数,即使[0-9]{4}只匹配整数字符串。

指定视图参数的默认值

一个方便的技巧是为视图的参数指定默认参数。以下是一个示例 URLconf:

# URLconf 
from django.conf.urls import url 

from . import views 

urlpatterns = [ 
    url(r'^reviews/$', views.page), 
    url(r'^reviews/page(?P<num>[0-9]+)/$', views.page), 
] 

# View (in reviews/views.py) 
def page(request, num="1"): 
    # Output the appropriate page of review entries, according to num. 
    ... 

在上面的示例中,两个 URL 模式都指向相同的视图-views.page-但第一个模式不会从 URL 中捕获任何内容。如果第一个模式匹配,page()函数将使用其默认参数num,即"1"。如果第二个模式匹配,page()将使用正则表达式捕获的num值。

注意

关键字参数 vs. 位置参数

Python 函数可以使用关键字参数或位置参数调用-在某些情况下,两者同时使用。在关键字参数调用中,您指定要传递的参数的名称以及值。在位置参数调用中,您只需传递参数,而不明确指定哪个参数匹配哪个值;关联是在参数的顺序中隐含的。例如,考虑这个简单的函数:

def sell(item, price, quantity): print "以%s 的价格出售%s 个单位的%s" % (quantity, item, price)

要使用位置参数调用它,您需要按照函数定义中列出的顺序指定参数:sell('Socks', '$2.50', 6)

要使用关键字参数调用它,您需要指定参数的名称以及值。以下语句是等效的:sell(item='Socks', price='$2.50', quantity=6) sell(item='Socks', quantity=6, price='$2.50') sell(price='$2.50', item='Socks', quantity=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', quantity=6) sell('Socks', quantity=6, price='$2.50')

性能

urlpatterns中的每个正则表达式在第一次访问时都会被编译。这使得系统运行非常快。

错误处理

当 Django 找不到与请求的 URL 匹配的正则表达式,或者当引发异常时,Django 将调用一个错误处理视图。用于这些情况的视图由四个变量指定。这些变量是:

  • handler404

  • handler500

  • handler403

  • handler400

它们的默认值对于大多数项目应该足够了,但可以通过为它们分配值来进一步定制。这些值可以在您的根 URLconf 中设置。在任何其他 URLconf 中设置这些变量都不会产生效果。值必须是可调用的,或者是表示应该被调用以处理当前错误条件的视图的完整 Python 导入路径的字符串。

包含其他 URLconfs

在任何时候,您的 urlpatterns 可以包括其他 URLconf 模块。这实质上将一组 URL 根据其他 URL 的下方。例如,这是 Django 网站本身的 URLconf 的摘录。它包括许多其他 URLconfs:

from django.conf.urls import include, url 

urlpatterns = [ 
    # ... 
    url(r'^community/', include('django_website.aggregator.urls')), 
    url(r'^contact/', include('django_website.contact.urls')), 
    # ... 
] 

请注意,此示例中的正则表达式没有 $(字符串结束匹配字符),但包括一个尾随斜杠。每当 Django 遇到 include() 时,它会截掉到目前为止匹配的 URL 的任何部分,并将剩余的字符串发送到包含的 URLconf 进行进一步处理。另一个可能性是通过使用 url() 实例的列表来包含其他 URL 模式。例如,考虑这个 URLconf:

from django.conf.urls import include, url 
from apps.main import views as main_views 
from credit import views as credit_views 

extra_patterns = [ 
    url(r'^reports/(?P<id>[0-9]+)/$', credit_views.report), 
    url(r'^charge/$', credit_views.charge), 
] 

urlpatterns = [ 
    url(r'^$', main_views.homepage), 
    url(r'^help/', include('apps.help.urls')), 
    url(r'^credit/', include(extra_patterns)), 
] 

在这个例子中,/credit/reports/ URL 将由 credit.views.report() Django 视图处理。这可以用来消除 URLconfs 中重复使用单个模式前缀的冗余。例如,考虑这个 URLconf:

from django.conf.urls import url 
from . import views 

urlpatterns = [ 
    url(r'^(?P<page_slug>\w+)-(?P<page_id>\w+)/history/$',   
        views.history), 
    url(r'^(?P<page_slug>\w+)-(?P<page_id>\w+)/edit/$', views.edit), 
    url(r'^(?P<page_slug>\w+)-(?P<page_id>\w+)/discuss/$',   
        views.discuss), 
    url(r'^(?P<page_slug>\w+)-(?P<page_id>\w+)/permissions/$',  
        views.permissions), 
] 

我们可以通过仅声明共同的路径前缀一次并分组不同的后缀来改进这一点:

from django.conf.urls import include, url 
from . import views 

urlpatterns = [ 
    url(r'^(?P<page_slug>\w+)-(?P<page_id>\w+)/',  
        include([ 
        url(r'^history/$', views.history), 
        url(r'^edit/$', views.edit), 
        url(r'^discuss/$', views.discuss), 
        url(r'^permissions/$', views.permissions), 
        ])), 
] 

捕获的参数

包含的 URLconf 会接收来自父 URLconfs 的任何捕获的参数,因此以下示例是有效的:

# In settings/urls/main.py 
from django.conf.urls import include, url 

urlpatterns = [ 
    url(r'^(?P<username>\w+)/reviews/', include('foo.urls.reviews')), 
] 

# In foo/urls/reviews.py 
from django.conf.urls import url 
from . import views 

urlpatterns = [ 
    url(r'^$', views.reviews.index), 
    url(r'^archive/$', views.reviews.archive), 
] 

在上面的示例中,捕获的 "username" 变量如预期地传递给了包含的 URLconf。

向视图函数传递额外选项

URLconfs 具有一个钩子,可以让您将额外的参数作为 Python 字典传递给视图函数。django.conf.urls.url() 函数可以接受一个可选的第三个参数,应该是一个额外关键字参数的字典,用于传递给视图函数。例如:

from django.conf.urls import url 
from . import views 

urlpatterns = [ 
    url(r'^reviews/(?P<year>[0-9]{4})/$',  
        views.year_archive,  
        {'foo': 'bar'}), 
] 

在这个例子中,对于对 /reviews/2005/ 的请求,Django 将调用 views.year_archive(request, year='2005', foo='bar')。这种技术在辅助框架中用于向视图传递元数据和选项(参见第十四章,“生成非 HTML 内容”)。

注意

处理冲突

可能会有一个 URL 模式,它捕获了命名的关键字参数,并且还在其额外参数的字典中传递了相同名称的参数。当这种情况发生时,字典中的参数将被用于替代 URL 中捕获的参数。

向 include() 传递额外的选项

同样,您可以向 include() 传递额外的选项。当您向 include() 传递额外的选项时,包含的 URLconf 中的每一行都将传递额外的选项。例如,这两个 URLconf 集是功能上相同的:集合一:

# main.py 
from django.conf.urls import include, url 

urlpatterns = [ 
    url(r'^reviews/', include('inner'), {'reviewid': 3}), 
] 

# inner.py 
from django.conf.urls import url 
from mysite import views 

urlpatterns = [ 
    url(r'^archive/$', views.archive), 
    url(r'^about/$', views.about), 
] 

集合二:

# main.py 
from django.conf.urls import include, url 
from mysite import views 

urlpatterns = [ 
    url(r'^reviews/', include('inner')), 
] 

# inner.py 
from django.conf.urls import url 

urlpatterns = [ 
    url(r'^archive/$', views.archive, {'reviewid': 3}), 
    url(r'^about/$', views.about, {'reviewid': 3}), 
] 

请注意,无论包含的 URLconf 中的视图是否实际接受这些选项作为有效选项,额外的选项都将始终传递给包含的 URLconf 中的每一行。因此,只有在您确定包含的 URLconf 中的每个视图都接受您传递的额外选项时,这种技术才有用。

URL 的反向解析

在开发 Django 项目时通常需要的是获取 URL 的最终形式,无论是用于嵌入生成的内容(视图和资源 URL,向用户显示的 URL 等)还是用于服务器端的导航流程处理(重定向等)

强烈建议避免硬编码这些 URL(一种费力、不可扩展和容易出错的策略)或者不得不设计专门的机制来生成与 URLconf 描述的设计并行的 URL,因此有可能在某个时刻产生过时的 URL。换句话说,需要的是一种 DRY 机制。

除了其他优点,它还允许 URL 设计的演变,而无需在整个项目源代码中搜索和替换过时的 URL。我们可以作为获取 URL 的起点的信息是处理它的视图的标识(例如名称),必须参与查找正确 URL 的其他信息是视图参数的类型(位置,关键字)和值。

Django 提供了一种解决方案,即 URL 映射器是 URL 设计的唯一存储库。您可以用 URLconf 提供给它,然后可以在两个方向上使用它:

  • 从用户/浏览器请求的 URL 开始,它调用正确的 Django 视图,并提供可能需要的任何参数及其值,这些值是从 URL 中提取的。

  • 从对应的 Django 视图的标识开始,以及将传递给它的参数的值,获取相关联的 URL。

第一个是我们在前几节中讨论的用法。第二个是所谓的URL 的反向解析反向 URL 匹配反向 URL 查找或简称URL 反转

Django 提供了执行 URL 反转的工具,这些工具与需要 URL 的不同层次匹配:

  • 在模板中:使用url模板标签。

  • 在 Python 代码中:使用django.core.urlresolvers.reverse()函数。

  • 与 Django 模型实例的 URL 处理相关的高级代码:get_absolute_url()方法。

示例

再次考虑这个 URLconf 条目:

from django.conf.urls import url 
from . import views 

urlpatterns = [ 
    #... 
    url(r'^reviews/([0-9]{4})/$', views.year_archive,  
        name='reviews-year-archive'), 
    #... 
] 

根据这个设计,对应于年份nnnn的存档的 URL 是/reviews/nnnn/。您可以通过在模板代码中使用以下方式来获取这些:

<a href="{% url 'reviews-year-archive' 2012 %}">2012 Archive</a> 
{# Or with the year in a template context variable: #} 

<ul> 
{% for yearvar in year_list %} 
<li><a href="{% url 'reviews-year-archive' yearvar %}">{{ yearvar }} Archive</a></li> 
{% endfor %} 
</ul> 

或者在 Python 代码中:

from django.core.urlresolvers import reverse 
from django.http import HttpResponseRedirect 

def redirect_to_year(request): 
    # ... 
    year = 2012 
    # ... 
    return HttpResponseRedirect(reverse('reviews-year-archive', args=(year,))) 

如果出于某种原因,决定更改发布年度审查存档内容的 URL,则只需要更改 URLconf 中的条目。在某些情况下,如果视图具有通用性质,则 URL 和视图之间可能存在多对一的关系。对于这些情况,当需要反转 URL 时,视图名称并不是足够好的标识符。阅读下一节以了解 Django 为此提供的解决方案。

命名 URL 模式

为了执行 URL 反转,您需要使用上面示例中所做的命名 URL 模式。用于 URL 名称的字符串可以包含任何您喜欢的字符。您不受限于有效的 Python 名称。当您命名您的 URL 模式时,请确保使用不太可能与任何其他应用程序选择的名称冲突的名称。如果您称呼您的 URL 模式为comment,另一个应用程序也这样做,那么当您使用这个名称时,无法保证将插入哪个 URL 到您的模板中。在您的 URL 名称上加上前缀,可能来自应用程序名称,将减少冲突的机会。我们建议使用myapp-comment而不是comment之类的东西。

URL 命名空间

URL 命名空间允许您唯一地反转命名的 URL 模式,即使不同的应用程序使用相同的 URL 名称。对于第三方应用程序来说,始终使用命名空间 URL 是一个好习惯。同样,它还允许您在部署多个应用程序实例时反转 URL。换句话说,由于单个应用程序的多个实例将共享命名的 URL,命名空间提供了一种区分这些命名的 URL 的方法。

正确使用 URL 命名空间的 Django 应用程序可以针对特定站点部署多次。例如,django.contrib.admin有一个AdminSite类,允许您轻松部署多个管理员实例。URL 命名空间由两部分组成,两者都是字符串:

  1. 应用程序命名空间:描述正在部署的应用程序的名称。单个应用程序的每个实例都将具有相同的应用程序命名空间。例如,Django 的管理员应用程序具有相对可预测的应用程序命名空间admin

  2. 实例命名空间:标识应用程序的特定实例。实例命名空间应该在整个项目中是唯一的。但是,实例命名空间可以与应用程序命名空间相同。这用于指定应用程序的默认实例。例如,默认的 Django 管理员实例具有admin的实例命名空间。

使用:运算符指定命名空间 URL。例如,管理员应用程序的主索引页面使用"admin:index"引用。这表示命名空间为"admin",命名为"index"。

命名空间也可以是嵌套的。命名为members:reviews:index的 URL 将在顶级命名空间members中查找名为"index"的模式。

反转命名空间 URL

在给定要解析的命名空间 URL(例如"reviews:index")时,Django 将完全限定的名称分成部分,然后尝试以下查找:

  1. 首先,Django 会查找匹配的应用程序命名空间(在本例中为reviews)。这将产生该应用程序的实例列表。

  2. 如果定义了当前应用程序,Django 会查找并返回该实例的 URL 解析器。当前应用程序可以作为请求的属性指定。期望有多个部署的应用程序应该在正在处理的请求上设置current_app属性。

  3. 当前应用程序也可以作为reverse()函数的参数手动指定。

  4. 如果没有当前应用程序。 Django 将寻找默认的应用程序实例。默认的应用程序实例是具有与应用程序命名空间匹配的实例命名空间的实例(在本例中,称为"reviews"的 reviews 的实例)。

  5. 如果没有默认的应用程序实例,Django 将选择应用程序的最后部署实例,无论其实例名称是什么。

  6. 如果提供的命名空间与第 1 步中的应用程序命名空间不匹配,Django 将尝试直接查找该命名空间作为实例命名空间。

如果有嵌套的命名空间,这些步骤将针对命名空间的每个部分重复,直到只剩下视图名称未解析。然后,视图名称将被解析为在找到的命名空间中的 URL。

URL 命名空间和包含的 URLconfs

包含的 URLconfs 的 URL 命名空间可以通过两种方式指定。首先,当构建 URL 模式时,您可以将应用程序和实例命名空间作为参数提供给include()。例如:

url(r'^reviews/', include('reviews.urls', namespace='author-reviews', 
    app_name='reviews')), 

这将包括在应用程序命名空间'reviews'中定义的 URL,实例命名空间为'author-reviews'。其次,您可以包含包含嵌入式命名空间数据的对象。如果您包含一个url()实例列表,那么该对象中包含的 URL 将被添加到全局命名空间中。但是,您也可以包含一个包含 3 个元素的元组:

(<list of url() instances>, <application namespace>, <instance namespace>) 

例如:

from django.conf.urls import include, url 

from . import views 

reviews_patterns = [ 
    url(r'^$', views.IndexView.as_view(), name='index'), 
    url(r'^(?P<pk>\d+)/$', views.DetailView.as_view(), name='detail'),  
] 

url(r'^reviews/', include((reviews_patterns, 'reviews', 
    'author-reviews'))), 

这将把提名的 URL 模式包含到给定的应用程序和实例命名空间中。例如,Django 管理界面被部署为AdminSite的实例。AdminSite对象有一个urls属性:一个包含相应管理站点中所有模式的 3 元组,加上应用程序命名空间"admin"和管理实例的名称。当你部署一个管理实例时,就是这个urls属性被include()到你的项目urlpatterns中。

一定要向include()传递一个元组。如果你只是简单地传递三个参数:include(reviews_patterns,'reviews','author-reviews'),Django 不会报错,但由于include()的签名,'reviews'将成为实例命名空间,'author-reviews'将成为应用程序命名空间,而不是相反。

接下来呢?

本章提供了许多关于视图和 URLconfs 的高级技巧。接下来,在第八章高级模板中,我们将对 Django 的模板系统进行高级处理。

第八章:高级模板

尽管你与 Django 的模板语言的大部分交互将是作为模板作者的角色,但你可能想要自定义和扩展模板引擎-要么使其执行一些它尚未执行的操作,要么以其他方式使你的工作更轻松。

本章深入探讨了 Django 模板系统的内部。它涵盖了如果你计划扩展系统或者只是对它的工作方式感到好奇,你需要了解的内容。它还涵盖了自动转义功能,这是一项安全措施,随着你继续使用 Django,你肯定会注意到它。

模板语言回顾

首先,让我们快速回顾一些在第三章模板中引入的术语:

  • 模板是一个文本文档,或者是一个普通的 Python 字符串,使用 Django 模板语言进行标记。模板可以包含模板标签和变量。

  • 模板标签是模板中的一个符号,它执行某些操作。这个定义是故意模糊的。例如,模板标签可以生成内容,充当控制结构(if语句或for循环),从数据库中获取内容,或者启用对其他模板标签的访问。

模板标签用{%%}括起来:

        {% if is_logged_in %} 
            Thanks for logging in! 
        {% else %} 
            Please log in. 
        {% endif %} 

  • 变量是模板中输出值的符号。

  • 变量标签用{{}}括起来:

  • 上下文是传递给模板的name->value映射(类似于 Python 字典)。

  • 模板通过用上下文中的值替换变量“洞”并执行所有模板标签来渲染上下文。

有关这些术语的基础知识的更多细节,请参考第三章模板。本章的其余部分讨论了扩展模板引擎的方法。不过,首先让我们简要地看一下第三章模板中省略的一些内部内容,以简化。

RequestContext 和上下文处理器

在渲染模板时,你需要一个上下文。这可以是django.template.Context的一个实例,但 Django 也带有一个子类django.template.RequestContext,它的行为略有不同。

RequestContext默认情况下向您的模板上下文添加了一堆变量-诸如HttpRequest对象或有关当前登录用户的信息。

render()快捷方式会创建一个RequestContext,除非显式传递了不同的上下文实例。例如,考虑这两个视图:

from django.template import loader, Context 

def view_1(request): 
    # ... 
    t = loader.get_template('template1.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('template2.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()的快捷方式-我们手动加载模板,构建上下文对象并渲染模板。我们为了清晰起见,详细说明了所有步骤。)

每个视图都传递相同的三个变量-appuserip_address-到它的模板。如果我们能够消除这种冗余,那不是很好吗?RequestContext和上下文处理器被创建来解决这个问题。上下文处理器允许您指定一些变量,这些变量在每个上下文中自动设置-而无需在每个render()调用中指定这些变量。

问题在于,当你渲染模板时,你必须使用RequestContext而不是Context。使用上下文处理器的最低级别方法是创建一些处理器并将它们传递给RequestContext。以下是如何使用上下文处理器编写上面的示例:

from django.template import loader, RequestContext 

def custom_proc(request): 
    # A context processor that provides '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('template1.html') 
    c = RequestContext(request,  
                       {'message': 'I am view 1.'},   
                       processors=[custom_proc]) 
    return t.render(c) 

def view_2(request): 
    # ... 
    t = loader.get_template('template2.html') 
    c = RequestContext(request,  
                       {'message': 'I am the second view.'},   
                       processors=[custom_proc]) 
    return t.render(c) 

让我们逐步了解这段代码:

  • 首先,我们定义一个函数custom_proc。这是一个上下文处理器-它接受一个HttpRequest对象,并返回一个要在模板上下文中使用的变量字典。就是这样。

  • 我们已将两个视图函数更改为使用RequestContext而不是Context。上下文构造方式有两个不同之处。首先,RequestContext要求第一个参数是一个HttpRequest对象-首先传递到视图函数中的对象(request)。其次,RequestContext需要一个可选的processors参数,它是要使用的上下文处理器函数的列表或元组。在这里,我们传入custom_proc,我们上面定义的自定义处理器。

  • 每个视图不再必须在其上下文构造中包含appuserip_address,因为这些由custom_proc提供。

  • 每个视图仍然具有灵活性,可以引入任何可能需要的自定义模板变量。在此示例中,message模板变量在每个视图中设置不同。

在第三章模板中,我介绍了render()快捷方式,它使您无需调用loader.get_template(),然后创建一个Context,然后在模板上调用render()方法。

为了演示上下文处理器的较低级别工作,上面的示例没有使用render()。但是,使用render()与上下文处理器是可能的,也是更好的。可以使用context_instance参数来实现这一点,如下所示:

from django.shortcuts import render 
from django.template import RequestContext 

def custom_proc(request): 
    # A context processor that provides 'app', 'user' and 'ip_address'. 
    return { 
        'app': 'My app', 
        'user': request.user, 
        'ip_address': request.META['REMOTE_ADDR'] 
    } 

def view_1(request): 
    # ... 
    return render(request, 'template1.html', 
                  {'message': 'I am view 1.'}, 
                  context_instance=RequestContext( 
                  request, processors=[custom_proc] 
                  ) 
    ) 

def view_2(request): 
    # ... 
    return render(request, 'template2.html',                  {'message': 'I am the second view.'}, 
                  context_instance=RequestContext( 
                  request, processors=[custom_proc] 
                  ) 
) 

在这里,我们已将每个视图的模板渲染代码简化为单个(包装)行。这是一个改进,但是,评估这段代码的简洁性时,我们必须承认我们现在几乎过度使用了另一端的频谱。我们消除了数据中的冗余(我们的模板变量),但增加了代码中的冗余(在processors调用中)。

如果您必须一直输入processors,使用上下文处理器并不能节省太多输入。因此,Django 提供了全局上下文处理器的支持。context_processors设置(在您的settings.py中)指定应始终应用于RequestContext的上下文处理器。这样可以避免每次使用RequestContext时都需要指定processors

默认情况下,context_processors设置如下:

'context_processors': [ 
            'django.template.context_processors.debug', 
            'django.template.context_processors.request', 
            'django.contrib.auth.context_processors.auth', 
'django.contrib.messages.context_processors.messages', 
        ], 

此设置是一个可调用对象的列表,其接口与上面的custom_proc函数相同-接受请求对象作为其参数,并返回要合并到上下文中的项目的字典。请注意,context_processors中的值被指定为字符串,这意味着处理器必须在 Python 路径的某个位置(因此您可以从设置中引用它们)。

每个处理器都按顺序应用。也就是说,如果一个处理器向上下文添加一个变量,并且第二个处理器使用相同的名称添加一个变量,则第二个处理器将覆盖第一个处理器。Django 提供了许多简单的上下文处理器,包括默认启用的处理器:

auth

django.contrib.auth.context_processors.auth

如果启用了此处理器,则每个RequestContext都将包含这些变量:

  • user:表示当前登录用户的auth.User实例(或AnonymousUser实例,如果客户端未登录)。

  • perms:表示当前登录用户具有的权限的django.contrib.auth.context_processors.PermWrapper实例。

DEBUG

django.template.context_processors.debug

如果启用了此处理器,则每个RequestContext都将包含这两个变量-但仅当您的DEBUG设置为True并且请求的 IP 地址(request.META['REMOTE_ADDR'])在INTERNAL_IPS设置中时:

  • debug-True:您可以在模板中使用此选项来测试是否处于DEBUG模式。

  • sql_queries:一个{'sql': ..., 'time': ...}字典的列表,表示请求期间发生的每个 SQL 查询及其所花费的时间。列表按查询顺序生成,并在访问时惰性生成。

i18n

django.template.context_processors.i18n

如果启用了此处理器,则每个RequestContext都将包含这两个变量:

  • LANGUAGESLANGUAGES设置的值。

  • LANGUAGE_CODErequest.LANGUAGE_CODE,如果存在的话。否则,为LANGUAGE_CODE设置的值。

媒体

django.template.context_processors.media

如果启用了此处理器,每个RequestContext都将包含一个名为MEDIA_URL的变量,该变量提供MEDIA_URL设置的值。

静态

django.template.context_processors.static

如果启用了此处理器,每个RequestContext都将包含一个名为STATIC_URL的变量,该变量提供STATIC_URL设置的值。

csrf

django.template.context_processors.csrf

此处理器添加了一个csrf_token模板标记所需的令牌,以防止跨站点请求伪造(请参见第十九章,“Django 中的安全性”)。

请求

django.template.context_processors.request

如果启用了此处理器,每个RequestContext都将包含一个名为request的变量,该变量是当前的HttpRequest

消息

django.contrib.messages.context_processors.messages

如果启用了此处理器,每个RequestContext都将包含这两个变量:

  • messages:已通过消息框架设置的消息(作为字符串)的列表。

  • DEFAULT_MESSAGE_LEVELS:消息级别名称与其数值的映射。

编写自己的上下文处理器指南

上下文处理器具有非常简单的接口:它只是一个接受一个HttpRequest对象的 Python 函数,并返回一个添加到模板上下文中的字典。每个上下文处理器必须返回一个字典。以下是一些编写自己上下文处理器的提示:

  • 使每个上下文处理器负责尽可能小的功能子集。使用多个处理器很容易,因此最好将功能拆分为将来重用的逻辑片段。

  • 请记住,TEMPLATE_CONTEXT_PROCESSORS中的任何上下文处理器都将在由该设置文件提供动力的每个模板中可用,因此请尝试选择与模板可能独立使用的变量名不太可能发生冲突的变量名。由于变量名区分大小写,因此最好使用所有大写字母来表示处理器提供的变量。

  • 自定义上下文处理器可以存在于代码库中的任何位置。Django 关心的是您的自定义上下文处理器是否由TEMPLATES设置中的'context_processors'选项指向,或者如果直接使用Engine,则由Enginecontext_processors参数指向。话虽如此,惯例是将它们保存在应用程序或项目中名为context_processors.py的文件中。

自动 HTML 转义

在从模板生成 HTML 时,总是存在一个变量包含影响生成的 HTML 的字符的风险。例如,考虑这个模板片段:

Hello, {{ name }}. 

起初,这似乎是一种无害的显示用户姓名的方式,但请考虑如果用户将他的名字输入为这样会发生什么:

<script>alert('hello')</script> 

使用这个名称值,模板将被渲染为:

Hello, <script>alert('hello')</script> 

……这意味着浏览器将弹出一个 JavaScript 警报框!同样,如果名称包含'<'符号,会怎么样?

<b>username 

这将导致渲染的模板如下:

Hello, <b>username 

……这将导致网页的其余部分变粗!显然,不应盲目信任用户提交的数据并直接插入到您的网页中,因为恶意用户可能利用这种漏洞做出潜在的坏事。

这种安全漏洞称为跨站脚本(XSS)攻击。(有关安全性的更多信息,请参见第十九章,“Django 中的安全性”)。为了避免这个问题,您有两个选择:

  • 首先,您可以确保通过escape过滤器运行每个不受信任的变量,该过滤器将潜在有害的 HTML 字符转换为无害的字符。这是 Django 最初几年的默认解决方案,但问题在于它把责任放在了,开发者/模板作者身上,确保您转义了所有内容。很容易忘记转义数据。

  • 其次,您可以利用 Django 的自动 HTML 转义。本节的其余部分将描述自动转义的工作原理。

  • 在 Django 中,默认情况下,每个模板都会自动转义每个变量标签的输出。具体来说,这五个字符会被转义:

  • < 被转换为 &lt;

  • > 被转换为 &gt;

  • '(单引号)被转换为'

  • "(双引号)被转换为&quot;

  • & 被转换为 &amp;

再次强调,这种行为默认情况下是开启的。如果您使用 Django 的模板系统,您就受到了保护。

如何关闭它

如果您不希望数据在每个站点、每个模板级别或每个变量级别自动转义,可以通过多种方式关闭它。为什么要关闭它?因为有时模板变量包含您希望呈现为原始 HTML 的数据,这种情况下您不希望它们的内容被转义。

例如,您可能会在数据库中存储一大段受信任的 HTML,并希望直接将其嵌入到模板中。或者,您可能正在使用 Django 的模板系统来生成非 HTML 文本-例如电子邮件消息。

对于单个变量

要为单个变量禁用自动转义,请使用safe过滤器:

This will be escaped: {{ data }} 
This will not be escaped: {{ data|safe }} 

safe视为免受进一步转义可以安全解释为 HTML的简写。在这个例子中,如果data包含<b>,输出将是:

This will be escaped: &lt;b&gt; 
This will not be escaped: <b> 

对于模板块

要控制模板的自动转义,可以将模板(或模板的特定部分)包装在autoescape标签中,如下所示:

{% autoescape off %} 
    Hello {{ name }} 
{% endautoescape %} 

autoescape标签接受onoff作为参数。有时,您可能希望在本来被禁用自动转义的情况下强制进行自动转义。以下是一个示例模板:

Auto-escaping is on by default. Hello {{ name }} 

{% autoescape off %} 
    This will not be auto-escaped: {{ data }}. 

    Nor this: {{ other_data }} 
    {% autoescape on %} 
        Auto-escaping applies again: {{ name }} 
    {% endautoescape %} 
{% endautoescape %} 

自动转义标签会将其效果传递给扩展当前模板以及通过include标签包含的模板,就像所有块标签一样。例如:

# base.html 

{% autoescape off %} 
<h1>{% block title %}{% endblock %}</h1> 
{% block content %} 
{% endblock %} 
{% endautoescape %} 

# child.html 

{% extends "base.html" %} 
{% block title %}This & that{% endblock %} 
{% block content %}{{ greeting }}{% endblock %} 

因为基础模板中关闭了自动转义,所以在子模板中也会关闭自动转义,当greeting变量包含字符串<b>Hello!</b>时,将会产生以下渲染的 HTML:

<h1>This & that</h1> 
<b>Hello!</b> 

一般来说,模板作者不需要太担心自动转义。Python 端的开发人员(编写视图和自定义过滤器的人)需要考虑数据不应该被转义的情况,并适当标记数据,以便在模板中正常工作。

如果您正在创建一个可能在您不确定自动转义是否启用的情况下使用的模板,那么请为任何需要转义的变量添加escape过滤器。当自动转义开启时,escape过滤器不会导致数据双重转义-escape过滤器不会影响自动转义的变量。

在过滤器参数中自动转义字符串文字

正如我们之前提到的,过滤器参数可以是字符串:

{{ data|default:"This is a string literal." }} 

所有字符串文字都会被插入到模板中,而不会进行任何自动转义-它们的行为就好像它们都通过了safe过滤器。背后的原因是模板作者控制着字符串文字的内容,因此他们可以确保在编写模板时正确地转义文本。

这意味着您应该这样写

{{ data|default:"3 &lt; 2" }} 

...而不是

{{ data|default:"3 < 2" }} <== Bad! Don't do this. 

这不会影响来自变量本身的数据。变量的内容仍然会在必要时自动转义,因为它们超出了模板作者的控制。

模板加载内部

通常,您会将模板存储在文件系统中,而不是自己使用低级别的Template API。将模板保存在指定为模板目录的目录中。 Django 根据您的模板加载设置在许多地方搜索模板目录(请参阅下面的Loader 类型),但指定模板目录的最基本方法是使用DIRS选项。

DIRS 选项

通过在设置文件中的TEMPLATES设置中使用DIRS选项或在Enginedirs参数中使用DIRS选项,告诉 Django 您的模板目录是什么。这应设置为包含完整路径的字符串列表,以包含模板目录:

TEMPLATES = [ 
    { 
        'BACKEND': 'django.template.backends.django.DjangoTemplates', 
        'DIRS': [ 
            '/home/html/templates/lawrence.com', 
            '/home/html/templates/default', 
        ], 
    }, 
] 

您的模板可以放在任何您想要的地方,只要目录和模板对 Web 服务器可读。它们可以具有任何您想要的扩展名,例如.html.txt,或者它们可以根本没有扩展名。请注意,这些路径应使用 Unix 样式的正斜杠,即使在 Windows 上也是如此。

加载程序类型

默认情况下,Django 使用基于文件系统的模板加载程序,但 Django 还配备了其他几个模板加载程序,它们知道如何从其他来源加载模板;其中最常用的应用程序加载程序将在下面进行描述。

文件系统加载程序

filesystem.Loader从文件系统加载模板,根据DIRS <TEMPLATES-DIRS>。此加载程序默认启用。但是,直到您将DIRS <TEMPLATES-DIRS>设置为非空列表之前,它才能找到任何模板:

TEMPLATES = [{ 
    'BACKEND': 'django.template.backends.django.DjangoTemplates', 
    'DIRS': [os.path.join(BASE_DIR, 'templates')], 
}] 

应用程序目录加载程序

app_directories.Loader从文件系统加载 Django 应用程序的模板。对于INSTALLED_APPS中的每个应用程序,加载程序都会查找templates子目录。如果目录存在,Django 将在其中查找模板。这意味着您可以将模板与各个应用程序一起存储。这也使得很容易使用默认模板分发 Django 应用程序。例如,对于此设置:

INSTALLED_APPS = ['myproject.reviews', 'myproject.music'] 

get_template('foo.html')将按照这些顺序在这些目录中查找foo.html

  • /path/to/myproject/reviews/templates/

  • /path/to/myproject/music/templates/

并使用它找到的第一个。

INSTALLED_APPS 的顺序很重要!

例如,如果您想要自定义 Django 管理界面,您可能会选择使用自己的myproject.reviews中的admin/base_site.html覆盖标准的admin/base_site.html模板,而不是使用django.contrib.admin

然后,您必须确保myproject.reviewsINSTALLED_APPS中出现在django.contrib.admin之前,否则将首先加载django.contrib.admin,并且您的将被忽略。

请注意,加载程序在首次运行时执行优化:它缓存了具有templates子目录的INSTALLED_APPS包的列表。

您只需将APP_DIRS设置为True即可启用此加载程序:

TEMPLATES = [{ 
    'BACKEND': 'django.template.backends.django.DjangoTemplates', 
    'APP_DIRS': True, 
}] 

其他加载程序

其余的模板加载程序是:

  • django.template.loaders.eggs.Loader

  • django.template.loaders.cached.Loader

  • django.template.loaders.locmem.Loader

这些加载程序默认情况下是禁用的,但是您可以通过在TEMPLATES设置中的DjangoTemplates后端中添加loaders选项或将loaders参数传递给Engine来激活它们。有关这些高级加载程序的详细信息,以及构建自己的自定义加载程序,可以在 Django 项目网站上找到。

扩展模板系统

现在您对模板系统的内部工作有了更多了解,让我们看看如何使用自定义代码扩展系统。大多数模板定制以自定义模板标签和/或过滤器的形式出现。尽管 Django 模板语言带有许多内置标签和过滤器,但您可能会组装自己的标签和过滤器库,以满足自己的需求。幸运的是,定义自己的功能非常容易。

代码布局

自定义模板标签和过滤器必须位于 Django 应用程序中。如果它们与现有应用程序相关,将它们捆绑在那里是有意义的;否则,您应该创建一个新的应用程序来保存它们。该应用程序应该包含一个templatetags目录,与models.pyviews.py等文件处于同一级别。如果这个目录还不存在,请创建它-不要忘记__init__.py文件,以确保该目录被视为 Python 包。

添加此模块后,您需要在使用模板中的标签或过滤器之前重新启动服务器。您的自定义标签和过滤器将位于templatetags目录中的一个模块中。

模块文件的名称是您以后将用来加载标签的名称,因此要小心选择一个不会与另一个应用程序中的自定义标签和过滤器冲突的名称。

例如,如果您的自定义标签/过滤器在名为review_extras.py的文件中,您的应用程序布局可能如下所示:

reviews/ 
    __init__.py 
    models.py 
    templatetags/ 
        __init__.py 
        review_extras.py 
    views.py 

在您的模板中,您将使用以下内容:

{% load review_extras %} 

包含自定义标签的应用程序必须在INSTALLED_APPS中,以便{% load %}标签能够工作。

注意

幕后

要获取大量示例,请阅读 Django 默认过滤器和标签的源代码。它们分别位于django/template/defaultfilters.pydjango/template/defaulttags.py中。有关load标签的更多信息,请阅读其文档。

创建模板库

无论您是编写自定义标签还是过滤器,首先要做的是创建一个模板库-这是 Django 可以连接到的一小部分基础设施。

创建模板库是一个两步过程:

  • 首先,决定哪个 Django 应用程序应该包含模板库。如果您通过manage.py startapp创建了一个应用程序,您可以将其放在那里,或者您可以创建另一个仅用于模板库的应用程序。我们建议选择后者,因为您的过滤器可能对将来的项目有用。无论您选择哪种路线,请确保将应用程序添加到您的INSTALLED_APPS设置中。我马上会解释这一点。

  • 其次,在适当的 Django 应用程序包中创建一个templatetags目录。它应该与models.pyviews.py等文件处于同一级别。例如:

        books/
        __init__.py
        models.py
        templatetags/
        views.py

templatetags目录中创建两个空文件:一个__init__.py文件(表示这是一个包含 Python 代码的包)和一个包含自定义标签/过滤器定义的文件。后者的文件名是您以后将用来加载标签的名称。例如,如果您的自定义标签/过滤器在名为review_extras.py的文件中,您可以在模板中写入以下内容:

{% load review_extras %} 

{% load %}标签查看您的INSTALLED_APPS设置,并且只允许加载已安装的 Django 应用程序中的模板库。这是一个安全功能;它允许您在单台计算机上托管许多模板库的 Python 代码,而不会为每个 Django 安装启用对所有模板库的访问。

如果您编写的模板库与任何特定的模型/视图无关,那么拥有一个仅包含templatetags包的 Django 应用程序包是有效的和非常正常的。

templatetags包中放置多少模块都没有限制。只需记住,{% load %}语句将加载给定 Python 模块名称的标签/过滤器,而不是应用程序的名称。

创建了该 Python 模块后,您只需根据您是编写过滤器还是标签来编写一些 Python 代码。要成为有效的标签库,模块必须包含一个名为register的模块级变量,它是template.Library的实例。

这是所有标签和过滤器注册的数据结构。因此,在您的模块顶部附近,插入以下内容:

from django import template 
register = template.Library() 

自定义模板标签和过滤器

Django 的模板语言配备了各种内置标签和过滤器,旨在满足应用程序的呈现逻辑需求。尽管如此,您可能会发现自己需要的功能不在核心模板原语集中。

您可以通过使用 Python 定义自定义标签和过滤器来扩展模板引擎,然后使用{% load %}标签将其提供给模板。

编写自定义模板过滤器

自定义过滤器只是接受一个或两个参数的 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:"0" }} 

大多数过滤器不带参数。在这种情况下,只需在函数中省略参数。例如:

def lower(value): # Only one argument. 
    """Converts a string into all lowercase""" 
    return value.lower() 

注册自定义过滤器

编写完过滤器定义后,您需要将其注册到您的Library实例中,以使其可用于 Django 的模板语言:

register.filter('cut', cut) 
register.filter('lower', lower) 

Library.filter()方法接受两个参数:

  1. 过滤器的名称-一个字符串。

  2. 编译函数-一个 Python 函数(而不是函数的名称作为字符串)。

您可以将register.filter()用作装饰器:

@register.filter(name='cut') 
def cut(value, arg): 
    return value.replace(arg, '') 

@register.filter 
def lower(value): 
    return value.lower() 

如果省略name参数,就像上面的第二个示例一样,Django 将使用函数的名称作为过滤器名称。最后,register.filter()还接受三个关键字参数,is_safeneeds_autoescapeexpects_localtime。这些参数在下面的过滤器和自动转义以及过滤器和时区中进行了描述。

期望字符串的模板过滤器

如果您正在编写一个模板过滤器,只期望第一个参数是字符串,您应该使用装饰器stringfilter。这将在将对象传递给您的函数之前将其转换为其字符串值:

from django import template 
from django.template.defaultfilters import stringfilter 

register = template.Library() 

@register.filter 
@stringfilter 
def lower(value): 
    return value.lower() 

这样,您就可以将一个整数传递给这个过滤器,它不会引起AttributeError(因为整数没有lower()方法)。

过滤器和自动转义

在编写自定义过滤器时,要考虑过滤器将如何与 Django 的自动转义行为交互。请注意,在模板代码中可以传递三种类型的字符串:

  • 原始字符串是本机 Python strunicode类型。在输出时,如果自动转义生效,它们会被转义并保持不变,否则。

  • 安全字符串是在输出时已标记为免受进一步转义的字符串。任何必要的转义已经完成。它们通常用于包含原始 HTML 的输出,该 HTML 旨在在客户端上按原样解释。

  • 在内部,这些字符串的类型是SafeBytesSafeText。它们共享一个名为SafeData的基类,因此您可以使用类似的代码对它们进行测试:

  • 如果valueSafeData的实例:

        # Do something with the "safe" string.
        ...
  • 标记为“需要转义”的字符串在输出时始终会被转义,无论它们是否在autoescape块中。但是,这些字符串只会被转义一次,即使自动转义适用。

在内部,这些字符串的类型是EscapeBytesEscapeText。通常,您不必担心这些问题;它们存在是为了实现escape过滤器。

模板过滤器代码分为两种情况:

  1. 您的过滤器不会在结果中引入任何 HTML 不安全的字符(<>'"&),这些字符在结果中本来就存在;或

  2. 或者,您的过滤器代码可以手动处理任何必要的转义。当您将新的 HTML 标记引入结果时,这是必要的。

在第一种情况下,您可以让 Django 为您处理所有自动转义处理。您只需要在注册过滤器函数时将is_safe标志设置为True,如下所示:

@register.filter(is_safe=True)
def myfilter(value):
    return value

这个标志告诉 Django,如果将安全字符串传递到您的过滤器中,则结果仍将是安全的,如果传递了不安全的字符串,则 Django 将自动转义它(如果需要的话)。您可以将其视为意味着“此过滤器是安全的-它不会引入任何不安全的 HTML 可能性。”

is_safe之所以必要是因为有很多普通的字符串操作会将SafeData对象转换回普通的strunicode对象,而不是尝试捕获它们所有,这将非常困难,Django 会在过滤器完成后修复损坏。

例如,假设您有一个过滤器,它将字符串xx添加到任何输入的末尾。由于这不会向结果引入危险的 HTML 字符(除了已经存在的字符),因此应该使用is_safe标记过滤器:

@register.filter(is_safe=True) 
def add_xx(value): 
    return '%sxx' % value 

当在启用自动转义的模板中使用此过滤器时,Django 将在输入未标记为安全时转义输出。默认情况下,is_safeFalse,并且您可以在任何不需要的过滤器中省略它。在决定您的过滤器是否确实将安全字符串保持为安全时要小心。如果您删除字符,可能会无意中在结果中留下不平衡的 HTML 标记或实体。

例如,从输入中删除>可能会将<a>变为<a,这需要在输出时进行转义,以避免引起问题。同样,删除分号(;)可能会将&amp;变为&amp,这不再是一个有效的实体,因此需要进一步转义。大多数情况下不会有这么棘手,但是在审查代码时要注意任何类似的问题。

标记过滤器is_safe将强制过滤器的返回值为字符串。如果您的过滤器应返回布尔值或其他非字符串值,则将其标记为is_safe可能会产生意想不到的后果(例如将布尔值False转换为字符串False)。

在第二种情况下,您希望标记输出为安全,以免进一步转义您的 HTML 标记,因此您需要自己处理输入。要将输出标记为安全字符串,请使用django.utils.safestring.mark_safe()

不过要小心。您需要做的不仅仅是标记输出为安全。您需要确保它确实是安全的,您的操作取决于自动转义是否生效。

这个想法是编写可以在模板中运行的过滤器,无论自动转义是打开还是关闭,以便为模板作者简化事情。

为了使您的过滤器知道当前的自动转义状态,请在注册过滤器函数时将needs_autoescape标志设置为True。(如果您不指定此标志,它将默认为False)。这个标志告诉 Django,您的过滤器函数希望传递一个额外的关键字参数,称为autoescape,如果自动转义生效,则为True,否则为False

例如,让我们编写一个过滤器,强调字符串的第一个字符:

from django import template 
from django.utils.html import conditional_escape 
from django.utils.safestring import mark_safe 

register = template.Library() 

@register.filter(needs_autoescape=True) 
def initial_letter_filter(text, autoescape=None): 
    first, other = text[0], text[1:] 
    if autoescape: 
        esc = conditional_escape 
    else: 
        esc = lambda x: x 
    result = '<strong>%s</strong>%s' % (esc(first), esc(other)) 
    return mark_safe(result) 

needs_autoescape标志和autoescape关键字参数意味着我们的函数将知道在调用过滤器时是否自动转义。我们使用autoescape来决定输入数据是否需要通过django.utils.html.conditional_escape传递。 (在后一种情况下,我们只使用身份函数作为“转义”函数。)

conditional_escape()函数类似于escape(),只是它只转义不是SafeData实例的输入。如果将SafeData实例传递给conditional_escape(),则数据将保持不变。

最后,在上面的例子中,我们记得将结果标记为安全,以便我们的 HTML 直接插入模板而不需要进一步转义。在这种情况下,不需要担心 is_safe 标志(尽管包含它也不会有什么坏处)。每当您手动处理自动转义问题并返回安全字符串时,is_safe 标志也不会改变任何东西。

过滤器和时区

如果您编写一个在 datetime 对象上操作的自定义过滤器,通常会将其注册为 expects_localtime 标志设置为 True

@register.filter(expects_localtime=True) 
def businesshours(value): 
    try: 
        return 9 <= value.hour < 17 
    except AttributeError: 
        return '' 

当设置了此标志时,如果您的过滤器的第一个参数是时区感知的日期时间,则 Django 会根据模板中的时区转换规则在适当时将其转换为当前时区后传递给您的过滤器。

注意

在重用内置过滤器时避免 XSS 漏洞

在重用 Django 的内置过滤器时要小心。您需要向过滤器传递 autoescape=True 以获得正确的自动转义行为,并避免跨站脚本漏洞。例如,如果您想编写一个名为 urlize_and_linebreaks 的自定义过滤器,该过滤器结合了 urlizelinebreaksbr 过滤器,那么过滤器将如下所示:

from django.template.defaultfilters import linebreaksbr, urlize @register.filter def urlize_and_linebreaks(text): return linebreaksbr( urlize(text, autoescape=True),autoescape=True) 然后: {{ comment|urlize_and_linebreaks }} 等同于: {{ comment|urlize|linebreaksbr }}

编写自定义模板标签

标签比过滤器更复杂,因为标签可以做任何事情。Django 提供了许多快捷方式,使编写大多数类型的标签更容易。首先我们将探讨这些快捷方式,然后解释如何为那些快捷方式不够强大的情况下从头编写标签。

简单标签

许多模板标签需要一些参数-字符串或模板变量-并且在仅基于输入参数和一些外部信息进行一些处理后返回结果。

例如,current_time 标签可能接受一个格式字符串,并根据格式化返回时间字符串。为了简化这些类型的标签的创建,Django 提供了一个辅助函数 simple_tag。这个函数是 django.template.Library 的一个方法,它接受一个接受任意数量参数的函数,将其包装在一个 render 函数和其他必要的部分中,并将其注册到模板系统中。

我们的 current_time 函数可以这样编写:

import datetime 
from django import template 

register = template.Library() 

@register.simple_tag 
def current_time(format_string): 
    return datetime.datetime.now().strftime(format_string) 

关于 simple_tag 辅助函数的一些注意事项:

  • 在我们的函数被调用时,已经检查了所需数量的参数等,所以我们不需要再做这些。

  • 参数(如果有)周围的引号已经被剥离,所以我们只收到一个普通字符串。

  • 如果参数是模板变量,则我们的函数会传递变量的当前值,而不是变量本身。

如果您的模板标签需要访问当前上下文,可以在注册标签时使用 takes_context 参数:

@register.simple_tag(takes_context=True) 
def current_time(context, format_string): 
    timezone = context['timezone'] 
    return your_get_current_time_method(timezone, format_string) 

请注意,第一个参数必须称为 context。有关 takes_context 选项的工作原理的更多信息,请参阅包含标签部分。如果您需要重命名标签,可以为其提供自定义名称:

register.simple_tag(lambda x: x-1, name='minusone') 

@register.simple_tag(name='minustwo') 
def some_function(value): 
    return value-2 

simple_tag 函数可以接受任意数量的位置参数或关键字参数。例如:

@register.simple_tag 
def my_tag(a, b, *args, **kwargs): 
    warning = kwargs['warning'] 
    profile = kwargs['profile'] 
    ... 
    return ... 

然后在模板中,可以传递任意数量的参数,用空格分隔,到模板标签。就像在 Python 中一样,关键字参数的值使用等号(“=`”)设置,并且必须在位置参数之后提供。例如:

{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %} 

包含标签

另一种常见的模板标签类型是通过呈现另一个模板来显示一些数据的类型。例如,Django 的管理界面使用自定义模板标签来显示“添加/更改”表单页面底部的按钮。这些按钮始终看起来相同,但链接目标会根据正在编辑的对象而变化-因此它们是使用填充了当前对象详细信息的小模板的完美案例。(在管理界面的情况下,这是submit_row标签。)

这些类型的标签被称为包含标签。编写包含标签最好通过示例来演示。让我们编写一个为给定的Author对象生成书籍列表的标签。我们将像这样使用该标签:

{% 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(authors__id=author.id) 
    return {'books': books} 

接下来,我们创建用于呈现标签输出的模板。根据我们的示例,模板非常简单:

<ul> 
{% for book in books %}<li>{{ book.title }}</li> 
{% endfor %} 
</ul> 

最后,我们通过在Library对象上调用inclusion_tag()方法来创建和注册包含标签。根据我们的示例,如果前面的模板在模板加载器搜索的目录中的名为book_snippet.html的文件中,我们可以像这样注册标签:

# Here, register is a django.template.Library instance, as before 
@register.inclusion_tag('book_snippet.html') 
def show_reviews(review): 
    ... 

或者,可以在首次创建函数时使用django.template.Template实例注册包含标签:

from django.template.loader import get_template 
t = get_template('book_snippet.html') 
register.inclusion_tag(t)(show_reviews) 

有时,你的包含标签可能需要大量的参数,这使得模板作者很难传递所有参数并记住它们的顺序。为了解决这个问题,Django 为包含标签提供了一个takes_context选项。如果在创建包含标签时指定了takes_context,则该标签将不需要必需的参数,而底层的 Python 函数将有一个参数:调用标签时的模板上下文。例如,假设你正在编写一个包含标签,它将始终在包含home_linkhome_title变量指向主页的上下文中使用。下面是 Python 函数的样子:

@register.inclusion_tag('link.html', takes_context=True) 
def jump_link(context): 
    return { 
        'link': context['home_link'], 
        'title': context['home_title'], 
    } 

(请注意,函数的第一个参数必须称为context。)模板link.html可能包含以下内容:

Jump directly to <a href="{{ link }}">{{ title }}</a>. 

然后,每当你想要使用该自定义标签时,加载它的库并在没有任何参数的情况下调用它,就像这样:

{% jump_link %} 

请注意,当使用takes_context=True时,无需向模板标签传递参数。它会自动访问上下文。takes_context参数默认为False。当设置为True时,标签将传递上下文对象,就像这个例子一样。这是这种情况和之前的inclusion_tag示例之间的唯一区别。像simple_tag一样,inclusion_tag函数也可以接受任意数量的位置或关键字参数。

分配标签

为了简化设置上下文变量的标签创建,Django 提供了一个辅助函数assignment_tag。这个函数的工作方式与simple_tag()相同,只是它将标签的结果存储在指定的上下文变量中,而不是直接输出它。因此,我们之前的current_time函数可以这样编写:

@register.assignment_tag 
def get_current_time(format_string): 
    return datetime.datetime.now().strftime(format_string) 

然后,你可以使用as参数将结果存储在模板变量中,并在适当的位置输出它:

{% get_current_time "%Y-%m-%d %I:%M %p" as the_time %} 
<p>The time is {{ the_time }}.</p> 

高级自定义模板标签

有时,创建自定义模板标签的基本功能不够。别担心,Django 让你完全访问所需的内部部分,从头开始构建模板标签。

快速概述

模板系统以两步过程工作:编译和渲染。要定义自定义模板标签,您需要指定编译如何工作以及渲染如何工作。当 Django 编译模板时,它将原始模板文本分割为节点。每个节点都是django.template.Node的一个实例,并且具有render()方法。编译的模板就是Node对象的列表。

当您在编译的模板对象上调用render()时,模板会在其节点列表中的每个Node上调用render(),并提供给定的上下文。结果都被连接在一起形成模板的输出。因此,要定义一个自定义模板标签,您需要指定原始模板标签如何转换为Node(编译函数),以及节点的render()方法的作用。

编写编译函数

对于模板解析器遇到的每个模板标签,它都会调用一个 Python 函数,该函数具有标签内容和解析器对象本身。此函数负责根据标签的内容返回一个基于Node的实例。例如,让我们编写一个我们简单模板标签{% current_time %}的完整实现,它显示当前日期/时间,根据标签中给定的参数以strftime()语法格式化。在任何其他事情之前,决定标签语法是一个好主意。在我们的情况下,让我们说标签应该像这样使用:

<p>The time is {% current_time "%Y-%m-%d %I:%M %p" %}.</p> 

此函数的解析器应该抓取参数并创建一个Node对象:

from django import template 

def do_current_time(parser, token): 
    try: 

      tag_name, format_string = token.split_contents() 

    except ValueError: 

      raise template.TemplateSyntaxError("%r tag requires a single  argument" % token.contents.split()[0]) 

   if not (format_string[0] == format_string[-1] and format_string[0]  in ('"', "'")): 
        raise template.TemplateSyntaxError("%r tag's argument should  be in quotes" % tag_name) 
   return CurrentTimeNode(format_string[1:-1]) 

注意:

  • parser是模板解析器对象。在这个例子中我们不需要它。

  • token.contents是标签的原始内容的字符串。在我们的例子中,它是'current_time "%Y-%m-%d %I:%M %p"'

  • token.split_contents()方法将参数在空格上分开,同时保持引号括起的字符串在一起。更直接的token.contents.split()不会那么健壮,因为它会简单地在所有空格上分割,包括引号括起的字符串中的空格。始终使用token.split_contents()是一个好主意。

  • 此函数负责为任何语法错误引发django.template.TemplateSyntaxError,并提供有用的消息。

  • TemplateSyntaxError异常使用tag_name变量。不要在错误消息中硬编码标签的名称,因为这会将标签的名称与您的函数耦合在一起。token.contents.split()[0]将始终是您的标签的名称-即使标签没有参数。

  • 该函数返回一个CurrentTimeNode,其中包含有关此标签的所有节点需要知道的信息。在这种情况下,它只传递参数"%Y-%m-%d %I:%M %p"。模板标签中的前导和尾随引号在format_string[1:-1]中被移除。

  • 解析是非常低级的。Django 开发人员尝试使用诸如 EBNF 语法之类的技术在此解析系统之上编写小型框架,但这些实验使模板引擎变得太慢。它是低级的,因为这是最快的。

编写渲染器

编写自定义标签的第二步是定义一个具有render()方法的Node子类。继续上面的例子,我们需要定义CurrentTimeNode

import datetime 
from django import template 

class CurrentTimeNode(template.Node): 
    def __init__(self, format_string): 
        self.format_string = format_string 

    def render(self, context): 
        return datetime.datetime.now().strftime(self.format_string) 

注意:

  • __init__()do_current_time()获取format_string。始终通过__init__()Node传递任何选项/参数/参数。

  • render()方法是实际工作发生的地方。

  • render()通常应该在生产环境中静默失败,特别是在DEBUGTEMPLATE_DEBUGFalse的情况下。然而,在某些情况下,特别是如果TEMPLATE_DEBUGTrue,此方法可能会引发异常以便更容易进行调试。例如,如果几个核心标签接收到错误数量或类型的参数,它们会引发django.template.TemplateSyntaxError

最终,编译和渲染的解耦导致了一个高效的模板系统,因为一个模板可以渲染多个上下文而不必多次解析。

自动转义注意事项

模板标签的输出不会自动通过自动转义过滤器运行。但是,在编写模板标签时,仍然有一些事项需要牢记。如果模板的render()函数将结果存储在上下文变量中(而不是以字符串返回结果),则应在适当时调用mark_safe()。最终呈现变量时,它将受到当时生效的自动转义设置的影响,因此需要将应该免受进一步转义的内容标记为这样。

此外,如果模板标签为执行某些子呈现创建新的上下文,请将自动转义属性设置为当前上下文的值。Context类的__init__方法接受一个名为autoescape的参数,您可以用于此目的。例如:

from django.template import Context 

def render(self, context): 
    # ... 
    new_context = Context({'var': obj}, autoescape=context.autoescape) 
    # ... Do something with new_context ... 

这不是一个非常常见的情况,但如果您自己呈现模板,则会很有用。例如:

def render(self, context): 
    t = context.template.engine.get_template('small_fragment.html') 
    return t.render(Context({'var': obj}, autoescape=context.autoescape)) 

如果在此示例中忽略了将当前context.autoescape值传递给我们的新Context,则结果将始终自动转义,这可能不是在模板标签用于内部时所期望的行为。

{% autoescape off %}块。

线程安全考虑

一旦解析了节点,就可以调用其render方法任意次数。由于 Django 有时在多线程环境中运行,单个节点可能会同时响应两个独立请求的不同上下文进行呈现。

因此,确保模板标签是线程安全的非常重要。为确保模板标签是线程安全的,不应在节点本身上存储状态信息。例如,Django 提供了内置的cycle模板标签,每次呈现时在给定字符串列表中循环:

{% for o in some_list %} 
    <tr class="{% cycle 'row1' 'row2' %}> 
        ... 
    </tr> 
{% endfor %} 

CycleNode的一个天真的实现可能如下所示:

import itertools 
from django import template 

class CycleNode(template.Node): 
    def __init__(self, cyclevars): 
        self.cycle_iter = itertools.cycle(cyclevars) 

    def render(self, context): 
        return next(self.cycle_iter) 

Thread 1 performs its first loop iteration, `CycleNode.render()` returns 'row1'Thread 2 performs its first loop iteration, `CycleNode.render()` returns 'row2'Thread 1 performs its second loop iteration, `CycleNode.render()` returns 'row1'Thread 2 performs its second loop iteration, `CycleNode.render()` returns 'row2'

CycleNode 正在迭代,但它是全局迭代的。就线程 1 和线程 2 而言,它总是返回相同的值。这显然不是我们想要的!

为了解决这个问题,Django 提供了一个render_context,它与当前正在呈现的模板的context相关联。render_context的行为类似于 Python 字典,并且应该用于在render方法的调用之间存储Node状态。让我们重构我们的CycleNode实现以使用render_context

class CycleNode(template.Node): 
    def __init__(self, cyclevars): 
        self.cyclevars = cyclevars 

    def render(self, context): 
        if self not in context.render_context: 
            context.render_context[self] =  itertools.cycle(self.cyclevars) 
        cycle_iter = context.render_context[self] 
        return next(cycle_iter) 

请注意,将全局信息存储为Node生命周期内不会更改的属性是完全安全的。

CycleNode的情况下,cyclevars参数在Node实例化后不会改变,因此我们不需要将其放入render_context中。但是,特定于当前正在呈现的模板的状态信息,例如CycleNode的当前迭代,应存储在render_context中。

注册标签

最后,按照上面“编写自定义模板过滤器”的说明,使用模块的Library实例注册标签。例如:

register.tag('current_time', do_current_time) 

tag()方法接受两个参数:

  • 模板标签的名称-一个字符串。如果不写,将使用编译函数的名称。

  • 编译函数-一个 Python 函数(而不是函数的名称作为字符串)。

与过滤器注册一样,也可以将其用作装饰器:

@register.tag(name="current_time") 
def do_current_time(parser, token): 
    ... 

@register.tag 
def shout(parser, token): 
    ... 

如果省略name参数,就像上面的第二个示例一样,Django 将使用函数的名称作为标签名称。

将模板变量传递给标签

尽管可以使用token.split_contents()将任意数量的参数传递给模板标签,但这些参数都会被解包为字符串文字。为了将动态内容(模板变量)作为参数传递给模板标签,需要进行更多的工作。

虽然前面的示例已将当前时间格式化为字符串并返回字符串,但假设您想要传递来自对象的DateTimeField并使模板标签格式化该日期时间:

<p>This post was last updated at {% format_time blog_entry.date_updated "%Y-%m-%d %I:%M %p" %}.</p> 

最初,token.split_contents()将返回三个值:

  1. 标签名称format_time

  2. 字符串'blog_entry.date_updated'(不包括周围的引号)。

  3. 格式化字符串'"%Y-%m-%d %I:%M %p"'split_contents()的返回值将包括字符串字面量的前导和尾随引号。

现在您的标签应该开始看起来像这样:

from django import template 

def do_format_time(parser, token): 
    try: 
        # split_contents() knows not to split quoted strings. 
        tag_name, date_to_be_formatted, format_string =    
        token.split_contents() 
    except ValueError: 
        raise template.TemplateSyntaxError("%r tag requires exactly  
          two arguments" % token.contents.split()[0]) 
    if not (format_string[0] == format_string[-1] and   
          format_string[0] in ('"', "'")): 
        raise template.TemplateSyntaxError("%r tag's argument should  
          be in quotes" % tag_name) 
    return FormatTimeNode(date_to_be_formatted, format_string[1:-1]) 

您还需要更改渲染器以检索blog_entry对象的date_updated属性的实际内容。这可以通过在django.template中使用Variable()类来实现。

要使用Variable类,只需使用要解析的变量的名称对其进行实例化,然后调用variable.resolve(context)。例如:

class FormatTimeNode(template.Node): 
    def __init__(self, date_to_be_formatted, format_string): 
        self.date_to_be_formatted =   
          template.Variable(date_to_be_formatted) 
        self.format_string = format_string 

    def render(self, context): 
        try: 
            actual_date = self.date_to_be_formatted.resolve(context) 
            return actual_date.strftime(self.format_string) 
        except template.VariableDoesNotExist: 
            return '' 

如果无法在页面的当前上下文中解析传递给它的字符串,变量解析将抛出VariableDoesNotExist异常。

在上下文中设置一个变量

上述示例只是简单地输出一个值。通常,如果您的模板标签设置模板变量而不是输出值,那么它会更灵活。这样,模板作者可以重用模板标签创建的值。要在上下文中设置一个变量,只需在render()方法中对上下文对象进行字典赋值。这是一个更新后的CurrentTimeNode版本,它设置了一个模板变量current_time而不是输出它:

import datetime 
from django import template 

class CurrentTimeNode2(template.Node): 
    def __init__(self, format_string): 
        self.format_string = format_string 
    def render(self, context): 
        context['current_time'] = 
 datetime.datetime.now().strftime(self.format_string)
 return ''

请注意,render()返回空字符串。render()应始终返回字符串输出。如果模板标签所做的只是设置一个变量,render()应返回空字符串。以下是如何使用标签的新版本:

{% current_time "%Y-%M-%d %I:%M %p" %} 
<p>The time is {{ current_time }}.</p> 

上下文中的变量范围

上下文中设置的任何变量只能在分配它的模板的相同block中使用。这种行为是有意的;它为变量提供了一个作用域,使它们不会与其他块中的上下文发生冲突。

但是,CurrentTimeNode2存在一个问题:变量名current_time是硬编码的。这意味着您需要确保您的模板不使用

{{ current_time }}在其他任何地方,因为{% current_time %}将盲目地覆盖该变量的值。

更清晰的解决方案是让模板标签指定输出变量的名称,如下所示:

{% current_time "%Y-%M-%d %I:%M %p" as my_current_time %} 
<p>The current time is {{ my_current_time }}.</p> 

为此,您需要重构编译函数和Node类,如下所示:

import re 

class CurrentTimeNode3(template.Node): 
    def __init__(self, format_string, var_name): 
        self.format_string = format_string 
        self.var_name = var_name 
    def render(self, context): 
        context[self.var_name] =    
          datetime.datetime.now().strftime(self.format_string) 
        return '' 

def do_current_time(parser, token): 
    # This version uses a regular expression to parse tag contents. 
    try: 
        # Splitting by None == splitting by spaces. 
        tag_name, arg = token.contents.split(None, 1) 
    except ValueError: 
        raise template.TemplateSyntaxError("%r tag requires arguments"    
          % token.contents.split()[0]) 
    m = re.search(r'(.*?) as (\w+)', arg) 
    if not m: 
        raise template.TemplateSyntaxError
          ("%r tag had invalid arguments"% tag_name) 
    format_string, var_name = m.groups() 
    if not (format_string[0] == format_string[-1] and format_string[0]   
       in ('"', "'")): 
        raise template.TemplateSyntaxError("%r tag's argument should be  
            in quotes" % tag_name) 
    return CurrentTimeNode3(format_string[1:-1], var_name) 

这里的区别在于do_current_time()获取格式字符串和变量名,并将两者都传递给CurrentTimeNode3。最后,如果您只需要为自定义上下文更新模板标签使用简单的语法,您可能希望考虑使用我们上面介绍的赋值标签快捷方式。

解析直到另一个块标签

模板标签可以协同工作。例如,标准的{% comment %}标签隐藏直到{% endcomment %}。要创建这样一个模板标签,可以在编译函数中使用parser.parse()。以下是一个简化的示例

{% comment %}标签可能被实现:

def do_comment(parser, token): 
    nodelist = parser.parse(('endcomment',)) 
    parser.delete_first_token() 
    return CommentNode() 

class CommentNode(template.Node): 
    def render(self, context): 
        return '' 

注意

{% comment %}的实际实现略有不同,它允许在{% comment %}{% endcomment %}之间出现损坏的模板标签。它通过调用parser.skip_past('endcomment')而不是parser.parse(('endcomment',)),然后是parser.delete_first_token()来实现这一点,从而避免生成节点列表。

parser.parse()接受一个块标签名称的元组''直到解析''。它返回django.template.NodeList的一个实例,这是解析器在遇到元组中命名的任何标签之前''遇到''的所有Node对象的列表。在上面的示例中的"nodelist = parser.parse(('endcomment',))"中,nodelist{% comment %}{% endcomment %}之间的所有节点的列表,不包括

{% comment %}{% endcomment %}本身。

在调用parser.parse()之后,解析器尚未“消耗”

{% endcomment %}标签,所以代码需要显式调用parser.delete_first_token()CommentNode.render()只是返回一个空字符串。{% comment %}{% endcomment %}之间的任何内容都会被忽略。

解析直到另一个块标签,并保存内容

在前面的例子中,do_comment()丢弃了{% comment %}{% endcomment %}之间的所有内容

{% comment %}{% endcomment %}。而不是这样做,可以对块标签之间的代码进行操作。例如,这里有一个自定义模板标签{% upper %},它会将其自身和之间的所有内容都大写

{% endupper %}。用法:

{% upper %}This will appear in uppercase, {{ your_name }}.{% endupper %} 

与前面的例子一样,我们将使用parser.parse()。但是这次,我们将将结果的nodelist传递给Node

def do_upper(parser, token): 
    nodelist = parser.parse(('endupper',)) 
    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.render(context) 
        return output.upper() 

这里唯一的新概念是UpperNode.render()中的self.nodelist.render(context)。有关复杂渲染的更多示例,请参阅django/template/defaulttags.py中的{% for %}django/template/smartif.py中的{% if %}的源代码。

接下来是什么

继续本节关于高级主题的主题,下一章涵盖了 Django 模型的高级用法。

第九章:高级模型

在第四章:
name = models.CharField(max_length=30)
address = models.CharField(max_length=50)
city = models.CharField(max_length=60)
state_province = models.CharField(max_length=30)
country = models.CharField(max_length=50)
website = models.URLField()

def __str__(self): 
    return self.name 

class Author(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=40)
email = models.EmailField()

def __str__(self): 
    return '%s %s' % (self.first_name, self.last_name) 

class Book(models.Model):
title = models.CharField(max_length=100)
authors = models.ManyToManyField(Author)
publisher = models.ForeignKey(Publisher)
publication_date = models.DateField()

def __str__(self): 
    return self.title 

正如我们在第四章 
>>> b.title 
'The Django Book' 

但我们之前没有提到的一件事是,相关对象-表达为ForeignKeyManyToManyField的字段-的行为略有不同。

访问 ForeignKey 值

当您访问一个ForeignKey字段时,您将获得相关的模型对象。例如:

>>> b = Book.objects.get(id=50) 
>>> b.publisher 
<Publisher: Apress Publishing> 
>>> b.publisher.website 
'http://www.apress.com/' 

对于ForeignKey字段,它也可以反向工作,但由于关系的非对称性,它略有不同。要获取给定出版商的书籍列表,请使用“publisher.book_set.all()”,如下所示:

>>> p = Publisher.objects.get(name='Apress Publishing') 
>>> p.book_set.all() 
[<Book: The Django Book>, <Book: Dive Into Python>, ...] 

在幕后,book_set只是一个QuerySet(如第四章

p.book_set.filter(title__icontains='django')
[<Book: The Django Book>, <Book: Pro Django>]


属性名称`book_set`是通过将小写模型名称附加到`_set`而生成的。

## 访问多对多值

多对多值的工作方式与外键值相似,只是我们处理的是`QuerySet`值而不是模型实例。例如,以下是如何查看书籍的作者:

```py
>>> b = Book.objects.get(id=50) 
>>> b.authors.all() 
[<Author: Adrian Holovaty>, <Author: Jacob Kaplan-Moss>] 
>>> b.authors.filter(first_name='Adrian') 
[<Author: Adrian Holovaty>] 
>>> b.authors.filter(first_name='Adam') 
[] 

它也可以反向工作。要查看作者的所有书籍,请使用author.book_set,如下所示:

>>> a = Author.objects.get(first_name='Adrian', last_name='Holovaty') 
>>> a.book_set.all() 
[<Book: The Django Book>, <Book: Adrian's Other Book>] 

在这里,与ForeignKey字段一样,book_set属性名称是通过将小写模型名称附加到_set而生成的。

管理器

在语句“Book.objects.all()”中,objects是一个特殊的属性,通过它您可以查询您的数据库。在第四章:
def title_count(self, keyword):
return self.filter(title__icontains=keyword).count()

class Book(models.Model):
title = models.CharField(max_length=100)
authors = models.ManyToManyField(Author)
publisher = models.ForeignKey(Publisher)
publication_date = models.DateField()
num_pages = models.IntegerField(blank=True, null=True)
objects = BookManager()

def __str__(self): 
    return self.title 

以下是有关代码的一些说明:

+   我们创建了一个扩展了`django.db.models.Manager`的`BookManager`类。这有一个名为“title_count()”的方法,用于进行计算。请注意,该方法使用“self.filter()”,其中`self`是指管理器本身。

+   我们将“BookManager()”分配给模型上的`objects`属性。这会替换模型的默认管理器,称为`objects`,如果您没有指定自定义管理器,则会自动创建。我们将其称为`objects`而不是其他名称,以便与自动创建的管理器保持一致。

有了这个管理器,我们现在可以这样做:

```py
>>> Book.objects.title_count('django') 
4 
>>> Book.objects.title_count('python') 
18 

显然,这只是一个例子-如果您在交互式提示符中键入此内容,您可能会得到不同的返回值。

为什么我们想要添加一个像 title_count() 这样的方法?为了封装常用的执行查询,这样我们就不必重复代码。

修改初始管理器查询集

管理器的基本 QuerySet 返回系统中的所有对象。例如,Book.objects.all() 返回书数据库中的所有书籍。你可以通过覆盖 Manager.get_queryset() 方法来覆盖管理器的基本 QuerySetget_queryset() 应该返回一个具有你需要的属性的 QuerySet

例如,以下模型有两个管理器-一个返回所有对象,一个只返回罗尔德·达尔的书。

from django.db import models 

# First, define the Manager subclass. 
class DahlBookManager(models.Manager): 
    def get_queryset(self): 
        return super(DahlBookManager, self).get_queryset().filter(author='Roald Dahl') 

# Then hook it into the Book model explicitly. 
class Book(models.Model): 
    title = models.CharField(max_length=100) 
    author = models.CharField(max_length=50) 
    # ... 

    objects = models.Manager() # The default manager. 
    dahl_objects = DahlBookManager() # The Dahl-specific manager. 

使用这个示例模型,Book.objects.all() 将返回数据库中的所有书籍,但 Book.dahl_objects.all() 只会返回罗尔德·达尔写的书。请注意,我们明确将 objects 设置为一个普通的 Manager 实例,因为如果我们没有这样做,唯一可用的管理器将是 dahl_objects。当然,因为 get_queryset() 返回一个 QuerySet 对象,你可以在其上使用 filter()exclude() 和所有其他 QuerySet 方法。因此,这些语句都是合法的:

Book.dahl_objects.all() 
Book.dahl_objects.filter(title='Matilda') 
Book.dahl_objects.count() 

这个例子还指出了另一个有趣的技术:在同一个模型上使用多个管理器。你可以将多个 Manager() 实例附加到一个模型上。这是定义模型的常见过滤器的简单方法。例如:

class MaleManager(models.Manager): 
    def get_queryset(self): 
        return super(MaleManager, self).get_queryset().filter(sex='M') 

class FemaleManager(models.Manager): 
    def get_queryset(self): 
        return super(FemaleManager, self).get_queryset().filter(sex='F') 

class Person(models.Model): 
    first_name = models.CharField(max_length=50) 
    last_name = models.CharField(max_length=50) 
    sex = models.CharField(max_length=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 的几个部分(尽管不包括管理应用程序)将专门使用该 Manager 来管理该模型。

因此,在选择默认管理器时要小心,以避免覆盖 get_queryset() 导致无法检索到你想要处理的对象的情况。

模型方法

在模型上定义自定义方法,为对象添加自定义的行级功能。而管理器旨在对整个表执行操作,模型方法应该作用于特定的模型实例。这是将业务逻辑集中在一个地方-模型中的一个有价值的技术。

举例是最容易解释这个问题的方法。下面是一个带有一些自定义方法的模型:

from django.db import models 

class Person(models.Model): 
    first_name = models.CharField(max_length=50) 
    last_name = models.CharField(max_length=50) 
    birth_date = models.DateField() 

    def baby_boomer_status(self): 
        # Returns the person's baby-boomer status. 
        import datetime 
        if self.birth_date < datetime.date(1945, 8, 1): 
            return "Pre-boomer" 
        elif self.birth_date < datetime.date(1965, 1, 1): 
            return "Baby boomer" 
        else: 
            return "Post-boomer" 

    def _get_full_name(self): 
        # Returns the person's full name." 
        return '%s %s' % (self.first_name, self.last_name) 
    full_name = property(_get_full_name) 

附录 A 中的模型实例引用,模型定义参考,列出了自动赋予每个模型的完整方法列表。你可以覆盖大部分方法(见下文),但有一些你几乎总是想要定义的:

  • __str__(): 一个 Python 魔术方法,返回任何对象的 Unicode 表示。这是 Python 和 Django 在需要将模型实例强制转换并显示为普通字符串时使用的方法。特别是,当你在交互式控制台或管理界面中显示对象时,就会发生这种情况。

  • 你总是希望定义这个方法;默认情况下并不是很有用。

  • get_absolute_url(): 这告诉 Django 如何计算对象的 URL。Django 在其管理界面中使用这个方法,以及任何时候它需要为对象计算 URL。

任何具有唯一标识 URL 的对象都应该定义这个方法。

覆盖预定义的模型方法

还有一组模型方法,封装了一堆你想要自定义的数据库行为。特别是,你经常会想要改变 save()delete() 的工作方式。你可以自由地覆盖这些方法(以及任何其他模型方法)来改变行为。覆盖内置方法的一个经典用例是,如果你想要在保存对象时发生某些事情。例如,(参见 save() 以获取它接受的参数的文档):

from django.db import models 

class Blog(models.Model): 
    name = models.CharField(max_length=100) 
    tagline = models.TextField() 

    def save(self, *args, **kwargs): 
        do_something() 
        super(Blog, self).save(*args, **kwargs) # Call the "real" save() method. 
        do_something_else() 

你也可以阻止保存:

from django.db import models 

class Blog(models.Model): 
    name = models.CharField(max_length=100) 
    tagline = models.TextField() 

    def save(self, *args, **kwargs): 
        if self.name == "Yoko Ono's blog": 
            return # Yoko shall never have her own blog! 
        else: 
            super(Blog, self).save(*args, **kwargs) # Call the "real" save() method. 

重要的是要记住调用超类方法-也就是super(Blog, self).save(*args, **kwargs),以确保对象仍然被保存到数据库中。如果忘记调用超类方法,就不会发生默认行为,数据库也不会被触及。

还要确保通过可以传递给模型方法的参数-这就是*args, **kwargs的作用。Django 会不时地扩展内置模型方法的功能,添加新的参数。如果在方法定义中使用*args, **kwargs,则可以确保在添加这些参数时,您的代码将自动支持这些参数。

执行原始 SQL 查询

当模型查询 API 不够用时,可以退而使用原始 SQL。Django 提供了两种执行原始 SQL 查询的方法:您可以使用Manager.raw()执行原始查询并返回模型实例,或者完全避开模型层并直接执行自定义 SQL。

注意

每次使用原始 SQL 时,都应该非常小心。您应该使用params正确转义用户可以控制的任何参数,以防止 SQL 注入攻击。

执行原始 SQL 查询

raw()管理器方法可用于执行返回模型实例的原始 SQL 查询:

Manager.raw(raw_query, params=None, translations=None)

此方法接受原始 SQL 查询,执行它,并返回一个django.db.models.query.RawQuerySet实例。这个RawQuerySet实例可以像普通的QuerySet一样进行迭代,以提供对象实例。这最好用一个例子来说明。假设您有以下模型:

class Person(models.Model): 
    first_name = models.CharField(...) 
    last_name = models.CharField(...) 
    birth_date = models.DateField(...) 

然后,您可以执行自定义的 SQL,就像这样:

>>> for p in Person.objects.raw('SELECT * FROM myapp_person'): 
...     print(p) 
John Smith 
Jane Jones 

当然,这个例子并不是很令人兴奋-它与运行Person.objects.all()完全相同。但是,raw()有很多其他选项,使其非常强大。

模型表名称

在前面的例子中,Person表的名称是从哪里来的?默认情况下,Django 通过将模型的应用程序标签(您在manage.py startapp中使用的名称)与模型的类名结合起来,它们之间用下划线连接来确定数据库表名称。在我们的例子中,假设Person模型位于名为myapp的应用程序中,因此其表将是myapp_person

有关db_table选项的更多详细信息,请查看文档,该选项还允许您手动设置数据库表名称。

注意

对传递给raw()的 SQL 语句不进行检查。Django 期望该语句将从数据库返回一组行,但不执行任何强制性操作。如果查询不返回行,将导致(可能是晦涩的)错误。

将查询字段映射到模型字段

raw()会自动将查询中的字段映射到模型中的字段。查询中字段的顺序并不重要。换句话说,以下两个查询的工作方式是相同的:

>>> Person.objects.raw('SELECT id, first_name, last_name, birth_date FROM myapp_person') 
... 
>>> Person.objects.raw('SELECT last_name, birth_date, first_name, id FROM myapp_person') 
... 

匹配是通过名称完成的。这意味着您可以使用 SQL 的AS子句将查询中的字段映射到模型字段。因此,如果您有其他表中有Person数据,您可以轻松地将其映射到Person实例中:

>>> Person.objects.raw('''SELECT first AS first_name, 
...                              last AS last_name, 
...                              bd AS birth_date, 
...                              pk AS id, 
...                       FROM some_other_table''') 

只要名称匹配,模型实例就会被正确创建。或者,您可以使用raw()translations参数将查询中的字段映射到模型字段。这是一个将查询中的字段名称映射到模型字段名称的字典。例如,前面的查询也可以这样写:

>>> name_map = {'first': 'first_name', 'last': 'last_name', 'bd': 'birth_date', 'pk': 'id'} 
>>> Person.objects.raw('SELECT * FROM some_other_table', translations=name_map) 

索引查找

raw()支持索引,因此如果只需要第一个结果,可以这样写:

>>> first_person = Person.objects.raw('SELECT * FROM myapp_person')[0] 

但是,索引和切片不是在数据库级别执行的。如果数据库中有大量的Person对象,限制 SQL 级别的查询效率更高:

>>> first_person = Person.objects.raw('SELECT * FROM myapp_person LIMIT 1')[0] 

延迟加载模型字段

字段也可以被省略:

>>> people = Person.objects.raw('SELECT id, first_name FROM myapp_person') 

此查询返回的Person对象将是延迟加载的模型实例(参见defer())。这意味着从查询中省略的字段将按需加载。例如:

>>> for p in Person.objects.raw('SELECT id, first_name FROM myapp_person'): 
...     print(p.first_name, # This will be retrieved by the original query 
...           p.last_name) # This will be retrieved on demand 
... 
John Smith 
Jane Jones 

从外观上看,这似乎是查询已检索了名字和姓氏。但是,这个例子实际上发出了 3 个查询。只有第一个名字是由raw()查询检索到的-当打印它们时,姓氏是按需检索的。

只有一个字段是不能省略的-主键字段。Django 使用主键来标识模型实例,因此它必须始终包含在原始查询中。如果您忘记包括主键,将会引发InvalidQuery异常。

添加注释

您还可以执行包含模型上未定义的字段的查询。例如,我们可以使用 PostgreSQL 的age()函数来获取一个人的年龄列表,其年龄由数据库计算得出:

>>> people = Person.objects.raw('SELECT *, age(birth_date) AS age FROM myapp_person') 
>>> for p in people: 
...     print("%s is %s." % (p.first_name, p.age)) 
John is 37\. 
Jane is 42\. 
... 

将参数传递给原始查询

如果您需要执行参数化查询,可以将params参数传递给raw()

>>> lname = 'Doe' 
>>> Person.objects.raw('SELECT * FROM myapp_person WHERE last_name = %s', [lname]) 

params是参数的列表或字典。您将在查询字符串中使用%s占位符来表示列表,或者使用%(key)s占位符来表示字典(其中key当然会被字典键替换),而不管您的数据库引擎如何。这些占位符将被params参数中的参数替换。

注意

不要在原始查询上使用字符串格式化!

很容易将前面的查询写成:

>>> query = 'SELECT * FROM myapp_person WHERE last_name = %s' % lname Person.objects.raw(query)

不要这样做。

使用params参数完全保护您免受 SQL 注入攻击,这是一种常见的攻击方式,攻击者会将任意 SQL 注入到您的数据库中。如果您使用字符串插值,迟早会成为 SQL 注入的受害者。只要记住始终使用params参数,您就会得到保护。

直接执行自定义 SQL

有时甚至Manager.raw()还不够:您可能需要执行与模型不太匹配的查询,或者直接执行UPDATEINSERTDELETE查询。在这些情况下,您可以始终直接访问数据库,完全绕过模型层。对象django.db.connection表示默认数据库连接。要使用数据库连接,调用connection.cursor()以获取游标对象。然后,调用cursor.execute(sql, [params])来执行 SQL,cursor.fetchone()cursor.fetchall()来返回结果行。例如:

from django.db import connection 

def my_custom_sql(self): 
    cursor = connection.cursor() 
    cursor.execute("UPDATE bar SET foo = 1 WHERE baz = %s", [self.baz]) 
    cursor.execute("SELECT foo FROM bar WHERE baz = %s", [self.baz]) 
    row = cursor.fetchone() 

    return row 

请注意,如果您想在查询中包含百分号,您必须在传递参数的情况下将其加倍:

cursor.execute("SELECT foo FROM bar WHERE baz = '30%'") 
cursor.execute("SELECT foo FROM bar WHERE baz = '30%%' AND  
  id = %s", [self.id]) 

如果您使用多个数据库,可以使用django.db.connections来获取特定数据库的连接(和游标)。django.db.connections是一个类似字典的对象,允许您使用其别名检索特定连接:

from django.db import connections 
cursor = connections['my_db_alias'].cursor() 
# Your code here... 

默认情况下,Python DB API 将返回结果而不带有它们的字段名称,这意味着您最终会得到一个值的list,而不是一个dict。以较小的性能成本,您可以通过类似以下的方式返回结果作为dict

def dictfetchall(cursor): 
    # Returns all rows from a cursor as a dict 
    desc = cursor.description 
    return [ 
        dict(zip([col[0] for col in desc], row)) 
        for row in cursor.fetchall() 
    ] 

以下是两者之间差异的示例:

>>> cursor.execute("SELECT id, parent_id FROM test LIMIT 2"); 
>>> cursor.fetchall() 
((54360982L, None), (54360880L, None)) 

>>> cursor.execute("SELECT id, parent_id FROM test LIMIT 2"); 
>>> dictfetchall(cursor) 
[{'parent_id': None, 'id': 54360982L}, {'parent_id': None, 'id': 54360880L}] 

连接和游标

connectioncursor大多实现了 PEP 249 中描述的标准 Python DB-API(有关更多信息,请访问www.python.org/dev/peps/pep-0249),除了在处理事务时。如果您不熟悉 Python DB-API,请注意cursor.execute()中的 SQL 语句使用占位符"%s",而不是直接在 SQL 中添加参数。

如果您使用这种技术,底层数据库库将根据需要自动转义参数。还要注意,Django 期望"%s"占位符,而不是 SQLite Python 绑定使用的?占位符。这是为了一致性和健全性。使用游标作为上下文管理器:

with connection.cursor() as c: 
    c.execute(...) 

等同于:

c = connection.cursor() 
try: 
    c.execute(...) 
finally: 
    c.close() 

添加额外的 Manager 方法

添加额外的Manager方法是向模型添加表级功能的首选方式。(对于行级功能,即对模型对象的单个实例进行操作的函数,请使用模型方法,而不是自定义的Manager方法。)自定义的Manager方法可以返回任何你想要的东西。它不一定要返回一个QuerySet

例如,这个自定义的Manager提供了一个名为with_counts()的方法,它返回所有OpinionPoll对象的列表,每个对象都有一个额外的num_responses属性,这是聚合查询的结果。

from django.db import models 

class PollManager(models.Manager): 
    def with_counts(self): 
        from django.db import connection 
        cursor = connection.cursor() 
        cursor.execute(""" 
            SELECT p.id, p.question, p.poll_date, COUNT(*) 
            FROM polls_opinionpoll p, polls_response r 
            WHERE p.id = r.poll_id 
            GROUP BY p.id, p.question, p.poll_date 
            ORDER BY p.poll_date DESC""") 
        result_list = [] 
        for row in cursor.fetchall(): 
            p = self.model(id=row[0], question=row[1], poll_date=row[2]) 
            p.num_responses = row[3] 
            result_list.append(p) 
        return result_list 

class OpinionPoll(models.Model): 
    question = models.CharField(max_length=200) 
    poll_date = models.DateField() 
    objects = PollManager() 

class Response(models.Model): 
    poll = models.ForeignKey(OpinionPoll) 
    person_name = models.CharField(max_length=50) 
    response = models.TextField() 

使用这个例子,您可以使用OpinionPoll.objects.with_counts()来返回带有num_responses属性的OpinionPoll对象列表。关于这个例子的另一点要注意的是,Manager方法可以访问self.model来获取它们所附加的模型类。

接下来呢?

在下一章中,我们将向您展示 Django 的通用视图框架,它可以帮助您节省时间,构建遵循常见模式的网站。

第十章:通用视图

这里再次出现了本书的一个重要主题:在最糟糕的情况下,Web 开发是乏味和单调的。到目前为止,我们已经介绍了 Django 如何在模型和模板层减轻了一些单调,但 Web 开发人员在视图层也会经历这种乏味。

Django 的通用视图是为了减轻这种痛苦而开发的。

它们采用了在视图开发中发现的某些常见习语和模式,并对它们进行抽象,以便您可以快速编写常见的数据视图,而无需编写太多代码。我们可以识别出某些常见任务,比如显示对象列表,并编写显示任何对象列表的代码。

然后,可以将相关模型作为 URLconf 的额外参数传递。Django 附带了用于执行以下操作的通用显示视图:

  • 显示单个对象的列表和详细页面。如果我们正在创建一个管理会议的应用程序,那么TalkListViewRegisteredUserListView将是列表视图的示例。单个讲话页面是我们称之为详细视图的示例。

  • 在年/月/日归档页面、相关详细信息和最新页面中呈现基于日期的对象。

  • 允许用户创建、更新和删除对象-无论是否授权。

这些视图一起提供了执行开发人员在视图中显示数据库数据时遇到的最常见任务的简单界面。最后,显示视图只是 Django 全面基于类的视图系统的一部分。有关 Django 提供的其他基于类的视图的完整介绍和详细描述,请参阅附录 C,通用视图参考

对象的通用视图

当涉及呈现数据库内容的视图时,Django 的通用视图确实表现出色。因为这是一个常见的任务,Django 附带了一些内置的通用视图,使生成对象的列表和详细视图变得非常容易。

让我们从一些显示对象列表或单个对象的示例开始。我们将使用这些模型:

# models.py 
from django.db import models 

class Publisher(models.Model): 
    name = models.CharField(max_length=30) 
    address = models.CharField(max_length=50) 
    city = models.CharField(max_length=60) 
    state_province = models.CharField(max_length=30) 
    country = models.CharField(max_length=50) 
    website = models.URLField() 

    class Meta: 
        ordering = ["-name"] 

    def __str__(self): 
        return self.name 

class Author(models.Model): 
    salutation = models.CharField(max_length=10) 
    name = models.CharField(max_length=200) 
    email = models.EmailField() 
    headshot = models.ImageField(upload_to='author_headshots') 

    def __str__(self): 
        return self.name 

class Book(models.Model): 
    title = models.CharField(max_length=100) 
    authors = models.ManyToManyField('Author') 
    publisher = models.ForeignKey(Publisher) 
    publication_date = models.DateField() 

现在我们需要定义一个视图:

# views.py 
from django.views.generic import ListView 
from books.models import Publisher 

class PublisherList(ListView): 
    model = Publisher 

最后将该视图挂接到您的 URL 中:

# urls.py 
from django.conf.urls import url 
from books.views import PublisherList 

urlpatterns = [ 
    url(r'^publishers/$', PublisherList.as_view()), 
] 

这是我们需要编写的所有 Python 代码。但是,我们仍然需要编写一个模板。但是,我们可以通过向视图添加template_name属性来明确告诉视图使用哪个模板,但在没有显式模板的情况下,Django 将从对象的名称中推断一个模板。在这种情况下,推断的模板将是books/publisher_list.html-books 部分来自定义模型的定义应用程序的名称,而“publisher”部分只是模型名称的小写版本。

因此,当(例如)在TEMPLATES中将DjangoTemplates后端的APP_DIRS选项设置为 True 时,模板位置可以是:/path/to/project/books/templates/books/publisher_list.html

这个模板将根据包含名为object_list的变量的上下文进行渲染,该变量包含所有发布者对象。一个非常简单的模板可能如下所示:

{% extends "base.html" %} 

{% block content %} 
    <h2>Publishers</h2> 
    <ul> 
        {% for publisher in object_list %} 
            <li>{{ publisher.name }}</li> 
        {% endfor %} 
    </ul> 
{% endblock %} 

这就是全部。通用视图的所有很酷的功能都来自于更改通用视图上设置的属性。附录 C,通用视图参考,详细记录了所有通用视图及其选项;本文档的其余部分将考虑您可能定制和扩展通用视图的一些常见方法。

创建“友好”的模板上下文

您可能已经注意到我们的示例发布者列表模板将所有发布者存储在名为object_list的变量中。虽然这样做完全没问题,但对于模板作者来说并不是很“友好”:他们必须“知道”他们在这里处理的是发布者。

在 Django 中,如果您正在处理模型对象,则已为您完成此操作。 当您处理对象或查询集时,Django 使用模型类名称的小写版本填充上下文。 除了默认的object_list条目之外,这是额外提供的,但包含完全相同的数据,即publisher_list

如果这仍然不是一个很好的匹配,您可以手动设置上下文变量的名称。 通用视图上的context_object_name属性指定要使用的上下文变量:

# views.py 
from django.views.generic import ListView 
from books.models import Publisher 

class PublisherList(ListView): 
    model = Publisher 
 context_object_name = 'my_favorite_publishers'

提供有用的context_object_name始终是一个好主意。 设计模板的同事会感谢您。

添加额外的上下文

通常,您只需要提供一些通用视图提供的信息之外的额外信息。 例如,考虑在每个出版商详细页面上显示所有书籍的列表。 DetailView通用视图提供了出版商的上下文,但是我们如何在模板中获取额外的信息呢?

答案是子类化DetailView并提供您自己的get_context_data方法的实现。 默认实现只是将要显示的对象添加到模板中,但您可以重写它以发送更多内容:

from django.views.generic import DetailView 
from books.models import Publisher, Book 

class PublisherDetail(DetailView): 

    model = Publisher 

    def get_context_data(self, **kwargs): 
        # Call the base implementation first to get a context 
        context = super(PublisherDetail, self).get_context_data(**kwargs) 
        # Add in a QuerySet of all the books 
        context['book_list'] = Book.objects.all() 
        return context 

注意

通常,get_context_data将合并当前类的所有父类的上下文数据。 要在您自己的类中保留此行为,其中您想要更改上下文,您应该确保在超类上调用get_context_data。 当没有两个类尝试定义相同的键时,这将产生预期的结果。

但是,如果任何类尝试在父类设置它之后覆盖键(在调用 super 之后),那么该类的任何子类在 super 之后也需要显式设置它,如果他们想确保覆盖所有父类。 如果您遇到问题,请查看视图的方法解析顺序。

查看对象的子集

现在让我们更仔细地看看我们一直在使用的model参数。 model参数指定视图将操作的数据库模型,在操作单个对象或一组对象的所有通用视图上都可用。 但是,model参数不是指定视图将操作的对象的唯一方法-您还可以使用queryset参数指定对象的列表:

from django.views.generic import DetailView 
from books.models import Publisher 

class PublisherDetail(DetailView): 

    context_object_name = 'publisher' 
    queryset = Publisher.objects.all() 

指定model = Publisher实际上只是简写为queryset = Publisher.objects.all()。 但是,通过使用queryset来定义对象的过滤列表,您可以更具体地了解视图中将可见的对象。 举个简单的例子,我们可能想要按出版日期对书籍列表进行排序,最新的排在前面:

from django.views.generic import ListView 
from books.models import Book 

class BookList(ListView): 
    queryset = Book.objects.order_by('-publication_date') 
    context_object_name = 'book_list' 

这是一个非常简单的例子,但它很好地说明了这个想法。 当然,您通常希望做的不仅仅是重新排序对象。 如果要显示特定出版商的书籍列表,可以使用相同的技术:

from django.views.generic import ListView 
from books.models import Book 

class AcmeBookList(ListView): 

    context_object_name = 'book_list' 
    queryset = Book.objects.filter(publisher__name='Acme Publishing') 
    template_name = 'books/acme_list.html' 

请注意,除了过滤的queryset之外,我们还使用了自定义模板名称。 如果没有,通用视图将使用与“普通”对象列表相同的模板,这可能不是我们想要的。

还要注意,这不是一个非常优雅的处理特定出版商书籍的方法。 如果我们想要添加另一个出版商页面,我们需要在 URLconf 中添加另外几行,而且超过几个出版商将变得不合理。 我们将在下一节中解决这个问题。

注意

如果在请求/books/acme/时收到 404 错误,请检查确保您实际上有一个名称为'ACME Publishing'的出版商。 通用视图具有allow_empty参数用于此情况。

动态过滤

另一个常见的需求是通过 URL 中的某个键来过滤列表页面中给定的对象。 早些时候,我们在 URLconf 中硬编码了出版商的名称,但是如果我们想编写一个视图,显示某个任意出版商的所有书籍怎么办?

方便的是,ListView 有一个我们可以重写的 get_queryset() 方法。以前,它只是返回 queryset 属性的值,但现在我们可以添加更多逻辑。使这项工作的关键部分是,当调用基于类的视图时,各种有用的东西都存储在 self 上;除了请求(self.request)之外,还包括根据 URLconf 捕获的位置参数(self.args)和基于名称的参数(self.kwargs)。

在这里,我们有一个带有单个捕获组的 URLconf:

# urls.py 
from django.conf.urls import url 
from books.views import PublisherBookList 

urlpatterns = [ 
    url(r'^books/([\w-]+)/$', PublisherBookList.as_view()), 
] 

接下来,我们将编写 PublisherBookList 视图本身:

# views.py 
from django.shortcuts import get_object_or_404 
from django.views.generic import ListView 
from books.models import Book, Publisher 

class PublisherBookList(ListView): 

    template_name = 'books/books_by_publisher.html' 

    def get_queryset(self): 
        self.publisher = get_object_or_404(Publisher name=self.args[0]) 
        return Book.objects.filter(publisher=self.publisher) 

正如你所看到的,向查询集选择添加更多逻辑非常容易;如果我们想的话,我们可以使用 self.request.user 来使用当前用户进行过滤,或者其他更复杂的逻辑。我们还可以同时将发布者添加到上下文中,这样我们可以在模板中使用它:

# ... 

def get_context_data(self, **kwargs): 
    # Call the base implementation first to get a context 
    context = super(PublisherBookList, self).get_context_data(**kwargs) 

    # Add in the publisher 
    context['publisher'] = self.publisher 
    return context 

执行额外的工作

我们将看一下最后一个常见模式,它涉及在调用通用视图之前或之后做一些额外的工作。想象一下,我们在我们的 Author 模型上有一个 last_accessed 字段,我们正在使用它来跟踪任何人最后一次查看该作者的时间:

# models.py 
from django.db import models 

class Author(models.Model): 
    salutation = models.CharField(max_length=10) 
    name = models.CharField(max_length=200) 
    email = models.EmailField() 
    headshot = models.ImageField(upload_to='author_headshots') 
    last_accessed = models.DateTimeField() 

当然,通用的 DetailView 类不会知道这个字段,但我们可以再次轻松地编写一个自定义视图来保持该字段更新。首先,我们需要在 URLconf 中添加一个作者详细信息,指向一个自定义视图:

from django.conf.urls import url 
from books.views import AuthorDetailView 

urlpatterns = [ 
    #... 
    url(r'^authors/(?P<pk>[0-9]+)/$', AuthorDetailView.as_view(), name='author-detail'), 
] 

然后我们会编写我们的新视图 - get_object 是检索对象的方法 - 所以我们只需重写它并包装调用:

from django.views.generic import DetailView 
from django.utils import timezone 
from books.models import Author 

class AuthorDetailView(DetailView): 

    queryset = Author.objects.all() 

    def get_object(self): 
        # Call the superclass 
        object = super(AuthorDetailView, self).get_object() 

        # Record the last accessed date 
        object.last_accessed = timezone.now() 
        object.save() 
        # Return the object 
        return object 

这里的 URLconf 使用了命名组 pk - 这个名称是 DetailView 用来查找用于过滤查询集的主键值的默认名称。

如果你想给组起一个别的名字,你可以在视图上设置 pk_url_kwarg。更多细节可以在 DetailView 的参考中找到。

接下来呢?

在这一章中,我们只看了 Django 预装的一些通用视图,但这里提出的一般思想几乎适用于任何通用视图。附录 C,通用视图参考,详细介绍了所有可用的视图,如果你想充分利用这一强大功能,建议阅读。

这结束了本书专门讨论模型、模板和视图的高级用法的部分。接下来的章节涵盖了现代商业网站中非常常见的一系列功能。我们将从构建交互式网站至关重要的主题开始 - 用户管理。

第十一章:Django 中的用户身份验证

现代互动网站的重要百分比允许某种形式的用户交互-从在博客上允许简单评论,到在新闻网站上完全控制文章的编辑。如果网站提供任何形式的电子商务,对付费客户进行身份验证和授权是必不可少的。

仅仅管理用户-忘记用户名、忘记密码和保持信息更新可能会是一个真正的痛苦。作为程序员,编写身份验证系统甚至可能更糟。

幸运的是,Django 提供了默认实现来管理用户帐户、组、权限和基于 cookie 的用户会话。

与 Django 中的大多数内容一样,默认实现是完全可扩展和可定制的,以满足项目的需求。所以让我们开始吧。

概述

Django 身份验证系统处理身份验证和授权。简而言之,身份验证验证用户是否是他们声称的人,授权确定经过身份验证的用户被允许做什么。这里使用身份验证一词来指代这两个任务。

身份验证系统包括:

  • 用户

  • 权限:二进制(是/否)标志,指示用户是否可以执行某项任务

  • 组:一种将标签和权限应用于多个用户的通用方法

  • 可配置的密码哈希系统

  • 用于管理用户身份验证和授权的表单。

  • 用于登录用户或限制内容的视图工具

  • 可插拔的后端系统

Django 中的身份验证系统旨在非常通用,并且不提供一些常见的 Web 身份验证系统中常见的功能。这些常见问题的解决方案已经在第三方软件包中实现:

  • 密码强度检查

  • 登录尝试的限制

  • 针对第三方的身份验证(例如 OAuth)

使用 Django 身份验证系统

Django 的身份验证系统在其默认配置中已经发展到满足最常见的项目需求,处理了相当广泛的任务,并且对密码和权限进行了谨慎的实现。对于身份验证需求与默认设置不同的项目,Django 还支持对身份验证进行广泛的扩展和定制。

用户对象

User对象是身份验证系统的核心。它们通常代表与您的站点交互的人,并用于启用诸如限制访问、注册用户配置文件、将内容与创建者关联等功能。在 Django 的身份验证框架中只存在一类用户,即superusers或管理员staff用户只是具有特殊属性设置的用户对象,而不是不同类别的用户对象。默认用户的主要属性是:

  • “用户名”

  • “密码”

  • “电子邮件”

  • “名”

  • “姓”

创建超级用户

使用createsuperuser命令创建超级用户:

python manage.py createsuperuser -username=joe -email=joe@example.com 

系统将提示您输入密码。输入密码后,用户将立即创建。如果省略-username-email选项,系统将提示您输入这些值。

创建用户

创建和管理用户的最简单、最不容易出错的方法是通过 Django 管理员。Django 还提供了内置的视图和表单,允许用户登录、退出和更改自己的密码。我们稍后将在本章中查看通过管理员和通用用户表单进行用户管理,但首先,让我们看看如何直接处理用户身份验证。

创建用户的最直接方法是使用包含的create_user()辅助函数:

>>> from Django.contrib.auth.models import User 
>>> user = User.objects.create_user('john', 'lennon@thebeatles.com', 'johnpassword') 

# At this point, user is a User object that has already been saved 
# to the database. You can continue to change its attributes 
# if you want to change other fields. 
>>> user.last_name = 'Lennon' 
>>> user.save() 

更改密码

Django 不会在用户模型上存储原始(明文)密码,而只会存储哈希值。因此,不要尝试直接操作用户的密码属性。这就是为什么在创建用户时使用辅助函数的原因。要更改用户的密码,您有两个选项:

  • manage.py changepassword username提供了一种从命令行更改用户密码的方法。它会提示您更改给定用户的密码,您必须输入两次。如果两者匹配,新密码将立即更改。如果您没有提供用户,命令将尝试更改与当前系统用户匹配的用户的密码。

  • 您还可以使用set_password()以编程方式更改密码:

        >>> from Django.contrib.auth.models import User 
        >>> u = User.objects.get(username='john') 
        >>> u.set_password('new password') 
        >>> u.save() 

更改用户的密码将注销其所有会话,如果启用了SessionAuthenticationMiddleware

权限和授权

Django 带有一个简单的权限系统。它提供了一种将权限分配给特定用户和用户组的方法。它被 Django 管理站点使用,但欢迎您在自己的代码中使用它。Django 管理站点使用权限如下:

  • 查看add表单和添加对象的访问权限仅限于具有该类型对象的add权限的用户。

  • 查看更改列表,查看change表单和更改对象的访问权限仅限于具有该类型对象的change权限的用户。

  • 删除对象的访问权限仅限于具有该类型对象的delete权限的用户。

权限不仅可以针对对象类型设置,还可以针对特定对象实例设置。通过使用ModelAdmin类提供的has_add_permission()has_change_permission()has_delete_permission()方法,可以为同一类型的不同对象实例自定义权限。User对象有两个多对多字段:groupsuser_permissionsUser对象可以像任何其他 Django 模型一样访问其相关对象。

默认权限

当在您的INSTALLED_APPS设置中列出Django.contrib.auth时,它将确保为您安装的应用程序中定义的每个 Django 模型创建三个默认权限-添加、更改和删除。每次运行manage.py migrate时,这些权限将为所有新模型创建。

用户组

Django.contrib.auth.models.Group模型是一种通用的方式,可以对用户进行分类,以便为这些用户应用权限或其他标签。用户可以属于任意数量的组。组中的用户将自动获得该组授予的权限。例如,如果组站点编辑具有权限can_edit_home_page,则该组中的任何用户都将具有该权限。

除了权限之外,用户组是一种方便的方式,可以对用户进行分类,给他们一些标签或扩展功能。例如,您可以创建一个名为特殊用户的用户组,并编写代码,例如,让他们访问站点的仅限会员部分,或者发送他们仅限会员的电子邮件。

以编程方式创建权限

虽然可以在模型的Meta类中定义自定义权限,但也可以直接创建权限。例如,您可以在books中的BookReview模型中创建can_publish权限:

from books.models import BookReview 
from Django.contrib.auth.models import Group, Permission 
from Django.contrib.contenttypes.models import ContentType 

content_type = ContentType.objects.get_for_model(BookReview) 
permission = Permission.objects.create(codename='can_publish', 
                                       name='Can Publish Reviews', 
                                       content_type=content_type) 

然后可以通过其user_permissions属性将权限分配给User,或者通过其permissions属性将权限分配给Group

权限缓存

ModelBackend在首次需要获取权限进行权限检查后,会在User对象上缓存权限。这通常对于请求-响应周期来说是可以的,因为权限通常不会在添加后立即进行检查(例如在管理站点中)。

如果您正在添加权限并立即进行检查,例如在测试或视图中,最简单的解决方案是重新从数据库中获取User。例如:

from Django.contrib.auth.models import Permission, User 
from Django.shortcuts import get_object_or_404 

def user_gains_perms(request, user_id): 
    user = get_object_or_404(User, pk=user_id) 
    # any permission check will cache the current set of permissions 
    user.has_perm('books.change_bar') 

    permission = Permission.objects.get(codename='change_bar') 
    user.user_permissions.add(permission) 

    # Checking the cached permission set 
    user.has_perm('books.change_bar')  # False 

    # Request new instance of User 
    user = get_object_or_404(User, pk=user_id) 

    # Permission cache is repopulated from the database 
    user.has_perm('books.change_bar')  # True 

    # ... 

Web 请求中的身份验证

Django 使用会话和中间件将认证系统连接到request对象。这些为每个请求提供了一个request.user属性,表示当前用户。如果当前用户没有登录,这个属性将被设置为AnonymousUser的一个实例,否则它将是User的一个实例。你可以用is_authenticated()来区分它们,就像这样:

if request.user.is_authenticated(): 
    # Do something for authenticated users. 
else: 
    # Do something for anonymous users. 

如何登录用户

要登录用户,从视图中使用login()。它接受一个HttpRequest对象和一个User对象。login()使用 Django 的会话框架在会话中保存用户的 ID。请注意,匿名会话期间设置的任何数据在用户登录后仍保留在会话中。这个例子展示了你可能如何同时使用authenticate()login()

from Django.contrib.auth import authenticate, login 

def my_view(request): 
    username = request.POST['username'] 
    password = request.POST['password'] 
    user = authenticate(username=username, password=password) 
    if user is not None: 
        if user.is_active: 
            login(request, user) 
            # Redirect to a success page. 
        else: 
            # Return a 'disabled account' error message 
    else: 
        # Return an 'invalid login' error message. 

注意

首先调用authenticate()

当你手动登录用户时,你必须在调用login()之前调用authenticate()authenticate()设置了一个属性,指示哪个认证后端成功地认证了该用户,这些信息在登录过程中稍后是需要的。如果你尝试直接从数据库中检索用户对象登录,将会引发错误。

如何注销用户

要注销通过login()登录的用户,使用logout()在你的视图中。它接受一个HttpRequest对象,没有返回值。例如:

from Django.contrib.auth import logout 

def logout_view(request): 
    logout(request) 
    # Redirect to a success page. 

请注意,如果用户未登录,logout()不会抛出任何错误。当你调用logout()时,当前请求的会话数据将被完全清除。所有现有的数据都将被删除。这是为了防止另一个人使用相同的网络浏览器登录并访问先前用户的会话数据。

如果你想把任何东西放到会话中,用户在注销后立即可用,那就在调用logout()后这样做。

限制已登录用户的访问

原始方法

限制访问页面的简单、原始方法是检查request.user.is_authenticated(),并重定向到登录页面:

from Django.shortcuts import redirect 

def my_view(request): 
    if not request.user.is_authenticated(): 
        return redirect('/login/?next=%s' % request.path) 
    # ... 

...或显示错误消息:

from Django.shortcuts import render 

def my_view(request): 
    if not request.user.is_authenticated(): 
        return render(request, 'books/login_error.html') 
    # ... 

login_required 装饰器

作为快捷方式,你可以使用方便的login_required()装饰器:

from Django.contrib.auth.decorators import login_required 

@login_required 
def my_view(request): 
    ... 

login_required()做了以下事情:

  • 如果用户未登录,重定向到LOGIN_URL,在查询字符串中传递当前的绝对路径。例如:/accounts/login/?next=/reviews/3/

  • 如果用户已登录,正常执行视图。视图代码可以自由假设用户已登录。

默认情况下,用户在成功验证后应重定向到的路径存储在一个名为next的查询字符串参数中。如果你想使用不同的名称来使用这个参数,login_required()接受一个可选的redirect_field_name参数:

from Django.contrib.auth.decorators import login_required 

@login_required(redirect_field_name='my_redirect_field') 
def my_view(request): 
    ... 

请注意,如果你为redirect_field_name提供一个值,你很可能需要自定义你的登录模板,因为模板上下文变量存储重定向路径将使用redirect_field_name的值作为其键,而不是next(默认值)。login_required()还接受一个可选的login_url参数。例如:

from Django.contrib.auth.decorators import login_required 

@login_required(login_url='/accounts/login/') 
def my_view(request): 
    ... 

请注意,如果你没有指定login_url参数,你需要确保LOGIN_URL和你的登录视图正确关联。例如,使用默认值,将以下行添加到你的 URLconf 中:

from Django.contrib.auth import views as auth_views 

url(r'^accounts/login/$', auth_views.login), 

LOGIN_URL也接受视图函数名称和命名的 URL 模式。这允许你在 URLconf 中自由重新映射你的登录视图,而不必更新设置。

注意:login_required装饰器不会检查用户的is_active标志。

限制已登录用户的访问,通过测试

基于某些权限或其他测试来限制访问,你需要做的基本上与前一节描述的一样。简单的方法是直接在视图中对request.user运行你的测试。例如,这个视图检查用户是否在所需的域中有电子邮件:

def my_view(request): 
    if not request.user.email.endswith('@example.com'): 
        return HttpResponse("You can't leave a review for this book.") 
    # ... 

作为快捷方式,你可以使用方便的user_passes_test装饰器:

from Django.contrib.auth.decorators import user_passes_test 

def email_check(user): 
    return user.email.endswith('@example.com') 

@user_passes_test(email_check) 
def my_view(request): 
    ... 

user_passes_test()需要一个必需的参数:一个接受User对象并在用户被允许查看页面时返回True的可调用对象。请注意,user_passes_test()不会自动检查User是否匿名。user_passes_test()接受两个可选参数:

  • login_url。允许您指定未通过测试的用户将被重定向到的 URL。如果您不指定,则可能是登录页面,默认为LOGIN_URL

  • redirect_field_name。与login_required()相同。将其设置为None会将其从 URL 中删除,如果您将未通过测试的用户重定向到没有下一页的非登录页面,则可能需要这样做。

例如:

@user_passes_test(email_check, login_url='/login/') 
def my_view(request): 
    ... 

permission_required()装饰器

检查用户是否具有特定权限是一个相对常见的任务。因此,Django 为这种情况提供了一个快捷方式-permission_required()装饰器:

from Django.contrib.auth.decorators import permission_required 

@permission_required('reviews.can_vote') 
def my_view(request): 
    ... 

就像has_perm()方法一样,权限名称采用<app label>.<permission codename>的形式(例如,reviews.can_vote表示reviews应用程序中模型的权限)。装饰器也可以接受一系列权限。请注意,permission_required()还接受一个可选的login_url参数。例如:

from Django.contrib.auth.decorators import permission_required 

@permission_required('reviews.can_vote', login_url='/loginpage/') 
def my_view(request): 
    ... 

login_required()装饰器一样,login_url默认为LOGIN_URL。如果给出了raise_exception参数,装饰器将引发PermissionDenied,提示 403(HTTP 禁止)视图,而不是重定向到登录页面。

密码更改时会话失效

如果您的AUTH_USER_MODEL继承自AbstractBaseUser,或者实现了自己的get_session_auth_hash()方法,经过身份验证的会话将包括此函数返回的哈希值。在AbstractBaseUser的情况下,这是密码字段的哈希消息认证码HMAC)。

如果启用了SessionAuthenticationMiddleware,Django 会验证每个请求中发送的哈希值是否与服务器端计算的哈希值匹配。这允许用户通过更改密码注销所有会话。

Django 默认包含的密码更改视图,Django.contrib.auth.views.password_change()Django.contrib.auth管理中的user_change_password视图,会使用新密码哈希更新会话,以便用户更改自己的密码时不会注销自己。如果您有自定义的密码更改视图,并希望具有类似的行为,请使用此函数:

Django.contrib.auth.decorators.update_session_auth_hash (request, user) 

此函数接受当前请求和更新的用户对象,从中派生新会话哈希,并适当更新会话哈希。例如用法:

from Django.contrib.auth import update_session_auth_hash 

def password_change(request): 
    if request.method == 'POST': 
        form = PasswordChangeForm(user=request.user, data=request.POST) 
        if form.is_valid(): 
            form.save() 
            update_session_auth_hash(request, form.user) 
    else: 
        ... 

由于get_session_auth_hash()基于SECRET_KEY,更新站点以使用新的密钥将使所有现有会话无效。

认证视图

Django 提供了几个视图,您可以用来处理登录、注销和密码管理。这些视图使用内置的认证表单,但您也可以传入自己的表单。Django 没有为认证视图提供默认模板-但是,每个视图的文档化模板上下文如下。

在项目中实现这些视图的方法有很多种,但是,最简单和最常见的方法是在您自己的 URLconf 中包含Django.contrib.auth.urls中提供的 URLconf,例如:

urlpatterns = [url('^', include('Django.contrib.auth.urls'))] 

这将使每个视图都可以在默认 URL 上使用(在下一节中详细说明)。

所有内置视图都返回一个TemplateResponse实例,这使您可以在渲染之前轻松自定义响应数据。大多数内置认证视图都提供了 URL 名称,以便更容易地引用。

登录

登录用户。

默认 URL: /login/

可选参数:

  • template_name:用于显示用户登录视图的模板的名称。默认为registration/login.html

  • redirect_field_name:包含登录后要重定向到的 URL 的GET字段的名称。默认为next

  • authentication_form:用于身份验证的可调用对象(通常只是一个表单类)。默认为AuthenticationForm

  • current_app:指示包含当前视图的应用程序的提示。有关更多信息,请参见命名空间 URL 解析策略。

  • extra_context:一个上下文数据的字典,将被添加到传递给模板的默认上下文数据中。

以下是login的功能:

  • 如果通过GET调用,它将显示一个登录表单,该表单提交到相同的 URL。稍后会详细介绍。

  • 如果通过用户提交的凭据调用POST,它尝试登录用户。如果登录成功,视图将重定向到next参数指定的 URL。如果未提供next,它将重定向到LOGIN_REDIRECT_URL(默认为/accounts/profile/)。如果登录不成功,它重新显示登录表单。

这是您的责任为登录模板提供 HTML,默认情况下称为registration/login.html

模板上下文

  • form:代表AuthenticationFormForm对象。

  • next:成功登录后要重定向到的 URL。这也可能包含查询字符串。

  • site:根据SITE_ID设置,当前的Site。如果您没有安装站点框架,这将被设置为RequestSite的一个实例,它从当前的HttpRequest中派生站点名称和域。

  • site_namesite.name的别名。如果您没有安装站点框架,这将被设置为request.META['SERVER_NAME']的值。

如果您不希望将模板称为registration/login.html,可以通过 URLconf 中视图的额外参数传递template_name参数。

注销

注销用户。

默认 URL: /logout/

可选参数:

  • next_page:注销后重定向的 URL。

  • template_name:在用户注销后显示的模板的完整名称。如果未提供参数,则默认为registration/logged_out.html

  • redirect_field_name:包含注销后要重定向到的 URL 的GET字段的名称。默认为next。如果传递了给定的GET参数,则覆盖next_page URL。

  • current_app:指示包含当前视图的应用程序的提示。有关更多信息,请参见命名空间 URL 解析策略。

  • extra_context:一个上下文数据的字典,将被添加到传递给模板的默认上下文数据中。

模板上下文:

  • title:字符串已注销,已本地化。

  • site:根据SITE_ID设置,当前的Site。如果您没有安装站点框架,这将被设置为RequestSite的一个实例,它从当前的HttpRequest中派生站点名称和域。

  • site_namesite.name的别名。如果您没有安装站点框架,这将被设置为request.META['SERVER_NAME']的值。

  • current_app:指示包含当前视图的应用程序的提示。有关更多信息,请参见命名空间 URL 解析策略。

  • extra_context:一个上下文数据的字典,将被添加到传递给模板的默认上下文数据中。

注销然后登录

注销用户,然后重定向到登录页面。

默认 URL:未提供。

可选参数:

  • login_url:要重定向到的登录页面的 URL。如果未提供,则默认为LOGIN_URL

  • current_app:指示包含当前视图的应用程序的提示。有关更多信息,请参见命名空间 URL 解析策略。

  • extra_context:一个上下文数据的字典,将被添加到传递给模板的默认上下文数据中。

更改密码

允许用户更改他们的密码。

默认 URL: /password_change/

可选参数:

  • template_name:用于显示更改密码表单的模板的完整名称。如果未提供,则默认为registration/password_change_form.html

  • post_change_redirect:成功更改密码后要重定向到的 URL。

  • password_change_form:必须接受user关键字参数的自定义更改密码表单。该表单负责实际更改用户的密码。默认为PasswordChangeForm

  • current_app:指示包含当前视图的应用程序的提示。有关更多信息,请参阅命名空间 URL 解析策略。

  • extra_context:要添加到传递给模板的默认上下文数据的上下文数据字典。

模板上下文:

  • form:密码更改表单(请参阅上面列表中的password_change_form)。

Password_change_done

用户更改密码后显示的页面。

默认 URL: /password_change_done/

可选参数:

  • template_name:要使用的模板的完整名称。如果未提供,默认为registration/password_change_done.html

  • current_app:指示包含当前视图的应用程序的提示。有关更多信息,请参阅命名空间 URL 解析策略。

  • extra_context:要添加到传递给模板的默认上下文数据的上下文数据字典。

Password_reset

允许用户通过生成一次性使用链接来重置其密码,并将该链接发送到用户注册的电子邮件地址。

如果提供的电子邮件地址在系统中不存在,此视图不会发送电子邮件,但用户也不会收到任何错误消息。这可以防止信息泄霏给潜在的攻击者。如果您想在这种情况下提供错误消息,可以对PasswordResetForm进行子类化并使用password_reset_form参数。

标记为不可用密码的用户不允许请求密码重置,以防止在使用外部身份验证源(如 LDAP)时被滥用。请注意,他们不会收到任何错误消息,因为这会暴露其帐户的存在,但也不会发送任何邮件。

默认 URL:/password_reset/

可选参数:

  • template_name:用于显示密码重置表单的模板的完整名称。如果未提供,默认为registration/password_reset_form.html

  • email_template_name:用于生成带有重置密码链接的电子邮件的模板的完整名称。如果未提供,默认为registration/password_reset_email.html

  • subject_template_name:用于重置密码链接电子邮件主题的模板的完整名称。如果未提供,默认为registration/password_reset_subject.txt

  • password_reset_form:将用于获取要重置密码的用户的电子邮件的表单。默认为PasswordResetForm

  • token_generator:用于检查一次性链接的类的实例。默认为default_token_generator,它是Django.contrib.auth.tokens.PasswordResetTokenGenerator的实例。

  • post_reset_redirect:成功重置密码请求后要重定向到的 URL。

  • from_email:有效的电子邮件地址。默认情况下,Django 使用DEFAULT_FROM_EMAIL

  • current_app:指示包含当前视图的应用程序的提示。有关更多信息,请参阅命名空间 URL 解析策略。

  • extra_context:要添加到传递给模板的默认上下文数据的上下文数据字典。

  • html_email_template_name:用于生成带有重置密码链接的text/html多部分电子邮件的模板的完整名称。默认情况下,不发送 HTML 电子邮件。

模板上下文:

  • form:用于重置用户密码的表单(请参阅password_reset_form)。

电子邮件模板上下文:

  • emailuser.email的别名

  • user:根据email表单字段,当前的User。只有活动用户才能重置他们的密码(User.is_active is True)。

  • site_namesite.name的别名。如果没有安装站点框架,这将设置为request.META['SERVER_NAME']的值。

  • domainsite.domain的别名。如果未安装站点框架,则将设置为request.get_host()的值。

  • protocol:http 或 https

  • uid:用户的 base 64 编码的主键。

  • token:用于检查重置链接是否有效的令牌。

示例registration/password_reset_email.html(电子邮件正文模板):

Someone asked for password reset for email {{ email }}. Follow the link below: 
{{ protocol}}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %} 

主题模板使用相同的模板上下文。主题必须是单行纯文本字符串。

Password_reset_done

用户收到重置密码链接的电子邮件后显示的页面。如果password_reset()视图没有显式设置post_reset_redirect URL,则默认调用此视图。默认 URL: /password_reset_done/

注意

如果提供的电子邮件地址在系统中不存在,用户处于非活动状态,或者密码无法使用,则用户仍将被重定向到此视图,但不会发送电子邮件。

可选参数:

  • template_name:要使用的模板的完整名称。如果未提供,则默认为registration/password_reset_done.html

  • current_app:提示当前视图所在的应用程序。有关更多信息,请参阅命名空间 URL 解析策略。

  • extra_context:要添加到模板传递的默认上下文数据的上下文数据字典。

Password_reset_confirm

提供一个输入新密码的表单。

默认 URL: /password_reset_confirm/

可选参数:

  • uidb64:用户 ID 以 base 64 编码。默认为None

  • token:用于检查密码是否有效的令牌。默认为None

  • template_name:要显示确认密码视图的模板的完整名称。默认值为registration/password_reset_confirm.html

  • token_generator:用于检查密码的类的实例。这将默认为default_token_generator,它是Django.contrib.auth.tokens.PasswordResetTokenGenerator的实例。

  • set_password_form:将用于设置密码的表单。默认为SetPasswordForm

  • post_reset_redirect:密码重置完成后要重定向的 URL。默认为None

  • current_app:提示当前视图所在的应用程序。有关更多信息,请参阅命名空间 URL 解析策略。

  • extra_context:要添加到模板传递的默认上下文数据的上下文数据字典。

模板上下文:

  • form:用于设置新用户密码的表单(参见set_password_form)。

  • validlink:布尔值,如果链接(uidb64token的组合)有效或尚未使用,则为 True。

Password_reset_complete

显示一个视图,通知用户密码已成功更改。

默认 URL: /password_reset_complete/

可选参数:

  • template_name:要显示视图的模板的完整名称。默认为registration/password_reset_complete.html

  • current_app:提示当前视图所在的应用程序。有关更多信息,请参阅命名空间 URL 解析策略。

  • extra_context:要添加到模板传递的默认上下文数据的上下文数据字典。

redirect_to_login辅助函数

Django 提供了一个方便的函数redirect_to_login,可用于在视图中实现自定义访问控制。它重定向到登录页面,然后在成功登录后返回到另一个 URL。

必需参数:

  • next:成功登录后要重定向到的 URL。

可选参数:

  • login_url:要重定向到的登录页面的 URL。如果未提供,则默认为LOGIN_URL

  • redirect_field_name:包含注销后要重定向到的 URL 的GET字段的名称。如果传递了给定的GET参数,则覆盖next

内置表单

如果您不想使用内置视图,但希望方便地不必为此功能编写表单,认证系统提供了位于Django.contrib.auth.forms中的几个内置表单(表 11-1)。

内置的身份验证表单对其正在使用的用户模型做出了某些假设。如果您使用自定义用户模型,则可能需要为身份验证系统定义自己的表单。

表单名称 描述
AdminPasswordChangeForm 用于在管理员界面更改用户密码的表单。以user作为第一个位置参数。
AuthenticationForm 用于登录用户的表单。以request作为其第一个位置参数,存储在子类中供使用。
PasswordChangeForm 允许用户更改密码的表单。
PasswordResetForm 用于生成和发送一次性使用链接以重置用户密码的表单。
SetPasswordForm 允许用户在不输入旧密码的情况下更改密码的表单。
UserChangeForm 用于在管理员界面更改用户信息和权限的表单。
UserCreationForm 用于创建新用户的表单。

表 11.1:Django 内置的身份验证表单

在模板中验证数据

当您使用RequestContext时,当前登录的用户及其权限将在模板上下文中提供。

用户

在渲染模板RequestContext时,当前登录的用户,即User实例或AnonymousUser实例,存储在模板变量中

{{ user }}

{% if user.is_authenticated %} 
    <p>Welcome, {{ user.username }}. Thanks for logging in.</p> 
{% else %} 
    <p>Welcome, new user. Please log in.</p> 
{% endif %} 

如果未使用RequestContext,则此模板上下文变量不可用。

权限

当前登录用户的权限存储在模板变量中

{{ perms }}。这是Django.contrib.auth.context_processors.PermWrapper的实例,它是权限的模板友好代理。在{{ perms }}对象中,单属性查找是User.has_module_perms的代理。如果已登录用户在foo应用程序中具有任何权限,则此示例将显示True

{{ perms.foo }} 

两级属性查找是User.has_perm的代理。如果已登录用户具有权限foo.can_vote,则此示例将显示True

{{ perms.foo.can_vote }} 

因此,您可以在模板中使用{% if %}语句检查权限:

{% if perms.foo %} 
    <p>You have permission to do something in the foo app.</p> 
    {% if perms.foo.can_vote %} 
        <p>You can vote!</p> 
    {% endif %} 
    {% if perms.foo.can_drive %} 
        <p>You can drive!</p> 
    {% endif %} 
{% else %} 
    <p>You don't have permission to do anything in the foo app.</p> 
{% endif %} 

也可以通过{% if in %}语句查找权限。例如:

{% if 'foo' in perms %} 
    {% if 'foo.can_vote' in perms %} 
        <p>In lookup works, too.</p> 
    {% endif %} 
{% endif %} 

在管理员中管理用户

当您安装了Django.contrib.adminDjango.contrib.auth时,管理员提供了一种方便的方式来查看和管理用户、组和权限。用户可以像任何 Django 模型一样创建和删除。可以创建组,并且可以将权限分配给用户或组。还会存储和显示在管理员中对模型的用户编辑的日志。

创建用户

您应该在主管理员索引页面的Auth部分中看到Users的链接。如果单击此链接,您应该看到用户管理屏幕(图 11.1)。

创建用户

图 11.1:Django 管理员用户管理屏幕

添加用户管理员页面与标准管理员页面不同,它要求您在允许编辑用户其余字段之前选择用户名和密码(图 11.2)。

注意

如果要求用户帐户能够使用 Django 管理员网站创建用户,则需要给他们添加用户和更改用户的权限(即添加用户更改用户权限)。如果帐户有添加用户的权限但没有更改用户的权限,则该帐户将无法添加用户。

为什么?因为如果您有添加用户的权限,您就有创建超级用户的权限,然后可以改变其他用户。因此,Django 要求添加和更改权限作为一种轻微的安全措施。

创建用户

图 11.2:Django 管理员添加用户屏幕

更改密码

用户密码不会在管理员界面中显示(也不会存储在数据库中),但密码存储细节会显示出来。在这些信息的显示中包括一个链接到一个密码更改表单,允许管理员更改用户密码(图 11.3)。

更改密码

图 11.3:更改密码的链接(已圈出)

点击链接后,您将进入更改密码表单(图 11.4)。

更改密码

图 11.4:Django 管理员更改密码表单

Django 中的密码管理

密码管理通常不应该被不必要地重新发明,Django 致力于为管理用户密码提供安全和灵活的工具集。本文档描述了 Django 如何存储密码,如何配置存储哈希,以及一些用于处理哈希密码的实用工具。

Django 如何存储密码

Django 提供了灵活的密码存储系统,并默认使用PBKDF2(更多信息请访问en.wikipedia.org/wiki/PBKDF2)。User对象的password属性是以这种格式的字符串:

<algorithm>$<iterations>$<salt>$<hash> 

这些是用于存储用户密码的组件,由美元符号分隔,并包括:哈希算法、算法迭代次数(工作因子)、随机盐和生成的密码哈希。

该算法是 Django 可以使用的一系列单向哈希或密码存储算法之一(请参阅以下代码)。迭代描述了算法在哈希上运行的次数。盐是使用的随机种子,哈希是单向函数的结果。默认情况下,Django 使用带有 SHA256 哈希的 PBKDF2 算法,这是 NIST 推荐的密码拉伸机制(更多信息请访问csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf)。这对大多数用户来说应该足够了:它非常安全,需要大量的计算时间才能破解。但是,根据您的要求,您可以选择不同的算法,甚至使用自定义算法来匹配您特定的安全情况。再次强调,大多数用户不应该需要这样做-如果您不确定,您可能不需要。

如果您这样做,请继续阅读:Django 通过查询PASSWORD_HASHERS设置来选择要使用的算法。这是一个哈希算法类的列表,该 Django 安装支持。此列表中的第一个条目(即settings.PASSWORD_HASHERS[0])将用于存储密码,而所有其他条目都是可以用于检查现有密码的有效哈希算法。

这意味着如果您想使用不同的算法,您需要修改PASSWORD_HASHERS,将您首选的算法列在列表的第一位。PASSWORD_HASHERS的默认值是:

PASSWORD_HASHERS = [
'Django.contrib.auth.hashers.PBKDF2PasswordHasher',
'Django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'Django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
'Django.contrib.auth.hashers.BCryptPasswordHasher',
'Django.contrib.auth.hashers.SHA1PasswordHasher',
'Django.contrib.auth.hashers.MD5PasswordHasher',
'Django.contrib.auth.hashers.CryptPasswordHasher',
]

这意味着 Django 将使用 PBKDF2 来存储所有密码,但将支持检查使用 PBKDF2SHA1、Bcrypt、SHA1 等存储的密码。接下来的几节描述了高级用户可能希望修改此设置的一些常见方法。

使用 Django 的 Bcrypt

Bcrypt(更多信息请访问en.wikipedia.org/wiki/Bcrypt)是一种流行的密码存储算法,专门设计用于长期密码存储。它不是 Django 的默认算法,因为它需要使用第三方库,但由于许多人可能想要使用它,Django 支持 Bcrypt,而且只需很少的努力。

要将 Bcrypt 作为默认存储算法,请执行以下操作:

  1. 安装bcrypt库。可以通过运行pip install Django[bcrypt]来完成,或者通过下载该库并使用python setup.py install进行安装。

  2. 修改PASSWORD_HASHERS,将BCryptSHA256PasswordHasher列在第一位。也就是说,在您的设置文件中,您需要添加:

    PASSWORD_HASHERS = [ 
        'Django.contrib.auth.hashers.BCryptSHA256PasswordHasher', 
        'Django.contrib.auth.hashers.BCryptPasswordHasher', 
        'Django.contrib.auth.hashers.PBKDF2PasswordHasher', 
        'Django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', 
        'Django.contrib.auth.hashers.SHA1PasswordHasher', 
        'Django.contrib.auth.hashers.MD5PasswordHasher', 
        'Django.contrib.auth.hashers.CryptPasswordHasher', 
] 

(您需要保留此列表中的其他条目,否则 Django 将无法升级密码;请参阅以下部分)。

就是这样-现在您的 Django 安装将使用 Bcrypt 作为默认存储算法。

BCryptPasswordHasher 的密码截断

Bcrypt 的设计者将所有密码截断为 72 个字符,这意味着bcrypt(具有 100 个字符的密码)== bcrypt(具有 100 个字符的密码[:72])。 原始的BCryptPasswordHasher没有任何特殊处理,因此也受到此隐藏密码长度限制的影响。 BCryptSHA256PasswordHasher通过首先使用 sha256 对密码进行哈希来修复此问题。 这可以防止密码截断,因此应优先于BCryptPasswordHasher

这种截断的实际影响非常小,因为普通用户的密码长度不超过 72 个字符,即使在 72 个字符处被截断,以任何有用的时间内暴力破解 Bcrypt 所需的计算能力仍然是天文数字。 尽管如此,我们仍建议您出于宁愿安全也不要抱歉的原则使用BCryptSHA256PasswordHasher

其他 Bcrypt 实现

有几种其他实现允许 Bcrypt 与 Django 一起使用。 Django 的 Bcrypt 支持与这些实现不兼容。 要升级,您需要修改数据库中的哈希值,使其形式为bcrypt$(原始 bcrypt 输出)

增加工作因素

PBKDF2 和 Bcrypt 算法使用多个迭代或哈希轮。 这故意减慢攻击者的速度,使攻击哈希密码变得更加困难。 但是,随着计算能力的增加,迭代次数需要增加。

Django 开发团队选择了一个合理的默认值(并将在每个 Django 版本发布时增加),但您可能希望根据安全需求和可用处理能力进行调整。 要这样做,您将对适当的算法进行子类化,并覆盖iterations参数。

例如,要增加默认 PBKDF2 算法使用的迭代次数:

  1. 创建Django.contrib.auth.hashers.PBKDF2PasswordHasher的子类:
    from Django.contrib.auth.hashers
        import PBKDF2PasswordHasher 

    class MyPBKDF2PasswordHasher(PBKDF2PasswordHasher):  
        iterations = PBKDF2PasswordHasher.iterations * 100 

  1. 将此保存在项目的某个位置。 例如,您可以将其放在类似myproject/hashers.py的文件中。

  2. 将新的哈希器添加为PASSWORD_HASHERS中的第一个条目:

    PASSWORD_HASHERS = [ 
      'myproject.hashers.MyPBKDF2PasswordHasher', 
      'Django.contrib.auth.hashers.PBKDF2PasswordHasher', 

      # ... # 
      ] 

就是这样-现在您的 Django 安装将在使用 PBKDF2 存储密码时使用更多迭代。

密码升级

当用户登录时,如果他们的密码存储使用的算法与首选算法不同,Django 将自动升级算法为首选算法。 这意味着旧版 Django 将随着用户登录自动变得更安全,也意味着您可以在发明新的(更好的)存储算法时切换到新的存储算法。

但是,Django 只能升级使用PASSWORD_HASHERS中提到的算法的密码,因此在升级到新系统时,您应确保永远不要删除此列表中的条目。 如果这样做,使用未提及算法的用户将无法升级。 更改 PBKDF2 迭代计数时将升级密码。

手动管理用户的密码

Django.contrib.auth.hashers模块提供了一组函数来创建和验证哈希密码。 您可以独立于User模型使用它们。

如果您想要通过比较数据库中的哈希密码和明文密码手动验证用户,请使用check_password()函数。 它接受两个参数:要检查的明文密码和要检查的数据库中用户password字段的完整值,并在它们匹配时返回True,否则返回False

make_password()创建了一个使用此应用程序的格式的哈希密码。 它接受一个必需参数:明文密码。

如果您不想使用默认值(PASSWORD_HASHERS设置的第一个条目),可以选择提供盐和哈希算法来使用。当前支持的算法有:'pbkdf2_sha256''pbkdf2_sha1''bcrypt_sha256''bcrypt''sha1''md5''unsalted_md5'(仅用于向后兼容)和'crypt'(如果已安装crypt库)。

如果密码参数为None,则返回一个不可用的密码(永远不会被check_password()接受的密码)。

is_password_usable()检查给定的字符串是否是一个经过哈希处理的密码,有可能通过check_password()进行验证。

在 Django 中自定义身份验证

Django 自带的身份验证对于大多数常见情况已经足够好,但您可能有一些默认设置无法满足的需求。要根据项目的需求自定义身份验证,需要了解所提供系统的哪些部分是可扩展或可替换的。

身份验证后端提供了一个可扩展的系统,用于当需要对与用户模型中存储的用户名和密码进行身份验证的服务进行不同于 Django 默认的身份验证时。您可以为您的模型提供自定义权限,可以通过 Django 的授权系统进行检查。您可以扩展默认的用户模型,或者替换完全自定义的模型。

其他身份验证源

有时您可能需要连接到另一个身份验证源,即另一个用户名和密码或身份验证方法的源。

例如,您的公司可能已经设置了一个 LDAP,用于存储每个员工的用户名和密码。如果用户在 LDAP 和基于 Django 的应用程序中有单独的帐户,这对网络管理员和用户本身都是一种麻烦。

因此,为了处理这样的情况,Django 身份验证系统允许您插入其他身份验证源。您可以覆盖 Django 的默认基于数据库的方案,或者您可以与其他系统一起使用默认系统。

指定身份验证后端

在幕后,Django 维护一个身份验证后端列表,用于进行身份验证检查。当有人调用authenticate()时(如前一节中描述的登录用户),Django 尝试在所有身份验证后端上进行身份验证。如果第一种身份验证方法失败,Django 尝试第二种方法,依此类推,直到尝试了所有后端。

要使用的身份验证后端列表在AUTHENTICATION_BACKENDS设置中指定。这应该是一个指向知道如何进行身份验证的 Python 类的 Python 路径名称列表。这些类可以位于 Python 路径的任何位置。默认情况下,AUTHENTICATION_BACKENDS设置为:

['Django.contrib.auth.backends.ModelBackend'] 

这是基本的身份验证后端,它检查 Django 用户数据库并查询内置权限。它不提供通过任何速率限制机制防止暴力攻击。您可以在自定义授权后端中实现自己的速率限制机制,或者使用大多数 Web 服务器提供的机制。AUTHENTICATION_BACKENDS的顺序很重要,因此如果相同的用户名和密码在多个后端中有效,Django 将在第一个正面匹配时停止处理。如果后端引发PermissionDenied异常,身份验证将立即失败。Django 不会检查后续的后端。

用户经过身份验证后,Django 会在用户的会话中存储用于对用户进行身份验证的后端,并在需要访问当前经过身份验证的用户时重复使用相同的后端。这实际上意味着身份验证源是基于每个会话进行缓存的,因此如果您更改了AUTHENTICATION_BACKENDS,则需要清除会话数据,以便强制用户使用不同的方法重新进行身份验证。一个简单的方法就是执行Session.objects.all().delete()

编写认证后端

认证后端是实现两个必需方法的类:get_user(user_id)authenticate(**credentials),以及一组可选的与权限相关的授权方法。get_user方法接受一个user_id(可以是用户名、数据库 ID 或其他任何内容,但必须是User对象的主键)并返回一个User对象。authenticate方法以关键字参数的形式接受凭据。大多数情况下,它看起来会像这样:

class MyBackend(object): 
    def authenticate(self, username=None, password=None): 
        # Check the username/password and return a User. 
        ... 

但它也可以验证令牌,如下所示:

class MyBackend(object): 
    def authenticate(self, token=None): 
        # Check the token and return a User. 
        ... 

无论哪种方式,authenticate都应该检查它收到的凭据,并且如果凭据有效,它应该返回与这些凭据匹配的User对象。如果它们无效,它应该返回None。Django 管理系统与本章开头描述的 Django User对象紧密耦合。

目前,处理这个问题的最佳方法是为后端中存在的每个用户创建一个 Django User对象(例如,在 LDAP 目录中,外部 SQL 数据库中等)。您可以提前编写一个脚本来执行此操作,或者您的authenticate方法可以在用户首次登录时执行此操作。

以下是一个示例后端,它根据settings.py文件中定义的用户名和密码变量进行身份验证,并在用户首次进行身份验证时创建一个 Django User对象:

from Django.conf import settings 
from Django.contrib.auth.models import User, check_password 

class SettingsBackend(object): 
    """ 
    Authenticate against the settings ADMIN_LOGIN and ADMIN_PASSWORD. 

    Use the login name, and a hash of the password. For example: 

    ADMIN_LOGIN = 'admin' 
    ADMIN_PASSWORD = 'sha1$4e987$afbcf42e21bd417fb71db8c66b321e9fc33051de' 
    """ 

    def authenticate(self, username=None, password=None): 
        login_valid = (settings.ADMIN_LOGIN == username) 
        pwd_valid = check_password(password, 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, because it won't be checked; the password 
                # from settings.py will. 
                user = User(username=username, password='password') 
                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 

处理自定义后端中的授权

自定义授权后端可以提供自己的权限。用户模型将权限查找功能(get_group_permissions()get_all_permissions()has_perm()has_module_perms())委托给实现这些功能的任何认证后端。用户获得的权限将是所有后端返回的权限的超集。换句话说,Django 授予用户任何一个后端授予的权限。

如果后端在has_perm()has_module_perms()中引发PermissionDenied异常,授权将立即失败,Django 将不会检查后续的后端。前面提到的简单后端可以相当简单地为管理员实现权限:

class SettingsBackend(object): 
    ... 
    def has_perm(self, user_obj, perm, obj=None): 
        if user_obj.username == settings.ADMIN_LOGIN: 
            return True 
        else: 
            return False 

这为在前面的示例中获得访问权限的用户提供了完全的权限。请注意,除了与相关的User函数给出的相同参数之外,后端授权函数都将匿名用户作为参数。

完整的授权实现可以在Django/contrib/auth/backends.py中的ModelBackend类中找到,这是默认的后端,大部分时间查询auth_permission表。如果您希望仅为后端 API 的部分提供自定义行为,可以利用 Python 继承并子类化ModelBackend,而不是在自定义后端中实现完整的 API。

匿名用户的授权

匿名用户是未经认证的用户,也就是说他们没有提供有效的认证详细信息。但是,这并不一定意味着他们没有获得任何授权。在最基本的级别上,大多数网站允许匿名用户浏览大部分网站,并且许多网站允许匿名发布评论等。

Django 的权限框架没有存储匿名用户的权限的地方。但是,传递给认证后端的用户对象可能是Django.contrib.auth.models.AnonymousUser对象,允许后端为匿名用户指定自定义授权行为。

这对于可重用应用程序的作者特别有用,他们可以将所有授权问题委托给认证后端,而不需要设置来控制匿名访问。

未激活用户的授权

未激活用户是已经认证但其属性is_active设置为False的用户。但是,这并不意味着他们没有获得任何授权。例如,他们被允许激活他们的帐户。

权限系统中对匿名用户的支持允许匿名用户执行某些操作,而未激活的经过身份验证的用户则不行。不要忘记在自己的后端权限方法中测试用户的is_active属性。

处理对象权限

Django 的权限框架为对象权限奠定了基础,尽管核心中没有对其进行实现。这意味着检查对象权限将始终返回False或空列表(取决于所执行的检查)。身份验证后端将为每个对象相关的授权方法接收关键字参数objuser_obj,并根据需要返回对象级别的权限。

自定义权限

要为给定模型对象创建自定义权限,请使用permissions模型 Meta 属性。这个示例任务模型创建了三个自定义权限,即用户可以或不可以对任务实例执行的操作,特定于您的应用程序:

class Task(models.Model): 
    ... 
    class Meta: 
        permissions = ( 
            ("view_task", "Can see available tasks"), 
            ("change_task_status", "Can change the status of tasks"), 
            ("close_task", "Can remove a task by setting its status as   
              closed"), 
        ) 

这样做的唯一作用是在运行manage.py migrate时创建这些额外的权限。当用户尝试访问应用程序提供的功能(查看任务,更改任务状态,关闭任务)时,您的代码负责检查这些权限的值。继续上面的示例,以下检查用户是否可以查看任务:

user.has_perm('app.view_task') 

扩展现有的用户模型

有两种方法可以扩展默认的User模型,而不替换自己的模型。如果您需要的更改纯粹是行为上的,并且不需要对数据库中存储的内容进行任何更改,可以创建一个基于User的代理模型。这允许使用代理模型提供的任何功能,包括默认排序、自定义管理器或自定义模型方法。

如果您希望存储与“用户”相关的信息,可以使用一个一对一的关系到一个包含额外信息字段的模型。这个一对一模型通常被称为配置文件模型,因为它可能存储有关站点用户的非认证相关信息。例如,您可以创建一个员工模型:

from Django.contrib.auth.models import User 

class Employee(models.Model): 
    user = models.OneToOneField(User) 
    department = models.CharField(max_length=100) 

假设已经存在一个名为 Fred Smith 的员工,他既有一个用户模型又有一个员工模型,您可以使用 Django 的标准相关模型约定访问相关信息:

>>> u = User.objects.get(username='fsmith') 
>>> freds_department = u.employee.department 

要将配置文件模型的字段添加到管理员中的用户页面中,可以在应用程序的admin.py中定义一个InlineModelAdmin(在本例中,我们将使用StackedInline),并将其添加到注册了User类的UserAdmin类中:

from Django.contrib import admin 
from Django.contrib.auth.admin import UserAdmin 
from Django.contrib.auth.models import User 

from my_user_profile_app.models import Employee 

# Define an inline admin descriptor for Employee model 
# which acts a bit like a singleton 
class EmployeeInline(admin.StackedInline): 
    model = Employee 
    can_delete = False 
    verbose_name_plural = 'employee' 

# Define a new User admin 
class UserAdmin(UserAdmin): 
    inlines = (EmployeeInline, ) 

# Re-register UserAdmin 
admin.site.unregister(User) 
admin.site.register(User, UserAdmin)

这些配置文件模型在任何方面都不特殊-它们只是恰好与用户模型有一对一的链接的 Django 模型。因此,它们在创建用户时不会自动创建,但可以使用Django.db.models.signals.post_save来创建或更新相关模型。

请注意,使用相关模型会导致额外的查询或连接以检索相关数据,并且根据您的需求,替换用户模型并添加相关字段可能是更好的选择。但是,项目应用程序中对默认用户模型的现有链接可能会证明额外的数据库负载是合理的。

替换自定义用户模型

某些类型的项目可能对 Django 内置的User模型的身份验证要求不太合适。例如,在某些站点上,使用电子邮件地址作为您的标识令牌可能更有意义,而不是使用用户名。Django 允许您通过为AUTH_USER_MODEL设置提供引用自定义模型的值来覆盖默认的用户模型:

AUTH_USER_MODEL = 'books.MyUser' 

这个点对描述了 Django 应用的名称(必须在INSTALLED_APPS中),以及您希望用作用户模型的 Django 模型的名称。

注意

更改AUTH_USER_MODEL对您的 Django 项目有很大影响,特别是对数据库结构。例如,如果在运行迁移后更改了AUTH_USER_MODEL,您将不得不手动更新数据库,因为它会影响许多数据库表关系的构建。除非有非常充分的理由这样做,否则不应更改您的AUTH_USER_MODEL

尽管前面的警告,Django 确实完全支持自定义用户模型,但是完整的解释超出了本书的范围。关于符合管理员标准的自定义用户应用的完整示例,以及关于自定义用户模型的全面文档可以在 Django 项目网站上找到(docs.Djangoproject.com/en/1.8/topics/auth/customizing/)。

接下来呢?

在本章中,我们已经了解了 Django 中的用户认证,内置的认证工具,以及可用的广泛定制。在下一章中,我们将涵盖创建和维护健壮应用程序的可能是最重要的工具-自动化测试。

第十二章:Django 中的测试

测试简介

像所有成熟的编程语言一样,Django 提供了内置的单元测试功能。单元测试是一种软件测试过程,其中测试软件应用程序的各个单元,以确保它们执行预期的操作。

单元测试可以在多个级别进行-从测试单个方法以查看它是否返回正确的值以及如何处理无效数据,到测试整套方法以确保一系列用户输入导致期望的结果。

单元测试基于四个基本概念:

  1. 测试装置是执行测试所需的设置。这可能包括数据库、样本数据集和服务器设置。测试装置还可能包括在测试执行后需要进行的任何清理操作。

  2. 测试用例是测试的基本单元。测试用例检查给定的输入是否导致预期的结果。

  3. 测试套件是一些测试用例或其他测试套件,作为一个组执行。

  4. 测试运行器是控制测试执行并将测试结果反馈给用户的软件程序。

软件测试是一个深入而详细的主题,本章应被视为对单元测试的简要介绍。互联网上有大量关于软件测试理论和方法的资源,我鼓励你就这个重要主题进行自己的研究。有关 Django 对单元测试方法的更详细讨论,请参阅 Django 项目网站。

引入自动化测试

什么是自动化测试?

在本书中,你一直在测试代码;也许甚至没有意识到。每当你使用 Django shell 来查看一个函数是否有效,或者查看给定输入的输出时,你都在测试你的代码。例如,在第二章中,视图和 URLconfs,我们向一个期望整数的视图传递了一个字符串,以生成TypeError异常。

测试是应用程序开发的正常部分,但自动化测试的不同之处在于系统为你完成了测试工作。你只需创建一组测试,然后在对应用程序进行更改时,可以检查你的代码是否仍然按照最初的意图工作,而无需进行耗时的手动测试。

那么为什么要创建测试?

如果创建像本书中那样简单的应用程序是你在 Django 编程中的最后一步,那么确实,你不需要知道如何创建自动化测试。但是,如果你希望成为一名专业程序员和/或在更复杂的项目上工作,你需要知道如何创建自动化测试。

创建自动化测试将会:

  • 节省时间:手动测试大型应用程序组件之间的复杂交互是耗时且容易出错的。自动化测试可以节省时间,让你专注于编程。

  • 预防问题:测试突出显示了代码的内部工作原理,因此你可以看到哪里出了问题。

  • 看起来专业:专业人士编写测试。Django 的原始开发人员之一 Jacob Kaplan-Moss 说:“没有测试的代码从设计上就是有问题的。”

  • 改善团队合作:测试可以确保同事们不会无意中破坏你的代码(而你也不会在不知情的情况下破坏他们的代码)。

基本测试策略

有许多方法可以用来编写测试。一些程序员遵循一种称为测试驱动开发的纪律;他们实际上是在编写代码之前编写他们的测试。这可能看起来有些反直觉,但事实上,这与大多数人通常会做的事情相似:他们描述一个问题,然后创建一些代码来解决它。

测试驱动开发只是在 Python 测试用例中正式化了问题。更常见的是,测试的新手会创建一些代码,然后决定它应该有一些测试。也许更好的做法是早些时候编写一些测试,但现在开始也不算太晚。

编写一个测试

要创建您的第一个测试,让我们在您的 Book 模型中引入一个错误。

假设您已经决定在您的 Book 模型上创建一个自定义方法,以指示书籍是否最近出版。您的 Book 模型可能如下所示:

import datetime 
from django.utils import timezone 

from django.db import models 

# ... # 

class Book(models.Model): 
    title = models.CharField(max_length=100) 
    authors = models.ManyToManyField(Author) 
    publisher = models.ForeignKey(Publisher) 
    publication_date = models.DateField() 

    def recent_publication(self): 
        return self.publication_date >= timezone.now().date() 
datetime.timedelta(weeks=8) 

    # ... # 

首先,我们导入了两个新模块:Python 的datetimedjango.utils中的timezone。我们需要这些模块来进行日期计算。然后,我们在Book模型中添加了一个名为recent_publication的自定义方法,该方法计算出八周前的日期,并在书籍的出版日期更近时返回 true。

所以让我们跳到交互式 shell 并测试我们的新方法:

python manage.py shell 

>>> from books.models import Book 
>>> import datetime 
>>> from django.utils import timezone 
>>> book = Book.objects.get(id=1) 
>>> book.title 
'Mastering Django: Core' 
>>> book.publication_date 
datetime.date(2016, 5, 1) 
>>>book.publication_date >= timezone.now().date()-datetime.timedelta(weeks=8) 
True 

到目前为止,一切都很顺利,我们已经导入了我们的书籍模型并检索到了一本书。今天是 2016 年 6 月 11 日,我已经在数据库中输入了我的书的出版日期为 5 月 1 日,这比八周前还要早,所以函数正确地返回了True

显然,您将不得不修改数据中的出版日期,以便在您完成这个练习时,这个练习仍然对您有效。

现在让我们看看如果我们将出版日期设置为未来的某个时间,比如说 9 月 1 日会发生什么:

>>> book.publication_date 
datetime.date(2016, 9, 1) 
>>>book.publication_date >= timezone.now().date()-datetime.timedelta(weeks=8) 
True 

哎呀!这里显然有些问题。您应该能够很快地看到逻辑上的错误-八周前之后的任何日期都将返回 true,包括未来的日期。

所以,暂且不管这是一个相当牵强的例子,现在让我们创建一个暴露我们错误逻辑的测试。

创建一个测试

当您使用 Django 的startapp命令创建了您的 books 应用程序时,它在您的应用程序目录中创建了一个名为tests.py的文件。这就是 books 应用程序的任何测试应该放置的地方。所以让我们马上开始编写一个测试:

import datetime 
from django.utils import timezone 
from django.test import TestCase 
from .models import Book 

class BookMethodTests(TestCase): 

    def test_recent_pub(self): 
""" 
        recent_publication() should return False for future publication  
        dates. 
        """ 

        futuredate = timezone.now().date() + datetime.timedelta(days=5) 
        future_pub = Book(publication_date=futuredate) 
        self.assertEqual(future_pub.recent_publication(), False) 

这应该非常简单明了,因为它几乎与我们在 Django shell 中所做的一样,唯一的真正区别是我们现在将我们的测试代码封装在一个类中,并创建了一个断言,用于测试我们的recent_publication()方法是否与未来日期相匹配。

我们将在本章后面更详细地介绍测试类和assertEqual方法-现在,我们只想在进入更复杂的主题之前,看一下测试是如何在非常基本的水平上工作的。

运行测试

现在我们已经创建了我们的测试,我们需要运行它。幸运的是,这非常容易做到,只需跳转到您的终端并键入:

python manage.py test books 

片刻之后,Django 应该打印出类似于这样的内容:

Creating test database for alias 'default'... 
F 
====================================================================== 
FAIL: test_recent_pub (books.tests.BookMethodTests) 
---------------------------------------------------------------------- 
Traceback (most recent call last): 
  File "C:\Users\Nigel\ ... mysite\books\tests.py", line 25, in test_recent_pub 
    self.assertEqual(future_pub.recent_publication(), False) 
AssertionError: True != False 

---------------------------------------------------------------------- 
Ran 1 test in 0.000s 

FAILED (failures=1) 
Destroying test database for alias 'default'... 

发生的事情是这样的:

  • Python manage.py test books在 books 应用程序中查找测试。

  • 它找到了django.test.TestCase类的一个子类

  • 它为测试目的创建了一个特殊的数据库

  • 它寻找以“test”开头的方法

  • test_recent_pub中,它创建了一个Book实例,其publication_date字段是未来的 5 天;而

  • 使用assertEqual()方法,它发现它的recent_publication()返回True,而应该返回False

测试告诉我们哪个测试失败了,甚至还告诉了失败发生的行。还要注意,如果您使用的是*nix 系统或 Mac,文件路径将会有所不同。

这就是 Django 中测试的非常基本的介绍。正如我在本章开头所说的,测试是一个深入而详细的主题,对于您作为程序员的职业非常重要。我不可能在一个章节中涵盖所有测试的方面,所以我鼓励您深入研究本章中提到的一些资源以及 Django 文档。

在本章的其余部分,我将介绍 Django 为您提供的各种测试工具。

测试工具

Django 提供了一套在编写测试时非常方便的工具。

测试客户端

测试客户端是一个 Python 类,充当虚拟网络浏览器,允许您以编程方式测试视图并与 Django 应用程序进行交互。测试客户端可以做的一些事情包括:

  • 模拟 URL 上的GETPOST请求,并观察响应-从低级 HTTP(结果标头和状态代码)到页面内容的一切。

  • 查看重定向链(如果有)并检查每一步的 URL 和状态代码。

  • 测试给定请求是否由给定的 Django 模板呈现,并且模板上下文包含某些值。

请注意,测试客户端并不打算替代 Selenium(有关更多信息,请访问seleniumhq.org/)或其他浏览器框架。Django 的测试客户端有不同的重点。简而言之:

  • 使用 Django 的测试客户端来确保正确的模板被渲染,并且模板传递了正确的上下文数据。

  • 使用浏览器框架(如 Selenium)测试呈现的 HTML 和网页的行为,即 JavaScript 功能。Django 还为这些框架提供了特殊的支持;有关更多详细信息,请参阅LiveServerTestCase部分。

全面的测试套件应该结合使用这两种测试类型。

有关 Django 测试客户端的更详细信息和示例,请参阅 Django 项目网站。

提供的 TestCase 类

普通的 Python 单元测试类扩展了unittest.TestCase的基类。Django 提供了一些这个基类的扩展:

简单的 TestCase

扩展unittest.TestCase,具有一些基本功能,如:

  • 保存和恢复 Python 警告机制的状态。

  • 添加了一些有用的断言,包括:

  • 检查可调用对象是否引发了特定异常。

  • 测试表单字段的呈现和错误处理。

  • 测试 HTML 响应中是否存在/缺少给定的片段。

  • 验证模板是否已/未用于生成给定的响应内容。

  • 验证应用程序执行了 HTTP 重定向。

  • 强大地测试两个 HTML 片段的相等性/不相等性或包含关系。

  • 强大地测试两个 XML 片段的相等性/不相等性。

  • 强大地测试两个 JSON 片段的相等性。

  • 使用修改后的设置运行测试的能力。

  • 使用测试Client

  • 自定义测试时间 URL 映射。

Transaction TestCase

Django 的TestCase类(在下一段中描述)利用数据库事务设施来加快在每个测试开始时将数据库重置为已知状态的过程。然而,这样做的一个后果是,一些数据库行为无法在 Django 的TestCase类中进行测试。

在这些情况下,您应该使用TransactionTestCaseTransactionTestCaseTestCase除了数据库重置到已知状态的方式和测试代码测试提交和回滚的效果外,两者是相同的:

  • TransactionTestCase通过截断所有表在测试运行后重置数据库。TransactionTestCase可以调用提交和回滚,并观察这些调用对数据库的影响。

  • 另一方面,TestCase在测试后不会截断表。相反,它将测试代码封装在数据库事务中,在测试结束时回滚。这保证了测试结束时的回滚将数据库恢复到其初始状态。

TransactionTestCase继承自SimpleTestCase

TestCase

这个类提供了一些对于测试网站有用的额外功能。将普通的unittest.TestCase转换为 Django 的TestCase很容易:只需将测试的基类从unittest.TestCase更改为django.test.TestCase。所有标准的 Python 单元测试功能仍然可用,但它将增加一些有用的附加功能,包括:

  • 自动加载 fixture。

  • 将测试包装在两个嵌套的atomic块中:一个用于整个类,一个用于每个测试。

  • 创建一个TestClient实例。

  • 用于测试重定向和表单错误等内容的 Django 特定断言。

TestCase继承自TransactionTestCase

LiveServerTestCase

LiveServerTestCase基本上与TransactionTestCase相同,只是多了一个功能:它在设置时在后台启动一个实时的 Django 服务器,并在拆卸时关闭它。这允许使用除 Django 虚拟客户端之外的自动化测试客户端,例如 Selenium 客户端,来在浏览器中执行一系列功能测试并模拟真实用户的操作。

测试用例特性

默认测试客户端

*TestCase实例中的每个测试用例都可以访问 Django 测试客户端的一个实例。可以将此客户端访问为self.client。每个测试都会重新创建此客户端,因此您不必担心状态(例如 cookies)从一个测试传递到另一个测试。这意味着,而不是在每个测试中实例化Client

import unittest 
from django.test import Client 

class SimpleTest(unittest.TestCase): 
    def test_details(self): 
        client = Client() 
        response = client.get('/customer/details/') 
        self.assertEqual(response.status_code, 200) 

    def test_index(self): 
        client = Client() 
        response = client.get('/customer/index/') 
        self.assertEqual(response.status_code, 200) 

...您可以像这样引用self.client

from django.test import TestCase 

class SimpleTest(TestCase): 
    def test_details(self): 
        response = self.client.get('/customer/details/') 
        self.assertEqual(response.status_code, 200) 

    def test_index(self): 
        response = self.client.get('/customer/index/') 
        self.assertEqual(response.status_code, 200) 

fixture 加载

如果数据库支持的网站的测试用例没有任何数据,则没有多大用处。为了方便地将测试数据放入数据库,Django 的自定义TransactionTestCase类提供了一种加载 fixtures 的方法。fixture 是 Django 知道如何导入到数据库中的数据集合。例如,如果您的网站有用户帐户,您可能会设置一个虚假用户帐户的 fixture,以便在测试期间填充数据库。

创建 fixture 的最直接方法是使用manage.pydumpdata命令。这假设您的数据库中已经有一些数据。有关更多详细信息,请参阅dumpdata文档。创建 fixture 并将其放置在INSTALLED_APPS中的fixtures目录中后,您可以通过在django.test.TestCase子类的fixtures类属性上指定它来在单元测试中使用它:

from django.test import TestCase 
from myapp.models import Animal 

class AnimalTestCase(TestCase): 
    fixtures = ['mammals.json', 'birds'] 

    def setUp(self): 
        # Test definitions as before. 
        call_setup_methods() 

    def testFluffyAnimals(self): 
        # A test that uses the fixtures. 
        call_some_test_code() 

具体来说,将发生以下情况:

  • 在每个测试用例开始之前,在运行setUp()之前,Django 将刷新数据库,将数据库返回到直接在调用migrate之后的状态。

  • 然后,所有命名的 fixtures 都将被安装。在此示例中,Django 将安装名为mammals的任何 JSON fixture,然后是名为birds的任何 fixture。有关定义和安装 fixtures 的更多详细信息,请参阅loaddata文档。

这个刷新/加载过程对测试用例中的每个测试都会重复进行,因此您可以确保一个测试的结果不会受到另一个测试或测试执行顺序的影响。默认情况下,fixture 只加载到default数据库中。如果您使用多个数据库并设置multi_db=True,fixture 将加载到所有数据库中。

覆盖设置

注意

使用函数在测试中临时更改设置的值。不要直接操作django.conf.settings,因为 Django 不会在此类操作后恢复原始值。

settings()

为了测试目的,通常在运行测试代码后临时更改设置并恢复到原始值是很有用的。对于这种用例,Django 提供了一个标准的 Python 上下文管理器(参见 PEP 343at www.python.org/dev/peps/pep-0343)称为settings(),可以像这样使用:

from django.test import TestCase 

class LoginTestCase(TestCase): 

    def test_login(self): 

        # First check for the default behavior 
        response = self.client.get('/sekrit/') 
        self.assertRedirects(response, '/accounts/login/?next=/sekrit/') 

        # Then override the LOGIN_URL setting 
        with self.settings(LOGIN_URL='/other/login/'): 
            response = self.client.get('/sekrit/') 
            self.assertRedirects(response, '/other/login/?next=/sekrit/') 

此示例将在with块中覆盖LOGIN_URL设置,并在之后将其值重置为先前的状态。

modify_settings()

重新定义包含值列表的设置可能会变得难以处理。实际上,添加或删除值通常就足够了。modify_settings()上下文管理器使这变得很容易:

from django.test import TestCase 

class MiddlewareTestCase(TestCase): 

    def test_cache_middleware(self): 
        with self.modify_settings(MIDDLEWARE_CLASSES={ 
'append': 'django.middleware.cache.FetchFromCacheMiddleware', 
'prepend': 'django.middleware.cache.UpdateCacheMiddleware', 
'remove': [ 
 'django.contrib.sessions.middleware.SessionMiddleware', 
 'django.contrib.auth.middleware.AuthenticationMiddleware',  
 'django.contrib.messages.middleware.MessageMiddleware', 
            ], 
        }): 
            response = self.client.get('/') 
            # ... 

对于每个操作,您可以提供一个值列表或一个字符串。当值已经存在于列表中时,appendprepend没有效果;当值不存在时,remove也没有效果。

override_settings()

如果要为测试方法覆盖设置,Django 提供了override_settings()装饰器(请参阅www.python.org/dev/peps/pep-0318的 PEP 318)。用法如下:

from django.test import TestCase, override_settings 

class LoginTestCase(TestCase): 

    @override_settings(LOGIN_URL='/other/login/') 
    def test_login(self): 
        response = self.client.get('/sekrit/') 
        self.assertRedirects(response, '/other/login/?next=/sekrit/') 

装饰器也可以应用于TestCase类:

from django.test import TestCase, override_settings 

@override_settings(LOGIN_URL='/other/login/') 
class LoginTestCase(TestCase): 

    def test_login(self): 
        response = self.client.get('/sekrit/') 
        self.assertRedirects(response, '/other/login/?next=/sekrit/') 

modify_settings()

同样,Django 还提供了modify_settings()装饰器:

from django.test import TestCase, modify_settings 

class MiddlewareTestCase(TestCase): 

    @modify_settings(MIDDLEWARE_CLASSES={ 
'append': 'django.middleware.cache.FetchFromCacheMiddleware', 
'prepend': 'django.middleware.cache.UpdateCacheMiddleware', 
    }) 
    def test_cache_middleware(self): 
        response = self.client.get('/') 
        # ... 

装饰器也可以应用于测试用例类:

from django.test import TestCase, modify_settings 

@modify_settings(MIDDLEWARE_CLASSES={ 
'append': 'django.middleware.cache.FetchFromCacheMiddleware', 
'prepend': 'django.middleware.cache.UpdateCacheMiddleware', 
}) 
class MiddlewareTestCase(TestCase): 

    def test_cache_middleware(self): 
        response = self.client.get('/') 
        # ... 

在覆盖设置时,请确保处理应用程序代码使用缓存或类似功能保留状态的情况,即使更改了设置。Django 提供了django.test.signals.setting_changed信号,让您注册回调以在更改设置时清理和重置状态。

断言

由于 Python 的普通unittest.TestCase类实现了assertTrue()assertEqual()等断言方法,Django 的自定义TestCase类提供了许多对测试 Web 应用程序有用的自定义断言方法:

  • assertRaisesMessage:断言可调用对象的执行引发了带有expected_message表示的异常。

  • assertFieldOutput:断言表单字段对各种输入的行为是否正确。

  • assertFormError:断言表单上的字段在表单上呈现时引发提供的错误列表。

  • assertFormsetError:断言formset在呈现时引发提供的错误列表。

  • assertContains:断言Response实例产生了给定的status_code,并且text出现在响应内容中。

  • assertNotContains:断言Response实例产生了给定的status_code,并且text不出现在响应内容中。

  • assertTemplateUsed:断言在呈现响应时使用了给定名称的模板。名称是一个字符串,例如'admin/index.html'

  • assertTemplateNotUsed:断言在呈现响应时未使用给定名称的模板。

  • assertRedirects:断言响应返回了status_code重定向状态,重定向到expected_url(包括任何GET数据),并且最终页面以target_status_code接收到。

  • assertHTMLEqual:断言字符串html1html2相等。比较基于 HTML 语义。比较考虑以下内容:

  • HTML 标签前后的空白会被忽略。

  • 所有类型的空白都被视为等效。

  • 所有未关闭的标签都会被隐式关闭,例如,当周围的标签关闭或 HTML 文档结束时。

  • 空标签等同于它们的自关闭版本。

  • HTML 元素的属性排序不重要。

  • 没有参数的属性等同于名称和值相等的属性(请参阅示例)。

  • assertHTMLNotEqual:断言字符串html1html2相等。比较基于 HTML 语义。详情请参阅assertHTMLEqual()

  • assertXMLEqual:断言字符串xml1xml2相等。比较基于 XML 语义。与assertHTMLEqual()类似,比较是基于解析内容的,因此只考虑语义差异,而不考虑语法差异。

  • assertXMLNotEqual:断言字符串xml1xml2相等。比较基于 XML 语义。详情请参阅assertXMLEqual()

  • assertInHTML:断言 HTML 片段needle包含在haystack中。

  • assertJSONEqual:断言 JSON 片段rawexpected_data相等。

  • assertJSONNotEqual:断言 JSON 片段rawexpected_data不相等。

  • assertQuerysetEqual:断言查询集qs返回特定的值列表values。使用transform函数执行qsvalues的内容比较;默认情况下,这意味着比较每个值的repr()

  • assertNumQueries:断言当使用*args**kwargs调用func时,将执行num个数据库查询。

电子邮件服务

如果您的 Django 视图使用 Django 的电子邮件功能发送电子邮件,您可能不希望每次使用该视图运行测试时都发送电子邮件。因此,Django 的测试运行器会自动将所有 Django 发送的电子邮件重定向到一个虚拟的 outbox。这样,您可以测试发送电子邮件的每个方面,从发送的消息数量到每个消息的内容,而无需实际发送消息。测试运行器通过透明地将正常的电子邮件后端替换为测试后端来实现这一点。(不用担心-这不会对 Django 之外的任何其他电子邮件发送者产生影响,比如您的机器邮件服务器,如果您正在运行的话。)

在测试运行期间,每封发送的电子邮件都会保存在django.core.mail.outbox中。这是所有已发送的EmailMessage实例的简单列表。outbox属性是仅在使用locmem电子邮件后端时才会创建的特殊属性。它通常不作为django.core.mail模块的一部分存在,也不能直接导入。以下代码显示了如何正确访问此属性。以下是一个检查django.core.mail.outbox长度和内容的示例测试:

from django.core import mail 
from django.test import TestCase 

class EmailTest(TestCase): 
    def test_send_email(self): 
        # Send message. 
        mail.send_mail('Subject here', 'Here is the message.', 
'from@example.com', ['to@example.com'], 
            fail_silently=False) 

        # Test that one message has been sent. 
        self.assertEqual(len(mail.outbox), 1) 

        # Verify that the subject of the first message is correct. 
        self.assertEqual(mail.outbox[0].subject, 'Subject here') 

如前所述,在 Django 的*TestCase中,测试 outbox 在每个测试开始时都会被清空。要手动清空 outbox,请将空列表分配给mail.outbox

from django.core import mail 

# Empty the test outbox 
mail.outbox = [] 

管理命令

可以使用call_command()函数测试管理命令。输出可以重定向到StringIO实例中:

from django.core.management import call_command 
from django.test import TestCase 
from django.utils.six import StringIO 

class ClosepollTest(TestCase): 
    def test_command_output(self): 
        out = StringIO() 
        call_command('closepoll', stdout=out) 
        self.assertIn('Expected output', out.getvalue()) 

跳过测试

unittest库提供了@skipIf@skipUnless装饰器,允许您在预先知道这些测试在特定条件下会失败时跳过测试。例如,如果您的测试需要特定的可选库才能成功,您可以使用@skipIf装饰测试用例。然后,测试运行器将报告该测试未被执行以及原因,而不是失败测试或完全省略测试。

测试数据库

需要数据库的测试(即模型测试)不会使用生产数据库;测试时会为其创建单独的空白数据库。无论测试是否通过,测试数据库在所有测试执行完毕时都会被销毁。您可以通过在测试命令中添加-keepdb标志来阻止测试数据库被销毁。这将在运行之间保留测试数据库。

如果数据库不存在,将首先创建它。任何迁移也将被应用以保持数据库的最新状态。默认情况下,测试数据库的名称是在DATABASES中定义的数据库的NAME设置值前加上test_。在使用 SQLite 数据库引擎时,默认情况下测试将使用内存数据库(即,数据库将在内存中创建,完全绕过文件系统!)。

如果要使用不同的数据库名称,请在DATABASES中为任何给定数据库的TEST字典中指定NAME。在 PostgreSQL 上,USER还需要对内置的postgres数据库具有读取权限。除了使用单独的数据库外,测试运行器将使用与设置文件中相同的数据库设置:ENGINEUSERHOST等。测试数据库由USER指定的用户创建,因此您需要确保给定的用户帐户具有在系统上创建新数据库的足够权限。

使用不同的测试框架

显然,unittest并不是唯一的 Python 测试框架。虽然 Django 不提供对替代框架的显式支持,但它提供了一种调用为替代框架构建的测试的方式,就像它们是普通的 Django 测试一样。

当您运行./manage.py test时,Django 会查看TEST_RUNNER设置以确定要执行的操作。默认情况下,TEST_RUNNER指向django.test.runner.DiscoverRunner。这个类定义了默认的 Django 测试行为。这种行为包括:

  1. 执行全局的测试前设置。

  2. 在当前目录中查找任何以下文件中的测试,其名称与模式test*.py匹配。

  3. 创建测试数据库。

  4. 运行迁移以将模型和初始数据安装到测试数据库中。

  5. 运行找到的测试。

  6. 销毁测试数据库。

  7. 执行全局的测试后拆卸。

如果您定义自己的测试运行器类并将TEST_RUNNER指向该类,Django 将在运行./manage.py test时执行您的测试运行器。

通过这种方式,可以使用任何可以从 Python 代码执行的测试框架,或者修改 Django 测试执行过程以满足您可能有的任何测试要求。

请查看 Django 项目网站,了解更多关于使用不同测试框架的信息。

接下来呢?

现在您已经知道如何为您的 Django 项目编写测试,一旦您准备将项目变成一个真正的网站,我们将继续讨论一个非常重要的话题-将 Django 部署到 Web 服务器。

第十三章:部署 Django

本章涵盖了构建 Django 应用程序的最后一个基本步骤:将其部署到生产服务器。

如果您一直在跟着我们的示例,您可能一直在使用 runserver,这使得事情变得非常容易-使用 runserver,您不必担心 web 服务器的设置。但是 runserver 仅适用于在本地机器上进行开发,而不适用于在公共网络上暴露。

要部署您的 Django 应用程序,您需要将其连接到像 Apache 这样的工业级 Web 服务器。在本章中,我们将向您展示如何做到这一点-但首先,我们将为您提供一个在您上线之前在您的代码库中要做的事情的清单。

为生产准备您的代码库

部署清单

互联网是一个敌对的环境。在部署 Django 项目之前,您应该花些时间审查您的设置,考虑安全性、性能和操作。

Django 包含许多安全功能。有些是内置的并且始终启用。其他是可选的,因为它们并不总是合适的,或者因为它们对开发来说不方便。例如,强制使用 HTTPS 可能不适用于所有网站,并且对于本地开发来说是不切实际的。

性能优化是另一类便利性的权衡。例如,在生产中缓存很有用,但在本地开发中不那么有用。错误报告的需求也是非常不同的。以下清单包括以下设置:

  • 必须正确设置才能让 Django 提供预期的安全级别,

  • 在每个环境中都有所不同,

  • 启用可选的安全功能,

  • 启用性能优化;和,

  • 提供错误报告。

许多这些设置是敏感的,应该被视为机密。如果您发布项目的源代码,一个常见的做法是发布适合开发的设置,并为生产使用私有设置模块。可以使用所描述的检查来自动化以下检查

-deploy 选项的 check 命令。务必根据选项的文档描述运行它针对您的生产设置文件。

关键设置

SECRET_KEY

秘钥必须是一个大的随机值,并且必须保密。

确保在生产中使用的密钥没有在其他任何地方使用,并且避免将其提交到源代码控制。这减少了攻击者可能获取密钥的向量数量。考虑从环境变量中加载秘密密钥,而不是在设置模块中将秘密密钥硬编码:

import os
SECRET_KEY = os.environ['SECRET_KEY']

或者从一个文件:

with open('/etc/secret_key.txt') as f:
SECRET_KEY = f.read().strip()

调试

您绝对不能在生产中启用调试。

当我们在第一章 Django 简介 和入门中创建项目时,django-admin startproject 命令创建了一个带有 DEBUG 设置为 Truesettings.py 文件。Django 的许多内部部分都会检查此设置,并在 DEBUG 模式开启时改变它们的行为。

例如,如果 DEBUG 设置为 True,那么:

  • 所有数据库查询将被保存在内存中作为对象 django.db.connection.queries。你可以想象,这会消耗内存!

  • 任何 404 错误都将由 Django 的特殊 404 错误页面(在第三章中介绍,模板)呈现,而不是返回正确的 404 响应。这个页面包含潜在的敏感信息,不应该暴露在公共互联网上。

  • 您的 Django 应用程序中的任何未捕获异常-从基本的 Python 语法错误到数据库错误和模板语法错误-都将由您可能已经了解和喜爱的 Django 漂亮错误页面呈现。这个页面包含的敏感信息甚至比 404 页面还要多,绝不能暴露给公众。

简而言之,将DEBUG设置为True告诉 Django 假设只有可信任的开发人员在使用您的网站。互联网上充满了不可信任的流氓,当您准备部署应用程序时,第一件事就是将DEBUG设置为False

特定于环境的设置

ALLOWED_HOSTS

DEBUG = False时,Django 在没有适当的ALLOWED_HOSTS值的情况下根本无法工作。这个设置是必需的,以保护您的网站免受一些 CSRF 攻击。如果您使用通配符,您必须执行自己的Host HTTP 头的验证,或者确保您不容易受到这类攻击的影响。

缓存

如果您使用缓存,连接参数在开发和生产中可能不同。缓存服务器通常具有弱身份验证。确保它们只接受来自应用服务器的连接。如果您使用Memcached,考虑使用缓存会话以提高性能。

数据库

开发和生产中的数据库连接参数可能不同。数据库密码非常敏感。您应该像保护SECRET_KEY一样保护它们。为了最大的安全性,请确保数据库服务器只接受来自应用服务器的连接。如果您还没有为数据库设置备份,请立即进行设置!

EMAIL_BACKEND 和相关设置

如果您的网站发送电子邮件,这些值需要正确设置。

STATIC_ROOT 和 STATIC_URL

静态文件由开发服务器自动提供。在生产中,您必须定义一个STATIC_ROOT目录,collectstatic将在其中复制它们。

MEDIA_ROOT 和 MEDIA_URL

媒体文件是由您的用户上传的。它们是不受信任的!确保您的 Web 服务器永远不会尝试解释它们。例如,如果用户上传了一个.php文件,Web 服务器不应该执行它。现在是检查这些文件的备份策略的好时机。

HTTPS

任何允许用户登录的网站都应强制执行全站 HTTPS,以避免在明文中传输访问令牌。在 Django 中,访问令牌包括登录/密码、会话 cookie 和密码重置令牌。(如果通过电子邮件发送它们,你无法保护密码重置令牌。)

保护敏感区域,如用户帐户或管理员是不够的,因为相同的会话 cookie 用于 HTTP 和 HTTPS。您的 Web 服务器必须将所有 HTTP 流量重定向到 HTTPS,并且只将 HTTPS 请求传输到 Django。设置 HTTPS 后,启用以下设置。

将其设置为True,以避免意外通过 HTTP 传输 CSRF cookie。

将其设置为True,以避免意外通过 HTTP 传输会话 cookie。

性能优化

DEBUG = False设置为禁用一些仅在开发中有用的功能。此外,您可以调整以下设置。

CONN_MAX_AGE

启用持久数据库连接可以在连接到数据库占请求处理时间的重要部分时获得良好的加速。这在网络性能有限的虚拟化主机上非常有帮助。

模板

启用缓存模板加载器通常会大大提高性能,因为它避免了每次需要呈现模板时都要编译模板。有关更多信息,请参阅模板加载器文档。

错误报告

当您将代码推送到生产环境时,希望它是健壮的,但您不能排除意外错误。幸运的是,Django 可以捕获错误并相应地通知您。

日志记录

在将网站投入生产之前,请检查您的日志配置,并在收到一些流量后立即检查它是否按预期工作。

ADMINS 和 MANAGERS

ADMINS将通过电子邮件收到 500 错误的通知。MANAGERS将收到 404 错误的通知。IGNORABLE_404_URLS可以帮助过滤掉虚假报告。

通过电子邮件进行错误报告并不是很有效。在您的收件箱被报告淹没之前,考虑使用 Sentry 等错误监控系统(有关更多信息,请访问 sentry.readthedocs.org/en/latest/)。Sentry 还可以聚合日志。

自定义默认错误视图

Django 包括了几个 HTTP 错误代码的默认视图和模板。您可能希望通过在根模板目录中创建以下模板来覆盖默认模板:404.html500.html403.html400.html。默认视图应该适用于 99% 的 Web 应用程序,但如果您希望自定义它们,请参阅这些指令(docs.djangoproject.com/en/1.8/topics/http/views/#customizing-error-views),其中还包含有关默认模板的详细信息:

  • http_not_found_view

  • http_internal_server_error_view

  • http_forbidden_view

  • http_bad_request_view

使用虚拟环境

如果您在虚拟环境中安装了项目的 Python 依赖项(有关更多信息,请访问 www.virtualenv.org/),您还需要将此虚拟环境的 site-packages 目录的路径添加到您的 Python 路径中。为此,添加一个额外的路径到您的 WSGIPythonPath 指令,如果使用类 UNIX 系统,则使用冒号(:)分隔多个路径,如果使用 Windows,则使用分号(;)分隔。如果目录路径的任何部分包含空格字符,则必须引用完整的参数字符串:

WSGIPythonPath /path/to/mysite.com:/path/to/your/venv/lib/python3.X/site-packages 

确保您提供正确的虚拟环境路径,并用正确的 Python 版本(例如 python3.4)替换 python3.X

在生产中使用不同的设置

到目前为止,在本书中,我们只处理了一个设置文件:由 django-admin startproject 生成的 settings.py。但是当您准备部署时,您可能会发现自己需要多个设置文件,以保持开发环境与生产环境隔离。(例如,您可能不希望在本地机器上测试代码更改时将 DEBUGFalse 更改为 True。)Django 通过允许您使用多个设置文件来使这一切变得非常容易。如果您希望将设置文件组织成生产和开发设置,您可以通过以下三种方式之一来实现:

  • 设置两个完全独立的设置文件。

  • 设置一个基本设置文件(比如开发),以及一个第二个(比如生产)设置文件,它只是从第一个文件中导入,并定义任何需要定义的覆盖。

  • 只使用一个具有 Python 逻辑的设置文件来根据上下文更改设置。

我们将逐个来看这些。首先,最基本的方法是定义两个单独的设置文件。如果您在跟着做,您已经有了 settings.py。现在,只需复制它,命名为 settings_production.py。(我们随便取的名字;您可以随意命名。)在这个新文件中,更改 DEBUG 等。第二种方法类似,但减少了冗余。不是拥有两个内容大部分相似的设置文件,您可以将一个作为基本文件,并创建另一个文件从中导入。例如:

# settings.py 

DEBUG = True 
TEMPLATE_DEBUG = DEBUG 

DATABASE_ENGINE = 'postgresql_psycopg2' 
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.pysettings.py 导入所有内容,并重新定义了特定于生产的设置。在这种情况下,DEBUG 设置为 False,但我们还为生产设置了不同的数据库访问参数。(后者表明您可以重新定义任何设置,而不仅仅是基本的设置,比如 DEBUG。)最后,实现两个设置环境最简洁的方法是使用一个设置文件,根据环境进行分支。其中一种方法是检查当前的主机名。例如:

# settings.py 

import socket 

if socket.gethostname() == 'my-laptop': 
    DEBUG = TEMPLATE_DEBUG = True 
else: 
    DEBUG = TEMPLATE_DEBUG = False 

# ... 

在这里,我们从 Python 的标准库中导入socket模块,并使用它来检查当前系统的主机名。我们可以检查主机名来确定代码是否在生产服务器上运行。这里的一个核心教训是,设置文件只是Python 代码。它们可以从其他文件导入,可以执行任意逻辑,等等。只要确保,如果您选择这条路,设置文件中的 Python 代码是无懈可击的。如果它引发任何异常,Django 可能会严重崩溃。

随意将您的settings.py重命名为settings_dev.pysettings/dev.pyfoobar.py-Django 不在乎,只要告诉它您正在使用哪个设置文件即可。

但是,如果您重命名了django-admin startproject生成的settings.py文件,您会发现manage.py会给出一个错误消息,说它找不到设置。这是因为它尝试导入一个名为settings的模块。您可以通过编辑manage.pysettings更改为您的模块的名称来解决此问题,或者使用django-admin而不是manage.py。在后一种情况下,您需要将DJANGO_SETTINGS_MODULE环境变量设置为您的设置文件的 Python 路径(例如,'mysite.settings')。

将 Django 部署到生产服务器

注意

无需头痛的部署

如果您真的想要部署一个实时网站,真的只有一个明智的选择-找到一个明确支持 Django 的主机。

不仅会得到一个独立的媒体服务器(通常是 Nginx),而且他们还会照顾一些小事情,比如正确设置 Apache 并设置一个定期重启 Python 进程的 cron 作业(以防止您的网站挂起)。对于更好的主机,您还可能会得到某种形式的一键部署。

省点心,花几块钱每月找一个懂 Django 的主机。

使用 Apache 和 mod_wsgi 部署 Django

使用 Apache(httpd.apache.org/)和mod_wsgicode.google.com/p/modwsgi)部署 Django 是一个经过验证的将 Django 投入生产的方法。mod_wsgi是一个可以托管任何 Python WSGI 应用程序(包括 Django)的 Apache 模块。Django 将与支持mod_wsgi的任何版本的 Apache 一起工作。官方的mod_wsgi文档非常棒;这是您获取有关如何使用mod_wsgi的所有细节的来源。您可能希望从安装和配置文档开始。

基本配置

一旦安装并激活了mod_wsgi,编辑 Apache 服务器的httpd.conf文件并添加以下内容。请注意,如果您使用的是早于 2.4 版本的 Apache,将Require all granted替换为Allow from all,并在其前面添加Order deny,allow行。

WSGIScriptAlias / /path/to/mysite.com/mysite/wsgi.py 
WSGIPythonPath /path/to/mysite.com 

<Directory /path/to/mysite.com/mysite> 
<Files wsgi.py> 
Require all granted 
</Files> 
</Directory> 

WSGIScriptAlias行中的第一部分是您希望在其上提供应用程序的基本 URL 路径(/表示根 URL),第二部分是系统上 WSGI 文件的位置,通常在您的项目包内(例如此示例中的mysite)。这告诉 Apache 使用在该文件中定义的 WSGI 应用程序来提供任何遵循给定 URL 的请求。

WSGIPythonPath行确保您的项目包可以在 Python 路径上导入;换句话说,import mysite有效。<Directory>部分只是确保 Apache 可以访问您的wsgi.py文件。

接下来,我们需要确保存在带有 WSGI 应用程序对象的wsgi.py。从 Django 版本 1.4 开始,startproject会为您创建一个;否则,您需要自己创建。

查看 WSGI 概述,了解您应该在此文件中放置的默认内容,以及您可以添加的其他内容。

注意

如果在单个mod_wsgi进程中运行多个 Django 站点,则所有这些站点都将使用首先运行的站点的设置。这可以通过更改wsgi.py中的os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings")来解决,例如:os.environ["DJANGO_SETTINGS_MODULE"] = "{{ project_name }}.settings"或者使用mod_wsgi守护程序模式,并确保每个站点在其自己的守护进程中运行。

使用 mod_wsgi 守护程序模式

守护程序模式是在非 Windows 平台上运行mod_wsgi的推荐模式。要创建所需的守护进程组并委托 Django 实例在其中运行,您需要添加适当的WSGIDaemonProcessWSGIProcessGroup指令。

如果使用守护程序模式,则对上述配置需要进一步更改,即不能使用WSGIPythonPath;相反,您应该使用WSGIDaemonProcesspython-path选项,例如:

WSGIDaemonProcess example.com python-path=/path/to/mysite.com:/path/to/venv/lib/python2.7/site-packages 
WSGIProcessGroup example.com 

有关设置守护程序模式的详细信息,请参阅官方mod_wsgi文档。

提供文件

Django 本身不提供文件服务;它将这项工作留给您选择的任何 Web 服务器。我们建议使用单独的 Web 服务器(即不运行 Django 的服务器)来提供媒体。以下是一些不错的选择:

然而,如果您别无选择,只能在与 Django 相同的 Apache VirtualHost上提供媒体文件,您可以设置 Apache 以将某些 URL 作为静态媒体提供,然后使用mod_wsgi接口将其他 URL 用于 Django。

此示例在站点根目录设置 Django,但显式提供robots.txtfavicon.ico,任何 CSS 文件以及/static//media/ URL 空间中的任何内容作为静态文件。所有其他 URL 将使用mod_wsgi进行提供:

Alias /robots.txt /path/to/mysite.com/static/robots.txt 
Alias /favicon.ico /path/to/mysite.com/static/favicon.ico 

Alias /media/ /path/to/mysite.com/media/ 
Alias /static/ /path/to/mysite.com/static/ 

<Directory /path/to/mysite.com/static> 
Require all granted 
</Directory> 

<Directory /path/to/mysite.com/media> 
Require all granted 
</Directory> 

WSGIScriptAlias / /path/to/mysite.com/mysite/wsgi.py 

<Directory /path/to/mysite.com/mysite> 
<Files wsgi.py> 
Require all granted 
</Files> 
</Directory> 

如果您使用的是早于 2.4 的 Apache 版本,请用Allow from all替换Require all granted,并在其前面添加Order deny,allow行。

提供管理文件

django.contrib.staticfilesINSTALLED_APPS中时,Django 开发服务器会自动提供管理应用程序(以及任何其他已安装的应用程序)的静态文件。但是,当您使用其他服务器安排时,情况并非如此。您需要负责设置 Apache 或您正在使用的任何其他 Web 服务器以提供管理文件。

管理文件位于 Django 分发的(django/contrib/admin/static/admin)中。我们建议使用django.contrib.staticfiles来处理管理文件(以及在前一节中概述的 Web 服务器一起使用;这意味着使用collectstatic管理命令在STATIC_ROOT中收集静态文件,然后配置您的 Web 服务器以在STATIC_URL处提供STATIC_ROOT),但这里有其他三种方法:

  1. 从您的文档根目录创建到管理静态文件的符号链接(这可能需要在 Apache 配置中使用+FollowSymLinks)。

  2. 使用Alias指令,如前一段所示,将适当的 URL(可能是STATIC_URL + admin/)别名为管理文件的实际位置。

  3. 复制管理静态文件,使其位于 Apache 文档根目录中。

如果遇到 UnicodeEncodeError

如果您正在利用 Django 的国际化功能,并且打算允许用户上传文件,您必须确保用于启动 Apache 的环境已配置为接受非 ASCII 文件名。如果您的环境配置不正确,当调用os.path中包含非 ASCII 字符的文件名时,将触发UnicodeEncodeError异常。

为了避免这些问题,用于启动 Apache 的环境应包含类似以下设置的环境:

export LANG='en_US.UTF-8' 
export LC_ALL='en_US.UTF-8' 

请查阅操作系统的文档,了解适当的语法和放置这些配置项的位置;在 Unix 平台上,/etc/apache2/envvars是一个常见的位置。添加了这些语句到您的环境后,重新启动 Apache。

在生产环境中提供静态文件

将静态文件放入生产环境的基本概述很简单:当静态文件更改时运行collectstatic命令,然后安排将收集的静态文件目录(STATIC_ROOT)移动到静态文件服务器并提供服务。

根据STATICFILES_STORAGE,文件可能需要手动移动到新位置,或者Storage类的post_process方法可能会处理这个问题。

当然,与所有部署任务一样,魔鬼在细节中。每个生产设置都会有所不同,因此您需要根据自己的需求调整基本概述。

以下是一些可能有所帮助的常见模式。

从同一服务器提供站点和静态文件

如果您希望从已经提供站点的同一服务器提供静态文件,则该过程可能看起来像这样:

  • 将您的代码推送到部署服务器。

  • 在服务器上,运行collectstatic将所有静态文件复制到STATIC_ROOT中。

  • 配置您的 Web 服务器,以便在STATIC_ROOT下的 URLSTATIC_URL中提供文件。

您可能希望自动化这个过程,特别是如果您有多个 Web 服务器。有许多种方法可以进行这种自动化,但许多 Django 开发人员喜欢的一个选择是 Fabric(fabfile.org/)。

接下来,在接下来的几节中,我们将展示一些示例fabfiles(即 Fabric 脚本),这些脚本可以自动化这些文件部署选项。fabfile 的语法相当简单,但这里不会涉及到; 请参阅 Fabric 的文档,了解语法的完整解释。因此,一个用于将静态文件部署到一对 Web 服务器的 fabfile 可能看起来像这样:

from fabric.api import * 

# Hosts to deploy onto 
env.hosts = ['www1.example.com', 'www2.example.com'] 

# Where your project code lives on the server 
env.project_root = '/home/www/myproject' 

def deploy_static(): 
    with cd(env.project_root): 
        run('./manage.py collectstatic -v0 -noinput') 

从专用服务器提供静态文件

大多数较大的 Django 站点使用单独的 Web 服务器-即不运行 Django 的服务器-用于提供静态文件。这个服务器通常运行不同类型的 Web 服务器-速度更快但功能不那么全面。一些常见的选择是:

  • Nginx

  • Apache 的简化版本

配置这些服务器不在本文档的范围之内; 请查看每个服务器的相应文档以获取说明。由于您的静态文件服务器不会运行 Django,因此您需要修改部署策略,看起来像这样:

  1. 当您的静态文件更改时,在本地运行collectstatic

  2. 将本地的STATIC_ROOT推送到静态文件服务器中正在提供服务的目录。rsyncrsync.samba.org/)是这一步的常见选择,因为它只需要传输已更改的静态文件位。

以下是 fabfile 中的示例:

from fabric.api import * 
from fabric.contrib import project 

# Where the static files get collected locally. Your STATIC_ROOT setting. 
env.local_static_root = '/tmp/static' 

# Where the static files should go remotely 
env.remote_static_root = '/home/www/static.example.com' 

@roles('static') 
def deploy_static(): 
    local('./manage.py collectstatic') 
    project.rsync_project( 
        remote_dir = env.remote_static_root, 
        local_dir = env.local_static_root, 
        delete = True 
    ) 

从云服务或 CDN 提供静态文件

另一种常见的策略是从云存储提供商(如 Amazon 的 S3)和/或 CDN(内容传送网络)提供静态文件。这样可以忽略提供静态文件的问题,并且通常可以使网页加载更快(特别是在使用 CDN 时)。

在使用这些服务时,基本工作流程看起来可能与前面的段落有些不同,只是不是使用rsync将静态文件传输到服务器,而是需要将静态文件传输到存储提供商或 CDN。您可能有许多种方法可以做到这一点,但如果提供商有 API,自定义文件存储后端将使这个过程变得非常简单。

如果您已经编写或正在使用第三方自定义存储后端,可以通过将STATICFILES_STORAGE设置为存储引擎来告诉collectstatic使用它。例如,如果您在myproject.storage.S3Storage中编写了一个 S3 存储后端,您可以使用它:

STATICFILES_STORAGE = 'myproject.storage.S3Storage'

完成后,您只需运行collectstatic,您的静态文件将通过存储包推送到 S3。如果以后需要切换到不同的存储提供程序,只需更改STATICFILES_STORAGE设置即可。有第三方应用程序可提供许多常见文件存储 API 的存储后端。一个很好的起点是djangopackages.com上的概述。

扩展

现在您知道如何在单个服务器上运行 Django,让我们看看如何扩展 Django 安装。本节将介绍网站如何从单个服务器扩展到一个可以每小时服务数百万次点击的大规模集群。然而,需要注意的是,几乎每个大型网站在不同方面都很大,因此扩展绝非一刀切的操作。

以下覆盖应该足以展示一般原则,并且在可能的情况下,我们将尝试指出可以做出不同选择的地方。首先,我们将做出一个相当大的假设,并且专门讨论 Apache 和mod_python下的扩展。虽然我们知道有许多成功的中到大型规模的 FastCGI 部署,但我们对 Apache 更为熟悉。

在单个服务器上运行

大多数网站最初在单个服务器上运行,其架构看起来有点像图 13.1。然而,随着流量的增加,您很快会发现不同软件之间存在资源争用的问题。

数据库服务器和 Web 服务器喜欢拥有整个服务器,因此当它们在同一台服务器上运行时,它们经常会争夺它们更愿意垄断的相同资源(RAM 和 CPU)。将数据库服务器移至第二台机器很容易解决这个问题。

在单个服务器上运行

图 13.1:单服务器 Django 设置。

分离数据库服务器

就 Django 而言,分离数据库服务器的过程非常简单:您只需要将DATABASE_HOST设置更改为数据库服务器的 IP 或 DNS 名称。如果可能的话,最好使用 IP,因为不建议依赖 DNS 来连接您的 Web 服务器和数据库服务器。有了单独的数据库服务器,我们的架构现在看起来像图 13.2

在这里,我们开始进入通常称为n 层架构的领域。不要被这个流行词吓到-它只是指 Web 堆栈的不同层被分离到不同的物理机器上。

在这一点上,如果您预计将来需要超出单个数据库服务器,最好开始考虑连接池和/或数据库复制。不幸的是,在本书中没有足够的空间来充分讨论这些主题,因此您需要咨询数据库的文档和/或社区以获取更多信息。

分离数据库服务器

图 13.2:将数据库移至专用服务器。

运行单独的媒体服务器

我们仍然有一个大问题留在单服务器设置中:从处理动态内容的同一台机器上提供媒体。这两个活动在不同情况下表现最佳,将它们合并在同一台机器上会导致它们都表现不佳。

因此,下一步是将媒体-即任何不是由 Django 视图生成的东西-分离到专用服务器上(见图 13.3)。

理想情况下,这个媒体服务器应该运行一个针对静态媒体传递进行优化的精简 Web 服务器。Nginx 是首选选项,尽管lighttpd是另一个选项,或者一个经过大幅简化的 Apache 也可以工作。对于静态内容丰富的网站(照片、视频等),将其移至单独的媒体服务器至关重要,很可能是扩展的第一步。

然而,这一步可能有点棘手。如果您的应用涉及文件上传,Django 需要能够将上传的媒体写入媒体服务器。如果媒体存储在另一台服务器上,您需要安排一种方式让该写入通过网络进行。

运行一个独立的媒体服务器

图 13.3:分离媒体服务器。

实现负载平衡和冗余

在这一点上,我们已经尽可能地将事情分解了。这种三台服务器的设置应该可以处理非常大量的流量-我们从这种结构中每天提供了大约 1000 万次点击-因此,如果您进一步增长,您将需要开始添加冗余。

实际上,这是一件好事。仅看一眼图 13.3就会告诉你,即使你的三台服务器中的一台失败,你也会使整个站点崩溃。因此,随着添加冗余服务器,不仅可以增加容量,还可以增加可靠性。为了这个例子,让我们假设 Web 服务器首先达到容量。

在不同的硬件上运行多个 Django 站点的副本相对容易-只需将所有代码复制到多台机器上,并在所有机器上启动 Apache。然而,您需要另一种软件来在多台服务器上分发流量:负载均衡器

您可以购买昂贵的专有硬件负载均衡器,但也有一些高质量的开源软件负载均衡器。Apache 的mod_proxy是一个选择,但我们发现 Perlbal(www.djangoproject.com/r/perlbal/)非常棒。它是一个由编写memcached的同一批人编写的负载均衡器和反向代理(参见第十六章,“Django 的缓存框架”)。

现在,随着 Web 服务器的集群化,我们不断发展的架构开始变得更加复杂,如图 13.4所示。

实现负载均衡和冗余

图 13.4:一个负载平衡、冗余的服务器设置。

请注意,在图中,Web 服务器被称为集群,表示服务器的数量基本上是可变的。一旦您在前面放置了负载均衡器,您就可以轻松地添加和删除后端 Web 服务器,而不会有一秒钟的停机时间。

扩大规模

在这一点上,接下来的几个步骤基本上是上一个步骤的衍生:

  • 当您需要更多的数据库性能时,您可能希望添加复制的数据库服务器。MySQL 包含内置的复制功能;PostgreSQL 用户应该研究 Slony(www.djangoproject.com/r/slony/)和 pgpool(www.djangoproject.com/r/pgpool/)用于复制和连接池。

  • 如果单个负载均衡器不够,您可以在前面添加更多负载均衡器机器,并使用轮询 DNS 进行分发。

  • 如果单个媒体服务器不够,您可以添加更多媒体服务器,并使用负载平衡集群分发负载。

  • 如果您需要更多的缓存存储,您可以添加专用的缓存服务器。

  • 在任何阶段,如果集群性能不佳,您可以向集群添加更多服务器。

经过几次迭代后,一个大规模的架构可能看起来像图 13.5

扩大规模

图 13.5:一个大规模 Django 设置的示例。

尽管我们在每个级别只显示了两到三台服务器,但你可以添加的服务器数量并没有根本限制。

性能调优

如果你有大量的资金,你可以不断地投入硬件来解决扩展问题。然而,对于我们其他人来说,性能调优是必不可少的。

注意

顺便说一句,如果有人拥有大量资金正在阅读这本书,请考虑向 Django 基金会进行大额捐赠。他们也接受未加工的钻石和金锭。

不幸的是,性能调优更多地是一门艺术而不是一门科学,而且比扩展更难写。如果你真的想部署一个大规模的 Django 应用程序,你应该花大量时间学习如何调优你的每个部分。

接下来的章节介绍了多年来我们发现的一些 Django 特定的调优技巧。

没有太多的 RAM 这种事

即使是非常昂贵的 RAM 在今天也相对实惠。尽可能多地购买 RAM,然后再多买一点。更快的处理器不会显著提高性能;大多数 Web 服务器花费高达 90%的时间在等待磁盘 I/O。一旦开始交换,性能就会急剧下降。更快的磁盘可能会稍微有所帮助,但它们比 RAM 要贵得多,以至于并不重要。

如果你有多台服务器,将 RAM 放在数据库服务器是首选。如果你有能力,获得足够的 RAM 来容纳整个数据库到内存中。这并不难;我们开发了一个拥有超过 50 万篇报纸文章的网站,只需要不到 2GB 的空间。

接下来,充分利用 Web 服务器上的 RAM。理想情况是服务器从不交换。如果你达到了这一点,你应该能够承受大部分正常的流量。

关闭保持活动状态

保持活动状态是 HTTP 的一个特性,允许多个 HTTP 请求通过单个 TCP 连接提供,避免了 TCP 建立/拆除的开销。乍一看,这看起来不错,但它可能会影响 Django 网站的性能。如果你从一个单独的服务器正确地提供媒体,每个浏览你网站的用户大约每十秒钟只会从你的 Django 服务器请求一个页面。这会让 HTTP 服务器等待下一个保持活动状态的请求,而空闲的 HTTP 服务器只会消耗应该被活跃服务器使用的内存。

使用 Memcached

尽管 Django 支持许多不同的缓存后端,但没有一个能够像 Memcached 一样快。如果你有一个高流量的网站,甚至不要考虑其他后端,直接使用 Memcached。

经常使用 Memcached

当然,如果你实际上不使用 Memcached,选择 Memcached 对你没有好处。第十六章,Django 的缓存框架,是你的好朋友:学习如何使用 Django 的缓存框架,并在可能的地方使用它。积极的、预防性的缓存通常是唯一能够在大流量下保持网站稳定的方法。

加入讨论

Django 的每个部分-从 Linux 到 Apache 再到 PostgreSQL 或 MySQL-都有一个强大的社区支持。如果你真的想从你的服务器中获得最后 1%,加入你软件背后的开源社区并寻求帮助。大多数自由软件社区成员都会乐意帮助。还要确保加入 Django 社区-一个活跃、不断增长的 Django 开发者群体。我们的社区有大量的集体经验可以提供。

接下来是什么?

剩下的章节关注其他 Django 功能,这取决于你的应用是否需要。随意按照你选择的任何顺序阅读它们。

第十四章:生成非 HTML 内容

通常,当我们谈论开发网站时,我们谈论的是生成 HTML。当然,网页不仅仅是 HTML;我们使用网页以各种格式分发数据:RSS、PDF、图像等等。

到目前为止,我们专注于 HTML 生成的常见情况,但在本章中,我们将走一条弯路,看看如何使用 Django 生成其他类型的内容。Django 有方便的内置工具,可以用来生成一些常见的非 HTML 内容:

  • 逗号分隔(CSV)文件,用于导入到电子表格应用程序中。

  • PDF 文件。

  • RSS/Atom 订阅源。

  • 站点地图(最初由谷歌开发的 XML 格式,为搜索引擎提供提示)。

我们稍后会详细讨论这些工具,但首先我们将介绍基本原则。

基础知识:视图和 MIME 类型

从第二章中回忆,视图和 URLconfs,视图函数只是一个接受 Web 请求并返回 Web 响应的 Python 函数。这个响应可以是网页的 HTML 内容,或者重定向,或者 404 错误,或者 XML 文档,或者图像...或者任何东西。更正式地说,Django 视图函数必须*:*

  1. 接受一个HttpRequest实例作为其第一个参数;和

  2. 返回一个HttpResponse实例。

从视图返回非 HTML 内容的关键在于HttpResponse类,特别是content_type参数。默认情况下,Django 将content_type设置为 text/html。但是,您可以将content_type设置为 IANA 管理的任何官方互联网媒体类型(MIME 类型)(有关更多信息,请访问www.iana.org/assignments/media-types/media-types.xhtml)。

通过调整 MIME 类型,我们可以告诉浏览器我们返回了不同格式的响应。例如,让我们看一个返回 PNG 图像的视图。为了保持简单,我们只需从磁盘上读取文件:

from django.http import HttpResponse 

def my_image(request): 
    image_data = open("/path/to/my/image.png", "rb").read() 
    return HttpResponse(image_data, content_type="image/png") 

就是这样!如果您用open()调用中的图像路径替换为真实图像的路径,您可以使用这个非常简单的视图来提供图像,浏览器将正确显示它。

另一个重要的事情是HttpResponse对象实现了 Python 的标准文件类对象 API。这意味着您可以在任何需要文件的地方使用HttpResponse实例,包括 Python(或第三方库)。让我们看一下如何使用 Django 生成 CSV 的示例。

生成 CSV

Python 自带一个 CSV 库,csv。使用它与 Django 的关键在于csv模块的 CSV 创建功能作用于类似文件的对象,而 Django 的HttpResponse对象是类似文件的对象。下面是一个例子:

import csv 
from django.http import HttpResponse 

def some_view(request): 
    # Create the HttpResponse object with the appropriate CSV header. 
    response = HttpResponse(content_type='text/csv') 
    response['Content-Disposition'] = 'attachment; 
      filename="somefilename.csv"' 

    writer = csv.writer(response) 
    writer.writerow(['First row', 'Foo', 'Bar', 'Baz']) 
    writer.writerow(['Second row', 'A', 'B', 'C', '"Testing"']) 

    return response 

代码和注释应该是不言自明的,但有几件事值得一提:

  • 响应获得了特殊的 MIME 类型text/csv。这告诉浏览器该文档是 CSV 文件,而不是 HTML 文件。如果不这样做,浏览器可能会将输出解释为 HTML,这将导致浏览器窗口中出现丑陋、可怕的胡言乱语。

  • 响应获得了额外的Content-Disposition头,其中包含 CSV 文件的名称。这个文件名是任意的;随便取什么名字。它将被浏览器用于“另存为...”对话框等。

  • 连接到 CSV 生成 API 很容易:只需将response作为csv.writer的第一个参数。csv.writer函数期望一个类似文件的对象,而HttpResponse对象符合要求。

  • 对于 CSV 文件中的每一行,调用writer.writerow,将其传递给一个可迭代对象,如列表或元组。

  • CSV 模块会为您处理引用,因此您不必担心用引号或逗号转义字符串。只需将writerow()传递给您的原始字符串,它就会做正确的事情。

流式传输大型 CSV 文件

处理生成非常大响应的视图时,您可能希望考虑改用 Django 的StreamingHttpResponse。例如,通过流式传输需要很长时间生成的文件,您可以避免负载均衡器在服务器生成响应时可能会超时而断开连接。在这个例子中,我们充分利用 Python 生成器来高效地处理大型 CSV 文件的组装和传输:

import csv 

from django.utils.six.moves import range 
from django.http import StreamingHttpResponse 

class Echo(object): 
    """An object that implements just the write method of the file-like 
    interface. 
    """ 
    def write(self, value): 
        """Write the value by returning it, instead of storing in a buffer.""" 
        return value 

def some_streaming_csv_view(request): 
    """A view that streams a large CSV file.""" 
    # Generate a sequence of rows. The range is based on the maximum number of 
    # rows that can be handled by a single sheet in most spreadsheet 
    # applications. 
    rows = (["Row {}".format(idx), str(idx)] for idx in range(65536)) 
    pseudo_buffer = Echo() 
    writer = csv.writer(pseudo_buffer) 
    response = StreamingHttpResponse((writer.writerow(row)  
      for row in rows), content_type="text/csv") 
    response['Content-Disposition'] = 'attachment;    
      filename="somefilename.csv"' 
    return response 

使用模板系统

或者,您可以使用 Django 模板系统来生成 CSV。这比使用方便的 Python csv模块更低级,但是这里提供了一个完整的解决方案。这里的想法是将一个项目列表传递给您的模板,并让模板在for循环中输出逗号。以下是一个示例,它生成与上面相同的 CSV 文件:

from django.http import HttpResponse 
from django.template import loader, Context 

def some_view(request): 
    # Create the HttpResponse object with the appropriate CSV header. 
    response = HttpResponse(content_type='text/csv') 
    response['Content-Disposition'] = 'attachment;    
      filename="somefilename.csv"' 

    # The data is hard-coded here, but you could load it  
    # from a database or some other source. 
    csv_data = ( 
        ('First row', 'Foo', 'Bar', 'Baz'), 
        ('Second row', 'A', 'B', 'C', '"Testing"', "Here's a quote"), 
    ) 

    t = loader.get_template('my_template_name.txt') 
    c = Context({'data': csv_data,}) 
    response.write(t.render(c)) 
    return response 

这个例子和之前的例子唯一的区别是这个例子使用模板加载而不是 CSV 模块。其余的代码,比如content_type='text/csv',都是一样的。然后,创建模板my_template_name.txt,其中包含以下模板代码:

{% for row in data %} 
            "{{ row.0|addslashes }}", 
            "{{ row.1|addslashes }}", 
            "{{ row.2|addslashes }}", 
            "{{ row.3|addslashes }}", 
            "{{ row.4|addslashes }}" 
{% endfor %} 

这个模板非常基础。它只是遍历给定的数据,并为每一行显示一个 CSV 行。它使用addslashes模板过滤器来确保引号没有问题。

其他基于文本的格式

请注意,这里与 CSV 相关的内容并不多,只是特定的输出格式。您可以使用这些技术中的任何一种来输出您梦想中的任何基于文本的格式。您还可以使用类似的技术来生成任意二进制数据;例如,生成 PDF 文件。

生成 PDF

Django 能够使用视图动态输出 PDF 文件。这得益于出色的开源 ReportLab(有关更多信息,请访问www.reportlab.com/opensource/)Python PDF 库。动态生成 PDF 文件的优势在于,您可以为不同目的创建定制的 PDF 文件,比如为不同用户或不同内容创建。

安装 ReportLab

ReportLab库可在 PyPI 上获得。还可以下载用户指南(不巧的是,是一个 PDF 文件)。您可以使用pip安装 ReportLab:

$ pip install reportlab 

通过在 Python 交互解释器中导入它来测试您的安装:

>>> import reportlab 

如果该命令没有引发任何错误,则安装成功。

编写您的视图

使用 Django 动态生成 PDF 的关键是 ReportLab API,就像csv库一样,它作用于文件样对象,比如 Django 的HttpResponse。以下是一个 Hello World 示例:

from reportlab.pdfgen import canvas 
from django.http import HttpResponse 

def some_view(request): 
    # Create the HttpResponse object with the appropriate PDF headers. 
    response = HttpResponse(content_type='application/pdf') 
    response['Content-Disposition'] = 'attachment;    
      filename="somefilename.pdf"' 

    # Create the PDF object, using the response object as its "file." 
    p = canvas.Canvas(response) 

    # Draw things on the PDF. Here's where the PDF generation happens. 
    # See the ReportLab documentation for the full list of functionality. 
    p.drawString(100, 100, "Hello world.") 

    # Close the PDF object cleanly, and we're done. 
    p.showPage() 
    p.save() 
    return response 

代码和注释应该是不言自明的,但有几点值得一提:

  • 响应获得了特殊的 MIME 类型,application/pdf。这告诉浏览器该文档是一个 PDF 文件,而不是 HTML 文件。

  • 响应获得了额外的Content-Disposition头部,其中包含 PDF 文件的名称。这个文件名是任意的:随便取什么名字都可以。浏览器将在“另存为...”对话框中使用它,等等。

  • 在这个例子中,Content-Disposition头部以'attachment; '开头。这会强制 Web 浏览器弹出一个对话框,提示/确认如何处理文档,即使在计算机上设置了默认值。如果省略'attachment;',浏览器将使用为 PDF 配置的任何程序/插件来处理 PDF。以下是该代码的样子:

        response['Content-Disposition'] = 'filename="somefilename.pdf"'
  • 连接到 ReportLab API 很容易:只需将response作为canvas.Canvas的第一个参数传递。Canvas类需要一个文件样对象,而HttpResponse对象正合适。

  • 请注意,所有后续的 PDF 生成方法都是在 PDF 对象(在本例中是p)上调用的,而不是在response上调用的。

  • 最后,重要的是在 PDF 文件上调用showPage()save()

复杂的 PDF

如果你正在使用 ReportLab 创建复杂的 PDF 文档,考虑使用io库作为 PDF 文件的临时存储位置。这个库提供了一个特别高效的类文件对象接口。以下是上面的 Hello World 示例重写,使用io

from io import BytesIO 
from reportlab.pdfgen import canvas 
from django.http import HttpResponse 

def some_view(request): 
    # Create the HttpResponse object with the appropriate PDF headers. 
    response = HttpResponse(content_type='application/pdf') 
    response['Content-Disposition'] = 'attachment;   
      filename="somefilename.pdf"' 

    buffer = BytesIO() 

    # Create the PDF object, using the BytesIO object as its "file." 
    p = canvas.Canvas(buffer) 

    # Draw things on the PDF. Here's where the PDF generation happens. 
    # See the ReportLab documentation for the full list of functionality. 
    p.drawString(100, 100, "Hello world.") 

    # Close the PDF object cleanly. 
    p.showPage() 
    p.save() 

    # Get the value of the BytesIO buffer and write it to the response. 
    pdf = buffer.getvalue() 
    buffer.close() 
    response.write(pdf) 
    return response 

更多资源

  • PDFlib (www.pdflib.org/)是另一个具有 Python 绑定的 PDF 生成库。要在 Django 中使用它,只需使用本文中解释的相同概念。

  • Pisa XHTML2PDF (www.xhtml2pdf.com/) 是另一个 PDF 生成库。Pisa 附带了如何将 Pisa 与 Django 集成的示例。

  • HTMLdoc (www.htmldoc.org/)是一个可以将 HTML 转换为 PDF 的命令行脚本。它没有 Python 接口,但你可以使用systempopen跳出到 shell,并在 Python 中检索输出。

其他可能性

在 Python 中,你可以生成许多其他类型的内容。以下是一些更多的想法和一些指向你可以用来实现它们的库的指针:

  • ZIP 文件:Python 的标准库配备了zipfile模块,可以读取和写入压缩的 ZIP 文件。你可以使用它提供一堆文件的按需存档,或者在请求时压缩大型文档。你也可以使用标准库的tarfile模块类似地生成 TAR 文件。

  • 动态图片Python Imaging LibraryPIL)(www.pythonware.com/products/pil/)是一个用于生成图片(PNG、JPEG、GIF 等)的绝妙工具包。你可以使用它自动缩小图片为缩略图,将多个图片合成单个框架,甚至进行基于网络的图像处理。

  • 图表和图表:有许多强大的 Python 绘图和图表库,你可以使用它们生成按需地图、图表、绘图和图表。我们不可能列出它们所有,所以这里是一些亮点:

  • matplotlib (matplotlib.sourceforge.net/)可用于生成通常使用 MatLab 或 Mathematica 生成的高质量图表。

  • pygraphviz (networkx.lanl.gov/pygraphviz/),一个与 Graphviz 图形布局工具包的接口,可用于生成图和网络的结构化图表。

一般来说,任何能够写入文件的 Python 库都可以连接到 Django。可能性是巨大的。现在我们已经了解了生成非 HTML 内容的基础知识,让我们提高一个抽象级别。Django 配备了一些非常巧妙的内置工具,用于生成一些常见类型的非 HTML 内容。

联合供稿框架

Django 配备了一个高级别的联合供稿生成框架,可以轻松创建 RSS 和 Atom 供稿。RSS 和 Atom 都是基于 XML 的格式,你可以用它们提供站点内容的自动更新供稿。在这里阅读更多关于 RSS 的信息(www.whatisrss.com/),并在这里获取有关 Atom 的信息(www.atomenabled.org/)。

创建任何联合供稿,你所要做的就是编写一个简短的 Python 类。你可以创建任意数量的供稿。Django 还配备了一个低级别的供稿生成 API。如果你想在网页上下文之外或以其他低级别方式生成供稿,可以使用这个 API。

高级别框架

概述

高级别的供稿生成框架由Feed类提供。要创建一个供稿,编写一个Feed类,并在你的 URLconf 中指向它的一个实例。

供稿类

Feed类是表示订阅源的 Python 类。订阅源可以是简单的(例如,站点新闻订阅,或者显示博客最新条目的基本订阅源)或更复杂的(例如,显示特定类别中的所有博客条目的订阅源,其中类别是可变的)。Feed 类是django.contrib.syndication.views.Feed的子类。它们可以存在于代码库的任何位置。Feed类的实例是视图,可以在您的 URLconf 中使用。

一个简单的例子

这个简单的例子,取自一个假设的警察打击新闻网站,描述了最新的五条新闻项目的订阅:

from django.contrib.syndication.views import Feed 
from django.core.urlresolvers import reverse 
from policebeat.models import NewsItem 

class LatestEntriesFeed(Feed): 
    title = "Police beat site news" 
    link = "/sitenews/" 
    description = "Updates on changes and additions to police beat central." 

    def items(self): 
        return NewsItem.objects.order_by('-pub_date')[:5] 

    def item_title(self, item): 
        return item.title 

    def item_description(self, item): 
        return item.description 

    # item_link is only needed if NewsItem has no get_absolute_url method. 
    def item_link(self, item): 
        return reverse('news-item', args=[item.pk]) 

要将 URL 连接到此订阅源,请在您的 URLconf 中放置Feed对象的实例。例如:

from django.conf.urls import url 
from myproject.feeds import LatestEntriesFeed 

urlpatterns = [ 
    # ... 
    url(r'^latest/feed/$', LatestEntriesFeed()), 
    # ... 
] 

注意:

  • Feed 类是django.contrib.syndication.views.Feed的子类。

  • titlelinkdescription分别对应于标准的 RSS<title><link><description>元素。

  • items()只是一个返回应包含在订阅源中的对象列表的方法。尽管此示例使用 Django 的对象关系映射器返回NewsItem对象,但不必返回模型实例。尽管使用 Django 模型可以免费获得一些功能,但items()可以返回任何类型的对象。

  • 如果您要创建 Atom 订阅源,而不是 RSS 订阅源,请设置subtitle属性,而不是description属性。有关示例,请参见本章后面的同时发布 Atom 和 RSS 订阅源。

还有一件事要做。在 RSS 订阅源中,每个<item>都有一个<title><link><description>。我们需要告诉框架将哪些数据放入这些元素中。

对于<title><description>的内容,Django 尝试在Feed类上调用item_title()item_description()方法。它们传递了一个参数item,即对象本身。这些是可选的;默认情况下,对象的 unicode 表示用于两者。

如果您想对标题或描述进行任何特殊格式化,可以使用 Django 模板。它们的路径可以在Feed类的title_templatedescription_template属性中指定。模板为每个项目呈现,并传递了两个模板上下文变量:

  • {{ obj }}-:当前对象(您在items()中返回的任何对象之一)。

  • {{ site }}-:表示当前站点的 Djangosite对象。这对于{{ site.domain }}{{ site.name }}非常有用。

请参阅下面使用描述模板的一个复杂的例子

如果您需要提供比之前提到的两个变量更多的信息,还有一种方法可以将标题和描述模板传递给您。您可以在Feed子类中提供get_context_data方法的实现。例如:

from mysite.models import Article 
from django.contrib.syndication.views import Feed 

class ArticlesFeed(Feed): 
    title = "My articles" 
    description_template = "feeds/articles.html" 

    def items(self): 
        return Article.objects.order_by('-pub_date')[:5] 

    def get_context_data(self, **kwargs): 
        context = super(ArticlesFeed, self).get_context_data(**kwargs) 
        context['foo'] = 'bar' 
        return context 

和模板:

Something about {{ foo }}: {{ obj.description }} 

此方法将针对items()返回的列表中的每个项目调用一次,并带有以下关键字参数:

  • item:当前项目。出于向后兼容的原因,此上下文变量的名称为{{ obj }}

  • obj:由get_object()返回的对象。默认情况下,这不会暴露给模板,以避免与{{ obj }}(见上文)混淆,但您可以在get_context_data()的实现中使用它。

  • site:如上所述的当前站点。

  • request:当前请求。

get_context_data()的行为模仿了通用视图的行为-您应该调用super()来从父类检索上下文数据,添加您的数据并返回修改后的字典。

要指定<link>的内容,您有两个选项。对于items()中的每个项目,Django 首先尝试在Feed类上调用item_link()方法。类似于标题和描述,它传递了一个参数-item。如果该方法不存在,Django 尝试在该对象上执行get_absolute_url()方法。

get_absolute_url()item_link()都应返回项目的 URL 作为普通的 Python 字符串。与get_absolute_url()一样,item_link()的结果将直接包含在 URL 中,因此您负责在方法本身内部执行所有必要的 URL 引用和转换为 ASCII。

一个复杂的例子

该框架还通过参数支持更复杂的源。例如,网站可以为城市中每个警察拍摄提供最新犯罪的 RSS 源。为每个警察拍摄创建单独的Feed类是愚蠢的;这将违反 DRY 原则,并将数据耦合到编程逻辑中。

相反,辛迪加框架允许您访问从 URLconf 传递的参数,因此源可以根据源 URL 中的信息输出项目。警察拍摄源可以通过以下 URL 访问:

  • /beats/613/rss/-:返回 613 拍摄的最新犯罪。

  • /beats/1424/rss/-:返回 1424 拍摄的最新犯罪。

这些可以与 URLconf 行匹配,例如:

url(r'^beats/(?P[0-9]+)/rss/$', BeatFeed()), 

与视图一样,URL 中的参数将与请求对象一起传递到get_object()方法。以下是这些特定于拍摄的源的代码:

from django.contrib.syndication.views import FeedDoesNotExist 
from django.shortcuts import get_object_or_404 

class BeatFeed(Feed): 
    description_template = 'feeds/beat_description.html' 

    def get_object(self, request, beat_id): 
        return get_object_or_404(Beat, pk=beat_id) 

    def title(self, obj): 
        return "Police beat central: Crimes for beat %s" % obj.beat 

    def link(self, obj): 
        return obj.get_absolute_url() 

    def description(self, obj): 
        return "Crimes recently reported in police beat %s" % obj.beat 

    def items(self, obj): 
        return Crime.objects.filter(beat=obj).order_by(  
          '-crime_date')[:30] 

为了生成源的<title><link><description>,Django 使用title()link()description()方法。

在上一个示例中,它们是简单的字符串类属性,但是此示例说明它们可以是字符串方法。对于titlelinkdescription,Django 遵循此算法:

  • 首先,它尝试调用一个方法,传递obj参数,其中objget_object()返回的对象。

  • 如果失败,它将尝试调用一个没有参数的方法。

  • 如果失败,它将使用 class 属性。

还要注意,items()也遵循相同的算法-首先尝试items(obj),然后尝试items(),最后尝试items类属性(应该是一个列表)。我们正在使用模板来描述项目。它可以非常简单:

{{ obj.description }} 

但是,您可以根据需要自由添加格式。下面的ExampleFeed类完整记录了Feed类的方法和属性。

指定源的类型

默认情况下,此框架生成的源使用 RSS 2.0。要更改此设置,请向您的Feed类添加feed_type属性,如下所示:

from django.utils.feedgenerator import Atom1Feed 

class MyFeed(Feed): 
    feed_type = Atom1Feed 

请注意,将feed_type设置为类对象,而不是实例。当前可用的源类型有:

  • django.utils.feedgenerator.Rss201rev2Feed(RSS 2.01。默认)。

  • django.utils.feedgenerator.RssUserland091Feed(RSS 0.91)。

  • django.utils.feedgenerator.Atom1Feed(Atom 1.0)。

附件

要指定附件,例如在创建播客源时使用的附件,请使用item_enclosure_urlitem_enclosure_lengthitem_enclosure_mime_type挂钩。有关用法示例,请参阅下面的ExampleFeed类。

语言

使用辛迪加框架创建的源自动包括适当的<language>标签(RSS 2.0)或xml:lang属性(Atom)。这直接来自您的LANGUAGE_CODE设置。

URL

link方法/属性可以返回绝对路径(例如,/blog/)或具有完全合格的域和协议的 URL(例如,http://www.example.com/blog/)。如果link不返回域,辛迪加框架将根据您的SITE_ID设置插入当前站点的域。Atom 源需要定义源的当前位置的<link rel="self">。辛迪加框架会自动填充这一点,使用当前站点的域,根据SITE_ID设置。

同时发布 Atom 和 RSS 源

一些开发人员喜欢提供其源的 Atom 和 RSS 版本。在 Django 中很容易做到:只需创建Feed类的子类,并将feed_type设置为不同的内容。然后更新您的 URLconf 以添加额外的版本。以下是一个完整的示例:

from django.contrib.syndication.views import Feed 
from policebeat.models import NewsItem 
from django.utils.feedgenerator import Atom1Feed 

class RssSiteNewsFeed(Feed): 
    title = "Police beat site news" 
    link = "/sitenews/" 
    description = "Updates on changes and additions to police beat central." 

    def items(self): 
        return NewsItem.objects.order_by('-pub_date')[:5] 

class AtomSiteNewsFeed(RssSiteNewsFeed): 
    feed_type = Atom1Feed 
    subtitle = RssSiteNewsFeed.description 

注意

在这个例子中,RSS feed 使用 description,而 Atom feed 使用 subtitle。这是因为 Atom feed 不提供 feed 级别的描述,但它们提供了一个副标题。如果您在 Feed 类中提供了 description,Django 将不会自动将其放入 subtitle 元素中,因为副标题和描述不一定是相同的。相反,您应该定义一个 subtitle 属性。

在上面的示例中,我们将 Atom feed 的 subtitle 设置为 RSS feed 的 description,因为它已经相当短了。并且相应的 URLconf:

from django.conf.urls import url 
from myproject.feeds import RssSiteNewsFeed, AtomSiteNewsFeed 

urlpatterns = [ 
    # ... 
    url(r'^sitenews/rss/$', RssSiteNewsFeed()), 
    url(r'^sitenews/atom/$', AtomSiteNewsFeed()), 
    # ... 
] 

注意

有关 Feed 类的所有可能属性和方法的示例,请参见:https://docs.djangoproject.com/en/1.8/ref/contrib/syndication/#feed-class-reference

低级别框架

在幕后,高级 RSS 框架使用较低级别的框架来生成 feed 的 XML。这个框架存在于一个单独的模块中:django/utils/feedgenerator.py。您可以自己使用这个框架进行较低级别的 feed 生成。您还可以创建自定义 feed 生成器子类,以便与 feed_type Feed 选项一起使用。

SyndicationFeed 类

feedgenerator 模块包含一个基类:

  • django.utils.feedgenerator.SyndicationFeed

和几个子类:

  • django.utils.feedgenerator.RssUserland091Feed

  • django.utils.feedgenerator.Rss201rev2Feed

  • django.utils.feedgenerator.Atom1Feed

这三个类都知道如何将某种类型的 feed 渲染为 XML。它们共享这个接口:

SyndicationFeed.init()

使用给定的元数据字典初始化 feed,该元数据适用于整个 feed。必需的关键字参数是:

  • 标题

  • 链接

  • 描述

还有一堆其他可选关键字:

  • 语言

  • 作者电子邮件

  • 作者名称

  • 作者链接

  • 副标题

  • 类别

  • feed_url

  • feed_copyright

  • feed_guid

  • ttl

您传递给 __init__ 的任何额外关键字参数都将存储在 self.feed 中,以便与自定义 feed 生成器一起使用。

所有参数都应该是 Unicode 对象,除了 categories,它应该是 Unicode 对象的序列。

SyndicationFeed.add_item()

使用给定参数向 feed 添加一个项目。

必需的关键字参数是:

  • 标题

  • 链接

  • 描述

可选关键字参数是:

  • 作者电子邮件

  • 作者名称

  • 作者链接

  • pubdate

  • 评论

  • unique_id

  • enclosure

  • 类别

  • item_copyright

  • ttl

  • updateddate

额外的关键字参数将被存储以供自定义 feed 生成器使用。所有参数,如果给定,都应该是 Unicode 对象,除了:

  • pubdate 应该是 Python datetime 对象。

  • updateddate 应该是 Python datetime 对象。

  • enclosure 应该是 django.utils.feedgenerator.Enclosure 的一个实例。

  • categories 应该是 Unicode 对象的序列。

SyndicationFeed.write()

将 feed 以给定编码输出到 outfile,这是一个类似文件的对象。

SyndicationFeed.writeString()

以给定编码的字符串形式返回 feed。例如,要创建 Atom 1.0 feed 并将其打印到标准输出:

>>> from django.utils import feedgenerator 
>>> from datetime import datetime 
>>> f = feedgenerator.Atom1Feed( 
...     , 
...     link="http://www.example.com/", 
...     description="In which I write about what I ate today.", 
...     language="en", 
...     author_name="Myself", 
...     feed_url="http://example.com/atom.xml") 
>>> f.add_item(, 
...     link="http://www.example.com/entries/1/", 
...     pubdate=datetime.now(), 
...     description="<p>Today I had a Vienna Beef hot dog. It was pink, plump and perfect.</p>") 
>>> print(f.writeString('UTF-8')) 
<?xml version="1.0" encoding="UTF-8"?> 
<feed  xml:lang="en"> 
... 
</feed> 

自定义 feed 生成器

如果您需要生成自定义 feed 格式,您有几个选择。如果 feed 格式完全自定义,您将需要对 SyndicationFeed 进行子类化,并完全替换 write()writeString() 方法。但是,如果 feed 格式是 RSS 或 Atom 的一个衍生格式(即 GeoRSS,(链接到网站 georss.org/),苹果的 iTunes podcast 格式(链接到网站 www.apple.com/itunes/podcasts/specs.html)等),您有更好的选择。

这些类型的 feed 通常会向底层格式添加额外的元素和/或属性,并且有一组方法,SyndicationFeed 调用这些额外的属性。因此,您可以对适当的 feed 生成器类(Atom1FeedRss201rev2Feed)进行子类化,并扩展这些回调。它们是:

SyndicationFeed.root_attributes(self, )

返回要添加到根源元素(feed/channel)的属性字典。

SyndicationFeed.add_root_elements(self, handler)

回调以在根源元素(feed/channel)内添加元素。handler是 Python 内置 SAX 库中的XMLGenerator;您将在其上调用方法以添加到正在处理的 XML 文档中。

SyndicationFeed.item_attributes(self, item)

返回要添加到每个条目(item/entry)元素的属性字典。参数item是传递给SyndicationFeed.add_item()的所有数据的字典。

SyndicationFeed.add_item_elements(self, handler, item)

回调以向每个条目(item/entry)元素添加元素。handleritem与上述相同。

注意

如果您覆盖了这些方法中的任何一个,请确保调用超类方法,因为它们会为每个 feed 格式添加所需的元素。

例如,您可以开始实现一个 iTunes RSS feed 生成器,如下所示:

class iTunesFeed(Rss201rev2Feed): 
    def root_attributes(self): 
        attrs = super(iTunesFeed, self).root_attributes() 
        attrs['xmlns:itunes'] =  
          'http://www.itunes.com/dtds/podcast-1.0.dtd' 
        return attrs 

    def add_root_elements(self, handler): 
        super(iTunesFeed, self).add_root_elements(handler) 
        handler.addQuickElement('itunes:explicit', 'clean') 

显然,要创建一个完整的自定义 feed 类还有很多工作要做,但上面的例子应该演示了基本思想。

站点地图框架

站点地图是您网站上的一个 XML 文件,告诉搜索引擎索引器您的页面更改的频率以及与站点上其他页面的重要性。这些信息有助于搜索引擎索引您的站点。有关站点地图的更多信息,请参阅 sitemaps.org 网站。

Django 站点地图框架通过让您在 Python 代码中表达此信息来自动创建此 XML 文件。它的工作方式与 Django 的 Syndication 框架类似。要创建站点地图,只需编写一个Sitemap类并在 URLconf 中指向它。

安装

要安装站点地图应用,请按照以下步骤进行:

  • "django.contrib.sitemaps"添加到您的INSTALLED_APPS设置中。

  • 确保您的TEMPLATES设置包含一个DjangoTemplates后端,其APP_DIRS选项设置为 True。默认情况下就在那里,所以只有在更改了该设置时才需要更改这一点。

  • 确保您已安装了站点框架。

初始化

要在 Django 站点上激活站点地图生成,请将此行添加到您的 URLconf 中:

from django.contrib.sitemaps.views import sitemap 

url(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, 
    name='django.contrib.sitemaps.views.sitemap') 

这告诉 Django 在客户端访问/sitemap.xml时构建站点地图。站点地图文件的名称并不重要,但位置很重要。搜索引擎只会索引站点地图中当前 URL 级别及以下的链接。例如,如果sitemap.xml位于根目录中,它可以引用站点中的任何 URL。但是,如果您的站点地图位于/content/sitemap.xml,它只能引用以/content/开头的 URL。

站点地图视图需要一个额外的必需参数:{'sitemaps': sitemaps}sitemaps应该是一个将短部分标签(例如blognews)映射到其Sitemap类(例如BlogSitemapNewsSitemap)的字典。它也可以映射到Sitemap类的实例(例如BlogSitemap(some_var))。

站点地图类

Sitemap类是一个简单的 Python 类,表示站点地图中的条目部分。例如,一个Sitemap类可以表示您博客的所有条目,而另一个可以表示您事件日历中的所有事件。

在最简单的情况下,所有这些部分都被合并到一个sitemap.xml中,但也可以使用框架生成引用各个站点地图文件的站点地图索引,每个部分一个文件。(请参阅下面的创建站点地图索引。)

Sitemap类必须是django.contrib.sitemaps.Sitemap的子类。它们可以存在于代码库中的任何位置。

一个简单的例子

假设您有一个博客系统,其中有一个Entry模型,并且您希望您的站点地图包括到您个人博客条目的所有链接。以下是您的站点地图类可能如何看起来:

from django.contrib.sitemaps import Sitemap 
from 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 

注意:

  • changefreqpriority是对应于<changefreq><priority>元素的类属性。它们可以作为函数调用,就像上面的lastmod一样。

  • items()只是返回对象列表的方法。返回的对象将传递给与站点地图属性(locationlastmodchangefreqpriority)对应的任何可调用方法。

  • lastmod应返回 Python datetime对象。

  • 在此示例中没有location方法,但您可以提供它以指定对象的 URL。默认情况下,location()调用每个对象上的get_absolute_url()并返回结果。

站点地图类参考

Sitemap类可以定义以下方法/属性:

items

**必需。**返回对象列表的方法。框架不关心它们是什么类型的对象;重要的是这些对象传递给location()lastmod()changefreq()priority()方法。

位置

**可选。**可以是方法或属性。如果是方法,它应该返回items()返回的给定对象的绝对路径。如果是属性,其值应该是表示items()返回的每个对象使用的绝对路径的字符串。

在这两种情况下,绝对路径表示不包括协议或域的 URL。示例:

  • 好的:'/foo/bar/'

  • 不好:'example.com/foo/bar/'

  • 不好:'http://example.com/foo/bar/'

如果未提供location,框架将调用items()返回的每个对象上的get_absolute_url()方法。要指定除http之外的协议,请使用protocol

lastmod

**可选。**可以是方法或属性。如果是方法,它应该接受一个参数-items()返回的对象-并返回该对象的最后修改日期/时间,作为 Python datetime.datetime对象。

如果它是一个属性,其值应该是一个 Python datetime.datetime对象,表示items()返回的每个对象的最后修改日期/时间。如果站点地图中的所有项目都有lastmod,则views.sitemap()生成的站点地图将具有等于最新lastmodLast-Modified标头。

您可以激活ConditionalGetMiddleware,使 Django 对具有If-Modified-Since标头的请求做出适当响应,这将防止在站点地图未更改时发送站点地图。

changefreq

**可选。**可以是方法或属性。如果是方法,它应该接受一个参数-items()返回的对象-并返回该对象的更改频率,作为 Python 字符串。如果是属性,其值应该是表示items()返回的每个对象的更改频率的字符串。无论您使用方法还是属性,changefreq的可能值是:

  • 'always'

  • 'hourly'

  • 'daily'

  • 'weekly'

  • 'monthly'

  • 'yearly'

  • 'never'

priority

**可选。**可以是方法或属性。如果是方法,它应该接受一个参数-items()返回的对象-并返回该对象的优先级,作为字符串或浮点数。

如果它是一个属性,其值应该是一个字符串或浮点数,表示items()返回的每个对象的优先级。priority的示例值:0.41.0。页面的默认优先级为0.5。有关更多信息,请参阅 sitemaps.org 文档。

协议

**可选。**此属性定义站点地图中 URL 的协议(httphttps)。如果未设置,将使用请求站点地图的协议。如果站点地图是在请求的上下文之外构建的,则默认值为http

i18n

**可选。**一个布尔属性,定义此站点地图的 URL 是否应使用所有LANGUAGES生成。默认值为False

快捷方式

网站地图框架为常见情况提供了一个方便的类-django.contrib.syndication.GenericSitemap

django.contrib.sitemaps.GenericSitemap类允许您通过向其传递至少包含queryset条目的字典来创建站点地图。此查询集将用于生成站点地图的项目。它还可以具有指定从queryset检索的对象的日期字段的date_field条目。

这将用于生成的站点地图中的lastmod属性。您还可以将prioritychangefreq关键字参数传递给GenericSitemap构造函数,以指定所有 URL 的这些属性。

例子

以下是使用GenericSitemap的 URLconf 示例:

from django.conf.urls import url 
from django.contrib.sitemaps import GenericSitemap 
from django.contrib.sitemaps.views import sitemap 
from blog.models import Entry 

info_dict = { 
    'queryset': Entry.objects.all(), 
    'date_field': 'pub_date', 
} 

urlpatterns = [ 
    # some generic view using info_dict 
    # ... 

    # the sitemap 
    url(r'^sitemap\.xml$', sitemap, 
        {'sitemaps': {'blog': GenericSitemap(info_dict, priority=0.6)}},  
        name='django.contrib.sitemaps.views.sitemap'), 
] 

静态视图的站点地图

通常,您希望搜索引擎爬虫索引既不是对象详细页面也不是平面页面的视图。解决方案是在sitemapitems中显式列出这些视图的 URL 名称,并在sitemaplocation方法中调用reverse()。例如:

# sitemaps.py 
from django.contrib import sitemaps 
from django.core.urlresolvers import reverse 

class StaticViewSitemap(sitemaps.Sitemap): 
    priority = 0.5 
    changefreq = 'daily' 

    def items(self): 
        return ['main', 'about', 'license'] 

    def location(self, item): 
        return reverse(item) 

# urls.py 
from django.conf.urls import url 
from django.contrib.sitemaps.views import sitemap 

from .sitemaps import StaticViewSitemap 
from . import views 

sitemaps = { 
    'static': StaticViewSitemap, 
} 

urlpatterns = [ 
    url(r'^$', views.main, name='main'), 
    url(r'^about/$', views.about, name='about'), 
    url(r'^license/$', views.license, name='license'), 
    # ... 
    url(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, 
        name='django.contrib.sitemaps.views.sitemap') 
] 

创建站点地图索引

站点地图框架还具有创建引用各自sitemaps字典中定义的每个部分的单独站点地图文件的站点地图索引的功能。使用的唯一区别是:

  • 您在 URLconf 中使用了两个视图:django.contrib.sitemaps.views.index()django.contrib.sitemaps.views.sitemap()

  • django.contrib.sitemaps.views.sitemap()视图应该接受一个section关键字参数。

以下是上述示例的相关 URLconf 行的样子:

from django.contrib.sitemaps import views 

urlpatterns = [ 
    url(r'^sitemap\.xml$', views.index, {'sitemaps': sitemaps}), 
    url(r'^sitemap-(?P<section>.+)\.xml$', views.sitemap,  
        {'sitemaps': sitemaps}), 
] 

这将自动生成一个sitemap.xml文件,其中引用了sitemap-flatpages.xmlsitemap-blog.xmlSitemap类和sitemaps字典完全不会改变。

如果您的站点地图中有超过 50,000 个 URL,则应创建一个索引文件。在这种情况下,Django 将自动对站点地图进行分页,并且索引将反映这一点。如果您没有使用原始站点地图视图-例如,如果它被缓存装饰器包装-您必须为您的站点地图视图命名,并将sitemap_url_name传递给索引视图:

from django.contrib.sitemaps import views as sitemaps_views 
from django.views.decorators.cache import cache_page 

urlpatterns = [ 
    url(r'^sitemap\.xml$', 
        cache_page(86400)(sitemaps_views.index), 
        {'sitemaps': sitemaps, 'sitemap_url_name': 'sitemaps'}), 
    url(r'^sitemap-(?P<section>.+)\.xml$', 
        cache_page(86400)(sitemaps_views.sitemap), 
        {'sitemaps': sitemaps}, name='sitemaps'), 
] 

模板自定义

如果您希望在站点上可用的每个站点地图或站点地图索引使用不同的模板,您可以通过在 URLconf 中向sitemapindex视图传递template_name参数来指定它:

from django.contrib.sitemaps import views 

urlpatterns = [ 
    url(r'^custom-sitemap\.xml$', views.index, { 
        'sitemaps': sitemaps, 
        'template_name': 'custom_sitemap.html' 
    }), 
    url(r'^custom-sitemap-(?P<section>.+)\.xml$', views.sitemap, { 
    'sitemaps': sitemaps, 
    'template_name': 'custom_sitemap.html' 
}), 
] 

上下文变量

在自定义index()sitemap()视图的模板时,您可以依赖以下上下文变量。

索引

变量sitemaps是每个站点地图的绝对 URL 的列表。

站点地图

变量urlset是应该出现在站点地图中的 URL 列表。每个 URL 都公开了Sitemap类中定义的属性:

  • changefreq

  • item

  • lastmod

  • 位置

  • priority

已为每个 URL 添加了item属性,以允许对模板进行更灵活的自定义,例如 Google 新闻站点地图。假设 Sitemap 的items()将返回一个具有publication_datatags字段的项目列表,类似这样将生成一个与 Google 兼容的站点地图:

{% spaceless %} 
{% for url in urlset %} 
    {{ url.location }} 
    {% if url.lastmod %}{{ url.lastmod|date:"Y-m-d" }}{% endif %} 
    {% if url.changefreq %}{{ url.changefreq }}{% endif %} 
    {% if url.priority %}{{ url.priority }}{% endif %} 

      {% if url.item.publication_date %}{{ url.item.publication_date|date:"Y-m-d" }}{% endif %} 
      {% if url.item.tags %}{{ url.item.tags }}{% endif %} 

{% endfor %} 
{% endspaceless %} 

ping google

当您的站点地图发生更改时,您可能希望向 Google 发送 ping,以便让它知道重新索引您的站点。站点地图框架提供了一个函数来实现这一点:

django.contrib.syndication.ping_google()

ping_google()接受一个可选参数sitemap_url,它应该是站点地图的绝对路径(例如'/sitemap.xml')。如果未提供此参数,ping_google()将尝试通过在 URLconf 中执行反向查找来确定您的站点地图。如果无法确定您的站点地图 URL,ping_google()会引发异常django.contrib.sitemaps.SitemapNotFound

从模型的save()方法中调用ping_google()的一个有用的方法是:

from django.contrib.sitemaps import ping_google 

class Entry(models.Model): 
    # ... 
    def save(self, force_insert=False, force_update=False): 
        super(Entry, self).save(force_insert, force_update) 
        try: 
            ping_google() 
        except Exception: 
            # Bare 'except' because we could get a variety 
            # of HTTP-related exceptions. 
            pass 

然而,更有效的解决方案是从 cron 脚本或其他计划任务中调用ping_google()。该函数会向 Google 的服务器发出 HTTP 请求,因此您可能不希望在每次调用save()时引入网络开销。

通过 manage.py 向 Google 发送 ping

一旦站点地图应用程序添加到您的项目中,您还可以使用ping_google管理命令来 ping Google:

python manage.py ping_google [/sitemap.xml] 

注意

**首先向 Google 注册!**只有在您已经在 Google 网站管理员工具中注册了您的站点时,ping_google()命令才能起作用。

接下来是什么?

接下来,我们将继续深入研究 Django 提供的内置工具,通过更仔细地查看 Django 会话框架。

第十五章:Django 会话

想象一下,如果您每次导航到另一个页面都必须重新登录到网站,或者您最喜欢的网站忘记了所有的设置,您每次访问时都必须重新输入?

现代网站如果没有一种方式来记住您是谁以及您在网站上的先前活动,就无法提供我们习惯的可用性和便利性。HTTP 是无状态的设计-在一次请求和下一次请求之间没有持久性,服务器无法判断连续的请求是否来自同一个人。

这种状态的缺乏是通过会话来管理的,这是您的浏览器和 Web 服务器之间的一种半永久的双向通信。当您访问现代网站时,在大多数情况下,Web 服务器将使用匿名会话来跟踪与您的访问相关的数据。会话被称为匿名,因为 Web 服务器只能记录您的操作,而不能记录您是谁。

我们都经历过这种情况,当我们在以后返回到电子商务网站时,发现我们放在购物车中的物品仍然在那里,尽管没有提供任何个人信息。会话通常使用经常受到诟病但很少被理解的cookie来持久化。与所有其他 Web 框架一样,Django 也使用 cookie,但以更聪明和安全的方式,您将看到。

Django 完全支持匿名会话。会话框架允许您在每个站点访问者的基础上存储和检索任意数据。它在服务器端存储数据并抽象了发送和接收 cookie。Cookie 包含会话 ID-而不是数据本身(除非您使用基于 cookie 的后端);这是一种比其他框架更安全的实现 cookie 的方式。

启用会话

会话是通过中间件实现的。要启用会话功能,请编辑MIDDLEWARE_CLASSES设置,并确保其中包含'django.contrib.sessions.middleware.SessionMiddleware'。由django-admin startproject创建的默认settings.py已激活SessionMiddleware

如果您不想使用会话,您也可以从MIDDLEWARE_CLASSES中删除SessionMiddleware行,并从INSTALLED_APPS中删除'django.contrib.sessions'。这将节省一点开销。

配置会话引擎

默认情况下,Django 将会话存储在数据库中(使用模型django.contrib.sessions.models.Session)。虽然这很方便,但在某些设置中,将会话数据存储在其他地方可能更快,因此可以配置 Django 将会话数据存储在文件系统或缓存中。

使用基于数据库的会话

如果您想使用基于数据库的会话,您需要将'django.contrib.sessions'添加到您的INSTALLED_APPS设置中。一旦配置了安装,运行manage.py migrate来安装存储会话数据的单个数据库表。

使用缓存会话

为了获得更好的性能,您可能希望使用基于缓存的会话后端。要使用 Django 的缓存系统存储会话数据,您首先需要确保已配置了缓存;有关详细信息,请参阅缓存文档。

注意

只有在使用 Memcached 缓存后端时,才应该使用基于缓存的会话。本地内存缓存后端不会保留数据足够长时间,因此直接使用文件或数据库会话而不是通过文件或数据库缓存后端发送所有内容将更快。此外,本地内存缓存后端不是多进程安全的,因此在生产环境中可能不是一个好选择。

如果在CACHES中定义了多个缓存,Django 将使用默认缓存。要使用另一个缓存,将SESSION_CACHE_ALIAS设置为该缓存的名称。配置好缓存后,您有两种选择来存储缓存中的数据:

  • SESSION_ENGINE设置为"django.contrib.sessions.backends.cache"以使用简单的缓存会话存储。会话数据将直接存储在缓存中。但是,会话数据可能不是持久的:如果缓存填满或缓存服务器重新启动,缓存数据可能会被驱逐。

  • 对于持久的缓存数据,将SESSION_ENGINE设置为"django.contrib.sessions.backends.cached_db"。这使用了一个写入缓存-每次写入缓存时也会写入数据库。会话读取仅在数据不在缓存中时才使用数据库。

这两种会话存储都非常快,但简单缓存更快,因为它忽略了持久性。在大多数情况下,cached_db后端将足够快,但如果您需要最后一点性能,并且愿意让会话数据不时被清除,那么cache后端适合您。如果您使用cached_db会话后端,还需要遵循使用基于数据库的会话的配置说明。

使用基于文件的会话

要使用基于文件的会话,请将SESSION_ENGINE设置为"django.contrib.sessions.backends.file"。您可能还想设置SESSION_FILE_PATH设置(默认为tempfile.gettempdir()的输出,很可能是/tmp)以控制 Django 存储会话文件的位置。请确保您的 Web 服务器有权限读取和写入此位置。

要使用基于 cookie 的会话,请将SESSION_ENGINE设置为"django.contrib.sessions.backends.signed_cookies"。会话数据将使用 Django 的加密签名工具和SECRET_KEY设置进行存储。

建议将SESSION_COOKIE_HTTPONLY设置为True,以防止 JavaScript 访问存储的数据。

注意

如果SECRET_KEY不保密,并且您使用PickleSerializer,这可能导致任意远程代码执行。

拥有SECRET_KEY的攻击者不仅可以生成被您的站点信任的伪造会话数据,还可以远程执行任意代码,因为数据使用 pickle 进行序列化。如果您使用基于 cookie 的会话,请特别注意始终保持您的秘钥完全保密,以防止任何可能远程访问的系统。

注意

会话数据已签名但未加密

在使用 cookie 后端时,会话数据可以被客户端读取。使用 MAC(消息认证码)来保护数据免受客户端的更改,因此当被篡改时会使会话数据无效。如果存储 cookie 的客户端(例如,您的用户浏览器)无法存储所有会话 cookie 并丢弃数据,也会发生相同的无效。即使 Django 压缩了数据,仍然完全有可能超过每个 cookie 的常见限制 4096 字节。

注意

没有新鲜度保证

还要注意,虽然 MAC 可以保证数据的真实性(即它是由您的站点生成的,而不是其他人),以及数据的完整性(即它是否完整且正确),但它无法保证新鲜度,也就是说,您被发送回客户端的是您最后发送的内容。这意味着对于某些会话数据的使用,cookie 后端可能会使您容易受到重放攻击。与其他会话后端不同,其他会话后端会在用户注销时保留每个会话的服务器端记录并使其无效,而基于 cookie 的会话在用户注销时不会被无效。因此,如果攻击者窃取了用户的 cookie,他们可以使用该 cookie 以该用户的身份登录,即使用户已注销。只有当 cookie 的年龄大于您的SESSION_COOKIE_AGE时,才会检测到 cookie 已过期。

最后,假设上述警告没有阻止您使用基于 cookie 的会话:cookie 的大小也会影响站点的速度。

在视图中使用会话

当激活SessionMiddleware时,每个HttpRequest对象-任何 Django 视图函数的第一个参数-都将有一个session属性,这是一个类似字典的对象。您可以在视图的任何时候读取它并写入request.session。您可以多次编辑它。

所有会话对象都继承自基类backends.base.SessionBase。它具有以下标准字典方法:

  • __getitem__(key)

  • __setitem__(key, value)

  • __delitem__(key)

  • __contains__(key)

  • get(key, default=None)

  • pop(key)

  • keys()

  • items()

  • setdefault()

  • clear()

它还具有这些方法:

flush()

从会话中删除当前会话数据并删除会话 cookie。如果您希望确保无法再次从用户的浏览器访问以前的会话数据(例如,django.contrib.auth.logout()函数调用它)。

设置一个测试 cookie 以确定用户的浏览器是否支持 cookie。由于 cookie 的工作方式,您将无法在用户的下一个页面请求之前测试这一点。有关更多信息,请参见下面的设置测试 cookie

返回TrueFalse,取决于用户的浏览器是否接受了测试 cookie。由于 cookie 的工作方式,您将不得不在先前的单独页面请求上调用set_test_cookie()。有关更多信息,请参见下面的设置测试 cookie

删除测试 cookie。使用此方法进行清理。

set_expiry(value)

设置会话的过期时间。您可以传递许多不同的值:

  • 如果value是一个整数,会话将在多少秒的不活动后过期。例如,调用request.session.set_expiry(300)会使会话在 5 分钟后过期。

  • 如果valuedatetimetimedelta对象,则会话将在特定日期/时间过期。请注意,只有在使用PickleSerializer时,datetimetimedelta值才能被序列化。

  • 如果value0,用户的会话 cookie 将在用户的 Web 浏览器关闭时过期。

  • 如果valueNone,会话将恢复使用全局会话过期策略。

阅读会话不被视为过期目的的活动。会话的过期是根据会话上次修改的时间计算的。

get_expiry_age()

返回直到此会话过期的秒数。对于没有自定义过期时间(或者设置为在浏览器关闭时过期)的会话,这将等于SESSION_COOKIE_AGE。此函数接受两个可选的关键字参数:

  • modification:会话的最后修改,作为datetime对象。默认为当前时间

  • expiry:会话的过期信息,作为datetime对象,一个int(以秒为单位),或None。默认为通过set_expiry()存储在会话中的值,如果有的话,或None

get_expiry_date()

返回此会话将过期的日期。对于没有自定义过期时间(或者设置为在浏览器关闭时过期)的会话,这将等于从现在开始SESSION_COOKIE_AGE秒的日期。此函数接受与get_expiry_age()相同的关键字参数。

get_expire_at_browser_close()

返回TrueFalse,取决于用户的会话 cookie 是否在用户的 Web 浏览器关闭时过期。

clear_expired()

从会话存储中删除过期的会话。这个类方法由clearsessions调用。

cycle_key()

在保留当前会话数据的同时创建一个新的会话密钥。django.contrib.auth.login()调用此方法以减轻会话固定。

会话对象指南

  • request.session上使用普通的 Python 字符串作为字典键。这更多是一种约定而不是一条硬性规定。

  • 以下划线开头的会话字典键是由 Django 内部使用的保留字。

不要用新对象覆盖request.session,也不要访问或设置其属性。像使用 Python 字典一样使用它。

会话序列化

在 1.6 版本之前,Django 默认使用pickle对会话数据进行序列化后存储在后端。如果您使用签名的 cookie 会话后端并且SECRET_KEY被攻击者知晓(Django 本身没有固有的漏洞会导致泄漏),攻击者可以在其会话中插入一个字符串,该字符串在反序列化时在服务器上执行任意代码。这种技术简单易行,并且在互联网上很容易获得。

尽管 cookie 会话存储对 cookie 存储的数据进行签名以防篡改,但SECRET_KEY泄漏会立即升级为远程代码执行漏洞。可以通过使用 JSON 而不是pickle对会话数据进行序列化来减轻此攻击。为了方便这一点,Django 1.5.3 引入了一个新的设置SESSION_SERIALIZER,用于自定义会话序列化格式。为了向后兼容,Django 1.5.x 中此设置默认使用django.contrib.sessions.serializers.PickleSerializer,但为了加强安全性,从 Django 1.6 开始默认使用django.contrib.sessions.serializers.JSONSerializer

即使在自定义序列化器中描述的注意事项中,我们强烈建议坚持使用 JSON 序列化特别是如果您使用 cookie 后端

捆绑的序列化器

序列化器.JSONSerializer

django.core.signing的 JSON 序列化器周围的包装器。只能序列化基本数据类型。此外,由于 JSON 仅支持字符串键,请注意在request.session中使用非字符串键将无法按预期工作:

>>> # initial assignment 
>>> request.session[0] = 'bar' 
>>> # subsequent requests following serialization & deserialization 
>>> # of session data 
>>> request.session[0]  # KeyError 
>>> request.session['0'] 
'bar' 

请参阅自定义序列化器部分,了解 JSON 序列化的限制详情。

序列化器.PickleSerializer

支持任意 Python 对象,但如上所述,如果SECRET_KEY被攻击者知晓,可能会导致远程代码执行漏洞。

编写自己的序列化器

请注意,与PickleSerializer不同,JSONSerializer无法处理任意 Python 数据类型。通常情况下,方便性和安全性之间存在权衡。如果您希望在 JSON 支持的会话中存储更高级的数据类型,包括datetimeDecimal,则需要编写自定义序列化器(或在将这些值存储在request.session之前将其转换为 JSON 可序列化对象)。

虽然序列化这些值相当简单(django.core.serializers.json.DateTimeAwareJSONEncoder可能会有所帮助),但编写一个可靠地获取与输入相同内容的解码器更加脆弱。例如,您可能会冒返回实际上是字符串的datetime的风险,只是碰巧与datetime选择的相同格式相匹配)。

您的序列化器类必须实现两个方法,dumps(self, obj)loads(self, data),分别用于序列化和反序列化会话数据字典。

设置测试 cookie

作为便利,Django 提供了一种简单的方法来测试用户的浏览器是否接受 cookie。只需在视图中调用request.sessionset_test_cookie()方法,并在随后的视图中调用test_cookie_worked(),而不是在同一视图调用中。

set_test_cookie()test_cookie_worked()之间的这种尴尬分离是由于 cookie 的工作方式。当您设置一个 cookie 时,实际上无法确定浏览器是否接受它,直到浏览器的下一个请求。在验证测试 cookie 有效后,请使用delete_test_cookie()进行清理是一个良好的做法。

以下是典型的用法示例:

def login(request): 
    if request.method == 'POST': 
        if request.session.test_cookie_worked(): 
            request.session.delete_test_cookie() 
            return HttpResponse("You're logged in.") 
        else: 
            return HttpResponse("Please enable cookies and try again.") 
    request.session.set_test_cookie() 
    return render_to_response('foo/login_form.html') 

在视图之外使用会话

本节中的示例直接从django.contrib.sessions.backends.db后端导入SessionStore对象。在您自己的代码中,您应该考虑从SESSION_ENGINE指定的会话引擎中导入SessionStore,如下所示:

>>> from importlib import import_module 
>>> from django.conf import settings 
>>> SessionStore = import_module(settings.SESSION_ENGINE).SessionStore 

API 可用于在视图之外操作会话数据:

>>> from django.contrib.sessions.backends.db import SessionStore 
>>> s = SessionStore() 
>>> # stored as seconds since epoch since datetimes are not serializable in JSON. 
>>> s['last_login'] = 1376587691 
>>> s.save() 
>>> s.session_key 
'2b1189a188b44ad18c35e113ac6ceead' 

>>> s = SessionStore(session_key='2b1189a188b44ad18c35e113ac6ceead') 
>>> s['last_login'] 
1376587691 

为了减轻会话固定攻击,不存在的会话密钥将被重新生成:

>>> from django.contrib.sessions.backends.db import SessionStore 
>>> s = SessionStore(session_key='no-such-session-here') 
>>> s.save() 
>>> s.session_key 
'ff882814010ccbc3c870523934fee5a2' 

如果您使用django.contrib.sessions.backends.db后端,每个会话只是一个普通的 Django 模型。Session模型在django/contrib/sessions/models.py中定义。因为它是一个普通模型,您可以使用普通的 Django 数据库 API 访问会话:

>>> from django.contrib.sessions.models import Session 
>>> s = Session.objects.get(pk='2b1189a188b44ad18c35e113ac6ceead') 
>>> s.expire_date 
datetime.datetime(2005, 8, 20, 13, 35, 12) 
Note that you'll need to call get_decoded() to get the session dictionary. This is necessary because the dictionary is stored in an encoded format: 
>>> s.session_data 
'KGRwMQpTJ19hdXRoX3VzZXJfaWQnCnAyCkkxCnMuMTExY2ZjODI2Yj...' 
>>> s.get_decoded() 
{'user_id': 42} 

会话保存时

默认情况下,只有在会话已被修改时(即其字典值已被分配或删除)Django 才会保存到会话数据库:

# 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, because this alters 
# request.session['foo'] instead of request.session. 
request.session['foo']['bar'] = 'baz' 

在上面示例的最后一种情况中,我们可以通过在会话对象上设置modified属性来明确告诉会话对象已被修改:

request.session.modified = True 

要更改此默认行为,请将SESSION_SAVE_EVERY_REQUEST设置为True。当设置为True时,Django 将在每个请求上将会话保存到数据库。请注意,只有在创建或修改会话时才会发送会话 cookie。如果SESSION_SAVE_EVERY_REQUESTTrue,则会在每个请求上发送会话 cookie。类似地,会话 cookie 的expires部分在每次发送会话 cookie 时都会更新。如果响应的状态码为 500,则不会保存会话。

浏览器长度会话与持久会话

您可以通过SESSION_EXPIRE_AT_BROWSER_CLOSE设置来控制会话框架是使用浏览器长度会话还是持久会话。默认情况下,SESSION_EXPIRE_AT_BROWSER_CLOSE设置为False,这意味着会话 cookie 将在用户的浏览器中存储,直到SESSION_COOKIE_AGE。如果您不希望用户每次打开浏览器时都需要登录,请使用此设置。

如果SESSION_EXPIRE_AT_BROWSER_CLOSE设置为True,Django 将使用浏览器长度的 cookie-即当用户关闭浏览器时立即过期的 cookie。

注意

一些浏览器(例如 Chrome)提供设置,允许用户在关闭和重新打开浏览器后继续浏览会话。在某些情况下,这可能会干扰SESSION_EXPIRE_AT_BROWSER_CLOSE设置,并阻止会话在关闭浏览器时过期。请在测试启用了SESSION_EXPIRE_AT_BROWSER_CLOSE设置的 Django 应用程序时注意这一点。

清除会话存储

当用户在您的网站上创建新会话时,会话数据可能会在会话存储中累积。Django 不提供自动清除过期会话。因此,您需要定期清除过期会话。Django 为此提供了一个清理管理命令:clearsessions。建议定期调用此命令,例如作为每日 cron 作业。

请注意,缓存后端不会受到此问题的影响,因为缓存会自动删除过时数据。Cookie 后端也不会受到影响,因为会话数据是由用户的浏览器存储的。

接下来是什么

接下来,我们将继续研究更高级的 Django 主题,通过检查 Django 的缓存后端。

第十六章:Django 的缓存框架

动态网站的一个基本权衡是,它们是动态的。每当用户请求一个页面时,Web 服务器都会进行各种计算,从数据库查询到模板渲染到业务逻辑再到创建用户所看到的页面。从处理开销的角度来看,这比标准的从文件系统中读取文件的服务器安排要昂贵得多。

对于大多数 Web 应用程序来说,这种开销并不是什么大问题。大多数 Web 应用程序不是 www.washingtonpost.com 或 www.slashdot.org;它们只是一些流量一般的中小型站点。但对于中高流量的站点来说,尽量减少开销是至关重要的。

这就是缓存的作用。缓存某些东西就是保存昂贵计算的结果,这样你就不必在下一次执行计算。下面是一些伪代码,解释了这在动态生成的网页上是如何工作的:

given a URL, try finding that page in the cache 
if the page is in the cache: 
    return the cached page 
else: 
    generate the page 
    save the generated page in the cache (for next time) 
    return the generated page 

Django 自带一个强大的缓存系统,可以让你保存动态页面,这样它们就不必为每个请求重新计算。为了方便起见,Django 提供了不同级别的缓存粒度:你可以缓存特定视图的输出,也可以只缓存难以生成的部分,或者缓存整个站点。

Django 也可以很好地与下游缓存(如 Squid,更多信息请访问 www.squid-cache.org/)和基于浏览器的缓存一起使用。这些是你无法直接控制的缓存类型,但你可以通过 HTTP 头提供关于你的站点应该缓存哪些部分以及如何缓存的提示。

设置缓存

缓存系统需要进行一些设置。主要是告诉它你的缓存数据应该存放在哪里;是在数据库中、在文件系统中还是直接在内存中。这是一个影响缓存性能的重要决定。

你的缓存偏好设置在设置文件的 CACHES 设置中。

Memcached

Django 原生支持的最快、最高效的缓存类型是 Memcached(更多信息请访问 memcached.org/),它是一个完全基于内存的缓存服务器,最初是为了处理 LiveJournal.com 上的高负载而开发的,并且后来由 Danga Interactive 开源。它被 Facebook 和 Wikipedia 等网站使用,以减少数据库访问并显著提高站点性能。

Memcached 作为守护进程运行,并被分配了指定的内存量。它所做的就是提供一个快速的接口,用于在缓存中添加、检索和删除数据。所有数据都直接存储在内存中,因此没有数据库或文件系统使用的开销。

安装完 Memcached 本身后,你需要安装一个 Memcached 绑定。有几个 Python Memcached 绑定可用;最常见的两个是 python-memcached(ftp://ftp.tummy.com/pub/python-memcached/)和 pylibmc(sendapatch.se/projects/pylibmc/)。要在 Django 中使用 Memcached:

  • BACKEND 设置为 django.core.cache.backends.memcached.MemcachedCachedjango.core.cache.backends.memcached.PyLibMCCache(取决于你选择的 memcached 绑定)

  • LOCATION 设置为 ip:port 值,其中 ip 是 Memcached 守护进程的 IP 地址,port 是 Memcached 运行的端口,或者设置为 unix:path 值,其中 path 是 Memcached Unix socket 文件的路径。

在这个例子中,Memcached 在本地主机(127.0.0.1)的端口 11211 上运行,使用 python-memcached 绑定:

CACHES = { 
    'default': { 
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 
        'LOCATION': '127.0.0.1:11211', 
    } 
} 

在这个例子中,Memcached 可以通过本地的 Unix socket 文件 /tmp/memcached.sock 使用 python-memcached 绑定来访问:

CACHES = { 
    'default': { 
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 
        'LOCATION': 'unix:/tmp/memcached.sock', 
    } 
} 

Memcached 的一个优秀特性是它能够在多台服务器上共享缓存。这意味着您可以在多台机器上运行 Memcached 守护程序,并且程序将把这组机器视为单个缓存,而无需在每台机器上复制缓存值。要利用这个特性,在LOCATION中包含所有服务器地址,可以用分号分隔或作为列表。

在这个例子中,缓存是在 IP 地址172.19.26.240172.19.26.242上运行的 Memcached 实例之间共享的,端口都是 11211:

CACHES = { 
    'default': { 
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 
        'LOCATION': [ 
            '172.19.26.240:11211', 
            '172.19.26.242:11211', 
        ] 
    } 
} 

在下面的例子中,缓存是在 IP 地址172.19.26.240(端口 11211)、172.19.26.242(端口 11212)和172.19.26.244(端口 11213)上运行的 Memcached 实例之间共享的:

CACHES = { 
    'default': { 
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 
        'LOCATION': [ 
            '172.19.26.240:11211', 
            '172.19.26.242:11212', 
            '172.19.26.244:11213', 
        ] 
    } 
} 

关于 Memcached 的最后一点是,基于内存的缓存有一个缺点:因为缓存数据存储在内存中,如果服务器崩溃,数据将丢失。

显然,内存并不适用于永久数据存储,因此不要仅依赖基于内存的缓存作为您唯一的数据存储。毫无疑问,Django 缓存后端都不应该用于永久存储-它们都是用于缓存而不是存储的解决方案-但我们在这里指出这一点是因为基于内存的缓存特别是临时的。

数据库缓存

Django 可以将其缓存数据存储在您的数据库中。如果您有一个快速、索引良好的数据库服务器,这将效果最佳。要将数据库表用作缓存后端:

  • BACKEND设置为django.core.cache.backends.db.DatabaseCache

  • LOCATION设置为tablename,即数据库表的名称。这个名称可以是任何你想要的,只要它是一个有效的表名,而且在你的数据库中还没有被使用。

在这个例子中,缓存表的名称是my_cache_table

CACHES = { 
    'default': { 
        'BACKEND': 'django.core.cache.backends.db.DatabaseCache', 
        'LOCATION': 'my_cache_table', 
    } 
} 

创建缓存表

在使用数据库缓存之前,您必须使用这个命令创建缓存表:

python manage.py createcachetable 

这将在您的数据库中创建一个符合 Django 数据库缓存系统期望的正确格式的表。表的名称取自LOCATION。如果您使用多个数据库缓存,createcachetable会为每个缓存创建一个表。如果您使用多个数据库,createcachetable会观察数据库路由器的allow_migrate()方法(见下文)。与migrate一样,createcachetable不会触及现有表。它只会创建缺失的表。

多个数据库

如果您在使用多个数据库进行数据库缓存,还需要为数据库缓存表设置路由指令。对于路由的目的,数据库缓存表显示为一个名为CacheEntry的模型,在名为django_cache的应用程序中。这个模型不会出现在模型缓存中,但模型的详细信息可以用于路由目的。

例如,以下路由器将所有缓存读取操作定向到cache_replica,并将所有写操作定向到cache_primary。缓存表只会同步到cache_primary

class CacheRouter(object): 
    """A router to control all database cache operations""" 

    def db_for_read(self, model, **hints): 
        # All cache read operations go to the replica 
        if model._meta.app_label in ('django_cache',): 
            return 'cache_replica' 
        return None 

    def db_for_write(self, model, **hints): 
        # All cache write operations go to primary 
        if model._meta.app_label in ('django_cache',): 
            return 'cache_primary' 
        return None 

    def allow_migrate(self, db, model): 
        # Only install the cache model on primary 
        if model._meta.app_label in ('django_cache',): 
            return db == 'cache_primary' 
        return None 

如果您没有为数据库缓存模型指定路由指令,缓存后端将使用default数据库。当然,如果您不使用数据库缓存后端,您就不需要担心为数据库缓存模型提供路由指令。

文件系统缓存

基于文件的后端将每个缓存值序列化并存储为单独的文件。要使用此后端,将BACKEND设置为'django.core.cache.backends.filebased.FileBasedCache',并将LOCATION设置为适当的目录。

例如,要将缓存数据存储在/var/tmp/django_cache中,使用以下设置:

CACHES = { 
    'default': { 
        'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', 
        'LOCATION': '/var/tmp/django_cache', 
    } 
} 

如果您在 Windows 上,将驱动器号放在路径的开头,就像这样:

CACHES = { 
    'default': { 
        'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', 
        'LOCATION': 'c:/foo/bar', 
    } 
} 

目录路径应该是绝对的-也就是说,它应该从文件系统的根目录开始。设置末尾是否加斜杠并不重要。确保此设置指向的目录存在,并且可以被运行您的网页服务器的系统用户读取和写入。继续上面的例子,如果您的服务器以用户apache运行,请确保目录/var/tmp/django_cache存在,并且可以被用户apache读取和写入。

本地内存缓存

如果在设置文件中未指定其他缓存,则这是默认缓存。如果您想要内存缓存的速度优势,但又没有运行 Memcached 的能力,请考虑使用本地内存缓存后端。要使用它,请将BACKEND设置为django.core.cache.backends.locmem.LocMemCache。例如:

CACHES = { 
    'default': { 
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 
        'LOCATION': 'unique-snowflake' 
    } 
} 

缓存LOCATION用于标识单个内存存储。如果您只有一个locmem缓存,可以省略LOCATION;但是,如果您有多个本地内存缓存,您将需要为其中至少一个分配一个名称,以便将它们分开。

请注意,每个进程将拥有自己的私有缓存实例,这意味着不可能进行跨进程缓存。这显然也意味着本地内存缓存不是特别内存高效,因此在生产环境中可能不是一个好选择。但对于开发来说是不错的选择。

虚拟缓存(用于开发)

最后,Django 附带了一个虚拟缓存,它实际上不缓存-它只是实现了缓存接口而不执行任何操作。如果您的生产站点在各个地方都使用了重度缓存,但在开发/测试环境中不想缓存并且不想改变代码以特殊处理后者,这将非常有用。要激活虚拟缓存,请将BACKEND设置如下:

CACHES = { 
    'default': { 
        'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 
    } 
} 

使用自定义缓存后端

尽管 Django 默认支持多种缓存后端,但有时您可能希望使用自定义的缓存后端。要在 Django 中使用外部缓存后端,请将 Python 导入路径作为CACHES设置的BACKEND,如下所示:

CACHES = { 
    'default': { 
        'BACKEND': 'path.to.backend', 
    } 
} 

如果您正在构建自己的后端,可以使用标准缓存后端作为参考实现。您可以在 Django 源代码的django/core/cache/backends/目录中找到这些代码。

注意

除非有一个真正令人信服的理由,比如不支持它们的主机,否则您应该坚持使用 Django 提供的缓存后端。它们经过了充分测试,易于使用。

缓存参数

每个缓存后端都可以提供额外的参数来控制缓存行为。这些参数作为CACHES设置中的额外键提供。有效参数如下:

  • TIMEOUT:用于缓存的默认超时时间(以秒为单位)。此参数默认为 300 秒(5 分钟)。您可以将TIMEOUT设置为None,以便默认情况下缓存键永不过期。值为0会导致键立即过期(实际上不缓存)。

  • OPTIONS:应传递给缓存后端的任何选项。有效选项的列表将随着每个后端的不同而变化,并且由第三方库支持的缓存后端将直接将它们的选项传递给底层缓存库。

  • 实现自己的清除策略的缓存后端(即locmemfilesystemdatabase后端)将遵守以下选项:

  • MAX_ENTRIES:在旧值被删除之前缓存中允许的最大条目数。此参数默认为300

  • CULL_FREQUENCY:当达到MAX_ENTRIES时被删除的条目比例。实际比例是1 / CULL_FREQUENCY,因此将CULL_FREQUENCY设置为2,以在达到MAX_ENTRIES时删除一半的条目。此参数应为整数,默认为3

  • CULL_FREQUENCY的值为0意味着当达到MAX_ENTRIES时整个缓存将被清除。在某些后端(特别是database)上,这样做会使清除快,但会增加缓存未命中的次数。

  • KEY_PREFIX:一个字符串,将自动包含(默认情况下是前置)到 Django 服务器使用的所有缓存键中。

  • VERSION:Django 服务器生成的缓存键的默认版本号。

  • KEY_FUNCTION:包含一个点路径到一个函数的字符串,该函数定义如何将前缀、版本和键组合成最终的缓存键。

在这个例子中,文件系统后端被配置为超时 60 秒,并且最大容量为 1000 个项目:

CACHES = { 
    'default': { 
        'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', 
        'LOCATION': '/var/tmp/django_cache', 
        'TIMEOUT': 60, 
        'OPTIONS': {'MAX_ENTRIES': 1000} 
    } 
} 

每个站点的缓存

设置缓存后,使用缓存的最简单方法是缓存整个站点。您需要将'django.middleware.cache.UpdateCacheMiddleware''django.middleware.cache.FetchFromCacheMiddleware'添加到您的MIDDLEWARE_CLASSES设置中,就像这个例子中一样:

MIDDLEWARE_CLASSES = [ 
    'django.middleware.cache.UpdateCacheMiddleware', 
    'django.middleware.common.CommonMiddleware', 
    'django.middleware.cache.FetchFromCacheMiddleware', 
] 

注意

不,这不是一个打字错误:更新中间件必须在列表中首先出现,获取中间件必须在最后出现。细节有点模糊,但是如果您想要完整的故事,请参阅下一章中的 MIDDLEWARE_CLASSES 顺序。

然后,将以下必需的设置添加到您的 Django 设置文件中:

  • CACHE_MIDDLEWARE_ALIAS:用于存储的缓存别名。

  • CACHE_MIDDLEWARE_SECONDS:每个页面应该被缓存的秒数。

  • CACHE_MIDDLEWARE_KEY_PREFIX-:如果缓存跨多个使用相同 Django 安装的站点共享,则将其设置为站点的名称,或者是对此 Django 实例唯一的其他字符串,以防止键冲突。如果您不在乎,可以使用空字符串。

FetchFromCacheMiddleware使用status 200缓存GETHEAD响应,其中请求和响应头允许。对于具有不同查询参数的相同 URL 的请求的响应被认为是唯一的页面,并且被单独缓存。此中间件期望HEAD请求以与相应的GET请求相同的响应头进行响应;在这种情况下,它可以为HEAD请求返回缓存的GET响应。此外,UpdateCacheMiddleware自动在每个HttpResponse中设置一些头:

  • Last-Modified头设置为请求新的(未缓存)页面时的当前日期/时间。

  • Expires头设置为当前日期/时间加上定义的CACHE_MIDDLEWARE_SECONDS

  • Cache-Control头设置为页面的最大年龄-同样,从CACHE_MIDDLEWARE_SECONDS设置。

如果视图设置了自己的缓存到期时间(即它在其max-age部分中有一个),则页面将被缓存直到到期时间,而不是CACHE_MIDDLEWARE_SECONDS

Cache-Control头)那么页面将被缓存直到到期时间,而不是CACHE_MIDDLEWARE_SECONDS。使用django.views.decorators.cache中的装饰器,您可以轻松地设置视图的到期时间(使用cache_control装饰器)或禁用视图的缓存(使用never_cache装饰器)。有关这些装饰器的更多信息,请参阅使用其他标头部分。

如果USE_I18N设置为True,则生成的缓存键将包括活动语言的名称。这样,您可以轻松地缓存多语言站点,而无需自己创建缓存键。

USE_L10N设置为True时,缓存键还包括活动语言,当USE_TZ设置为True时,还包括当前时区。

每个视图的缓存

使用缓存框架的更细粒度的方法是通过缓存单个视图的输出。django.views.decorators.cache定义了一个cache_page装饰器,它将自动为您缓存视图的响应。使用起来很容易:

from django.views.decorators.cache import cache_page 

@cache_page(60 * 15) 
def my_view(request): 
    ... 

cache_page接受一个参数:缓存超时时间,以秒为单位。在上面的例子中,my_view()视图的结果将被缓存 15 分钟。(请注意,我已经将其写成60 * 15,以便阅读。60 * 15将被计算为900-也就是说,15 分钟乘以 60 秒每分钟。)

每个视图的缓存,就像每个站点的缓存一样,是基于 URL 的。如果多个 URL 指向同一个视图,每个 URL 将被单独缓存。继续my_view的例子,如果您的 URLconf 如下所示:

urlpatterns = [ 
    url(r'^foo/([0-9]{1,2})/$', my_view), 
] 

然后对/foo/1//foo/23/的请求将被分别缓存,正如你可能期望的那样。但一旦请求了特定的 URL(例如/foo/23/),随后对该 URL 的请求将使用缓存。

cache_page还可以接受一个可选的关键字参数cache,它指示装饰器在缓存视图结果时使用特定的缓存(来自你的CACHES设置)。

默认情况下,将使用default缓存,但你可以指定任何你想要的缓存:

@cache_page(60 * 15, cache="special_cache") 
def my_view(request): 
    ... 

你也可以在每个视图的基础上覆盖缓存前缀。cache_page接受一个可选的关键字参数key_prefix,它的工作方式与中间件的CACHE_MIDDLEWARE_KEY_PREFIX设置相同。可以像这样使用:

@cache_page(60 * 15, key_prefix="site1") 
def my_view(request): 
    ... 

key_prefixcache参数可以一起指定。key_prefix参数和在CACHES下指定的KEY_PREFIX将被连接起来。

在 URLconf 中指定每个视图的缓存

前一节中的示例已经硬编码了视图被缓存的事实,因为cache_page会直接修改my_view函数。这种方法将你的视图与缓存系统耦合在一起,这对于几个原因来说都不理想。例如,你可能希望在另一个没有缓存的站点上重用视图函数,或者你可能希望将视图分发给可能希望在没有被缓存的情况下使用它们的人。

解决这些问题的方法是在 URLconf 中指定每个视图的缓存,而不是在视图函数旁边。这样做很容易:只需在 URLconf 中引用视图函数时用cache_page包装视图函数即可。

这是之前的旧 URLconf:

urlpatterns = [ 
    url(r'^foo/([0-9]{1,2})/$', my_view), 
] 

这里是相同的内容,my_view被包裹在cache_page中:

from django.views.decorators.cache import cache_page 

urlpatterns = [ 
    url(r'^foo/([0-9]{1,2})/$', cache_page(60 * 15)(my_view)), 
] 

模板片段缓存

如果你想要更多的控制,你也可以使用cache模板标签来缓存模板片段。为了让你的模板可以访问这个标签,放置

在模板顶部附近使用{% load cache %}{% cache %}模板标签会缓存给定时间内的块内容。

它至少需要两个参数:缓存超时(以秒为单位)和要给缓存片段的名称。名称将被直接使用,不要使用变量。

例如:

{% load cache %} 
{% cache 500 sidebar %} 
    .. sidebar .. 
{% endcache %} 

有时你可能希望根据片段内部出现的一些动态数据来缓存多个副本的片段。

例如,你可能希望为站点的每个用户使用前面示例中使用的侧边栏的单独缓存副本。通过向{% cache %}模板标签传递额外的参数来唯一标识缓存片段来实现这一点:

{% load cache %} 
{% cache 500 sidebar request.user.username %} 
    .. sidebar for logged in user .. 
{% endcache %} 

指定多个参数来标识片段是完全可以的。只需向{% cache %}传递所需的参数即可。如果USE_I18N设置为True,则每个站点的中间件缓存将遵循活动语言。

对于cache模板标签,你可以使用模板中可用的翻译特定变量之一来实现相同的结果:

{% load i18n %} 
{% load cache %} 

{% get_current_language as LANGUAGE_CODE %} 

{% cache 600 welcome LANGUAGE_CODE %} 
    {% trans "Welcome to example.com" %} 
{% endcache %} 

缓存超时可以是一个模板变量,只要模板变量解析为整数值即可。

例如,如果模板变量my_timeout设置为值600,那么以下两个示例是等价的:

{% cache 600 sidebar %} ... {% endcache %} 
{% cache my_timeout sidebar %} ... {% endcache %} 

这个功能在模板中避免重复很有用。你可以在一个地方设置超时,然后只需重用该值。默认情况下,缓存标签将尝试使用名为template_fragments的缓存。如果没有这样的缓存存在,它将退回到使用默认缓存。你可以选择一个备用的缓存后端来与using关键字参数一起使用,这必须是标签的最后一个参数。

{% cache 300 local-thing ...  using="localcache" %} 

指定未配置的缓存名称被认为是一个错误。

如果你想获取用于缓存片段的缓存键,你可以使用make_template_fragment_keyfragment_namecache模板标签的第二个参数相同;vary_on是传递给标签的所有额外参数的列表。这个函数对于使缓存项无效或覆盖缓存项可能很有用,例如:

>>> from django.core.cache import cache 
>>> from django.core.cache.utils import make_template_fragment_key 
# cache key for {% cache 500 sidebar username %} 
>>> key = make_template_fragment_key('sidebar', [username]) 
>>> cache.delete(key) # invalidates cached template fragment 

低级别缓存 API

有时,缓存整个渲染页面并不会带来太多好处,实际上,这种方式过度。例如,您的站点可能包括一个视图,其结果取决于几个昂贵的查询,这些查询的结果在不同的时间间隔内发生变化。在这种情况下,使用每个站点或每个视图缓存策略提供的全页缓存并不理想,因为您不希望缓存整个结果(因为某些数据经常更改),但仍然希望缓存很少更改的结果。

对于这样的情况,Django 公开了一个简单的低级缓存 API。您可以使用此 API 以任何您喜欢的粒度存储对象。您可以缓存任何可以安全进行 pickle 的 Python 对象:字符串,字典,模型对象列表等(大多数常见的 Python 对象都可以进行 pickle;有关 pickling 的更多信息,请参阅 Python 文档)。

访问缓存

您可以通过类似字典的对象django.core.cache.caches访问CACHES设置中配置的缓存。在同一线程中对同一别名的重复请求将返回相同的对象。

>>> from django.core.cache import caches 
>>> cache1 = caches['myalias'] 
>>> cache2 = caches['myalias'] 
>>> cache1 is cache2 
True 

如果命名键不存在,则将引发InvalidCacheBackendError。为了提供线程安全性,将为每个线程返回缓存后端的不同实例。

作为快捷方式,默认缓存可用为django.core.cache.cache

>>> from django.core.cache import cache 

此对象等同于caches['default']

基本用法

基本接口是set(key,value,timeout)get(key)

>>> cache.set('my_key', 'hello, world!', 30) 
>>> cache.get('my_key') 
'hello, world!' 

timeout参数是可选的,默认为CACHES设置中适当后端的timeout参数(如上所述)。这是值应在缓存中存储的秒数。将None传递给timeout将永远缓存该值。timeout0将不会缓存该值。如果对象在缓存中不存在,则cache.get()将返回None

# Wait 30 seconds for 'my_key' to expire... 

>>> cache.get('my_key') 
None 

我们建议不要将文字值None存储在缓存中,因为您无法区分存储的None值和由返回值None表示的缓存未命中。cache.get()可以接受default参数。这指定如果对象在缓存中不存在时要返回的值:

>>> cache.get('my_key', 'has expired') 
'has expired' 

要仅在键不存在时添加键,请使用add()方法。它接受与set()相同的参数,但如果指定的键已经存在,则不会尝试更新缓存:

>>> cache.set('add_key', 'Initial value') 
>>> cache.add('add_key', 'New value') 
>>> cache.get('add_key') 
'Initial value' 

如果您需要知道add()是否将值存储在缓存中,可以检查返回值。如果存储了该值,则返回True,否则返回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} 

要更有效地设置多个值,请使用set_many()传递键值对的字典:

>>> cache.set_many({'a': 1, 'b': 2, 'c': 3}) 
>>> cache.get_many(['a', 'b', 'c']) 
{'a': 1, 'b': 2, 'c': 3} 

cache.set()类似,set_many()接受一个可选的timeout参数。您可以使用delete()显式删除键。这是清除特定对象的缓存的简单方法:

>>> cache.delete('a') 

如果要一次清除一堆键,delete_many()可以接受要清除的键的列表:

>>> cache.delete_many(['a', 'b', 'c']) 

最后,如果要删除缓存中的所有键,请使用cache.clear()。请注意;clear()将从缓存中删除所有内容,而不仅仅是应用程序设置的键。

>>> cache.clear() 

您还可以使用incr()decr()方法来增加或减少已经存在的键。默认情况下,现有的缓存值将增加或减少 1。可以通过向增量/减量调用提供参数来指定其他增量/减量值。

如果您尝试增加或减少不存在的缓存键,则会引发ValueError

>>> cache.set('num', 1) 
>>> cache.incr('num') 
2 
>>> cache.incr('num', 10) 
12 
>>> cache.decr('num') 
11 
>>> cache.decr('num', 5) 
6 

如果缓存后端实现了close(),则可以使用close()关闭与缓存的连接。

>>> cache.close() 

请注意,对于不实现close方法的缓存,close()是一个空操作。

缓存键前缀

如果您在服务器之间共享缓存实例,或在生产和开发环境之间共享缓存实例,那么一个服务器缓存的数据可能会被另一个服务器使用。如果缓存数据在服务器之间的格式不同,这可能会导致一些非常难以诊断的问题。

为了防止这种情况发生,Django 提供了为服务器中使用的所有缓存键添加前缀的功能。当保存或检索特定缓存键时,Django 将自动使用KEY_PREFIX缓存设置的值作为缓存键的前缀。通过确保每个 Django 实例具有不同的KEY_PREFIX,您可以确保缓存值不会发生冲突。

缓存版本

当您更改使用缓存值的运行代码时,您可能需要清除任何现有的缓存值。这样做的最简单方法是刷新整个缓存,但这可能会导致仍然有效和有用的缓存值的丢失。Django 提供了一种更好的方法来定位单个缓存值。

Django 的缓存框架具有系统范围的版本标识符,使用VERSION缓存设置指定。此设置的值将自动与缓存前缀和用户提供的缓存键结合,以获取最终的缓存键。

默认情况下,任何键请求都将自动包括站点默认的缓存键版本。但是,原始缓存函数都包括一个version参数,因此您可以指定要设置或获取的特定缓存键版本。例如:

# Set version 2 of a cache key 
>>> cache.set('my_key', 'hello world!', version=2) 
# Get the default version (assuming version=1) 
>>> cache.get('my_key') 
None 
# Get version 2 of the same key 
>>> cache.get('my_key', version=2) 
'hello world!' 

特定键的版本可以使用incr_version()decr_version()方法进行增加和减少。这使得特定键可以升级到新版本,而不影响其他键。继续我们之前的例子:

# Increment the version of 'my_key' 
>>> cache.incr_version('my_key') 
# The default version still isn't available 
>>> cache.get('my_key') 
None 
# Version 2 isn't available, either 
>>> cache.get('my_key', version=2) 
None 
# But version 3 *is* available 
>>> cache.get('my_key', version=3) 
'hello world!' 

缓存键转换

如前两节所述,用户提供的缓存键不会直接使用-它与缓存前缀和键版本结合以提供最终的缓存键。默认情况下,这三个部分使用冒号连接以生成最终字符串:

def make_key(key, key_prefix, version): 
    return ':'.join([key_prefix, str(version), key]) 

如果您想以不同的方式组合部分,或对最终键应用其他处理(例如,对键部分进行哈希摘要),可以提供自定义键函数。KEY_FUNCTION缓存设置指定了与上面make_key()原型匹配的函数的点路径。如果提供了此自定义键函数,它将被用于替代默认的键组合函数。

缓存键警告

Memcached,最常用的生产缓存后端,不允许缓存键超过 250 个字符或包含空格或控制字符,使用这样的键将导致异常。为了鼓励可移植的缓存代码并最小化不愉快的惊喜,其他内置缓存后端在使用可能导致在 memcached 上出错的键时会发出警告(django.core.cache.backends.base.CacheKeyWarning)。

如果您正在使用可以接受更广泛键范围的生产后端(自定义后端或非 memcached 内置后端之一),并且希望在没有警告的情况下使用此更广泛范围,您可以在一个INSTALLED_APPSmanagement模块中使用以下代码来消除CacheKeyWarning

import warnings 

from django.core.cache import CacheKeyWarning 

warnings.simplefilter("ignore", CacheKeyWarning) 

如果您想为内置后端之一提供自定义键验证逻辑,可以对其进行子类化,仅覆盖validate_key方法,并按照使用自定义缓存后端的说明进行操作。

例如,要为locmem后端执行此操作,请将此代码放入一个模块中:

from django.core.cache.backends.locmem import LocMemCache 

class CustomLocMemCache(LocMemCache): 
    def validate_key(self, key): 
        # Custom validation, raising exceptions or warnings as needed. 
        # ... 

...并在CACHES设置的BACKEND部分使用此类的点 Python 路径。

下游缓存

到目前为止,本章重点介绍了缓存自己的数据。但是,与 Web 开发相关的另一种缓存也很重要:下游缓存执行的缓存。这些是在请求到达您的网站之前就为用户缓存页面的系统。以下是一些下游缓存的示例:

  • 您的 ISP 可能会缓存某些页面,因此,如果您从http://example.com/请求页面,则您的 ISP 将向您发送页面,而无需直接访问example.comexample.com的维护者对此缓存一无所知;ISP 位于example.com和您的 Web 浏览器之间,透明地处理所有缓存。

  • 您的 Django 网站可能位于代理缓存之后,例如 Squid Web 代理缓存(有关更多信息,请访问www.squid-cache.org/),该缓存可提高页面性能。在这种情况下,每个请求首先将由代理处理,只有在需要时才会传递给您的应用程序。

  • 您的 Web 浏览器也会缓存页面。如果网页发送适当的头,则您的浏览器将对该页面的后续请求使用本地缓存副本,而无需再次联系网页以查看其是否已更改。

下游缓存是一个不错的效率提升,但也存在危险:许多网页的内容基于认证和一系列其他变量而异,盲目保存页面的缓存系统可能向随后访问这些页面的访问者公开不正确或敏感的数据。

例如,假设您运营一个 Web 电子邮件系统,收件箱页面的内容显然取决于哪个用户已登录。如果 ISP 盲目缓存您的站点,那么通过该 ISP 首次登录的用户将使其特定于用户的收件箱页面缓存供站点的后续访问者使用。这不好。

幸运的是,HTTP 提供了解决这个问题的方法。存在许多 HTTP 头,用于指示下游缓存根据指定的变量延迟其缓存内容,并告诉缓存机制不要缓存特定页面。我们将在接下来的部分中查看其中一些头。

使用 vary 头

Vary头定义了缓存机制在构建其缓存键时应考虑哪些请求头。例如,如果网页的内容取决于用户的语言首选项,则称该页面取决于语言。默认情况下,Django 的缓存系统使用请求的完全限定 URL 创建其缓存键,例如http://www.example.com/stories/2005/?order_by=author

这意味着对该 URL 的每个请求都将使用相同的缓存版本,而不考虑用户代理的差异,例如 cookie 或语言首选项。但是,如果此页面根据请求头的某些差异(例如 cookie、语言或用户代理)生成不同的内容,则需要使用Vary头来告诉缓存机制页面输出取决于这些内容。

要在 Django 中执行此操作,请使用方便的django.views.decorators.vary.vary_on_headers()视图装饰器,如下所示:

from django.views.decorators.vary import vary_on_headers 

@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', 'Cookie') 
def my_view(request): 
    # ... 

这告诉下游缓存在两者上变化,这意味着每个用户代理和 cookie 的组合都将获得自己的缓存值。例如,具有用户代理Mozilla和 cookie 值foo=bar的请求将被视为与具有用户代理Mozilla和 cookie 值foo=ham的请求不同。因为在 cookie 上变化是如此常见,所以有一个django.views.decorators.vary.vary_on_cookie()装饰器。这两个视图是等效的。

@vary_on_cookie 
def my_view(request): 
    # ... 

@vary_on_headers('Cookie') 
def my_view(request): 
    # ... 

您传递给vary_on_headers的标头不区分大小写;User-Agentuser-agent是相同的。您还可以直接使用辅助函数django.utils.cache.patch_vary_headers()。此函数设置或添加到Vary标头。例如:

from django.utils.cache import patch_vary_headers 

def my_view(request): 
    # ... 
    response = render_to_response('template_name', context) 
    patch_vary_headers(response, ['Cookie']) 
    return response 

patch_vary_headersHttpResponse实例作为其第一个参数,并将不区分大小写的标头名称列表/元组作为其第二个参数。有关Vary标头的更多信息,请参阅官方 Vary 规范(有关更多信息,请访问www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.44)。

控制缓存:使用其他标头

缓存的其他问题是数据的隐私和数据应该存储在缓存级联中的哪个位置的问题。用户通常面临两种缓存:自己的浏览器缓存(私有缓存)和其提供者的缓存(公共缓存)。

公共缓存由多个用户使用,并由其他人控制。这会带来敏感数据的问题-您不希望您的银行账号存储在公共缓存中。因此,Web 应用程序需要一种告诉缓存哪些数据是私有的,哪些是公共的方法。

解决方案是指示页面的缓存应该是私有的。在 Django 中,使用cache_control视图装饰器。例如:

from django.views.decorators.cache import cache_control 

@cache_control(private=True) 
def my_view(request): 
    # ... 

此装饰器负责在后台发送适当的 HTTP 标头。请注意,缓存控制设置privatepublic是互斥的。装饰器确保如果应该设置private,则删除公共指令(反之亦然)。

两个指令的一个示例用法是提供公共和私有条目的博客网站。公共条目可以在任何共享缓存上缓存。以下代码使用django.utils.cache.patch_cache_control(),手动修改缓存控制标头的方法(它由cache_control装饰器内部调用):

from django.views.decorators.cache import patch_cache_control 
from django.views.decorators.vary import vary_on_cookie 

@vary_on_cookie 
def list_blog_entries_view(request): 
    if request.user.is_anonymous(): 
        response = render_only_public_entries() 
        patch_cache_control(response, public=True) 
    else: 
        response = render_private_and_public_entries(request.user) 
        patch_cache_control(response, private=True) 

    return response 

还有其他控制缓存参数的方法。例如,HTTP 允许应用程序执行以下操作:

  • 定义页面应缓存的最长时间。

  • 指定缓存是否应该始终检查更新版本,仅在没有更改时提供缓存内容。(某些缓存可能会在服务器页面更改时提供缓存内容,仅因为缓存副本尚未过期。)

在 Django 中,使用cache_control视图装饰器来指定这些缓存参数。在此示例中,cache_control告诉缓存在每次访问时重新验证缓存,并将缓存版本存储最多 3600 秒:

from django.views.decorators.cache import cache_control 

@cache_control(must_revalidate=True, max_age=3600) 
def my_view(request): 
    # ... 

cache_control()中的任何有效的Cache-Control HTTP 指令在cache_control()中都是有效的。以下是完整列表:

  • 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-Control HTTP 指令的解释,请参阅 Cache-Control 规范(有关更多信息,请访问www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9)。 (请注意,缓存中间件已经使用CACHE_MIDDLEWARE_SECONDS设置的值设置了缓存标头的max-age。如果您在cache_control装饰器中使用自定义的max_age,装饰器将优先,并且标头值将被正确合并。)

如果要使用标头完全禁用缓存,django.views.decorators.cache.never_cache是一个视图装饰器,它添加标头以确保响应不会被浏览器或其他缓存缓存。例如:

from django.views.decorators.cache import never_cache 

@never_cache 
def myview(request): 
    # ... 

接下来是什么?

在下一章中,我们将看一下 Django 的中间件。

第十七章:Django 中间件

中间件是 Django 请求/响应处理的钩子框架。它是一个轻量级的、低级别的插件系统,用于全局修改 Django 的输入或输出。

每个中间件组件负责执行一些特定的功能。例如,Django 包括一个中间件组件AuthenticationMiddleware,它使用会话将用户与请求关联起来。

本文档解释了中间件的工作原理,如何激活中间件以及如何编写自己的中间件。Django 附带了一些内置的中间件,您可以直接使用。请参见本章后面的可用中间件

激活中间件

要激活中间件组件,请将其添加到 Django 设置中的MIDDLEWARE_CLASSES列表中。

MIDDLEWARE_CLASSES中,每个中间件组件都由一个字符串表示:中间件类名的完整 Python 路径。例如,这是由django-admin startproject创建的默认值:

MIDDLEWARE_CLASSES = [ 
    'django.contrib.sessions.middleware.SessionMiddleware', 
    'django.middleware.common.CommonMiddleware', 
    'django.middleware.csrf.CsrfViewMiddleware', 
    'django.contrib.auth.middleware.AuthenticationMiddleware', 
    'django.contrib.messages.middleware.MessageMiddleware', 
    'django.middleware.clickjacking.XFrameOptionsMiddleware', 
] 

Django 安装不需要任何中间件-如果你愿意的话,MIDDLEWARE_CLASSES可以为空,但强烈建议至少使用CommonMiddleware

MIDDLEWARE_CLASSES中的顺序很重要,因为一个中间件可能依赖于其他中间件。例如,AuthenticationMiddleware将认证用户存储在会话中;因此,它必须在SessionMiddleware之后运行。有关 Django 中间件类的常见提示的中间件排序,请参见本章后面。

钩子和应用顺序

在请求阶段,在调用视图之前,Django 按照在MIDDLEWARE_CLASSES中定义的顺序应用中间件,从上到下。有两个钩子可用:

  • process_request()

  • process_view()

在响应阶段,在调用视图之后,中间件按照从下到上的顺序应用。有三个钩子可用:

  • process_exception()

  • process_template_response()

  • process_response()

如果您愿意,您也可以将其视为洋葱:每个中间件类都是包装视图的一层。

下面描述了每个钩子的行为。

编写自己的中间件

编写自己的中间件很容易。每个中间件组件都是一个单独的 Python 类,定义了以下一个或多个方法:

process_request

方法:process_request(request)

  • request是一个HttpRequest对象。

  • process_request()在 Django 决定执行哪个视图之前,对每个请求都会调用。

它应该返回None或者一个HttpResponse对象。如果返回None,Django 将继续处理此请求,执行任何其他process_request()中间件,然后执行process_view()中间件,最后执行适当的视图。

如果返回一个HttpResponse对象,Django 将不再调用任何其他请求、视图或异常中间件,或者适当的视图;它将对该HttpResponse应用响应中间件,并返回结果。

process_view

方法:process_view(request, view_func, view_args, view_kwargs)

  • request是一个HttpRequest对象。

  • view_func是 Django 即将使用的 Python 函数。(它是实际的函数对象,而不是函数名作为字符串。)

  • view_args是将传递给视图的位置参数列表。

  • view_kwargs是将传递给视图的关键字参数字典。

  • view_argsview_kwargs都不包括第一个视图参数(request)。

process_view()在 Django 调用视图之前调用。它应该返回None或者一个HttpResponse对象。如果返回None,Django 将继续处理此请求,执行任何其他process_view()中间件,然后执行适当的视图。

如果返回一个HttpResponse对象,Django 将不再调用任何其他视图或异常中间件,或者适当的视图;它将对该HttpResponse应用响应中间件,并返回结果。

注意

process_requestprocess_view 中从中间件访问 request.POST 将阻止任何在中间件之后运行的视图能够修改请求的上传处理程序,并且通常应该避免这样做。

CsrfViewMiddleware 类可以被视为一个例外,因为它提供了 csrf_exempt()csrf_protect() 装饰器,允许视图明确控制 CSRF 验证应该在何时发生。

process_template_response

方法:process_template_response(request, response)

  • request 是一个 HttpRequest 对象。

  • response 是由 Django 视图或中间件返回的 TemplateResponse 对象(或等效对象)。

如果响应实例具有 render() 方法,表示它是 TemplateResponse 或等效对象,则会在视图执行完成后立即调用 process_template_response()

它必须返回一个实现 render 方法的响应对象。它可以通过更改 response.template_nameresponse.context_data 来修改给定的 response,也可以创建并返回全新的 TemplateResponse 或等效对象。

您不需要显式渲染响应-一旦调用了所有模板响应中间件,响应将自动渲染。

在响应阶段中,中间件按照相反的顺序运行,其中包括 process_template_response()

process_response

方法:process_response(request, response)

  • request 是一个 HttpRequest 对象。

  • response 是由 Django 视图或中间件返回的 HttpResponseStreamingHttpResponse 对象。

在将响应返回给浏览器之前,将调用 process_response()。它必须返回一个 HttpResponseStreamingHttpResponse 对象。它可以修改给定的 response,也可以创建并返回全新的 HttpResponseStreamingHttpResponse

process_request()process_view() 方法不同,process_response() 方法始终会被调用,即使同一中间件类的 process_request()process_view() 方法被跳过(因为之前的中间件方法返回了一个 HttpResponse)。特别是,这意味着您的 process_response() 方法不能依赖于在 process_request() 中进行的设置。

最后,在响应阶段,中间件按照从下到上的顺序应用。这意味着在 MIDDLEWARE_CLASSES 的末尾定义的类将首先运行。

处理流式响应

HttpResponse 不同,StreamingHttpResponse 没有 content 属性。因此,中间件不能再假定所有响应都有 content 属性。如果它们需要访问内容,它们必须测试流式响应并相应地调整其行为:

if response.streaming: 
    response.streaming_content =  wrap_streaming_content(response.streaming_content) 
else: 
    response.content = alter_content(response.content) 

streaming_content 应被假定为太大而无法在内存中保存。响应中间件可以将其包装在一个新的生成器中,但不得消耗它。包装通常实现如下:

def wrap_streaming_content(content): 
    for chunk in content: 
        yield alter_content(chunk) 

process_exception

方法:process_exception(request, exception)

  • request 是一个 HttpRequest 对象。

  • exception 是由视图函数引发的 Exception 对象。

当视图引发异常时,Django 调用 process_exception()process_exception() 应该返回 None 或一个 HttpResponse 对象。如果它返回一个 HttpResponse 对象,模板响应和响应中间件将被应用,并将生成的响应返回给浏览器。否则,将启用默认的异常处理。

同样,在响应阶段中,中间件按照相反的顺序运行,其中包括 process_exception。如果异常中间件返回一个响应,那么该中间件上面的中间件类将根本不会被调用。

init

大多数中间件类不需要初始化器,因为中间件类本质上是 process_* 方法的占位符。如果您需要一些全局状态,可以使用 __init__ 进行设置。但是,请记住一些注意事项:

  1. Django 在不带任何参数的情况下初始化您的中间件,因此您不能将 __init__ 定义为需要任何参数。

  2. 与每个请求调用一次的 process_* 方法不同,__init__ 仅在 Web 服务器响应第一个请求时调用一次。

将中间件标记为未使用

有时在运行时确定是否应使用某个中间件是有用的。在这些情况下,您的中间件的 __init__ 方法可能会引发 django.core.exceptions.MiddlewareNotUsed。Django 将从中间件流程中删除该中间件,并在 DEBUG 设置为 True 时,将在 django.request 记录器中记录调试消息。

其他指南

  • 中间件类不必是任何东西的子类。

  • 中间件类可以存在于 Python 路径的任何位置。Django 关心的是 MIDDLEWARE_CLASSES 设置包含其路径。

  • 随时查看 Django 提供的中间件示例。

  • 如果您编写了一个您认为对其他人有用的中间件组件,请为社区做出贡献!让我们知道,我们将考虑将其添加到 Django 中。

可用的中间件

缓存中间件

django.middleware.cache.UpdateCacheMiddleware; 和 django.middleware.cache.FetchFromCacheMiddleware

启用站点范围的缓存。如果启用了这些选项,则每个由 Django 提供动力的页面将根据 CACHE_MIDDLEWARE_SECONDS 设置的定义缓存。请参阅缓存文档。

常见中间件

django.middleware.common.CommonMiddleware

为完美主义者添加了一些便利:

  • 禁止访问 DISALLOWED_USER_AGENTS 设置中的用户代理,该设置应该是编译的正则表达式对象的列表。

  • 基于 APPEND_SLASHPREPEND_WWW 设置执行 URL 重写。

  • 如果 APPEND_SLASHTrue,并且初始 URL 不以斜杠结尾,并且在 URLconf 中找不到,则将通过在末尾添加斜杠来形成新的 URL。如果在 URLconf 中找到此新 URL,则 Django 将重定向请求到此新 URL。否则,将像往常一样处理初始 URL。

  • 例如,如果您没有 foo.com/bar 的有效 URL 模式,但是有 foo.com/bar/ 的有效模式,则将重定向到 foo.com/bar/

  • 如果 PREPEND_WWWTrue,则缺少前导 www. 的 URL 将重定向到具有前导 www. 的相同 URL。

  • 这两个选项都旨在规范化 URL。哲学是每个 URL 应该存在于一个且仅一个位置。从技术上讲,URL foo.com/barfoo.com/bar/ 是不同的-搜索引擎索引器将其视为单独的 URL-因此最佳做法是规范化 URL。

  • 根据 USE_ETAGS 设置处理 ETags。如果 USE_ETAGS 设置为 True,Django 将通过对页面内容进行 MD5 哈希来计算每个请求的 ETag,并在适当时负责发送 Not Modified 响应。

  • CommonMiddleware.response_redirect_class. 默认为 HttpResponsePermanentRedirect。子类 CommonMiddleware 并覆盖属性以自定义中间件发出的重定向。

  • django.middleware.common.BrokenLinkEmailsMiddleware. 将损坏的链接通知邮件发送给 MANAGERS.

GZip 中间件

django.middleware.gzip.GZipMiddleware

注意

安全研究人员最近披露,当网站使用压缩技术(包括 GZipMiddleware)时,该网站会暴露于许多可能的攻击。这些方法可以用来破坏 Django 的 CSRF 保护,等等。在您的网站上使用 GZipMiddleware 之前,您应该非常仔细地考虑您是否受到这些攻击的影响。如果您对自己是否受影响有任何疑问,您应该避免使用 GZipMiddleware。有关更多详细信息,请参阅 breachattack.com

为了理解 GZip 压缩的浏览器压缩内容(所有现代浏览器)。

此中间件应放置在需要读取或写入响应正文的任何其他中间件之前,以便在之后进行压缩。

如果以下任何条件为真,则不会压缩内容:

  • 内容主体长度小于 200 字节。

  • 响应已设置了Content-Encoding头。

  • 请求(浏览器)未发送包含gzipAccept-Encoding头。

您可以使用gzip_page()装饰器将 GZip 压缩应用于单个视图。

有条件的 GET 中间件

django.middleware.http.ConditionalGetMiddleware

处理有条件的 GET 操作。如果响应具有ETagLast-Modified头,并且请求具有If-None-MatchIf-Modified-Since,则响应将被HttpResponseNotModified替换。

还设置了DateContent-Length响应头。

区域中间件

django.middleware.locale.LocaleMiddleware

基于请求数据启用语言选择。它为每个用户定制内容。请参阅国际化文档。

LocaleMiddleware.response_redirect_class

默认为HttpResponseRedirect。子类化LocaleMiddleware并覆盖属性以自定义中间件发出的重定向。

消息中间件

django.contrib.messages.middleware.MessageMiddleware

启用基于 cookie 和会话的消息支持。请参阅消息文档。

安全中间件

注意

如果您的部署情况允许,通常最好让您的前端 Web 服务器执行SecurityMiddleware提供的功能。这样,如果有一些不是由 Django 提供服务的请求(如静态媒体或用户上传的文件),它们将具有与请求到您的 Django 应用程序相同的保护。

django.middleware.security.SecurityMiddleware为请求/响应周期提供了几个安全增强功能。SecurityMiddleware通过向浏览器传递特殊头来实现这一点。每个头都可以通过设置独立启用或禁用。

HTTP 严格传输安全

设置:

  • SECURE_HSTS_INCLUDE_SUBDOMAINS

  • SECURE_HSTS_SECONDS

对于应该只能通过 HTTPS 访问的网站,您可以通过设置Strict-Transport-Security头,指示现代浏览器拒绝通过不安全的连接连接到您的域名(在一定时间内)。这减少了您对一些 SSL 剥离中间人(MITM)攻击的风险。

如果将SECURE_HSTS_SECONDS设置为非零整数值,则SecurityMiddleware将在所有 HTTPS 响应上为您设置此头。

启用 HSTS 时,最好首先使用一个小值进行测试,例如,SECURE_HSTS_SECONDS = 3600表示一小时。每次 Web 浏览器从您的站点看到 HSTS 头时,它将拒绝在给定时间内与您的域进行非安全(使用 HTTP)通信。

一旦确认您的站点上的所有资产都安全提供服务(即,HSTS 没有破坏任何内容),最好增加此值,以便偶尔访问者受到保护(31536000 秒,即 1 年,是常见的)。

此外,如果将SECURE_HSTS_INCLUDE_SUBDOMAINS设置为TrueSecurityMiddleware将在Strict-Transport-Security头中添加includeSubDomains标记。这是建议的(假设所有子域都仅使用 HTTPS 提供服务),否则您的站点可能仍然会通过不安全的连接对子域进行攻击。

注意

HSTS 策略适用于整个域,而不仅仅是您设置头的响应的 URL。因此,只有在整个域通过 HTTPS 提供服务时才应该使用它。

正确尊重 HSTS 头的浏览器将拒绝允许用户绕过警告并连接到具有过期、自签名或其他无效 SSL 证书的站点。如果使用 HSTS,请确保您的证书状况良好并保持良好!

X-content-type-options: nosniff

设置:

  • SECURE_CONTENT_TYPE_NOSNIFF

一些浏览器会尝试猜测它们获取的资产的内容类型,覆盖Content-Type头。虽然这可以帮助显示配置不正确的服务器的站点,但也可能带来安全风险。

如果您的网站提供用户上传的文件,恶意用户可能会上传一个特制的文件,当您期望它是无害的时,浏览器会将其解释为 HTML 或 Javascript。

为了防止浏览器猜测内容类型并强制它始终使用Content-Type头中提供的类型,您可以传递X-Content-Type-Options: nosniff头。如果SECURE_CONTENT_TYPE_NOSNIFF设置为TrueSecurityMiddleware将对所有响应执行此操作。

请注意,在大多数部署情况下,Django 不涉及提供用户上传的文件,这个设置对您没有帮助。例如,如果您的MEDIA_URL是由您的前端 Web 服务器(nginx,Apache 等)直接提供的,那么您需要在那里设置这个头部。

另一方面,如果您正在使用 Django 执行诸如要求授权才能下载文件之类的操作,并且无法使用您的 Web 服务器设置头部,那么这个设置将很有用。

X-XSS 保护

设置:

  • SECURE_BROWSER_XSS_FILTER

一些浏览器有能力阻止看起来像 XSS 攻击的内容。它们通过查找页面的 GET 或 POST 参数中的 Javascript 内容来工作。如果服务器的响应中重放了 Javascript,则页面将被阻止渲染,并显示错误页面。

X-XSS-Protection header用于控制 XSS 过滤器的操作。

为了在浏览器中启用 XSS 过滤器,并强制它始终阻止疑似的 XSS 攻击,您可以传递X-XSS-Protection: 1; mode=block头。如果SECURE_BROWSER_XSS_FILTER设置为TrueSecurityMiddleware将对所有响应执行此操作。

注意

浏览器 XSS 过滤器是一种有用的防御措施,但不能完全依赖它。它无法检测所有的 XSS 攻击,也不是所有的浏览器都支持该头部。确保您仍在验证和所有输入,以防止 XSS 攻击。

SSL 重定向

设置:

  • SECURE_REDIRECT_EXEMPT

  • SECURE_SSL_HOST

  • SECURE_SSL_REDIRECT

如果您的网站同时提供 HTTP 和 HTTPS 连接,大多数用户最终将默认使用不安全的连接。为了最佳安全性,您应该将所有 HTTP 连接重定向到 HTTPS。

如果将SECURE_SSL_REDIRECT设置为 True,SecurityMiddleware将永久(HTTP 301)将所有 HTTP 连接重定向到 HTTPS。

出于性能原因,最好在 Django 之外进行这些重定向,在前端负载均衡器或反向代理服务器(如 nginx)中。SECURE_SSL_REDIRECT适用于这种情况下无法选择的部署情况。

如果SECURE_SSL_HOST设置有值,所有重定向将发送到该主机,而不是最初请求的主机。

如果您的网站上有一些页面应该通过 HTTP 可用,并且不重定向到 HTTPS,您可以在SECURE_REDIRECT_EXEMPT设置中列出正则表达式来匹配这些 URL。

如果您部署在负载均衡器或反向代理服务器后,并且 Django 似乎无法确定请求实际上已经安全,您可能需要设置SECURE_PROXY_SSL_HEADER设置。

会话中间件

django.contrib.sessions.middleware.SessionMiddleware

启用会话支持。有关更多信息,请参见第十五章,“Django 会话”。

站点中间件

django.contrib.sites.middleware.CurrentSiteMiddleware

为每个传入的HttpRequest对象添加代表当前站点的site属性。有关更多信息,请参见站点文档(docs.djangoproject.com/en/1.8/ref/contrib/sites/)。

身份验证中间件

django.contrib.auth.middleware提供了三个用于身份验证的中间件:

  • *.AuthenticationMiddleware. 向每个传入的HttpRequest对象添加代表当前登录用户的user属性。

  • *.RemoteUserMiddleware. 用于利用 Web 服务器提供的身份验证。

  • *.SessionAuthenticationMiddleware. 允许在用户密码更改时使用户会话失效。此中间件必须出现在MIDDLEWARE_CLASSES*.AuthenticationMiddleware之后。

有关 Django 中用户身份验证的更多信息,请参见第十一章,“Django 中的用户身份验证”。

CSRF 保护中间件

django.middleware.csrf.CsrfViewMiddleware

通过向 POST 表单添加隐藏的表单字段并检查请求的正确值来防止跨站点请求伪造(CSRF)。有关 CSRF 保护的更多信息,请参见第十九章,“Django 中的安全性”。

X-Frame-options 中间件

django.middleware.clickjacking.XFrameOptionsMiddleware

通过 X-Frame-Options 标头进行简单的点击劫持保护。

中间件排序

表 17.1提供了有关各种 Django 中间件类的排序的一些提示:

注释
UpdateCacheMiddleware 在修改Vary标头的中间件之前(SessionMiddlewareGZipMiddlewareLocaleMiddleware)。
GZipMiddleware 在可能更改或使用响应正文的任何中间件之前。在UpdateCacheMiddleware之后:修改Vary标头。
ConditionalGetMiddleware CommonMiddleware之前:当USE_ETAGS=True时使用其Etag标头。
SessionMiddleware UpdateCacheMiddleware之后:修改Vary标头。
LocaleMiddleware 在顶部之一,之后是SessionMiddleware(使用会话数据)和CacheMiddleware(修改Vary标头)。
CommonMiddleware 在可能更改响应的任何中间件之前(它计算ETags)。在GZipMiddleware之后,因此它不会在经过 gzip 处理的内容上计算ETag标头。靠近顶部:当APPEND_SLASHPREPEND_WWW设置为True时进行重定向。
CsrfViewMiddleware 在假定已处理 CSRF 攻击的任何视图中间件之前。
AuthenticationMiddleware SessionMiddleware之后:使用会话存储。
MessageMiddleware SessionMiddleware之后:可以使用基于会话的存储。
FetchFromCacheMiddleware 在修改Vary标头的任何中间件之后:该标头用于选择缓存哈希键的值。
FlatpageFallbackMiddleware 应该靠近底部,因为它是一种最后一招的中间件。
RedirectFallbackMiddleware 应该靠近底部,因为它是一种最后一招的中间件。

表 17.1:中间件类的排序

接下来是什么?

在下一章中,我们将研究 Django 中的国际化。

第十八章:国际化

当从 JavaScript 源代码创建消息文件时,Django 最初是在美国中部开发的,字面上说,劳伦斯市距离美国大陆的地理中心不到 40 英里。然而,像大多数开源项目一样,Django 的社区逐渐包括来自全球各地的人。随着 Django 社区变得越来越多样化,国际化本地化变得越来越重要。

Django 本身是完全国际化的;所有字符串都标记为可翻译,并且设置控制着像日期和时间这样的与区域相关的值的显示。Django 还附带了 50 多种不同的本地化文件。如果您不是以英语为母语,那么 Django 已经被翻译成您的主要语言的可能性很大。

用于这些本地化的相同国际化框架可供您在自己的代码和模板中使用。

因为许多开发人员对国际化和本地化的实际含义理解模糊,所以我们将从一些定义开始。

定义

国际化

指的是为任何区域的潜在使用设计程序的过程。这个过程通常由软件开发人员完成。国际化包括标记文本(如 UI 元素和错误消息)以供将来翻译,抽象显示日期和时间,以便可以遵守不同的本地标准,提供对不同时区的支持,并确保代码不包含对其用户位置的任何假设。您经常会看到国际化缩写为I18N。(18 指的是 I 和 N 之间省略的字母数)。

本地化

指的是实际将国际化程序翻译为特定区域的过程。这项工作通常由翻译人员完成。有时您会看到本地化缩写为L10N

以下是一些其他术语,将帮助我们处理常见的语言:

区域名称

区域名称,可以是ll形式的语言规范,也可以是ll_CC形式的组合语言和国家规范。例如:itde_ATespt_BR。语言部分始终为小写,国家部分为大写。分隔符是下划线。

语言代码

表示语言的名称。浏览器使用这种格式在Accept-Language HTTP 标头中发送它们接受的语言名称。例如:itde-atespt-br。语言代码通常以小写表示,但 HTTP Accept-Language标头不区分大小写。分隔符是破折号。

消息文件

消息文件是一个纯文本文件,代表单一语言,包含所有可用的翻译字符串以及它们在给定语言中的表示方式。消息文件的文件扩展名为.po

翻译字符串

可翻译的文字。

格式文件

格式文件是定义给定区域的数据格式的 Python 模块。

翻译

为了使 Django 项目可翻译,您必须在 Python 代码和模板中添加最少量的钩子。这些钩子称为翻译字符串。它们告诉 Django:如果该文本在该语言中有翻译,则应将此文本翻译成最终用户的语言。标记可翻译字符串是您的责任;系统只能翻译它知道的字符串。

然后 Django 提供了工具来提取翻译字符串到消息文件中。这个文件是翻译人员以目标语言提供翻译字符串的方便方式。一旦翻译人员填写了消息文件,就必须对其进行编译。这个过程依赖 GNU gettext工具集。

完成后,Django 会根据用户的语言偏好即时翻译 Web 应用程序。

基本上,Django 做了两件事:

  • 它允许开发人员和模板作者指定其应用程序的哪些部分应该是可翻译的。

  • 它使用这些信息根据用户的语言偏好来翻译 Web 应用程序。

Django 的国际化钩子默认打开,这意味着在框架的某些地方有一些与 i18n 相关的开销。如果您不使用国际化,您应该花两秒钟在设置文件中设置USE_I18N = False。然后 Django 将进行一些优化,以便不加载国际化机制,这将节省一些开销。还有一个独立但相关的USE_L10N设置,用于控制 Django 是否应该实现格式本地化。

国际化:在 Python 代码中

标准翻译

使用函数ugettext()指定翻译字符串。按照惯例,将其导入为更短的别名_,以节省输入。

Python 的标准库gettext模块将_()安装到全局命名空间中,作为gettext()的别名。在 Django 中,出于几个原因,我们选择不遵循这种做法:

  • 对于国际字符集(Unicode)支持,ugettext()gettext()更有用。有时,您应该使用ugettext_lazy()作为特定文件的默认翻译方法。在全局命名空间中没有_()时,开发人员必须考虑哪个是最合适的翻译函数。

  • 下划线字符(_)用于表示 Python 交互式 shell 和 doctest 测试中的先前结果。安装全局_()函数会导致干扰。显式导入ugettext()作为_()可以避免这个问题。

在这个例子中,文本“欢迎来到我的网站。”被标记为翻译字符串:

from django.utils.translation import ugettext as _ 
from django.http import HttpResponse 

def my_view(request): 
    output = _("Welcome to my site.") 
    return HttpResponse(output) 

显然,您可以在不使用别名的情况下编写此代码。这个例子与前一个例子相同:

from django.utils.translation import ugettext 
from django.http import HttpResponse 

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 的翻译字符串检测实用程序django-admin makemessages将无法找到这些字符串。稍后再讨论makemessages。)

您传递给_()ugettext()的字符串可以使用 Python 的标准命名字符串插值语法指定占位符。示例:

def my_view(request, m, d): 
    output = _('Today is %(month)s %(day)s.') % {'month': m, 'day': d} 
    return HttpResponse(output) 

这种技术允许特定语言的翻译重新排列占位符文本。例如,英语翻译可能是“今天是 11 月 26 日。”,而西班牙语翻译可能是“Hoy es 26 de Noviembre。”-月份和日期占位符交换了位置。

因此,当您有多个参数时,应使用命名字符串插值(例如%(day)s)而不是位置插值(例如%s%d)。如果使用位置插值,翻译将无法重新排列占位符文本。

翻译者注释

如果您想给翻译者有关可翻译字符串的提示,可以在前一行添加一个以Translators关键字为前缀的注释,例如:

def my_view(request): 
    # Translators: This message appears on the home page only 
    output = ugettext("Welcome to my site.") 

该注释将出现在与其下方的可翻译结构相关联的生成的.po文件中,并且大多数翻译工具也应该显示该注释。

只是为了完整起见,这是生成的.po文件的相应片段:

#. Translators: This message appears on the home page only 
# path/to/python/file.py:123 
msgid "Welcome to my site." 
msgstr "" 

这也适用于模板。有关更多详细信息,请参见模板中的翻译注释。

标记字符串为 No-Op

使用函数django.utils.translation.ugettext_noop()将字符串标记为翻译字符串而不进行翻译。稍后从变量中翻译字符串。

如果您有应存储在源语言中的常量字符串,因为它们在系统或用户之间交换-例如数据库中的字符串-但应在最后可能的时间点进行翻译,例如在向用户呈现字符串时,请使用此功能。

复数形式

使用函数django.utils.translation.ungettext()来指定复数形式的消息。

ungettext需要三个参数:单数翻译字符串、复数翻译字符串和对象的数量。

当您的 Django 应用程序需要本地化到复数形式比英语中使用的两种形式更多的语言时,此功能非常有用('object'表示单数,'objects'表示count与 1 不同的所有情况,而不考虑其值。)

例如:

from django.utils.translation import ungettext 
from django.http import HttpResponse 

def hello_world(request, count): 
    page = ungettext( 
        'there is %(count)d object', 
        'there are %(count)d objects', 
    count) % { 
        'count': count, 
    } 
    return HttpResponse(page) 

在此示例中,对象的数量作为count变量传递给翻译语言。

请注意,复数形式很复杂,并且在每种语言中的工作方式都不同。将count与 1 进行比较并不总是正确的规则。这段代码看起来很复杂,但对于某些语言来说会产生错误的结果:

from django.utils.translation import ungettext 
from myapp.models import Report 

count = Report.objects.count() 
if count == 1: 
    name = Report._meta.verbose_name 
else: 
    name = Report._meta.verbose_name_plural 

text = ungettext( 
    'There is %(count)d %(name)s available.', 
    'There are %(count)d %(name)s available.', 
    count 
    ) % { 
      'count': count, 
      'name': name 
    } 

不要尝试实现自己的单数或复数逻辑,这是不正确的。在这种情况下,考虑以下内容:

text = ungettext( 
    'There is %(count)d %(name)s object available.', 
    'There are %(count)d %(name)s objects available.', 
    count 
    ) % { 
      'count': count, 
      'name': Report._meta.verbose_name, 
    } 

使用ungettext()时,请确保在文字中包含的每个外推变量使用单个名称。在上面的示例中,请注意我们如何在两个翻译字符串中都使用了name Python 变量。这个示例,除了如上所述在某些语言中是不正确的,还会失败:

text = ungettext( 
    'There is %(count)d %(name)s available.', 
    'There are %(count)d %(plural_name)s available.', 
    count 
    ) % { 
      'count': Report.objects.count(), 
      'name': Report._meta.verbose_name, 
      'plural_name': Report._meta.verbose_name_plural 
    } 

运行django-admin compilemessages时会出现错误:

a format specification for argument 'name', as in 'msgstr[0]', doesn't exist in 'msgid' 

上下文标记

有时单词有几个含义,例如英语中的May,它既指月份名称又指动词。为了使翻译人员能够在不同的上下文中正确翻译这些单词,您可以使用django.utils.translation.pgettext()函数,或者如果字符串需要复数形式,则使用django.utils.translation.npgettext()函数。两者都将上下文字符串作为第一个变量。

在生成的.po文件中,该字符串将出现的次数与相同字符串的不同上下文标记一样多(上下文将出现在msgctxt行上),允许翻译人员为每个上下文标记提供不同的翻译。

例如:

from django.utils.translation import pgettext 

month = pgettext("month name", "May") 

或:

from django.db import models 
from django.utils.translation import pgettext_lazy 

class MyThing(models.Model): 
    name = models.CharField(help_text=pgettext_lazy( 
        'help text for MyThing model', 'This is the help text')) 

将出现在.po文件中:

msgctxt "month name" 
msgid "May" 
msgstr "" 

上下文标记也受transblocktrans模板标记的支持。

延迟翻译

django.utils.translation中使用翻译函数的延迟版本(通过它们的名称中的lazy后缀很容易识别)来延迟翻译字符串-当访问值而不是在调用它们时。

这些函数存储字符串的延迟引用-而不是实际的翻译。当字符串在字符串上下文中使用时(例如在模板渲染中),翻译本身将在最后可能的时间点进行。

当这些函数的调用位于模块加载时执行的代码路径中时,这是必不可少的。

这很容易发生在定义模型、表单和模型表单时,因为 Django 实现了这些,使得它们的字段实际上是类级属性。因此,在以下情况下,请确保使用延迟翻译。

模型字段和关系

例如,要翻译以下模型中name字段的帮助文本,请执行以下操作:

from django.db import models 
from django.utils.translation import ugettext_lazy as _ 

class MyThing(models.Model): 
    name = models.CharField(help_text=_('This is the help text')) 

您可以通过使用它们的verbose_name选项将ForeignKeyManyToManyFieldOneToOneField关系的名称标记为可翻译:

class MyThing(models.Model): 
    kind = models.ForeignKey(ThingKind, related_name='kinds',  verbose_name=_('kind')) 

就像您在verbose_name中所做的那样,当需要时,应为关系提供一个小写的详细名称文本,Django 将在需要时自动将其转换为标题大小写。

模型详细名称值

建议始终提供明确的verbose_nameverbose_name_plural选项,而不是依赖于 Django 通过查看模型类名执行的后备英语中心且有些天真的决定详细名称:

from django.db import models 
from django.utils.translation import ugettext_lazy as _ 

class MyThing(models.Model): 
    name = models.CharField(_('name'), help_text=_('This is the help  text')) 

    class Meta: 
        verbose_name = _('my thing') 
        verbose_name_plural = _('my things') 

模型方法的short_description属性值

对于模型方法,你可以使用short_description属性为 Django 和管理站点提供翻译:

from django.db import models 
from django.utils.translation import ugettext_lazy as _ 

class MyThing(models.Model): 
    kind = models.ForeignKey(ThingKind, related_name='kinds', 
                             verbose_name=_('kind')) 

    def is_mouse(self): 
        return self.kind.type == MOUSE_TYPE 
        is_mouse.short_description = _('Is it a mouse?') 

使用延迟翻译对象

ugettext_lazy()调用的结果可以在 Python 中任何需要使用 Unicode 字符串(类型为unicode的对象)的地方使用。如果你试图在需要字节字符串(str对象)的地方使用它,事情将不会按预期工作,因为ugettext_lazy()对象不知道如何将自己转换为字节字符串。你也不能在字节字符串中使用 Unicode 字符串,因此这与正常的 Python 行为一致。例如:

# This is fine: putting a unicode proxy into a unicode string. 
"Hello %s" % ugettext_lazy("people") 

# This will not work, since you cannot insert a unicode object 
# into a bytestring (nor can you insert our unicode proxy there) 
b"Hello %s" % ugettext_lazy("people") 

如果你看到类似"hello <django.utils.functional...>"的输出,你尝试将ugettext_lazy()的结果插入到字节字符串中。这是你代码中的一个错误。

如果你不喜欢长长的ugettext_lazy名称,你可以将其别名为_(下划线),就像这样:

from django.db import models 
from django.utils.translation import ugettext_lazy as _ 

class MyThing(models.Model): 
    name = models.CharField(help_text=_('This is the help text')) 

在模型和实用函数中使用ugettext_lazy()ungettext_lazy()标记字符串是一个常见的操作。当你在代码的其他地方使用这些对象时,你应该确保不要意外地将它们转换为字符串,因为它们应该尽可能晚地转换(以便正确的区域设置生效)。这就需要使用下面描述的辅助函数。

延迟翻译和复数

当使用延迟翻译来处理复数字符串([u]n[p]gettext_lazy)时,通常在字符串定义时不知道number参数。因此,你可以授权将一个键名而不是整数作为number参数传递。然后在字符串插值期间,number将在字典中查找该键下的值。这里有一个例子:

from django import forms 
from django.utils.translation import ugettext_lazy 

class MyForm(forms.Form): 
    error_message = ungettext_lazy("You only provided %(num)d    
      argument", "You only provided %(num)d arguments", 'num') 

    def clean(self): 
        # ... 
        if error: 
            raise forms.ValidationError(self.error_message %  
              {'num': number}) 

如果字符串只包含一个未命名的占位符,你可以直接使用number参数进行插值:

class MyForm(forms.Form): 
    error_message = ungettext_lazy("You provided %d argument", 
        "You provided %d arguments") 

    def clean(self): 
        # ... 
        if error: 
            raise forms.ValidationError(self.error_message % number) 

连接字符串:string_concat()

标准的 Python 字符串连接(''.join([...]))在包含延迟翻译对象的列表上不起作用。相反,你可以使用django.utils.translation.string_concat(),它创建一个延迟对象,只有在结果包含在字符串中时才将其内容连接并转换为字符串。例如:

from django.utils.translation import string_concat 
from django.utils.translation import ugettext_lazy 
# ... 
name = ugettext_lazy('John Lennon') 
instrument = ugettext_lazy('guitar') 
result = string_concat(name, ': ', instrument) 

在这种情况下,result中的延迟翻译只有在result本身在字符串中使用时才会转换为字符串(通常在模板渲染时)。

延迟翻译的其他用途

对于任何其他需要延迟翻译的情况,但必须将可翻译的字符串作为参数传递给另一个函数,你可以自己在延迟调用内部包装这个函数。例如:

from django.utils import six  # Python 3 compatibility 
from django.utils.functional import lazy 
from django.utils.safestring import mark_safe 
from django.utils.translation import ugettext_lazy as _ 

mark_safe_lazy = lazy(mark_safe, six.text_type) 

然后稍后:

lazy_string = mark_safe_lazy(_("<p>My <strong>string!</strong></p>")) 

语言的本地化名称

get_language_info()函数提供了关于语言的详细信息:

>>> from django.utils.translation import get_language_info 
>>> li = get_language_info('de') 
>>> print(li['name'], li['name_local'], li['bidi']) 
German Deutsch False 

字典的namename_local属性包含了语言的英文名称和该语言本身的名称。bidi属性仅对双向语言为 True。

语言信息的来源是django.conf.locale模块。类似的访问这些信息的方式也适用于模板代码。见下文。

国际化:在模板代码中

Django 模板中的翻译使用了两个模板标签和与 Python 代码略有不同的语法。为了让你的模板可以访问这些标签,将

在你的模板顶部使用{% load i18n %}。与所有模板标签一样,这个标签需要在使用翻译的所有模板中加载,即使是那些从已经加载了i18n标签的其他模板继承的模板也是如此。

trans 模板标签

{% trans %}模板标签可以翻译常量字符串(用单引号或双引号括起来)或变量内容:

<title>{% trans "This is the title." %}</title> 
<title>{% trans myvar %}</title> 

如果存在noop选项,变量查找仍然会发生,但翻译会被跳过。这在需要将来进行翻译的内容中是有用的:

<title>{% trans "myvar" noop %}</title> 

在内部,内联翻译使用了ugettext()调用。

如果将模板变量(如上面的 myvar)传递给标签,则标签将首先在运行时将该变量解析为字符串,然后在消息目录中查找该字符串。

不可能在 {% trans %} 内部的字符串中混合模板变量。如果您的翻译需要带有变量(占位符)的字符串,请改用 {% blocktrans %}。如果您想要检索翻译后的字符串而不显示它,可以使用以下语法:

{% trans "This is the title" as the_title %} 

在实践中,您将使用此功能来获取在多个地方使用的字符串,或者应该用作其他模板标签或过滤器的参数:

{% trans "starting point" as start %} 
{% trans "end point" as end %} 
{% trans "La Grande Boucle" as race %} 

<h1> 
  <a href="/" >{{ race }}</a> 
</h1> 
<p> 
{% for stage in tour_stages %} 
    {% cycle start end %}: {{ stage }}{% if forloop.counter|divisibleby:2 %}<br />{% else %}, {% endif %} 
{% endfor %} 
</p> 

{% trans %} 也支持使用 context 关键字进行上下文标记:

{% trans "May" context "month name" %} 

blocktrans 模板标签

blocktrans 标签允许您通过使用占位符标记由文字和变量内容组成的复杂句子进行翻译。

{% blocktrans %}This string will have {{ value }} inside.{% endblocktrans %} 

要翻译模板表达式,比如访问对象属性或使用模板过滤器,您需要将表达式绑定到本地变量,以便在翻译块内使用。例如:

{% blocktrans with amount=article.price %} 
That will cost $ {{ amount }}. 
{% endblocktrans %} 

{% blocktrans with myvar=value|filter %} 
This will have {{ myvar }} inside. 
{% endblocktrans %} 

您可以在单个 blocktrans 标签内使用多个表达式:

{% blocktrans with book_t=book|title author_t=author|title %} 
This is {{ book_t }} by {{ author_t }} 
{% endblocktrans %} 

仍然支持以前更冗长的格式:{% blocktrans with book|title as book_t and author|title as author_t %}

不允许在 blocktrans 标签内部使用其他块标签(例如 {% for %}{% if %})。

如果解析其中一个块参数失败,blocktrans 将通过使用 deactivate_all() 函数临时停用当前活动的语言来回退到默认语言。

此标签还提供了复数形式。使用方法如下:

  • 指定并绑定名为 count 的计数器值。此值将用于选择正确的复数形式。

  • 使用两种形式分隔单数和复数形式

  • {% plural %} 标签在 {% blocktrans %}{% endblocktrans %} 标签内。

一个例子:

{% blocktrans count counter=list|length %} 
There is only one {{ name }} object. 
{% plural %} 
There are {{ counter }} {{ name }} objects. 
{% endblocktrans %} 

一个更复杂的例子:

{% blocktrans with amount=article.price count years=i.length %} 
That will cost $ {{ amount }} per year. 
{% plural %} 
That will cost $ {{ amount }} per {{ years }} years. 
{% endblocktrans %} 

当您同时使用复数形式功能并将值绑定到本地变量以及计数器值时,请记住 blocktrans 结构在内部转换为 ungettext 调用。这意味着与 ungettext 变量相关的相同注释也适用。

不能在 blocktrans 内部进行反向 URL 查找,应该事先检索(和存储):

{% url 'path.to.view' arg arg2 as the_url %} 
{% blocktrans %} 
This is a URL: {{ the_url }} 
{% endblocktrans %} 

{% blocktrans %} 还支持使用 context 关键字进行上下文标记:

{% blocktrans with name=user.username context "greeting" %} 
Hi {{ name }}{% endblocktrans %} 

{% blocktrans %} 支持的另一个功能是 trimmed 选项。此选项将从 {% blocktrans %} 标签的内容开头和结尾删除换行符,替换行开头和结尾的任何空格,并使用空格字符将所有行合并成一行。

这对于缩进 {% blocktrans %} 标签的内容而不使缩进字符出现在 PO 文件中的相应条目中非常有用,这样可以使翻译过程更加简单。

例如,以下 {% blocktrans %} 标签:

{% blocktrans trimmed %} 
  First sentence. 
  Second paragraph. 
{% endblocktrans %} 

如果未指定 trimmed 选项,将在 PO 文件中生成条目 "First sentence. Second paragraph.",而不是 "\n First sentence.\n Second sentence.\n"

传递给标签和过滤器的字符串文字

您可以使用熟悉的 _() 语法将作为参数传递给标签和过滤器的字符串文字进行翻译:

{% some_tag _("Page not found") value|yesno:_("yes,no") %} 

在这种情况下,标签和过滤器都将看到翻译后的字符串,因此它们不需要知道翻译。

在此示例中,翻译基础设施将传递字符串 "yes,no",而不是单独的字符串 "yes" 和 "no"。翻译后的字符串需要包含逗号,以便过滤器解析代码知道如何分割参数。例如,德语翻译者可能将字符串 "yes,no" 翻译为 "ja,nein"(保持逗号不变)。

模板中的翻译者注释

与 Python 代码一样,这些翻译者注释可以使用注释指定,可以使用 comment 标签:

{% comment %}Translators: View verb{% endcomment %} 
{% trans "View" %} 

{% comment %}Translators: Short intro blurb{% endcomment %} 
<p>{% blocktrans %} 
    A multiline translatable literal. 
   {% endblocktrans %} 
</p> 

或者使用 {# ... #} 单行注释结构:

{# Translators: Label of a button that triggers search #} 
<button type="submit">{% trans "Go" %}</button> 

{# Translators: This is a text of the base template #} 
{% blocktrans %}Ambiguous translatable block of text{% endblocktrans %} 

仅供完整性,这些是生成的.po文件的相应片段:

#. Translators: View verb 
# path/to/template/file.html:10 
msgid "View" 
msgstr "" 

#. Translators: Short intro blurb 
# path/to/template/file.html:13 
msgid "" 
"A multiline translatable" 
"literal." 
msgstr "" 

# ... 

#. Translators: Label of a button that triggers search 
# path/to/template/file.html:100 
msgid "Go" 
msgstr "" 

#. Translators: This is a text of the base template 
# path/to/template/file.html:103 
msgid "Ambiguous translatable block of text" 
msgstr "" 

在模板中切换语言

如果要在模板中选择语言,则可以使用language模板标签:

{% load i18n %} 

{% get_current_language as LANGUAGE_CODE %} 
<!-- Current language: {{ LANGUAGE_CODE }} --> 
<p>{% trans "Welcome to our page" %}</p> 

{% language 'en' %} 

    {% get_current_language as LANGUAGE_CODE %} 
    <!-- Current language: {{ LANGUAGE_CODE }} --> 
    <p>{% trans "Welcome to our page" %}</p> 

{% endlanguage %} 

虽然欢迎来到我们的页面的第一次出现使用当前语言,但第二次将始终是英语。

其他标签

这些标签还需要{% load i18n %}

  • {% get_available_languages as LANGUAGES %}返回一个元组列表,其中第一个元素是语言代码,第二个是语言名称(翻译为当前活动的区域设置)。

  • {% get_current_language as LANGUAGE_CODE %}返回当前用户的首选语言,作为字符串。例如:en-us。(请参见本章后面的django 如何发现语言偏好。)

  • {% get_current_language_bidi as LANGUAGE_BIDI %}返回当前区域设置的方向。如果为 True,则是从右到左的语言,例如希伯来语,阿拉伯语。如果为 False,则是从左到右的语言,例如英语,法语,德语等。

如果启用了django.template.context_processors.i18n上下文处理器,则每个RequestContext将可以访问LANGUAGESLANGUAGE_CODELANGUAGE_BIDI,如上所定义。

对于新项目,默认情况下不会为i18n上下文处理器启用。

您还可以使用提供的模板标签和过滤器检索有关任何可用语言的信息。要获取有关单个语言的信息,请使用{% get_language_info %}标签:

{% get_language_info for LANGUAGE_CODE as lang %} 
{% get_language_info for "pl" as lang %} 

然后您可以访问这些信息:

Language code: {{ lang.code }}<br /> 
Name of language: {{ lang.name_local }}<br /> 
Name in English: {{ lang.name }}<br /> 
Bi-directional: {{ lang.bidi }} 

您还可以使用{% get_language_info_list %}模板标签来检索语言列表的信息(例如在LANGUAGES中指定的活动语言)。请参阅关于set_language重定向视图的部分,了解如何使用{% get_language_info_list %}显示语言选择器的示例。

除了LANGUAGES风格的元组列表外,{% get_language_info_list %}还支持简单的语言代码列表。如果在视图中这样做:

context = {'available_languages': ['en', 'es', 'fr']} 
return render(request, 'mytemplate.html', context) 

您可以在模板中迭代这些语言:

{% get_language_info_list for available_languages as langs %} 
{% for lang in langs %} ... {% endfor %} 

还有一些简单的过滤器可供使用:

  • {{ LANGUAGE_CODE|language_name }}(德语)

  • {{ LANGUAGE_CODE|language_name_local }}(德语)

  • {{ LANGUAGE_CODE|language_bidi }} (False)

国际化:在 JavaScript 代码中

向 JavaScript 添加翻译会带来一些问题:

  • JavaScript 代码无法访问gettext实现。

  • JavaScript 代码无法访问.po.mo文件;它们需要由服务器传送。

  • JavaScript 的翻译目录应尽可能保持小。

Django 为这些问题提供了一个集成的解决方案:它将翻译传递到 JavaScript 中,因此您可以在 JavaScript 中调用gettext等。

javascript_catalog 视图

这些问题的主要解决方案是django.views.i18n.javascript_catalog()视图,它发送一个 JavaScript 代码库,其中包含模仿gettext接口的函数,以及一个翻译字符串数组。

这些翻译字符串是根据您在info_dict或 URL 中指定的内容来自应用程序或 Django 核心。LOCALE_PATHS中列出的路径也包括在内。

您可以这样连接它:

from django.views.i18n import javascript_catalog 

js_info_dict = { 
    'packages': ('your.app.package',), 
} 

urlpatterns = [ 
    url(r'^jsi18n/$', javascript_catalog, js_info_dict), 
] 

packages中的每个字符串都应该是 Python 点分包语法(与INSTALLED_APPS中的字符串格式相同),并且应该引用包含locale目录的包。如果指定多个包,所有这些目录都将合并为一个目录。如果您的 JavaScript 使用来自不同应用程序的字符串,则这很有用。

翻译的优先级是这样的,packages参数中后面出现的包比出现在开头的包具有更高的优先级,这在相同文字的冲突翻译的情况下很重要。

默认情况下,视图使用djangojs gettext域。这可以通过修改domain参数来更改。

您可以通过将包放入 URL 模式中使视图动态化:

urlpatterns = [ 
    url(r'^jsi18n/(?P<packages>\S+?)/$', javascript_catalog), 
] 

通过这种方式,您可以将包作为 URL 中由+符号分隔的包名称列表指定。如果您的页面使用来自不同应用的代码,并且这些代码经常更改,您不希望拉入一个大的目录文件,这将特别有用。作为安全措施,这些值只能是django.confINSTALLED_APPS设置中的任何包。

LOCALE_PATHS设置中列出的路径中找到的 JavaScript 翻译也总是包含在内。为了保持与用于 Python 和模板的翻译查找顺序算法的一致性,LOCALE_PATHS中列出的目录具有最高的优先级,先出现的目录比后出现的目录具有更高的优先级。

使用 JavaScript 翻译目录

要使用目录,只需像这样拉入动态生成的脚本:

<script type="text/javascript" src="img/{% url  'django.views.i18n.javascript_catalog' %}"></script> 

这使用了反向 URL 查找来查找 JavaScript 目录视图的 URL。加载目录时,您的 JavaScript 代码可以使用标准的gettext接口来访问它:

document.write(gettext('this is to be translated')); 

还有一个ngettext接口:

var object_cnt = 1 // or 0, or 2, or 3, ... 
s = ngettext('literal for the singular 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. Remaining: %s', 11); 
        s = interpolate(fmts, [11, 20]); 
        // s is 'There are 11 objects. Remaining: 20' 

  • 命名插值:通过将可选的布尔命名参数设置为 true 来选择此模式。obj包含一个 JavaScript 对象或关联数组。例如:
        d = { 
            count: 10, 
            total: 50 
        }; 

        fmts = ngettext('Total: %(total)s, there 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_catalog()视图会在每次请求时从.mo文件生成目录。由于它的输出是恒定的-至少对于站点的特定版本来说-它是一个很好的缓存候选者。

服务器端缓存将减少 CPU 负载。可以使用cache_page()装饰器轻松实现。要在翻译更改时触发缓存失效,请提供一个版本相关的键前缀,如下例所示,或者将视图映射到一个版本相关的 URL。

from django.views.decorators.cache import cache_page 
from django.views.i18n import javascript_catalog 

# The value returned by get_version() must change when translations change. 
@cache_page(86400, key_prefix='js18n-%s' % get_version()) 
def cached_javascript_catalog(request, domain='djangojs', packages=None): 
    return javascript_catalog(request, domain, packages) 

客户端缓存将节省带宽并使您的站点加载更快。如果您使用 ETags(USE_ETAGS = True),则已经覆盖了。否则,您可以应用条件装饰器。在下面的示例中,每当重新启动应用程序服务器时,缓存就会失效。

from django.utils import timezone 
from django.views.decorators.http import last_modified 
from django.views.i18n import javascript_catalog 

last_modified_date = timezone.now() 

@last_modified(lambda req, **kw: last_modified_date) 
def cached_javascript_catalog(request, domain='djangojs', packages=None): 
    return javascript_catalog(request, domain, packages) 

您甚至可以在部署过程的一部分预先生成 JavaScript 目录,并将其作为静态文件提供。django-statici18n.readthedocs.org/en/latest/

国际化:在 URL 模式中

Django 提供了两种国际化 URL 模式的机制:

  • 将语言前缀添加到 URL 模式的根部,以便LocaleMiddleware可以从请求的 URL 中检测要激活的语言。

  • 通过django.utils.translation.ugettext_lazy()函数使 URL 模式本身可翻译。

使用这些功能中的任何一个都需要为每个请求设置一个活动语言;换句话说,您需要在MIDDLEWARE_CLASSES设置中拥有django.middleware.locale.LocaleMiddleware

URL 模式中的语言前缀

这个函数可以在您的根 URLconf 中使用,Django 将自动将当前活动语言代码添加到i18n_patterns()中定义的所有 URL 模式之前。示例 URL 模式:

from django.conf.urls import include, url 
from django.conf.urls.i18n import i18n_patterns 
from about import views as about_views 
from news import views as news_views 
from sitemap.views import sitemap 

urlpatterns = [ 
    url(r'^sitemap\.xml$', sitemap, name='sitemap_xml'), 
] 

news_patterns = [ 
    url(r'^$', news_views.index, name='index'), 
    url(r'^category/(?P<slug>[\w-]+)/$',  
        news_views.category, 
        name='category'), 
    url(r'^(?P<slug>[\w-]+)/$', news_views.details, name='detail'), 
] 

urlpatterns += i18n_patterns( 
    url(r'^about/$', about_views.main, name='about'), 
    url(r'^news/', include(news_patterns, namespace='news')), 
) 

定义这些 URL 模式后,Django 将自动将语言前缀添加到由i18n_patterns函数添加的 URL 模式。例如:

from django.core.urlresolvers import reverse 
from django.utils.translation import activate 

>>> activate('en') 
>>> reverse('sitemap_xml') 
'/sitemap.xml' 
>>> reverse('news:index') 
'/en/news/' 

>>> activate('nl') 
>>> reverse('news:detail', kwargs={'slug': 'news-slug'}) 
'/nl/news/news-slug/' 

i18n_patterns()只允许在根 URLconf 中使用。在包含的 URLconf 中使用它将引发ImproperlyConfigured异常。

翻译 URL 模式

URL 模式也可以使用ugettext_lazy()函数进行标记翻译。例如:

from django.conf.urls import include, url 
from django.conf.urls.i18n import i18n_patterns 
from django.utils.translation import ugettext_lazy as _ 

from about import views as about_views 
from news import views as news_views 
from sitemaps.views import sitemap 

urlpatterns = [ 
    url(r'^sitemap\.xml$', sitemap, name='sitemap_xml'), 
] 

news_patterns = [ 
    url(r'^$', news_views.index, name='index'), 
    url(_(r'^category/(?P<slug>[\w-]+)/$'),  
        news_views.category, 
        name='category'), 
    url(r'^(?P<slug>[\w-]+)/$', news_views.details, name='detail'), 
] 

urlpatterns += i18n_patterns( 
    url(_(r'^about/$'), about_views.main, name='about'), 
    url(_(r'^news/'), include(news_patterns, namespace='news')), 
) 

创建了翻译后,reverse()函数将返回活动语言的 URL。例如:

>>> from django.core.urlresolvers import reverse 
>>> from django.utils.translation import activate 

>>> activate('en') 
>>> reverse('news:category', kwargs={'slug': 'recent'}) 
'/en/news/category/recent/' 

>>> activate('nl') 
>>> reverse('news:category', kwargs={'slug': 'recent'}) 
'/nl/nieuws/categorie/recent/' 

在大多数情况下,最好只在语言代码前缀的模式块中使用翻译后的 URL(使用i18n_patterns()),以避免疏忽翻译的 URL 导致与未翻译的 URL 模式发生冲突的可能性。

在模板中进行反向操作

如果在模板中反转了本地化的 URL,它们将始终使用当前语言。要链接到另一种语言的 URL,请使用language模板标签。它在封闭的模板部分中启用给定的语言:

{% load i18n %} 

{% get_available_languages as languages %} 

{% trans "View this category in:" %} 
{% for lang_code, lang_name in languages %} 
    {% language lang_code %} 
    <a href="{% url 'category' slug=category.slug %}">{{ lang_name }}</a> 
    {% endlanguage %} 
{% endfor %} 

language标签期望语言代码作为唯一参数。

本地化:如何创建语言文件

一旦应用程序的字符串文字被标记为以后进行翻译,翻译本身需要被编写(或获取)。下面是它的工作原理。

消息文件

第一步是为新语言创建一个消息文件。消息文件是一个纯文本文件,代表单一语言,包含所有可用的翻译字符串以及它们在给定语言中的表示方式。消息文件具有.po文件扩展名。

Django 附带了一个工具django-admin makemessages,它可以自动创建和维护这些文件。

makemessages命令(以及稍后讨论的compilemessages)使用 GNU gettext工具集中的命令:xgettextmsgfmtmsgmergemsguniq

支持的gettext实用程序的最低版本为 0.15。

要创建或更新消息文件,请运行此命令:

django-admin makemessages -l de 

...其中de是要创建的消息文件的区域名称。例如,pt_BR表示巴西葡萄牙语,de_AT表示奥地利德语,id表示印尼语。

该脚本应该从以下两个地方之一运行:

  • 您的 Django 项目的根目录(包含manage.py的目录)。

  • 您的 Django 应用程序之一的根目录。

该脚本在项目源树或应用程序源树上运行,并提取所有标记为翻译的字符串(请参阅 how-django-discovers-translations 并确保LOCALE_PATHS已正确配置)。它在目录locale/LANG/LC_MESSAGES中创建(或更新)一个消息文件。在de的示例中,文件将是locale/de/LC_MESSAGES/django.po

当您从项目的根目录运行makemessages时,提取的字符串将自动分发到适当的消息文件中。也就是说,从包含locale目录的应用程序文件中提取的字符串将放在该目录下的消息文件中。从不包含任何locale目录的应用程序文件中提取的字符串将放在LOCALE_PATHS中列出的第一个目录下的消息文件中,如果LOCALE_PATHS为空,则会生成错误。

默认情况下,django-admin makemessages检查具有.html.txt文件扩展名的每个文件。如果要覆盖默认设置,请使用-extension-e选项指定要检查的文件扩展名:

django-admin makemessages -l de -e txt 

用逗号分隔多个扩展名和/或多次使用-e-extension

django-admin makemessages -l de -e html,txt -e xml 

注意

从 JavaScript 源代码创建消息文件时,需要使用特殊的'djangojs'域,而不是e js

如果您没有安装gettext实用程序,makemessages将创建空文件。如果是这种情况,要么安装gettext实用程序,要么只需复制英文消息文件(locale/en/LC_MESSAGES/django.po)(如果有的话)并将其用作起点;它只是一个空的翻译文件。

如果您使用 Windows 并且需要安装 GNU gettext实用程序以便makemessages正常工作,请参阅本章稍后的在 Windows 上使用 gettext以获取更多信息。

.po文件的格式很简单。每个.po文件包含一小部分元数据,例如翻译维护者的联系信息,但文件的大部分是消息的列表-翻译字符串和特定语言的实际翻译文本之间的简单映射。

例如,如果您的 Django 应用程序包含了文本"欢迎来到我的网站。"的翻译字符串,如下所示:

_("Welcome to my site.") 

然后django-admin makemessages将创建一个包含以下片段消息的.po文件:

#: path/to/python/module.py:23 
msgid "Welcome to my site." 
msgstr "" 

一个简单的解释:

  • msgid是出现在源中的翻译字符串。不要更改它。

  • msgstr是您放置特定于语言的翻译的地方。它起初是空的,所以您有责任更改它。确保您在翻译周围保留引号。

  • 为了方便起见,每条消息都包括一个以#为前缀的注释行,位于msgid行上方,其中包含了翻译字符串所在的文件名和行号。

长消息是一个特殊情况。在那里,msgstr(或msgid)之后的第一个字符串是一个空字符串。然后内容本身将作为下面几行的一个字符串写入。这些字符串直接连接在一起。不要忘记字符串内的尾随空格;否则,它们将被连接在一起而没有空格!

由于gettext工具的内部工作方式,以及我们希望允许 Django 核心和您的应用程序中的非 ASCII 源字符串,您必须将 UTF-8 用作 PO 文件的编码(创建 PO 文件时的默认值)。这意味着每个人都将使用相同的编码,在 Django 处理 PO 文件时这一点很重要。

要重新检查所有源代码和模板以获取新的翻译字符串,并为所有语言更新所有消息文件,请运行以下命令:

django-admin makemessages -a 

编译消息文件

创建消息文件后,每次对其进行更改时,您都需要将其编译为gettext可以使用的更高效的形式。使用django-admin compilemessages实用程序进行此操作。

此工具将遍历所有可用的.po文件,并创建.mo文件,这些文件是为gettext使用而优化的二进制文件。在您运行django-admin makemessages的同一目录中运行:

django-admin compilemessages 

就是这样。您的翻译已经准备好了。

如果您使用 Windows 并且需要安装 GNU gettext实用程序以使django-admin compilemessages正常工作,请参阅下面有关 Windows 上的gettext的更多信息。

Django 仅支持以 UTF-8 编码且没有任何 BOM(字节顺序标记)的.po文件,因此如果您的文本编辑器默认在文件开头添加这些标记,那么您需要重新配置它。

从 JavaScript 源代码创建消息文件

您可以像其他 Django 消息文件一样使用django-admin makemessages工具创建和更新消息文件。唯一的区别是,您需要显式指定在这种情况下称为djangojs域的gettext术语中的域,通过提供一个-d djangojs参数,就像这样:

django-admin makemessages -d djangojs -l de 

这将为德语创建或更新 JavaScript 的消息文件。更新消息文件后,只需像处理普通 Django 消息文件一样运行django-admin compilemessages

Windows 上的 gettext

这仅适用于那些想要提取消息 ID 或编译消息文件(.po)的人。翻译工作本身只涉及编辑这种类型的现有文件,但如果您想创建自己的消息文件,或者想测试或编译已更改的消息文件,您将需要gettext实用程序:

X是版本号;需要版本0.15或更高版本。

  • 将这两个文件夹中bin\目录的内容提取到系统上的同一个文件夹中(即C:\Program Files\gettext-utils

  • 更新系统 PATH:

  • 控制面板 > 系统 > 高级 > 环境变量

  • 系统变量列表中,点击Path,点击Edit

  • Variable value字段的末尾添加;C:\Program Files\gettext-utils\bin

您也可以使用其他地方获取的gettext二进制文件,只要xgettext -version命令正常工作。如果在 Windows 命令提示符中输入xgettext -version命令会弹出一个窗口说 xgettext.exe 已经生成错误并将被 Windows 关闭,请不要尝试使用 Django 翻译工具与gettext包。

自定义 makemessages 命令

如果您想向xgettext传递额外的参数,您需要创建一个自定义的makemessages命令并覆盖其xgettext_options属性:

from django.core.management.commands import makemessages 

class Command(makemessages.Command): 
    xgettext_options = makemessages.Command.xgettext_options +  
      ['-keyword=mytrans'] 

如果您需要更灵活性,您还可以向自定义的makemessages命令添加一个新参数:

from django.core.management.commands import makemessages 

class Command(makemessages.Command): 

    def add_arguments(self, parser): 
        super(Command, self).add_arguments(parser) 
        parser.add_argument('-extra-keyword', 
                            dest='xgettext_keywords',  
                            action='append') 

    def handle(self, *args, **options): 
        xgettext_keywords = options.pop('xgettext_keywords') 
        if xgettext_keywords: 
            self.xgettext_options = ( 
                makemessages.Command.xgettext_options[:] + 
                ['-keyword=%s' % kwd for kwd in xgettext_keywords] 
            ) 
        super(Command, self).handle(*args, **options) 

显式设置活动语言

您可能希望明确为当前会话设置活动语言。也许用户的语言偏好是从另一个系统中检索的。例如,您已经介绍了django.utils.translation.activate()。这仅适用于当前线程。要使语言在整个会话中持续存在,还要修改会话中的LANGUAGE_SESSION_KEY

from django.utils import translation 
user_language = 'fr' 
translation.activate(user_language) 
request.session[translation.LANGUAGE_SESSION_KEY] = user_language 

通常您希望同时使用:django.utils.translation.activate()将更改此线程的语言,并修改会话使此偏好在将来的请求中持续存在。

如果您不使用会话,语言将保留在一个 cookie 中,其名称在LANGUAGE_COOKIE_NAME中配置。例如:

from django.utils import translation 
from django import http 
from django.conf import settings 
user_language = 'fr' 
translation.activate(user_language) 
response = http.HttpResponse(...) 
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, user_language) 

在视图和模板之外使用翻译

虽然 Django 提供了丰富的国际化工具供视图和模板使用,但它并不限制使用于 Django 特定的代码。Django 的翻译机制可以用于将任意文本翻译成 Django 支持的任何语言(当然,前提是存在适当的翻译目录)。

您可以加载一个翻译目录,激活它并将文本翻译成您选择的语言,但请记住切换回原始语言,因为激活翻译目录是基于每个线程的,这样的更改将影响在同一线程中运行的代码。

例如:

from django.utils import translation 
def welcome_translated(language): 
    cur_language = translation.get_language() 
    try: 
        translation.activate(language) 
        text = translation.ugettext('welcome') 
    finally: 
        translation.activate(cur_language) 
    return text 

使用值'de'调用此函数将给您"Willkommen",而不管LANGUAGE_CODE和中间件设置的语言如何。

特别感兴趣的功能是django.utils.translation.get_language(),它返回当前线程中使用的语言,django.utils.translation.activate(),它激活当前线程的翻译目录,以及django.utils.translation.check_for_language(),它检查给定的语言是否受 Django 支持。

实现说明

Django 翻译的特点

Django 的翻译机制使用了 Python 自带的标准gettext模块。如果您了解gettext,您可能会注意到 Django 在翻译方面的一些特点:

  • 字符串域是djangodjangojs。这个字符串域用于区分存储其数据在一个共同的消息文件库中的不同程序(通常是/usr/share/locale/)。django域用于 Python 和模板翻译字符串,并加载到全局翻译目录中。djangojs域仅用于 JavaScript 翻译目录,以确保其尽可能小。

  • Django 不仅仅使用xgettext。它使用围绕xgettextmsgfmt的 Python 包装器。这主要是为了方便。

Django 如何发现语言偏好

一旦您准备好您的翻译,或者如果您只想使用 Django 提供的翻译,您需要为您的应用程序激活翻译。

在幕后,Django 有一个非常灵活的模型来决定应该使用哪种语言-全局安装、特定用户或两者。

要设置全局安装的语言偏好,请设置LANGUAGE_CODE。Django 将使用此语言作为默认翻译-如果通过区域设置中间件采用的方法找不到更好的匹配翻译,则作为最后一次尝试。

如果您只想使用本地语言运行 Django,您只需要设置LANGUAGE_CODE并确保相应的消息文件及其编译版本(.mo)存在。

如果要让每个用户指定他们喜欢的语言,那么您还需要使用LocaleMiddlewareLocaleMiddleware基于请求中的数据启用语言选择。它为每个用户定制内容。

要使用LocaleMiddleware,请将'django.middleware.locale.LocaleMiddleware'添加到您的MIDDLEWARE_CLASSES设置中。因为中间件顺序很重要,所以您应该遵循以下准则:

  • 确保它是最先安装的中间件之一。

  • 它应该放在SessionMiddleware之后,因为LocaleMiddleware使用会话数据。它应该放在CommonMiddleware之前,因为CommonMiddleware需要激活的语言来解析请求的 URL。

  • 如果使用CacheMiddleware,请在其后放置LocaleMiddleware

例如,您的MIDDLEWARE_CLASSES可能如下所示:

MIDDLEWARE_CLASSES = [ 
   'django.contrib.sessions.middleware.SessionMiddleware', 
   'django.middleware.locale.LocaleMiddleware', 
   'django.middleware.common.CommonMiddleware', 
] 

有关中间件的更多信息,请参见第十七章,Django 中间件

LocaleMiddleware尝试通过以下算法确定用户的语言偏好:

  • 首先,它会在请求的 URL 中查找语言前缀。只有在您的根 URLconf 中使用i18n_patterns函数时才会执行此操作。有关语言前缀以及如何国际化 URL 模式的更多信息,请参见国际化

  • 如果失败,它会查找当前用户会话中的LANGUAGE_SESSION_KEY键。

  • 如果失败,它会查找一个 cookie。使用的 cookie 的名称由LANGUAGE_COOKIE_NAME设置。 (默认名称是django_language。)

  • 如果失败,它会查看Accept-Language HTTP 标头。此标头由您的浏览器发送,并告诉服务器您首选的语言(按优先级顺序)。Django 尝试标头中的每种语言,直到找到具有可用翻译的语言。

  • ***** 如果失败,它会使用全局LANGUAGE_CODE设置。

注意:

  • 在这些地方中,语言偏好应该是标准语言格式的字符串。例如,巴西葡萄牙语是pt-br

  • 如果基本语言可用但未指定子语言,则 Django 将使用基本语言。例如,如果用户指定de-at(奥地利德语),但 Django 只有de可用,Django 将使用de

  • 只有在LANGUAGES设置中列出的语言才能被选择。如果要将语言选择限制为提供的语言的子集(因为您的应用程序没有提供所有这些语言),请将LANGUAGES设置为语言列表。例如:


        LANGUAGES = [ 
          ('de', _('German')), 
          ('en', _('English')), 
        ] 

此示例将可用于自动选择的语言限制为德语和英语(以及任何子语言,如de-chen-us)。

  • 如果您定义了自定义的LANGUAGES设置,如前面的项目所述,您可以将语言名称标记为翻译字符串-但使用ugettext_lazy()而不是ugettext()以避免循环导入。

这里有一个示例设置文件:

from django.utils.translation import ugettext_lazy as _ 

LANGUAGES = [ 
    ('de', _('German')), 
    ('en', _('English')), 
] 

一旦LocaleMiddleware确定了用户的偏好,它会将这个偏好作为request.LANGUAGE_CODE对每个HttpRequest可用。请随意在您的视图代码中读取这个值。这里有一个简单的例子:

from django.http import HttpResponse 

def hello_world(request, count): 
    if request.LANGUAGE_CODE == 'de-at': 
        return HttpResponse("You prefer to read Austrian German.") 
    else: 
        return HttpResponse("You prefer to read another language.") 

请注意,对于静态(无中间件)翻译,语言在settings.LANGUAGE_CODE中,而对于动态(中间件)翻译,它在request.LANGUAGE_CODE中。

Django 如何发现翻译

在运行时,Django 会构建一个内存中的统一的文字翻译目录。为了实现这一点,它会按照一定的顺序查找不同文件路径来加载编译好的消息文件(.mo),并确定同一文字的多个翻译的优先级。

  • LOCALE_PATHS中列出的目录具有最高的优先级,出现在前面的优先级高于后面的。

  • 然后,它会查找并使用(如果存在)每个已安装应用程序中的INSTALLED_APPS列表中的locale目录。出现在前面的优先级高于后面的。

  • 最后,Django 提供的基础翻译在django/conf/locale中被用作后备。

在所有情况下,包含翻译的目录的名称应该使用语言环境的命名规范。例如,dept_BRes_AR等。

通过这种方式,您可以编写包含自己翻译的应用程序,并且可以覆盖项目中的基础翻译。或者,您可以构建一个由多个应用程序组成的大型项目,并将所有翻译放入一个特定于您正在组合的项目的大型共同消息文件中。选择权在您手中。

所有消息文件存储库的结构都是相同的。它们是:

  • 在您的设置文件中列出的LOCALE_PATHS中搜索<language>/LC_MESSAGES/django.(po|mo)

  • $APPPATH/locale/<language>/LC_MESSAGES/django.(po|mo)

  • $PYTHONPATH/django/conf/locale/<language>/LC_MESSAGES/django.(po|mo).

要创建消息文件,您可以使用django-admin makemessages工具。您可以使用django-admin compilemessages来生成二进制的.mo文件,这些文件将被gettext使用。

您还可以运行django-admin compilemessages来使编译器处理LOCALE_PATHS设置中的所有目录。

接下来是什么?

在下一章中,我们将讨论 Django 中的安全性。

第十九章:Django 中的安全性

确保您构建的网站是安全的对于专业的 Web 应用程序开发人员至关重要。

Django 框架现在非常成熟,大多数常见的安全问题都以某种方式得到了解决,但是没有安全措施是 100%保证的,而且新的威胁不断出现,因此作为 Web 开发人员,您需要确保您的网站和应用程序是安全的。

Web 安全是一个庞大的主题,无法在一本书的章节中深入讨论。本章概述了 Django 的安全功能,并提供了有关保护 Django 网站的建议,这将在 99%的时间内保护您的网站,但您需要随时了解 Web 安全的变化。

有关 Web 安全的更详细信息,请参阅 Django 的安全问题存档(有关更多信息,请访问docs.djangoproject.com/en/1.8/releases/security/),以及维基百科的 Web 应用程序安全页面(en.wikipedia.org/wiki/web_application_security)。

Django 内置的安全功能

跨站点脚本攻击(XSS)保护

跨站点脚本XSS)攻击允许用户向其他用户的浏览器注入客户端脚本。

这通常是通过将恶意脚本存储在数据库中,然后检索并显示给其他用户,或者让用户点击一个链接,从而导致攻击者的 JavaScript 在用户的浏览器中执行。但是,XSS 攻击可能源自任何不受信任的数据源,例如 cookie 或 Web 服务,只要在包含在页面中之前未经充分净化。

使用 Django 模板可以保护您免受大多数 XSS 攻击。但是,重要的是要了解它提供的保护措施及其局限性。

Django 模板会转义对 HTML 特别危险的特定字符。虽然这可以保护用户免受大多数恶意输入,但并非绝对安全。例如,它无法保护以下内容:

<style class={{ var }}>...</style> 

如果var设置为'class1 onmouseover=javascript:func()',这可能导致未经授权的 JavaScript 执行,具体取决于浏览器如何呈现不完美的 HTML。(引用属性值将修复此情况)。

在使用自定义模板标记时,使用is_safesafe模板标记、mark_safe以及关闭autoescape时要特别小心。

此外,如果您使用模板系统输出除 HTML 之外的内容,可能需要转义完全不同的字符和单词。

在存储 HTML 在数据库时,特别需要非常小心,特别是当检索和显示该 HTML 时。

跨站点请求伪造(CSRF)保护

跨站点请求伪造CSRF)攻击允许恶意用户在不知情或未经同意的情况下使用另一个用户的凭据执行操作。

Django 内置了对大多数 CSRF 攻击的保护,只要您已启用并在适当的地方使用它。但是,与任何缓解技术一样,存在局限性。

例如,可以全局禁用 CSRF 模块或特定视图。只有在知道自己在做什么时才应该这样做。如果您的网站具有超出您控制范围的子域,还存在其他限制。

CSRF 保护通过检查每个POST请求中的一次性令牌来实现。这确保了恶意用户无法简单地重放表单POST到您的网站,并使另一个已登录的用户无意中提交该表单。恶意用户必须知道一次性令牌,这是用户特定的(使用 cookie)。

在使用 HTTPS 部署时,CsrfViewMiddleware将检查 HTTP 引用头是否设置为同一来源的 URL(包括子域和端口)。因为 HTTPS 提供了额外的安全性,所以必须确保连接在可用时使用 HTTPS,通过转发不安全的连接请求并为受支持的浏览器使用 HSTS。

非常小心地标记视图为csrf_exempt装饰器,除非绝对必要。

Django 的 CSRF 中间件和模板标签提供了易于使用的跨站请求伪造保护。

对抗 CSRF 攻击的第一道防线是确保GET请求(以及其他“安全”方法,如 9.1.1 安全方法,HTTP 1.1,RFC 2616 中定义的方法(有关更多信息,请访问tools.ietf.org/html/rfc2616.html#section-9.1.1)是无副作用的。然后,通过以下步骤保护通过“不安全”方法(如POSTPUTDELETE)的请求。

如何使用它

要在视图中利用 CSRF 保护,请按照以下步骤进行操作:

  1. CSRF 中间件在MIDDLEWARE_CLASSES设置中默认激活。如果您覆盖该设置,请记住'django.middleware.csrf.CsrfViewMiddleware'应该在任何假设已处理 CSRF 攻击的视图中间件之前。

  2. 如果您禁用了它,这是不推荐的,您可以在要保护的特定视图上使用csrf_protect()(见下文)。

  3. 在任何使用POST表单的模板中,如果表单用于内部 URL,请在<form>元素内使用csrf_token标签,例如:

        <form action="." method="post">{% csrf_token %} 

  1. 不应该对目标外部 URL 的POST表单执行此操作,因为这会导致 CSRF 令牌泄漏,从而导致漏洞。

  2. 在相应的视图函数中,确保使用了'django.template.context_processors.csrf'上下文处理器。通常,可以通过以下两种方式之一完成:

  3. 使用RequestContext,它始终使用'django.template.context_processors.csrf'(无论在TEMPLATES设置中配置了哪些模板上下文处理器)。如果您使用通用视图或贡献应用程序,则已经涵盖了,因为这些应用程序始终在整个RequestContext中使用。

  4. 手动导入并使用处理器生成 CSRF 令牌,并将其添加到模板上下文中。例如:

        from django.shortcuts import render_to_response 
        from django.template.context_processors import csrf 

        def my_view(request): 
            c = {} 
            c.update(csrf(request)) 
            # ... view code here 
            return render_to_response("a_template.html", c) 

  1. 您可能希望编写自己的render_to_response()包装器,以便为您处理此步骤。

AJAX

虽然上述方法可以用于 AJAX POST 请求,但它有一些不便之处:您必须记住在每个 POST 请求中将 CSRF 令牌作为 POST 数据传递。因此,有一种替代方法:在每个XMLHttpRequest上,将自定义的X-CSRFToken标头设置为 CSRF 令牌的值。这通常更容易,因为许多 JavaScript 框架提供了允许在每个请求上设置标头的钩子。

首先,您必须获取 CSRF 令牌本身。令牌的推荐来源是csrftoken cookie,如果您已经按上述方式为视图启用了 CSRF 保护,它将被设置。

CSRF 令牌 cookie 默认名为csrftoken,但您可以通过CSRF_COOKIE_NAME设置控制 cookie 名称。

获取令牌很简单:

// using jQuery 
function getCookie(name) { 
    var cookieValue = null; 
    if (document.cookie && document.cookie != '') { 
        var cookies = document.cookie.split(';'); 
        for (var i = 0; i < cookies.length; i++) { 
            var cookie = jQuery.trim(cookies[i]); 
            // Does this cookie string begin with the name we want? 
            if (cookie.substring(0, name.length + 1) == (name + '=')) { 
                cookieValue =  decodeURIComponent(cookie.substring(name.length + 1)); 
                break; 
            } 
        } 
    } 
    return cookieValue; 
} 
var csrftoken = getCookie('csrftoken'); 

通过使用 jQuery cookie 插件(plugins.jquery.com/cookie/)来替换getCookie,可以简化上述代码:

var csrftoken = $.cookie('csrftoken'); 

注意

CSRF 令牌也存在于 DOM 中,但仅当在模板中明确包含csrf_token时才会存在。cookie 包含规范令牌;CsrfViewMiddleware将优先使用 cookie 而不是 DOM 中的令牌。无论如何,如果 DOM 中存在令牌,则保证会有 cookie,因此应该使用 cookie!

注意

如果您的视图没有呈现包含csrf_token模板标签的模板,则 Django 可能不会设置 CSRF 令牌 cookie。这在动态添加表单到页面的情况下很常见。为了解决这种情况,Django 提供了一个视图装饰器,强制设置 cookie:ensure_csrf_cookie()

最后,您将需要在 AJAX 请求中实际设置标头,同时使用 jQuery 1.5.1 及更高版本中的settings.crossDomain保护 CSRF 令牌,以防止发送到其他域:

function csrfSafeMethod(method) { 
    // these HTTP methods do not require CSRF protection 
    return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); 
} 
$.ajaxSetup({ 
    beforeSend: function(xhr, settings) { 
        if (!csrfSafeMethod(settings.type) && !this.crossDomain) { 
            xhr.setRequestHeader("X-CSRFToken", csrftoken); 
        } 
    } 
}); 

其他模板引擎

当使用与 Django 内置引擎不同的模板引擎时,您可以在确保它在模板上下文中可用后,在表单中手动设置令牌。

例如,在 Jinja2 模板语言中,您的表单可以包含以下内容:

<div style="display:none"> 
    <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}"> 
</div> 

您可以使用类似于上面的 AJAX 代码的 JavaScript 来获取 CSRF 令牌的值。

装饰器方法

您可以使用csrf_protect装饰器,而不是将CsrfViewMiddleware作为一种全面的保护措施,该装饰器具有完全相同的功能,用于需要保护的特定视图。它必须同时用于在输出中插入 CSRF 令牌的视图和接受POST表单数据的视图。(这些通常是相同的视图函数,但并非总是如此)。

不建议单独使用装饰器,因为如果您忘记使用它,将会有安全漏洞。同时使用两者的“双重保险”策略是可以的,并且会产生最小的开销。

django.views.decorators.csrf.csrf_protect(view)

提供对视图的CsrfViewMiddleware保护的装饰器。

用法:

from django.views.decorators.csrf import csrf_protect 
from django.shortcuts import render 

@csrf_protect 
def my_view(request): 
    c = {} 
    # ... 
    return render(request, "a_template.html", c) 

如果您正在使用基于类的视图,可以参考装饰基于类的视图。

被拒绝的请求

默认情况下,如果传入请求未通过CsrfViewMiddleware执行的检查,则向用户发送403 Forbidden响应。通常只有在存在真正的跨站请求伪造或由于编程错误,CSRF 令牌未包含在POST表单中时才会看到这种情况。

然而,错误页面并不是很友好,因此您可能希望为处理此条件提供自己的视图。要做到这一点,只需设置CSRF_FAILURE_VIEW设置。

工作原理

CSRF 保护基于以下几点:

  • 设置为随机值的 CSRF cookie(称为会话独立 nonce),其他站点将无法访问。

  • 这个 cookie 是由CsrfViewMiddleware设置的。它是永久性的,但由于没有办法设置永不过期的 cookie,因此它会随着每次调用django.middleware.csrf.get_token()(内部用于检索 CSRF 令牌的函数)的响应一起发送。

  • 所有传出的 POST 表单中都有一个名为csrfmiddlewaretoken的隐藏表单字段。该字段的值是 CSRF cookie 的值。

  • 这部分是由模板标签完成的。

  • 对于所有不使用 HTTP GETHEADOPTIONSTRACE的传入请求,必须存在 CSRF cookie,并且必须存在并正确的csrfmiddlewaretoken字段。如果没有,用户将收到 403 错误。

  • 这个检查是由CsrfViewMiddleware完成的。

  • 此外,对于 HTTPS 请求,CsrfViewMiddleware会进行严格的引用检查。这是必要的,以解决在 HTTPS 下使用会话独立 nonce 时可能发生的中间人攻击,因为(不幸的是)客户端接受了对 HTTPS 站点进行通信的“Set-Cookie”标头。 (在 HTTP 请求下不进行引用检查,因为在 HTTP 下,引用标头的存在不够可靠。)

这确保只有来自您网站的表单才能用于将数据POST回来。

它故意忽略GET请求(以及 RFC 2616 定义为“安全”的其他请求)。这些请求不应该具有任何潜在的危险副作用,因此使用GET请求的 CSRF 攻击应该是无害的。RFC 2616 将POSTPUTDELETE定义为“不安全”,并假定所有其他方法都是不安全的,以获得最大的保护。

缓存

如果模板使用csrf_token模板标签(或以其他方式调用get_token函数),CsrfViewMiddleware将向响应添加一个 cookie 和一个Vary: Cookie标头。这意味着如果按照指示使用缓存中间件(UpdateCacheMiddleware在所有其他中间件之前),中间件将与缓存中间件协同工作。

然而,如果您在单个视图上使用缓存装饰器,CSRF 中间件还没有能够设置Vary标头或 CSRF cookie,响应将被缓存而没有任何一个。

在这种情况下,对于任何需要插入 CSRF 令牌的视图,您应该首先使用django.views.decorators.csrf.csrf_protect()装饰器:

from django.views.decorators.cache import cache_page 
from django.views.decorators.csrf import csrf_protect 

@cache_page(60 * 15) 
@csrf_protect 
def my_view(request): 
    ... 

如果您正在使用基于类的视图,可以参考 Django 文档中的装饰基于类的视图(docs.djangoproject.com/en/1.8/topics/class-based-views/intro/#decorating-class-based-views)。

测试

由于需要在每个POST请求中发送 CSRF 令牌,CsrfViewMiddleware通常会对测试视图函数造成很大的阻碍。因此,Django 的测试 HTTP 客户端已经修改,以在请求上设置一个标志,从而放宽中间件和csrf_protect装饰器,使其不再拒绝请求。在其他方面(例如发送 cookie 等),它们的行为是相同的。

如果出于某种原因,您希望测试客户端执行 CSRF 检查,您可以创建一个强制执行 CSRF 检查的测试客户端实例:

>>> from django.test import Client 
>>> csrf_client = Client(enforce_csrf_checks=True) 

限制

站点内的子域将能够在整个域上为客户端设置 cookie。通过设置 cookie 并使用相应的令牌,子域将能够规避 CSRF 保护。避免这种情况的唯一方法是确保子域由受信任的用户控制(或者至少无法设置 cookie)。

请注意,即使没有 CSRF,也存在其他漏洞,例如会话固定,这使得将子域分配给不受信任的方可能不是一个好主意,而且这些漏洞在当前浏览器中不能轻易修复。

边缘情况

某些视图可能具有不符合此处正常模式的特殊要求。在这些情况下,一些实用程序可能会有用。它们可能需要的场景在下一节中描述。

实用程序

下面的示例假定您正在使用基于函数的视图。如果您正在使用基于类的视图,可以参考 Django 文档中的装饰基于类的视图。

django.views.decorators.csrf.csrf_exempt(view)

大多数视图需要 CSRF 保护,但有一些不需要。与其禁用中间件并将csrf_protect应用于所有需要它的视图,不如启用中间件并使用csrf_exempt()

这个装饰器标记一个视图被中间件保护豁免。示例:

from django.views.decorators.csrf import csrf_exempt 
from django.http import HttpResponse 

@csrf_exempt 
def my_view(request): 
    return HttpResponse('Hello world') 

django.views.decorators.csrf.requires_csrf_token(view)

有些情况下,CsrfViewMiddleware.process_view可能在您的视图运行之前没有运行-例如 404 和 500 处理程序-但您仍然需要表单中的 CSRF 令牌。

通常,如果CsrfViewMiddleware.process_view或类似csrf_protect没有运行,csrf_token模板标签将无法工作。视图装饰器requires_csrf_token可用于确保模板标签正常工作。这个装饰器的工作方式类似于csrf_protect,但从不拒绝传入的请求。

示例:

from django.views.decorators.csrf import requires_csrf_token 
from django.shortcuts import render 

@requires_csrf_token 
def my_view(request): 
    c = {} 
    # ... 
    return render(request, "a_template.html", c) 

还可能有一些未受保护的视图已经被csrf_exempt豁免,但仍需要包含 CSRF 令牌。在这些情况下,使用csrf_exempt()后跟requires_csrf_token()。(即requires_csrf_token应该是最内层的装饰器)。

最后一个例子是,当视图仅在一组条件下需要 CSRF 保护,并且在其余时间不得具有保护时。解决方案是对整个视图函数使用csrf_exempt(),并对其中需要保护的路径使用csrf_protect()

例如:

from django.views.decorators.csrf import csrf_exempt, csrf_protect 

@csrf_exempt 
def my_view(request): 

    @csrf_protect 
    def protected_path(request): 
        do_something() 

    if some_condition(): 
       return protected_path(request) 
    else: 
       do_something_else() 

django.views.decorators.csrf.ensure_csrf_cookie(view)

这个装饰器强制视图发送 CSRF cookie。如果页面通过 AJAX 进行 POST 请求,并且页面没有带有csrf_token的 HTML 表单,这将导致所需的 CSRF cookie 被发送。解决方案是在发送页面的视图上使用ensure_csrf_cookie()

贡献和可重用应用程序

由于开发人员可以关闭CsrfViewMiddleware,因此贡献应用程序中的所有相关视图都使用csrf_protect装饰器来确保这些应用程序对 CSRF 的安全性。建议其他希望获得相同保障的可重用应用程序的开发人员也在其视图上使用csrf_protect装饰器。

CSRF 设置

可以用一些设置来控制 Django 的 CSRF 行为:

  • CSRF_COOKIE_AGE

  • CSRF_COOKIE_DOMAIN

  • CSRF_COOKIE_HTTPONLY

  • CSRF_COOKIE_NAME

  • CSRF_COOKIE_PATH

  • CSRF_COOKIE_SECURE

  • CSRF_FAILURE_VIEW

有关这些设置的更多信息,请参见附录 D,设置

SOL 注入保护

SQL 注入是一种攻击类型,恶意用户能够在数据库上执行任意的 SQL 代码。这可能导致记录被删除或数据泄露。

通过使用 Django 的查询集,生成的 SQL 将由底层数据库驱动程序正确转义。但是,Django 还赋予开发人员编写原始查询或执行自定义 SQL 的权力。这些功能应该谨慎使用,并且您应该始终小心地正确转义用户可以控制的任何参数。此外,在使用extra()时应谨慎。

点击劫持保护

点击劫持是一种攻击类型,恶意站点在框架中包裹另一个站点。当恶意站点欺骗用户点击他们在隐藏框架或 iframe 中加载的另一个站点的隐藏元素时,就会发生这种类型的攻击。

Django 包含防止点击劫持的保护,即X-Frame-Options 中间件,在支持的浏览器中可以防止网站在框架内呈现。可以在每个视图的基础上禁用保护,或配置发送的确切标头值。

强烈建议对于任何不需要其页面被第三方站点包裹在框架中的站点,或者只需要允许站点的一小部分进行包裹的站点使用中间件。

点击劫持的一个例子

假设一个在线商店有一个页面,用户可以在其中点击“立即购买”来购买商品。用户选择一直保持登录以方便使用。攻击者站点可能在其自己的页面上创建一个“我喜欢小马”按钮,并以透明的iframe加载商店的页面,使得“立即购买”按钮被隐形地覆盖在“我喜欢小马”按钮上。如果用户访问攻击者的站点,点击“我喜欢小马”将导致无意中点击“立即购买”按钮,并无意中购买商品。

防止点击劫持

现代浏览器遵守 X-Frame-Options(有关更多信息,请访问 developer.mozilla.org/en/The_X-FRAME-OPTIONS_response_header)HTTP 头部,该头部指示资源是否允许在框架或 iframe 中加载。如果响应包含带有 SAMEORIGIN 值的头部,则浏览器只会在请求源自同一站点时才在框架中加载资源。如果头部设置为 DENY,则浏览器将阻止资源在框架中加载,无论哪个站点发出了请求。

Django 提供了一些简单的方法来在您的站点的响应中包含这个头部:

  • 一个简单的中间件,可以在所有响应中设置头部。

  • 一组视图装饰器,可用于覆盖中间件或仅为特定视图设置头部。

如何使用它

为所有响应设置 X-Frame-Options

要为站点中的所有响应设置相同的 X-Frame-Options 值,请将 'django.middleware.clickjacking.XFrameOptionsMiddleware' 放到 MIDDLEWARE_CLASSES 中:

MIDDLEWARE_CLASSES = [ 
    # ... 
    'django.middleware.clickjacking.XFrameOptionsMiddleware', 
    # ... 
] 

此中间件在由 startproject 生成的设置文件中启用。

默认情况下,中间件将为每个传出的 HttpResponse 设置 X-Frame-Options 头部为 SAMEORIGIN。如果要改为 DENY,请设置 X_FRAME_OPTIONS 设置:

X_FRAME_OPTIONS = 'DENY' 

在使用中间件时,可能存在一些视图,您不希望设置 X-Frame-Options 头部。对于这些情况,您可以使用视图装饰器告诉中间件不要设置头部:

from django.http import HttpResponse 
from django.views.decorators.clickjacking import xframe_options_exempt 

@xframe_options_exempt 
def ok_to_load_in_a_frame(request): 
    return HttpResponse("This page is safe to load in a frame on any site.") 

为每个视图设置 X-Frame-Options

要在每个视图基础上设置 X-Frame-Options 头部,Django 提供了这些装饰器:

from django.http import HttpResponse 
from django.views.decorators.clickjacking import xframe_options_deny 
from django.views.decorators.clickjacking import  xframe_options_sameorigin 

@xframe_options_deny 
def view_one(request): 
    return HttpResponse("I won't display in any frame!") 

@xframe_options_sameorigin 
def view_two(request): 
    return HttpResponse("Display in a frame if it's from the same    
      origin as me.") 

请注意,您可以将装饰器与中间件一起使用。使用装饰器会覆盖中间件。

限制

X-Frame-Options 头部只会在现代浏览器中保护免受点击劫持攻击。旧版浏览器会悄悄地忽略这个头部,并需要其他点击劫持防护技术。

支持 X-Frame-Options 的浏览器

  • Internet Explorer 8+

  • Firefox 3.6.9+

  • Opera 10.5+

  • Safari 4+

  • Chrome 4.1+

SSL/HTTPS

尽管在所有情况下部署站点在 HTTPS 后面对于安全性来说总是更好的,但并非在所有情况下都是实际可行的。如果没有这样做,恶意网络用户可能会窃取身份验证凭据或客户端和服务器之间传输的任何其他信息,并且在某些情况下,主动的网络攻击者可能会更改在任一方向上发送的数据。

如果您希望获得 HTTPS 提供的保护,并已在服务器上启用了它,则可能需要一些额外的步骤:

  • 如有必要,请设置 SECURE_PROXY_SSL_HEADER,确保您已充分理解其中的警告。不这样做可能会导致 CSRF 漏洞,并且不正确地执行也可能很危险!

  • 设置重定向,以便通过 HTTP 的请求被重定向到 HTTPS。

  • 这可以通过使用自定义中间件来实现。请注意 SECURE_PROXY_SSL_HEADER 下的注意事项。对于反向代理的情况,配置主要的 Web 服务器来执行重定向到 HTTPS 可能更容易或更安全。

  • 使用 secure cookies。如果浏览器最初通过 HTTP 连接,这是大多数浏览器的默认设置,现有的 cookies 可能会泄漏。因此,您应该将 SESSION_COOKIE_SECURECSRF_COOKIE_SECURE 设置为 True。这指示浏览器仅在 HTTPS 连接上发送这些 cookies。请注意,这意味着会话将无法在 HTTP 上工作,并且 CSRF 保护将阻止任何通过 HTTP 接受的 POST 数据(如果您将所有 HTTP 流量重定向到 HTTPS,则这将是可以接受的)。

  • 使用 HTTP 严格传输安全(HSTS)。HSTS 是一个 HTTP 标头,通知浏览器所有未来连接到特定站点应始终使用 HTTPS(见下文)。结合将请求重定向到 HTTPS,这将确保连接始终享有 SSL 提供的额外安全性,只要成功连接一次。HSTS 通常在 Web 服务器上配置。

HTTP 严格传输安全

对于应仅通过 HTTPS 访问的站点,您可以指示现代浏览器拒绝通过不安全连接(在一定时间内)连接到您的域名,方法是设置 Strict-Transport-Security 标头。这将减少您对某些 SSL 剥离中间人(MITM)攻击的风险。

如果将SECURE_HSTS_SECONDS设置为非零整数值,SecurityMiddleware将在所有 HTTPS 响应上为您设置此标头。

在启用 HSTS 时,最好首先使用一个小值进行测试,例如SECURE_HSTS_SECONDS = 3600表示一小时。每次 Web 浏览器从您的站点看到 HSTS 标头时,它将拒绝在给定时间内与您的域进行非安全通信(使用 HTTP)。

一旦确认您的站点上的所有资产都安全提供(即 HSTS 没有破坏任何内容),最好增加此值,以便偶尔访问者受到保护(31536000 秒,即 1 年,是常见的)。

此外,如果将SECURE_HSTS_INCLUDE_SUBDOMAINS设置为TrueSecurityMiddleware将向Strict-Transport-Security标头添加includeSubDomains标记。这是推荐的(假设所有子域都仅使用 HTTPS 提供服务),否则您的站点仍可能通过不安全的连接对子域进行攻击。

注意

HSTS 策略适用于整个域,而不仅仅是您在响应上设置标头的 URL。因此,只有在整个域仅通过 HTTPS 提供服务时才应使用它。

浏览器正确尊重 HSTS 标头将拒绝允许用户绕过警告并连接到具有过期、自签名或其他无效 SSL 证书的站点。如果您使用 HSTS,请确保您的证书状况良好并保持良好!

如果您部署在负载均衡器或反向代理服务器后,并且未将Strict-Transport-Security标头添加到您的响应中,可能是因为 Django 没有意识到它处于安全连接中;您可能需要设置SECURE_PROXY_SSL_HEADER设置。

主机标头验证

Django 使用客户端提供的Host标头在某些情况下构建 URL。虽然这些值经过清理以防止跨站点脚本攻击,但可以使用虚假的Host值进行跨站点请求伪造、缓存污染攻击和电子邮件中的链接污染。因为即使看似安全的 Web 服务器配置也容易受到虚假的Host标头的影响,Django 会在django.http.HttpRequest.get_host()方法中针对ALLOWED_HOSTS设置验证Host标头。此验证仅适用于get_host();如果您的代码直接从request.META访问Host标头,则会绕过此安全保护。

会话安全

与 CSRF 限制类似,要求站点部署在不受信任用户无法访问任何子域的情况下,django.contrib.sessions也有限制。有关详细信息,请参阅安全主题指南部分的会话主题。

用户上传的内容

注意

考虑从云服务或 CDN 提供静态文件以避免其中一些问题。

  • 如果您的站点接受文件上传,强烈建议您在 Web 服务器配置中限制这些上传的大小,以防止拒绝服务(DOS)攻击。在 Apache 中,可以使用LimitRequestBody指令轻松设置这一点。

  • 如果您正在提供自己的静态文件,请确保像 Apache 的mod_php这样的处理程序已被禁用,因为它会将静态文件作为代码执行。您不希望用户能够通过上传和请求特制文件来执行任意代码。

  • 当媒体以不遵循安全最佳实践的方式提供时,Django 的媒体上传处理会存在一些漏洞。具体来说,如果 HTML 文件包含有效的 PNG 标头,后跟恶意 HTML,则可以将 HTML 文件上传为图像。这个文件将通过 Django 用于ImageField图像处理的库(Pillow)的验证。当此文件随后显示给用户时,根据您的 Web 服务器的类型和配置,它可能会显示为 HTML。

在框架级别没有防弹的技术解决方案可以安全地验证所有用户上传的文件内容,但是,您可以采取一些其他步骤来减轻这些攻击:

  1. 一类攻击可以通过始终从不同的顶级或二级域名提供用户上传的内容来防止。这可以防止任何被同源策略(有关更多信息,请访问en.wikipedia.org/wiki/Same-origin_policy)阻止的利用,例如跨站脚本。例如,如果您的站点运行在example.com上,您希望从类似usercontent-example.com的地方提供上传的内容(MEDIA_URL设置)。仅仅从子域名(如usercontent.example.com)提供内容是不够的。

  2. 此外,应用程序可以选择为用户上传的文件定义一个允许的文件扩展名白名单,并配置 Web 服务器仅提供这些文件。

其他安全提示

  • 尽管 Django 在开箱即用时提供了良好的安全保护,但仍然很重要正确部署应用程序并利用 Web 服务器、操作系统和其他组件的安全保护。

  • 确保您的 Python 代码位于 Web 服务器的根目录之外。这将确保您的 Python 代码不会被意外地作为纯文本(或意外执行)提供。

  • 小心处理任何用户上传的文件。

  • Django 不会限制对用户进行身份验证的请求。为了防止针对身份验证系统的暴力攻击,您可以考虑部署 Django 插件或 Web 服务器模块来限制这些请求。

  • 保持您的SECRET_KEY是秘密的。

  • 限制缓存系统和数据库的可访问性是一个好主意。

安全问题档案

Django 的开发团队坚决致力于负责任地报告和披露安全相关问题,如 Django 的安全政策所述。作为承诺的一部分,他们维护了一个已修复和披露的问题的历史列表。有关最新列表,请参阅安全问题档案(docs.djangoproject.com/en/1.8/releases/security/)。

加密签名

Web 应用程序安全的黄金法则是永远不要相信来自不受信任来源的数据。有时通过不受信任的媒介传递数据可能是有用的。通过加密签名的值可以通过不受信任的渠道传递,以确保任何篡改都将被检测到。Django 提供了用于签名值的低级 API 和用于设置和读取签名 cookie 的高级 API,签名在 Web 应用程序中是最常见的用途之一。您可能还会发现签名对以下内容有用:

  • 为失去密码的用户生成找回我的账户URL。

  • 确保存储在隐藏表单字段中的数据没有被篡改。

  • 为允许临时访问受保护资源(例如,用户已支付的可下载文件)生成一次性秘密 URL。

保护 SECRET_KEY

当您使用startproject创建一个新的 Django 项目时,settings.py文件会自动生成并获得一个随机的SECRET_KEY值。这个值是保护签名数据的关键-您必须保持它安全,否则攻击者可能会使用它来生成自己的签名值。

使用低级 API

Django 的签名方法位于django.core.signing模块中。要签名一个值,首先实例化一个Signer实例:

>>> from django.core.signing import Signer
>>> signer = Signer()
>>> value = signer.sign('My string')
>>> value
'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'

签名附加到字符串的末尾,跟在冒号后面。您可以使用unsign方法检索原始值:

>>> original = signer.unsign(value)
>>> original
'My string'

如果签名或值以任何方式被更改,将引发django.core.signing.BadSignature异常:

>>> from django.core import signing
>>> value += 'm'
>>> try:
   ... original = signer.unsign(value)
   ... except signing.BadSignature:
   ... print("Tampering detected!")

默认情况下,Signer类使用SECRET_KEY设置生成签名。您可以通过将其传递给Signer构造函数来使用不同的密钥:

>>> signer = Signer('my-other-secret')
>>> value = signer.sign('My string')
>>> value
'My string:EkfQJafvGyiofrdGnuthdxImIJw'

django.core.signing.Signer返回一个签名者,该签名者使用key生成签名,sep用于分隔值。sep不能在 URL 安全的 base64 字母表中。这个字母表包含字母数字字符、连字符和下划线。

使用盐参数

如果您不希望特定字符串的每次出现都具有相同的签名哈希,可以使用Signer类的可选salt参数。使用盐将使用盐和您的SECRET_KEY对签名哈希函数进行种子处理:

>>> signer = Signer()
>>> signer.sign('My string')
'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
>>> signer = Signer(salt='extra')
>>> signer.sign('My string')
'My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw'
>>> signer.unsign('My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw')
'My string'

以这种方式使用盐将不同的签名放入不同的命名空间。来自一个命名空间(特定盐值)的签名不能用于验证使用不同盐设置的不同命名空间中的相同纯文本字符串。结果是防止攻击者使用在代码中的一个地方生成的签名字符串作为输入到另一段使用不同盐生成(和验证)签名的代码。

与您的SECRET_KEY不同,您的盐参数不需要保密。

验证时间戳值

TimestampSignerSigner的子类,它附加了一个签名的时间戳到值。这允许您确认签名值是在指定的时间段内创建的:

>>> from datetime import timedelta
>>> from django.core.signing import TimestampSigner
>>> signer = TimestampSigner()
>>> value = signer.sign('hello')
>>> value 'hello:1NMg5H:oPVuCqlJWmChm1rA2lyTUtelC-c'
>>> signer.unsign(value)
'hello'
>>> signer.unsign(value, max_age=10)
...
SignatureExpired: Signature age 15.5289158821 > 10 seconds
>>> signer.unsign(value, max_age=20)
'hello'
>>> signer.unsign(value, max_age=timedelta(seconds=20))
'hello'

sign(value)签名value并附加当前时间戳。

unsign(value, max_age=None)检查value是否在max_age秒之内签名,否则会引发SignatureExpiredmax_age参数可以接受整数或datetime.timedelta对象。

保护复杂的数据结构

如果您希望保护列表、元组或字典,可以使用签名模块的dumpsloads函数。这些函数模仿了 Python 的 pickle 模块,但在底层使用 JSON 序列化。JSON 确保即使您的SECRET_KEY被盗,攻击者也无法利用 pickle 格式执行任意命令:

>>> from django.core import signing
>>> value = signing.dumps({"foo": "bar"})
>>> value 'eyJmb28iOiJiYXIifQ:1NMg1b:zGcDE4-TCkaeGzLeW9UQwZesciI'
>>> signing.loads(value) {'foo': 'bar'}

由于 JSON 的性质(没有本地区分列表和元组的区别),如果传入元组,您将从signing.loads(object)得到一个列表:

>>> from django.core import signing
>>> value = signing.dumps(('a','b','c'))
>>> signing.loads(value)
['a', 'b', 'c']

django.core.signing.dumps(obj, key=None, salt='django.core.signing', compress=False)

返回 URL 安全的,经过 sha1 签名的 base64 压缩的 JSON 字符串。序列化对象使用TimestampSigner进行签名。

django.core.signing.loads(string, key=None, salt='django.core.signing', max_age=None)

dumps()的反向操作,如果签名失败则引发BadSignature。如果给定,检查max_age(以秒为单位)。

安全中间件

注意

如果您的部署情况允许,通常最好让前端 Web 服务器执行SecurityMiddleware提供的功能。这样,如果有一些请求不是由 Django 提供的(例如静态媒体或用户上传的文件),它们将具有与请求到您的 Django 应用程序相同的保护。

django.middleware.security.SecurityMiddleware为请求/响应周期提供了几个安全增强功能。每个功能都可以通过设置独立启用或禁用。

  • SECURE_BROWSER_XSS_FILTER

  • SECURE_CONTENT_TYPE_NOSNIFF

  • SECURE_HSTS_INCLUDE_SUBDOMAINS

  • SECURE_HSTS_SECONDS

  • SECURE_REDIRECT_EXEMPT

  • SECURE_SSL_HOST

  • SECURE_SSL_REDIRECT

有关安全标头和这些设置的更多信息,请参阅第十七章Django 中间件

接下来是什么?

在下一章中,我们将扩展来自第一章的快速安装指南,Django 简介和入门,并查看 Django 的一些额外安装和配置选项。

第二十章:更多关于安装 Django 的信息

本章涵盖了与安装和维护 Django 相关的一些常见附加选项和场景。首先,我们将看看除了 SQLite 之外使用其他数据库的安装配置,然后我们将介绍如何升级 Django 以及如何手动安装 Django。最后,我们将介绍如何安装 Django 的开发版本,以防您想要尝试 Django 开发的最前沿。

运行其他数据库

如果您计划使用 Django 的数据库 API 功能,则需要确保数据库服务器正在运行。Django 支持许多不同的数据库服务器,并且官方支持 PostgreSQL、MySQL、Oracle 和 SQLite。

第二十一章,高级数据库管理,包含了连接 Django 到这些数据库的额外信息,但是,本书的范围不包括向您展示如何安装它们;请参考每个项目网站上的数据库文档。

如果您正在开发一个简单的项目或者您不打算在生产环境中部署,SQLite 通常是最简单的选择,因为它不需要运行单独的服务器。但是,SQLite 与其他数据库有许多不同之处,因此,如果您正在开发一些实质性的东西,建议使用与生产环境中计划使用的相同数据库进行开发。

除了数据库后端,您还需要确保安装了 Python 数据库绑定。

  • 如果您使用 PostgreSQL,则需要postgresql_psycopg2initd.org/psycopg/)包。您可能需要参考 PostgreSQL 的注意事项,以获取有关此数据库的进一步技术细节。如果您使用 Windows,请查看非官方编译的 Windows 版本(stickpeople.com/projects/python/win-psycopg/)。

  • 如果您正在使用 MySQL,则需要MySQL-python包,版本为 1.2.1p2 或更高版本。您还需要阅读 MySQL 后端的特定于数据库的注意事项。

  • 如果您使用 SQLite,您可能需要阅读 SQLite 后端的注意事项。

  • 如果您使用 Oracle,则需要cx_Oracle的副本(cx-oracle.sourceforge.net/),但请阅读有关 Oracle 后端的特定于数据库的注意事项,以获取有关 Oracle 和cx_Oracle支持版本的重要信息。

  • 如果您使用非官方的第三方后端,请查阅所提供的文档以获取任何额外要求。

如果您计划使用 Django 的manage.py migrate命令自动为模型创建数据库表(在安装 Django 并创建项目后),您需要确保 Django 有权限在您使用的数据库中创建和更改表;如果您计划手动创建表,您可以简单地授予 DjangoSELECTINSERTUPDATEDELETE权限。在创建具有这些权限的数据库用户后,您将在项目的设置文件中指定详细信息,请参阅DATABASES以获取详细信息。

如果您使用 Django 的测试框架来测试数据库查询,Django 将需要权限来创建测试数据库。

手动安装 Django

  1. 从 Django 项目下载页面下载最新版本的发布版(www.djangoproject.com/download/)。

  2. 解压下载的文件(例如,tar xzvf Django-X.Y.tar.gz,其中X.Y是最新发布版的版本号)。如果您使用 Windows,您可以下载命令行工具bsdtar来执行此操作,或者您可以使用基于 GUI 的工具,如 7-zip(www.7-zip.org/)。

  3. 切换到步骤 2 中创建的目录(例如,cd Django-X.Y)。

  4. 如果您使用 Linux、Mac OS X 或其他 Unix 变种,请在 shell 提示符下输入sudo python setup.py install命令。如果您使用 Windows,请以管理员权限启动命令 shell,并运行python setup.py install命令。这将在 Python 安装的site-packages目录中安装 Django。

注意

删除旧版本

如果您使用此安装技术,特别重要的是首先删除任何现有的 Django 安装(请参见下文)。否则,您可能会得到一个包含自 Django 已删除的以前版本的文件的损坏安装。

升级 Django

删除任何旧版本的 Django

如果您正在从以前的版本升级 Django 安装,您需要在安装新版本之前卸载旧的 Django 版本。

如果以前使用pipeasy_install安装了 Django,则再次使用pipeasy_install安装将自动处理旧版本,因此您无需自己操作。

如果您以前手动安装了 Django,卸载就像删除 Python site-packages中的django目录一样简单。要找到需要删除的目录,您可以在 shell 提示符(而不是交互式 Python 提示符)下运行以下命令:

python -c "import sys; sys.path = sys.path[1:]; import django; print(django.__path__)"

安装特定于发行版的软件包

检查特定于发行版的说明,看看您的平台/发行版是否提供官方的 Django 软件包/安装程序。发行版提供的软件包通常允许自动安装依赖项和简单的升级路径;但是,这些软件包很少包含 Django 的最新版本。

安装开发版本

如果您决定使用 Django 的最新开发版本,您需要密切关注开发时间表,并且需要关注即将发布的版本的发布说明。这将帮助您了解您可能想要使用的任何新功能,以及在更新 Django 副本时需要进行的任何更改。(对于稳定版本,任何必要的更改都在发布说明中记录。)

如果您希望偶尔能够使用最新的错误修复和改进更新 Django 代码,请按照以下说明操作:

  1. 确保已安装 Git,并且可以从 shell 运行其命令。(在 shell 提示符处输入git help来测试这一点。)

  2. 像这样查看 Django 的主要开发分支(trunkmaster):

 git clone 
      git://github.com/django/django.git django-trunk

  1. 这将在当前目录中创建一个名为django-trunk的目录。

  2. 确保 Python 解释器可以加载 Django 的代码。最方便的方法是通过 pip。运行以下命令:

 sudo pip install -e django-trunk/

  1. (如果使用virtualenv,或者运行 Windows,可以省略sudo。)

这将使 Django 的代码可导入,并且还将使django-admin实用程序命令可用。换句话说,您已经准备好了!

注意

不要运行sudo python setup.py install,因为您已经在第 3 步中执行了相应的操作。

当您想要更新 Django 源代码的副本时,只需在django-trunk目录中运行git pull命令。这样做时,Git 将自动下载任何更改。

接下来是什么?

在下一章中,我们将介绍有关在特定数据库上运行 Django 的附加信息

第二十一章:高级数据库管理

本章提供了有关 Django 中支持的每个关系数据库的额外信息,以及连接到传统数据库的注意事项和技巧。

一般注意事项

Django 尝试在所有数据库后端上支持尽可能多的功能。然而,并非所有的数据库后端都是一样的,Django 开发人员必须对支持哪些功能和可以安全假设的内容做出设计决策。

本文件描述了一些可能与 Django 使用相关的特性。当然,它并不打算替代特定服务器的文档或参考手册。

持久连接

持久连接避免了在每个请求中重新建立与数据库的连接的开销。它们由CONN_MAX_AGE参数控制,该参数定义了连接的最大生存期。它可以独立设置每个数据库。默认值为 0,保留了在每个请求结束时关闭数据库连接的历史行为。要启用持久连接,请将CONN_MAX_AGE设置为正数秒数。要获得无限的持久连接,请将其设置为None

连接管理

Django 在首次进行数据库查询时会打开与数据库的连接。它会保持这个连接打开,并在后续请求中重用它。一旦连接超过CONN_MAX_AGE定义的最大寿命,或者不再可用,Django 会关闭连接。

具体来说,Django 在需要连接数据库时会自动打开一个连接,如果没有已经存在的连接,要么是因为这是第一个连接,要么是因为上一个连接已经关闭。

在每个请求开始时,如果连接已经达到最大寿命,Django 会关闭连接。如果您的数据库在一段时间后终止空闲连接,您应该将CONN_MAX_AGE设置为较低的值,这样 Django 就不会尝试使用已被数据库服务器终止的连接。(这个问题可能只影响非常低流量的站点。)

在每个请求结束时,如果连接已经达到最大寿命或处于不可恢复的错误状态,Django 会关闭连接。如果在处理请求时发生了任何数据库错误,Django 会检查连接是否仍然有效,如果无效则关闭连接。因此,数据库错误最多影响一个请求;如果连接变得无法使用,下一个请求将获得一个新的连接。

注意事项

由于每个线程都维护自己的连接,因此您的数据库必须支持至少与您的工作线程一样多的同时连接。

有时,数据库不会被大多数视图访问,例如,因为它是外部系统的数据库,或者由于缓存。在这种情况下,您应该将CONN_MAX_AGE设置为较低的值,甚至为0,因为维护一个不太可能被重用的连接是没有意义的。这将有助于保持对该数据库的同时连接数较小。

开发服务器为每个处理的请求创建一个新的线程,从而抵消了持久连接的效果。在开发过程中不要启用它们。

当 Django 建立与数据库的连接时,它会根据所使用的后端设置适当的参数。如果启用了持久连接,这个设置就不会在每个请求中重复。如果您修改了连接的隔离级别或时区等参数,您应该在每个请求结束时恢复 Django 的默认设置,或者在每个请求开始时强制设置适当的值,或者禁用持久连接。

编码

Django 假设所有数据库都使用 UTF-8 编码。使用其他编码可能会导致意外行为,例如数据库对 Django 中有效的数据产生值过长的错误。有关如何正确设置数据库的信息,请参阅以下特定数据库的注意事项。

postgreSQL 注意事项

Django 支持 PostgreSQL 9.0 及更高版本。它需要使用 Psycopg2 2.0.9 或更高版本。

优化 postgreSQL 的配置

Django 需要其数据库连接的以下参数:

  • client_encoding: 'UTF8',

  • default_transaction_isolation: 默认为'read committed',或者连接选项中设置的值(见此处),

  • timezone: 当USE_TZTrue时为'UTC',否则为TIME_ZONE的值。

如果这些参数已经具有正确的值,Django 不会为每个新连接设置它们,这会稍微提高性能。您可以直接在postgresql.conf中配置它们,或者更方便地通过ALTER ROLE为每个数据库用户配置它们。

Django 在没有进行此优化的情况下也可以正常工作,但每个新连接都会执行一些额外的查询来设置这些参数。

隔离级别

与 PostgreSQL 本身一样,Django 默认使用READ COMMITTED隔离级别。如果需要更高的隔离级别,如REPEATABLE READSERIALIZABLE,请在DATABASES中的数据库配置的OPTIONS部分中设置它:

import psycopg2.extensions 

DATABASES = { 
    # ... 
    'OPTIONS': { 
        'isolation_level': psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE, 
    }, 
} 

在更高的隔禅级别下,您的应用程序应该准备好处理由于序列化失败而引发的异常。此选项设计用于高级用途。

varchar 和 text 列的索引

在模型字段上指定db_index=True时,Django 通常会输出一个CREATE INDEX语句。但是,如果字段的数据库类型为varchartext(例如,由CharFieldFileFieldTextField使用),那么 Django 将创建一个使用适当的 PostgreSQL 操作符类的额外索引。额外的索引是必要的,以正确执行使用LIKE操作符的查找,这在它们的 SQL 中使用containsstartswith查找类型时会发生。

MySQL 注意事项

版本支持

Django 支持 MySQL 5.5 及更高版本。

Django 的inspectdb功能使用包含所有数据库模式详细数据的information_schema数据库。

Django 期望数据库支持 Unicode(UTF-8 编码)并委托给它执行事务和引用完整性的任务。重要的是要意识到,当使用 MyISAM 存储引擎时,MySQL 实际上并不执行这两个任务,详见下一节。

存储引擎

MySQL 有几种存储引擎。您可以在服务器配置中更改默认存储引擎。

直到 MySQL 5.5.4,默认引擎是 MyISAM。MyISAM 的主要缺点是它不支持事务或强制外键约束。另一方面,直到 MySQL 5.6.4,它是唯一支持全文索引和搜索的引擎。

自 MySQL 5.5.5 以来,默认存储引擎是 InnoDB。该引擎完全支持事务,并支持外键引用。这可能是目前最好的选择。但是,请注意,InnoDB 自增计数器在 MySQL 重新启动时会丢失,因为它不记住AUTO_INCREMENT值,而是将其重新创建为max(id)+1。这可能导致AutoField值的意外重用。

如果您将现有项目升级到 MySQL 5.5.5,然后添加一些表,请确保您的表使用相同的存储引擎(即 MyISAM vs. InnoDB)。特别是,如果在它们之间具有ForeignKey的表使用不同的存储引擎,那么在运行migrate时可能会看到以下错误:

_mysql_exceptions.OperationalError: ( 
    1005, "Can't create table '\\db_name\\.#sql-4a8_ab' (errno: 150)" 
) 

MySQL DB API 驱动程序

Python 数据库 API 在 PEP 249 中有描述。MySQL 有三个实现此 API 的知名驱动程序:

所有这些驱动程序都是线程安全的,并提供连接池。MySQLdb是目前唯一不支持 Python 3 的驱动程序。

除了 DB API 驱动程序,Django 还需要一个适配器来访问其 ORM 中的数据库驱动程序。Django 为 MySQLdb/mysqlclient 提供了一个适配器,而 MySQL Connector/Python 则包含了自己的适配器。

mySQLdb

Django 需要 MySQLdb 版本 1.2.1p2 或更高版本。

如果在尝试使用 Django 时看到ImportError: cannot import name ImmutableSet,则您的 MySQLdb 安装可能包含一个过时的sets.py文件,与 Python 2.4 及更高版本中同名的内置模块发生冲突。要解决此问题,请验证您是否安装了 MySQLdb 版本 1.2.1p2 或更新版本,然后删除 MySQLdb 目录中由早期版本留下的sets.py文件。

MySQLdb 将日期字符串转换为 datetime 对象时存在已知问题。具体来说,值为0000-00-00的日期字符串对于 MySQL 是有效的,但在 MySQLdb 中会被转换为None

这意味着在使用可能具有0000-00-00值的行的 loaddata/dumpdata 时,您应该小心,因为它们将被转换为None

在撰写本文时,最新版本的 MySQLdb(1.2.4)不支持 Python 3。要在 Python 3 下使用 MySQLdb,您需要安装mysqlclient

mySQLclient

Django 需要 mysqlclient 1.3.3 或更高版本。请注意,不支持 Python 3.2。除了 Python 3.3+支持外,mysqlclient 应该与 MySQLdb 大致相同。

mySQL connector/python

MySQL Connector/Python 可从下载页面获取。Django 适配器可在 1.1.X 及更高版本中获取。它可能不支持最新版本的 Django。

时区定义

如果您打算使用 Django 的时区支持,请使用mysql_tzinfo_to_sql将时区表加载到 MySQL 数据库中。这只需要针对您的 MySQL 服务器执行一次,而不是每个数据库。

创建您的数据库

您可以使用命令行工具和以下 SQL 创建您的数据库:

CREATE DATABASE <dbname> CHARACTER SET utf8; 

这可以确保所有表和列默认使用 UTF-8。

校对设置

列的校对设置控制数据排序的顺序以及哪些字符串比较相等。它可以在数据库范围内设置,也可以在每个表和每个列上设置。这在 MySQL 文档中有详细说明。在所有情况下,您都可以通过直接操作数据库表来设置校对;Django 不提供在模型定义中设置这一点的方法。

默认情况下,对于 UTF-8 数据库,MySQL 将使用utf8_general_ci校对。这导致所有字符串相等比较以不区分大小写的方式进行。也就是说,"Fred"和"freD"在数据库级别被视为相等。如果在字段上有唯一约束,尝试将"aa"和"AA"插入同一列将是非法的,因为它们比较为相等(因此不唯一)。

在许多情况下,这个默认值不会有问题。但是,如果您真的想在特定列或表上进行区分大小写的比较,您将更改列或表以使用utf8_bin排序规则。在这种情况下要注意的主要事情是,如果您使用的是 MySQLdb 1.2.2,则 Django 中的数据库后端将为从数据库接收到的任何字符字段返回字节串(而不是 Unicode 字符串)。这与 Django 始终返回 Unicode 字符串的正常做法有很大的不同。

由您作为开发人员来处理这样一个事实,即如果您配置表使用utf8_bin排序规则,您将收到字节串。Django 本身应该大部分可以顺利地处理这样的列(除了这里描述的contrib.sessions``Sessioncontrib.admin``LogEntry表),但是您的代码必须准备在必要时调用“django.utils.encoding.smart_text()”,如果它真的想要处理一致的数据-Django 不会为您做这个(数据库后端层和模型填充层在内部是分开的,因此数据库层不知道它需要在这一个特定情况下进行这种转换)。

如果您使用的是 MySQLdb 1.2.1p2,Django 的标准CharField类将即使使用utf8_bin排序规则也返回 Unicode 字符串。但是,TextField字段将作为array.array实例(来自 Python 的标准array模块)返回。Django 对此无能为力,因为再次,当数据从数据库中读取时,所需的信息不可用。这个问题在 MySQLdb 1.2.2 中得到了解决,因此,如果您想要在utf8_bin排序规则下使用TextField,则升级到 1.2.2 版本,然后按照之前描述的处理字节串(这不应该太困难)是推荐的解决方案。

如果您决定在 MySQLdb 1.2.1p2 或 1.2.2 中使用utf8_bin排序规则来处理一些表,您仍应该为django.contrib.sessions.models.Session表(通常称为django_session)和django.contrib.admin.models.LogEntry表(通常称为django_admin_log)使用utf8_general_ci(默认值)排序规则。请注意,根据 MySQL Unicode 字符集,utf8_general_ci排序规则的比较速度更快,但比utf8_unicode_ci排序规则稍微不正确。如果这对您的应用程序是可以接受的,您应该使用utf8_general_ci,因为它更快。如果这是不可接受的(例如,如果您需要德语字典顺序),请使用utf8_unicode_ci,因为它更准确。

注意

模型表单集以区分大小写的方式验证唯一字段。因此,在使用不区分大小写的排序规则时,具有仅大小写不同的唯一字段值的表单集将通过验证,但在调用“save()”时,将引发IntegrityError

连接到数据库

连接设置按以下顺序使用:

  • OPTIONS

  • NAMEUSERPASSWORDHOSTPORT

  • MySQL 选项文件

换句话说,如果在OPTIONS中设置了数据库的名称,这将优先于NAME,这将覆盖 MySQL 选项文件中的任何内容。以下是一个使用 MySQL 选项文件的示例配置:

# settings.py 
DATABASES = { 
    'default': { 
        'ENGINE': 'django.db.backends.mysql', 
        'OPTIONS': {'read_default_file': '/path/to/my.cnf',}, 
    } 
} 

# my.cnf 
[client] 
database = NAME 
user = USER 
password = PASSWORD 
default-character-set = utf8 

其他一些 MySQLdb 连接选项可能会有用,例如sslinit_commandsql_mode。请参阅 MySQLdb 文档以获取更多详细信息。

创建您的表

当 Django 生成模式时,它不指定存储引擎,因此表将使用数据库服务器配置的默认存储引擎创建。

最简单的解决方案是将数据库服务器的默认存储引擎设置为所需的引擎。

如果您使用托管服务并且无法更改服务器的默认存储引擎,则有几种选择。

  • 创建表后,执行ALTER TABLE语句将表转换为新的存储引擎(例如 InnoDB):
        ALTER TABLE <tablename> ENGINE=INNODB; 

  • 如果您有很多表,这可能会很麻烦。

  • 另一个选项是在创建表之前使用 MySQLdb 的init_command选项:

        'OPTIONS': { 
           'init_command': 'SET storage_engine=INNODB', 
        } 

这将在连接到数据库时设置默认存储引擎。创建表后,应删除此选项,因为它会向每个数据库连接添加一个仅在表创建期间需要的查询。

表名

即使在最新版本的 MySQL 中,也存在已知问题,可能会在特定条件下执行某些 SQL 语句时更改表名的情况。建议您尽可能使用小写表名,以避免可能由此行为引起的任何问题。Django 在自动生成模型的表名时使用小写表名,因此,如果您通过db_table参数覆盖表名,则主要考虑这一点。

保存点

Django ORM 和 MySQL(使用 InnoDB 存储引擎时)都支持数据库保存点。

如果使用 MyISAM 存储引擎,请注意,如果尝试使用事务 API 的保存点相关方法,您将收到数据库生成的错误。原因是检测 MySQL 数据库/表的存储引擎是一项昂贵的操作,因此决定不值得根据此类检测结果动态转换这些方法为无操作。

特定字段的注意事项

字符字段

如果您对字段使用unique=True,则存储为VARCHAR列类型的任何字段的max_length将限制为 255 个字符。这会影响CharFieldSlugFieldCommaSeparatedIntegerField

时间和日期时间字段的分数秒支持

MySQL 5.6.4 及更高版本可以存储分数秒,前提是列定义包括分数指示(例如,DATETIME(6))。早期版本根本不支持它们。此外,早于 1.2.5 的 MySQLdb 版本存在一个错误,也会阻止与 MySQL 一起使用分数秒。

如果数据库服务器支持,Django 不会将现有列升级以包括分数秒。如果要在现有数据库上启用它们,您需要手动更新目标数据库上的列,例如执行以下命令:

ALTER TABLE `your_table` MODIFY `your_datetime_column` DATETIME(6) 

或在数据迁移中使用RunSQL操作。

默认情况下,使用 mysqlclient 或 MySQLdb 1.2.5 或更高版本在 MySQL 5.6.4 或更高版本上创建新的DateTimeFieldTimeField列时现在支持分数秒。

时间戳列

如果您使用包含TIMESTAMP列的旧数据库,则必须将USE_TZ = False设置为避免数据损坏。inspectdb将这些列映射到DateTimeField,如果启用时区支持,则 MySQL 和 Django 都将尝试将值从 UTC 转换为本地时间。

使用 Queryset.Select_For_Update()进行行锁定

MySQL 不支持SELECT ... FOR UPDATE语句的NOWAIT选项。如果使用select_for_update()并且nowait=True,则会引发DatabaseError

自动类型转换可能导致意外结果

在对字符串类型执行查询时,但具有整数值时,MySQL 将在执行比较之前将表中所有值的类型强制转换为整数。如果您的表包含值"abc","def",并且您查询WHERE mycolumn=0,则两行都将匹配。同样,WHERE mycolumn=1将匹配值"abc1"。因此,在 Django 中包含的字符串类型字段在使用它进行查询之前将始终将该值转换为字符串。

如果您实现了直接继承自Field的自定义模型字段,正在覆盖get_prep_value(),或使用extra()raw(),则应确保执行适当的类型转换。

SQLite 注意事项

SQLite 为主要是只读或需要较小安装占用空间的应用程序提供了一个优秀的开发替代方案。然而,与所有数据库服务器一样,SQLite 具有一些特定于 SQLite 的差异,您应该注意。

子字符串匹配和区分大小写

对于所有 SQLite 版本,在尝试匹配某些类型的字符串时,会出现一些略微反直觉的行为。这些行为在 Querysets 中使用iexactcontains过滤器时会触发。行为分为两种情况:

  1. 对于子字符串匹配,所有匹配都是不区分大小写的。也就是说,过滤器filter(name__contains="aa")将匹配名称为“Aabb”的名称。

  2. 对于包含 ASCII 范围之外字符的字符串,所有精确的字符串匹配都是区分大小写的,即使在查询中传递了不区分大小写的选项。因此,在这些情况下,iexact过滤器的行为将与精确过滤器完全相同。

这些问题的一些可能的解决方法在 sqlite.org 上有记录,但默认的 Django SQLite 后端没有使用它们,因为将它们整合起来可能会相当困难。因此,Django 暴露了默认的 SQLite 行为,您在进行不区分大小写或子字符串过滤时应该注意这一点。

旧的 SQLite 和 CASE 表达式

SQLite 3.6.23.1 及更早版本在处理包含ELSE和算术的CASE表达式中的查询参数时存在一个错误。

SQLite 3.6.23.1 于 2010 年 3 月发布,大多数不同平台的当前二进制发行版都包含了更新版本的 SQLite,但值得注意的是 Python 2.7 的 Windows 安装程序除外。

截至目前,Windows-Python 2.7.10 的最新版本包括 SQLite 3.6.21。您可以安装pysqlite2或将sqlite3.dll(默认安装在C:\Python27\DLLs中)替换为来自 sqlite.org 的更新版本以解决此问题。

使用更新版本的 SQLite DB-API 2.0 驱动程序

如果发现可用的话,Django 将优先使用pysqlite2模块而不是 Python 标准库中附带的sqlite3

如果需要,这提供了升级 DB-API 2.0 接口或 SQLite 3 本身到比特定 Python 二进制发行版中包含的版本更新的能力。

数据库被锁定的错误

SQLite 旨在成为一个轻量级的数据库,因此无法支持高并发。OperationalError: database is locked错误表明您的应用程序正在经历比sqlite默认配置中可以处理的并发更多的情况。这个错误意味着一个线程或进程在数据库连接上有一个独占锁,另一个线程在等待锁被释放时超时了。

Python 的 SQLite 包装器具有默认的超时值,确定第二个线程在锁上等待多长时间才会超时并引发OperationalError: database is locked错误。

如果您遇到此错误,您可以通过以下方法解决:

  • 切换到另一个数据库后端。在某一点上,SQLite 对于真实世界的应用程序来说变得太轻,这些并发错误表明您已经达到了这一点。

  • 重写您的代码以减少并发并确保数据库事务的持续时间较短。

  • 通过设置timeout数据库选项来增加默认超时值:

        'OPTIONS': { # ... 'timeout': 20, # ... } 

这只会使 SQLite 在抛出数据库被锁定错误之前等待更长的时间;它实际上并不会真正解决这些问题。

queryset.Select_For_Update()不支持

SQLite 不支持SELECT ... FOR UPDATE语法。调用它不会产生任何效果。

原始查询中不支持 pyformat 参数样式

对于大多数后端,原始查询(Manager.raw()cursor.execute())可以使用 pyformat 参数样式,其中查询中的占位符为'%(name)s',参数作为字典而不是列表传递。SQLite 不支持这一点。

连接.queries 中未引用的参数

sqlite3不提供在引用和替换参数后检索 SQL 的方法。相反,在connection.queries中的 SQL 将使用简单的字符串插值重新构建。这可能是不正确的。在将查询复制到 SQLite shell 之前,请确保在必要的地方添加引号。

Oracle 注意事项

Django 支持 Oracle 数据库服务器版本 11.1 及更高版本。需要版本 4.3.1 或更高版本的cx_Oraclecx-oracle.sourceforge.net/)Python 驱动程序,尽管我们建议使用版本 5.1.3 或更高版本,因为这些版本支持 Python 3。

请注意,由于cx_Oracle 5.0 中存在 Unicode 损坏错误,因此不应该使用该驱动程序的该版本与 Django 一起使用;cx_Oracle 5.0.1 解决了此问题,因此如果您想使用更新的cx_Oracle,请使用版本 5.0.1。

cx_Oracle 5.0.1 或更高版本可以选择使用WITH_UNICODE环境变量进行编译。这是推荐的,但不是必需的。

为了使python manage.py migrate命令工作,您的 Oracle 数据库用户必须具有运行以下命令的权限:

  • CREATE TABLE

  • CREATE SEQUENCE

  • CREATE PROCEDURE

  • CREATE TRIGGER

要运行项目的测试套件,用户通常需要这些额外权限:

  • CREATE USER

  • DROP USER

  • CREATE TABLESPACE

  • DROP TABLESPACE

  • CREATE SESSION WITH ADMIN OPTION

  • CREATE TABLE WITH ADMIN OPTION

  • CREATE SEQUENCE WITH ADMIN OPTION

  • CREATE PROCEDURE WITH ADMIN OPTION

  • CREATE TRIGGER WITH ADMIN OPTION

请注意,虽然RESOURCE角色具有所需的CREATE TABLECREATE SEQUENCECREATE PROCEDURECREATE TRIGGER权限,而且授予RESOURCE WITH ADMIN OPTION的用户可以授予RESOURCE,但这样的用户不能授予单个权限(例如CREATE TABLE),因此RESOURCE WITH ADMIN OPTION通常不足以运行测试。

一些测试套件还会创建视图;要运行这些视图,用户还需要CREATE VIEW WITH ADMIN OPTION权限。特别是 Django 自己的测试套件需要这个权限。

所有这些权限都包含在 DBA 角色中,这适用于在私人开发人员的数据库上使用。

Oracle 数据库后端使用SYS.DBMS_LOB包,因此您的用户将需要对其具有执行权限。通常情况下,默认情况下所有用户都可以访问它,但如果不行,您将需要授予权限,如下所示:

GRANT EXECUTE ON SYS.DBMS_LOB TO user; 

连接到数据库

要使用 Oracle 数据库的服务名称进行连接,您的settings.py文件应该如下所示:

DATABASES = { 
    'default': { 
        'ENGINE': 'django.db.backends.oracle', 
        'NAME': 'xe', 
        'USER': 'a_user', 
        'PASSWORD': 'a_password', 
        'HOST': '', 
        'PORT': '', 
    } 
} 

在这种情况下,您应该将HOSTPORT都留空。但是,如果您不使用tnsnames.ora文件或类似的命名方法,并且希望使用 SID(在此示例中为xe)进行连接,那么请填写HOSTPORT如下:

DATABASES = { 
    'default': { 
        'ENGINE': 'django.db.backends.oracle', 
        'NAME': 'xe', 
        'USER': 'a_user', 
        'PASSWORD': 'a_password', 
        'HOST': 'dbprod01ned.mycompany.com', 
        'PORT': '1540', 
    } 
} 

您应该同时提供HOSTPORT,或者将两者都留空。Django 将根据选择使用不同的连接描述符。

线程选项

如果您计划在多线程环境中运行 Django(例如,在任何现代操作系统上使用默认 MPM 模块的 Apache),那么您必须将 Oracle 数据库配置的threaded选项设置为 True:

'OPTIONS': { 
    'threaded': True, 
}, 

未能这样做可能会导致崩溃和其他奇怪的行为。

INSERT ... RETURNING INTO

默认情况下,Oracle 后端使用RETURNING INTO子句来高效地检索AutoField的值,当插入新行时。这种行为可能会导致某些不寻常的设置中出现DatabaseError,例如在远程表中插入,或者在具有INSTEAD OF触发器的视图中插入。

RETURNING INTO子句可以通过将数据库配置的use_returning_into选项设置为 False 来禁用:

'OPTIONS': { 
    'use_returning_into': False, 
}, 

在这种情况下,Oracle 后端将使用单独的SELECT查询来检索AutoField值。

命名问题

Oracle 对名称长度有 30 个字符的限制。

为了适应这一点,后端将数据库标识符截断以适应,用可重复的 MD5 哈希值替换截断名称的最后四个字符。此外,后端将数据库标识符转换为全大写。

为了防止这些转换(通常仅在处理传统数据库或访问属于其他用户的表时才需要),请使用带引号的名称作为db_table的值:

class LegacyModel(models.Model): 
    class Meta: 
        db_table = '"name_left_in_lowercase"' 

class ForeignModel(models.Model): 
    class Meta: 
        db_table = '"OTHER_USER"."NAME_ONLY_SEEMS_OVER_30"' 

带引号的名称也可以与 Django 的其他支持的数据库后端一起使用;但是,除了 Oracle 之外,引号没有任何效果。

在运行migrate时,如果将某些 Oracle 关键字用作模型字段的名称或db_column选项的值,则可能会遇到ORA-06552错误。 Django 引用所有在查询中使用的标识符,以防止大多数此类问题,但是当 Oracle 数据类型用作列名时,仍然可能发生此错误。特别要注意避免使用名称datetimestampnumberfloat作为字段名称。

NULL 和空字符串

Django 通常更喜欢使用空字符串(''“)而不是NULL,但是 Oracle 将两者视为相同。为了解决这个问题,Oracle 后端会忽略对具有空字符串作为可能值的字段的显式null选项,并生成 DDL,就好像null=True一样。在从数据库中获取数据时,假定这些字段中的NULL值实际上意味着空字符串,并且数据会被默默地转换以反映这一假设。

Textfield 的限制

Oracle 后端将TextField存储为NCLOB列。 Oracle 对此类 LOB 列的使用施加了一些限制:

  • LOB 列不能用作主键。

  • LOB 列不能用于索引。

  • LOB 列不能在SELECT DISTINCT列表中使用。这意味着在包含TextField列的模型上尝试使用QuerySet.distinct方法将导致针对 Oracle 运行时出错。作为解决方法,使用QuerySet.defer方法与distinct()结合使用,以防止TextField列被包括在SELECT DISTINCT列表中。

使用第三方数据库后端

除了官方支持的数据库外,还有第三方提供的后端,允许您使用其他数据库与 Django 一起使用:

  • SAP SQL Anywhere

  • IBM DB2

  • Microsoft SQL Server

  • Firebird

  • ODBC

  • ADSDB

这些非官方后端支持的 Django 版本和 ORM 功能差异很大。关于这些非官方后端的具体功能以及任何支持查询,应该直接向每个第三方项目提供的支持渠道提出。

将 Django 与传统数据库集成

虽然 Django 最适合开发新应用程序,但完全可以将其集成到传统数据库中。Django 包括一些实用程序,以尽可能自动化这个过程。

设置好 Django 后,您将按照以下一般流程与现有数据库集成。

给 Django 提供您的数据库参数

您需要告诉 Django 您的数据库连接参数是什么,数据库的名称是什么。通过编辑DATABASES设置并为'default'连接分配值来完成这一点:

  • NAME

  • ENGINE <DATABASE-ENGINE>

  • USER

  • PASSWORD

  • HOST

  • PORT

自动生成模型

Django 带有一个名为inspectdb的实用程序,可以通过内省现有数据库来创建模型。您可以通过运行此命令查看输出:

python manage.py inspectdb 

使用标准的 Unix 输出重定向将此保存为文件:

python manage.py inspectdb > models.py 

此功能旨在作为快捷方式,而不是最终的模型生成。有关更多信息,请参阅inspectdb的文档。

清理模型后,将文件命名为models.py并将其放在包含您的应用程序的 Python 包中。然后将该应用程序添加到您的INSTALLED_APPS设置中。

默认情况下,inspectdb创建的是不受管理的模型。也就是说,在模型的Meta类中的managed = False告诉 Django 不要管理每个表的创建、修改和删除:

class Person(models.Model): 
    id = models.IntegerField(primary_key=True) 
    first_name = models.CharField(max_length=70) 
    class Meta: 
       managed = False 
       db_table = 'CENSUS_PERSONS' 

如果你确实希望 Django 管理表的生命周期,你需要将前面的managed选项更改为True(或者简单地删除它,因为True是它的默认值)。

安装核心 Django 表

接下来,运行migrate命令来安装任何额外需要的数据库记录,比如管理员权限和内容类型:

python manage.py migrate 

清理生成的模型

正如你所期望的那样,数据库内省并不完美,你需要对生成的模型代码进行一些轻微的清理。以下是处理生成模型的一些建议:

  • 每个数据库表都转换为一个模型类(也就是说,数据库表和模型类之间是一对一的映射)。这意味着你需要将许多对多连接表的模型重构为ManyToManyField对象。

  • 每个生成的模型都有一个属性对应每个字段,包括 id 主键字段。然而,要记住,如果一个模型没有主键,Django 会自动添加一个 id 主键字段。因此,你需要删除任何看起来像这样的行:

        id = models.IntegerField(primary_key=True) 

  • 这些行不仅是多余的,而且如果你的应用程序将向这些表中添加记录,它们还会引起问题。

  • 每个字段的类型(例如CharFieldDateField)是通过查看数据库列类型(例如VARCHARDATE)来确定的。如果inspectdb无法将列的类型映射到模型字段类型,它将使用TextField,并在生成的模型中在字段旁边插入 Python 注释'This field type is a guess.'。留意这一点,如果需要,相应地更改字段类型。

  • 如果数据库中的字段没有良好的 Django 等效项,你可以放心地将其删除。Django 模型层并不要求包含表中的每个字段。

  • 如果数据库列名是 Python 保留字(比如passclassfor),inspectdb会在属性名后面添加"_field",并将db_column属性设置为真实字段名(例如passclassfor)。

  • 例如,如果一个表有一个名为forINT列,生成的模型将有一个类似这样的字段:

        for_field = models.IntegerField(db_column='for') 

  • inspectdb会在字段旁边插入 Python 注释'Field renamed because it was a Python reserved word.'

  • 如果你的数据库包含引用其他表的表(大多数数据库都是这样),你可能需要重新排列生成的模型的顺序,以便引用其他模型的模型被正确排序。例如,如果模型Book有一个指向模型AuthorForeignKey,模型Author应该在模型Book之前定义。如果需要在尚未定义的模型上创建关系,你可以使用包含模型名称的字符串,而不是模型对象本身。

  • inspectdb检测 PostgreSQL、MySQL 和 SQLite 的主键。也就是说,它会在适当的地方插入primary_key=True。对于其他数据库,你需要在每个模型中至少插入一个primary_key=True字段,因为 Django 模型需要有一个primary_key=True字段。

  • 外键检测只适用于 PostgreSQL 和某些类型的 MySQL 表。在其他情况下,外键字段将被生成为IntegerField,假设外键列是一个INT列。

测试和调整

这些是基本步骤-从这里开始,你需要调整 Django 生成的模型,直到它们按照你的意愿工作。尝试通过 Django 数据库 API 访问数据,并尝试通过 Django 的管理站点编辑对象,并相应地编辑模型文件。

接下来是什么?

就是这样!

希望您喜欢阅读《精通 Django:核心》,并从这本书中学到了很多。虽然这本书将为您提供 Django 的完整参考,但没有什么能替代老实的实践-所以开始编码,祝您在 Django 职业生涯中一切顺利!

剩下的章节纯粹供您参考。它们包括附录和所有 Django 函数和字段的快速参考。

附录 A.模型定义参考

第四章中的模型解释了定义模型的基础知识,并且我们在本书的其余部分中使用它们。然而,还有大量的模型选项可用,其他地方没有涵盖。本附录解释了每个可能的模型定义选项。

字段

模型最重要的部分-也是模型的唯一必需部分-是它定义的数据库字段列表。

字段名称限制

Django 对模型字段名称只有两个限制:

  1. 字段名称不能是 Python 保留字,因为那将导致 Python 语法错误。例如:
        class Example(models.Model): 
        pass = models.IntegerField() # 'pass' is a reserved word! 

  1. 由于 Django 的查询查找语法的工作方式,字段名称不能连续包含多个下划线。例如:
        class Example(models.Model): 
            # 'foo__bar' has two underscores! 
            foo__bar = models.IntegerField()  

您模型中的每个字段都应该是适当Field类的实例。Django 使用字段类类型来确定一些事情:

  • 数据库列类型(例如,INTEGERVARCHAR

  • 在 Django 的表单和管理站点中使用的小部件,如果您愿意使用它(例如,<input type="text"><select>

  • 最小的验证要求,这些要求在 Django 的管理界面和表单中使用

每个字段类都可以传递一系列选项参数,例如当我们在第四章中构建书籍模型时,我们的num_pages字段如下所示:

num_pages = models.IntegerField(blank=True, null=True) 

在这种情况下,我们为字段类设置了blanknull选项。表 A.2列出了 Django 中的所有字段选项。

许多字段还定义了特定于该类的其他选项,例如CharField类具有一个必需选项max_length,默认为None。例如:

title = models.CharField(max_length=100) 

在这种情况下,我们将max_length字段选项设置为 100,以将我们的书名限制为 100 个字符。

字段类的完整列表按字母顺序排列在表 A.1中。

字段 默认小部件 描述
AutoField N/A 根据可用 ID 自动递增的IntegerField
BigIntegerField NumberInput 64 位整数,类似于IntegerField,只是它保证适合从-92233720368547758089223372036854775807的数字
BinaryField N/A 用于存储原始二进制数据的字段。它只支持bytes赋值。请注意,此字段功能有限。
BooleanField CheckboxInput 真/假字段。如果需要接受null值,则使用NullBooleanField
CharField TextInput 用于小到大的字符串的字符串字段。对于大量的文本,请使用TextFieldCharField有一个额外的必需参数:max_length。字段的最大长度(以字符为单位)。
DateField DateInput 日期,在 Python 中由datetime.date实例表示。有两个额外的可选参数:auto_now,每次保存对象时自动将字段设置为现在,auto_now_add,在对象首次创建时自动将字段设置为现在。
DateTimeField DateTimeInput 日期和时间,在 Python 中由datetime.datetime实例表示。接受与DateField相同的额外参数。
DecimalField TextInput 固定精度的十进制数,在 Python 中由Decimal实例表示。有两个必需的参数:max_digitsdecimal_places
DurationField TextInput 用于存储时间段的字段-在 Python 中由timedelta建模。
EmailField TextInput 使用EmailValidator验证输入的CharFieldmax_length默认为254
FileField ClearableFileInput 文件上传字段。有关FileField的更多信息,请参见下一节。
FilePathField Select CharField,其选择限于文件系统上某个目录中的文件名。
FloatField NumberInput 由 Python 中的float实例表示的浮点数。注意,当field.localizeFalse时,默认小部件是TextInput
ImageField ClearableFileInput 继承自FileField的所有属性和方法,但也验证上传的对象是否是有效的图像。额外的heightwidth属性。需要在 http://pillow.readthedocs.org/en/latest/上可用的 Pillow 库。
IntegerField NumberInput 一个整数。在 Django 支持的所有数据库中,从-21474836482147483647的值都是安全的。
GenericIPAddressField TextInput 一个 IPv4 或 IPv6 地址,以字符串格式表示(例如,192.0.2.302a02:42fe::4)。
NullBooleanField NullBooleanSelect BooleanField,但允许NULL作为其中一个选项。
PositiveIntegerField NumberInput 一个整数。在 Django 支持的所有数据库中,从02147483647的值都是安全的。
SlugField TextInput Slug 是一个报纸术语。Slug 是某物的一个简短标签,只包含字母、数字、下划线或连字符。
SmallIntegerField NumberInput IntegerField,但只允许在某个点以下的值。在 Django 支持的所有数据库中,从-3276832767的值都是安全的。
TextField Textarea 一个大文本字段。如果指定了max_length属性,它将反映在自动生成的表单字段的Textarea小部件中。
TimeField TextInput 一个时间,由 Python 中的datetime.time实例表示。
URLField URLInput 用于 URL 的CharField。可选的max_length参数。
UUIDField TextInput 用于存储通用唯一标识符的字段。使用 Python 的UUID类。

表 A.1:Django 模型字段参考

FileField 注意事项

不支持primary_keyunique参数,如果使用将会引发TypeError

  • 有两个可选参数:FileField.upload_to

  • FileField.storage

FileField FileField.upload_to

一个本地文件系统路径,将被附加到您的MEDIA_ROOT设置,以确定url属性的值。这个路径可能包含strftime()格式,它将被文件上传的日期/时间替换(这样上传的文件不会填满给定的目录)。这也可以是一个可调用的,比如一个函数,它将被调用来获取上传路径,包括文件名。这个可调用必须能够接受两个参数,并返回一个 Unix 风格的路径(带有正斜杠),以便传递给存储系统。

将传递的两个参数是:

  • **实例:**模型的一个实例,其中定义了 FileField。更具体地说,这是当前文件被附加的特定实例。在大多数情况下,这个对象还没有保存到数据库中,所以如果它使用默认的AutoField,它可能还没有主键字段的值。

  • **文件名:**最初给定的文件名。在确定最终目标路径时可能会考虑这个文件名。

FileField.storage

一个存储对象,用于处理文件的存储和检索。这个字段的默认表单小部件是ClearableFileInput。在模型中使用FileFieldImageField(见下文)需要几个步骤:

  • 在您的设置文件中,您需要将MEDIA_ROOT定义为一个目录的完整路径,您希望 Django 存储上传的文件在其中。(出于性能考虑,这些文件不存储在数据库中。)将MEDIA_URL定义为该目录的基本公共 URL。确保这个目录对 Web 服务器的用户帐户是可写的。

  • FileFieldImageField添加到您的模型中,定义upload_to选项以指定MEDIA_ROOT的子目录,用于上传文件。

  • 在数据库中存储的只是文件的路径(相对于 MEDIA_ROOT)。您很可能会想要使用 Django 提供的便捷的 url 属性。例如,如果您的 ImageField 名为 mug_shot,您可以在模板中使用 {{ object.mug_shot.url }} 获取图像的绝对路径。

请注意,每当处理上传的文件时,都应该密切关注您上传文件的位置和文件类型,以避免安全漏洞。验证所有上传的文件,以确保文件是您认为的文件。例如,如果您盲目地让某人上传文件,而没有进行验证,到您的 Web 服务器文档根目录中,那么某人可能会上传一个 CGI 或 PHP 脚本,并通过访问其 URL 在您的网站上执行该脚本。不要允许这种情况发生。

还要注意,即使是上传的 HTML 文件,由于浏览器可以执行它(尽管服务器不能),可能会带来等同于 XSS 或 CSRF 攻击的安全威胁。FileField 实例在数据库中以 varchar 列的形式创建,具有默认的最大长度为 100 个字符。与其他字段一样,您可以使用 max_length 参数更改最大长度。

FileField 和 FieldFile

当您在模型上访问 FileField 时,会得到一个 FieldFile 的实例,作为访问底层文件的代理。除了从 django.core.files.File 继承的功能外,此类还具有几个属性和方法,可用于与文件数据交互:

FieldFile.url

通过调用底层 Storage 类的 url() 方法来访问文件的相对 URL 的只读属性。

FieldFile.open(mode='rb')

行为类似于标准的 Python open() 方法,并以 mode 指定的模式打开与此实例关联的文件。

FieldFile.close()

行为类似于标准的 Python file.close() 方法,并关闭与此实例关联的文件。

FieldFile.save(name, content, save=True)

此方法接受文件名和文件内容,并将它们传递给字段的存储类,然后将存储的文件与模型字段关联起来。如果您想手动将文件数据与模型上的 FileField 实例关联起来,可以使用 save() 方法来持久化该文件数据。

需要两个必需参数:name 是文件的名称,content 是包含文件内容的对象。可选的 save 参数控制在更改与此字段关联的文件后是否保存模型实例。默认为 True

请注意,content 参数应该是 django.core.files.File 的实例,而不是 Python 的内置文件对象。您可以像这样从现有的 Python 文件对象构造一个 File

from django.core.files import File 
# Open an existing file using Python's built-in open() 
f = open('/tmp/hello.world') 
myfile = File(f) 

或者您可以像这样从 Python 字符串构造一个:

from django.core.files.base import ContentFile 
myfile = ContentFile("hello world") 

FieldFile.delete(save=True)

删除与此实例关联的文件并清除字段上的所有属性。如果在调用 delete() 时文件处于打开状态,此方法将关闭文件。

可选的 save 参数控制在删除与此字段关联的文件后是否保存模型实例。默认为 True

请注意,当模型被删除时,相关文件不会被删除。如果您需要清理孤立的文件,您需要自行处理(例如,使用自定义的管理命令,可以手动运行或通过例如 cron 定期运行)。

通用字段选项

表 A.2 列出了 Django 中所有字段类型的所有可选字段参数。

选项 描述
null 如果为 True,Django 将在数据库中将空值存储为 NULL。默认为 False。避免在诸如 CharFieldTextField 等基于字符串的字段上使用 null,因为空字符串值将始终被存储为空字符串,而不是 NULL。对于基于字符串和非基于字符串的字段,如果希望在表单中允许空值,还需要设置 blank=True。如果要接受带有 BooleanFieldnull 值,请改用 NullBooleanField
blank 如果为 True,则允许该字段为空。默认为 False。请注意,这与 null 是不同的。null 纯粹是与数据库相关的,而 blank 是与验证相关的。
choices 一个可迭代对象(例如列表或元组),其中包含正好两个项的可迭代对象(例如 [(A, B), (A, B) ...]),用作此字段的选择。如果给出了这个选项,默认的表单小部件将是一个带有这些选择的选择框,而不是标准文本字段。每个元组中的第一个元素是要在模型上设置的实际值,第二个元素是人类可读的名称。
db_column 用于此字段的数据库列的名称。如果没有给出,Django 将使用字段的名称。
db_index 如果为 True,将为此字段创建数据库索引。
db_tablespace 用于此字段索引的数据库表空间的名称,如果此字段已被索引。默认值是项目的 DEFAULT_INDEX_TABLESPACE 设置(如果设置了),或者模型的 db_tablespace(如果有)。如果后端不支持索引的表空间,则将忽略此选项。
default 该字段的默认值。这可以是一个值或一个可调用对象。如果是可调用的,它将在创建新对象时每次被调用。默认值不能是可变对象(模型实例、列表、集合等),因为在所有新模型实例中将使用对该对象的相同实例的引用作为默认值。
editable 如果为 False,该字段将不会显示在管理界面或任何其他 ModelForm 中。它们也会在模型验证期间被跳过。默认为 True
error_messages error_messages 参数允许您覆盖字段将引发的默认消息。传入一个字典,其中键与您想要覆盖的错误消息相匹配。错误消息键包括 nullblankinvalidinvalid_choiceuniqueunique_for_date
help_text 要与表单小部件一起显示的额外帮助文本。即使您的字段在表单上没有使用,这也是有用的文档。请注意,此值在自动生成的表单中 是 HTML 转义的。这样,如果您愿意,可以在 help_text 中包含 HTML。
primary_key 如果为 True,则该字段是模型的主键。如果您没有为模型中的任何字段指定 primary_key=True,Django 将自动添加一个 AutoField 来保存主键,因此您不需要在任何字段上设置 primary_key=True,除非您想要覆盖默认的主键行为。主键字段是只读的。
unique 如果为 True,则此字段必须在整个表中是唯一的。这是在数据库级别和模型验证期间强制执行的。此选项对除 ManyToManyFieldOneToOneFieldFileField 之外的所有字段类型都有效。
unique_for_date 将其设置为 DateFieldDateTimeField 的名称,以要求此字段对于日期字段的值是唯一的。例如,如果有一个字段 title,其 unique_for_date="pub_date",那么 Django 将不允许输入具有相同 titlepub_date 的两条记录。这是在模型验证期间由 Model.validate_unique() 强制执行的,但不是在数据库级别上。
unique_for_month 类似于 unique_for_date,但要求该字段相对于月份是唯一的。
unique_for_year 类似于 unique_for_date,但要求该字段相对于年份是唯一的。
verbose_name 字段的可读名称。如果未给出详细名称,Django 将使用字段的属性名称自动创建它,将下划线转换为空格。
validators 一个要为此字段运行的验证器列表。

表 A.2:Django 通用字段选项

字段属性引用

每个Field实例都包含几个属性,允许内省其行为。在需要编写依赖于字段功能的代码时,请使用这些属性,而不是isinstance检查。这些属性可以与Model._meta API 一起使用,以缩小对特定字段类型的搜索。自定义模型字段应实现这些标志。

字段属性

Field.auto_created

布尔标志,指示字段是否自动创建,例如模型继承中使用的OneToOneField

Field.concrete

布尔标志,指示字段是否与数据库列关联。

Field.hidden

布尔标志,指示字段是否用于支持另一个非隐藏字段的功能(例如,构成GenericForeignKeycontent_typeobject_id字段)。hidden标志用于区分模型上的字段的公共子集与模型上的所有字段。

Field.is_relation

布尔标志,指示字段是否包含对一个或多个其他模型的引用,以实现其功能(例如,ForeignKeyManyToManyFieldOneToOneField等)。

Field.model

返回定义字段的模型。如果字段在模型的超类上定义,则model将引用超类,而不是实例的类。

具有关系的字段属性

这些属性用于查询关系的基数和其他细节。这些属性存在于所有字段上;但是,只有在字段是关系类型(Field.is_relation=True)时,它们才会有有意义的值。

Field.many_to_many

布尔标志,如果字段具有多对多关系,则为True;否则为False。Django 中唯一包含此标志为True的字段是ManyToManyField

Field.many_to_one

布尔标志,如果字段具有多对一关系(例如ForeignKey),则为True;否则为False

Field.one_to_many

布尔标志,如果字段具有一对多关系(例如GenericRelationForeignKey的反向关系),则为True;否则为False

Field.one_to_one

布尔标志,如果字段具有一对一关系(例如OneToOneField),则为True;否则为False

Field.related_model

指向字段相关的模型。例如,在ForeignKey(Author)中的Author。如果字段具有通用关系(例如GenericForeignKeyGenericRelation),则related_model将为None

关系

Django 还定义了一组表示关系的字段。

ForeignKey

多对一关系。需要一个位置参数:模型相关的类。要创建递归关系(与自身具有多对一关系的对象),请使用models.ForeignKey('self')

如果需要在尚未定义的模型上创建关系,可以使用模型的名称,而不是模型对象本身:

from django.db import models 

class Car(models.Model): 
    manufacturer = models.ForeignKey('Manufacturer') 
    # ... 

class Manufacturer(models.Model): 
    # ... 
    pass 

要引用另一个应用程序中定义的模型,可以明确指定具有完整应用程序标签的模型。例如,如果上面的Manufacturer模型在另一个名为production的应用程序中定义,则需要使用:

class Car(models.Model): 
    manufacturer = models.ForeignKey('production.Manufacturer') 

在两个应用程序之间解析循环导入依赖关系时,这种引用可能很有用。在ForeignKey上自动创建数据库索引。您可以通过将db_index设置为False来禁用此功能。

如果您创建外键以确保一致性而不是连接,或者如果您将创建替代索引(如部分索引或多列索引),则可能希望避免索引的开销。

数据库表示

在幕后,Django 将字段名附加"_id"以创建其数据库列名。在上面的示例中,Car模型的数据库表将具有manufacturer_id列。

您可以通过指定db_column来明确更改这一点,但是,除非编写自定义 SQL,否则您的代码不应该处理数据库列名。您将始终处理模型对象的字段名称。

参数

ForeignKey接受一组额外的参数-全部是可选的-用于定义关系的详细信息。

limit_choices_to

设置此字段的可用选择的限制,当使用ModelForm或管理员渲染此字段时(默认情况下,查询集中的所有对象都可供选择)。可以使用字典、Q对象或返回字典或Q对象的可调用对象。例如:

staff_member = models.ForeignKey(User, limit_choices_to={'is_staff': True}) 

导致ModelForm上的相应字段仅列出is_staff=TrueUsers。这在 Django 管理员中可能会有所帮助。可调用形式可能会有所帮助,例如,当与 Python datetime模块一起使用以限制日期范围的选择时。例如:

def limit_pub_date_choices(): 
    return {'pub_date__lte': datetime.date.utcnow()} 
limit_choices_to = limit_pub_date_choices 

如果limit_choices_to是或返回Q 对象,对于复杂查询很有用,那么它只会影响在模型的ModelAdmin中未列出raw_id_fields时管理员中可用的选择。

用于从相关对象返回到此对象的关系的名称。这也是related_query_name的默认值(从目标模型返回的反向过滤器名称)。有关完整说明和示例,请参阅相关对象文档。请注意,在定义抽象模型上的关系时,必须设置此值;在这样做时,一些特殊的语法是可用的。如果您希望 Django 不创建反向关系,请将related_name设置为'+'或以'+'结尾。例如,这将确保User模型不会有到此模型的反向关系:

user = models.ForeignKey(User, related_name='+') 

用于从目标模型返回的反向过滤器名称的名称。如果设置了related_name,则默认为related_name的值,否则默认为模型的名称:

# Declare the ForeignKey with related_query_name 
class Tag(models.Model): 
    article = models.ForeignKey(Article, related_name="tags",
      related_query_name="tag") 
    name = models.CharField(max_length=255) 

# That's now the name of the reverse filter 
Article.objects.filter(tag__name="important") 

to_field

关系对象上的字段。默认情况下,Django 使用相关对象的主键。

db_constraint

控制是否应为此外键在数据库中创建约束。默认值为True,这几乎肯定是您想要的;将其设置为False可能对数据完整性非常不利。也就是说,有一些情况下您可能希望这样做:

  • 您有无效的旧数据。

  • 您正在对数据库进行分片。

如果设置为False,访问不存在的相关对象将引发其DoesNotExist异常。

删除时

当被ForeignKey引用的对象被删除时,Django 默认会模拟 SQL 约束ON DELETE CASCADE的行为,并删除包含ForeignKey的对象。可以通过指定on_delete参数来覆盖此行为。例如,如果您有一个可空的ForeignKey,并且希望在删除引用对象时将其设置为 null:

user = models.ForeignKey(User, blank=True, null=True, on_delete=models.SET_NULL) 

on_delete的可能值可以在django.db.models中找到:

  • CASCADE:级联删除;默认值

  • PROTECT:通过引发ProtectedErrordjango.db.IntegrityError的子类)来防止删除引用对象

  • SET_NULL:将ForeignKey设置为 null;只有在nullTrue时才可能

  • SET_DEFAULT:将ForeignKey设置为其默认值;必须设置ForeignKey的默认值

可交换

控制迁移框架对指向可交换模型的此ForeignKey的反应。如果为True-默认值-那么如果ForeignKey指向与当前settings.AUTH_USER_MODEL的值(或其他可交换模型设置)匹配的模型,则关系将在迁移中使用对设置的引用而不是直接对模型进行存储。

只有在确定模型应始终指向替换模型时才要将其覆盖为False,例如,如果它是专门为自定义用户模型设计的配置文件模型。将其设置为False并不意味着即使替换了模型,也可以引用可交换模型-False只是意味着使用此ForeignKey进行的迁移将始终引用您指定的确切模型(例如,如果用户尝试使用您不支持的用户模型,则会严重失败)。如果有疑问,请将其保留为默认值True

ManyToManyField

多对多关系。需要一个位置参数:模型相关的类,其工作方式与ForeignKey完全相同,包括递归和延迟关系。可以使用字段的RelatedManager添加、删除或创建相关对象。

数据库表示

在幕后,Django 创建一个中间连接表来表示多对多关系。默认情况下,此表名是使用多对多字段的名称和包含它的模型的表名生成的。

由于某些数据库不支持超过一定长度的表名,这些表名将自动截断为 64 个字符,并使用唯一性哈希。这意味着您可能会看到表名如author_books_9cdf4;这是完全正常的。您可以使用db_table选项手动提供连接表的名称。

参数

ManyToManyField接受一组额外的参数-全部是可选的-用于控制关系的功能。

ForeignKey.related_name相同。

ForeignKey.related_query_name相同。

limit_choices_to

ForeignKey.limit_choices_to相同。当在使用through参数指定自定义中间表的ManyToManyField上使用limit_choices_to时,limit_choices_to没有效果。

对称的

仅在自身的 ManyToManyFields 的定义中使用。考虑以下模型:

from django.db import models 

class Person(models.Model): 
    friends = models.ManyToManyField("self") 

当 Django 处理此模型时,它会识别出它在自身上有一个ManyToManyField,因此它不会向Person类添加person_set属性。相反,假定ManyToManyField是对称的-也就是说,如果我是你的朋友,那么你也是我的朋友。

如果不希望在self的多对多关系中具有对称性,请将symmetrical设置为False。这将强制 Django 添加反向关系的描述符,从而允许ManyToManyField关系不对称。

通过

Django 将自动生成一个表来管理多对多关系。但是,如果要手动指定中间表,可以使用through选项来指定表示要使用的中间表的 Django 模型。

此选项的最常见用法是当您想要将额外数据与多对多关系关联时。如果不指定显式的through模型,则仍然有一个隐式的through模型类,您可以使用它直接访问创建以保存关联的表。它有三个字段:

  • id:关系的主键

  • <containing_model>_id:声明ManyToManyField的模型的id

  • <other_model>_idManyToManyField指向的模型的id

此类可用于像普通模型一样查询给定模型实例的关联记录。

through_fields

仅在指定自定义中介模型时使用。Django 通常会确定中介模型的哪些字段以自动建立多对多关系。

db_table

用于存储多对多数据的表的名称。如果未提供此名称,Django 将基于定义关系的模型的表的名称和字段本身的名称假定默认名称。

db_constraint

控制是否应在中介表的外键在数据库中创建约束。默认值为True,这几乎肯定是您想要的;将其设置为False可能对数据完整性非常不利。

也就是说,以下是一些可能需要这样做的情况:

  • 您有不合法的遗留数据

  • 您正在对数据库进行分片

传递db_constraintthrough是错误的。

swappable

如果此ManyToManyField指向可交换模型,则控制迁移框架的反应。如果为True-默认值-如果ManyToManyField指向与settings.AUTH_USER_MODEL(或其他可交换模型设置)的当前值匹配的模型,则关系将存储在迁移中,使用对设置的引用,而不是直接对模型。

只有在确定模型应始终指向替换模型的情况下,才希望将其覆盖为False-例如,如果它是专门为自定义用户模型设计的配置文件模型。如果有疑问,请将其保留为默认值TrueManyToManyField不支持validatorsnull没有影响,因为没有办法在数据库级别要求关系。

OneToOneField

一对一关系。在概念上,这类似于具有unique=TrueForeignKey,但关系的反向侧将直接返回单个对象。这在作为模型的主键时最有用,该模型以某种方式扩展另一个模型;通过向子模型添加从子模型到父模型的隐式一对一关系来实现多表继承,例如。

需要一个位置参数:将与之相关的类。这与ForeignKey的工作方式完全相同,包括递归和延迟关系的所有选项。如果未为OneToOneField指定related_name参数,Django 将使用当前模型的小写名称作为默认值。使用以下示例:

from django.conf import settings 
from django.db import models 

class MySpecialUser(models.Model): 
    user = models.OneToOneField(settings.AUTH_USER_MODEL) 
    supervisor = models.OneToOneField(settings.AUTH_USER_MODEL, 
      related_name='supervisor_of') 

您的生成的User模型将具有以下属性:

>>> user = User.objects.get(pk=1)
>>> hasattr(user, 'myspecialuser')
True
>>> hasattr(user, 'supervisor_of')
True

当访问相关表中的条目不存在时,将引发DoesNotExist异常。例如,如果用户没有由MySpecialUser指定的主管:

>>> user.supervisor_of
Traceback (most recent call last):
 ...
DoesNotExist: User matching query does not exist.

此外,OneToOneField接受ForeignKey接受的所有额外参数,以及一个额外参数:

当在继承自另一个具体模型的模型中使用时,True表示应使用此字段作为返回到父类的链接,而不是通常通过子类隐式创建的额外OneToOneField。有关OneToOneField的用法示例,请参见下一章中的一对一关系

模型元数据选项

表 A.3是您可以在其内部class Meta中为模型提供的完整模型元选项列表。有关每个元选项的更多详细信息以及示例,请参阅 Django 文档docs.djangoproject.com/en/1.8/ref/models/options/

选项 说明
abstract 如果abstract = True,此模型将是一个抽象基类。
app_label 如果模型在INSTALLED_APPS之外定义,它必须声明属于哪个应用程序。
db_table 用于模型的数据库表的名称。
- db_tablespace 用于此模型的数据库表空间的名称。如果设置了项目的 DEFAULT_TABLESPACE 设置,则默认为该设置。如果后端不支持表空间,则忽略此选项。
- default_related_name 从相关对象返回到此对象的关系的默认名称。默认为 <model_name>_set
- get_latest_by 模型中可排序字段的名称,通常为 DateFieldDateTimeFieldIntegerField
- managed 默认为 True,意味着 Django 将在migrate或作为迁移的一部分中创建适当的数据库表,并在flush管理命令的一部分中删除它们。
- order_with_respect_to 标记此对象相对于给定字段是可排序的。
- ordering 对象的默认排序,用于获取对象列表时使用。
- permissions 创建此对象时要输入权限表的额外权限。
- default_permissions 默认为 ('add', 'change', 'delete')
- proxy 如果 proxy = True,则子类化另一个模型的模型将被视为代理模型。
- select_on_save 确定 Django 是否使用 pre-1.6 django.db.models.Model.save() 算法。
- unique_together 一起使用的字段集,必须是唯一的。
- index_together 一起使用的字段集,被索引。
- verbose_name 对象的可读名称,单数形式。
- verbose_name_plural 对象的复数名称。

表 A.3:模型元数据选项

附录 B.数据库 API 参考

Django 的数据库 API 是附录 A 中讨论的模型 API 的另一半。一旦定义了模型,您将在需要访问数据库时使用此 API。您已经在整本书中看到了此 API 的使用示例;本附录详细解释了各种选项。

在本附录中,我将引用以下模型,这些模型组成了一个 Weblog 应用程序:

from django.db import models 

class Blog(models.Model): 
    name = models.CharField(max_length=100) 
    tagline = models.TextField() 

    def __str__(self): 
        return self.name 

class Author(models.Model): 
    name = models.CharField(max_length=50) 
    email = models.EmailField() 

    def __str__(self): 
        return self.name 

class Entry(models.Model): 
    blog = models.ForeignKey(Blog) 
    headline = models.CharField(max_length=255) 
    body_text = models.TextField() 
    pub_date = models.DateField() 
    mod_date = models.DateField() 
    authors = models.ManyToManyField(Author) 
    n_comments = models.IntegerField() 
    n_pingbacks = models.IntegerField() 
    rating = models.IntegerField() 

    def __str__(self):        
        return self.headline 

创建对象

为了在 Python 对象中表示数据库表数据,Django 使用了一个直观的系统:模型类表示数据库表,该类的实例表示数据库表中的特定记录。

要创建对象,请使用模型类的关键字参数进行实例化,然后调用save()将其保存到数据库中。

假设模型位于文件mysite/blog/models.py中,这是一个示例:

>>> from blog.models import Blog
>>> b = Blog(name='Beatles Blog', tagline='All the latest Beatles news.')
>>> b.save()

这在幕后执行INSERT SQL 语句。直到您明确调用save()之前,Django 不会访问数据库。

save()方法没有返回值。

要在单个步骤中创建和保存对象,请使用create()方法。

保存对象的更改

要保存已经在数据库中的对象的更改,请使用save()

假设已经将Blog实例b5保存到数据库中,此示例更改其名称并更新数据库中的记录:

>>> b5.name = 'New name'
>>> b5.save()

这在幕后执行UPDATE SQL 语句。Django 直到您明确调用save()之前才会访问数据库。

保存 ForeignKey 和 ManyToManyField 字段

更新ForeignKey字段的方式与保存普通字段的方式完全相同-只需将正确类型的对象分配给相关字段。此示例更新了Entry实例entryblog属性,假设已经适当保存了EntryBlog的实例到数据库中(因此我们可以在下面检索它们):

>>> from blog.models import Entry
>>> entry = Entry.objects.get(pk=1)
>>> cheese_blog = Blog.objects.get(name="Cheddar Talk")
>>> entry.blog = cheese_blog
>>> entry.save()

更新ManyToManyField的方式略有不同-使用字段上的add()方法将记录添加到关系中。此示例将Author实例joe添加到entry对象中:

>>> from blog.models import Author
>>> joe = Author.objects.create(name="Joe")
>>> entry.authors.add(joe)

要一次向ManyToManyField添加多条记录,请在调用add()时包含多个参数,如下所示:

>>> john = Author.objects.create(name="John")
>>> paul = Author.objects.create(name="Paul")
>>> george = Author.objects.create(name="George")
>>> ringo = Author.objects.create(name="Ringo")
>>> entry.authors.add(john, paul, george, ringo)

如果尝试分配或添加错误类型的对象,Django 会发出警告。

检索对象

要从数据库中检索对象,请通过模型类上的Manager构建QuerySet

QuerySet表示来自数据库的对象集合。它可以有零个、一个或多个过滤器。过滤器根据给定的参数缩小查询结果。在 SQL 术语中,QuerySet等同于SELECT语句,而过滤器是诸如WHERELIMIT的限制子句。

通过使用模型的Manager来获取QuerySet。每个模型至少有一个Manager,默认情况下称为objects。直接通过模型类访问它,就像这样:

>>> Blog.objects
<django.db.models.manager.Manager object at ...>
>>> b = Blog(name='Foo', tagline='Bar')
>>> b.objects
Traceback:
 ...
AttributeError: "Manager isn't accessible via Blog instances."

检索所有对象

从表中检索对象的最简单方法是获取所有对象。要做到这一点,使用Manager上的all()方法:

>>> all_entries = Entry.objects.all()

all()方法返回数据库中所有对象的QuerySet

使用过滤器检索特定对象

all()返回的QuerySet描述了数据库表中的所有对象。通常,您需要选择完整对象集的子集。

要创建这样的子集,您需要细化初始的QuerySet,添加过滤条件。细化QuerySet的两种最常见的方法是:

  • filter(**kwargs)。返回一个包含匹配给定查找参数的对象的新QuerySet

  • exclude(**kwargs)。返回一个包含不匹配给定查找参数的对象的新QuerySet

查找参数(上述函数定义中的**kwargs)应该以本章后面描述的字段查找格式。

链接过滤器

细化QuerySet的结果本身是一个QuerySet,因此可以将细化链接在一起。例如:

>>> Entry.objects.filter(
...     headline__startswith='What'
... ).exclude(
...     pub_date__gte=datetime.date.today()
... ).filter(pub_date__gte=datetime(2005, 1, 30)
... )

这需要数据库中所有条目的初始QuerySet,添加一个过滤器,然后一个排除,然后另一个过滤器。最终结果是一个包含所有以What开头的标题的条目,发布日期在 2005 年 1 月 30 日和当天之间的QuerySet

过滤的查询集是唯一的

每次细化QuerySet,您都会得到一个全新的QuerySet,它与以前的QuerySet没有任何关联。每次细化都会创建一个单独且独特的QuerySet,可以存储、使用和重复使用。

例子:

>>> q1 = Entry.objects.filter(headline__startswith="What")
>>> q2 = q1.exclude(pub_date__gte=datetime.date.today())
>>> q3 = q1.filter(pub_date__gte=datetime.date.today())

这三个QuerySets是独立的。第一个是一个基本的QuerySet,包含所有以 What 开头的标题的条目。第二个是第一个的子集,增加了一个额外的条件,排除了pub_date是今天或将来的记录。第三个是第一个的子集,增加了一个额外的条件,只选择pub_date是今天或将来的记录。初始的QuerySetq1)不受细化过程的影响。

QuerySets 是惰性的

QuerySets是惰性的-创建QuerySet的行为不涉及任何数据库活动。您可以整天堆叠过滤器,Django 实际上不会运行查询,直到QuerySet评估。看看这个例子:

>>> q = Entry.objects.filter(headline__startswith="What")
>>> q = q.filter(pub_date__lte=datetime.date.today())
>>> q = q.exclude(body_text__icontains="food")
>>> print(q)

尽管看起来像是三次数据库访问,实际上只有一次,在最后一行(print(q))访问数据库。通常情况下,只有在要求时,QuerySet的结果才会从数据库中获取。当您这样做时,通过访问数据库来评估QuerySet

使用 get 检索单个对象

filter()总是会给你一个QuerySet,即使只有一个对象匹配查询-在这种情况下,它将是包含单个元素的QuerySet

如果您知道只有一个对象与您的查询匹配,您可以在Manager上使用get()方法直接返回对象:

>>> one_entry = Entry.objects.get(pk=1)

您可以像使用filter()一样使用get()的任何查询表达式-再次参见本章的下一节中的字段查找

请注意,使用get()和使用filter()[0]的切片之间存在差异。如果没有结果与查询匹配,get()将引发DoesNotExist异常。此异常是正在执行查询的模型类的属性-因此在上面的代码中,如果没有主键为 1 的Entry对象,Django 将引发Entry.DoesNotExist

类似地,如果get()查询匹配多个项目,Django 将抱怨。在这种情况下,它将引发MultipleObjectsReturned,这也是模型类本身的属性。

其他查询集方法

大多数情况下,当您需要从数据库中查找对象时,您将使用all()get()filter()exclude()。但这远非全部;请参阅docs.djangoproject.com/en/1.8/ref/models/querysets/上的 QuerySet API 参考,了解所有各种QuerySet方法的完整列表。

限制查询集

使用 Python 的数组切片语法的子集来限制您的QuerySet到一定数量的结果。这相当于 SQL 的LIMITOFFSET子句。

例如,这将返回前 5 个对象(LIMIT 5):

>>> Entry.objects.all()[:5]

这将返回第六到第十个对象(OFFSET 5 LIMIT 5):

>>> Entry.objects.all()[5:10]

不支持负索引(即Entry.objects.all()[-1])。

通常,对QuerySet进行切片会返回一个新的QuerySet-它不会评估查询。一个例外是如果您使用 Python 切片语法的步长参数。例如,这实际上会执行查询,以返回前 10 个对象中每第二个对象的列表:

>>> Entry.objects.all()[:10:2]

要检索单个对象而不是列表(例如,SELECT foo FROM bar LIMIT 1),请使用简单的索引而不是切片。

例如,这将按标题字母顺序返回数据库中的第一个Entry

>>> Entry.objects.order_by('headline')[0]

这大致相当于:

>>> Entry.objects.order_by('headline')[0:1].get()

但是请注意,如果没有对象符合给定的条件,第一个将引发IndexError,而第二个将引发DoesNotExist。有关更多详细信息,请参见get()

字段查找

字段查找是指定 SQL WHERE子句的主要方式。它们被指定为QuerySet方法filter()exclude()get()的关键字参数。(这是一个双下划线)。例如:

>>> Entry.objects.filter(pub_date__lte='2006-01-01')

翻译(大致)成以下 SQL:

SELECT * FROM blog_entry WHERE pub_date <= '2006-01-01';

查找中指定的字段必须是模型字段的名称。不过有一个例外,在ForeignKey的情况下,可以指定带有_id后缀的字段名。在这种情况下,值参数预期包含外键模型主键的原始值。例如:

>>> Entry.objects.filter(blog_id=4)

如果传递了无效的关键字参数,查找函数将引发TypeError

字段查找的完整列表如下:

  • 精确的

  • 忽略大小写的精确的

  • 包含

  • 包含

  • 在…中

  • 大于

  • 大于或等于

  • 小于

  • 小于或等于

  • 以…开头

  • 以…开头

  • 以…结尾

  • 以…结尾

  • 范围

  • 星期几

  • 小时

  • 分钟

  • 为空

  • 搜索

  • 正则表达式

  • iregex

可以在字段查找参考中找到每个字段查找的完整参考和示例docs.djangoproject.com/en/1.8/ref/models/querysets/#field-lookups

跨关系的查找

Django 提供了一种强大且直观的方式来在查找中跟踪关系,自动在幕后为您处理 SQL JOIN。要跨越关系,只需使用跨模型的相关字段的字段名称,用双下划线分隔,直到您找到想要的字段。

这个例子检索所有name'Beatles Blog'Blog对象的Entry对象:

>>> Entry.objects.filter(blog__name='Beatles Blog')

这种跨度可以深入到您想要的程度。

它也可以反向操作。要引用反向关系,只需使用模型的小写名称。

这个例子检索所有至少有一个Entryheadline包含'Lennon'Blog对象:

>>> Blog.objects.filter(entry__headline__contains='Lennon')

如果您在多个关系中进行过滤,并且中间模型之一没有满足过滤条件的值,Django 将把它视为一个空(所有值都为NULL),但有效的对象。这只意味着不会引发错误。例如,在这个过滤器中:

Blog.objects.filter(entry__authors__name='Lennon') 

(如果有一个相关的Author模型),如果一个条目没有与作者相关联,它将被视为没有附加名称,而不是因为缺少作者而引发错误。通常这正是您希望发生的。唯一可能令人困惑的情况是如果您使用isnull。因此:

Blog.objects.filter(entry__authors__name__isnull=True) 

将返回在author上有一个空的nameBlog对象,以及在entry上有一个空的authorBlog对象。如果您不想要后者的对象,您可以这样写:

Blog.objects.filter(entry__authors__isnull=False, 
        entry__authors__name__isnull=True) 

跨多值关系

当您基于ManyToManyField或反向ForeignKey对对象进行过滤时,可能会对两种不同类型的过滤感兴趣。考虑Blog/Entry关系(BlogEntry是一对多关系)。我们可能对找到有一个条目既在标题中有Lennon又在 2008 年发布的博客感兴趣。

或者我们可能想要找到博客中有一个标题中带有Lennon的条目以及一个在 2008 年发布的条目。由于一个Blog关联多个条目,这两个查询都是可能的,并且在某些情况下是有意义的。

ManyToManyField相同类型的情况也会出现。例如,如果Entry有一个名为tagsManyToManyField,我们可能想要找到链接到名称为musicbands的标签的条目,或者我们可能想要一个包含名称为music和状态为public的标签的条目。

为了处理这两种情况,Django 有一种一致的处理filter()exclude()调用的方式。单个filter()调用中的所有内容同时应用于过滤掉符合所有这些要求的项目。

连续的filter()调用进一步限制对象集,但对于多值关系,它们适用于与主要模型链接的任何对象,不一定是由先前的filter()调用选择的对象。

这可能听起来有点混乱,所以希望通过一个例子来澄清。要选择包含标题中都有Lennon并且在 2008 年发布的条目的所有博客(同时满足这两个条件的相同条目),我们将写:

Blog.objects.filter(entry__headline__contains='Lennon',
        entry__pub_date__year=2008) 

要选择包含标题中有Lennon的条目以及 2008 年发布的条目的所有博客,我们将写:

Blog.objects.filter(entry__headline__contains='Lennon').filter(
        entry__pub_date__year=2008) 

假设只有一个博客既包含Lennon的条目,又包含 2008 年的条目,但 2008 年的条目中没有包含Lennon。第一个查询将不会返回任何博客,但第二个查询将返回那一个博客。

在第二个例子中,第一个过滤器将查询集限制为所有链接到标题中有Lennon的条目的博客。第二个过滤器将进一步将博客集限制为那些还链接到 2008 年发布的条目的博客。

第二个过滤器选择的条目可能与第一个过滤器中的条目相同,也可能不同。我们正在使用每个过滤器语句过滤Blog项,而不是Entry项。

所有这些行为也适用于exclude():单个exclude()语句中的所有条件都适用于单个实例(如果这些条件涉及相同的多值关系)。后续filter()exclude()调用中涉及相同关系的条件可能最终会过滤不同的链接对象。

过滤器可以引用模型上的字段

到目前为止给出的例子中,我们已经构建了比较模型字段值与常量的过滤器。但是,如果您想要比较模型字段的值与同一模型上的另一个字段呢?

Django 提供了F 表达式来允许这样的比较。F()的实例充当查询中模型字段的引用。然后可以在查询过滤器中使用这些引用来比较同一模型实例上两个不同字段的值。

例如,要查找所有博客条目中评论比 pingbacks 多的条目列表,我们构建一个F()对象来引用 pingback 计数,并在查询中使用该F()对象:

>>> from django.db.models import F
>>> Entry.objects.filter(n_comments__gt=F('n_pingbacks'))

Django 支持使用F()对象进行加法、减法、乘法、除法、取模和幂运算,既可以与常量一起使用,也可以与其他F()对象一起使用。要查找所有评论比 pingbacks 多两倍的博客条目,我们修改查询:

>>> Entry.objects.filter(n_comments__gt=F('n_pingbacks') * 2)

要查找所有评分小于 pingback 计数和评论计数之和的条目,我们将发出查询:

>>> Entry.objects.filter(rating__lt=F('n_comments') + F('n_pingbacks'))

您还可以使用双下划线符号来跨越F()对象中的关系。带有双下划线的F()对象将引入访问相关对象所需的任何连接。

例如,要检索所有作者名称与博客名称相同的条目,我们可以发出查询:

>>> Entry.objects.filter(authors__name=F('blog__name'))

对于日期和日期/时间字段,您可以添加或减去一个timedelta对象。以下将返回所有在发布后 3 天以上修改的条目:

>>> from datetime import timedelta
>>> Entry.objects.filter(mod_date__gt=F('pub_date') + timedelta(days=3))

F()对象支持按位操作,通过.bitand().bitor(),例如:

>>> F('somefield').bitand(16)

pk 查找快捷方式

为了方便起见,Django 提供了一个pk查找快捷方式,代表主键。

在示例Blog模型中,主键是id字段,因此这三个语句是等价的:

>>> Blog.objects.get(id__exact=14) # Explicit form
>>> Blog.objects.get(id=14) # __exact is implied
>>> Blog.objects.get(pk=14) # pk implies id__exact

pk的使用不限于__exact查询-任何查询条件都可以与pk组合,以对模型的主键执行查询:

# Get blogs entries with id 1, 4 and 7
>>> Blog.objects.filter(pk__in=[1,4,7])
# Get all blog entries with id > 14
>>> Blog.objects.filter(pk__gt=14)

pk查找也适用于连接。例如,这三个语句是等价的:

>>> Entry.objects.filter(blog__id__exact=3) # Explicit form
>>> Entry.objects.filter(blog__id=3)        # __exact is implied
>>> Entry.objects.filter(blog__pk=3)        # __pk implies __id__exact

在 LIKE 语句中转义百分号和下划线

等同于LIKE SQL 语句的字段查找(iexactcontainsicontainsstartswithistartswithendswithiendswith)将自动转义LIKE语句中使用的两个特殊字符-百分号和下划线。(在LIKE语句中,百分号表示多字符通配符,下划线表示单字符通配符。)

这意味着事情应该直观地工作,所以抽象不会泄漏。例如,要检索包含百分号的所有条目,只需像对待其他字符一样使用百分号:

>>> Entry.objects.filter(headline__contains='%')

Django 会为您处理引用;生成的 SQL 将类似于这样:

SELECT ... WHERE headline LIKE '%\%%';

下划线也是一样。百分号和下划线都会被透明地处理。

缓存和查询集

每个QuerySet都包含一个缓存,以最小化数据库访问。了解它的工作原理将使您能够编写最有效的代码。

在新创建的QuerySet中,缓存是空的。第一次评估QuerySet时-因此,数据库查询发生时-Django 会将查询结果保存在QuerySet类的缓存中,并返回已经明确请求的结果(例如,如果正在迭代QuerySet,则返回下一个元素)。后续的QuerySet评估将重用缓存的结果。

请记住这种缓存行为,因为如果您没有正确使用您的QuerySet,它可能会给您带来麻烦。例如,以下操作将创建两个QuerySet,对它们进行评估,然后丢弃它们:

>>> print([e.headline for e in Entry.objects.all()])
>>> print([e.pub_date for e in Entry.objects.all()])

这意味着相同的数据库查询将被执行两次,有效地增加了数据库负载。此外,两个列表可能不包括相同的数据库记录,因为在两个请求之间的瞬间,可能已经添加或删除了Entry

为了避免这个问题,只需保存QuerySet并重复使用它:

>>> queryset = Entry.objects.all()
>>> print([p.headline for p in queryset]) # Evaluate the query set.
>>> print([p.pub_date for p in queryset]) # Re-use the cache from the evaluation.

当查询集没有被缓存时

查询集并不总是缓存它们的结果。当仅评估查询集的部分时,会检查缓存,但如果它没有被填充,那么后续查询返回的项目将不会被缓存。具体来说,这意味着使用数组切片或索引限制查询集将不会填充缓存。

例如,重复获取查询集对象中的某个索引将每次查询数据库:

>>> queryset = Entry.objects.all()
>>> print queryset[5] # Queries the database
>>> print queryset[5] # Queries the database again

然而,如果整个查询集已经被评估,那么将检查缓存:

>>> queryset = Entry.objects.all()
>>> [entry for entry in queryset] # Queries the database
>>> print queryset[5] # Uses cache
>>> print queryset[5] # Uses cache

以下是一些其他操作的例子,这些操作将导致整个查询集被评估,因此填充缓存:

>>> [entry for entry in queryset]
>>> bool(queryset)
>>> entry in queryset
>>> list(queryset)

使用 Q 对象进行复杂的查找

关键字参数查询-在filter()和其他地方-会被 AND 在一起。如果您需要执行更复杂的查询(例如带有OR语句的查询),您可以使用Q 对象

Q 对象django.db.models.Q)是一个用于封装一组关键字参数的对象。这些关键字参数如上面的字段查找中所指定的那样。

例如,这个Q对象封装了一个单一的LIKE查询:

from django.db.models import Q 
Q(question__startswith='What') 

Q对象可以使用&|运算符进行组合。当两个Q对象上使用运算符时,它会产生一个新的Q对象。

例如,这个语句产生一个代表两个"question__startswith"查询的 OR 的单个Q对象:

Q(question__startswith='Who') | Q(question__startswith='What') 

这等同于以下 SQL WHERE子句:

WHERE question LIKE 'Who%' OR question LIKE 'What%'

您可以通过使用&|运算符组合Q对象并使用括号分组来组成任意复杂的语句。此外,Q对象可以使用~运算符进行否定,从而允许组合查找结合了正常查询和否定(NOT)查询:

Q(question__startswith='Who') | ~Q(pub_date__year=2005) 

每个接受关键字参数的查找函数(例如filter()exclude()get())也可以作为位置(非命名)参数传递一个或多个Q对象。如果向查找函数提供多个Q对象参数,则这些参数将被 AND 在一起。例如:

Poll.objects.get( 
    Q(question__startswith='Who'), 
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)) 
) 

...大致翻译成 SQL:

SELECT * from polls WHERE question LIKE 'Who%'
 AND (pub_date = '2005-05-02' OR pub_date = '2005-05-06')

查找函数可以混合使用Q对象和关键字参数。提供给查找函数的所有参数(无论是关键字参数还是Q对象)都会被 AND 在一起。但是,如果提供了Q对象,它必须在任何关键字参数的定义之前。例如:

Poll.objects.get( 
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)), 
    question__startswith='Who') 

...将是一个有效的查询,等同于前面的示例;但是:

# INVALID QUERY 
Poll.objects.get( 
    question__startswith='Who', 
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6))) 

...将无效。

比较对象

要比较两个模型实例,只需使用标准的 Python 比较运算符,双等号:==。在幕后,这比较了两个模型的主键值。

使用上面的Entry示例,以下两个语句是等价的:

>>> some_entry == other_entry
>>> some_entry.id == other_entry.id

如果模型的主键不叫id,没问题。比较将始终使用主键,无论它叫什么。例如,如果模型的主键字段叫name,这两个语句是等价的:

>>> some_obj == other_obj
>>> some_obj.name == other_obj.name

删除对象

方便地,删除方法被命名为delete()。此方法立即删除对象,并且没有返回值。例如:

e.delete() 

您还可以批量删除对象。每个QuerySet都有一个delete()方法,用于删除该QuerySet的所有成员。

例如,这将删除所有pub_date年份为 2005 的Entry对象:

Entry.objects.filter(pub_date__year=2005).delete() 

请记住,这将在可能的情况下纯粹在 SQL 中执行,因此在过程中不一定会调用单个对象实例的delete()方法。如果您在模型类上提供了自定义的delete()方法,并希望确保它被调用,您将需要手动删除该模型的实例(例如,通过迭代QuerySet并在每个对象上调用delete())而不是使用QuerySet的批量delete()方法。

当 Django 删除一个对象时,默认情况下会模拟 SQL 约束ON DELETE CASCADE的行为-换句话说,任何具有指向要删除的对象的外键的对象都将与其一起被删除。例如:

b = Blog.objects.get(pk=1) 
# This will delete the Blog and all of its Entry objects. 
b.delete() 

此级联行为可以通过ForeignKeyon_delete参数进行自定义。

请注意,delete()是唯一不在Manager本身上公开的QuerySet方法。这是一个安全机制,可以防止您意外请求Entry.objects.delete(),并删除所有条目。如果确实要删除所有对象,则必须显式请求完整的查询集:

Entry.objects.all().delete() 

复制模型实例

虽然没有内置的方法来复制模型实例,但可以轻松地创建具有所有字段值的新实例。在最简单的情况下,您可以将pk设置为None。使用我们的博客示例:

blog = Blog(name='My blog', tagline='Blogging is easy') 
blog.save() # blog.pk == 1 

blog.pk = None 
blog.save() # blog.pk == 2 

如果使用继承,情况会变得更加复杂。考虑Blog的子类:

class ThemeBlog(Blog): 
    theme = models.CharField(max_length=200) 

django_blog = ThemeBlog(name='Django', tagline='Django is easy',
  theme='python') 
django_blog.save() # django_blog.pk == 3 

由于继承的工作原理,您必须将pkid都设置为 None:

django_blog.pk = None 
django_blog.id = None 
django_blog.save() # django_blog.pk == 4 

此过程不会复制相关对象。如果要复制关系,您需要编写更多的代码。在我们的示例中,Entry有一个到Author的多对多字段:

entry = Entry.objects.all()[0] # some previous entry 
old_authors = entry.authors.all() 
entry.pk = None 
entry.save() 
entry.authors = old_authors # saves new many2many relations 

一次更新多个对象

有时,您希望为QuerySet中的所有对象设置一个特定的值。您可以使用update()方法来实现这一点。例如:

# Update all the headlines with pub_date in 2007.
Entry.objects.filter(pub_date__year=2007).update(headline='Everything is the same')

您只能使用此方法设置非关系字段和ForeignKey字段。要更新非关系字段,请将新值提供为常量。要更新ForeignKey字段,请将新值设置为要指向的新模型实例。例如:

>>> b = Blog.objects.get(pk=1)
# Change every Entry so that it belongs to this Blog.
>>> Entry.objects.all().update(blog=b)

update()方法会立即应用,并返回查询匹配的行数(如果某些行已经具有新值,则可能不等于更新的行数)。

更新的QuerySet的唯一限制是它只能访问一个数据库表,即模型的主表。您可以基于相关字段进行过滤,但只能更新模型主表中的列。例如:

>>> b = Blog.objects.get(pk=1)
# Update all the headlines belonging to this Blog.
>>> Entry.objects.select_related().filter(blog=b).update
(headline='Everything is the same')

请注意,update()方法会直接转换为 SQL 语句。这是一个用于直接更新的批量操作。它不会运行任何模型的save()方法,也不会发出pre_savepost_save信号(这是调用save()的结果),也不会遵守auto_now字段选项。如果您想保存QuerySet中的每个项目,并确保在每个实例上调用save()方法,您不需要任何特殊的函数来处理。只需循环遍历它们并调用save()

for item in my_queryset: 
    item.save() 

对更新的调用也可以使用F 表达式来根据模型中另一个字段的值更新一个字段。这对于根据其当前值递增计数器特别有用。例如,要为博客中的每个条目递增 pingback 计数:

>>> Entry.objects.all().update(n_pingbacks=F('n_pingbacks') + 1)

但是,与在过滤和排除子句中使用F()对象不同,当您在更新中使用F()对象时,您不能引入连接-您只能引用要更新的模型本地字段。如果尝试使用F()对象引入连接,将引发FieldError

# THIS WILL RAISE A FieldError
>>> Entry.objects.update(headline=F('blog__name'))

相关对象

当您在模型中定义关系(即ForeignKeyOneToOneFieldManyToManyField)时,该模型的实例将具有便捷的 API 来访问相关对象。

使用本页顶部的模型,例如,Entry对象e可以通过访问blog属性获取其关联的Blog对象:e.blog

(在幕后,这个功能是由 Python 描述符实现的。这对您来说并不重要,但我在这里指出它是为了满足好奇心。)

Django 还为关系的另一侧创建了 API 访问器-从相关模型到定义关系的模型的链接。例如,Blog对象b通过entry_set属性可以访问所有相关的Entry对象的列表:b.entry_set.all()

本节中的所有示例都使用本页顶部定义的示例BlogAuthorEntry模型。

一对多关系

前向

如果模型具有ForeignKey,则该模型的实例将可以通过模型的简单属性访问相关(外键)对象。例如:

>>> e = Entry.objects.get(id=2)
>>> e.blog # Returns the related Blog object.

您可以通过外键属性进行获取和设置。正如您可能期望的那样,对外键的更改直到调用save()之前都不会保存到数据库。例如:

>>> e = Entry.objects.get(id=2)
>>> e.blog = some_blog
>>> e.save()

如果ForeignKey字段设置了null=True(即允许NULL值),则可以分配None来删除关系。例如:

>>> e = Entry.objects.get(id=2)
>>> e.blog = None
>>> e.save() # "UPDATE blog_entry SET blog_id = NULL ...;"

第一次访问相关对象时,可以缓存对一对多关系的前向访问。对同一对象实例上的外键的后续访问将被缓存。例如:

>>> e = Entry.objects.get(id=2)
>>> print(e.blog)  # Hits the database to retrieve the associated Blog.
>>> print(e.blog)  # Doesn't hit the database; uses cached version.

请注意,select_related() QuerySet 方法会预先递归填充所有一对多关系的缓存。例如:

>>> e = Entry.objects.select_related().get(id=2)
>>> print(e.blog)  # Doesn't hit the database; uses cached version.
>>> print(e.blog)  # Doesn't hit the database; uses cached version.

向后跟踪关系

如果模型具有ForeignKey,则外键模型的实例将可以访问返回第一个模型的所有实例的Manager。默认情况下,此Manager命名为foo_set,其中foo是源模型名称的小写形式。此Manager返回QuerySets,可以像上面的检索对象部分中描述的那样进行过滤和操作。

例如:

>>> b = Blog.objects.get(id=1)
>>> b.entry_set.all() # Returns all Entry objects related to Blog.
# b.entry_set is a Manager that returns QuerySets.
>>> b.entry_set.filter(headline__contains='Lennon')
>>> b.entry_set.count()

您可以通过在ForeignKey定义中设置related_name参数来覆盖foo_set名称。例如,如果Entry模型被修改为blog = ForeignKey(Blog, related_name='entries'),上面的示例代码将如下所示:

>>> b = Blog.objects.get(id=1)
>>> b.entries.all() # Returns all Entry objects related to Blog.
# b.entries is a Manager that returns QuerySets.
>>> b.entries.filter(headline__contains='Lennon')
>>> b.entries.count()

使用自定义反向管理器

默认情况下,用于反向关系的RelatedManager是该模型的默认管理器的子类。如果您想为给定查询指定不同的管理器,可以使用以下语法:

from django.db import models 

class Entry(models.Model): 
    #... 
    objects = models.Manager()  # Default Manager 
    entries = EntryManager()    # Custom Manager 

b = Blog.objects.get(id=1) 
b.entry_set(manager='entries').all() 

如果EntryManager在其get_queryset()方法中执行默认过滤,则该过滤将应用于all()调用。

当然,指定自定义的反向管理器也使您能够调用其自定义方法:

b.entry_set(manager='entries').is_published() 

处理相关对象的附加方法

除了之前检索对象中定义的QuerySet方法之外,ForeignKey Manager还有其他用于处理相关对象集合的方法。每个方法的概要如下(完整详情可以在相关对象参考中找到docs.djangoproject.com/en/1.8/ref/models/relations/#related-objects-reference):

  • add(obj1, obj2, ...) 将指定的模型对象添加到相关对象集合

  • create(**kwargs) 创建一个新对象,保存它并将其放入相关对象集合中。返回新创建的对象

  • remove(obj1, obj2, ...) 从相关对象集合中删除指定的模型对象

  • clear() 从相关对象集合中删除所有对象

  • set(objs) 替换相关对象的集合

要一次性分配相关集合的成员,只需从任何可迭代对象中分配给它。可迭代对象可以包含对象实例,也可以只是主键值的列表。例如:

b = Blog.objects.get(id=1) 
b.entry_set = [e1, e2] 

在这个例子中,e1e2可以是完整的 Entry 实例,也可以是整数主键值。

如果clear()方法可用,那么在将可迭代对象(在本例中是一个列表)中的所有对象添加到集合之前,entry_set中的任何现有对象都将被移除。如果clear()方法可用,则将添加可迭代对象中的所有对象,而不会移除任何现有元素。

本节中描述的每个反向操作都会立即对数据库产生影响。每次添加、创建和删除都会立即自动保存到数据库中。

多对多关系

多对多关系的两端都自动获得对另一端的 API 访问权限。API 的工作方式与上面的反向一对多关系完全相同。

唯一的区别在于属性命名:定义ManyToManyField的模型使用该字段本身的属性名称,而反向模型使用原始模型的小写模型名称,再加上'_set'(就像反向一对多关系一样)。

一个例子可以更容易理解:

e = Entry.objects.get(id=3) 
e.authors.all() # Returns all Author objects for this Entry. 
e.authors.count() 
e.authors.filter(name__contains='John') 

a = Author.objects.get(id=5) 
a.entry_set.all() # Returns all Entry objects for this Author. 

ForeignKey一样,ManyToManyField可以指定related_name。在上面的例子中,如果Entry中的ManyToManyField指定了related_name='entries',那么每个Author实例将具有一个entries属性,而不是entry_set

一对一关系

一对一关系与多对一关系非常相似。如果在模型上定义了OneToOneField,那么该模型的实例将通过模型的简单属性访问相关对象。

例如:

class EntryDetail(models.Model): 
    entry = models.OneToOneField(Entry) 
    details = models.TextField() 

ed = EntryDetail.objects.get(id=2) 
ed.entry # Returns the related Entry object. 

不同之处在于反向查询。一对一关系中的相关模型也可以访问Manager对象,但该Manager代表单个对象,而不是一组对象:

e = Entry.objects.get(id=2) 
e.entrydetail # returns the related EntryDetail object 

如果没有对象分配给这个关系,Django 将引发DoesNotExist异常。

实例可以被分配到反向关系,就像你分配正向关系一样:

e.entrydetail = ed 

涉及相关对象的查询

涉及相关对象的查询遵循涉及正常值字段的查询相同的规则。在指定要匹配的查询值时,您可以使用对象实例本身,也可以使用对象的主键值。

例如,如果您有一个id=5的 Blog 对象b,那么以下三个查询将是相同的:

Entry.objects.filter(blog=b) # Query using object instance 
Entry.objects.filter(blog=b.id) # Query using id from instance 
Entry.objects.filter(blog=5) # Query using id directly 

回退到原始 SQL

如果你发现自己需要编写一个对 Django 的数据库映射器处理过于复杂的 SQL 查询,你可以回退到手动编写 SQL。

最后,重要的是要注意,Django 数据库层只是与您的数据库交互的接口。您可以通过其他工具、编程语言或数据库框架访问您的数据库;您的数据库与 Django 无关。

附录 C. 通用视图参考

第十章 通用视图介绍了通用视图,但略去了一些细节。本附录描述了每个通用视图以及每个视图可以采用的选项摘要。在尝试理解接下来的参考资料之前,请务必阅读第十章 通用视图。您可能希望参考该章中定义的BookPublisherAuthor对象;后面的示例使用这些模型。如果您想深入了解更高级的通用视图主题(例如在基于类的视图中使用混合),请参阅 Django 项目网站docs.djangoproject.com/en/1.8/topics/class-based-views/

通用视图的常见参数

这些视图大多需要大量的参数,可以改变通用视图的行为。这些参数中的许多在多个视图中起着相同的作用。表 C.1描述了每个这些常见参数;每当您在通用视图的参数列表中看到这些参数时,它将按照表中描述的方式工作。

参数 描述
allow_empty 一个布尔值,指定是否在没有可用对象时显示页面。如果这是False并且没有可用对象,则视图将引发 404 错误,而不是显示空页面。默认情况下,这是True
context_processors 要应用于视图模板的附加模板上下文处理器(除了默认值)的列表。有关模板上下文处理器的信息,请参见第九章 高级模型
extra_context 要添加到模板上下文中的值的字典。默认情况下,这是一个空字典。如果字典中的值是可调用的,则通用视图将在呈现模板之前调用它。
mimetype 用于生成文档的 MIME 类型。如果您没有更改它,默认为DEFAULT_MIME_TYPE设置的值,即text/html
queryset 从中读取对象的QuerySet(例如Author.objects.all())。有关QuerySet对象的更多信息,请参见附录 B。大多数通用视图都需要此参数。
template_loader 加载模板时要使用的模板加载程序。默认情况下是django.template.loader。有关模板加载程序的信息,请参见第九章 高级模型
template_name 用于呈现页面的模板的完整名称。这使您可以覆盖从QuerySet派生的默认模板名称。
template_object_name 模板上下文中要使用的模板变量的名称。默认情况下,这是'object'。列出多个对象的视图(即object_list视图和各种日期对象视图)将在此参数的值后附加'_list'

表 C.1:常见的通用视图参数

简单的通用视图

模块django.views.generic.base包含处理一些常见情况的简单视图:在不需要视图逻辑时呈现模板和发出重定向。

呈现模板-TemplateView

此视图呈现给定模板,传递一个包含在 URL 中捕获的关键字参数的上下文。

示例:

给定以下 URLconf:

from django.conf.urls import url 

    from myapp.views import HomePageView 

    urlpatterns = [ 
        url(r'^$', HomePageView.as_view(), name='home'), 
    ] 

和一个示例views.py

from django.views.generic.base import TemplateView 
from articles.models import Article 

class HomePageView(TemplateView): 

    template_name = "home.html" 

    def get_context_data(self, **kwargs): 
        context = super(HomePageView, self).get_context_data(**kwargs) 
        context['latest_articles'] = Article.objects.all()[:5] 
        return context 

/的请求将呈现模板home.html,返回一个包含前 5 篇文章列表的上下文。

重定向到另一个 URL

django.views.generic.base.RedirectView()将重定向到给定的 URL。

给定的 URL 可能包含类似字典的字符串格式,它将根据在 URL 中捕获的参数进行插值。因为关键字插值总是会执行(即使没有传入参数),所以 URL 中的任何"%"字符必须写为"%%",以便 Python 将它们转换为输出的单个百分号。

如果给定的 URL 为None,Django 将返回一个HttpResponseGone(410)。

示例 views.py

from django.shortcuts import get_object_or_404 

from django.views.generic.base import RedirectView 

from articles.models import Article 

class ArticleCounterRedirectView(RedirectView): 

    permanent = False 
    query_string = True 
    pattern_name = 'article-detail' 

    def get_redirect_url(self, *args, **kwargs): 
        article = get_object_or_404(Article, pk=kwargs['pk']) 
        article.update_counter() 
        return super(ArticleCounterRedirectView,  
                     self).get_redirect_url(*args, **kwargs) 

示例 urls.py

from django.conf.urls import url 
from django.views.generic.base import RedirectView 

from article.views import ArticleCounterRedirectView, ArticleDetail 

urlpatterns = [ 
    url(r'^counter/(?P<pk>[0-9]+)/$',  
        ArticleCounterRedirectView.as_view(),  
        name='article-counter'), 
    url(r'^details/(?P<pk>[0-9]+)/$',  
        ArticleDetail.as_view(), 
        name='article-detail'), 
    url(r'^go-to-django/$',  
        RedirectView.as_view(url='http://djangoproject.com'),  
        name='go-to-django'), 
] 

属性

url

要重定向的 URL,作为字符串。或者None以引发 410(已消失)HTTP 错误。

pattern_name

要重定向到的 URL 模式的名称。将使用与此视图传递的相同的*args**kwargs进行反转。

永久

重定向是否应该是永久的。这里唯一的区别是返回的 HTTP 状态代码。如果为True,则重定向将使用状态码 301。如果为False,则重定向将使用状态码 302。默认情况下,permanentTrue

query_string

是否将 GET 查询字符串传递到新位置。如果为True,则查询字符串将附加到 URL。如果为False,则查询字符串将被丢弃。默认情况下,query_stringFalse

方法

get_redirect_url(*args, **kwargs)构造重定向的目标 URL。

默认实现使用url作为起始字符串,并使用在 URL 中捕获的命名组执行%命名参数的扩展。

如果未设置urlget_redirect_url()将尝试使用在 URL 中捕获的内容(命名和未命名组都将被使用)来反转pattern_name

如果由query_string请求,则还将查询字符串附加到生成的 URL。子类可以实现任何他们希望的行为,只要该方法返回一个准备好的重定向 URL 字符串。

列表/详细通用视图

列表/详细通用视图处理在一个视图中显示项目列表的常见情况,并在另一个视图中显示这些项目的单独详细视图。

对象列表

django.views.generic.list.ListView 

使用此视图显示代表对象列表的页面。

示例 views.py

from django.views.generic.list import ListView 
from django.utils import timezone 

from articles.models import Article 

class ArticleListView(ListView): 

    model = Article 

    def get_context_data(self, **kwargs): 
        context = super(ArticleListView, self).get_context_data(**kwargs) 
        context['now'] = timezone.now() 
        return context 

示例 myapp/urls.py

from django.conf.urls import url 

from article.views import ArticleListView 

urlpatterns = [ 
    url(r'^$', ArticleListView.as_view(), name='article-list'), 
] 

示例 myapp/article_list.html

<h1>Articles</h1> 
<ul> 
{% for article in object_list %} 
    <li>{{ article.pub_date|date }}-{{ article.headline }}</li> 
{% empty %} 
    <li>No articles yet.</li> 
{% endfor %} 
</ul> 

详细视图

django.views.generic.detail.DetailView

此视图提供单个对象的详细视图。

示例 myapp/views.py

from django.views.generic.detail import DetailView 
from django.utils import timezone 

from articles.models import Article 

class ArticleDetailView(DetailView): 

    model = Article 

    def get_context_data(self, **kwargs): 
        context = super(ArticleDetailView,  
                        self).get_context_data(**kwargs) 
        context['now'] = timezone.now() 
        return context 

示例 myapp/urls.py

from django.conf.urls import url 

from article.views import ArticleDetailView 

urlpatterns = [ 
    url(r'^(?P<slug>[-_\w]+)/$',  
        ArticleDetailView.as_view(),  
        name='article-detail'), 
] 

示例 myapp/article_detail.html

<h1>{{ object.headline }}</h1> 
<p>{{ object.content }}</p> 
<p>Reporter: {{ object.reporter }}</p> 
<p>Published: {{ object.pub_date|date }}</p> 
<p>Date: {{ now|date }}</p> 

基于日期的通用视图

提供在django.views.generic.dates中的基于日期的通用视图,用于显示基于日期的数据的钻取页面。

存档索引视图

顶级索引页面显示最新的对象,按日期。除非将allow_future设置为True,否则不包括未来日期的对象。

上下文

除了django.views.generic.list.MultipleObjectMixin提供的上下文(通过django.views.generic.dates.BaseDateListView),模板的上下文将是:

  • date_list:包含根据queryset可用的所有年份的DateQuerySet对象,以降序表示为datetime.datetime对象

注意

  • 使用默认的context_object_namelatest

  • 使用默认的template_name_suffix_archive

  • 默认提供date_list按年份,但可以使用属性date_list_period更改为按月或日。这也适用于所有子类视图:

Example myapp/urls.py: 
from django.conf.urls import url 
from django.views.generic.dates import ArchiveIndexView 

from myapp.models import Article 

urlpatterns = [ 
    url(r'^archive/$', 
        ArchiveIndexView.as_view(model=Article, date_field="pub_date"), 
        name="article_archive"), 
] 

示例 myapp/article_archive.html

<ul> 
    {% for article in latest %} 
        <li>{{ article.pub_date }}: {{ article.title }}</li> 
    {% endfor %} 
</ul> 

这将输出所有文章。

YearArchiveView

年度存档页面显示给定年份中所有可用月份。除非将allow_future设置为True,否则不显示未来日期的对象。

上下文

除了django.views.generic.list.MultipleObjectMixin提供的上下文(通过django.views.generic.dates.BaseDateListView),模板的上下文将是:

  • date_list:包含根据queryset可用的所有月份的DateQuerySet对象,以升序表示为datetime.datetime对象

  • year:表示给定年份的date对象

  • next_year:表示下一年第一天的date对象,根据allow_emptyallow_future

  • previous_year:表示上一年第一天的date对象,根据allow_emptyallow_future

注释

  • 使用默认的template_name_suffix_archive_year

示例 myapp/views.py

from django.views.generic.dates import YearArchiveView 

from myapp.models import Article 

class ArticleYearArchiveView(YearArchiveView): 
    queryset = Article.objects.all() 
    date_field = "pub_date" 
    make_object_list = True 
    allow_future = True 

示例 myapp/urls.py

from django.conf.urls import url 

from myapp.views import ArticleYearArchiveView 

urlpatterns = [ 
    url(r'^(?P<year>[0-9]{4})/$', 
        ArticleYearArchiveView.as_view(), 
        name="article_year_archive"), 
] 

示例 myapp/article_archive_year.html

<ul> 
    {% for date in date_list %} 
        <li>{{ date|date }}</li> 
    {% endfor %} 
</ul> 
<div> 
    <h1>All Articles for {{ year|date:"Y" }}</h1> 
    {% for obj in object_list %} 
        <p> 
            {{ obj.title }}-{{ obj.pub_date|date:"F j, Y" }} 
        </p> 
    {% endfor %} 
</div> 

月存档视图

显示给定月份内所有对象的月度存档页面。具有未来日期的对象不会显示,除非您将allow_future设置为True

上下文

除了MultipleObjectMixin(通过BaseDateListView)提供的上下文之外,模板的上下文将是:

  • date_list:包含给定月份中具有可用对象的所有日期的DateQuerySet对象,根据queryset表示为datetime.datetime对象,按升序排列

  • month:表示给定月份的date对象

  • next_month:表示下个月第一天的date对象,根据allow_emptyallow_future

  • previous_month:表示上个月第一天的date对象,根据allow_emptyallow_future

注释

  • 使用默认的template_name_suffix_archive_month

示例 myapp/views.py

from django.views.generic.dates import MonthArchiveView 

from myapp.models import Article 

class ArticleMonthArchiveView(MonthArchiveView): 
    queryset = Article.objects.all() 
    date_field = "pub_date" 
    make_object_list = True 
    allow_future = True 

示例 myapp/urls.py

from django.conf.urls import url 

from myapp.views import ArticleMonthArchiveView 

urlpatterns = [ 
    # Example: /2012/aug/ 
    url(r'^(?P<year>[0-9]{4})/(?P<month>[-\w]+)/$', 
        ArticleMonthArchiveView.as_view(), 
        name="archive_month"), 
    # Example: /2012/08/ 
    url(r'^(?P<year>[0-9]{4})/(?P<month>[0-9]+)/$', 
        ArticleMonthArchiveView.as_view(month_format='%m'), 
        name="archive_month_numeric"), 
] 

示例 myapp/article_archive_month.html

<ul> 
    {% for article in object_list %} 
        <li>{{ article.pub_date|date:"F j, Y" }}:  
            {{ article.title }} 
        </li> 
    {% endfor %} 
</ul> 

<p> 
    {% if previous_month %} 
        Previous Month: {{ previous_month|date:"F Y" }} 
    {% endif %} 
    {% if next_month %} 
        Next Month: {{ next_month|date:"F Y" }} 
    {% endif %} 
</p> 

周存档视图

显示给定周内所有对象的周存档页面。具有未来日期的对象不会显示,除非您将allow_future设置为True

上下文

除了MultipleObjectMixin(通过BaseDateListView)提供的上下文之外,模板的上下文将是:

  • week:表示给定周的第一天的date对象

  • next_week:表示下周第一天的date对象,根据allow_emptyallow_future

  • previous_week:表示上周第一天的date对象,根据allow_emptyallow_future

注释

  • 使用默认的template_name_suffix_archive_week

示例 myapp/views.py

from django.views.generic.dates import WeekArchiveView 

from myapp.models import Article 

class ArticleWeekArchiveView(WeekArchiveView): 
    queryset = Article.objects.all() 
    date_field = "pub_date" 
    make_object_list = True 
    week_format = "%W" 
    allow_future = True 

示例 myapp/urls.py

from django.conf.urls import url 

from myapp.views import ArticleWeekArchiveView 

urlpatterns = [ 
    # Example: /2012/week/23/ 
    url(r'^(?P<year>[0-9]{4})/week/(?P<week>[0-9]+)/$', 
        ArticleWeekArchiveView.as_view(), 
        name="archive_week"), 
] 

示例 myapp/article_archive_week.html

<h1>Week {{ week|date:'W' }}</h1> 

<ul> 
    {% for article in object_list %} 
        <li>{{ article.pub_date|date:"F j, Y" }}: {{ article.title }}</li> 
    {% endfor %} 
</ul> 

<p> 
    {% if previous_week %} 
        Previous Week: {{ previous_week|date:"F Y" }} 
    {% endif %} 
    {% if previous_week and next_week %}--{% endif %} 
    {% if next_week %} 
        Next week: {{ next_week|date:"F Y" }} 
    {% endif %} 
</p> 

在这个例子中,您正在输出周数。WeekArchiveView中的默认week_format使用基于美国周系统的周格式"%U",其中周从星期日开始。"%W"格式使用 ISO 周格式,其周从星期一开始。"%W"格式在strftime()date中是相同的。

但是,date模板过滤器没有支持基于美国周系统的等效输出格式。date过滤器"%U"输出自 Unix 纪元以来的秒数。

日存档视图

显示给定日期内所有对象的日存档页面。未来的日期会抛出 404 错误,无论未来日期是否存在任何对象,除非您将allow_future设置为True

上下文

除了MultipleObjectMixin(通过BaseDateListView)提供的上下文之外,模板的上下文将是:

  • day:表示给定日期的date对象

  • next_day:表示下一天的date对象,根据allow_emptyallow_future

  • previous_day:表示前一天的date对象,根据allow_emptyallow_future

  • next_month:表示下个月第一天的date对象,根据allow_emptyallow_future

  • previous_month:表示上个月第一天的date对象,根据allow_emptyallow_future

注释

  • 使用默认的template_name_suffix_archive_day

示例 myapp/views.py

from django.views.generic.dates import DayArchiveView 

from myapp.models import Article 

class ArticleDayArchiveView(DayArchiveView): 
    queryset = Article.objects.all() 
    date_field = "pub_date" 
    make_object_list = True 
    allow_future = True 

示例 myapp/urls.py

from django.conf.urls import url 

from myapp.views import ArticleDayArchiveView 

urlpatterns = [ 
    # Example: /2012/nov/10/ 
    url(r'^(?P<year>[0-9]{4})/(?P<month>[-\w]+)/(?P<day>[0-9]+)/$', 
        ArticleDayArchiveView.as_view(), 
        name="archive_day"), 
] 

示例 myapp/article_archive_day.html

<h1>{{ day }}</h1> 

<ul> 
    {% for article in object_list %} 
        <li> 
        {{ article.pub_date|date:"F j, Y" }}: {{ article.title }} 
        </li> 
    {% endfor %} 
</ul> 

<p> 
    {% if previous_day %} 
        Previous Day: {{ previous_day }} 
    {% endif %} 
    {% if previous_day and next_day %}--{% endif %} 
    {% if next_day %} 
        Next Day: {{ next_day }} 
    {% endif %} 
</p> 

今天存档视图

显示今天的所有对象的日存档页面。这与django.views.generic.dates.DayArchiveView完全相同,只是使用今天的日期而不是year/month/day参数。

注释

  • 使用默认的template_name_suffix_archive_today

示例 myapp/views.py

from django.views.generic.dates import TodayArchiveView 

from myapp.models import Article 

class ArticleTodayArchiveView(TodayArchiveView): 
    queryset = Article.objects.all() 
    date_field = "pub_date" 
    make_object_list = True 
    allow_future = True 

示例 myapp/urls.py

from django.conf.urls import url 

from myapp.views import ArticleTodayArchiveView 

urlpatterns = [ 
    url(r'^today/$', 
        ArticleTodayArchiveView.as_view(), 
        name="archive_today"), 
] 

TodayArchiveView的示例模板在哪里?

此视图默认使用与上一个示例中的DayArchiveView相同的模板。如果需要不同的模板,请将template_name属性设置为新模板的名称。

DateDetailView

表示单个对象的页面。如果对象具有未来的日期值,默认情况下视图将抛出 404 错误,除非您将allow_future设置为True

上下文

  • 包括与DateDetailView中指定的model相关联的单个对象

  • 使用默认的template_name_suffix_detail
Example myapp/urls.py: 
from django.conf.urls import url 
from django.views.generic.dates import DateDetailView 

urlpatterns = [ 
    url(r'^(?P<year>[0-9]+)/(?P<month>[-\w]+)/(?P<day>[0-9]+)/ 
      (?P<pk>[0-9]+)/$', 
        DateDetailView.as_view(model=Article, date_field="pub_date"), 
        name="archive_date_detail"), 
] 

示例 myapp/article_detail.html

<h1>{{ object.title }}</h1> 

使用基于类的视图处理表单

表单处理通常有 3 条路径:

  • 初始GET(空白或预填充表单)

  • POST无效数据(通常重新显示带有错误的表单)

  • POST有效数据(处理数据并通常重定向)

自己实现这个通常会导致大量重复的样板代码(请参见在视图中使用表单)。为了避免这种情况,Django 提供了一组用于表单处理的通用基于类的视图。

基本表单

给定一个简单的联系表单:

# forms.py 

from django import forms 

class ContactForm(forms.Form): 
   name = forms.CharField() 
   message = forms.CharField(widget=forms.Textarea) 

   def send_email(self): 
       # send email using the self.cleaned_data dictionary 
       pass 

可以使用FormView构建视图:

# views.py 

from myapp.forms import ContactForm 
from django.views.generic.edit import FormView 

class ContactView(FormView): 
   template_name = 'contact.html' 
   form_class = ContactForm 
   success_url = '/thanks/' 

   def form_valid(self, form): 
       # This method is called when valid form data has been POSTed. 
       # It should return an HttpResponse. 
       form.send_email() 
       return super(ContactView, self).form_valid(form) 

注:

  • FormView继承了TemplateResponseMixin,因此template_name可以在这里使用

  • form_valid()的默认实现只是重定向到success_url

模型表单

与模型一起工作时,通用视图真正发挥作用。这些通用视图将自动创建ModelForm,只要它们可以确定要使用哪个模型类:

  • 如果给定了model属性,将使用该模型类

  • 如果get_object()返回一个对象,将使用该对象的类

  • 如果给定了queryset,将使用该查询集的模型

模型表单视图提供了一个form_valid()实现,可以自动保存模型。如果有特殊要求,可以覆盖此功能;请参阅下面的示例。

对于CreateViewUpdateView,甚至不需要提供success_url-如果可用,它们将使用模型对象上的get_absolute_url()

如果要使用自定义的ModelForm(例如添加额外的验证),只需在视图上设置form_class

注意

在指定自定义表单类时,仍然必须指定模型,即使form_class可能是一个 ModelForm。

首先,我们需要在我们的Author类中添加get_absolute_url()

# models.py 

from django.core.urlresolvers import reverse 
from django.db import models 

class Author(models.Model): 
    name = models.CharField(max_length=200) 

    def get_absolute_url(self): 
        return reverse('author-detail', kwargs={'pk': self.pk}) 

然后我们可以使用CreateView和其他视图来执行实际工作。请注意,我们只是在这里配置通用基于类的视图;我们不必自己编写任何逻辑:

# views.py 

from django.views.generic.edit import CreateView, UpdateView, DeleteView 
from django.core.urlresolvers import reverse_lazy 
from myapp.models import Author 

class AuthorCreate(CreateView): 
    model = Author 
    fields = ['name'] 

class AuthorUpdate(UpdateView): 
    model = Author 
    fields = ['name'] 

class AuthorDelete(DeleteView): 
    model = Author 
    success_url = reverse_lazy('author-list') 

我们必须在这里使用reverse_lazy(),而不仅仅是reverse,因为在导入文件时未加载 URL。

fields属性的工作方式与ModelForm上内部Meta类的fields属性相同。除非以其他方式定义表单类,否则该属性是必需的,如果没有,视图将引发ImproperlyConfigured异常。

如果同时指定了fieldsform_class属性,将引发ImproperlyConfigured异常。

最后,我们将这些新视图挂接到 URLconf 中:

# urls.py 

from django.conf.urls import url 
from myapp.views import AuthorCreate, AuthorUpdate, AuthorDelete 

urlpatterns = [ 
    # ... 
    url(r'author/add/$', AuthorCreate.as_view(), name='author_add'), 
    url(r'author/(?P<pk>[0-9]+)/$', AuthorUpdate.as_view(),   
        name='author_update'), 
    url(r'author/(?P<pk>[0-9]+)/delete/$', AuthorDelete.as_view(),  
        name='author_delete'), 
] 

在这个例子中:

  • CreateViewUpdateView使用myapp/author_form.html

  • DeleteView使用myapp/author_confirm_delete.html

如果您希望为CreateViewUpdateView设置单独的模板,可以在视图类上设置template_nametemplate_name_suffix

模型和 request.user

要跟踪使用CreateView创建对象的用户,可以使用自定义的ModelForm来实现。首先,将外键关系添加到模型中:

# models.py 

from django.contrib.auth.models import User 
from django.db import models 

class Author(models.Model): 
    name = models.CharField(max_length=200) 
    created_by = models.ForeignKey(User) 

    # ... 

在视图中,确保不要在要编辑的字段列表中包含created_by,并覆盖form_valid()以添加用户:

# views.py 

from django.views.generic.edit import CreateView 
from myapp.models import Author 

class AuthorCreate(CreateView): 
    model = Author 
    fields = ['name'] 

    def form_valid(self, form): 
        form.instance.created_by = self.request.user 
        return super(AuthorCreate, self).form_valid(form) 

请注意,您需要使用login_required()装饰此视图,或者在form_valid()中处理未经授权的用户。

AJAX 示例

这里是一个简单的示例,展示了如何实现一个既适用于 AJAX 请求又适用于普通表单POST的表单。

from django.http import JsonResponse 
from django.views.generic.edit import CreateView 
from myapp.models import Author 

class AjaxableResponseMixin(object): 
    def form_invalid(self, form): 
        response = super(AjaxableResponseMixin, self).form_invalid(form) 
        if self.request.is_ajax(): 
            return JsonResponse(form.errors, status=400) 
        else: 
            return response 

    def form_valid(self, form): 
        # We make sure to call the parent's form_valid() method because 
        # it might do some processing (in the case of CreateView, it will 
        # call form.save() for example). 
        response = super(AjaxableResponseMixin, self).form_valid(form) 
        if self.request.is_ajax(): 
            data = { 
                'pk': self.object.pk, 
            } 
            return JsonResponse(data) 
        else: 
            return response 

class AuthorCreate(AjaxableResponseMixin, CreateView): 
    model = Author 
    fields = ['name'] 

附录 D。设置

你的 Django 设置文件包含了你的 Django 安装的所有配置。本附录解释了设置的工作原理以及可用的设置。

什么是设置文件?

设置文件只是一个具有模块级变量的 Python 模块。以下是一些示例设置:

ALLOWED_HOSTS = ['www.example.com'] DEBUG = False DEFAULT_FROM_EMAIL = 'webmaster@example.com' 

注意

如果将DEBUG设置为False,还需要正确设置ALLOWED_HOSTS设置。

因为设置文件是一个 Python 模块,所以以下规则适用:

  • 它不允许有 Python 语法错误

  • 它可以使用常规的 Python 语法动态分配设置,例如:

        MY_SETTING = [str(i) for i in range(30)] 

  • 它可以从其他设置文件中导入值

默认设置

如果不需要,Django 设置文件不必定义任何设置。每个设置都有一个合理的默认值。这些默认值存储在模块django/conf/global_settings.py中。以下是 Django 在编译设置时使用的算法:

  • global_settings.py加载设置

  • 从指定的设置文件中加载设置,必要时覆盖全局设置

请注意,设置文件不应该从global_settings导入,因为那是多余的。

查看您已更改的设置

有一种简单的方法来查看您的设置中有哪些与默认设置不同。命令python manage.py diffsettings显示当前设置文件与 Django 默认设置之间的差异。有关更多信息,请参阅diffsettings文档。

在 Python 代码中使用设置

在 Django 应用程序中,通过导入对象django.conf.settings来使用设置。例如:

from django.conf import settings
if settings.DEBUG:
     # Do something 

请注意,django.conf.settings不是一个模块-它是一个对象。因此,无法导入单个设置:

from django.conf.settings import DEBUG  # This won't work. 

还要注意,您的代码不应该从global_settings或您自己的设置文件中导入。django.conf.settings抽象了默认设置和站点特定设置的概念;它提供了一个单一的接口。它还将使用设置的代码与设置的位置解耦。

在运行时更改设置

您不应该在应用程序中在运行时更改设置。例如,在视图中不要这样做:

from django.conf import settings
settings.DEBUG = True   # Don't do this! 

唯一应该分配设置的地方是在设置文件中。

安全

由于设置文件包含敏感信息,例如数据库密码,您应该尽一切努力限制对其的访问。例如,更改文件权限,以便只有您和您的 Web 服务器用户可以读取它。这在共享托管环境中尤为重要。

创建自己的设置

没有什么能阻止您为自己的 Django 应用程序创建自己的设置。只需遵循这些约定:

  • 设置名称全部大写

  • 不要重新发明已经存在的设置

对于序列的设置,Django 本身使用元组,而不是列表,但这只是一种约定。

DJANGO_SETTINGS_MODULE

当您使用 Django 时,您必须告诉它您正在使用哪些设置。通过使用环境变量DJANGO_SETTINGS_MODULE来实现。DJANGO_SETTINGS_MODULE的值应该是 Python 路径语法,例如mysite.settings

django-admin 实用程序

在使用django-admin时,您可以设置环境变量一次,或者每次运行实用程序时显式传递设置模块。示例(Unix Bash shell):

export DJANGO_SETTINGS_MODULE=mysite.settings 
django-admin runserver

示例(Windows shell):

set DJANGO_SETTINGS_MODULE=mysite.settings 
django-admin runserver

使用--settings命令行参数手动指定设置:

django-admin runserver --settings=mysite.settings

在服务器上(mod_wsgi)

在您的生产服务器环境中,您需要告诉您的 WSGI 应用程序使用哪个设置文件。使用os.environ来实现:

import os
os.environ['DJANGO_SETTINGS_MODULE'] = 'mysite.settings'

阅读第十三章,“部署 Django”,了解有关 Django WSGI 应用程序的更多信息和其他常见元素。

在没有设置 DJANGO_SETTINGS_MODULE 的情况下使用设置

在某些情况下,您可能希望绕过DJANGO_SETTINGS_MODULE环境变量。例如,如果您仅使用模板系统,您可能不希望设置一个指向设置模块的环境变量。在这些情况下,可以手动配置 Django 的设置。通过调用:

django.conf.settings.configure(default_settings, **settings) 

例子:

from django.conf import settings 
settings.configure(DEBUG=True, TEMPLATE_DEBUG=True) 

可以将configure()作为许多关键字参数传递,每个关键字参数表示一个设置及其值。每个参数名称应全部大写,与上述描述的设置名称相同。如果没有将特定设置传递给configure()并且在以后的某个时刻需要,Django 将使用默认设置值。

以这种方式配置 Django 在使用框架的一部分时通常是必要的,而且是推荐的。因此,当通过settings.configure()配置时,Django 不会对进程环境变量进行任何修改(请参阅TIME_ZONE的文档,了解为什么通常会发生这种情况)。在这些情况下,假设您已经完全控制了您的环境。

自定义默认设置

如果您希望默认值来自于django.conf.global_settings之外的某个地方,可以将提供默认设置的模块或类作为configure()调用中的default_settings参数(或作为第一个位置参数)传递。在此示例中,默认设置来自myapp_defaults,并且DEBUG设置为True,而不管它在myapp_defaults中的值是什么:

from django.conf import settings 
from myapp import myapp_defaults 

settings.configure(default_settings=myapp_defaults, DEBUG=True) 

使用myapp_defaults作为位置参数的以下示例是等效的:

settings.configure(myapp_defaults, DEBUG=True) 

通常,您不需要以这种方式覆盖默认设置。Django 的默认设置足够温和,您可以放心使用它们。请注意,如果您传入一个新的默认模块,它将完全替换 Django 的默认设置,因此您必须为可能在导入的代码中使用的每个可能的设置指定一个值。在django.conf.settings.global_settings中查看完整列表。

要么 configure(),要么 DJANGO_SETTINGS_MODULE 是必需的

如果您没有设置DJANGO_SETTINGS_MODULE环境变量,则在使用读取设置的任何代码之前必须在某个时刻调用configure()。如果您没有设置DJANGO_SETTINGS_MODULE并且没有调用configure(),Django 将在第一次访问设置时引发ImportError异常。如果您设置了DJANGO_SETTINGS_MODULE,以某种方式访问设置值,然后调用configure(),Django 将引发RuntimeError,指示已经配置了设置。有一个专门用于此目的的属性:

django.conf.settings.configured 

例如:

from django.conf import settings 
if not settings.configured:
     settings.configure(myapp_defaults, DEBUG=True) 

此外,多次调用configure()或在访问任何设置之后调用configure()都是错误的。归根结底:只使用configure()DJANGO_SETTINGS_MODULE中的一个。既不是两者,也不是两者都不是。

可用设置

Django 有大量可用的设置。为了方便参考,我将它们分成了六个部分,每个部分在本附录中都有相应的表。

  • 核心设置(表 D.1

  • 身份验证设置(表 D.2

  • 消息设置(表 D.3

  • 会话设置(表 D.4

  • Django 站点设置(表 D.5

  • 静态文件设置(表 D.6

每个表列出了可用设置及其默认值。有关每个设置的附加信息和用例,请参阅 Django 项目网站docs.djangoproject.com/en/1.8/ref/settings/

注意

在覆盖设置时要小心,特别是当默认值是非空列表或字典时,例如MIDDLEWARE_CLASSESSTATICFILES_FINDERS。确保保留 Django 所需的组件,以便使用您希望使用的 Django 功能。

核心设置

设置 默认值
ABSOLUTE_URL_OVERRIDES {} (空字典)
ADMINS [] (空列表)
ALLOWED_HOSTS [](空列表)
APPEND_SLASH True
CACHE_MIDDLEWARE_ALIAS default
CACHES { 'default': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', } }
CACHE_MIDDLEWARE_KEY_PREFIX ''(空字符串)
CACHE_MIDDLEWARE_SECONDS 600
CSRF_COOKIE_AGE 31449600 (1 年,以秒计)
CSRF_COOKIE_DOMAIN None
CSRF_COOKIE_HTTPONLY False
CSRF_COOKIE_NAME Csrftoken
CSRF_COOKIE_PATH '/'
CSRF_COOKIE_SECURE False
DATE_INPUT_FORMATS [ '%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', '%b %d %Y', '%b %d, %Y', '%d %b %Y','%d %b, %Y', '%B %d %Y', '%B %d, %Y', '%d %B %Y', '%d %B, %Y', ]
DATETIME_FORMAT 'N j, Y, P'(例如,Feb. 4, 2003, 4 p.m.)
DATETIME_INPUT_FORMATS [ '%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M:%S.%f', '%Y-%m-%d %H:%M', '%Y-%m-%d', '%m/%d/%Y %H:%M:%S', '%m/%d/%Y %H:%M:%S.%f', '%m/%d/%Y %H:%M', '%m/%d/%Y', '%m/%d/%y %H:%M:%S',``'%m/%d/%y %H:%M:%S.%f', '%m/%d/%y %H:%M', '%m/%d/%y',``]
DEBUG False
DEBUG_PROPAGATE_EXCEPTIONS False
DECIMAL_SEPARATOR '.'(点)
DEFAULT_CHARSET 'utf-8'
DEFAULT_CONTENT_TYPE 'text/html'
DEFAULT_EXCEPTION_REPORTER_FILTER django.views.debug. SafeExceptionReporterFilter
DEFAULT_FILE_STORAGE django.core.files.storage. FileSystemStorage
DEFAULT_FROM_EMAIL 'webmaster@localhost'.
DEFAULT_INDEX_TABLESPACE ''(空字符串)
DEFAULT_TABLESPACE ''(空字符串)
DISALLOWED_USER_AGENTS [](空列表)
EMAIL_BACKEND django.core.mail.backends.smtp. EmailBackend
EMAIL_HOST 'localhost'
EMAIL_HOST_PASSWORD ''(空字符串)
EMAIL_HOST_USER ''(空字符串)
EMAIL_PORT 25
EMAIL_SUBJECT_PREFIX '[Django] '
EMAIL_USE_TLS False
EMAIL_USE_SSL False
EMAIL_SSL_CERTFILE None
EMAIL_SSL_KEYFILE None
EMAIL_TIMEOUT None
FILE_CHARSET 'utf-8'
FILE_UPLOAD_HANDLERS [ 'django.core.files.uploadhandler. MemoryFileUploadHandler', 'django.core.files.uploadhandler. TemporaryFileUploadHandler' ]
FILE_UPLOAD_MAX_MEMORY_SIZE 2621440(即 2.5 MB)
FILE_UPLOAD_DIRECTORY_PERMISSIONS None
FILE_UPLOAD_PERMISSIONS None
FILE_UPLOAD_TEMP_DIR None
FIRST_DAY_OF_WEEK 0(星期日)
FIXTURE_DIRS [](空列表)
FORCE_SCRIPT_NAME None
FORMAT_MODULE_PATH None
IGNORABLE_404_URLS [](空列表)
INSTALLED_APPS [](空列表)
INTERNAL_IPS [](空列表)
LANGUAGE_CODE 'en-us'
LANGUAGE_COOKIE_AGE None(在浏览器关闭时过期)
LANGUAGE_COOKIE_DOMAIN None
LANGUAGE_COOKIE_NAME 'django_language'
LANGUAGES 所有可用语言的列表
LOCALE_PATHS [](空列表)
LOGGING 一个日志配置字典
LOGGING_CONFIG 'logging.config.dictConfig'
MANAGERS [](空列表)
MEDIA_ROOT ''(空字符串)
MEDIA_URL ''(空字符串)
MIDDLEWARE_CLASSES [ 'django.middleware.common. CommonMiddleware', 'django.middleware.csrf.  CsrfViewMiddleware' ]
MIGRATION_MODULES {}(空字典)
MONTH_DAY_FORMAT 'F j'
NUMBER_GROUPING 0
PREPEND_WWW False
ROOT_URLCONF 未定义
SECRET_KEY ''(空字符串)
SECURE_BROWSER_XSS_FILTER False
SECURE_CONTENT_TYPE_NOSNIFF False
SECURE_HSTS_INCLUDE_SUBDOMAINS False
SECURE_HSTS_SECONDS 0
SECURE_PROXY_SSL_HEADER None
SECURE_REDIRECT_EXEMPT [](空列表)
SECURE_SSL_HOST None
SECURE_SSL_REDIRECT False
SERIALIZATION_MODULES 未定义
SERVER_EMAIL 'root@localhost'
SHORT_DATE_FORMAT m/d/Y(例如,12/31/2003)
SHORT_DATETIME_FORMAT m/d/Y P(例如,12/31/2003 4 p.m.)
SIGNING_BACKEND 'django.core.signing.TimestampSigner'
SILENCED_SYSTEM_CHECKS [](空列表)
TEMPLATES [](空列表)
TEMPLATE_DEBUG False
TEST_RUNNER 'django.test.runner.DiscoverRunner'
TEST_NON_SERIALIZED_APPS [](空列表)
THOUSAND_SEPARATOR ,(逗号)
TIME_FORMAT 'P'(例如,下午 4 点)
TIME_INPUT_FORMATS [ '%H:%M:%S',``'%H:%M:%S.%f', '%H:%M',``]
TIME_ZONE 'America/Chicago'
USE_ETAGS False
USE_I18N True
USE_L10N False
USE_THOUSAND_SEPARATOR False
USE_TZ False
USE_X_FORWARDED_HOST False
WSGI_APPLICATION None
YEAR_MONTH_FORMAT 'F Y'
X_FRAME_OPTIONS 'SAMEORIGIN'

表 D.1:Django 核心设置

认证

设置 默认值
AUTHENTICATION_BACKENDS 'django.contrib.auth.backends.ModelBackend'
AUTH_USER_MODEL 'auth.User'
LOGIN_REDIRECT_URL '/accounts/profile/'
LOGIN_URL '/accounts/login/'
LOGOUT_URL '/accounts/logout/'
PASSWORD_RESET_TIMEOUT_DAYS 3
PASSWORD_HASHERS [ 'django.contrib.auth.hashers.PBKDF2PasswordHasher', 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', 'django.contrib.auth.hashers.BCryptPasswordHasher', 'django.contrib.auth.hashers.SHA1PasswordHasher', 'django.contrib.auth.hashers.MD5PasswordHasher', 'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher', 'django.contrib.auth.hashers.CryptPasswordHasher' ]

表 D.2:Django 身份验证设置

消息

设置 默认值
MESSAGE_LEVEL messages
MESSAGE_STORAGE 'django.contrib.messages.storage.fallback.FallbackStorage'
MESSAGE_TAGS { messages.DEBUG: 'debug', messages.INFO: 'info', messages.SUCCESS: 'success', messages.WARNING: 'warning', messages.ERROR: 'error' }

表 D.3:Django 消息设置

会话

设置 默认值
SESSION_CACHE_ALIAS default
SESSION_COOKIE_AGE 1209600(2 周,以秒计)。
SESSION_COOKIE_DOMAIN None
SESSION_COOKIE_HTTPONLY True.
SESSION_COOKIE_NAME 'sessionid'
SESSION_COOKIE_PATH '/'
SESSION_COOKIE_SECURE False
SESSION_ENGINE 'django.contrib.sessions.backends.db'
SESSION_EXPIRE_AT_BROWSER_CLOSE False
SESSION_FILE_PATH None
SESSION_SAVE_EVERY_REQUEST False
SESSION_SERIALIZER 'django.contrib.sessions.serializers. JSONSerializer'

表 D.4:Django 会话设置

站点

设置 默认值
SITE_ID Not defined

表 D.5:Django 站点设置

静态文件

设置 默认值
STATIC_ROOT None
STATIC_URL None
STATICFILES_DIRS [](空列表)
STATICFILES_STORAGE 'django.contrib.staticfiles.storage.StaticFilesStorage'
STATICFILES_FINDERS [``"django.contrib.staticfiles.finders.FileSystemFinder", "django.contrib.staticfiles.finders. AppDirectoriesFinder"``]

表 D.6:Django 静态文件设置

附录 E. 内置模板标签和过滤器

第三章, 模板, 列出了一些最有用的内置模板标签和过滤器。但是,Django 还附带了许多其他内置标签和过滤器。本附录提供了 Django 中所有模板标签和过滤器的摘要。有关更详细的信息和用例,请参见 Django 项目网站 docs.djangoproject.com/en/1.8/ref/templates/builtins/

内置标签

autoescape

控制当前自动转义行为。此标签接受 onoff 作为参数,决定块内是否生效自动转义。块以 endautoescape 结束标签关闭。

当自动转义生效时,所有变量内容在放入输出结果之前都会应用 HTML 转义(但在应用任何过滤器之后)。这相当于手动对每个变量应用 escape 过滤器。

唯一的例外是已经标记为不需要转义的变量,要么是由填充变量的代码标记的,要么是因为已经应用了 safeescape 过滤器。示例用法:

{% autoescape on %} 
    {{ body }} 
{% endautoescape %} 

block

定义一个可以被子模板覆盖的块。有关更多信息,请参见 第三章, 模板, 中的 "模板继承"。

comment

忽略 {% comment %}{% endcomment %} 之间的所有内容。第一个标签中可以插入一个可选的注释。例如,当注释掉代码以记录为什么禁用代码时,这是有用的。

Comment 标签不能嵌套。

csrf_token

此标签用于 CSRF 保护。有关 跨站点请求伪造 (CSRF) 的更多信息,请参见 第三章, 模板, 和 第十九章, Django 中的安全性

cycle

每次遇到此标签时,产生其中的一个参数。第一次遇到时产生第一个参数,第二次遇到时产生第二个参数,依此类推。一旦所有参数用完,标签就会循环到第一个参数并再次产生它。这个标签在循环中特别有用:

{% for o in some_list %} 
    <tr class="{% cycle 'row1' 'row2' %}"> 
        ... 
    </tr> 
{% endfor %} 

第一次迭代生成引用 row1 类的 HTML,第二次生成 row2,第三次再次生成 row1,依此类推。您也可以使用变量。例如,如果有两个模板变量 rowvalue1rowvalue2,您可以像这样在它们的值之间交替:

{% for o in some_list %} 
    <tr class="{% cycle rowvalue1 rowvalue2 %}"> 
        ... 
    </tr> 
{% endfor %} 

您还可以混合变量和字符串:

{% for o in some_list %} 
    <tr class="{% cycle 'row1' rowvalue2 'row3' %}"> 
        ... 
    </tr> 
{% endfor %} 

您可以在 cycle 标签中使用任意数量的值,用空格分隔。用单引号 (') 或双引号 (") 括起来的值被视为字符串字面量,而没有引号的值被视为模板变量。

debug

输出大量的调试信息,包括当前上下文和导入的模块。

extends

表示此模板扩展了父模板。此标签可以以两种方式使用:

  • {% extends "base.html" %}(带引号)使用字面值 "base.html" 作为要扩展的父模板的名称。

  • {% extends variable %} 使用 variable 的值。如果变量求值为字符串,Django 将使用该字符串作为父模板的名称。如果变量求值为 Template 对象,Django 将使用该对象作为父模板。

filter

通过一个或多个过滤器过滤块的内容。有关 Django 中过滤器的列表,请参见附录后面的内置过滤器部分。

firstof

输出第一个不是 False 的参数变量。如果所有传递的变量都是 False,则不输出任何内容。示例用法:

{% firstof var1 var2 var3 %} 

这相当于:

{% if var1 %} 
    {{ var1 }} 
{% elif var2 %} 
    {{ var2 }} 
{% elif var3 %} 
    {{ var3 }} 
{% endif %} 

for

在数组中循环每个项目,使项目在上下文变量中可用。例如,要显示提供的 athlete_list 中的运动员列表:

<ul> 
{% for athlete in athlete_list %} 
    <li>{{ athlete.name }}</li> 
{% endfor %} 
</ul> 

您可以通过使用{% for obj in list reversed %}在列表上进行反向循环。如果需要循环遍历一个列表的列表,可以将每个子列表中的值解压缩为单独的变量。如果需要访问字典中的项目,这也可能很有用。例如,如果您的上下文包含一个名为data的字典,则以下内容将显示字典的键和值:

{% for key, value in data.items %} 
    {{ key }}: {{ value }} 
{% endfor %} 

for... empty

for标签可以带一个可选的{% empty %}子句,如果给定的数组为空或找不到,则显示其文本:

<ul> 
{% for athlete in athlete_list %} 
    <li>{{ athlete.name }}</li> 
{% empty %} 
    <li>Sorry, no athletes in this list.</li> 
{% endfor %} 
</ul> 

如果

{% if %}标签评估一个变量,如果该变量为真(即存在,不为空,并且不是 false 布尔值),则输出块的内容:

{% if athlete_list %} 
    Number of athletes: {{ athlete_list|length }} 
{% elif athlete_in_locker_room_list %} 
    Athletes should be out of the locker room soon! 
{% else %} 
    No athletes. 
{% endif %} 

在上面的例子中,如果athlete_list不为空,则将通过{{ athlete_list|length }}变量显示运动员的数量。正如您所看到的,if标签可以带一个或多个{% elif %}子句,以及一个{% else %}子句,如果所有先前的条件都失败,则将显示该子句。这些子句是可选的。

布尔运算符

if标签可以使用andornot来测试多个变量或否定给定变量:

{% if athlete_list and coach_list %} 
    Both athletes and coaches are available. 
{% endif %} 

{% if not athlete_list %} 
    There are no athletes. 
{% endif %} 

{% if athlete_list or coach_list %} 
    There are some athletes or some coaches. 
{% endif %} 

在同一个标签中使用andor子句是允许的,例如,and的优先级高于or

{% if athlete_list and coach_list or cheerleader_list %} 

将被解释为:

if (athlete_list and coach_list) or cheerleader_list 

if标签中使用实际括号是无效的语法。如果需要它们来表示优先级,应该使用嵌套的if标签。

if标签也可以使用==!=<><=>=in运算符,其工作方式如表 E.1中所列。

运算符 示例
== {% if somevar == "x" %} ...
!= {% if somevar != "x" %} ...
< {% if somevar < 100 %} ...
> {% if somevar > 10 %} ...
{% if somevar ⇐ 100 %} ...
>= {% if somevar >= 10 %} ...
In {% if "bc" in "abcdef" %}

表 E.1:模板标签中的布尔运算符

复杂表达式

所有上述内容都可以组合成复杂的表达式。对于这样的表达式,了解在评估表达式时运算符是如何分组的可能很重要,即优先级规则。运算符的优先级从低到高依次为:

  • or

  • and

  • not

  • in

  • ==!=<><=>=

这个优先顺序与 Python 完全一致。

过滤器

您还可以在if表达式中使用过滤器。例如:

{% if messages|length >= 100 %} 
   You have lots of messages today! 
{% endif %} 

ifchanged

检查值是否与循环的上一次迭代不同。

{% ifchanged %}块标签在循环内使用。它有两种可能的用法:

  • 检查其自身的渲染内容与其先前状态是否不同,仅在内容发生变化时显示内容

  • 如果给定一个或多个变量,检查任何变量是否发生了变化

ifequal

如果两个参数相等,则输出块的内容。示例:

{% ifequal user.pk comment.user_id %} 
    ... 
{% endifequal %} 

ifequal标签的替代方法是使用if标签和==运算符。

ifnotequal

ifequal类似,只是它测试两个参数是否不相等。使用ifnotequal标签的替代方法是使用if标签和!=运算符。

包括

加载模板并使用当前上下文进行渲染。这是在模板中包含其他模板的一种方式。模板名称可以是一个变量:

{% include template_name %} 

或硬编码(带引号)的字符串:

{% include "foo/bar.html" %} 

加载

加载自定义模板标签集。例如,以下模板将加载somelibraryotherlibrary中注册的所有标签和过滤器,这些库位于package包中:

{% load somelibrary package.otherlibrary %} 

您还可以使用from参数从库中选择性地加载单个过滤器或标签。

在这个例子中,模板标签/过滤器foobar将从somelibrary中加载:

{% load foo bar from somelibrary %} 

有关更多信息,请参阅自定义标签过滤器库

lorem

显示随机的 lorem ipsum 拉丁文。这对于在模板中提供示例数据很有用。用法:

{% lorem [count] [method] [random] %} 

{% lorem %}标签可以使用零个、一个、两个或三个参数。这些参数是:

  • **计数:**生成段落或单词的数量(默认为 1)的数字(或变量)。

  • **方法:**单词的 w,HTML 段落的 p 或纯文本段落块的 b(默认为 b)。

  • **随机:**单词随机,如果给定,则在生成文本时不使用常见段落(Lorem ipsum dolor sit amet...)。

例如,{% lorem 2 w random %}将输出两个随机拉丁单词。

now

显示当前日期和/或时间,使用与给定字符串相符的格式。该字符串可以包含格式说明符字符,如date过滤器部分所述。例如:

It is {% now "jS F Y H:i" %} 

传递的格式也可以是预定义的格式之一DATE_FORMATDATETIME_FORMATSHORT_DATE_FORMATSHORT_DATETIME_FORMAT。预定义的格式可能会根据当前区域设置和格式本地化的启用情况而有所不同,例如:

It is {% now "SHORT_DATETIME_FORMAT" %} 

regroup

通过共同属性对类似对象的列表进行重新分组。

{% regroup %}生成组对象的列表。每个组对象有两个属性:

  • grouper:按其共同属性进行分组的项目(例如,字符串 India 或 Japan)

  • list:此组中所有项目的列表(例如,所有country = "India"的城市列表)

请注意,{% regroup %}不会对其输入进行排序!

任何有效的模板查找都是regroup标记的合法分组属性,包括方法、属性、字典键和列表项。

spaceless

删除 HTML 标签之间的空格。这包括制表符和换行符。例如用法:

{% spaceless %} 
    <p> 
        <a href="foo/">Foo</a> 
    </p> 
{% endspaceless %} 

此示例将返回此 HTML:

<p><a href="foo/">Foo</a></p> 

templatetag

输出用于组成模板标记的语法字符之一。由于模板系统没有转义的概念,因此要显示模板标记中使用的位之一,必须使用{% templatetag %}标记。参数告诉要输出哪个模板位:

  • openblock输出:{%

  • closeblock输出:%}

  • openvariable输出:{{

  • closevariable输出:}}

  • openbrace输出:{

  • closebrace输出:}

  • opencomment输出:{#

  • closecomment输出:#}

示例用法:

{% templatetag openblock %} url 'entry_list' {% templatetag closeblock %} 

url

返回与给定视图函数和可选参数匹配的绝对路径引用(不包括域名的 URL)。结果路径中的任何特殊字符都将使用iri_to_uri()进行编码。这是一种在模板中输出链接的方法,而不违反 DRY 原则,因为不必在模板中硬编码 URL:

{% url 'some-url-name' v1 v2 %} 

第一个参数是视图函数的路径,格式为package.package.module.function。它可以是带引号的文字或任何其他上下文变量。其他参数是可选的,应该是用空格分隔的值,这些值将用作 URL 中的参数。

verbatim

阻止模板引擎渲染此块标记的内容。常见用途是允许与 Django 语法冲突的 JavaScript 模板层。

widthratio

用于创建条形图等,此标记计算给定值与最大值的比率,然后将该比率应用于常数。例如:

<img src="img/bar.png" alt="Bar" 
     height="10" width="{% widthratio this_value max_value max_width %}" /> 

with

将复杂变量缓存到更简单的名称下。在多次访问昂贵的方法(例如,多次访问数据库的方法)时很有用。例如:

{% with total=business.employees.count %} 
    {{ total }} employee{{ total|pluralize }} 
{% endwith %} 

内置过滤器

add

将参数添加到值。例如:

{{ value|add:"2" }} 

如果value4,则输出将是6

addslashes

在引号前添加斜杠。例如,在 CSV 中转义字符串很有用。例如:

{{ value|addslashes }} 

如果valueI'm using Django,输出将是I'm using Django

capfirst

将值的第一个字符大写。如果第一个字符不是字母,则此过滤器无效。

center

将值居中在给定宽度的字段中。例如:

"{{ value|center:"14" }}" 

如果valueDjango,输出将是Django

cut

从给定字符串中删除所有arg的值。

date

根据给定的格式格式化日期。使用与 PHP 的date()函数类似的格式,但有一些不同之处。

注意

这些格式字符在 Django 模板之外不使用。它们旨在与 PHP 兼容,以便设计人员更轻松地过渡。有关格式字符串的完整列表,请参见 Django 项目网站docs.djangoproject.com/en/dev/ref/templates/builtins/#date

例如:

{{ value|date:"D d M Y" }} 

如果valuedatetime对象(例如,datetime.datetime.now()的结果),输出将是字符串Fri 01 Jul 2016。传递的格式可以是预定义的DATE_FORMATDATETIME_FORMATSHORT_DATE_FORMATSHORT_DATETIME_FORMAT之一,也可以是使用日期格式说明符的自定义格式。

默认

如果值评估为False,则使用给定的默认值。否则,使用该值。例如:

{{ value|default:"nothing" }}     

default_if_none

如果(且仅当)值为None,则使用给定的默认值。否则,使用该值。

dictsort

接受一个字典列表并返回按参数中给定的键排序的列表。例如:

{{ value|dictsort:"name" }} 

dictsortreversed

接受一个字典列表并返回按参数中给定的键的相反顺序排序的列表。

可被整除

如果值可以被参数整除,则返回True。例如:

{{ value|divisibleby:"3" }} 

如果value21,输出将是True

转义

转义字符串的 HTML。具体来说,它进行以下替换:

  • <转换为&lt;

  • >转换为&gt;

  • '(单引号)转换为'

  • "(双引号)转换为&quot;

  • &转换为&amp;

转义仅在输出字符串时应用,因此不管在过滤器的链式序列中放置escape的位置如何:它始终会被应用,就好像它是最后一个过滤器一样。

escapejs

转义用于 JavaScript 字符串。这并使字符串在 HTML 中安全使用,但可以保护您免受在使用模板生成 JavaScript/JSON 时的语法错误。

filesizeformat

格式化值,如“人类可读”的文件大小(即'13 KB''4.1 MB''102 bytes'等)。例如:

{{ value|filesizeformat }} 

如果value123456789,输出将是117.7 MB

第一

返回列表中的第一项。

floatformat

在没有参数的情况下使用时,将浮点数四舍五入到小数点后一位,但只有在有小数部分要显示时才会这样做。如果与数字整数参数一起使用,floatformat将将数字四舍五入到该小数位数。

例如,如果value34.23234{{ value|floatformat:3 }}将输出34.232

get_digit

给定一个整数,返回请求的数字,其中 1 是最右边的数字。

iriencode

国际化资源标识符IRI)转换为适合包含在 URL 中的字符串。

join

使用字符串将列表连接起来,就像 Python 的str.join(list)一样。

最后

返回列表中的最后一项。

长度

返回值的长度。这适用于字符串和列表。

length_is

如果值的长度是参数,则返回True,否则返回False。例如:

{{ value|length_is:"4" }} 

linebreaks

用适当的 HTML 替换纯文本中的换行符;单个换行符变成 HTML 换行符(<br />),换行符后面跟着一个空行变成段落换行符(</p>)。

linebreaksbr

将纯文本中的所有换行符转换为 HTML 换行符(<br />)。

行号

显示带有行号的文本。

ljust

将值左对齐在给定宽度的字段中。例如:

{{ value|ljust:"10" }} 

如果valueDjango,输出将是Django

lower

将字符串转换为全部小写。

make_list

返回转换为列表的值。对于字符串,它是一个字符列表。对于整数,在创建列表之前,参数被转换为 Unicode 字符串。

phone2numeric

将电话号码(可能包含字母)转换为其数字等价物。输入不一定是有效的电话号码。这将愉快地转换任何字符串。例如:

{{ value|phone2numeric }} 

如果value800-COLLECT,输出将是800-2655328

pluralize

如果值不是1,则返回复数后缀。默认情况下,此后缀为s

对于不通过简单后缀复数化的单词,可以指定由逗号分隔的单数和复数后缀。例如:

You have {{ num_cherries }} cherr{{ num_cherries|pluralize:"y,ies" }}. 

漂亮打印

pprint.pprint()的包装器-用于调试。

随机

从给定列表返回一个随机项。

rjust

将值右对齐到给定宽度的字段。例如:

{{ value|rjust:"10" }} 

如果valueDjango,输出将是Django

安全

将字符串标记为在输出之前不需要进一步的 HTML 转义。当自动转义关闭时,此过滤器没有效果。

safeseq

safe过滤器应用于序列的每个元素。与操作序列的其他过滤器(如join)一起使用时很有用。例如:

{{ some_list|safeseq|join:", " }} 

在这种情况下,您不能直接使用safe过滤器,因为它首先会将变量转换为字符串,而不是处理序列的各个元素。

切片

返回列表的一个切片。使用与 Python 列表切片相同的语法。

slugify

转换为 ASCII。将空格转换为连字符。删除非字母数字、下划线或连字符的字符。转换为小写。还会去除前导和尾随空格。

stringformat

根据参数格式化变量,一个字符串格式化说明符。此说明符使用 Python 字符串格式化语法,唯一的例外是省略了前导%。

去除标签

尽一切可能去除所有[X]HTML 标记。例如:

{{ value|striptags }} 

时间

根据给定的格式格式化时间。给定的格式可以是预定义的TIME_FORMAT,也可以是与date过滤器相同的自定义格式。

timesince

将日期格式化为自那日期以来的时间(例如,4 天,6 小时)。接受一个可选参数,该参数是包含要用作比较点的日期的变量(没有参数,则比较点是now)。

timeuntil

从现在起测量到给定日期或datetime的时间。

标题

通过使单词以大写字母开头并将其余字符转换为小写,将字符串转换为标题大小写。

truncatechars

如果字符串长度超过指定的字符数,则截断字符串。截断的字符串将以可翻译的省略号序列(...)结尾。例如:

{{ value|truncatechars:9 }} 

truncatechars_html

类似于truncatechars,只是它知道 HTML 标记。

truncatewords

在一定数量的单词后截断字符串。

truncatewords_html

类似于truncatewords,只是它知道 HTML 标记。

unordered_list

递归地获取自我嵌套列表并返回一个不带开放和关闭标签的 HTML 无序列表。

上限

将字符串转换为大写。

urlencode

为在 URL 中使用而转义值。

urlize

将文本中的 URL 和电子邮件地址转换为可点击的链接。此模板标签适用于以http://https://www.为前缀的链接。

urlizetrunc

将 URL 和电子邮件地址转换为可点击的链接,就像urlize一样,但截断超过给定字符限制的 URL。例如:

{{ value|urlizetrunc:15 }} 

如果valueCheck out www.djangoproject.com,输出将是Check out <a href="http://www.djangoproject.com" rel="nofollow">www.djangopr...</a>。与urlize一样,此过滤器只应用于纯文本。

wordcount

返回单词数。

wordwrap

在指定的行长度处包装单词。

yesno

将真、假和(可选)无映射值为字符串 yes、no、maybe,或作为逗号分隔列表传递的自定义映射之一,并根据值返回其中之一:例如:

{{ value|yesno:"yeah,no,maybe" }} 

国际化标签和过滤器

Django 提供模板标签和过滤器来控制模板中国际化的每个方面。它们允许对翻译、格式化和时区转换进行细粒度控制。

i18n

此库允许在模板中指定可翻译的文本。要启用它,请将USE_I18N设置为True,然后使用{% load i18n %}加载它。

l10n

这个库提供了对模板中数值本地化的控制。你只需要使用{% load l10n %}加载库,但通常会将USE_L10N设置为True,以便默认情况下启用本地化。

tz

这个库提供了对模板中时区转换的控制。像l10n一样,你只需要使用{% load tz %}加载库,但通常也会将USE_TZ设置为True,以便默认情况下进行本地时间转换。请参阅模板中的时区。

其他标签和过滤器库

static

要链接到保存在STATIC_ROOT中的静态文件,Django 附带了一个static模板标签。无论你是否使用RequestContext,你都可以使用它。

{% load static %} 
<img src="img/{% static "images/hi.jpg" %}" alt="Hi!" /> 

它还能够使用标准上下文变量,例如,假设一个user_stylesheet变量被传递给模板:

{% load static %} 
<link rel="stylesheet" href="{% static user_stylesheet %}" type="text/css" media="screen" /> 

如果你想要检索静态 URL 而不显示它,你可以使用稍微不同的调用:

{% load static %} 
{% static "images/hi.jpg" as myphoto %} 
<img src="img/{{ myphoto }}"></img> 

staticfiles contrib 应用程序还附带了一个static 模板标签,它使用staticfiles STATICFILES_STORAGE来构建给定路径的 URL(而不仅仅是使用STATIC_URL设置和给定路径的urllib.parse.urljoin())。如果你有高级用例,比如使用云服务来提供静态文件,那就使用它:

{% load static from staticfiles %} 
<img src="img/{% static "images/hi.jpg" %}" alt="Hi!" /> 

get_static_prefix

你应该优先使用static模板标签,但如果你需要更多控制STATIC_URL被注入到模板的位置和方式,你可以使用get_static_prefix模板标签:

{% load static %} 
<img src="img/hi.jpg" alt="Hi!" /> 

还有第二种形式,如果你需要多次使用该值,可以避免额外的处理:

{% load static %} 
{% get_static_prefix as STATIC_PREFIX %} 

<img src="img/hi.jpg" alt="Hi!" /> 
<img src="img/hi2.jpg" alt="Hello!" /> 

get_media_prefix

类似于get_static_prefixget_media_prefix会用媒体前缀MEDIA_URL填充模板变量,例如:

<script type="text/javascript" charset="utf-8"> 
var media_path = '{% get_media_prefix %}'; 
</script> 

Django 还附带了一些其他模板标签库,你必须在INSTALLED_APPS设置中显式启用它们,并在模板中使用{% load %}标签启用它们。

附录 F. 请求和响应对象

Django 使用请求和响应对象通过系统传递状态。

当请求页面时,Django 会创建一个包含有关请求的元数据的HttpRequest对象。然后 Django 加载适当的视图,将HttpRequest作为第一个参数传递给视图函数。每个视图负责返回一个HttpResponse对象。

本文档解释了django.http模块中定义的HttpRequestHttpResponse对象的 API。

HttpRequest 对象

属性

除非以下另有说明,否则应将所有属性视为只读。session是一个值得注意的例外。

HttpRequest.scheme

表示请求的方案(通常是httphttps)的字符串。

HttpRequest.body

原始的 HTTP 请求正文作为字节字符串。这对于以不同于常规 HTML 表单的方式处理数据很有用:二进制图像,XML 有效负载等。要处理常规表单数据,请使用HttpRequest.POST

您还可以使用类似文件的接口从 HttpRequest 中读取。请参阅HttpRequest.read()

HttpRequest.path

表示所请求页面的完整路径的字符串,不包括域名。

示例:/music/bands/the_beatles/

HttpRequest.path_info

在某些 Web 服务器配置下,主机名后的 URL 部分被分成脚本前缀部分和路径信息部分。path_info属性始终包含路径信息部分的路径,无论使用的是哪个 Web 服务器。使用这个而不是path可以使您的代码更容易在测试和部署服务器之间移动。

例如,如果您的应用程序的WSGIScriptAlias设置为/minfo,那么path可能是/minfo/music/bands/the_beatles/,而path_info将是/music/bands/the_beatles/

HttpRequest.method

表示请求中使用的 HTTP 方法的字符串。这是保证大写的。例如:

if request.method == 'GET':
     do_something() elif request.method == 'POST':
     do_something_else() 

HttpRequest.encoding

表示用于解码表单提交数据的当前编码的字符串(或None,表示使用DEFAULT_CHARSET设置)。您可以写入此属性以更改访问表单数据时使用的编码。

任何后续的属性访问(例如从GETPOST读取)将使用新的encoding值。如果您知道表单数据不是使用DEFAULT_CHARSET编码的,则这很有用。

HttpRequest.GET

包含所有给定的 HTTP GET参数的类似字典的对象。请参阅下面的QueryDict文档。

HttpRequest.POST

包含所有给定的 HTTP POST参数的类似字典的对象,前提是请求包含表单数据。请参阅下面的QueryDict文档。

如果您需要访问请求中发布的原始或非表单数据,请通过HttpRequest.body属性访问。

可能会通过POST以空的POST字典形式进行请求-例如,通过POST HTTP 方法请求表单,但不包括表单数据。因此,您不应该使用if request.POST来检查是否使用了POST方法;而是使用if request.method == 'POST'(见上文)。

注意:POST不包括文件上传信息。请参阅FILES

HttpRequest.COOKIES

包含所有 cookie 的标准 Python 字典。键和值都是字符串。

HttpRequest.FILES

包含所有上传文件的类似字典的对象。FILES中的每个键都是<input type="file" name="" />中的nameFILES中的每个值都是UploadedFile

请注意,如果请求方法是POST,并且<form>发布到请求的enctype="multipart/form-data",则FILES将只包含数据。否则,FILES将是一个空的类似字典的对象。

HttpRequest.META

包含所有可用的 HTTP 标头的标准 Python 字典。可用的标头取决于客户端和服务器,但这里有一些示例:

  • CONTENT_LENGTH:请求正文的长度(作为字符串)

  • CONTENT_TYPE: 请求主体的 MIME 类型

  • HTTP_ACCEPT_ENCODING: 响应的可接受编码

  • HTTP_ACCEPT_LANGUAGE: 响应的可接受语言

  • HTTP_HOST: 客户端发送的 HTTP Host 标头

  • HTTP_REFERER: 引用页面(如果有)

  • HTTP_USER_AGENT: 客户端的用户代理字符串

  • QUERY_STRING: 查询字符串,作为单个(未解析的)字符串

  • REMOTE_ADDR: 客户端的 IP 地址

  • REMOTE_HOST: 客户端的主机名

  • REMOTE_USER: 由 Web 服务器认证的用户(如果有)

  • REQUEST_METHOD: 诸如"GET"或"POST"的字符串

  • SERVER_NAME: 服务器的主机名

  • SERVER_PORT: 服务器的端口(作为字符串)

除了CONTENT_LENGTHCONTENT_TYPE之外,请求中的任何 HTTP 标头都会通过将所有字符转换为大写字母,将任何连字符替换为下划线,并在名称前添加HTTP_前缀来转换为META键。因此,例如,名为X-Bender的标头将被映射到METAHTTP_X_BENDER

HttpRequest.user

表示当前已登录用户的AUTH_USER_MODEL类型的对象。如果用户当前未登录,user将被设置为django.contrib.auth.models.AnonymousUser的实例。您可以使用is_authenticated()来区分它们,如下所示:

if request.user.is_authenticated():
     # Do something for logged-in users. else:
     # Do something for anonymous users. 

只有在您的 Django 安装已激活AuthenticationMiddleware时,user才可用。

HttpRequest.session

一个可读写的类似字典的对象,表示当前会话。只有在您的 Django 安装已激活会话支持时才可用。

HttpRequest.urlconf

Django 本身未定义,但如果其他代码(例如自定义中间件类)设置了它,它将被读取。当存在时,它将被用作当前请求的根 URLconf,覆盖ROOT_URLCONF设置。

HttpRequest.resolver_match

表示已解析 URL 的ResolverMatch的实例。此属性仅在 URL 解析发生后设置,这意味着它在所有视图中都可用,但在执行 URL 解析之前执行的中间件方法中不可用(例如process_request,您可以改用process_view)。

方法

HttpRequest.get_host()

使用HTTP_X_FORWARDED_HOST(如果启用了USE_X_FORWARDED_HOST)和HTTP_HOST标头的信息返回请求的原始主机,按顺序。如果它们没有提供值,该方法将使用SERVER_NAMESERVER_PORT的组合,详见 PEP 3333。

示例:127.0.0.1:8000

注意

当主机位于多个代理后面时,get_host()方法会失败。一个解决方案是使用中间件来重写代理标头,就像以下示例中的那样:

class MultipleProxyMiddleware(object):
     FORWARDED_FOR_FIELDS = [
         'HTTP_X_FORWARDED_FOR',
         'HTTP_X_FORWARDED_HOST',
         'HTTP_X_FORWARDED_SERVER',
     ]
     def process_request(self, request):
         """
         Rewrites the proxy headers so that only the most
         recent proxy is used.
         """
         for field in self.FORWARDED_FOR_FIELDS:
             if field in request.META:
                 if ',' in request.META[field]:
                     parts = request.META[field].split(',')
                     request.META[field] = parts[-1].strip() 

此中间件应该放置在依赖于get_host()值的任何其他中间件之前,例如CommonMiddlewareCsrfViewMiddleware

HttpRequest.get_full_path()

返回path,以及附加的查询字符串(如果适用)。

示例:/music/bands/the_beatles/?print=true

HttpRequest.build_absolute_uri(location)

返回location的绝对 URI 形式。如果未提供位置,则位置将设置为request.get_full_path()

如果位置已经是绝对 URI,则不会被改变。否则,将使用此请求中可用的服务器变量构建绝对 URI。

示例:http://example.com/music/bands/the_beatles/?print=true

HttpRequest.get_signed_cookie()

返回已签名 cookie 的值,如果签名不再有效,则引发django.core.signing.BadSignature异常。如果提供default参数,异常将被抑制,而将返回该默认值。

可选的salt参数可用于提供额外的保护,防止对您的秘密密钥进行暴力攻击。如果提供了salt参数,将根据附加到 cookie 值的签名时间戳检查max_age参数,以确保 cookie 的年龄不超过max_age秒。

例如:

>>> request.get_signed_cookie('name') 
'Tony' 
>>> request.get_signed_cookie('name', salt='name-salt') 
'Tony' # assuming cookie was set using the same salt 
>>> request.get_signed_cookie('non-existing-cookie') 
...
KeyError: 'non-existing-cookie' 
>>> request.get_signed_cookie('non-existing-cookie', False) 
False 
>>> request.get_signed_cookie('cookie-that-was-tampered-with') 
... 
BadSignature: ... 
>>> request.get_signed_cookie('name', max_age=60)
...
SignatureExpired: Signature age 1677.3839159 > 60 seconds 
>>> request.get_signed_cookie('name', False, max_age=60) 
False

HttpRequest.is_secure()

如果请求是安全的,则返回True;也就是说,如果是通过 HTTPS 发出的请求。

HttpRequest.is_ajax()

通过检查HTTP_X_REQUESTED_WITH标头中的字符串"XMLHttpRequest",如果请求是通过XMLHttpRequest发出的,则返回True。大多数现代 JavaScript 库都会发送此标头。如果您自己编写XMLHttpRequest调用(在浏览器端),如果要使is_ajax()起作用,您将不得不手动设置此标头。

如果响应因是否通过 AJAX 请求而有所不同,并且您正在使用类似 Django 的cache middleware的某种缓存形式,则应该使用vary_on_headers('HTTP_X_REQUESTED_WITH')装饰视图,以便正确缓存响应。

HttpRequest.read(size=None)

HttpRequest.readline()

HttpRequest.readlines()

HttpRequest.xreadlines()

HttpRequest.iter()

实现从HttpRequest实例读取的类似文件的接口的方法。这使得可以以流式方式消耗传入的请求。一个常见的用例是使用迭代解析器处理大型 XML 有效负载,而不必在内存中构造整个 XML 树。

根据这个标准接口,可以直接将HttpRequest实例传递给诸如ElementTree之类的 XML 解析器:

import xml.etree.ElementTree as ET 
for element in ET.iterparse(request):
     process(element) 

QueryDict 对象

HttpRequest对象中,GETPOST属性是django.http.QueryDict的实例,这是一个类似字典的类,专门用于处理同一键的多个值。这是必要的,因为一些 HTML 表单元素,特别是<select multiple>,会传递同一键的多个值。

在正常的请求/响应周期中,request.POSTrequest.GET中的QueryDict将是不可变的。要获得可变版本,您需要使用.copy()

方法

QueryDict实现了所有标准字典方法,因为它是字典的子类,但有以下例外。

QueryDict.init()

基于query_string实例化一个QueryDict对象。

>>> QueryDict('a=1&a=2&c=3') 
<QueryDict: {'a': ['1', '2'], 'c': ['3']}>

如果未传递query_string,则生成的QueryDict将为空(它将没有键或值)。

您遇到的大多数QueryDict,特别是request.POSTrequest.GET中的那些,将是不可变的。如果您自己实例化一个,可以通过在其__init__()中传递mutable=True来使其可变。

用于设置键和值的字符串将从encoding转换为 Unicode。如果未设置编码,则默认为DEFAULT_CHARSET

QueryDict.getitem(key)

返回给定键的值。如果键有多个值,__getitem__()将返回最后一个值。如果键不存在,则引发django.utils.datastructures.MultiValueDictKeyError

QueryDict.setitem(key, value)

将给定的键设置为[value](一个 Python 列表,其单个元素为value)。请注意,像其他具有副作用的字典函数一样,只能在可变的QueryDict上调用(例如通过copy()创建的QueryDict)。

QueryDict.contains(key)

如果设置了给定的键,则返回True。这使您可以执行例如if "foo" in request.GET

QueryDict.get(key, default)

使用与上面的__getitem__()相同的逻辑,具有返回默认值的钩子,如果键不存在。

QueryDict.setdefault(key, default)

与标准字典的setdefault()方法一样,只是它在内部使用__setitem__()

QueryDict.update(other_dict)

接受QueryDict或标准字典。就像标准字典的update()方法一样,只是它将项目附加到当前字典项,而不是替换它们。例如:

>>> q = QueryDict('a=1', mutable=True) 
>>> q.update({'a': '2'}) 
>>> q.getlist('a')
['1', '2'] 
>>> q['a'] # returns the last 
['2']

QueryDict.items()

与标准字典items()方法类似,只是这使用与__getitem__()相同的最后值逻辑。例如:

>>> q = QueryDict('a=1&a=2&a=3') 
>>> q.items() 
[('a', '3')]

QueryDict.iteritems()

与标准字典iteritems()方法类似。与QueryDict.items()一样,这使用与QueryDict.__getitem__()相同的最后值逻辑。

QueryDict.iterlists()

QueryDict.iteritems()类似,只是它包括字典的每个成员的所有值作为列表。

QueryDict.values()

与标准字典values()方法类似,只是这使用与__getitem__()相同的最后值逻辑。例如:

>>> q = QueryDict('a=1&a=2&a=3') 
>>> q.values() 
['3']

QueryDict.itervalues()

QueryDict.values()类似,只是一个迭代器。

此外,QueryDict有以下方法:

QueryDict.copy()

返回对象的副本,使用 Python 标准库中的copy.deepcopy()。即使原始对象不是可变的,此副本也将是可变的。

QueryDict.getlist(key, default)

返回请求的键的数据,作为 Python 列表。如果键不存在且未提供默认值,则返回空列表。除非默认值不是列表,否则保证返回某种列表。

QueryDict.setlist(key, list)

将给定的键设置为list_(与__setitem__()不同)。

QueryDict.appendlist(key, item)

将项目附加到与键关联的内部列表。

QueryDict.setlistdefault(key, default_list)

setdefault类似,只是它接受值的列表而不是单个值。

QueryDict.lists()

items()类似,只是它包括字典的每个成员的所有值作为列表。例如:

>>> q = QueryDict('a=1&a=2&a=3') 
>>> q.lists() 
[('a', ['1', '2', '3'])]

QueryDict.pop(key)

返回给定键的值列表并从字典中删除它们。如果键不存在,则引发KeyError。例如:

>>> q = QueryDict('a=1&a=2&a=3', mutable=True) 
>>> q.pop('a') 
['1', '2', '3']

QueryDict.popitem()

删除字典的任意成员(因为没有排序的概念),并返回一个包含键和键的所有值的列表的两个值元组。在空字典上调用时引发KeyError。例如:

>>> q = QueryDict('a=1&a=2&a=3', mutable=True) 
>>> q.popitem() 
('a', ['1', '2', '3'])

QueryDict.dict()

返回QueryDictdict表示。对于QueryDict中的每个(key,list)对,dict将有(key,item),其中 item 是列表的一个元素,使用与QueryDict.__getitem__()相同的逻辑:

>>> q = QueryDict('a=1&a=3&a=5') 
>>> q.dict() 
{'a': '5'}

QueryDict.urlencode([safe])

返回查询字符串格式的数据字符串。例如:

>>> q = QueryDict('a=2&b=3&b=5') 
>>> q.urlencode() 
'a=2&b=3&b=5'

可选地,urlencode 可以传递不需要编码的字符。例如:

>>> q = QueryDict(mutable=True) 
>>> q['next'] = '/a&b/' 
>>> q.urlencode(safe='/') 
'next=/a%26b/'

HttpResponse 对象

与 Django 自动创建的HttpRequest对象相反,HttpResponse对象是您的责任。您编写的每个视图都负责实例化,填充和返回HttpResponse

HttpResponse类位于django.http模块中。

用法

传递字符串

典型用法是将页面内容作为字符串传递给HttpResponse构造函数:

>>> from django.http import HttpResponse 
>>> response = HttpResponse("Here's the text of the Web page.") 
>>> response = HttpResponse("Text only, please.",
   content_type="text/plain")

但是,如果您想逐步添加内容,可以将response用作类似文件的对象:

>>> response = HttpResponse() 
>>> response.write("<p>Here's the text of the Web page.</p>") 
>>> response.write("<p>Here's another paragraph.</p>")

传递迭代器

最后,您可以传递HttpResponse一个迭代器而不是字符串。HttpResponse将立即消耗迭代器,将其内容存储为字符串,并丢弃它。

如果您需要响应从迭代器流式传输到客户端,则必须使用StreamingHttpResponse类。

设置头字段

要设置或删除响应中的头字段,请将其视为字典:

>>> response = HttpResponse() 
>>> response['Age'] = 120 
>>> del response['Age']

请注意,与字典不同,如果头字段不存在,del不会引发KeyError

为了设置Cache-ControlVary头字段,建议使用django.utils.cache中的patch_cache_control()patch_vary_headers()方法,因为这些字段可以有多个逗号分隔的值。修补方法确保其他值,例如中间件添加的值,不会被移除。

HTTP 头字段不能包含换行符。尝试设置包含换行符(CR 或 LF)的头字段将引发BadHeaderError

告诉浏览器将响应视为文件附件

要告诉浏览器将响应视为文件附件,请使用content_type参数并设置Content-Disposition标头。例如,这是如何返回 Microsoft Excel 电子表格的方式:

>>> response = HttpResponse
  (my_data, content_type='application/vnd.ms-excel') 
>>> response['Content-Disposition'] = 'attachment; filename="foo.xls"'

Content-Disposition标头与 Django 无关,但很容易忘记语法,因此我们在这里包含了它。

属性

HttpResponse.content

表示内容的字节串,如果需要,从 Unicode 对象编码而来。

HttpResponse.charset

表示响应将被编码的字符集的字符串。如果在HttpResponse实例化时未给出,则将从content_type中提取,如果不成功,则将使用DEFAULT_CHARSET设置。

HttpResponse.status_code

响应的 HTTP 状态码。

HttpResponse.reason_phrase

响应的 HTTP 原因短语。

HttpResponse.streaming

这总是False

此属性存在,以便中间件可以将流式响应与常规响应区分对待。

HttpResponse.closed

如果响应已关闭,则为True

方法

HttpResponse.init()

HttpResponse.__init__(content='', 
  content_type=None, status=200, reason=None, charset=None) 

使用给定的页面内容和内容类型实例化HttpResponse对象。content应该是迭代器或字符串。如果它是迭代器,它应该返回字符串,并且这些字符串将被连接在一起形成响应的内容。如果它不是迭代器或字符串,在访问时将被转换为字符串。有四个参数:

  • content_type是可选地由字符集编码完成的 MIME 类型,并用于填充 HTTP Content-Type标头。如果未指定,它将由DEFAULT_CONTENT_TYPEDEFAULT_CHARSET设置形成,默认为:text/html; charset=utf-8。

  • status是响应的 HTTP 状态码。

  • reason是 HTTP 响应短语。如果未提供,将使用默认短语。

  • charset是响应将被编码的字符集。如果未给出,将从content_type中提取,如果不成功,则将使用DEFAULT_CHARSET设置。

HttpResponse.setitem(header, value)

将给定的标头名称设置为给定的值。headervalue都应该是字符串。

HttpResponse.delitem(header)

删除具有给定名称的标头。如果标头不存在,则静默失败。不区分大小写。

HttpResponse.getitem(header)

返回给定标头名称的值。不区分大小写。

HttpResponse.has_header(header)

基于对具有给定名称的标头进行不区分大小写检查,返回TrueFalse

HttpResponse.setdefault(header, value)

除非已经设置了标头,否则设置标头。

HttpResponse.set_cookie()

HttpResponse.set_cookie(key, value='', 
  max_age=None, expires=None, path='/', 
  domain=None, secure=None, httponly=False) 

设置 cookie。参数与 Python 标准库中的Morsel cookie 对象中的参数相同。

  • max_age应该是秒数,或者None(默认),如果 cookie 只应该持续客户端浏览器会话的时间。如果未指定expires,将进行计算。

  • expires应该是格式为"Wdy, DD-Mon-YY HH:MM:SS GMT"的字符串,或者是 UTC 中的datetime.datetime对象。如果expiresdatetime对象,则将计算max_age

  • 如果要设置跨域 cookie,请使用domain。例如,domain=".lawrence.com"将设置一个可由 www.lawrence.com、blogs.lawrence.com 和 calendars.lawrence.com 读取的 cookie。否则,cookie 只能由设置它的域读取。

  • 如果要防止客户端 JavaScript 访问 cookie,请使用httponly=True

HTTPOnly是包含在 Set-Cookie HTTP 响应标头中的标志。它不是 RFC 2109 标准的一部分,并且并非所有浏览器都一致地遵守。但是,当它被遵守时,它可以是减轻客户端脚本访问受保护的 cookie 数据的有用方式。

HttpResponse.set_signed_cookie()

set_cookie()类似,但在设置之前对 cookie 进行加密签名。与HttpRequest.get_signed_cookie()一起使用。您可以使用可选的salt参数来增加密钥强度,但您需要记住将其传递给相应的HttpRequest.get_signed_cookie()调用。

HttpResponse.delete_cookie()

删除具有给定键的 cookie。如果键不存在,则静默失败。

由于 cookie 的工作方式,pathdomain应该与您在set_cookie()中使用的值相同-否则可能无法删除 cookie。

HttpResponse.write(content)

HttpResponse.flush()

HttpResponse.tell()

这些方法实现了与HttpResponse类似的文件接口。它们与相应的 Python 文件方法的工作方式相同。

HttpResponse.getvalue()

返回HttpResponse.content的值。此方法使HttpResponse实例成为类似流的对象。

HttpResponse.writable()

始终为True。此方法使HttpResponse实例成为类似流的对象。

HttpResponse.writelines(lines)

将一系列行写入响应。不会添加行分隔符。此方法使HttpResponse实例成为类似流的对象。

HttpResponse 子类

Django 包括许多处理不同类型 HTTP 响应的HttpResponse子类。与HttpResponse一样,这些子类位于django.http中。

HttpResponseRedirect

构造函数的第一个参数是必需的-重定向的路径。这可以是一个完全合格的 URL(例如,www.yahoo.com/search/)或者没有域的绝对路径(例如,/search/)。查看HttpResponse以获取其他可选的构造函数参数。请注意,这会返回一个 HTTP 状态码 302。

HttpResponsePermanentRedirect

HttpResponseRedirect类似,但返回永久重定向(HTTP 状态码 301)而不是找到重定向(状态码 302)。

HttpResponseNotModified

构造函数不接受任何参数,也不应向此响应添加任何内容。使用此方法指定自上次用户请求以来页面未被修改(状态码 304)。

HttpResponseBadRequest

行为与HttpResponse相同,但使用 400 状态码。

HttpResponseNotFound

行为与HttpResponse相同,但使用 404 状态码。

HttpResponseForbidden

行为与HttpResponse相同,但使用 403 状态码。

HttpResponseNotAllowed

HttpResponse类似,但使用 405 状态码。构造函数的第一个参数是必需的:允许的方法列表(例如,['GET', 'POST'])。

HttpResponseGone

行为与HttpResponse相同,但使用 410 状态码。

HttpResponseServerError

行为与HttpResponse相同,但使用 500 状态码。

如果HttpResponse的自定义子类实现了render方法,Django 将把它视为模拟SimpleTemplateResponse,并且render方法本身必须返回一个有效的响应对象。

JsonResponse 对象

class JsonResponse(data, encoder=DjangoJSONEncoder, safe=True, **kwargs) 

帮助创建 JSON 编码响应的HttpResponse子类。它继承了大部分行为,但有一些不同之处:

  • 其默认的Content-Type头设置为application/json

  • 第一个参数data应该是一个dict实例。如果将safe参数设置为False(见下文),则可以是任何可 JSON 序列化的对象。

  • encoder默认为django.core.serializers.json.DjangoJSONEncoder,将用于序列化数据。

safe布尔参数默认为True。如果设置为False,则可以传递任何对象进行序列化(否则只允许dict实例)。如果safeTrue,并且将非dict对象作为第一个参数传递,将引发TypeError

用法

典型的用法可能如下:

>>> from django.http import JsonResponse >>> response = JsonResponse({'foo': 'bar'}) >>> response.content '{"foo": "bar"}'

序列化非字典对象

为了序列化除dict之外的对象,您必须将safe参数设置为False

response = JsonResponse([1, 2, 3], safe=False) 

如果不传递safe=False,将引发TypeError

更改默认的 JSON 编码器

如果需要使用不同的 JSON 编码器类,可以将encoder参数传递给构造方法:

response = JsonResponse(data, encoder=MyJSONEncoder) 

StreamingHttpResponse 对象

StreamingHttpResponse类用于从 Django 向浏览器流式传输响应。如果生成响应需要太长时间或使用太多内存,你可能会想要这样做。例如,用于生成大型 CSV 文件非常有用。

性能考虑

Django 设计用于短暂的请求。流式响应将会绑定一个工作进程,直到响应完成。这可能导致性能不佳。

一般来说,你应该在请求-响应周期之外执行昂贵的任务,而不是使用流式响应。

StreamingHttpResponse不是HttpResponse的子类,因为它具有稍微不同的 API。但是,它几乎是相同的,具有以下显着的区别:

  • 它应该给出一个产生字符串作为内容的迭代器。

  • 除了迭代响应对象本身,你无法访问它的内容。这只能在响应返回给客户端时发生。

  • 它没有content属性。相反,它有一个streaming_content属性。

  • 你不能使用类似文件的对象的tell()write()方法。这样做会引发异常。

StreamingHttpResponse应该只在绝对需要在将数据传输给客户端之前不迭代整个内容的情况下使用。因为无法访问内容,许多中间件无法正常工作。例如,对于流式响应,无法生成ETagContent-Length标头。

属性

StreamingHttpResponse具有以下属性:

    • *.streaming_content. 一个表示内容的字符串的迭代器。
    • *.status_code. 响应的 HTTP 状态码。
    • *.reason_phrase. 响应的 HTTP 原因短语。
    • *.streaming. 这总是True

FileResponse 对象

FileResponse是针对二进制文件进行了优化的StreamingHttpResponse的子类。如果 wsgi 服务器提供了wsgi.file_wrapper,它将使用它,否则它会以小块流式传输文件。

FileResponse期望以二进制模式打开的文件,如下所示:

>>> from django.http import FileResponse 
>>> response = FileResponse(open('myfile.png', 'rb'))

错误视图

Django 默认提供了一些视图来处理 HTTP 错误。要使用自定义视图覆盖这些视图,请参阅自定义错误视图。

404(页面未找到)视图

defaults.page_not_found(request, template_name='404.html')

当你在视图中引发Http404时,Django 会加载一个专门处理 404 错误的特殊视图。默认情况下,它是视图django.views.defaults.page_not_found(),它要么生成一个非常简单的未找到消息,要么加载和呈现模板404.html(如果你在根模板目录中创建了它)。

默认的 404 视图将向模板传递一个变量:request_path,这是导致错误的 URL。

关于 404 视图有三件事需要注意:

  • 如果 Django 在检查 URLconf 中的每个正则表达式后找不到匹配项,也会调用 404 视图。

  • 404 视图传递一个RequestContext,并且将可以访问由你的模板上下文处理器提供的变量(例如MEDIA_URL)。

  • 如果DEBUG设置为True(在你的设置模块中),那么你的 404 视图将永远不会被使用,而且你的 URLconf 将被显示出来,带有一些调试信息。

500(服务器错误)视图

defaults.server_error(request, template_name='500.html')

同样地,Django 在视图代码运行时出现运行时错误的情况下执行特殊行为。如果视图导致异常,Django 将默认调用视图django.views.defaults.server_error,它要么生成一个非常简单的服务器错误消息,要么加载和呈现模板500.html(如果你在根模板目录中创建了它)。

默认的 500 视图不会向500.html模板传递任何变量,并且使用空的Context进行呈现,以减少额外错误的可能性。

如果DEBUG设置为True(在您的设置模块中),则永远不会使用您的 500 视图,而是显示回溯信息,附带一些调试信息。

403(HTTP Forbidden)视图

defaults.permission_denied(request, template_name='403.html')

与 404 和 500 视图一样,Django 还有一个视图来处理 403 Forbidden 错误。如果视图导致 403 异常,那么 Django 将默认调用视图django.views.defaults.permission_denied

此视图加载并呈现根模板目录中的模板403.html,如果该文件不存在,则根据 RFC 2616(HTTP 1.1 规范)提供文本 403 Forbidden。

django.views.defaults.permission_deniedPermissionDenied异常触发。要在视图中拒绝访问,可以使用以下代码:

from django.core.exceptions import PermissionDenied

def edit(request, pk):
     if not request.user.is_staff:
         raise PermissionDenied
     # ... 

400(错误请求)视图

defaults.bad_request(request, template_name='400.html')

当 Django 中引发SuspiciousOperation时,可能会由 Django 的某个组件处理(例如重置会话数据)。如果没有特别处理,Django 将认为当前请求是'bad request'而不是服务器错误。

django.views.defaults.bad_request,在其他方面与server_error视图非常相似,但返回状态码 400,表示错误条件是客户端操作的结果。

DEBUGFalse时,也只有bad_request视图才会被使用。

自定义错误视图

Django 中的默认错误视图应该适用于大多数 Web 应用程序,但如果需要任何自定义行为,可以轻松地覆盖它们。只需在 URLconf 中指定处理程序(在其他任何地方设置它们都不会起作用)。

page_not_found()视图被handler404覆盖:

handler404 = 'mysite.views.my_custom_page_not_found_view' 

server_error()视图被handler500覆盖:

handler500 = 'mysite.views.my_custom_error_view' 

permission_denied()视图被handler403覆盖:

handler403 = 'mysite.views.my_custom_permission_denied_view' 

bad_request()视图被handler400覆盖:

handler400 = 'mysite.views.my_custom_bad_request_view' 

附录 G. 使用 Visual Studio 开发 Django

不管您在互联网上听到了什么,微软 Visual Studio(VS)一直是一个非常强大和功能齐全的集成开发环境(IDE)。作为多平台开发人员,我尝试过几乎所有其他的东西,最终都回到了 VS。

过去,更广泛使用 VS 的最大障碍是(在我看来):

  • 缺乏对微软生态系统之外的语言(C++、C#和 VB)的良好支持

  • 完整功能的 IDE 成本。以前的微软“免费”IDE 版本在专业开发方面有些不够用。

几年前发布了 Visual Studio 社区版,最近发布了 Python Tools for Visual Studio(PTVS),这种情况已经发生了戏剧性的变化。因此,我现在在 VS 中进行所有开发工作-包括 Microsoft 技术和 Python 和 Django。

我不打算继续谈论 VS 的优点,以免开始听起来像是在为微软做广告,所以让我们假设您至少已经决定尝试一下 VS 和 PTVS。

首先,我将解释如何在 Windows 上安装 VS 和 PTVS,然后我将简要概述您可以使用的所有酷炫的 Django 和 Python 工具。

安装 Visual Studio

注意

开始之前

因为它仍然是微软,我们无法忽视 VS 是一个庞大的安装程序。

为了最大程度地减少麻烦的可能性,请:

  1. 在安装过程中关闭您的防病毒软件

  2. 确保您有良好的互联网连接。有线连接比无线连接更好

  3. 暂停其他占用内存/磁盘空间的应用程序,如 OneDrive 和 Dropbox

  4. 关闭所有不必要打开的应用程序

在您仔细注意了前面的警告之后,访问 Visual Studio 网站(www.visualstudio.com/)并下载免费的 Visual Studio 社区版 2015(图 G.1):

安装 Visual Studio

图 G.1:Visual Studio 下载

启动下载的安装程序文件,确保选择默认安装选项(图 G.2)并点击安装:

安装 Visual Studio

图 G.2:Visual Studio 的默认安装

现在是时候去冲杯咖啡了。或者七杯。记住,微软-这需要一些时间。根据您的互联网连接,这可能需要 15 分钟到一小时以上的时间。

在极少数情况下会失败。这总是(根据我的经验)要么是忘记关闭防病毒软件,要么是您的互联网连接短暂中断。幸运的是,VS 的恢复过程非常强大,我发现在失败后重新启动安装过程每次都有效。VS 甚至会记住它的进度,所以您不必从头开始。

安装 PTVS 和 Web Essentials

安装完 VS 后,是时候添加 Python Tools for Visual Studio(PTVS)和 Visual Studio Web Essentials 了。从顶部菜单中选择工具 > 扩展和更新图 G.3):

安装 PTVS 和 Web Essentials

图 G.3:安装 Visual Studio 的扩展

一旦打开“扩展和更新”窗口,从左侧的下拉菜单中选择在线,进入 VS 在线应用程序库。在右上角的搜索框中输入python,PTVS 扩展应该出现在列表的顶部(图 G.4):

安装 PTVS 和 Web Essentials

图 G.4:安装 PTVS 扩展

重复相同的过程安装 VS Web Essentials(图 G.5)。请注意,根据 VS 的版本和之前安装的扩展,Web Essentials 可能已经安装。如果是这种情况,下载按钮将被替换为绿色的勾号图标:

安装 PTVS 和 Web Essentials

图 G.5:安装 Web Essentials 扩展

创建一个 Django 项目

在 VS 中进行 Django 开发的一大优势是,除了 VS 之外,您只需要安装 Python。因此,如果您按照第一章的说明安装了 Python,那么除了 VS 以外没有其他事情要做-VS 会处理虚拟环境,安装您需要的任何 Python 模块,甚至在 IDE 中内置了所有 Django 的管理命令。

为了演示这些功能,让我们从第一章的介绍 Django 和入门中创建我们的mysite项目,但这次我们将在 VS 内部完成所有操作。

开始一个 Django 项目

从顶部菜单中选择文件 > 新建 > 项目,然后从左侧的下拉菜单中选择一个 Python Web 项目。您应该看到类似图 G.6的东西。选择一个空白的 Django Web 项目,为您的项目命名,然后单击确定:

开始一个 Django 项目

图 G.6:创建一个空白的 Django 项目

然后,Visual Studio 将显示一个弹出窗口,指出此项目需要外部包(图 G.7)。这里最简单的选项是直接安装到虚拟环境(选项 1),但这将安装最新版本的 Django,在撰写本文时是 1.9.7。由于本书是针对 1.8 LTS 版本的,我们希望选择选项 3我将自己安装它们,以便我们可以对requirements.txt文件进行必要的更改:

开始一个 Django 项目

图 G.7:安装外部包

项目安装完成后,您会注意到在 VS 屏幕右侧的“解决方案资源管理器”中,已为您创建了完整的 Django 项目结构。下一步是添加一个运行 Django 1.8 的虚拟环境。在撰写本文时,最新版本是 1.8.13,因此我们必须编辑我们的requirements.txt文件,使第一行读取:

django==1.8.13 

保存文件,然后在解决方案资源管理器中右键单击Python 环境,选择添加虚拟环境...图 G.8):

开始一个 Django 项目

图 G.8:添加虚拟环境

在弹出窗口中,将默认环境名称从env更改为更有意义的名称(如果您正在从第一章的示例中继续进行,Django 简介和入门,请使用env_mysite)。单击创建,VS 将为您创建一个虚拟环境(图 G.9):

注意

在使用 VS 时,您不必显式激活虚拟环境-您运行的任何代码都会自动在解决方案资源管理器中的活动虚拟环境中运行。

这对于像针对 Python 2.7 和 3.4 测试代码这样的情况非常有用-您只需右键单击并激活您想要运行的任何环境。

开始一个 Django 项目

图 G.9:创建虚拟环境

在 Visual Studio 中进行 Django 开发

微软公司已经付出了很多努力,以确保在 VS 中开发 Python 应用程序尽可能简单和无忧。对于初学者来说,最重要的功能是对所有 Python 和 Django 模块的完整智能感知。这将加速您的学习,因为您不必查看模块实现的文档。

VS 真正简化的 Python/Django 编程的另一个重要方面是:

  • Django 管理命令的集成

  • 轻松安装 Python 包

  • 轻松安装新的 Django 应用程序

Django 管理命令的集成

所有 Django 的常见管理命令都可以从项目菜单中找到(图 G.10):

Django 管理命令的集成

图 G.10:项目菜单上的常见 Django 命令

从此菜单中,您可以运行迁移,创建超级用户,打开 Django shell 并运行开发服务器。

轻松安装 Python 包

Python 包可以直接安装到任何虚拟环境中,只需在 Solution Explorer 中右键单击环境,然后选择安装 Python 包...图 G.11)。

包可以使用pipeasy_install安装。

轻松安装新的 Django 应用程序

最后,向项目添加新的 Django 应用程序就像右键单击项目并选择添加 > Django 应用程序...一样简单(图 G.12)。给您的应用程序命名,然后单击确定,VS 将向您的项目添加一个新应用程序:

轻松安装新的 Django 应用程序

图 G.11:安装 Python 包

轻松安装新的 Django 应用程序

图 G.12:添加 Django 应用程序

这只是 Visual Studio 可以做的事情的简要概述;只是为了让您开始。值得探索的其他事物包括:

  • VS 的存储库管理包括与本地 Git 存储库和 GitHub 的完全集成。

  • 使用免费的 MSDN 开发人员帐户部署到 Azure(仅在写作时支持 MySQL 和 SQLite)。

  • 内置混合模式调试器。例如,在同一个调试器中调试 Django 和 JavaScript。

  • 内置测试支持。

  • 我提到了完整的 IntelliSense 支持吗?

posted @ 2024-05-20 16:52  绝不原创的飞龙  阅读(15)  评论(0编辑  收藏  举报