Django-高级教程-三-

118 阅读1小时+

Django 高级教程(三)

原文:Pro Django

协议:CC BY-NC-SA 4.0

六、模板

Abstract

虽然第二章明确指出 Django 完全构建在 Python 之上,并且适用标准的 Python 规则,但模板是规则的例外。模板是 Django 生成基于文本的输出的方式,比如 HTML 或电子邮件,编辑这些文档的人可能没有任何 Python 经验。因此,模板被设计成避免直接使用 Python,而是倾向于使用专为 Django 构建的可扩展、易于使用的定制语言。

虽然第二章明确指出 Django 完全构建在 Python 之上,并且适用标准的 Python 规则,但模板是规则的例外。模板是 Django 生成基于文本的输出的方式,比如 HTML 或电子邮件,编辑这些文档的人可能没有任何 Python 经验。因此,模板被设计成避免直接使用 Python,而是倾向于使用专为 Django 构建的可扩展、易于使用的定制语言。

通过禁止任意的 Python 表达式,模板在某些方面肯定会受到限制,但是有两点需要记住。首先,模板系统是由 Python 支持的,就像 Django 中的其他东西一样,所以总是可以为特定的特性添加 Python 级别的代码。在模板本身中包含实际的 Python 代码是不好的形式,所以 Django 提供了插入额外代码的其他方法。

更重要的是,在模板和支持它们的 Python 代码之间划出一条清晰的界限,可以让两个不同背景和技能的群体一起工作。对于许多业余爱好者的项目来说,这可能听起来像是一种浪费,因为在网站上工作的人只有开发人员。然而,在许多商业环境中,开发人员通常是与维护网站内容和视觉结构的人员分开的一群人。

通过明确分离开发和模板编辑的任务,很容易建立一个环境,在这个环境中,开发人员从事他们真正需要的工作,而内容编辑和设计人员可以从事不需要开发经验的工作。Django 的模板本质上相当简单,任何人都很容易掌握,甚至没有任何编程经验的人。

模板语法的基本细节以及包含的标签和过滤器在其他地方有很好的描述。本章将不再关注这些更高层次的细节,而是涵盖如何加载、解析和呈现模板,如何在模板中管理变量,以及如何创建新的标签和过滤器。本质上,这是关于开发者可以做些什么来使他们的内容编辑同行的生活尽可能的简单。

模板是由什么组成的

尽管模板不是直接用 Python 编写的,但它们有 Python 的支持,使得所有好的东西成为可能。当从文件或其他来源读入模板的代码时,它被编译成 Python 对象的集合,这些对象负责在以后呈现它。对于基本的模板使用,可以忽略这些对象是如何工作的,但是和其他事情一样,正确的理解可以打开一个可能性的世界。

查看一下django.template包内部,Template类作为模板操作的起点非常突出,这是理所当然的。当一个模板被加载时,它的内容被传递给一个新的Template实例,还有一些关于模板本身来自哪里的可选信息。有三个参数被传入新的Template对象,这是一切的基础。

  • template_string—唯一必需的参数,它包含从文件中读取的模板的实际内容。这里最棒的是Template接受一个字符串,而不是文件名或打开的文件对象。只接受一个字符串——无论是 Unicode 字符串还是 UTF 8 编码的常规字符串——就可以从任何来源设置模板。在本章的应用技巧中可以找到一些有趣的用法。
  • origin—表示模板来源的对象,如模板加载器或只是一个原始字符串。只有当TEMPLATE_DEBUG设置为True时才使用它,通常可以忽略它而不会受到惩罚,但是最好将它包含在开发环境中,在那里它可以帮助调试涉及多个模板加载器的问题。
  • name—模板的名称,传递给任何请求它的加载程序(如果有的话)。这通常只是模板的相对路径,但理论上可以是在特定情况下有意义的任何路径。毕竟 Django 真正关心的是template_string;其余的只是在调试问题时有用。

Template的实际代码相当少,将大部分工作委托给一个名为compile_string()的实用函数,该函数解析原始文本,将其编译成一系列节点。这些节点只是 Python 对象,每个节点都是为模板的特定部分配置的。总的来说,它们代表了整个模板,从开始到结束,以一种更容易和更有效的方式呈现。

这些节点作为名为nodelist的属性附加到模板上。当用数据呈现模板时,它简单地遍历这个列表,单独呈现每个节点。这使得Template代码非常少,同时允许最大的灵活性。毕竟,如果每个单独的主题负责呈现自己,它就拥有 Python 的全部能力。因此,创建或定制模板节点是编写一些真正的 Python 代码的简单事情。

例外

所有这些都假设模板一直都工作正常。使用模板时,有许多事情可能会出错,因此可能会引发一些不同的异常。虽然下面的异常在大多数情况下都是自动处理的,但是也可以捕捉这些异常并分别处理。

  • django.template.TemplateSyntaxError—模板代码无法验证为正确的语法,通常是由于使用了无效的标记名。当试图实例化一个Template对象时,这个问题会立即出现。
  • django.template.TemplateDoesNotExist—任何已知的模板加载程序都无法加载请求的模板。这是由本章“检索模板”一节中描述的模板加载函数发出的。
  • django.template.TemplateEncodingErrorTemplate对象无法将提供的模板字符串强制转换为 Unicode 字符串。模板字符串必须已经是 Unicode 字符串,或者是 UTF-8 编码的。任何其他编码在传递给新的Template之前都必须转换成这两种类型中的一种。当试图构造一个新的Template对象时,这个问题会立即出现。
  • django.template.VariableDoesNotExist—在当前上下文中无法解析指定的变量名。请参阅本章后面的“上下文”部分,了解有关此过程的详细信息,以及什么情况会引发此异常。
  • django.template.InvalidTemplateLibrary—模板标签为标签库注册函数之一指定了一些无效参数。发出这种错误的单个标签将导致整个标签库停止加载,并且没有标签可用于模板。这是在使用{% load %}模板标签时引发的。

整个过程

从加载器获得字符串后,必须将其从单个字符串转换为一组可以呈现的 Python 对象。这是自动发生的,大多数情况下不需要干预,但是对于大多数 Django 来说,理解这些内部机制是非常有用的。以下步骤解释了如何处理模板。所有涉及的班级都住在django.template

A new Template object accepts the raw string of the template’s contents, forming the object that will be used later.   A Lexer object also receives the raw template string, to begin processing the template contents.   Lexer.tokenize() uses a regular expression to split the template into individual components, called tokens.   These tokens populate a new Parser object.   Parser.parse() goes through the available tokens, creating nodes along the way.   For each block tag, Parser.parse() calls an external function that understands the tag’s syntax and returns a compiled Node for that tag.   The list of compiled nodes is stored on the Template object as its nodelist attribute.  

完成后,留给您的是一个包含 Python 代码引用的Template对象,而不是启动该过程的原始字符串。创建节点列表后,原始字符串将被丢弃,因为这些节点包含呈现模板所需的所有功能。一旦这个过程完成,LexerParser和所有的Token对象也会被丢弃,但是在这个过程中它们会非常有用。

内容令牌

Lexer对象负责第一次遍历模板的内容,识别存在的不同组件。除了模板字符串本身,Lexer还接受一个origin,它表明模板来自哪里。这个处理由Lexer.tokenize()方法完成,该方法返回一个Token对象的列表。这可以被看作是处理模板的语法,而不是它的语义:单个组件被识别,但是它们还没有太多的意义。

令牌包含创建节点所需的所有信息,但令牌本身相对简单。它们只有两个属性:token_typecontentsToken.token_type的值将是在django.template中定义的四个常量之一,而它的contents将由它所属的令牌类型定义。

  • TOKEN_VAR—使用{{ var }}语法的变量标签是数据的占位符,在呈现模板之前不会提供。contents属性包含未解析的完整变量引用字符串。
  • TOKEN_BLOCK—块标记—通常称为“模板标记”—使用{% name %}语法,并由 Python 对象填充,该对象可以在模板渲染期间执行自定义代码。contents属性包含标签的全部内容,包括标签的名称及其所有参数。
  • TOKEN_COMMENT—注释标签使用{# comment #}语法,基本上被模板引擎忽略。作为 lexing 过程的一部分,为它们生成了令牌,但是它们的contents是空的,在这个过程的后面,它们不会成为节点。
  • TOKEN_TEXT—为模板中的所有其他内容生成文本标记,将文本存储在contents中。

在标准模板处理过程中,总是自动创建和使用一个Lexer,但也可以直接使用。这是一种检查和分析模板的有用方法,不需要完全编译它们。为了说明这一点,请考虑下面的示例,该示例将一个简单的单行模板解析为一系列标记。请注意,token_type只按值打印;将这个值与之前命名的常量进行比较要有用得多。

>>> from django.template import Lexer

>>> template = 'This is {# only #}{{ a }}{% test %}'

>>> for token in Lexer(template, 'shell').tokenize():

...     print '%s: %s' % (token.token_type, token.contents)

...

0: This is

3: only

1: a

2: test

将令牌解析为节点

一旦一个Lexer将模板字符串分割成一个令牌列表,这些令牌就被传递给一个Parser,后者会对它们进行更详细的检查。这是模板处理的语义方面,通过将相应的Node对象附加到模板上,每个标记都被赋予了意义。这些节点的复杂性差异很大;注释标记根本不产生节点,文本节点有非常简单的节点,而块标记可以有包含整个模板剩余部分的节点。

Parser对象本身比Lexer要复杂一些,因为它负责更多的进程。它的parse()方法必须遍历令牌列表,确定哪些令牌需要节点,以及在这个过程中要创建哪种类型的节点。使用Parser.next_token()从列表中检索和删除每个令牌。然后,该令牌用于确定要创建的节点类型。

对于文本和变量标记,Django 提供了用于所有实例的标准节点。分别是TextNodeVariableNode,在django.template也有。注释标记被简单地忽略,根本不生成任何节点。块标记通过模板标记库,用节点编译函数匹配标记名。

这些编译函数将在本章后面的“为模板添加特性”一节的“模板标签”部分进行描述,每个函数负责解析一个令牌的contents并返回一个Node对象。每个函数接收两个参数:Parser对象和当前令牌。通过访问Parser对象,节点编译函数可以访问一些额外的方法来帮助控制节点可以访问多少模板。

  • parse(parse_until=None)—这是第一次处理模板时调用的同一方法,也可以从节点内调用。通过为parse_until参数提供一个标记名,这个方法将只返回那些标记名之前的节点。这就是像blockiffor这样的标签如何在开始和结束标签之间环绕附加内容。请注意,这将返回完全编译的节点。
  • next_token()—从列表中检索并返回一个令牌。它还会删除该令牌,以便将来的节点不会收到任何已经处理过的令牌。请注意,这将返回一个尚未编译到节点中的令牌。
  • skip_past(endtag)—该方法类似于parse(),接受一个标记,该标记标记模板应该被处理的结束位置。主要的区别在于,skip_past()并没有将任何记号解析成节点,也没有返回任何找到的记号。它只是将模板推进到结束标记之外,忽略其间的任何内容。

模板节点

虽然这看起来像是一个复杂的概念,但是模板节点相当简单。所有模板节点都扩展了基本的Node类,位于django.template。除了一个用来定制节点行为的__init__()方法之外,节点只有几个需要包含的方法。

首先,为了在一个模板中的所有对象之间维护一个公共结构,每个模板节点都是可迭代的,产生包含在所讨论的节点中的所有节点,而不是呈现它们的内容。这提供了一种获取模板中所有节点的简单方法。

默认情况下,Node简单地产生它自己,这对于简单的模板标签来说工作得很好,这些标签只是呈现一小段文本。对于封装其他内容的更复杂的标签,这个__iter__()应该返回包含在其中的所有节点。

此外,节点还必须提供一个名为get_nodes_by_type()的方法,尽管默认方法对于大多数节点来说已经足够好了。该方法采用一个参数nodetype,即要检索的节点的类。将检查调用该方法的节点,看它是否是该类的实例,以及其中的任何其他节点。找到的确实是指定类型的实例的所有节点将在列表中返回,或者如果没有找到,将返回一个空列表。

节点上最重要的方法是render(),用来输出最终的文本。因为呈现文本需要传递给模板的数据,所以该方法接受单个参数,即一个上下文对象,如即将到来的“上下文”部分所述。

渲染模板

由于模板实际上只是一个编译指令的集合,让这些指令产生输出文本需要一个单独的步骤。可以使用简单的render()方法呈现模板,该方法将一个上下文对象作为唯一的参数。

render()方法根据编译的节点和上下文变量返回一个字符串,其中包含完全呈现的输出。这个输出通常是 HTML,但也可以是任何内容,因为 Django 模板设计用于任何基于文本的格式。渲染的大部分工作被委托给单个节点本身,模板只是遍历所有节点,依次调用每个节点上的render()

通过将这项工作卸载到每个节点本身,整个模板代码可以不那么复杂,同时也最大化了模板系统的灵活性。由于每个节点都对其行为完全负责,因此可能性几乎是无限的。

语境

模板本身主要是一堆静态内容、逻辑和数据占位符,供以后填充。如果没有数据来填补空白,它对 Web 应用来说就没什么用了。从表面上看,标准的 Python 字典似乎已经足够了,因为模板变量只是名称,可以映射到值。事实上,Django 甚至允许在某些情况下使用字典。

这种方法的一个缺点是,在某些情况下,模板标签可能需要更改一些数据,并且只在模板的特定部分保留这种更改。例如,当遍历一个列表时,列表中的每一项都应该可供其他标签使用,但是一旦循环完成,模板的其余部分就不能再访问该变量了。除此之外,如果一个循环定义了一个已经有值的变量,那么一旦循环结束执行,这个现有的值就应该被恢复。

CONTEXTS VS. NAMESPACES

在 Python 中,变量被分配给名称空间,以后可以通过名称检索它们,这使得模板上下文非常相似。还有一些显著的差异可能会引起一些混淆。

Python 允许名称空间嵌套,但只能在定义的类或函数中。在这些嵌套的名称空间中,新变量不能被封装它们的其他名称空间访问。其他类型的代码块,如条件和循环,与它们周围的任何代码共享命名空间,因此新的变量赋值在代码块完成执行后仍然存在。这样做很好,因为名称空间是基于代码编写的位置,而不是代码执行的位置,所以程序员可以很容易地确保相关名称没有任何冲突。

当编写模板标签时,没有办法知道在使用标签的模板中将定义什么变量。如果它向上下文中添加任何新的变量,这些变量很可能会覆盖模板中已经设置的其他内容。为了克服这个问题,模板提供了push()pop()方法,允许标签手动创建一个新的嵌套层次,并在完成时删除它。

这使得模板在这方面的工作方式与 Python 代码有所不同,因为像循环这样的块本质上是在执行期间创建一个新的名称空间,完成后就删除它。这些差异一开始可能会让程序员感到困惑,但是只使用模板的设计人员将只需要习惯一种行为。

为了完成所有这些,Django 将它的数据映射实现为一个特殊的Context对象,它的行为很像一个标准字典,但是有一些额外的特性。最值得注意的是,它在内部封装了一个字典列表,每个字典代表数据地图中的一个特定层。这样,它也可以像一个堆栈一样工作,能够在上面push()新值,并在不再需要时pop()去掉一层。

push()pop()都不接受任何争论。相反,它们只是在列表前面添加或删除一个字典,调整查找变量时首先使用哪个字典,如下所述。该功能在大多数情况下阻止了标准词典的使用;只要模板简单,它就能正常工作,但是一旦遇到其中一个标签,它就会引发一个AttributeError,因为它缺少这些额外的方法。

简单可变分辨率

在上下文中查找数据是最基本的操作之一,尽管在模板中引用变量时会发生很多情况。首先,当使用标准的{{ var }}语法时,Django 自动检查上下文字典,从最近添加的字典到最先添加的字典。这种查找也可以使用标准的字典查找语法在上下文本身上手动执行,这对于检索值和设置值同样有效。

如果给定的名称在最顶层的字典中不存在,上下文将退回到下一个字典,再次检查该名称,然后继续这个过程。通常,短语“当前上下文”用于描述在任何特定时间点模板标签可用的值。尽管在整个渲染过程中模板将使用相同的上下文对象,但是在任何给定点的当前上下文将根据使用的标签和这些标签检索的值而变化。

>>> from django.template.context import Context

>>> c = Context({'a': 1, 'b': 2})

>>> c['a'], c['b']

(1, 2)

>>> c.push()

>>> c['a'], c['b']

(1, 2)

>>> c['b'] = 3

>>> c['a'], c['b']

(1, 3)

>>> c.pop()

{'b': 3}

>>> c['a'], c['b]

(1, 2)

如果它遍历了所有可用的字典却没有发现任何东西,它会像标准字典一样引发一个KeyError。这个KeyError通常由 Django 直接处理,用站点设置中定义的常量值替换变量引用。默认情况下,TEMPLATE_STRING_IF_INVALID设置为空字符串,但这可能会被任何希望在这种情况下显示不同内容的站点覆盖。

复杂变量查找

除了简单的名称查找之外,变量还可以包含对某个对象的某些部分的引用,使用句点来分隔层与层。这使得一个变量节点不仅可以引用一个对象,还可以引用该对象的一个属性、一个方法调用或者字典或列表中的一个条目。这也是嵌套的,所以每次一个点解析一个新变量,另一个点就可以解析下一层深度,比如{{ request.META.host }}

这是使用一个单独的类来处理的,恰当地命名为Variable。它是用一个参数实例化的,这个字符串将被用作变量的路径,包括分隔路径各部分的任何句点。实例化后,它提供了一个方法resolve(),用于执行检索请求值的所有必要步骤。该方法采用单个参数,即应该找到变量的上下文。

如果变量是用文字值声明的,比如数字或带引号的字符串,而不是命名变量,则该值将总是直接返回,甚至不引用提供的上下文。否则,这将使用前面描述的简单查找来解析变量的第一部分。如果找到该部分,它将继续下一部分,依此类推。

链中第一步之后的每一步都基于前一步中检索到的对象。当确定在每个阶段得到什么时,resolve()经历几个不同的阶段,每个阶段的错误导致查找继续到下一个阶段。

  • 字典查找—提供的名称用作字典关键字。
  • 属性查找-名称用于标准getattr()方法。
  • 方法调用-如果属性查找检索到可调用项,如函数,则执行该可调用项时不带任何参数。如果成功,将使用返回值,但是如果函数需要任何参数,将跳过该函数。此外,如果函数的alters_data属性设置为True,作为安全预防措施,该函数将被跳过。
  • 列表索引查找-如果可能,变量名被强制为整数,并用作索引查找来查看值是否出现在列表中。

>>> from django.template import Variable

>>> c = Context({'var': [1, 2, {'spam': u'eggs'}]})

>>> var = Variable('var')

>>> zero = Variable('var.0')

>>> one = Variable('var.1')

>>> spam = Variable('var.2.spam')

>>> var.resolve(c)

[1, 2, {'spam': u'eggs'}]

>>> zero.resolve(c)

1

>>> one.resolve(c)

2

>>> spam.resolve(c)

u'eggs'

因为这提供了一种更健壮、功能更丰富的访问变量的方式,所以当节点需要能够从模板中访问数据时,最好使用Variable。这将确保模板作者在引用变量时有尽可能多的灵活性,即使是在自定义标签中。

包括请求的各个方面

通常有必要从传入的 HTTP 请求中包含某些属性,或者至少根据这些属性查找一些其他有用的信息,并将它们包含在模板上下文中。Django 没有办法神奇地将请求从视图传入模板系统,所以必须手工传递。

因为Context本身只接受字典作为参数,所以需要一个不同的对象来实现这一点。同样位于django.template.contextRequestContext接受一个请求对象作为它的第一个参数,而普通字典被推回到第二个参数。然后,在准备供模板使用的上下文时,可以检索请求的各个方面。

每当在 HTTP 循环中呈现模板时,最好使用RequestContext。Django 自己的通用视图一致地使用它,大多数第三方应用也可靠地使用它。不使用RequestContext可能会导致模板无法访问必要的数据,从而导致模板无法正确渲染。

对于许多网站来说,模板可能会作为自动化流程的一部分呈现,例如每夜发送账单通知电子邮件的作业。在这些情况下,没有 HTTP 请求进来,所以RequestContext是不合适的。在这些情况下,简单地使用标准的Context就足够了。

一旦用请求实例化了一个RequestContext,它必须根据请求的属性填充上下文变量。它不会随意这样做,而是运行 Django 中另一个钩子指定的代码。

正在检索模板

到目前为止,所展示的都是如何使用已经存在的模板。在现实世界中,模板必须根据特定视图的需求按需加载,因此显然还有更多工作要做。

检索模板的一个特殊要求是只能通过名称来引用它们,这样就可以从开发和生产环境之间的不同位置加载它们,而不需要更改任何视图的代码。第八章展示了如何编写自己的模板加载器,进一步增加了可用的选项。为了处理这种抽象,Django 提供了两个在检索模板时应该使用的实用函数。

django . template . loader . get _ template(模板名称)

大多数时候,一个视图只知道一个模板,所以只给出一个名称。get_template()函数采用所请求模板的名称,并返回一个完全实例化的Template对象。然后,可以根据视图的需要呈现该模板。

在后台,get_template()检查每个模板加载器是否存在给定名称的模板,然后返回找到的第一个模板。如果没有找到匹配指定名称的模板,就会引发一个TemplateDoesNotExist异常。

django . template . loader . select _ template(模板名称列表)

有时,有必要使用几个不同名称中的一个来检索模板。当应用希望在每次访问视图时提供某种默认模板,同时在某些情况下允许加载不同的模板时,通常会出现这种情况。

考虑一个房地产网站,其中的所有房产列表看起来都是一样的。自然,属性列表的视图将简单地为数据库中的每个列表使用相同的标准模板。但是,如果某个物业对其上市有特殊要求,例如额外的买家激励措施或关于紧急需要快速关闭的特殊通知,标准模板可能没有这样的地方。对于特定的列表,该信息可能还需要在页面上重新排列。

为了处理这些情况,select_template()接受一个模板名称列表,而不仅仅是一个值。对于列表中的每个名字,它调用get_template()来尝试检索它,如果失败,它简单地移动到列表中的下一个名字。这样,可以先提供一个更具体的名称——通常基于对象的 ID 或 slug——然后是一个更通用的后援。

>>> from django.template import loader

>>> t = loader.get_template('property/listing.html')

>>> t.name

'property/listing.html'>>> loader.get_template('property/listing_123.html')

Traceback (most recent call last):

...

django.template.TemplateDoesNotExist: property/listing_123.html

>>> t = loader.select_template(['property/listing_123.html'

'property/listing.html'])

>>> t.name

'property/listing.html'

在实际的应用中,包含在最具体的模板名称中的数字将由动态的东西提供,比如被请求的 URL。这样,新的属性列表将默认使用通用模板,但是定制一个单独的列表就像使用更具体的名称放入一个新模板一样简单。

加载和渲染模板的快捷方式

虽然完全控制如何加载和呈现模板当然很好,但常见的流程是只加载模板,用给定的上下文呈现它,然后访问结果字符串。这涉及到几个步骤,很容易重复,所以 Django 提供了一些方法来简化这个过程。

render_to_string(模板名称,字典=无,上下文实例=无)

生活在django.templates.loader中,这个简单的函数接受几个参数并返回一个由模板渲染产生的字符串。根据提供的名称来检索模板名称,然后通过将给定的字典传递到提供的上下文中来立即呈现模板名称。

如果没有提供字典,则使用一个空字典,而如果没有提供上下文,Django 将简单地使用一个Context。大多数情况下,使用RequestContext是最合适的,这样所有的上下文处理器都能得到应用。因为 Django 不能神奇地找到正在使用的请求,所以必须首先用请求实例化一个RequestContext,然后作为context_instance传入。

render_to_response(模板名称,字典=无,上下文实例=无,内容类型=无)

生活在django.shortcuts,这个函数的工作方式几乎和render_to_string()一样,除了它使用产生的字符串来填充一个HttpResponse对象,这将在下一章详细介绍。唯一不同的是,它接受一个可选的mimetype,这将在填充HttpResponse时使用。

为模板添加功能

也许 Django 模板最强大的特性是可以轻松地添加新特性,而不必修改框架本身。每个应用都可以提供自己的一套新功能,而不是期望网站开发者提供自己的功能。

Django 自己的模板特性可以分为两种类型,变量和标签,定制插件正好适合这两个领域。变量实际上不能添加到代码中,因为它们是由模板的上下文控制的,但是变量过滤器是应用允许变量被容易地修改的一种方式。另一方面,标签可以做任何事情,从在上下文中添加或修改变量到基于变量的分支到注入其他模板。

设置软件包

为了方便模板作者,Django 要求模板特性存在于应用中的特定包结构中。{% load %}标签使用这种结构在所有已安装的应用中定位特定的模块,而不需要复杂的配置,复杂的配置会使模板设计者的生活更加困难。

任何应用都可以通过在应用的主包中创建一个templatetags包来提供新的模板特性。这个新包可以包含任意数量的模块,每个模块包含一组相互关联的特性。例如,邮件应用可以提供格式化文本、执行基本数学运算和显示消息之间关系的功能。包的结构看起来像这样:

mail/

__init__.py

forms.py

models.py

urls.py

views.py

templatetags/

__init__.py

text.py

math.py

relationships.py

在为这个应用或者您在站点中使用的任何其他应用编写模板时,{% load %}标签使这些特性可用,接受要加载的模块的名称。这些模块可以来自您的INSTALLED_APPS设置中的任何应用。Django 首先在每个应用中寻找一个templatetags包,然后寻找在{% load %}标签中命名的模块。

{% load text math relationships %}

可变过滤器

当在模板中使用变量时,它们通常只是被当前视图传递到上下文中。有时,有必要格式化或修改其中的一些值,以满足特定页面的需要。这些类型的表示细节最好放在模板中,这样视图就可以传递原始值,而不用考虑模板会对它们做什么。

Django 在其核心发行版中提供了许多这样的过滤器,旨在处理您可能会遇到的许多最常见的情况。完整的文档可在网上获得, 1 但这里有一些最常见的过滤器:

  • capfirst—返回首字母大写的字符串
  • length—返回给定序列中的项目数
  • date—使用字符串作为参数来格式化日期

过滤器只是 Python 函数,它将变量值作为输入,并将修改后的值作为返回值返回。这听起来很简单,但是仍然有很大的灵活性。下面是一个简单的过滤函数,显示变量的前几个字符,用作{{ var_name|first:3 }}

from django.template import Library

from django.template.defaultfilters import stringfilter

register = Library()

@register.filter

@stringfilter

def first(value, count=1):

"""

Returns the first portion of a string, according to the count provided.

"""

return value[:count]

接受一个价值

第一个参数是变量的值,并且总是被传入,所以它应该总是过滤器函数所需要的。这个值通常是传递到模板上下文中的变量,但是过滤器可以链接在一起,所以这个值实际上可能是另一个过滤器已经执行的结果。因此,过滤器应该尽可能通用,接受广泛的输入并尽可能优雅地处理它。

Tip

这种“接受自由”的概念长期以来被认为是可互操作系统的最佳实践。早在 1980 年,在今天的网络所基于的技术形成期间,它就已经被记载了。与之相对应的是“发送的内容要保守”,在这种情况下,建议过滤器应该总是返回相同的数据类型。

因为该值可能包含来自视图提供的任何数据,或者来自任何先前过滤器的结果,所以在对其类型进行假设时应该小心。它通常是一个字符串,但也可以是一个数字、模型实例或任何数量的其他本地 Python 类型。

大多数过滤器都是为处理字符串而设计的,所以 Django 也提供了一个处理字符串的快捷方式。不能保证输入是一个字符串,所以基于字符串的过滤器总是需要在继续之前将值强制转换成一个字符串。这个流程已经有一个装饰器,叫做stringfilter,位于django.template.defaultfilters。这会自动将传入的值强制转换为字符串,因此过滤器本身不必这样做。

确保不直接对该对象进行更改也很重要。如果输入是一个可变的对象,比如一个列表或一个字典,那么在过滤器中所做的任何更改也将反映在模板中该变量的任何未来使用中。如果要进行任何更改,例如添加前缀或重新组织项目,必须先制作一份副本,这样这些更改才会反映在过滤器本身中。

接受一个论点

除了接收变量本身,过滤器还可以接受一个参数来定制它的用法。接受一个参数所需的唯一改变是在函数上定义一个额外的参数。使参数可选也很容易,只需在函数定义中提供默认值。

像变量本身一样,它也可以是任何类型,因为它可以被指定为一个文本,也可以通过另一个变量提供。没有提供任何装饰器来将该值强制转换为字符串,因为数字作为过滤器参数非常常见。无论您的过滤器期望什么参数,只要确保将它显式地强制为它需要的类型,并且总是捕捉在强制过程中可能发生的任何异常。

返回值

大多数情况下,过滤器应该返回一个字符串,因为它应该被发送到呈现模板的输出中。返回其他类型数据的过滤器也有一定的用途,例如对列表中的数字进行平均,以数字的形式返回结果的过滤器,但是这些过滤器很少使用,应该很好地记录下来,以防其他过滤器与它们链接在一起,以避免意外的结果。

更重要的是,过滤器应该总是返回值。如果在过滤器的处理过程中出现任何问题,都不应该引发异常。这也意味着如果过滤器调用其他可能引发异常的函数,那么异常应该由过滤器来处理,这样它就不会引发更多的异常。如果出现这些问题,过滤器应该返回原始输入或一个空字符串;使用哪一种取决于所讨论的过滤器的目的。

注册为过滤器

一旦函数编写完成,它就通过使用在django.template提供的Library类注册到 Django。一旦实例化,Library有一个可以用作装饰器的filter()方法,当应用于过滤函数时,它会自动向 Django 注册。这就是代码方面所需要的全部内容。

默认情况下,这并没有使它对所有模板都可用,而是告诉 Django 应用提供了它。任何想要使用它的模板仍然必须使用{% load %}标签加载应用的模板特性。

模板标签

过滤器有一个非常有用和实用的目的,但是由于它们最多只能接收两个值——变量和参数——并且只能返回一个值,所以很容易看出应用可以多快超越它们。获得更多的功能需要使用模板标签,它允许任何事情。

像过滤器一样,Django 在其核心发行版中提供了许多标记,这些标记在网上有文档记录。这里列出了一些比较常见的标签,并简要描述了它们的功能。

  • for—允许模板循环遍历序列中的项目
  • filter—将模板过滤器(如前所述)应用于标签中包含的所有内容
  • now—打印当前时间,使用一些可选参数对其进行格式化

模板标签被实现为一个函数和一个Node类的配对,前者配置后者。该节点就像前面描述的节点一样,表示模板标记的编译结构。另一方面,函数用于接受标记的各种允许的语法选项,并相应地实例化节点。

简单的标签

最简单的标记形式只存在于自身中,通常是根据一些参数将附加内容注入页面。这种情况下的节点非常简单,只需获取和存储这些参数,并在呈现期间将它们格式化为一个字符串。如果将上一节中的过滤器实现为一个标记,看起来会是这样。在模板中,这将类似于{% first var_name 3 %}

from django.template import Library, Node, Variable

register = Library()

class FirstNode(Node):

def __init__(self, var, count):

self.var = var

self.count = count

def render(self, context):

value = self.var.resolve(context)

return value[:self.count]

另一方面,编译它的函数要复杂一些。与过滤函数不同,标记函数总是接受两个参数:模板解析器对象和表示标记中包含的文本的标记。由编译函数从这两个对象中提取必要的信息。

对于这样一个简单的标记,不需要担心解析器对象,但是如果指定了参数,令牌对于获取参数仍然是必要的。

关于标记最重要的事情是split_contents()方法,它智能地将标记的声明分解成单独的组件,包括标记的名称和参数。

它可以正确地处理变量引用、带引号的字符串和数字,尽管它不做任何变量解析,并将引号放在带引号的字符串周围。

为了从我们的模板标签中获取两个必要的信息位,使用token.split_contents()从声明的字符串中提取它们。然后,可以将它们强制转换为正确的类型,并用于实例化前面描述的节点。

@register.tag

def first(parser, token):

var, count = token.split_contents()[1:]

return FirstNode(Variable(var), int(count))

简单标签的快捷方式

谢天谢地,有一条捷径可以让这个过程变得简单很多。Library对象包含另一个装饰器方法simple_tag(),它处理类似这样的简单情况。在幕后,它处理参数的解析和解析,甚至节点类的创建,所以留给模板标记的只是一个看起来非常类似于变量过滤器的函数。

from django.template import Library

register = Library()

@register.simple_tag

def first(value, count):

return value[:count]

这仍然是有限的用途,但有许多这样的情况下,一个简单的标签是必要的,快捷方式可以成为一个相当省时。对于更高级的需求,手动创建节点可以提供更大的能力和灵活性。

向所有模板添加要素

默认情况下,Django 不会自动加载所有应用的模板过滤器和标签;相反,它只对所有模板使用默认设置。对于那些需要访问特定应用的模板特性并且使用{% load %}标签开销太大的模板,还有一个选项可以将应用添加到所有模板的默认标签集中。

同样在django.template中,add_to_builtins()函数默认包含应用的名称。具体来说,这是该应用的app_label,如第三章所述。一旦一个应用被提供给这个函数,它的所有标签和过滤器将对所有模板可用。这可以在应用加载时执行的任何地方调用,比如它的__init__.py模块。

这应该谨慎使用,因为对于那些不使用该应用任何特性的模板来说,它确实会带来一些额外的开销。然而,在某些情况下,有必要覆盖默认过滤器或标签的行为,而add_to_builtins()提供了这个选项。请记住,不止一个应用可以做到这一点,所以仍然不能保证将使用哪个应用版本的特定功能。Django 将在遇到它们时简单地覆盖它们,所以最后加载的应用将被使用。小心使用。

应用技术

Django 模板旨在让那些必须定期编写模板的人尽可能地简单。先进的技术被用来强化这一思想,简化了在模板中执行起来过于复杂的任务。应用通常有自己独特的模板需求,应该提供标签和过滤器来满足这些需求。更好的是,如果需要的话,提供可以被其他应用重用的特性。

嵌入另一个模板引擎

虽然 Django 的模板引擎适用于大多数常见的情况,但它的局限性可能会在需要更强大或更灵活的情况下造成挫折。模板标签扩展了 Django 的功能,但只是通过让程序员为每个单独的需求编写它们。

另一个具有不同设计理念的模板引擎可能更适合这些需求。通过允许模板设计者为模板的部分切换到替代引擎,无需额外编程就可以展示额外的功能。标签仍然可以简化常见的任务,但是切换模板引擎可能是支持极限情况的一种简单方法。

一个这样的替代模板引擎是 Jinja, 2 ,它具有与 Django 相当类似的语法。这两种设计理念存在根本差异,这使得 Jinja 成为输出需要复杂条件和逻辑的情况下的更好选择。这些方面使它成为嵌入 Django 模板的完美候选。

为了说明这一点,考虑一个需要计算复合值以在模板中显示的模板。这个特性在 Django 中是不可用的,所以它通常需要一个定制的模板标签或者一个视图,在将值发送到模板之前计算它的值。

{% load jinja %}

{% for property in object_list %}

Address: {{ property.address }}

Internal area: {{ property.square_feet }} square feet

Lot size: {{ property.lot_width }}' by {{ property.lot_depth }}'

{% jinja %}

Lot area: {{ property.lot_width * property.lot_depth / 43560 }} acres

{% endjinja %}

{% endfor %}

Django 将自动处理直到jinja标记的所有内容,将所有剩余的标记和Parser对象一起传递给 Jinja 编译函数。解析器和令牌可以用来提取写在jinjaendjinja标签之间的内容。然后,在传递给 Jinja 进行渲染之前,需要将其转换回字符串内容。

将令牌转换为字符串

在研究完整的编译函数之前,首先要注意令牌必须转换回字符串,以便 Jinja 处理它们。Jinja 为其模板使用了非常相似的语法,所以 Django 的Lexer准确地识别了变量、块和注释标签。尽管 Jinja 也为这些标签创建了标记,但是来自两个模板引擎的标记彼此不兼容,所以它们必须转换回字符串。Jinja 可以像处理任何来源的模板一样处理它们。

为了完成这种转换,节点编译函数将依赖一个单独的函数,该函数接受一个令牌并返回一个字符串。它的工作原理是,django.template也包含这些标签的开始和结束部分的常量。有了这些信息和记号的结构,就可以从给定的记号创建合适的字符串。

from django import template

def string_from_token(token):

"""

Converts a lexer token back into a string for use with Jinja.

"""

if token.token_type == template.TOKEN_TEXT:

return token.contents

elif token.token_type == template.TOKEN_VAR:

return '%s %s %s' % (

template.VARIABLE_TAG_START

token.contents

template.VARIABLE_TAG_END

)

elif token.token_type == template.TOKEN_BLOCK:

return '%s %s %s' % (

template.BLOCK_TAG_START

token.contents

template.BLOCK_TAG_END

)

elif token.token_type == template.TOKEN_COMMENT:

return u'' # Django doesn't store the content of comments

这不会产生原始模板字符串的精确副本。在 Django 的Lexer处理过程中,一些空白被删除,注释的内容完全丢失。标签的所有功能都被保留下来,所以模板仍然可以正常工作,但是要知道这种技术可能会产生一些小的格式问题。

编译到节点

有了为jinja块中的标记再现字符串的函数,下一步是生成一个Node,它将用于呈现内容以及模板的其余部分。当收集开始标签和结束标签之间的内容时,编译函数通常使用Parser.parse()方法,传入结束标签的名称,这将返回表示内部内容的Node对象列表。

由于 Jinja 标签不能使用 Django 的节点函数进行处理,Parser.parse()会由于不正确的语法而导致问题。相反,Jinja 编译函数必须直接访问令牌,然后可以将令牌转换回字符串。没有提供完全做到这一点的函数,但是将Parser.next_token()与一些额外的逻辑结合起来会工作得很好。

编译函数可以遍历可用的令牌,每次都调用Parser.next_token()。这个循环将一直执行,直到找到一个endjinja块标记或者模板中不再有标记。一旦从解析器获得了令牌,就可以将它转换成一个字符串,并添加到一个内部模板字符串中,该字符串可以用来填充一个JinjaNode

import jinja2

from django import template

from django.base import TemplateSyntaxError

register = template.Library()

def jinja(parser, token):

"""

Define a block that gets rendered by Jinja, rather than Django's templates.

"""

bits = token.contents.split()

if len(bits) != 1:

raise TemplateSyntaxError("'%s' tag doesn't take any arguments." % bits[0])

# Manually collect tokens for the tag's content, so Django's template

# parser doesn't try to make sense of it.

contents = []

while 1:

try:

token = parser.next_token()

except IndexError:

# Reached the end of the template without finding the end tag

raise TemplateSyntaxError("'endjinja' tag is required.")

if token.token_type == template.TOKEN_BLOCK and \

token.contents == 'endjinja':

break

contents.append(string_from_token(token))

contents = ''.join(contents)

return JinjaNode(jinja2.Template(contents))

jinja = register.tag(jinja)

Caution

不使用解析器的parse()方法,就不能在{% jinja %}标签中使用任何其他 Django 标签。这在这里不是问题,因为内容是由 Jinja 处理的,但是在没有充分理由的情况下使用这种技术会导致其他类型的标签出现问题。

准备金贾模板

一旦编译函数从 Django 模板标记中检索到 Jinja 模板内容,就会创建一个JinjaNode来访问该模板。Jinja 提供了自己的Template对象,将内容编译成有形对象,因此在创建JinjaNode时使用它是有意义的。

然后,到了渲染JinjaNode的时候,只需要渲染编译好的 Jinja 模板,并将输出返回给 Django 的模板。这个任务比表面上看起来更棘手,因为 Django 的Context对象包含应该传递给 Jinja 的变量,其行为不完全像 Python 字典。它们支持访问键的通用字典式语法,但是在内部,它们的结构与 Jinja 所期望的完全不同。

为了将嵌套的Context对象正确地传递给 Jinja 模板,必须首先将其展平为一个标准的 Python 字典。这很容易做到,只需遍历存储在上下文中的各个字典,并将它们分配给一个新字典,保持 Django 本身使用的优先级:某个键的第一次出现优先于该键的任何其他实例。只有当一个键在新的 Jinja 上下文字典中不存在时,才应该添加它,这样在这个过程中就不会覆盖现有的值。

一旦字典可用,数据就可以传递给 Jinja 自己的Template.render()方法。该方法的结果是可以从JinjaNode.render()返回的正确呈现的内容,将该内容放置在页面中。

import jinja2

class JinjaNode(template.Node):

def __init__(self, template):

self.template = template

def render(self, django_context):

# Jinja can't use Django's Context objects, so we have to

# flatten it out to a single dictionary before using it.

jinja_context = {}

for layer in django_context:

for key, value in layer.items():

if key not in jinja_context:

jinja_context[key] = value

return self.template.render(jinja_context)

启用用户提交的主题

在本章的前面,我们发现模板可以从任何来源加载,只要有一个合适的加载器知道如何检索它们。这种方法的一个缺点是它只对每个人加载模板有效;没有办法将模板与特定用户相关联。

无论如何,这都不是真正的失败,因为大多数应用都需要它完全按照自己的方式工作。此外,用户信息只有在收到请求时才可用,因此无法以通用的方式访问它。每个工具都有它的使用时间,当然也有将模板绑定到用户身上的时候。

考虑这样一个网站,它鼓励用户定制他们自己的体验,提供他们登录时使用的定制主题。这给了用户很大的控制权,让他们可以更好地参与到网站中,并能让他们更深入地体验网站。如果他们有机会将自己的自定义主题提供给其他人使用,这一点还可以进一步增强。这种想法并不适用于所有的网站,但是对于大量面向社区的网站,尤其是艺术界的网站,它可以极大地提升用户体验。

A WORD ABOUT ADVERTISING

今天,网络上的许多站点至少部分是由放置在它们的各种页面上的广告资助的。这种广告只有在真正展示给用户时才会起作用,因此他们有机会点击广告并购买产品或服务。通过向网站引入用户可编辑的主题,用户有一个绝佳的机会删除网站可能依赖的任何广告,所以仔细考虑这是否适合你的网站是很重要的。

网站工作人员批准供网站普通观众使用的任何主题都可以首先进行检查,以确保它们不会对网站上的广告或网站自身的品牌造成任何伤害。这是在过程中至少实施一些质量控制的好方法。问题在于,用户可以在提交主题以供审批之前,按照自己喜欢的方式创建主题,并可以通过网站自行使用,从自己的体验中删除广告。

将这个问题的影响最小化的一个方法是提供付费的网站会员资格,其中一个好处是能够创建自定义主题。通过这种方式,免费用户将永远把广告视为资助他们使用网站的一种方式,而付费用户则通过年费来抵消他们缺乏广告的影响。

事实上,如果你的网站采用这种模式,最好是完全删除付费用户的广告,不管他们用的是什么主题。没有人喜欢为使用一个网站付费,只是为了给同一个网站带来更多的收入。

从表面上看,这似乎是层叠样式表(CSS)的完美工作。CSS 完全是关于网站的表现,但是它总是受到页面内容排序的限制。例如,文档中位置较高的标记很难放在页面的底部,反之亦然。通过允许用户编辑决定这些位置的模板,很容易打开更多的可能性。

使用 Django 模板带来了一些必须克服的技术和安全挑战,解决这些挑战揭示了许多使用模板的有趣方法。首先考虑需要解决的问题。

  • 如果要编辑模板,它们应该存储在数据库中。
  • 模板需要绑定到一个特定的用户,以限制他们编辑任何东西,并且当主题得到推广时,还需要将功劳分配给适当的作者。
  • 用户无法使用 Django 模板语言的全部内容。这是一个安全风险,会向任何人暴露太多的信息。
  • 主题在提供给所有人使用之前,必须得到工作人员的批准。
  • 一旦主题被提交以供批准,并且在它们被批准之后,用户应该不能进行任何修改。
  • 用户的个人主题——无论是个人创作的还是从他人作品中挑选的——都应该在网站的所有部分使用。
  • 除了模板本身,每个主题都应该有一个与之相关的 CSS 文件,以更好地设计网站的其他方面。

这是一个需要涵盖的相当多的东西的列表,并且单个站点可能有更多的需求。这并不像表面上看起来那么糟糕,因为 Django 已经准备了很多东西来使这些问题容易解决。

建立模型

首要任务是为模板在数据库中的存储腾出空间。在标准的 Django 方式中,这是通过一个模型来完成的,这个模型带有表示模板各种属性的字段。对于这个应用,主题由一些不同的信息组成:

  • 用作模板内容的文本块
  • CSS 文件的 URL
  • 创建它的用户
  • 一个标题,这样其他用户可以很容易地引用它,如果它对每个人都可用的话
  • 指示它是否是站点范围的默认设置,以便尚未选择主题的用户仍然可以使用

这些信息中的大部分只会被主题对象本身使用,因为只有主要的文本块会被传递到模板中。很容易把主题想象成一个独立的模板,它同时是存储在数据库中的一组数据和用于呈现 HTML 的一组指令。Python 提供了一种明确这一概念的方式,并提供了一种处理主题的简单方法。

通过使用多重继承,主题有可能既是模型又是模板,以手头任务所需的任何方式表现。该类继承自django.db.models.Modeldjango.template.Template,并且__init__()被覆盖以分别初始化两端:

from django.db import models

from django import template

from django.contrib.auth.models import User

from themes.managers import ThemeManager

class Theme(models.Model, template.Template):

EDITING, PENDING, APPROVED = range(3)

STATUS_CHOICES = (

(EDITING, u'Editing')

(PENDING, u'Pending Approval')

(APPROVED, u'Approved')

)

author = models.ForeignKey(User, related_name='authored_themes')

title = models.CharField(max_length=255)

template_string = models.TextField()

css = models.URLField(null=True, blank=True)

status = models.SmallIntegerField(choices=STATUS_CHOICES, default=EDITING)

is_default = models.BooleanField()

objects = ThemeManager()

def __init__(self, *args, **kwargs):

# super() won't work here, because the two __init__()

# method signatures accept different sets of arguments

models.Model.__init__(self, *args, **kwargs)

template.Template.__init__(self, self.template_string

origin=repr(self), name=unicode(self))

def save(self):

if self.is_default:

# Since only one theme can be the site-wide default, any new model that

# is defined as default must remove the default setting from any other

# theme before committing to the database.

self.objects.all().update(is_default=False)

super(Theme, self).save()

def __unicode__(self):

return self.title

这足以将主题本身存储在数据库中,但仍然没有涵盖用户在浏览网站时如何选择主题。通常,这将被设置为引用Theme的模型上的ForeignKey,但是由于User模型在我们的控制之外,所以需要做一些其他的事情。

存储以用户为中心的信息(如首选项)的一种方法是添加自定义用户模型。Django 的官方文档 3 详细介绍了这一点,但基本思想是你可以提供自己的模型来代替 Django 自己的用户模型使用。您的自定义模型可以包含任何与用户相关的附加字段,包括选定的主题。然而,一个站点只能有一个自定义用户模型,仅仅为了支持主题而劫持这个特性是没有意义的。相反,我们可以使用一个ManyToManyField将它连接到User模型。

class Theme(models.Model):

...  # All the other fields shown above

users = models.ManyToManyField(User, through='SelectedTheme')

...  # All the other methods shown above

class SelectedTheme(models.Model):

user = models.OneToOneField(User)

theme = models.ForeignKey(Theme)

通过使用一个OneToOneField,我们可以确保每个用户在中间表中只出现一次。这样,每个用户只能有一个选定的主题。还有一些实用函数可以帮助管理这种行为。实际上,这里有两种不同的方法会很有用,都是基于用户来获取主题的。一个用于检索用户选择的主题,另一个用于检索用户创建的主题。

from django.db import models

from django.conf import settings

class ThemeManager(models.Manager):

def by_author(self, user):

"""

A convenience method for retrieving the themes a user has authored.

Since the only time we'll be retrieving themes by author is when

they're being edited, this also limits the query to those themes

that haven't yet been submitted for review.

"""

return self.filter(author=self, status=self.model.EDITING)

def get_current_theme(self, user):

return SelectedTheme.objects.get(user=user).theme

有了这个管理器,就可以很容易地检索特定用户的主题,包括用户可以编辑的主题和用户在浏览站点时应该使用的主题。拥有这些快捷方式有助于使视图更简单,让他们专注于他们真正要做的事情。一个站点范围的主题的全部意义在于它被用于每一个视图,所以很明显需要做一些其他的事情来适应它。

支持站点范围的主题

单个视图已经有足够多的事情要操心了,不应该负责管理主题。相反,需要一种方法来检索用户选择的主题——或者默认主题——并将其自动应用于视图使用的任何模板。理想情况下,所有这些都应该在不对视图进行任何更改的情况下发生,因此需要做的额外工作很少。

这是最适合上下文处理器的工作,这是本章前面描述的概念。通过使用上下文处理器,每个使用RequestContext的视图将自动访问正确的主题。这使得总是使用RequestContext这个通常很好的建议变成了一个绝对的要求。正如我们将在下一节看到的,模板将明确依赖于可用的主题,而不使用RequestContext将违反这一假设。

这个过程所需的上下文处理器相当简单,但是它必须提供一些特定的特性。它必须确定当前用户是否登录,识别用户选择的主题,如果没有选择主题或者用户没有登录,则返回默认主题,并且它必须返回正确的主题,以便可以将它添加到模板的上下文中。这些代码将被放在一个名为context_processors.py的模块中,与 Django 内部使用的约定保持一致。

from django.conf import settings

from themes.models import Theme

def theme(request):

if hasattr(request, 'user') and request.user.is_authenticated():

# A valid user is logged in, so use the manager method

theme = Theme.objects.get_current_theme(request.user)

else:

# The user isn't logged in, so fall back to the default

theme = Theme.objects.get(is_default=True)

name = getattr(settings, 'THEME_CONTEXT_NAME', 'theme')

return {name: theme}

注意测试中使用了hasattr()来查看用户是否登录。这看起来可能没有必要,但是通过在测试中添加这个简单的条件,它允许在没有中间件需求的情况下使用这个上下文处理器。否则,它总是需要django.contrib.auth.middleware.AuthenticationMiddleware,将user属性放在请求上。如果没有使用这个中间件,每个用户将会收到默认的主题。

另外,请注意上下文变量的名称是由另一个新设置驱动的,这次称为THEME_CONTEXT_NAME。这默认为'theme',因此没有必要显式提供名称,除非这会导致与其他特性冲突。这是一个反复出现的主题(双关语),因为对于一个必须与自身之外的大量事物交互的应用,比如用户模型和模板上下文,确保将冲突保持在最低限度是很重要的。

有了这个文件,剩下的唯一事情就是将'themes.context_processors.theme'添加到TEMPLATE_CONTEXT_PROCESSORS设置中,以确保它被应用到所有的模板中。一旦主题对模板可用,仍然需要确保模板可以访问和使用它。

设置模板以使用主题

主题的最终目标是对页面的组件进行重新排序,所以确定什么是“组件”是很重要的。就 Django 模板而言,这意味着一个标记块,由{% block %}模板标记标识。页面的每个组件可以在单独的块中定义,将每个位分隔到自己的空间中。

使用 Django 的模板继承,可以在一个模板中定义块,用另一个模板的内容填充这些块。这样,特定于页面的模板可以定义每个块中的内容,而基本模板可以指定这些块呈现的位置,以及它们周围放置的其他标记。这将是对页面的重要部分进行重新排序的一个很好的方法,只要有一种方法可以动态地指定基本模板放置所有块的位置。

Django 通过{% extends %}标签支持模板继承,该标签使用一个参数来标识要扩展的基本模板。通常,这是用作基础的模板的硬编码名称。它还可以接受一个上下文变量,该变量包含一个用作基本模板的字符串。如果该上下文变量指向一个模板实例,Django 将使用它,而不是费力地在其他地方查找模板。

在模板中利用这一点很容易;把{% extends theme %}放在模板的顶部就可以了。如果你已经为你的站点明确地指定了一个THEME_CONTEXT_NAME,确保将theme更改为你为该设置输入的任何值。这仍然只是一部分。仍然需要获取模板来利用主题中定义的块。

没有通用的方法可以做到这一点,因为每个站点都有自己的模板继承设置,以及自己的一组每个页面都需要填充的模块。通常情况下,这些块将用于页面标题、导航、页面内容和页脚,但不同的网站可能有不同的需求。

此外,一个站点可能有更多的块不能被重新排列,而是被定义在其他块的内部。目前我们不会考虑这些,因为主题只与可以移动的方块有关。考虑一个具有以下可定制模块的应用:

  • logo—网站的标志,作为图像
  • title—当前页面的标题
  • search—搜索框,可能带有高级选项
  • navigation—用于浏览网站的链接或其他界面的集合
  • sidebar—与当前页面相关的一点内容
  • content—当前页面的主体,无论是产品列表、新闻稿、搜索结果还是联系表单
  • 版权免责声明,以及一些职位空缺、投资者关系和联系信息的链接

每个主题都必须定义所有这些块,以确保整个网站得到显示,所以明确地概述它们是很重要的。网站上的每个模板都需要定义要放入这些块中的内容,以便总是有东西放在正确的位置。这些块中有许多并没有指定给任何特定的页面,所以模板继承在这里也起到了拯救作用。

通过在主题和单个页面之间放置另一个模板层,可以为所有页面自动填充一些块,而将其他块留给单个页面填充。单个页面模板仍然具有最终的权威,如果需要,可以用新内容覆盖任何块。这就留下了确保模板确实定义了站点继承方案所需的所有块的问题。

验证和保护主题

任何时候一个网站接受用户的输入,它必须被仔细检查,以确保它满足一定的要求,并保持在可接受的范围内。主题也不例外,但是用户可编辑的模板也代表了一个非常真实的安全风险。Django 采取措施确保模板不能执行任何修改数据库的常用函数,但是模板可以做很多其他的事情。

默认情况下,只有 Django 自己的数据修改方法通过使用alters_data属性从模板中得到保护。任何应用的模型都可能定义对数据库进行更改的其他方法,如果这些方法没有用alters_data标记,它们就可以在模板中使用。即使是只读访问,如果不加以控制,也会成为一个问题。每个页面都使用一个主题,许多页面将通过模型关系访问大量对象。

有太多的方法可以访问应该保持隐私的东西,以至于没有黑名单方法可以指望是完整的。相反,白名单方法是必要的,其中主题只允许使用 Django 的模板系统提供的一小部分功能。诀窍是确定解决这类问题的正确方法。

从表面上看,正则表达式似乎是可行的。毕竟,Django 本身使用一个正则表达式来解析模板并将它们分解成节点,所以编写一个更有限的表达式来保护模板肯定是微不足道的。目前可能是这样,但是请记住 Django 正在不断改进,未来可能会给模板带来新的语法。

不管这种可能性有多大,如果真的发生了,无论我们如何精心制作正则表达式,都无法预测将来会包含什么新语法。任何逃过这一保护的东西都有可能损害网站或泄露机密信息。模板语法保持不变的希望太大了。

相反,我们将依赖 Django 自己的正则表达式将模板编译成一个节点列表,就像平常一样。然后,一旦它被编译成一个nodelist,就很容易看到这些节点,以确保它们都在做正确的事情。利用这一点,表单可以很容易地验证模板是否定义了所有正确的块。主题模板必须:

  • THEME_EXTENDS设置引用的模板继承。
  • 提供一个块,其名称由THEME_CONTAINER_BLOCK设置引用。
  • THEME_BLOCKS设置中引用的所有块填充该块。
  • 在任何THEME_BLOCKS块中不提供内容。
  • 除了在THEME_BLOCKS设置中提到的那些,不要提供其他模块。
  • 不包含任何其他标签,只有文本。

from django import forms

from django import template

from django.template.loader_tags import BlockNode, ExtendsNode

from django.conf import settings

from theme import models

class ThemeForm(forms.ModelForm):

title = forms.CharField()

body = forms.CharField(widget=forms.Textarea)

def clean_body(self):

try:

tpl = template.Template(self.cleaned_data['body'])

except template.TemplateSyntaxError as e:

# The template is invalid, which is an input error.

raise forms.ValidationError(unicode(e))

if [type(n) for n in tpl.nodelist] != [ExtendsNode] or \

tpl.nodelist[0].parent_name != settings.THEME_EXTENDS:

# No 'extends' tag was found

error_msg = u"Template must extend '%s'" % settings.THEME_EXTENDS

raise forms.ValidationError(error_msg)

if [type(n) for n in tpl.nodelist[0].nodelist] != [BlockNode] or \

tpl.nodelist[0].nodelist[0].name != settings.THEME_CONTAINER_BLOCK:

# Didn't find exactly one block tag with the required name

error_msg = u"Theme needs exactly one '%s' block" % \

settings.THEME_CONTAINER_BLOCK

raise forms.ValidationError(error_msg)

required_blocks = list(settings.THEME_BLOCKS[:])

for node in tpl.nodelist[0].nodelist[0].nodelist:

if type(node) is BlockNode:

if node.name not in required_blocks:

error_msg = u"'%s' is not valid for themes." % node.name)

raise forms.ValidationError(error_msg)

required_blocks.remove(node.name)

if node.nodelist:

error_msg = u"'%s' block must be empty." % node.name)

raise forms.ValidationError(error_msg)

elif type(node) is template.TextNode:

# Text nodes between blocks are acceptable.

pass

else:

# All other tags, including variables, are invalid.

error_msg = u"Only 'extends', 'block' and plain text are allowed."

raise forms.ValidationError(error_msg)

if required_blocks:

# Some blocks were missing from the template.

blocks = ', '.join(map(repr, required_blocks))

error_msg =  u"The following blocks must be defined: %s" % blocks

raise forms.ValidationError(error_msg)

class Meta:

model = models.Theme

主题示例

即使有了应用,也可能很难理解如何编写一个主题来与站点协同工作。考虑一个使用此themes应用的站点,具有以下设置:

THEME_EXTENDS = 'base.html'

THEME_CONTEXT_NAME = 'theme'

THEME_CONTAINER_BLOCK = 'theme'

THEME_BLOCKS = (

'title'

'sidebar'

'links'

)

位于继承链根的base.html模板可能如下所示:

<html>

<head>

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

<link rel="stylesheet" type="text/css" href="/style.css"/>

</head>

<body>{% block theme %}{% endblock %}</body>

</html>

然后可以编写一个主题来满足应用的需求:从base.html开始扩展,提供一个theme块并用空的titlesidebarlinks块填充它。与其他模板不同,这段代码将作为Theme模型的一个实例存储在数据库中。

{% extends 'base.html' %}

{% block theme %}

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

<ul id="links">{% block links %}{% endblock %}</ul>

<div id="content">{% block content %}{% endblock %}</div>

{% endblock %}

现在,可以为网站的其余部分编写单独的模板,从theme变量扩展并填充titlesidebarlinks块。考虑一个房地产网站的根的模板:

{% extends theme %}

{% block title %}Acme Real Estate{% endblock %}

{% block links %}

<li><a href="{% url home_page %}">Home</a></li>

<li><a href="{% url property_list %}">Properties</a></li>

<li><a href="{% url about_page %}">About</a></li>

{% endblock %}

{% block content %}

<p>Welcome to Acme Real Estate!</p>

{% endblock %}

有了所有这些模板,加载站点的根目录将产生一个完整的 HTML 文档,如下所示:

<html>

<head>

<title>Acme Real Estate</title>

<link rel="stylesheet" type="text/css" href="/style.css"/>

</head>

<body>

<h1>Acme Real Estate</h1>

<ul id="links">

<li><a href="/">Home</a></li>

<li><a href="/properties/">Properties</a></li>

<li><a href="/about/">About</a></li>

</ul>

<div id="content">

<p>Welcome to Acme Real Estate!</p>

</div>

</body>

</html>

现在怎么办?

视图和模板相结合来决定什么内容应该发送给用户,但是它仍然必须到达浏览器。Django 能流利地讲 HTTP,所以有很多方法可以定制这个旅程。

Footnotes 1

http://prodjango.com/tags/

  2

http://prodjango.com/jinja/

  3

http://prodjango.com/custom-user/

七、处理 HTTP

Abstract

超文本传输协议(HTTP)是网络通信的基本语言。Web 服务器和 Web 浏览器都在使用它,还有各种处理 Web 的专业工具。

超文本传输协议(HTTP)是网络通信的基本语言。Web 服务器和 Web 浏览器都在使用它,还有各种处理 Web 的专业工具。

Python 社区已经做了大量的工作来标准化与 HTTP 交互的应用的行为,最终产生了 PEP-333,1Web 服务器网关接口(WSGI)。因为 Django 遵循 WSGI 规范,所以本章列出的许多细节都是遵循 PEP-333 的直接结果。

请求和回应

因为 HTTP 是一种无状态协议,其核心是请求和响应的概念。客户端向服务器发出一个请求,服务器返回一个包含客户端请求的信息的响应,或者返回一个错误,指出请求无法实现的原因。

虽然请求和响应遵循详细的规范,但是 Django 提供了一对 Python 对象,旨在使协议更容易在您自己的代码中处理。协议的基本工作知识是有用的,但是大多数细节是在幕后处理的。本节描述了这些对象,以及说明应参考的规范相关部分的注释。

对象

正如在第四章中所描述的,每个 Django 视图都接收一个对象作为它的第一个参数,这个对象表示传入的 HTTP 请求。这个对象是HttpRequest类的一个实例,它封装了关于请求的各种细节,以及一些用于执行有用功能的实用方法。

基本的HttpRequest类存在于django.http中,但是单个的服务器连接器将定义一个子类,该子类具有特定于所使用的 Web 服务器的附加属性或被覆盖的方法。任何被覆盖的方法或属性的行为应该与这里记录的一样,任何附加信息最好记录在服务器接口本身的代码中。

HttpRequest.method

HTTP 规范概述了各种可用于描述正在执行的请求类型的动词。这通常被称为 its 方法,不同的请求方法对如何处理它们有特定的期望。在 Django 中,用于请求的方法被表示为HttpRequest对象的method属性。它将作为标准字符串包含在内,方法名全部用大写字母。

每种方法都描述了服务器应该如何处理 URL 所标识的资源。大多数 Web 应用只实现 GET 和 POST,但是其他一些应用也值得在这里解释一下。关于这些以及这里没有列出的其他内容的更多细节可以在 HTTP 规范、 2 以及网络上的许多其他资源中找到。

  • 删除—请求删除资源。Web 浏览器不实现这个方法,所以它的使用仅限于 Web 服务应用。在典型的 Web 浏览器应用中,这样的操作是通过 POST 请求完成的,因为 GET 请求不允许有副作用,比如删除资源。
  • 获取—检索由 URL 指定的资源。到目前为止,这是 Web 上最常见的请求类型,因为每个标准的 Web 页面检索都是通过 GET 请求完成的。正如在“安全方法”一节中提到的,GET 请求被认为对服务器没有副作用;他们应该检索指定的资源,不做任何其他事情。
  • HEAD—检索有关资源的一些信息,但不获取全部内容。具体来说,对 HEAD 请求的响应应该返回与 GET 请求完全相同的头,只是响应体中没有任何内容。Web 浏览器不实现这种方法,但是由于服务器端操作本质上只是一个没有响应体的 GET 请求,所以很少会被遗漏。在 Web 服务应用中,HEAD 请求可以是一种低带宽的方式来检索有关资源的信息,如资源是否存在、上次更新时间或内容大小。
  • POST—请求以某种与 URL 指定的资源相关的方式存储附加数据。这可能意味着对博客帖子或新闻文章的评论,对问题的回答,对基于网络的电子邮件的回复或任何其他相关情况。这个定义只在 Web 服务环境中有效,在 Web 服务环境中可以区分 PUT 和 POST。在标准的 Web 浏览器中,只有 GET 和 POST 是可靠可用的,所以 POST 用于任何修改服务器上信息的情况。使用 POST 从表单提交数据只不过是官方 HTTP 规范中的一个脚注,但却是这种方法最常见的用法。
  • PUT—请求将附加数据存储在 URL 指定的资源中。这可以被视为“创建”或“替换”操作,具体取决于资源是否已经存在。不过,这种方法传统上在 Web 浏览器中不可用,所以它的使用仅限于 Web 服务应用。在标准的 Web 浏览器中,PUT 指定的操作是通过 POST 请求来完成的。
“安全”方法

正如前面提到的,各种类型的 HTTP 请求之间有一个重要的区别。该规范将 GET 和 HEAD 称为“安全”方法,它们只检索由 URL 指定的资源,而不对服务器做任何更改。明确地说,处理 GET 或 HEAD 请求的视图不应该做任何修改,除了那些与检索页面相关的修改。

安全方法的目标是允许在不同的时间多次发出相同的请求,而不会产生任何负面影响。这种假设允许书签和浏览器历史使用 GET 请求,而不会在多次发出请求时警告用户。允许更改的一个示例是更新指示页面被查看次数的计数。

“等幂”方法

除了安全方法之外,HTTP 规范还将 PUT 和 DELETE 描述为“幂等的”,这意味着,即使它们旨在对服务器进行更改,这些更改也足够可靠,以至于多次调用具有相同主体的相同请求将总是进行相同的更改。

在 PUT 的情况下,资源将在第一次执行请求时创建,并且每个后续请求将简单地用最初提交的相同数据替换资源,从而使其保持不变。对于 DELETE,最初删除资源后的每个后续请求都会导致一个错误,表明资源不存在,因此每次都保持资源的状态不变。另一方面,POST 需要对每个请求进行修改或添加。为了表示这种情况,当 POST 请求执行多次时,Web 浏览器会显示一条消息,警告用户后续请求可能会导致问题。

HttpRequest.path

该属性包含所请求的完整路径,没有附加任何查询字符串参数。这可以用来识别被请求的资源,而不依赖于哪个视图将被调用或者它将如何表现。

访问提交的数据

任何时候请求进来,都可能伴随着 Web 浏览器提供的各种数据。处理这些信息是使网站具有动态性和交互性的关键,因此 Django 使之变得简单而灵活。正如有许多方法可以向 Web 服务器提交数据一样,一旦数据到达,也有许多方法可以访问这些数据。

大多数浏览器发送的使用标准查询字符串格式 3 的数据被自动解析成一种特殊类型的字典类QueryDict。这是MultiValueDict的一个不可变子类,这意味着它的功能很像一个字典,但是增加了一些选项来处理字典中每个键的多个值。

QueryDict最重要的细节是它是用来自传入请求的查询字符串实例化的。有关如何访问QueryDict中的值的详细信息,请参见第九章中的MultiValueDict的详细信息。

HttpRequest。得到

如果请求是通过 GET 方法传入的,那么它的GET属性将是一个QueryDict,包含 URL 的查询字符串部分中包含的所有值。当然,虽然对于什么时候可以使用GET从 URL 中获取参数没有技术限制,但是干净 URL 的目标限制了它最有利的情况。

特别是,将标识资源的参数与自定义资源检索方式的参数分开是非常重要的。这是一个微妙但重要的区别。考虑下面的例子:

  • /book/pro-django/chapter07/
  • /news/2008/jun/15/website-launched/
  • /report/2008/expenses/?ordering=category

正如您所看到的,发送到 GET 请求视图的大部分数据应该放在 URL 本身,而不是查询字符串中。这将有助于搜索引擎更有效地索引它们,同时也使用户更容易记住它们并与他人交流。与许多其他原则一样,这不是一条绝对的规则,所以在工具箱中保留查询字符串和GET属性,但是要小心使用它们。

HttpRequest。邮政

如果请求是使用标准 HTML 表单的 PUT 或 POST 方法,这将是一个包含表单提交的所有值的QueryDict。无论编码类型如何,有无文件,所有标准表单都将填充POST属性。

然而,HTTP 规范允许这些请求以任何格式提供数据,因此如果传入的数据不符合查询字符串的格式,HttpRequest.POST将为空,数据将必须通过HttpRequest.raw_post_data直接读入。

HttpRequest。文件

如果传入的 PUT 或 POST 请求包含任何上传的文件,这些文件将存储在FILES属性中,该属性也是一个QueryDict,每个值是一个django.core.files.uploadedfile.UploadedFile对象。这是稍后在第九章中描述的File对象的子类,提供了一些特定于上传文件的额外属性。

  • content_type—与文件相关联的Content-Type,如果提供的话。Web 浏览器通常根据文件名的最后一部分进行分配,尽管 Web 服务调用可以根据内容的实际类型更准确地指定这一点。
  • charset—为上传文件内容指定的字符集。
HttpRequest.raw_post_data

每当请求中包含数据时,就像 PUT 和 POST 一样,raw_post_data属性提供对这些内容的访问,而不需要任何解析。对于大多数网站来说,这通常是不必要的,因为GETPOST属性更适合最常见的请求类型。Web 服务可以接受任何格式的数据,许多使用 XML 作为数据传输的主要手段。

HttpRequest.META

当请求进来时,有大量与请求相关的信息没有出现在查询字符串中,并且在请求的GETPOST属性中不可用。相反,关于请求来自哪里以及如何到达服务器的数据存储在请求的META属性中。META中可用值的详细信息可在 PEP-333 中找到。

此外,每一个请求都伴随着许多头,这些头描述了客户机想要知道的各种选项。HTTP 规范中明确规定了这些类型的头可以包含什么, 4 但是它们通常控制诸如首选语言、允许的内容类型和关于 Web 浏览器的信息之类的事情。

这些头文件也存储在META中,但是其格式与最初略有不同。所有的 HTTP 头名称都变成大写,以HTTP_为前缀,所有的破折号都用下划线代替。

  • Host变成了HTTP_HOST
  • Referer变成了HTTP_REFERER
  • X-Forwarded-For变成了HTTP_X_FORWARDED_FOR
HttpRequest。饼干

因为每个 HTTP 请求都是客户机和服务器之间的一个新连接,所以 cookies 被用作识别发出多个请求的客户机的一种方式。简而言之,cookies 只不过是向 Web 浏览器发送名称和相关值的一种方式,浏览器在每次向网站发出新请求时都会将名称和相关值发送回来。

虽然 cookie 是在流程的响应阶段设置的,如HttpResponse所述,但是从传入请求中读取 cookie 的任务非常简单。请求的COOKIES属性是一个标准的 Python 字典,将 cookies 的名称映射到之前发送的值。

请记住,该字典将包含浏览器发送的所有 cookies 的条目,即使它们是由同一服务器上的另一个应用设置的。本章后面的HttpResponse部分介绍了浏览器如何决定随特定请求发送哪些 cookies 以及如何控制该行为的具体规则。

HttpRequest.get_signed_cookie(密钥[,…])

如果您存储在 cookie 中的信息在被篡改时可能会被用来对付您,您可以选择对您的 cookie 进行签名,并在读取 cookie 时验证这些签名。签名本身是使用本章稍后描述的HttpResponse.set_signed_cookie()方法提供的,但是当在请求中读取它们时,您将需要使用这个方法。

您可以使用几个附加参数来控制 cookie 检索的行为:

  • default=RAISE_ERROR—此参数允许您指定在请求的密钥未找到或无效时应返回的默认值。这相当于向标准字典的get()方法传递一个默认值。如果您不提供一个值,这个方法将在密钥丢失时产生一个标准的KeyError,或者在签名无效时产生一个标准的django.core.signing.BadSignature
  • salt=''—这是对set_signed_cookie()方法中相同参数的补充。它允许您在应用的不同方面使用同一个密钥,也许是在多个域上,而没有签名在不同用途之间重复使用的风险。这必须与您在设置 cookie 时提供的值相匹配,以便签名检查匹配。
  • max_age=None—默认情况下,cookie 签名也有一个与之关联的过期时间,以避免它们被重复使用超过预期时间。如果您提供一个超过给定 cookie 年龄的max_age,您将得到一个django.core.signing.SignatureExpired异常。默认情况下,这不会在验证签名时检查到期日期。
HttpRequest.get_host()

许多服务器配置允许单个 Web 应用响应发送到多个不同域名的请求。为了帮助解决这些情况,传入请求的get_host()方法允许视图识别 Web 浏览器用来访问网站的名称。

除了用于发出请求的主机名之外,如果服务器被配置为在非标准端口上响应,则从该方法返回的值将包括端口号。

HttpRequest.get_full_path()

除了主机信息,get_full_path()方法还返回 URL 的完整路径部分;协议和域信息之后的所有内容。这包括用于确定使用哪个视图的完整路径,以及提供的任何查询字符串。

http request . build _ absolute _ uri(location = None)

此方法为所提供的位置(如果有)生成一个绝对 URL。如果没有显式提供位置,则返回请求的当前 URL,包括查询字符串。如果提供了位置,方法的确切行为取决于传入的值。

  • 如果该值包含完全限定的 URL(包括协议),则该 URL 已经是绝对的,并按提供的方式返回。
  • 如果该值以正斜杠(/)开头,它将被附加到当前 URL 的协议和域信息中,然后返回。这将为所提供的路径生成一个绝对 URL,而不必对服务器信息进行硬编码。
  • 否则,该值被假定为相对于请求的当前 URL 的路径,并且这两者将使用 Python 的urlparse.urljoin()实用函数连接在一起。
HttpRequest.is_secure()

如果请求使用安全套接字层(SSL)协议,这个简单的方法返回True,如果请求不安全,则返回False

HttpRequest.is_ajax()

对于“Web 2.0”站点很有用,如果请求有一个值为“XMLHttpRequest”的X-Requested-With头,这个方法返回True。大多数设计用来调用服务器的 JavaScript 库都会提供这个头,提供了一种方便的方法来识别它们。

HttpRequest.encoding

这是一个简单的属性,表示在访问前面描述的GETPOST属性时使用的编码。如果设置了一个编码,那么这些字典中的值将被强制转换为使用这种编码的unicode对象。默认情况下,它的值是None,访问值时将使用默认编码utf-8

在大多数情况下,该属性可以保持不变,大多数输入都使用默认编码进行正确转换。特定的应用可能有不同的需求,所以如果应用需要不同编码的输入,只需将该属性设置为能够正确解码这些值的值。

HttpResponse

在请求被接收和处理之后,每个视图负责返回一个响应——一个HttpResponse的实例。这个对象清晰地映射到实际的 HTTP 响应,包括头,并且是控制发送回 Web 浏览器的内容的唯一方法。像请求的表亲一样,HttpResponse住在django.http,但是有几个快捷方式可以更容易地创建响应。

创建响应

与请求不同,视图的作者可以完全控制如何创建响应,允许多种选择。标准的HttpResponse类的实例化相当简单,但是接受三个参数来定制它的行为。这些都不是必需的;本节稍后描述的选项可以用其他方式设置这些值。

  • content—接受文本或其他内容作为请求的主体。
  • status—设置请求发送的 HTTP 状态码 5
  • content_type—控制与请求一起发送的Content-Type报头。如果提供了这个,确保它在适当的时候也包含了charset值。

>>> from django.http import HttpResponse

>>> print HttpResponse()

Content-Type: text/html; charset=utf-8

>>> print HttpResponse(content_type='application/xml; charset=utf-8')

Content-Type: application/xml; charset=utf-8

>>> print HttpResponse('content')

Content-Type: text/html; charset=utf-8

content

还有一个mimetype参数,用于向后兼容旧的 Django 应用,但是应该使用content_type。不过,记住mimetype仍然很重要,因为这意味着statuscontent_type应该被指定为关键字参数,如果提供的话。

对标题的字典访问

一旦创建了响应,就很容易使用标准的字典语法定制将与其内容一起发送出去的头。这非常简单,正如您所期望的那样。与标准字典唯一值得注意的不同是,所有的键比较都不区分大小写。

>>> from django.http import HttpResponse

>>> response = HttpResponse('test content')

>>> response['Content-Type']

'text/html; charset=utf-8'

>>> response['Content-Length']

Traceback (most recent call last):

...

KeyError: 'content-length'

>>> response['Content-Length'] = 12

>>> for name, value in response.items():

...     print '%s is set to %r' % (name, value)

...

Content-Length is set to '12'

Content-Type is set to 'text/html; charset=utf-8'

对内容的类似文件的访问

除了在创建响应对象时将正文内容指定为字符串的能力之外,许多知道如何编写打开文件的第三方库也可以创建内容。Django 的HttpResponse实现了一些文件协议方法——最著名的是write()——这使得它可以被许多这样的库视为只写文件。当使用 Django 在视图中动态生成二进制内容(如 PDF 文件)时,这种技术尤其有用。

关于对响应体的类文件访问,需要注意的一件重要事情是,并不是所有的文件协议方法都实现了。这意味着某些库,比如 Python 自己的zipfile.ZipFile类,需要这些额外的方法,将会失败,并带有一个AttributeError,指示哪个方法丢失了。这是故意的,因为 HTTP 响应不是真正的文件,所以没有可预测的方法来实现这些方法。

http 响应.状态 _ 代码

此属性包含代表发送到客户端的响应类型的数字状态代码。如前所述,这可以在实例化响应对象时立即设置,但作为一个标准的对象属性,它也可以在响应创建后的任何时候设置。

这应该只设置为已知的 HTTP 响应状态代码。有关有效状态代码的详细信息,请参见 HTTP 规范。这种状态可以在实例化响应时设置,但也可以设置为子类的类属性,Django 就是这样配置它的许多专门化响应的。

HttpResponse.set_cookie(key,value=''[,…])

当希望跨多个请求存储值时,cookies 是首选工具,它通过特殊的头将值传递给 Web 浏览器,然后在后续请求时将值发送回服务器。通过用一个键和一个值调用set_cookie(),发送到客户端的 HTTP 响应将包含一个单独的头,告诉浏览器存储什么以及何时将其发送回服务器。

除了键和值之外,set_cookie()还可以接受一些额外的参数,用于配置浏览器何时应该将 cookie 发送回服务器。虽然追求可读性建议使用关键字来指定这些参数,但是这个列表使用它们的位置顺序。在 HTTP 状态管理的官方规范中可以找到关于每个选项允许的值的更多细节。 6

  • max_age=None—对应于规范中的max-age选项,指定 cookie 应该保持活动的秒数。
  • 不是所有的浏览器都像官方规范要求的那样接受和尊重max-age,而是遵循 Netscape 制定的早期模式。expires属性获取 cookie 到期的确切日期,而不是以秒为单位的偏移量。指定日期的格式如下:Sun, 15-Jun-2008 12:34:56 GMT
  • path='/'—指定浏览器将此 cookie 发送回服务器的基本路径。也就是说,如果被请求的 URL 的路径以此处指定的值开始,浏览器将随请求一起发送 cookie 的值。
  • domain=None—类似于path,它指定了 cookie 将被发送到的域。如果保留为None,cookie 将被限制到发布它的同一个域,而提供一个值将允许更大的灵活性。
  • secure=False—如果设置为True,则表示 cookie 包含敏感信息,只能通过安全连接(如 SSL)发送到服务器。

>>> response = HttpResponse()

>>> response.set_cookie('a', '1')

>>> response.set_cookie('b', '2', max_age=3600)

>>> response.set_cookie('c', '3', path='/test/', secure=True)

>>> print response.cookies

Set-Cookie: a=1; Path=/

Set-Cookie: b=2; Max-Age=3600; Path=/

Set-Cookie: c=3; Path=/test/; secure

请记住,只有在响应通过网络后,才会在浏览器中设置 cookie。这意味着在浏览器的下一次请求之前,cookie 的值在请求对象上不可用。

COOKIES AND SECURITY

尽管 cookies 是跨多个 HTTP 请求维护状态的非常有用的方法,但是它们存储在用户的计算机上,有知识的用户可以访问它们并修改它们的内容。Cookies 本身是不安全的,不应用于存储敏感数据或控制用户如何访问网站的数据。

解决这个问题的典型方法是只在 cookie 中存储一个引用,它可以用来从服务器上的某个地方检索“真正的”数据,比如用户无权访问的数据库或文件。本章末尾的“应用技术”一节提供了一种在 cookies 中安全存储数据的替代方法,这样他们的数据实际上是可信的。

HttpResponse.delete_cookie(key,path='/',domain=None)

如果一个 cookie 已经被发送到 Web 浏览器,并且不再需要或者已经无效,那么可以使用delete_cookie()方法来指示浏览器删除它。如上所述,这里提供的路径和域必须与现有的 cookie 相匹配,以便正确删除它。

它通过设置一个新的 cookie 来做到这一点,将max-age设置为0,将expires设置为Thu, 01-Jan-1970 00:00:00 GMT。这会导致浏览器覆盖任何匹配相同keypathdomain的现有 cookie,然后立即使其过期。

httpresponse . cookies

除了能够在响应阶段显式设置和删除 cookie 之外,您还可以查看将被发送到 Web 浏览器的 cookie。cookies属性使用 Python 的标准Cookie模块, 7 ,属性本身是一个SimpleCookie对象,其行为很像一个字典,每个值都是一个Morsel对象。

使用 cookie 的名称作为键,可以检索代表特定 cookie 值的Morsel,以及相关的选项。这个对象可以用作字典来引用这些附加选项,而它的value属性包含为 cookie 设置的值。使用这个字典甚至可以访问已删除的 cookie,因为这个过程涉及到设置一个将立即过期的新 cookie。

>>> len(response.cookies)

3

>>> for name, cookie in response.cookies.items():

...     print '%s: %s (path: %s)' % (name, cookie.value, cookie['path'])

...

a: 1 (path: /)

b: 2 (path: /test/)

c: 3 (path: /)

httpresponse . set _ signed _ cookie(key,value,salt=''[,…])

这就像set_cookie()一样工作,除了它在将值发送到浏览器之前也加密签名。因为 cookie 存储在浏览器中,这确保了用户在再次访问您的站点之前不会修改这些 cookie 中的值。您仍然不想在 cookie 中存储敏感信息,但这允许您放心地在 cookie 中存储登录用户名等信息,而用户无法将其用作攻击媒介。

它采用与set_cookie()相同的参数,只增加了一个参数:salt。默认情况下,Django 使用您的settings.SECRET_KEY来生成签名,这在大多数情况下是没问题的,因为带有特定键的 cookie 只可能用于一个目的。在其他情况下,salt参数允许您为您当前拥有的任何用途制作一个签名。

例如,如果您使用一个 Django 安装提供多个域,您可以使用域名作为签名的 salt,这样用户就不能在不同的域上重用一个域的签名。不同的 salts 确保签名是不同的,因此当在您的视图中检索 cookie 时,复制的签名将无法通过签名测试。

HttpResponse.content

此属性提供对响应正文的字符串内容的访问。这可以被读取或写入,并且在中间件处理的响应阶段特别有用。

专业回应对象

由于有几种常见的 HTTP 状态代码,Django 提供了一组定制的HttpResponse子类,它们的status_code属性已经相应地设置好了。和HttpResponse本身一样,这些都住在django.http。其中一些采用了与标准HttpResponse不同的一组参数,这些差异也在这里列出。

  • HttpResponseRedirect—采用单个参数,即浏览器将重定向到的 URL。它还将status_code设置为 302,指示资源所在的“发现”状态。
  • HttpResponsePermanentRedirect—采用单个参数,即浏览器将重定向到的 URL。它将status_code设置为 301,表示资源被永久地移动到指定的 URL。
  • HttpResponseNotModified—将status_code设置为 304,表示“未修改”状态,当响应没有改变与请求相关的条件时,用于响应条件 GET。
  • HttpResponseBadRequest—将status_code设置为 400,表示视图无法理解请求中使用的语法的“错误请求”。
  • HttpResponseForbidden—将status_code设置为 403,“禁止”,其中请求的资源确实存在,但是请求用户没有访问它的权限。
  • HttpResponseNotFound—可能是所有定制类中最常见的,它将status_code设置为 404,“未找到”,其中请求中的 URL 没有映射到已知资源。
  • HttpResponseNotAllowed—将status_code设置为 405,“不允许”,表示请求中使用的方法对于 URL 指定的资源无效。
  • HttpResponseGone—将status_code设置为 410,“消失”,表示由 URL 指定的资源不再可用,并且不能在任何其他 URL 上找到。
  • HttpResponseServerError—将status_code设置为 500,“服务器错误”,每当视图遇到不可恢复的错误时使用。

Web 浏览器不支持其中的一些专用响应,但是它们对于 Web 服务应用都非常有用,在 Web 服务应用中有更广泛的选项可用。在站点范围内设置这些状态通常更有意义,因此单个视图不必担心直接管理它们。为此,Django 提供了 HTTP 中间件。

编写 HTTP 中间件

Django 自己创建一个HttpRequest,每个视图负责创建一个HttpResponse,应用通常需要对每个传入请求或传出响应执行特定的任务。流程的这一部分称为中间件,是将高级处理注入流程的有用方式。

中间件处理的常见示例包括压缩响应内容、拒绝访问某些类型的请求或来自某些主机的请求,以及记录请求及其相关响应。虽然这些任务可以在单独的视图中完成,但是这样做不仅需要大量的样板文件,还需要每个视图了解将要应用的中间件的每一部分。

这也意味着添加或删除 HTTP 处理需要触及整个项目中的每一个视图。这不仅是一个维护问题,而且如果您的项目使用任何第三方应用,还会导致额外的维护问题。毕竟,更改第三方代码会限制您在将来升级代码时避免不必要的麻烦。Django 通过在请求/响应周期的独立部分执行中间件操作来解决这些问题。

每个中间件都只是一个 Python 类,它至少定义了以下方法之一。这门课没有其他要求;也就是说,它不必继承任何提供的基类,包含任何特定的属性或以任何特定的方式实例化。只要在一个重要的位置提供这个类,站点就能够激活它。

中间件可以在四个不同的点上与 Django 的 HTTP 处理挂钩,执行它需要的任何任务。通过在中间件类上指定一个方法,可以简单地控制流程的每个部分。记住,这只是 Python,所以任何有效的 Python 在中间件中也是有效的。

middleware class . process _ request(self,request)

一旦传入的 HTTP 请求变成了一个HttpRequest对象,中间件就有机会改变事情的处理方式。这个钩子甚至在 Django 分析 URL 以决定使用哪个视图之前就出现了。

作为标准的 Python,process_request()方法可以执行任何任务,但是常见的任务包括禁止访问某些客户端或请求类型,为上下文处理器使用的请求添加属性,或者基于请求的细节返回之前缓存的响应。

这个方法可以更改请求的任何属性,但是要记住,任何更改都会影响 Django 在流程的其余部分处理请求的方式。例如,因为这个方法在 URL 解析之前被调用,它可以修改request.path来将请求重定向到一个完全不同的视图。虽然像这样的行为通常是所期望的,但它可能是一个意想不到的副作用,所以在修改请求时要小心。

middleware class . process _ view(self,request,view,args,kwargs)

该方法在 URL 被映射到视图并从中提取参数之后,但在实际调用视图之前被调用。除了请求之外,传递给此方法的参数如下:

  • view—将被调用的视图功能。这是实际的函数对象,而不是名称,无论视图是使用字符串还是可调用的。
  • args—包含将传递给视图的位置参数的元组。
  • kwargs—包含将传递给视图的关键字参数的字典。

既然已经从 URL 中提取了视图的参数,就有可能根据配置应该获得的内容来验证这些参数。这在开发过程中非常有用,可以用来验证一切都配置正确。简单地建立一个中间件类来打印出argskwargs变量以及request.path。然后,如果一个视图出现任何问题,开发服务器的控制台将有一个便捷的方法来识别或排除潜在的问题。

这似乎是对即将执行的视图进行详细记录的绝佳机会,因为视图函数对象也是可用的。虽然这是真的,但是在视图上装饰器的普遍使用使事情变得复杂了。具体来说,传递给这个方法的视图函数通常是由装饰器创建的包装函数,而不是视图本身。

这意味着第二章中详述的内省特性不能可靠地用于将位置参数与它们在函数定义中的名称对齐。尽管如此,仍然有一些好处,因为只要装饰者使用在第九章的中描述的特殊的wraps装饰者,你仍然能够访问视图的模块和名称。

class ArgumentLogMiddleware(object):

def process_view(request, view, args, kwargs):

print 'Calling %s.%s' % (view.__module__, view.__name__)

print 'Arguments: %s' % (kwargs or (args,))

middleware class . process _ response(自身、请求、响应)

视图执行后,新的响应对象可供中间件查看并进行必要的修改。在这里,中间件可以缓存响应以备将来使用,压缩响应体以加快网络传输,或者修改将与响应一起发送的头和内容。

它接收原始请求对象以及视图返回的响应对象。此时,请求对于 HTTP 循环已经没有任何用处了,但是如果使用它的一些属性来决定如何处理响应,那么它还是很有用的。在被方法返回之前,响应对象可以在这个阶段被修改,而且经常被修改。

process_response()方法应该总是返回一个HttpResponse对象,不管之前对它做了什么。大多数情况下,这将是最初给出的响应,只是有一些小的修改。有时,返回一个完全不同的响应可能更有意义,比如重定向到一个不同的 URL。

middleware class . process _ exception(自身,请求,异常)

如果在请求处理过程的任何部分出错,包括中间件方法,通常会抛出一个异常。这些异常中的大多数将被发送到process_exception()以记录或以特殊方式处理。传递给该方法的异常参数是抛出的异常对象,它可用于检索有关出错的具体细节。

这个阶段的一个常见任务是以特定于当前使用的站点的方式记录异常。异常的字符串表示及其类型通常就足够了,尽管它的确切用途将取决于引发的异常。通过将原始请求的细节与异常的细节结合起来,您可以生成有用且可读的日志。

在中间件和视图装饰器之间做出选择

第四章展示了视图如何使用装饰器在视图执行之前或之后执行额外的工作,热心的读者会注意到中间件可以执行类似的功能。视图装饰者可以访问传入的请求以及视图生成的响应。他们甚至可以访问视图函数和将要传递给它的参数,并且可以将视图包装在一个try块中,以处理任何引发的异常。

那么是什么使它们不同,什么时候应该使用其中一个而不是另一个呢?这是一个相当主观的话题,没有一个答案可以满足所有情况。每种方法都有优点和缺点,这应该有助于您决定对于特定的应用采用哪种方法。

范围上的差异

中间件和视图装饰器之间最显著的区别之一是覆盖了多少站点。中间件在一个站点的settings.py中被激活,因此它覆盖了任何 URL 上的所有请求。这个简单的事实提供了一些优势:

  • 许多操作——比如缓存或压缩——对于站点上的每个请求都应该自然发生;中间件使得这些任务很容易实现。
  • 未来对站点的添加会自动被现有的中间件覆盖,而不必为它们提供的行为做任何特殊的让步。
  • 第三方应用不需要任何修改就可以利用中间件的行为。

另一方面,decorator 应用于单独的函数,这意味着每个视图都必须手动添加 decorator。这使得 decorator 的管理更加耗时,但是一些操作——比如访问限制或专门的缓存需求——更适合于站点的有限部分,在那里 decorator 可以发挥巨大的作用。

配置选项

中间件类被引用为包含类的导入路径的字符串,这不允许任何直接的方式来配置它们的任何特性。大多数接受选项中间件都是通过特定于该中间件的自定义设置来接受选项的。这确实提供了一种定制中间件工作方式的方法,但是就像中间件本身一样,根据定义,这些设置是站点范围的。没有为个人视图定制它们的空间。

如第二章所示,装饰器可以被编写成在应用于一个函数时接受配置选项,视图装饰器也不例外。每个视图可以有一组单独的选项,或者用一组预配置的参数用curry创建一个全新的装饰器。

使用中间件作为装饰者

考虑到中间件和装饰器之间的相似性,Django 提供了一个实用程序来将现有的中间件类转换成装饰器。这使得代码可以在整个站点中重用,在任何情况下都可以使用最好的工具。

django.utils.decorators中,特殊的decorator_from_middleware()函数将一个应该应用于单个视图的中间件类作为唯一的参数。返回值是一个完美的函数修饰器,可以应用于任意数量的视图。

允许配置选项

由于装饰者可以接受选项来配置他们的行为,我们需要一种方法让中间件类利用这种灵活性。在中间件类上提供一个接受额外参数的__init__()方法将允许从头开始编写一个类,既可以用作中间件,也可以用作视图装饰器。

需要记住的一点是,中间件最常见的调用是不带任何参数的,所以您定义的任何附加参数都必须使用默认值。如果不这样做,那么无论何时当它被用作标准中间件时,都会导致一个TypeError,并且单独使用decorator_from_middleware(),它不接受任何参数。

class MinimumResponseMiddleware(object):

"""

Makes sure a response is at least a certain size

"""

def __init__(self, min_length=1024):

self.min_length = min_length

def process_response(self, request, response):

"""

Pads the response content to be at least as

long as the length specified in __init__()

"""

response.content = response.content.ljust(self.min_length)

当用作中间件时,该类将填充所有响应,长度至少为 1,024 个字符。为了让单个视图获得这个最小长度的具体值,我们可以转而使用decorator_from_middleware_with_args()。它将在修饰视图时接受参数,并将这些参数传递给中间件类的__init__()方法。

另外,请注意,如果一个中间件类已经被定义为中间件和装饰器,那么任何使用装饰器的视图实际上都将为每个请求调用中间件两次。对于某些应用,比如在请求对象上设置属性的应用,这不是问题。对于其他人,尤其是那些修改外发响应的人,这可能会带来很多麻烦。

HTTP 相关信号

因为请求不受任何应用代码的控制,所以使用信号来通知应用代码所有请求/响应周期的开始和完成。像所有的信号一样,这些只是简单的Signal对象,它们存在于django.core.signals。关于信号、信号如何工作以及如何使用信号的更多信息,参见第九章。

django . core . signals . request _ started

每当从外部收到一个请求时,这个信号被触发,没有任何附加参数。它在进程的早期触发,甚至在HttpRequest对象被创建之前。如果没有任何参数,它的用途是有限的,但它确实提供了一种方法,在任何中间件有机会访问请求对象之前,在收到请求时通知应用。

这种方法的一个潜在用途是为其他信号注册新的侦听器,它应该只在通过 HTTP 传入的请求期间运行。这与其他信号可能由于一些非 HTTP 事件而被触发的情况形成对比,例如一个调度的作业或一个命令行应用。

django . core . signals . request _ finished

一旦视图生成了响应并且中间件得到了处理,这个信号就在将响应发送回发送原始请求的客户机之前触发。像request_started一样,它没有向监听器提供任何参数,所以它的使用相当有限,但是它可以作为一种方法来断开在request_started触发时连接的任何监听器。

django . core . signals . got _ request _ exception

如果在处理请求的过程中出现异常,但没有在其他地方显式处理,Django 会触发只有一个参数的got_request_exception信号:正在处理的请求对象。

这与中间件的process_exception()方法不同,后者只在视图执行过程中发生错误时触发。许多其他异常也会触发这个信号,比如 URL 解析或任何其他中间件方法中的问题。

应用技术

通过在协议处理中提供如此多的钩子,Django 使得为应用修改 HTTP 流量的各种选项成为可能。在这个领域,每个应用都有自己的需求,这取决于它接收的流量类型和它期望提供的接口类型。因此,下面的例子更多地解释了如何挂接 Django 的 HTTP 处理,而不是详尽地列出定制这种行为可以做些什么。

自动签署 Cookies

Django 对签名 cookie 的支持很方便,但是它要求您调用不同的方法来设置和检索 cookie,以确保签名被正确地应用和验证。您不能简单地访问请求上的cookies属性而不失去签名的安全性优势。然而,通过使用定制的中间件,完全可以做到这一点:使用通常为未签名的 cookies 保留的简单访问方法,自动添加和验证签名。

概括地说,这个中间件将负责几项任务:

  • 在传出请求中签署 cookies
  • 验证和删除传入请求的 cookies
  • 管理这些签名的 salt 和过期选项

前两个任务可以相当简单地完成,通过检查请求和响应来寻找 cookie,并调用 cookie 方法的有符号变体来管理签名。让我们从设置响应 cookies 开始。

签名传出响应 Cookies

中间件可以从process_response()方法开始,该方法需要找到视图设置的任何 cookies,并将签名添加到它们的值中。

class SignedCookiesMiddleware(object):

def process_response(self, request, response):

for (key, morsel) in response.cookies.items():

response.set_signed_cookie(key, morsel.value

max_age=morsel['max-age']

expires=morsel['expires']

path=morsel['path']

domain=morsel['domain']

secure=morsel['secure']

)

return response

这种方法在设置新的 cookie 时使用原始 cookie 的所有属性,因此除了用于设置它的方法之外,它是完全相同的。使用set_signed_cookie()将在幕后做所有适当的事情。

被删除的 cookies 也会出现在response.cookies中,尽管它们没有值,也不需要签名。这些可以通过它们的0max-age来识别,这可以用来忽略它们,只对应用重要的实际值进行签名。

class SignedCookiesMiddleware(object):

def process_response(self, request, response):

for (key, morsel) in response.cookies.items():

if morsel['max-age'] == 0:

# Deleted cookies don't need to be signed

continue

response.set_signed_cookie(key, morsel.value

max_age=morsel['max-age']

expires=morsel['expires']

path=morsel['path']

domain=morsel['domain']

secure=morsel['secure']

)

return response

正在验证传入的请求 Cookies

处理传入的请求也相当简单。process_request()方法是这部分流程的入口点,它只需找到所有传入的 cookies,并使用get_signed_cookie()来检查签名,并从值中删除这些签名。

class SignedCookiesMiddlewar

e(object):

def process_request(self, request):

for key in request.COOKIES:

request.COOKIES[key] = request.get_signed_cookie(key)

读取 cookies 比编写它们更简单,因为我们不必处理所有的单个参数;它们已经是饼干的一部分了。但是,这段代码仍然有一个问题。如果任何签名丢失、无效或过期,get_signed_cookie()将引发一个异常,我们需要以某种方式处理它。

一种选择是简单地让错误通过,希望它们会在其他代码中被捕获,但是因为您的视图和其他中间件甚至不知道这个中间件正在签署 cookies,所以它们不太可能处理签名异常。更糟糕的是,如果您没有处理这些异常的代码,它们将会一直出现在您的用户面前,通常以 HTTP 500 错误的形式出现,这根本不能解释这种情况。

相反,这个中间件可以直接处理异常。由于只有具有有效签名的值才能传递给视图,一个显而易见的方法是简单地从请求中删除所有无效的 cookies。异常会随着生成这些异常的 cookies 一起消失。您的视图将只看到有效的 cookie,正如它们所期望的那样,任何无效的 cookie 都不会再存在于请求中。用户可以随时清除他们的 cookie,所以依赖于 cookie 的视图应该总是处理丢失 cookie 的请求,所以这种方法非常适合视图已经在做的事情。

支持这种行为只需要捕捉相关的异常并删除那些引发异常的 cookies。

from django.core.signing import BadSignature, SignatureExpired

class SignedCookiesMiddleware(object):

def process_request(self, request):

for (key, signed_value) in request.COOKIES.items():

try:

request.COOKIES[key] = request.get_signed_cookie(key)

except (BadSignature, SignatureExpired):

# Invalid cookies should behave as if they were never sent

del request.COOKIES[key]

作为装饰者签署饼干

到目前为止,SignedCookiesMiddleware在设置和检索签名的 cookies 时还没有使用任何特定于签名的选项。对于打算在整个站点上使用的中间件来说,默认设置通常已经足够好了。但是,由于中间件也可以用作装饰器,所以我们还需要考虑对单个视图的定制。这就是 salt 和 expiration 设置有用的地方。

如本章前面所示,decorator_from_middleware()可以向中间件的__init__()方法提供参数,这样就为定制saltmax_age参数提供了一个途径。一旦在__init__()中接受了这些参数,各个钩子方法就可以适当地合并它们。

from django.core.signing import BadSignature, SignatureExpired

class SignedCookiesMiddleware(object):

def __init__(self, salt='', max_age=None):

self.salt = salt

self.max_age = max_age

def process_request(self, request):

for (key, signed_value) in request.COOKIES.items():

try:

request.COOKIES[key] = request.get_signed_cookie(key

salt=self.salt

max_age=self.max_age)

except (BadSignature, SignatureExpired):

# Invalid cookies should behave as if they were never sent

del request.COOKIES[key]

def process_response(self, request, response):

for (key, morsel) in response.cookies.items():

if morsel['max-age'] == 0:

# Deleted cookies don't need to be signed

continue

response.set_signed_cookie(key, morsel.value

salt=self.salt

max_age=self.max_age or morsel['max-age']

expires=morsel['expires']

path=morsel['path']

domain=morsel['domain']

secure=morsel['secure']

)

return response

现在您可以使用decorator_from_middleware_with_args()创建一个装饰器,并提供saltmax_age参数来为每个视图定制装饰器的行为。

from django.utils.decorators import decorator_from_middleware_with_args

signed_cookies = decorator_from_middleware_with_args(SignedCookiesMiddleware)

@signed_cookies(salt='foo')

def foo(request, ...):

...

现在怎么办?

请求和响应周期是 Django 应用用来与外界通信的主要接口。同样重要的是后台可用的实用程序集合,它们允许应用执行最基本的任务。

Footnotes 1

http://prodjango.com/pep-333/

  2

http://prodjango.com/http-methods/

  3

http://prodjango.com/query-string/

  4

http://prodjango.com/http-headers/

  5

http://prodjango.com/http-status-codes/

  6

http://prodjango.com/cookie-spec/

  7

http://prodjango.com/r/cookie-module/

八、后端协议

Abstract

作为一个框架,Django 的目的是提供一组内聚的接口,使最常见的任务变得更容易。其中一些工具完全包含在 Django 本身中,很容易保持一致性。许多其他功能是——或者至少可能是——由外部软件包提供的。

作为一个框架,Django 的目的是提供一组内聚的接口,使最常见的任务变得更容易。其中一些工具完全包含在 Django 本身中,很容易保持一致性。许多其他功能是——或者至少可能是——由外部软件包提供的。

尽管 Django 本身支持这些不同特性的一些最常见的软件包,但还有更多,尤其是在公司环境中。除了开发人员对一种类型的数据库比对另一种类型的数据库的偏好之外,许多其他服务器已经被现有的应用使用,这些应用不容易被转换以使用不同的东西。

因为这些类型的问题在现实生活中确实会出现,Django 提供了简单的方法来引用这些特性,而不用担心是什么实现在后台实际发生了这些问题。这个相同的机制还允许您用第三方代码替换掉许多这些低级功能,以支持连接到其他系统,或者只是定制某些方面的行为。

本章列出的部分有双重目的。除了记录 Django 针对这些特性的通用 API 之外,每一节还将描述如何编写新的后端来实现这些特性。这不仅包括要声明什么类和方法,还包括包结构可能是什么样子,以及拼图的每一部分应该如何表现。

数据库访问

连接到数据库是现代 Web 应用最基本的需求之一,有多种选择。目前,Django 支持一些更流行的开源数据库引擎,包括 MySQL、PostgreSQL 和 SQLite,甚至一些商业产品,如 Oracle。

考虑到不同数据库系统的独特特性和 SQL 不一致性,Django 需要在它的模型和数据库本身之间增加一个额外的层,这个层必须为使用的每个数据库引擎专门编写。每个受支持的选项在 Django 中都作为包含这个中间层的独立 Python 包提供,但是其他数据库也可以通过外部提供这个层来支持。

虽然 Python 提供了一个访问数据库的标准化 API,但每个数据库系统都以稍微不同的方式解释基本的 SQL 语法,并在此基础上支持不同的特性集,因此这一节将重点关注 Django 提供的与模型访问数据库的方式挂钩的领域。这就把在每种情况下制定正确查询的本质细节留给了读者。

Django、db、backends

这引用了后端包的base模块,从这里可以访问整个数据库。以这种方式访问数据库后端确保了统一、一致的界面,而不管后台使用的是哪个数据库包。

Django 做了大量的工作来使这种级别的访问变得不必要,但是在不使事情过于复杂的情况下,它只能做到这一步。当 ORM 无法提供一些必要的功能时——例如,在纯 SQL 中根据另一列的值更新一列——总是可以直接找到源代码,查看真正发生了什么,并调整标准行为或完全替换它。

因为这实际上只是一个后端特定模块的别名,所以本章列出的完整导入路径只有在试图以这种方式访问数据库时才有效。当实现一个新的后端时,包路径将特定于该后端。例如,如果一个用于连接 IBM 的 DB2 2 的后端被放在一个名为db2的包中,这个模块实际上应该位于db2/base.py

数据库包装器

数据库后端的主要特性之一是DatabaseWrapper,这个类充当 Django 和数据库库本身特性之间的桥梁。所有数据库特性和操作都要通过这个类,特别是在django.db.connection提供的一个实例。

使用DATABASE_OPTIONS设置作为关键字参数的字典,自动创建DatabaseWrapper的实例。这个类没有任何强制的参数集,所以记录后端接受什么参数是很重要的,这样开发人员就可以相应地定制它。

DatabaseWrapper类中有一些属性和方法定义了后端行为的一些更一般的方面。其中大多数都在一个基类中进行了适当的定义,以使这变得更容易。通过子类化django.db.backends.BaseDatabaseWrapper,可以继承一些明智的默认行为。

尽管单个后端可以自由地用任何合适的自定义行为覆盖它们,但有些行为必须总是由后端的DatabaseWrapper明确定义。在这种情况下,以下部分将直接陈述这一要求。

数据库包装器.功能

这个对象通常是一个被指定为django.db.backends.DatabaseFeatures的类的实例,它包含的属性表明后端是否支持 Django 可以利用的各种数据库相关特性。虽然这个类在技术上可以被命名为任何东西,因为它只作为DatabaseWrapper的一个属性被访问,但是最好还是与 Django 自己的命名约定保持一致,以避免混淆。

DatabaseWrapper本身一样,Django 提供了一个基类,为这个对象上所有可用的属性指定默认值。位于django.db.backends.BaseDatabaseFeatures,这可以用来大大简化特定后端中的特性定义。只需覆盖与所讨论的后端不同的任何特性定义。

以下是受支持功能及其默认支持状态的列表:

  • allows_group_by_pk—指示GROUP BY子句是否可以使用主键列。如果是这样,Django 可以在这些情况下使用它来优化查询;默认为False
  • can_combine_inserts_with_and_without_auto_increment_pk—当一次插入多条记录时,该属性指示后端是否可以支持将一些具有自动递增主键的值的记录与其他没有值的记录一起插入。这默认为False,Django 将在将记录插入数据库之前从数据中删除这些主键值。
  • can_defer_constraint_checks—指示数据库是否允许删除记录而不首先使指向该记录的任何关系无效;默认为False
  • can_distinct_on_fields—表示数据库是否支持使用DISTINCT ON子句只检查某些字段的唯一性。这默认为True,所以如果数据库不支持这个子句,一定要覆盖下一节中描述的distinct_sql()方法,以便在请求字段时引发一个异常。
  • can_introspect_foreign_keys—指示数据库是否为 Django 提供了一种方法来确定正在使用哪些外键;默认为True
  • can_return_id_from_insert—指示后端是否可以在插入记录后立即提供新的自动递增的主键 ID。默认为False;如果设置为True,您还需要提供下一节描述的return_insert_id()功能。
  • can_use_chunked_reads—指示数据库是否可以迭代部分结果集,而不必一次全部读入内存。默认为True;如果False,Django 将把所有结果加载到内存中,然后再把它们传递回应用。
  • empty_fetchmany_value—指定当提取多行时,数据库库返回什么值来指示没有更多数据可用;默认为空列表。
  • has_bulk_insert—表示后端是否支持在一条 SQL 语句中插入多条记录;默认为False
  • has_select_for_update—表示数据库是否支持SELECT FOR UPDATE查询,即在使用行时锁定该行;默认为False
  • has_select_for_update_nowait—如果您使用SELECT FOR UPDATE,并且另一个查询已经有一个锁,一些后端允许您指定一个NOWAIT选项立即失败,而不是等待锁被释放。此属性指示数据库是否支持此功能;默认为False
  • interprets_empty_strings_as_nulls—表示数据库是否将空字符串视为与NULL相同的值;默认为False
  • needs_datetime_string_cast—表示从数据库中检索日期后,是否需要将日期从字符串转换为datetime对象;默认为True
  • related_fields_match_type—指示数据库是否要求关系字段与其相关字段的类型相同。这专门用于PositiveIntegerFieldPositiveSmallIntegerField类型;如果True,将使用相关字段的实际类型来描述关系;如果默认为False,Django 将使用IntegerField来代替。
  • supports_mixed_date_datetime_comparisons—指示数据库是否支持在查找记录时使用timedeltadatedatetime进行比较;默认为True。如果设置为True,确保也提供下一节描述的date_interval_sql()方法。
  • supports_select_related—指示后端是否允许QuerySet提前拉入相关信息,以减少许多情况下的查询数量。它默认为True,但是在处理非关系数据库时可以设置为False,在非关系数据库中,“相关”的概念实际上并不适用。
  • supports_tablespaces—表示该表是否支持表空间。它们不是 SQL 标准的一部分,所以默认为False。如果将其设置为True,请务必执行下一节中描述的tablespace_sql()方法。
  • update_can_self_select—指示数据库是否能够对当前正在用UPDATE查询修改的表执行SELECT子查询;默认为True
  • uses_autocommit—表示后端是否允许数据库直接管理自动提交行为;默认为False
  • uses_custom_query_class—指示后端是否提供其自己的Query类,该类将用于定制如何执行查询;默认为False
  • uses_savepoints—指示除了完整事务之外,数据库是否支持保存点。保存点允许在更细粒度的基础上回滚数据库查询,而不需要在出错时撤销整个事务。该属性默认为False;将其设置为True还需要实现下一节中描述的savepoint_create_sql()savepoint_commit_sql()savepoint_rollback_sql(sid)方法。

除了在测试中,这个类还有一些 Django 不直接使用的附加属性。如果您试图使用这些特性,Django 将简单地传递原始数据库错误。这些属性仅在测试中使用,以确认数据库确实应该为相关操作引发错误。

  • allow_sliced_subqueries—表示后端是否可以对子查询执行切片操作;默认为True
  • allows_primary_key_0—表示后端是否允许 0 作为主键列的值;默认为True
  • has_real_datatype—指示数据库是否具有表示实数的本地数据类型;默认为False
  • ignores_nulls_in_unique_constraints—当在具有跨多个列的唯一约束的表上检查重复项时,一些数据库将考虑NULL值并防止重复项,而其他数据库将忽略它们。该属性默认为True,这表明如果只有重复的列包含NULL值,数据库将允许重复的条目。
  • requires_explicit_null_ordering_when_grouping—指示当使用GROUP BY子句来防止数据库尝试对记录进行不必要的排序时,数据库是否需要额外的ORDER BY NULL子句;默认为False
  • requires_rollback_on_dirty_transaction—如果某个事务由于某种原因无法完成,该属性表示该事务是否需要回滚才能开始新的事务;默认为False
  • supports_1000_query_parameters—指示后端是否支持最多 1000 个传递到查询中的参数,尤其是在使用IN操作符时;默认为True
  • supports_bitwise_or—顾名思义,这个表示数据库是否支持按位OR操作;默认为True
  • supports_date_lookup_using_string—表示在查询datedatetime字段时,是否可以使用字符串而不是数字;默认为True
  • supports_forward_references—如果数据库在事务结束时检查外键约束,一条记录将能够引用另一条尚未添加到事务中的记录。默认情况下这是True,但是如果数据库改为为事务中的每条记录检查这些约束,您需要将它设置为False
  • supports_long_model_names—这一条更加简单明了,表明数据库是否允许表名比您通常预期的要长。这默认为True,主要用于测试 MySQL,它只支持 64 个字符的表名。
  • supports_microsecond_precision—指示datetimetime字段在数据库级别是否支持微秒;默认为True
  • supports_regex_backreferencing—表示数据库的正则表达式引擎是否支持使用分组和那些组的反向引用;默认为True
  • supports_sequence_reset—表示数据库是否支持重置序列;默认为True
  • supports_subqueries_in_group_by—指示数据库是否支持从子查询中选择,同时使用GROUP BY子句执行聚合;默认为True
  • supports_timezones—表示在与数据库中的datetime字段交互时,是否可以提供具有时区的datetime对象;默认为True
  • supports_unspecified_pk—如果模型使用主键而不是默认的自动递增选项,每个实例通常需要指定一个主键。如果数据库甚至在没有主键的情况下保存实例,您需要将其设置为True,这样 Django 就可以跳过对该行为的测试。
  • test_db_allows_multiple_connections—指示仅测试数据库是否支持多个连接。这默认为True,因为大多数数据库都支持它,但其他数据库可能会使用内存数据库之类的东西进行测试,这可能不支持多个连接。
数据库包装器

这是大多数特定于数据库的特性的入口,主要是处理每个数据库在处理特定类型的 SQL 子句时的各种差异。每个数据库供应商都有自己的一套需要支持的特殊语法,在后端定义这些语法可以让 Django 无需担心这些细节。

像前面描述的情况一样,后端只需要编写那些偏离标准的操作。同样位于django.db.models.backendsBaseDatabaseOperations,为许多这些操作提供了默认行为,而其他的必须由后端自己实现。下面的列表解释了它们的用途和默认行为。

  • autoinc_sql(table, column)—返回创建自动递增主键所需的 SQL。如果数据库有一个本地支持的字段,那么将使用“创建新结构”一节中描述的creation模块选择该字段,并且该方法应该返回None而不是任何 SQL 语句,这也是默认行为。
  • bulk_batch_size(fields, objs)—批量插入记录时,您会发现有些数据库有限制,要求将记录拆分成多个批次。给定要插入的字段和包含这些字段值的对象,此方法返回要在单个批处理中插入的记录数。默认实现只是返回对象的数量,因此总是使用一个批处理来插入任意数量的记录。
  • cache_key_culling_sql()—返回用于选择要剔除的缓存关键字的 SQL 模板。返回的模板字符串应该包含一个%s占位符,它将是缓存表的名称。它还应该包含一个%%s引用,这样以后就可以用应该剔除的键之前的最后一个键的索引来替换它。
  • compiler(compiler_name)—根据给定的编译器名称返回 SQL 编译器。默认情况下,该方法将根据BaseDatabaseOperations对象的compiler_module属性导入一个模块,并在该模块中查找给定的compiler_namecompiler_module被设置为"django.db.models.sql.compiler",但是如果您想使用自己的编译器而不覆盖这个方法,您可以覆盖它。
  • date_extract_sql(lookup_type, field_name)—返回只提取部分日期的 SQL 语句,以便与过滤器参数进行比较。lookup_type将是"year""month""day"中的一个,而field_name是包含要检查的日期的表格列的名称。这没有默认行为,必须由后端定义以避免出现NotImplementedError
  • date_interval_sql(sql, connector, timedelta)—返回一个 SQL 子句,该子句将执行带有datedatetime列和timedelta值的操作。sql参数将包含用于datedatetime列的必要 SQL,而connector将包含将与timedelta值一起使用的运算符。这个方法负责格式化表达式,以及使用数据库的词汇表描述timedelta
  • date_trunc_sql(lookup_type, field_name)—返回一条 SQL 语句,该语句删除了超出lookup_type所提供的特定性的日期部分。可能的值与date_extract_sql()的值相同,但不同之处在于,例如,如果lookup_type"month",它将返回一个指定月份和年份的值,而date_extract_sql()将返回不带年份的月份。同样像date_extract_sql()一样,这不是默认行为,必须实现。
  • datetime_cast_sql()—返回将datetime值强制转换为数据库库使用的任何格式所需的 SQL,以返回 Python 中真正的datetime对象。返回值将被用作 Python 格式的字符串,它将只接收字段名,在字符串中被引用为%s。默认情况下,它只返回"%s",这对于不需要任何特殊类型转换的数据库来说很好。
  • deferrable_sql()—返回附加到约束定义所需的 SQL,以使该约束最初被延迟,以便在事务结束之前不会被检查。这将被附加在约束定义之后,因此如果需要空格,返回值必须在开头包含空格。默认情况下,这将返回一个空字符串。
  • distinct_sql(fields)—返回一个 SQL 子句来选择唯一的记录,也可以根据字段名称列表进行选择。当fields为空时,默认实现返回"DISTINCT",当fields被填充时,默认实现引发NotImplementedError,所以如果数据库支持基于有限字段集的唯一性检查,一定要覆盖这个实现。
  • drop_foreignkey_sql()—返回将删除外键引用的 SQL 片段,作为ALTER TABLE语句的一部分。引用的名称将在后面自动追加,因此只需指定命令本身。例如,默认返回值仅仅是"DROP CONSTRAINT"
  • drop_sequence_sql(table)—返回一条 SQL 语句,从指定的表中删除自动递增序列。这与autoinc_sql()形成了某种配对,因为如果序列是显式创建的,那么只需要显式删除它。默认情况下,这将返回None,表示不采取任何行动。
  • end_transaction_sql(success=True)—返回结束打开的事务所需的 SQL。success参数指示交易是否成功,并可用于确定采取什么行动。例如,如果success被设置为True,则默认实现返回"COMMIT;",否则返回"ROLLBACK;"
  • fetch_returned_insert_id(cursor)—返回支持获取该信息的后端最后插入的记录的 ID。默认实现调用cursor.fetchone()[0]
  • field_cast_sql(db_type)—返回一个 SQL 片段,用于将指定的数据库列类型转换为某个值,该值可以更准确地与WHERE子句中的筛选器参数进行比较。返回值必须是 Python 格式的字符串,唯一的参数是要转换的字段的名称。默认返回值是"%s"
  • force_no_ordering()—返回可在ORDER BY子句中使用的名称列表,以从查询中删除所有排序。默认情况下,这将返回一个空列表。
  • for_update_sql(nowait=False)—返回一个 SQL 子句,该子句将在从数据库中选择数据时请求锁定。nowait参数指示是否包含必要的子句,以便在锁已经就位的情况下立即失效,而不是等待锁被释放。
  • fulltext_search_sql(field_name)—返回一个 SQL 片段,用于对指定字段进行全文搜索(如果支持的话)。返回的字符串还应该包含一个用于搜索用户指定值的%s占位符,该占位符将在该方法之外自动加引号。如果数据库不支持全文搜索,那么默认行为是通过引发一个带有适当消息的NotImplementedError来表明这一点。
  • last_executed_query(cursor, sql, params)—返回发送到数据库的最后一个查询,与发送时完全相同。默认情况下,这个方法必须通过用params提供的参数替换sql参数中的占位符来重新构建查询,这对于所有后端都是正确的,不需要任何额外的工作。一些后端可能有更快或更方便的快捷方式来检索最后一个查询,因此也提供了数据库光标,作为使用该快捷方式的一种方式。
  • last_insert_id(cursor, table_name, pk_name)—返回最后一个INSERT插入到数据库中的行的 ID。默认情况下,这只是返回cursor.lastrowid,正如 PEP-249 所指定的,但是其他后端可能有其他方法来检索这个值。为了帮助相应地访问它,该方法还接收插入行的表的名称和主键列的名称。
  • lookup_cast(lookup_type)—返回将值转换为可与指定的lookup_type一起使用的格式所需的 SQL。返回值还必须包含一个用于要转换的实际值的%s占位符,默认情况下,它只返回"%s"
  • max_in_list_size()—返回可在单个IN子句中使用的项目数。默认返回值None,表示这些项目的数量没有限制。
  • max_name_length()—返回数据库引擎允许用于表名和列名的最大字符数。默认情况下,这将返回None,表示没有限制。
  • no_limit_value()-返回应该用于指示无穷大极限的值,在指定没有极限的偏移时使用。一些数据库允许无限制地使用偏移量,在这些情况下,这个方法应该返回None。默认情况下,这会引发一个NotImplementedError,并且必须由后端实现,以允许无限制地使用偏移量。
  • pk_default_value()—返回发出INSERT语句时要使用的值,以指示主键字段应该使用其默认值—即递增序列—而不是某个指定的 ID;默认为"DEFAULT"
  • prep_for_like_query(x)—返回x的修改形式,适用于查询的WHERE子句中的LIKE比较。默认情况下,这会对x中的任何百分号(%)、下划线(_)或双反斜杠(\\)进行转义,并在适当的时候加上额外的反斜杠。
  • prep_for_ilike_query(x)—就像prep_for_like_query(),但是不区分大小写的比较。默认情况下,这是prep_for_like_query()的精确副本,但是如果数据库以不同的方式对待不区分大小写的比较,这可以被覆盖。
  • process_clob(value)—返回 CLOB 列引用的值,以防数据库需要一些额外的处理来生成实际值。默认情况下,它只返回提供的值。
  • query_class(DefaultQueryClass)—如果后端提供了一个自定义的Query类,如DatabaseWrapper.features.uses_custom_query_class所示,该方法必须根据提供的DefaultQueryClass返回一个自定义的Query类。如果uses_custom_query_classFalse,这个方法永远不会被调用,所以默认行为是简单地返回None
  • quote_name(name)—返回给定name的格式副本,带有适用于数据库引擎的引号。所提供的名称可能已经被引用过一次,因此该方法还应该注意检查这一点,在这种情况下不要添加额外的引号。因为在查询中引用名字没有既定的标准,所以这必须由后端实现,否则会引发一个NotImplementedError
  • random_function_sql()—返回生成随机值所需的 SQL 默认为"RANDOM()"
  • regex_lookup(lookup_type)—返回用于对列执行正则表达式匹配的 SQL。返回值应该包含两个%s占位符,第一个用于列名,另一个用于要匹配的值。查找类型可以是regexiregex,区别在于区分大小写。默认情况下,这会引发一个NotImplementedError,表明数据库后端不支持正则表达式。然而,对于简单的情况,可以使用下一节描述的DatabaseWrapper.operators字典来支持regexiregex
  • return_insert_id()—返回一个子句,该子句可在INSERT查询的末尾使用,以返回新插入记录的 ID。默认情况下,这只是返回None,不会向查询添加任何内容。
  • savepoint_create_sql(sid)—返回用于创建新保存点的 SQL 语句。sid参数是保存点的名称,以便以后引用。
  • savepoint_commit_sql(sid)—明确提交由sid参数引用的保存点。
  • savepoint_rollback_sql(sid)—根据sid参数引用的保存点回滚事务的一部分。
  • set_time_zone_sql()—返回可用于设置数据库连接时区的 SQL 模板。模板应该接受一个%s值,该值将被替换为要使用的时区。默认情况下,这会返回一个空字符串,表明数据库不支持时区。
  • sql_flush(style, tables, sequences)—返回从指定结构中删除所有数据所需的 SQL,同时保持结构本身不变。因为这在不同的数据库引擎之间是如此不同,所以默认行为会引发一个NotImplementedError,并且必须由后端实现。
  • sequence_reset_by_name_sql(style, sequences)—返回重置sequences列表中命名的自动递增序列所需的 SQL 语句列表。像autoinc_sql()drop_sequence_sql()一样,这只对维护自动 id 的独立序列的数据库有用,如果不需要,可以返回一个空列表,这是默认行为。
  • sequence_reset_sql(style, model_list)—像sequence_reset_by_name_sql(),—返回重置自动递增序列所需的 SQL 语句列表,但是指定的列表包含 Django 模型,而不是序列名称。这也与返回空列表的默认行为相同。
  • start_transaction_sql()—返回用于输入新事务的 SQL 默认为"BEGIN;"
  • sql_for_tablespace(tablespace, inline=False)—返回 SQL 来声明一个表空间,如果数据库不支持它们,则返回None,这是默认设置。
  • validate_autopk_value(value)—验证给定值是否适合用作数据库中的序列 ID。例如,如果数据库不允许零作为有效 id,那么该值应该引发一个ValueError。默认情况下,这只是返回一个值,表明它是有效的。
  • value_to_db_date(value)—将date对象转换为适用于数据库中DateField列的对象。
  • value_to_db_datetime(value)—将datetime对象转换为适用于DateTimeField列的值。
  • value_to_db_time(value)—将time对象转换为可用于数据库的TimeField列的值。
  • value_to_db_decimal(value)—将Decimal对象转换为数据库可以放入DecimalField列的值。
  • year_lookup_bounds(value)—返回表示给定年份的下限和上限的两项列表。value参数是一个int年份,每个返回值是一个表示完整日期和时间的字符串。第一个返回值是被视为所提供年份的一部分的最小日期和时间,而第二个返回值是被视为同一年的一部分的最大日期和时间。
  • year_lookup_bounds_for_date_feld(value)—还返回一个 2 项列表,表示作为value提供的年份的日期和时间的上限和下限。默认情况下,这取决于year_lookup_bounds(),但是如果数据库不能将完整的日期/时间值与DateField进行比较,这一点可以被覆盖。
比较运算符

许多可以在数据库中进行的比较遵循一种简单的格式,一个值后跟某种运算符,然后再跟另一个值进行比较。因为这是一种常见的情况,并且使用起来非常简单,Django 使用了一种更简单的方法来定义这些类型的比较操作符。

DatabaseWrapper对象的另一个属性operators包含一个字典,将各种查找类型映射到实现它们的数据库操作符。这非常依赖于基本结构,因为虽然这个字典的键是查找类型,但是值是应该放在被比较的字段名称之后的 SQL 片段。

例如,考虑由标准的=操作符处理"exact"查找的常见情况,这将由如下的字典处理:

class DatabaseWrapper(BaseDatabaseWrapper):

operators = {

"exact": "= %s"

}

然后用 Django 支持的其他操作符来填充这个字典。

获取光标

将所有这些特定于数据库的特性与 Django 的面向对象数据库 API 结合起来,可以提供一个可能性的世界,但它们都是为了涵盖最常见的情况而设计的。数据库支持各种各样的附加功能,这些功能要么不常用,要么在不同的实现中完全不同。Django 没有试图在所有数据库中支持所有这些特性,而是提供了直接访问数据库本身的便利。

DatabaseWrappercursor()方法直接从用于连接数据库本身的第三方库中返回一个数据库游标。为了与标准 Python 策略保持一致,这个 cursor 对象与 PEP-249 兼容,因此它甚至可以与其他数据库抽象库一起使用。因为这个对象的属性和方法的行为不在 Django 的控制之内——通常在不同的实现之间变化很大——所以最好查阅完整的 PEP 和您的数据库库文档,以获得关于如何使用它的详细信息。

创建新的结构

Django 的数据库连接提供的一个更方便的特性是能够完全基于 Python 中声明的模型定义自动创建表、列和索引。与强大的数据库查询 API 一起,这是避免在整个应用中使用 SQL 代码的一个关键特性,保持了它的整洁和可移植性。

虽然 SQL 语法本身在创建数据结构方面已经相当好地标准化了,但是各个字段类型可用的名称和选项在不同的实现中有很大的不同。这就是 Django 的数据库后端的用武之地,它提供了 Django 的基本字段类型到特定数据库的适当列类型的映射。

这个映射存储在后端包的creation模块中,该模块必须包含一个DatabaseCreation类,该类是django.db.backends.creation.BaseDatabaseCreation的子类。这个类包含一个名为data_types的属性,其中包含一个字典,该字典的键与来自各种Field子类的可用返回值以及将作为列定义传递给数据库的字符串值相匹配。

该值也可以是 Python 格式的字符串,它将被赋予一个字段属性字典,以便可以使用自定义的字段设置来确定如何创建列。例如,CharField就是这样传递max_length属性的。虽然许多字段类型有共同的属性,但对列类型最有用的属性可能是每个单独字段特有的。请查阅字段的源代码,以确定哪些属性可用于此映射。

有许多基本字段类型可用作内部列类型:

  • AutoField—自动递增的数值字段,用于模型中未明确定义的主键。
  • BooleanField—仅代表两个可能值的字段:开和关。如果数据库没有表示这种情况的单独的列,也可以使用单字符的CharField来存储"1""0"来模拟这种行为。
  • CharField—包含有限自由格式文本的字段。通常,这在数据库中使用可变长度的字符串类型,使用额外的max_length属性来定义存储值的最大长度。
  • CommaSeparatedIntegerField—包含整数列表的字段,通常表示 id,存储在由逗号分隔的单个字符串中。因为列表存储为字符串,所以在数据库端也使用可变长度的字符串类型。虽然一些数据库可能有更智能和有效的方法来存储这种类型的数据,但字段的代码仍然需要一个数字字符串,所以后端应该总是返回一个数字。
  • DateField—标准日期,没有任何相关的时间信息。大多数数据库应该有一个日期列类型,所以这应该很容易支持。只需确保所使用的列类型在检索时返回 Python datetime.date
  • DateTimeField—日期,但附有相关的时间信息,不包括时区。同样,大多数合理的数据库将很容易支持这一点,但要确保从数据库中检索时 Python 库返回一个datetime.datetime
  • DecimalField—固定精度的十进制数。这是使用字段属性定义数据库列的另一个例子,因为max_digitsdecimal_places字段属性应该控制数据库列的对等项。
  • FileField—存储在别处的文件的名称和位置。Django 不支持将文件作为二进制数据存储在数据库中,所以它的文件由相对路径和名称引用,存储在关联的列中。因为那是文本,所以它再次使用了标准的可变长度文本字段,该字段也利用了max_length字段属性。
  • FilePathField—存储系统中文件的名称和路径。该字段在许多方面与FileField相似,但这是为了允许用户从现有文件中选择,而FileField的存在是为了允许保存新文件。因为实际存储的数据本质上是相同的格式,所以它以相同的方式工作,使用由max_length属性指定的可变长度字符串。
  • FloatField—包含浮点数的字段。数据库是否在内部存储固定精度的数字并不重要,只要 Python 库为存储在列中的值返回一个float即可。
  • IntegerField—包含有符号 32 位整数的字段。
  • BigIntegerField—包含有符号 64 位整数的字段。
  • IPAddressField—互联网协议(IP)地址,使用当前的 IPv4 3 标准,用 Python 表示为字符串。
  • GenericIPAddressField—使用原始 IPv4 标准或较新的 IPv6 4 标准的 IP 地址。
  • NullBooleanField—一个布尔字段,也允许将NULL值存储在数据库中。
  • PositiveIntegerField—包含无符号 32 位整数的字段。
  • PositiveSmallIntegerField—包含无符号 8 位整数的字段。
  • SmallIntegerField—包含有符号 8 位整数的字段。
  • TextField—不限长度的文本字段,或者至少是数据库提供的最大文本字段。max_length属性对该字段的长度没有影响。
  • TimeField—表示一天中的时间的字段,没有任何相关的日期信息。数据库库应该为该列中的值返回一个datetime.time对象。

反思现有结构

除了能够基于模型信息创建新的表结构之外,还可以使用现有的表结构来生成新的模型。这不是一个完美的过程,因为有些模型信息没有存储在表自己的定义中,但是对于必须使用现有数据库的新项目来说,这是一个很好的起点,这些新项目通常与正在淘汰的遗留应用一起运行。

为此,后端应该提供一个名为introspection.py的模块,该模块为一个DatabaseIntrospection类提供了许多方法来检索关于表结构的各种细节。每个方法接收一个活动的数据库游标;下面的列表中记录了这些方法的所有参数和返回值,以及另一个用于根据基础列类型选择正确字段类型的映射。

  • get_table_list(cursor)—返回数据库中存在的表名列表。
  • get_table_description(cursor, table_name)—给定使用get_table_list()找到的特定表的名称,这将返回一个元组列表,每个元组描述表中的一列。对于光标的description属性:(name, type_code, display_size, internal_size, precision, scale, null_ok),每个元组都遵循 PEP-249 的标准。这里的type_code是数据库用来标识列类型的内部类型,这将被本节末尾描述的反向映射所使用。
  • get_relations(cursor, table_name)—给定一个表名,这将返回一个字典,详细说明该表与其他表的关系。每个键都是该列在所有列的列表中的索引,而关联的值是一个 2 元组。第一项是相关字段根据其表列的索引,第二项是相关表的名称。如果数据库没有提供一种简单的方法来访问这些信息,那么这个函数可以抛出NotImplementedError,关系将被从生成的模型中排除。
  • get_key_columns(cursor, table_name)—给定一个表名,这将返回与其他表相关的列的列表以及这些引用如何工作。列表中的每一项都是一个元组,由列名、它引用的表以及被引用的表中的列组成。
  • get_indexes(cursor, table_name)—给定表的名称,这将返回以任何方式索引的所有字段的字典。字典的键是列名,而值是附加字典。每个值的字典包含两个键:'primary_key''unique',每个键不是True就是False。如果两者都是False,则该列仍然由于在外部字典中而被指示为已索引;它只是一个普通的索引,没有主键或唯一约束。像get_relations()一样,如果没有简单的方法来获取这些信息,这也会引发NotImplementedError

除了前面的方法,自省类还提供了一个名为data_types_reverse的字典,它映射从get_table_description()返回的字典中的type_code值。关键字是作为type_code返回的任何值,不管它是字符串、整数还是其他什么。这些值是字符串,包含支持相关列类型的 Django 字段的名称。

数据库客户端

这个类位于数据库后端的client.py模块中,负责调用由DATABASE_ENGINE指定的当前数据库的命令行接口(shell)。这是使用manage.py dbshell命令调用的,允许用户在必要时手动管理底层表的结构和数据。

这个类只包含一个方法runshell(),它没有参数。然后,该方法负责读取给定后端的适当数据库设置,并配置对数据库 shell 程序的调用。

数据库错误和完整性错误

{{ backend }}.base拉进来的这些类允许异常被容易地处理,同时仍然能够交换出数据库。IntegrityError应该是DatabaseError的子类,所以如果错误的确切类型不重要,应用可以只检查DatabaseError

符合 PEP-249 的第三方库已经有这些可用的类,所以它们通常可以被分配到base模块的名称空间中,并正常工作。唯一需要对它们进行子类化或直接定义的时候是,如果所使用的库的行为方式与 Django 支持的其他数据库不同。请记住,这是关于整个框架的一致性。

证明

虽然用户名和密码的组合是一种非常常见的身份验证方法,但它远不是唯一可用的方法。其他方法,如 OpenID,使用完全不同的技术,甚至不包括用户名或密码。此外,一些使用用户名和密码的系统可能已经将这些信息存储在不同的数据库或结构中,而不是 Django 默认查看的数据库或结构中,因此仍然需要进行一些额外的处理,以根据正确的数据验证凭证。

为了解决这些情况,Django 的认证机制可以用定制代码来代替,支持任何需要使用的系统。事实上,多种身份验证方案可以一起使用,如果不能产生有效的用户帐户,每一种都可以退回到下一种。这完全由分配给AUTHENTICATION_BACKENDS设置的一组导入路径控制。它们会按照从第一个到最后一个的顺序被尝试,只有所有后端都返回None才会被认为认证失败。每个身份验证后端只是一个标准的 Python 类,它提供了两个特定的方法。

获取用户(用户标识)

任何时候预先知道用户的 ID,无论是从会话变量、数据库记录还是其他地方,认证后端负责将该 ID 转换成可用的django.contrib.auth.models.User实例。对于不同的后端来说,ID 的含义可能是不同的,所以这个参数的确切类型也可能根据所使用的后端而变化。对于 Django 附带的默认设置django.contrib.auth.backends.ModelBackend,这是存储用户信息的数据库 ID。对其他人来说,它可能是用户名、域名或其他任何东西。

认证(* *凭证)

当不知道用户的 ID 时,有必要要求一些凭证,使用这些凭证可以识别和检索适当的User帐户。在默认情况下,这些凭据是用户名和密码,但其他凭据可能使用 URL 或一次性令牌。在现实世界中,后端不会接受使用**语法的参数,而是只接受那些对它有意义的参数。然而,因为不同的后端将采用不同的凭证集,所以没有适合所有情况的单一方法定义。

PASSING INFORMATION TO CUSTOM BACKENDS

您可能已经从前面几节中注意到,传递到身份验证后端的数据在很大程度上取决于所使用的后端。默认情况下,Django 从其登录表单传入用户名和密码,但是其他表单可以提供适合该表单的任何其他凭证。

存储用户信息

身份验证的一个不明显的方面是,无论出于何种目的,所有用户都必须在 Django 中被表示为django.contrib.auth应用中的User对象。Django 并不严格要求将这作为一个框架,但是大多数应用——包括提供的管理界面——都希望用户存在于数据库中,并与该模型建立关系。

对于调用外部服务进行身份验证的后端,这意味着复制 Django 数据库中的每个用户,以确保应用正常工作。从表面上看,这听起来像是一场维护噩梦;不仅需要复制每个现有用户,还需要添加新用户,并且对用户信息的更改也应该反映在 Django 中。如果所有这些都必须由所有用户手工管理,这肯定会是一个相当大的问题。

但是请记住,认证后端的唯一真正要求是它接收用户的凭证并返回一个User对象。在这两者之间,都只是标准的 Python,Django 的整个模型 API 都是公开的。一旦用户在后台通过了身份验证,后端可以简单地创建一个新的User(如果还没有的话)。如果确实存在,它甚至可以用“真实”用户数据库中更新的任何新信息来更新现有记录。这样,一切都可以保持同步,而不必为 Django 做任何特别的事情。只需使用您已经在使用的任何系统来管理您的用户,让您的身份验证后端处理剩下的工作。

文件

Web 应用通常将大部分时间花在处理数据库中的信息上,但是有很多原因导致应用可能也需要直接处理文件。无论是用户上传头像或演示文稿、即时生成图像或其他静态内容,还是定期备份日志文件,文件都可能成为应用中非常重要的一部分。和许多其他东西一样,Django 既提供了一个处理文件的单一接口,也为额外的后端提供了一个 API 来提供额外的功能。

基本文件类

无论来源、目的或用途如何,Django 中的所有文件都表示为django.core.files.File的实例。这与 Python 自己的文件对象非常相似,但是做了一些添加和修改,以便用于 Web 和大型文件。File的子类可以改变幕后发生的事情,但是下面的 API 是所有文件类型的标准。以下属性适用于所有File对象:

  • File.closed—表示文件是否已关闭的布尔值。实例化时,所有的File对象都是打开的,可以立即访问它的内容。close()方法将此设置为True,在再次访问文件内容之前,必须使用open()重新打开文件。
  • File.DEFAULT_CHUNK_SIZE—通常是文件类的一个属性,而不是它的一个实例,它决定了chunks()方法应该使用多大的块。
  • File.mode—打开文件的访问模式;默认为'rb'
  • File.name—文件的名称,包括相对于其打开位置的任何给定路径。
  • File.size—文件内容的大小,以字节为单位。

以下方法也适用于File对象:

  • File.chunks(chunk_size=None)—遍历文件内容,生成一个或多个较小的块,以避免大文件填满服务器的可用内存。如果没有提供chunk_size,将使用默认为 64 KB 的DEFAULT_CHUNK_SIZE
  • File.close()—关闭文件,使其内容不可访问。
  • File.flush()—将任何新的挂起内容写入实际的文件系统。
  • File.multiple_chunks(chunk_size=None)—如果文件足够大,需要多次调用chunks()来检索全部内容,则返回True;如果可以一次读取全部内容,则返回Falsechunk_size的论证和chunks()中的一样。请注意,此时这实际上不会读取文件;它根据文件的size确定值。
  • File.open(mode=None)—如果文件先前已经关闭,则重新打开该文件。mode参数是可选的,默认为文件上次打开时使用的模式。
  • File.read(num_bytes=None)—从文件中检索一定数量的字节。如果在没有 size 参数的情况下调用,这将读取文件的剩余部分。
  • File.readlines()—以行列表的形式检索文件内容,如文件中出现的换行符(\r\n)所示。这些换行符留在列表中每一行的末尾。
  • File.seek(position)—将文件的内部位置移动到指定位置。所有的读写操作都是相对于这个位置的,所以这允许文件的不同部分被相同的代码访问。
  • File.tell()—返回内部指针的位置,以文件开头的字节数表示。
  • File.write(content)—将指定内容写入文件。这仅在文件以写模式打开时可用(该模式以'w'开始)。
  • File.xreadlines()readlines()的生成器版本,一次生成一行,包括换行符。为了与 Python 自身从xreadlines()的转变保持一致,该功能也是通过迭代File对象本身来提供的。

处理上传

当接受来自用户的文件时,事情变得有点棘手,因为在您的代码有机会检查它们之前,这些文件不应该与您的其他文件保存在一起。为了方便起见,Django 对待上传文件的方式有所不同,使用上传处理程序来决定应该用File的哪个子类来表示它们。每个上传处理器都有机会在上传过程中介入并改变 Django 的进程。

上传处理程序是用FILE_UPLOAD_HANDLERS设置指定的,它采用一系列导入路径。在处理上传的文件时,Django 依次调用每个处理程序的各种方法,这样它们就可以在数据进来时检查数据。不需要直接完成所有这些,因为它是由 Django 的请求处理代码自动处理的,但是新上传处理程序的 API 提供了足够的机会来定制如何管理传入文件。

  • FileUploadHandler.__init__(request)—每当有附加文件的请求传入时,处理程序被初始化,并且传入的请求被传入,以便处理程序可以决定它是否需要处理请求的文件。例如,如果它被设计为将上传的细节写入开发服务器的控制台,它可能会检查DEBUG设置是否是True以及request.META['REMOTE_ADDR']是否在INTERNAL_IPS设置中。如果处理程序应该总是处理每个请求,这不需要手动定义;继承的缺省值将满足大多数情况。
  • FileUploadHandler.new_file(field_name, file_name, content_type, content_length, charset=None)—这是为请求中提交的每个文件调用的,具有关于文件的各种细节,但没有其实际内容。field_name是用于上传文件的表单字段名称,而file_name是浏览器报告的文件本身的名称。content_typecontent_lengthcharset都是文件内容的属性,但是应该有所保留,因为不访问文件内容就无法验证它们。虽然没有严格要求,但是这个方法的主要功能是在调用received_data_chunk()时为文件内容留出一个存储位置。对于使用什么类型的存储,或者使用什么属性没有要求,所以几乎任何东西都是公平的。常见的例子是临时文件或StringIO对象。此外,该方法提供了一种方式来决定是否应该启用某些功能,例如由content_type确定的自动生成的图像缩略图。
  • FileUploadHandler.receive_data_chunk(raw_data, start)—这是仅有的两个必需方法之一,在文件的整个处理过程中被重复调用,每次接收文件内容的一部分作为raw_data,其中start是文件中找到该内容的偏移量。每次调用的数据量基于处理程序的chunk_size属性,默认为 64KiB。一旦这个方法完成了对数据块的处理,它还可以控制其他处理程序如何处理这些数据。这是由方法是否返回任何数据决定的,任何返回的数据都被传递给下一个处理程序。如果它返回None,Django 将简单地对下一个数据块重复这个过程。
  • FileUploadHandler.file_complete(file_size)—作为对new_file()的补充,当 Django 在请求中找到文件的结尾时,这个方法被调用。因为这也是唯一可以确定文件总大小的时候,Django 给每个处理程序一个机会来决定如何处理这些信息。这是上传处理程序中唯一需要的方法,如果文件是由这个处理程序处理的,它应该返回一个UploadedFile对象。返回的UploadedFile将被关联的表单用作用于上传文件的字段的内容。如果处理程序没有对文件做任何事情,不管出于什么原因,它都会返回None。但是,要小心这一点,因为至少有一个上传处理程序必须返回一个用于表单的UploadedFile
  • FileUploadHandler.upload_complete()—当每个文件加载完成时调用file_complete(),当所有上传的文件处理完毕后,每个请求调用一次upload_complete()。如果处理程序在处理所有文件时需要设置任何临时资源,这个方法是清理自身的地方,为应用的其余部分释放资源。

请注意,这些方法实现的许多特性依赖于一个方法知道前一个方法已经做出了什么决定,但是没有明显的方法来持久保存这些信息。由于处理程序是针对每个传入的请求和流程文件一次实例化一个,因此可以简单地在处理程序对象本身上设置自定义属性,将来的方法调用可以回读这些属性以确定如何继续。

例如,如果__init__()self.activated设置为Falsereceive_data_chunk()可以读取该属性,以确定它是否应该处理它接收到的块,或者只是将它们传递给队列中的下一个处理程序。new_file()也可以设置相同或相似的属性,因此这些类型的决定可以基于每个文件和每个请求做出。

因为每个处理程序都是独立工作的,所以对于使用哪些属性或它们的用途没有任何标准。相反,各种已安装的上传处理程序之间的交互是通过在各种情况下引发大量异常来处理的。上传处理程序的正确操作不需要使用其中的任何一个,但是它们可以很大程度上定制多个处理程序如何协同工作。和FileUploadHandler一样,这些在django.core.files.uploadhander都有。

  • StopUpload—告诉 Django 停止处理上传中的所有文件,防止所有处理程序处理比它们已经处理的更多的数据。它还接受一个可选参数connection_reset,一个布尔值,指示 Django 是否应该停止,而不读取输入流的剩余部分。这个参数的缺省值False意味着 Django 将在把控制传递回表单之前读取整个请求,而True将在没有全部读取的情况下停止,导致在用户浏览器中显示“连接重置”消息。
  • SkipFile—告知上传过程停止处理当前文件,但继续处理列表中的下一个文件。如果请求中的单个文件有问题,这是更合适的行为,这不会影响可能同时上传的任何其他文件。
  • StopFutureHandlers—只有从new_file()方法中抛出时才有效,这表示当前上传处理程序将直接处理当前文件,在此之后任何其他处理程序都不应接收任何数据。任何在引发该异常的处理程序之前处理数据的处理程序都将继续按照它们原来的顺序执行,这取决于它们在FILE_UPLOAD_HANDLERS设置中的位置。

存储文件

所有文件存储操作都由位于django.core.files.storageStorageBase实例处理,默认存储系统由DEFAULT_FILE_STORAGE设置中的导入路径指定。存储系统包含处理文件存储和检索的方式和位置的所有必要功能。通过使用这一额外的层,可以交换所使用的存储系统,而不必对现有代码进行任何更改。这在从开发转移到生产时尤其重要,因为生产服务器通常有存储和服务静态文件的特殊需求。

为了提高这种灵活性,Django 提供了一个 API 来处理文件,这个 API 超越了 Python 提供的标准open()函数和相关的file对象。在这一章的前面,Django 的File对象被描述,解释了处理单个文件的可用特性。但是,当希望存储、检索或列出文件时,存储系统有一套不同的工具可用。

  • Storage.delete(name)—从存储系统中删除文件。
  • Storage.exists(name)—返回一个布尔值,表明指定的名称是否引用了存储系统中已存在的文件。
  • Storage.get_valid_name(name)—返回适用于当前存储系统的给定名称的版本。如果它已经有效,它将被原封不动地返回。这是仅有的两种默认实现方法之一,它将返回适用于本地文件系统的文件名,与操作系统无关。
  • Storage.get_available_name(name)—给定一个有效的名称,这将返回它的一个版本,该版本实际上可用于写入新文件,而不会覆盖任何现有文件。作为另一个具有默认行为的方法,这将在所请求的名称后面添加下划线,直到找到一个可用的名称。
  • Storage.open(name, mode='rb', mixin=None)—返回一个打开的File对象,通过该对象可以访问文件的内容。mode接受与 Python 的open()函数相同的所有参数,允许读写访问。可选的mixin参数接受一个与存储系统提供的File子类一起使用的类,以支持返回文件的附加特性。
  • Storage.path(name)—返回文件在本地文件系统上的绝对路径,可以使用 Python 的内置open()函数直接访问文件。这是为了方便文件存储在本地文件系统的常见情况。对于其他存储系统,如果没有可以访问文件的有效文件系统路径,这将引发一个NotImplementedError。除非您使用的库只接受文件路径,而不接受打开的文件对象,否则您应该始终使用Storage.open()打开文件,它适用于所有存储系统。
  • Storage.save(name, content)—将给定的内容保存到存储系统,最好以给定的名称保存。这个名称在保存之前将通过get_valid_name()get_available_name(),这个方法的返回值将是实际用来存储内容的名称。提供给该方法的content参数应该是一个File对象,通常是文件上传的结果。
  • Storage.size(name)—返回由name引用的文件的大小,以字节为单位。
  • Storage.url(name)—返回一个绝对 URL,文件的内容可以通过 Web 浏览器直接访问。
  • listdir(path)—返回由path参数指定的目录的内容。返回值是一个包含两个列表的元组:第一个列表用于位于该路径的目录,第二个列表用于位于同一路径的文件。

默认情况下,Django 附带了FileSystemStorage,顾名思义,它将文件存储在本地文件系统中。通常这意味着服务器的硬盘驱动器,但是有很多方法可以将其他类型的文件系统映射到本地路径,所以已经有很多可能性了。然而,还有更多的存储选项可用,并且有很多方法可以定制现有选项的行为。通过对StorageBase进行子类化,可以提供许多其他选项。

存储系统必须提供许多东西,从这些方法中的大多数开始。其中一个方法get_available_name(),严格来说不需要由新的存储类提供,因为它的默认实现适用于许多情况;覆盖它是一个偏好问题,而不是需求问题。另一方面,get_valid_name()方法有一个适合大多数后端的默认行为,但是有些可能有不同的文件命名需求,需要一个新的方法来覆盖它。

另外两种方法open()save()还有进一步的要求。根据定义,这两者都需要对每个不同的存储系统进行特殊处理,但是在大多数情况下不应该直接覆盖它们。除了存储和检索文件所必需的逻辑之外,它们还提供了额外的逻辑,这种逻辑应该得到维护。相反,他们将与实际存储机制的交互分别委托给了_open()_save(),这两者有着更简单的期望。

  • Storage._open(name, mode='rb')namemode参数与open()相同,但它不再有mixin逻辑来处理,因此_open()可以专注于返回一个适合访问所请求文件的File对象。
  • Storage._save(name, content)—这里的参数和save()一样,但是这里提供的名字会已经经过get_valid_name()get_available_name(),内容保证是File实例。这使得_save()方法可以专注于将文件内容提交给具有给定名称的存储系统。

除了提供这些方法之外,大多数定制存储系统还需要提供一个带有read()write()方法的File子类,这些方法旨在以最有效的方式访问底层数据。chunks()方法在内部遵从read(),因此不需要做任何事情来使应用处理大文件时更加内存友好。记住,不是所有的文件系统都允许只读写文件的一部分,所以在这些情况下,File子类可能还需要采取额外的步骤来最小化内存使用和网络流量。

会话管理

当用户偶然浏览一个网站时,为他们临时跟踪一些信息通常是有用的,即使还没有与他们相关联的User账户。这可以从他们第一次访问网站的时间到购物车。在这些情况下,典型的解决方案是会话——由存储在浏览器端 cookie 中的键引用的服务器端数据存储。Django 内置了对会话的支持,并留有一点配置空间。

大部分会话过程都是恒定的:识别没有会话的用户,分配一个新的密钥,将该密钥存储在 cookie 中,稍后检索该密钥,并且一直像字典一样工作。对于键的名称和使用时间有一些基本的设置,但是为了跨多个页面视图持久保存任何信息,键被用来引用存储在服务器上某个地方的一些数据,这就是大部分定制的来源。

Django 使用SESSION_ENGINE设置来确定哪个数据存储类应该自己处理实际数据。Django 自带了三个数据存储,涵盖了文件、数据库记录和内存缓存等常用策略,但是在不同的环境中还有其他选项,甚至 stock 类也可能需要额外的定制。为了适应这一点,SESSION_ENGINE接受完整的导入路径,允许将会话数据存储放在任何 Django 应用中。这个导入路径指向一个包含名为SessionStore的类的模块,它提供了完整的数据存储实现。

像大多数 Django 的可切换后端一样,有一个基本实现提供了大部分特性,留给子类的细节很少。对于会话,基类是SessionBase,位于django.contrib.sessions.backends.base。它处理会话密钥生成、cookie 管理、字典访问,并仅在必要时访问数据存储。这使得自定义的SessionStore类只需要实现五个方法,它们组合起来完成整个过程。

  • SessionStore.exists(session_key)—如果提供的会话密钥已经存在于数据存储中,则返回True,或者如果它可用于新会话,则返回False
  • SessionStore.load()—从数据存储使用的任何存储机制加载会话数据,返回表示该数据的字典。如果不存在会话数据,这将返回一个空字典,一些后端可能需要在返回之前保存新字典。
  • SessionStore.save()—使用当前会话密钥作为标识符,将当前会话数据提交到数据存储。这还应该使用会话的到期日期或期限来确定会话何时失效。
  • SessionStore.delete(session_key)—从数据存储中删除与给定密钥相关联的会话数据。
  • SessionStore.create()—创建新会话并返回它,以便外部代码可以向它添加新值。该方法负责创建新的数据容器,生成唯一的会话密钥,将该密钥存储在会话对象中,并在返回之前将空容器提交给后端。

此外,为了帮助会话数据存储访问完成工作所需的信息,Django 还提供了一些由SessionBase管理的附加属性。

  • session_key—存储在客户端 cookie 中的随机生成的会话密钥。
  • _session—包含与当前会话密钥相关联的会话数据的字典。
  • get_expiry_date()—返回一个表示会话何时到期的datetime.datetime对象。
  • get_expiry_age()—返回会话到期前的秒数。

通过在SessionBase的子类上实现五个方法,几乎可以在任何地方存储会话数据。尽管这些数据没有绑定到一个User对象,但它仍然是特定于浏览网站的个人的。为了存储对每个人都有用的临时信息,还需要一些其他的东西。

贮藏

当应用有大量很少改变的信息要处理时,在服务器上缓存这些信息通常是有用的,这样就不必在每次访问时都生成这些信息。这可以节省服务器上的内存使用、每个请求的处理时间,并最终帮助应用在相同的时间内处理更多的请求。

有很多方法可以访问 Django 的缓存机制,这取决于需要缓存多少信息。在线文档 5 涵盖了关于如何设置站点范围的缓存和每个视图的缓存的许多一般情况,但是较低级别的细节需要更多的解释。

指定后端

在 Django 中指定缓存后端的工作方式与本章中讨论的其他后端有很大不同。尽管需要考虑多个配置选项,但只有一个设置可以控制所有选项。这个设置CACHE_BACKEND使用 URI 语法 6 以一种可以被可靠解析的方式接受所有必要的信息。它可以分成三个独立的部分,每个部分都有自己的要求。

CACHE_BACKEND = '{{ scheme }}://{{ host }}/?{{ arguments }}'

  • scheme 部分指定应该使用哪个后端代码来提供缓存。Django 提供了四个后端,涵盖了大多数情况——dbfilelocmemmemcached7 、——这些都在网上有很好的文档记录,涵盖了大多数情况。对于自定义后端,这部分设置还可以接受一个模块的完整导入路径,该模块实现下一节中描述的协议。
  • 主机指定缓存实际应该存储在哪里,其格式将根据所使用的后端而有所不同。例如,db期望一个单一的数据库名称,file期望一个完整的目录路径,memcached期望一个服务器地址列表,而locmem根本不需要任何东西。宿主还可以通过尾部斜杠包含,这有助于可读性,因为它使整个设置看起来更像 URI。
  • 参数是可选的,可以用来定制在后端如何进行缓存。它们是使用查询字符串格式提供的,所有后端都需要一个参数:timeout,即项目从缓存中移除之前的秒数。对于大多数后端(包括 Django 提供的除了memcached之外的所有后端),还有两个参数可用:max_entries,剔除旧项目之前应该存储在缓存中的项目总数;以及cull_frequency,它控制当到达max_entries时从缓存中清除多少项。
  • 关于cull_frequency要意识到的一件重要的事情是,它的值实际上并不是项目应该被移除的频率。相反,该值用于一个简单的公式1 / cull_frequency,该公式确定有多少项受到影响。因此,如果你想一次清除 25%的条目,这相当于 1/4,所以你可以将cull_frequency=4作为参数传递给缓存后端,而一半(1/2)的条目需要传递cull_frequency=2。从本质上来说,cull_frequency是缓存必须被剔除的次数,以确保所有项目都被清除。

手动使用缓存

除了标准的站点范围和每个视图的缓存选项之外,直接使用缓存也很简单,存储特定的值,以便以后可以检索它们,而不必对不经常更改的数据执行昂贵的操作。这个低级 API 可以通过位于django.core.cachecache对象以通用形式获得。这个对象的大部分有用性来自三种方法— get()set()delete()—它们的工作方式与您的预期大致相同。

>>> cache.set('result', 2 ** 16 – 64 * 4)

>>> print cache.get('result')

65280

>>> cache.delete('result')

>>> print cache.get('result')

None

有一些关于这些方法的细节需要更多的解释,还有一些额外的方法被证明是有用的。下面是可用方法的完整列表,以及它们的功能细节。

  • CacheClass.set(key, value, timeout=None)—使用提供的key在缓存中设置指定的value。默认情况下,值从缓存中过期的超时时间由传递到CACHE_BACKEND设置中的超时时间决定,但是可以通过指定一个不同的超时时间作为该方法的参数来覆盖。
  • CacheClass.get(key, default=None)—该方法返回指定key的缓存中包含的值。通常,如果缓存中不存在关键字,cache.get()会返回None,但有时None是缓存中的有效值。在这些情况下,只需将default设置为某个不应该存在于缓存中的值,该值将被返回而不是None
  • CacheClass.delete(key)—删除与给定键相关的值。
  • CacheClass.get_many(keys)—给定一个键列表,它返回一个相应的键值列表。对于一些后端,像memcached,这可以提供一个比每个单独的键调用cache.get()更快的速度。
  • CacheClass.has_key(key)—如果指定的键在缓存中已经有值,则该方法返回True,如果该键未设置或已经过期,则返回False
  • CacheClass.add(key, value, timeout=None)—此方法仅尝试使用指定的值和超时时间向缓存添加新的键。如果给定的键已经存在于缓存中,此方法不会将缓存更新为新值。

使用缓存时,一个常见的习惯用法是首先检查缓存中是否已经存在某个值,如果不存在,则计算该值并将其存储在缓存中。然后,可以从缓存中检索该值,而不管它是否在缓存中,从而使代码变得简单明了。为了使这一点更加 Pythonic 化,cache对象的功能也有点像字典,支持in操作符作为has_key()方法的别名。

def get_complex_data(complex_data):

if 'complex-data-key' not in cache:

# Perform complex operations to generate the data here.

cache.set('complex-data-key', complex_data)

return cache.get('complex-data-key')

模板加载

虽然第六章展示了当一个视图或其他代码请求一个模板呈现时,它只是传入一个名称和一个相对路径,实际的模板检索是由特殊的加载器完成的,每个加载器以不同的方式访问模板。通过向TEMPLATE_LOADERS设置提供一个或多个导入路径,Django 不需要预先知道如何存储模板或者将模板存储在哪里。

Django 附带了三个模板加载器,代表了模板最常见的使用方式,在某些配置中从文件系统加载文件。当这些选项不够用时,添加您自己的模板加载器来以最适合您的环境的方式定位和检索模板是相当简单的。

这实际上是最容易编写的可插拔接口之一,因为它实际上只是一个函数。甚至没有任何关于函数应该调用什么的假设,更不用说它应该在哪个模块中,或者它应该属于哪个类了。TEMPLATE_LOADERS中的条目直接指向函数本身,因此不需要其他结构。

load_template_source(模板名,模板目录=无)

虽然加载器可以被称为任何东西,但是 Django 为它所有的模板加载器使用的名称是load_template_source,所以为了便于理解,通常最好坚持这个约定。这通常也放在它自己的模块中,但是同样,必须显式地提供导入路径,所以只要确保它的位置被很好地记录就行了。

第一个参数显然是要加载的模板的名称,它通常只是一个标准的文件名。这不必映射到一个实际的文件,但是视图通常会使用文件名请求模板,所以由模板加载器将这个名称转换成模板使用的任何引用。这可能是数据库记录、指向外部存储系统的 URL,或者您的站点可能用来存储和加载模板的任何东西。

load_template_source()的第二个参数是搜索模板时使用的目录列表。在 Django 内部,这通常是不提供的,所以使用默认的None,表明应该使用TEMPLATE_DIRS设置。使用文件系统的加载器应该始终遵循这种行为,以保持与其他模板加载器工作方式的一致性。如果加载器从其他地方获取模板,这个参数可以被忽略。

模板加载器内部发生的事情在不同的模板加载器之间会有很大的不同,这取决于每个加载器如何定位模板。一旦找到模板,加载器必须返回一个包含两个值的元组:一个字符串形式的模板内容,一个字符串表示找到模板的位置。第二个值用于为新的Template对象生成origin参数,这样如果出现问题,就很容易找到模板。

如果给定的名称与加载程序所知道的任何模板都不匹配,它应该引发TemplateDoesNotExist异常,如第六章中的所述。这将指示 Django 移动到列表中的下一个模板加载器,或者如果没有更多的加载器可以使用,就显示一个错误。

load_template_source

如果 Python 环境没有模板加载器运行的要求,Django 还为加载器提供了一种方式来表明它不应该被使用。如果模板加载程序依赖于尚未安装的第三方库,这将非常有用。给函数添加一个is_usable属性,设置为TrueFalse,将告诉 Django 是否可以使用模板加载器。

load_template(模板名,模板目录=无)

除了简单地加载模板的源代码之外,这个方法还负责返回一个能够被呈现的模板。默认情况下,从load_template_source()返回的源代码是由 Django 自己的模板语言处理的,但是这给了你一个机会用其他东西完全替换它。这仍然应该在内部使用load_template_source()方法来获取模板的代码,这样用户就可以将在哪里找到模板的决定与应该如何解释这些模板的决定分开。

返回值只需要一个方法就能正常工作:render(context)。这个render()方法接受一个模板上下文,并返回一个由模板源代码生成的字符串。这里传入的上下文很像一个标准字典,但是 Django 的上下文实际上是一堆字典,所以如果您打算将这个上下文传入另一个呈现的模板,您可能需要先将其展平为一个字典。

flat_dict = {}

for d in context.dicts:

flat_dict.update(d)

之后,您将拥有一个包含所有值的字典,这通常适用于大多数模板语言。

上下文处理器

当一个模板被渲染时,它会被传递一个变量上下文,用来显示信息和做出基本的表示决策。如果使用一种特殊类型的上下文RequestContext,它可以从django.template和标准的Context一起获得,Django 运行一个上下文处理器列表,每个处理器都有机会向模板的上下文添加新的变量。这不仅是向站点上使用的每个模板添加公共变量的好方法,而且是基于来自传入的HttpRequest对象的信息提供信息的非常简单的方法。

上下文处理器的接口非常简单;它只不过是一个标准的 Python 函数,将一个请求作为唯一的参数,并返回一个要添加到模板上下文中的数据字典。它不应该引发异常,如果不需要添加新的变量,根据指定的请求,它应该只返回一个空字典。这里有一个示例上下文处理器来添加一个包含请求用户 IP 地址的ip_address变量。

def remote_addr(request):

return {'ip_address': request.META['REMOTE_ADDR']}

Note

在代理和负载平衡器之后并不可靠,因为它的值将是代理的值,而不是真正的远程 IP 地址。如果您正在使用这些类型的软件,请确保使用适合您的环境的值。

安装上下文处理器就像在CONTEXT_PROCESSORS设置列表中添加一个字符串一样简单,每个条目都是一个完整的 Python 导入路径,包括它末尾的函数名。另外,记住上下文处理器只在使用RequestContext渲染模板时被调用。因为上下文处理器接受传入的请求作为参数,所以没有这个信息就无法调用它们。

应用技术

本章中描述的工具有许多不同的可用用途,但是有几个简单的例子可以说明它们如何很好地用于一些常见的需求。用一撮盐和一根欧芹把它们做成你自己的。如果事先不了解应用的工作环境,任何可以给出的例子,从定义上来说,都是相当抽象的,但是它们应该是如何很好地使用这些技术的一个很好的概述。

扫描传入文件中的病毒

对于允许用户上传文件以分发给其他用户的网站,人们非常信任这些传入文件的质量。与任何形式的用户输入一样,这种信息中肯定存在一定程度的不信任,因为总有人想对你的网站及其用户造成伤害。

当希望让用户共享特定类型的文件时,使用旨在理解这些文件的第三方库进行验证通常很容易。另一方面,共享任意文件打开了一个充满其他可能性的世界,其中许多会将您的站点及其用户置于风险之中。防病毒是这种应用安全性的重要组成部分,Django 的上传处理程序使这成为一项极其简单的任务。

对于这个例子,我们将使用一个优秀的开源病毒扫描应用,ClamAV, 8 ,它是为在服务器中使用而设计的,以及 pyclamd, 9 一个用于与 ClamAV 交互的 Python 库。总之,它们提供了一个易于使用的接口,可以在任何传入的文件被传递给应用的其他部分之前对其进行扫描。如果发现了病毒,可以在病毒对任何人造成伤害之前,立即将恶意文件从输入流中删除。

import pyclamd

from django.core.files import uploadhandler

from django.conf import settings

# Set up pyclamd to access running instance of clamavd, according to settings

host = getattr(settings, 'CLAMAV_HOST', 'localhost')

port = getattr(settings, 'CLAMAV_PORT', 3310)

pyclamd.init_network_socket(host, port)

class VirusScan(uploadhandler.FileUploadHandler):

def receive_data_chunk(self, raw_data, start): try:

if pyclamd.scan_stream(raw_data):

# A virus was found, so the file should

# be removed from the input stream.

raise uploadhandler.SkipFile()

except pyclamd.ScanError:

# Clam AV couldn't be contacted, so the file wasn't scanned.

# Since we can't guarantee the safety of any files

# no other files should be processed either.

raise uploadhander.StopUpload()

# If everything went fine, pass the data along

return raw_data

def file_complete(self, file_size):

# This doesn't store the file anywhere, so it should

# rely on other handlers to provide a File instance.

return None

您的应用可能有更具体的要求,比如向用户解释发现了哪种病毒,以及他们应该考虑在尝试与他人共享文件之前清理自己的系统。这个例子的关键是实现这种类型的行为有多容易,表面上看起来可能非常困难。

现在怎么办?

尽管有很多关于访问这些不同类型的后端协议的知识需要学习,但是很好地使用它们需要大量的想象力。关于如何以及为什么访问或替换这些低级接口,这样的书只能说这么多,所以由您来决定什么最适合您的环境和应用。

虽然本章讨论了如何使用和检修 Django 基础设施的主要部分,但有时只需要一个简单的实用程序来替换或避免大量冗余代码。了解两者的区别很重要,下一章将概述 Django 核心发行版中提供的许多基本实用程序。

Footnotes 1

http://prodjango.com/pep-249/

  2

http://prodjango.com/db2/

  3

http://prodjango.com/ipv4/

  4

http://prodjango.com/ipv6/

  5

http://prodjango.com/caching/

  6

http://prodjango.com/uri/

  7

http://prodjango.com/memcached/

  8

http://prodjango.com/clamav/

  9

http://prodjango.com/pyclamd/