Django-高级教程-二-

96 阅读56分钟

Django 高级教程(二)

原文:Pro Django

协议:CC BY-NC-SA 4.0

四、URL 和视图

Abstract

这本书的大部分内容被分成了相当独立的章节,但这本书涵盖了两个看似不相关的概念,因为它们彼此非常依赖。URL 是站点的主要入口点,而视图是响应输入事件的代码。视图中发生的事情是非常开放的。除了接受请求和返回响应之外,没有视图应该遵守的特定协议,也没有关于它们允许或不允许做什么的规则。

这本书的大部分内容被分成了相当独立的章节,但这本书涵盖了两个看似不相关的概念,因为它们彼此非常依赖。URL 是站点的主要入口点,而视图是响应输入事件的代码。视图中发生的事情是非常开放的。除了接受请求和返回响应之外,没有视图应该遵守的特定协议,也没有关于它们允许或不允许做什么的规则。

视图的可能性太大了,以至于无法详细描述,而且也没有专门为视图在执行时使用而设计的实用程序。相反,可以挂钩到 Django 用来将 Web 地址映射到它们应该执行的视图的过程。这使得 URL 和视图之间的链接变得极其重要,对它的透彻理解可以实现更高级的技术。

此外,就 Django 如何管理传入请求而言,URL 配置的存在只是为了将请求分派给能够处理它的视图。独立于视图讨论 URL 和 URL 配置没有什么价值。

资源定位符

由于对 Web 服务器的所有传入请求都源于 Web 浏览器对 URL 的访问,因此讨论 URL 是一个重要的起点。浏览器将 URL 转换成发送到 Web 服务器的消息的过程超出了本章的范围,但是第七章提供了更多信息。

一个常见的混淆点是网址应该被称为统一资源标识符(URI)还是统一资源定位器(URL)。许多人交替使用这两个术语,不管他们是否知道区别。简而言之,URI 是一个完整的寻址机制,包括两条信息。

  • 用于连接到资源的方案或协议的名称。这后面总是跟着一个冒号。
  • 可以找到资源的路径。对于不同的方案,此路径的确切格式可能不同,因此并非所有 URI 路径看起来都一样。

另一方面,URL 是一小组连接方案中的地址,它们的路径部分都符合一种格式。该协议集中包括 HTTP、HTTPS 和 FTP 等常见协议,本质上是当今网络上常见的协议。这些协议共享的路径格式如下。

  • 用于访问资源的协议,例如标准 HTTP 的http://。这是对 URI 的 scheme 部分的一点扩展,因为它假设所有的 URL 协议都会在冒号后包含两个正斜杠。
  • 资源所在的主机域,如 prodjango.comwww.prodjango.com
  • 或者,服务器响应的端口号。每个协议都有一个默认端口,如果没有提供,将会使用该端口。对于标准 HTTP,这是80,而对于使用安全套接字层(SSL)的加密 HTTP,这将是443
  • 资源在服务器上的路径,如/ 第四章 /

因此,虽然所有的网址肯定是 URIs,但不是所有的 URIs 网址。当在网上工作时,这种微妙的区别可能会令人困惑,因为这两个词都可以用来描述随处可见的地址。由于 Django 是为网络而构建的——也就是 URL 方案下的地址——本书的其余部分将把这些地址称为 URL,因为 URIs 的全部范围可能不适合 Django 的调度机制。

DESIGNING CLEAN URLS

在理想情况下,你第一次建立网站时选择的 URL 永远不会改变, 1 保持不变,直到文档或者整个服务器不再可维护。仅仅因为网站的重新设计或重组而改变 URL 通常是不好的形式,应该避免。

让 URL 长期可维护并让用户更容易跟踪它们的关键是首先要把它们设计好。Django 让这变得很容易,允许你以任何你喜欢的层次结构设计你的 URL,在 URL 中分配变量,并把 URL 结构分成可管理的块。

最重要的是,URL 是应用用户界面的一部分,因为用户必须看到它们,阅读它们,并且经常手动输入它们。在设计你的 URL 时,请记住这一点。

标准 URL 配置

Django 没有提供任何为任何站点自动发现或生成 URL 结构的特性。相反,每个站点和应用都应该使用 URL 配置明确声明最合适的寻址方案。这不是一个限制——这是一个允许你以自己喜欢的方式定义站点地址的特性。毕竟,网站就像房地产一样;你的网络框架不应该决定你的平面图。

定义一个 URL 配置看起来很简单,但是有一点需要特别注意,特别是因为 Django 自己的工具并不是定义这个配置的唯一方法。这个实现位于django.conf.urls.defaults中,提供了两个协同工作来管理 URL 配置的函数。

patterns()函数

URL 配置由一个模式列表组成,每个模式将特定类型的 URL 映射到一个视图。这些模式每个都有一些组件,但是它们都被一起指定为patterns()函数的参数。

from django.conf.urls.defaults import *

urlpatterns = patterns(''

(r'^$', 'post_list')

(r'^(?P<id>\d+)/$', 'post_detail')

(r'^(?P<id>\d+)/comment/$', 'post_comment', {'template': 'comment_form.html'})

)

该函数的参数可以分为两组:

  • 指定为字符串的任何视图的单个导入路径前缀
  • 任意数量的 URL 模式

从历史上看,所有视图都被指定为字符串,因此前缀是减少从单个应用将 URL 映射到视图所需的重复量的好方法。最近,URL 模式被允许将视图指定为可调用的,在这种情况下,前缀将被忽略。使用前缀将视图指定为字符串仍然很有用,因为它不需要视图的一组导入,从而减少了整体代码。

传统上,url 模式是以元组的形式传入的,尽管本章后面的“URL()函数”一节描述了一个更新的内容。该元组的每个部分的细节如下:

  • 用于匹配 URL 的正则表达式
  • 为匹配此模式的请求调用的视图函数
  • 或者,要传递给函数的参数字典

这个元组包含将传入请求映射到视图函数所需的所有信息。将根据正则表达式检查 URL 的路径,如果发现匹配,请求将被传递给指定的视图。正则表达式捕获的任何参数都与额外字典中的显式参数相结合,然后与请求对象一起传递给视图。

Note

像大多数正则表达式一样,URL 模式通常使用原始字符串来描述,由前缀r表示。原始字符串不经过标准转义,这在这里很有用,因为正则表达式提供了自己的转义形式。如果我们不使用原始字符串,我们必须对每个反斜杠进行转义,以便将其传递给正则表达式。这里的例子可以写成没有原始字符串的'^(?P<id>\\d+)/$'

MULTIPLE ARGUMENTS WITH THE SAME NAME

单个 URL 配置可以以两种不同的方式提供值:在 URL 的正则表达式中和在附加到模式的字典中。接受来自两个不同来源的参数使得为同一个键提供两个不同的值成为可能,这需要以某种方式解决。如果你尝试对一个标准函数的关键字参数这样做,Python 会抛出一个TypeError,如第二章所述。

Django 允许指定多个参数而不会引发异常,但是它们不能一起传递给视图。正如本章的第二部分所示,视图的调用就像任何普通的 Python 函数一样,所以这些多个参数会导致与第二章中的相同的TypeError。为了正确地解决这个问题,Django 必须可靠地选择一个而不是另一个。URL 配置中字典提供的任何参数都将优先于 URL 中找到的任何内容。

以这种方式提供同名的多个参数是不好的,因为它严重依赖 Django 对情况的处理才能正常工作。虽然这种行为不太可能因一时兴起而改变,但依赖它可能会在未来引发问题。更重要的是,在多个地方指定相同的参数名称会大大降低 URL 配置的可读性。即使在闭源应用中,在你完成代码后很久,其他人也可能需要阅读你的代码。

url()函数

为了提供更好的长期灵活性,URL 模式元组已经被弃用,取而代之的是url()实用函数。url()采用传入元组的相同参数,但也可以采用额外的关键字参数来指定所描述的 URL 模式的名称。

这样,一个站点可以多次使用同一个视图,但仍然可以通过反向 URL 查找被引用。在这一节的后面可以找到更多的信息。

包含( )函数

与在一个文件中提供所有的 URL 模式不同,include()函数允许它们被分割到多个文件中。它只有一个参数:一个可以找到另一个 URL 配置模块的导入路径。这不仅允许将 URL 配置拆分到多个文件中,还允许将正则表达式用作包含的 URL 模式的前缀。

使用include()时要记住的一件重要事情是不要在正则表达式中指定字符串的结尾。表达式不应以美元符号($)结尾。美元符号($)使得表达式只匹配完整的 URL。这不会留下任何额外的 URL 片段传递给包含的配置。这意味着额外的 URL 模式只有在专门检查空字符串时才会匹配。

将 URL 解析到视图

视图很少被您自己的代码直接调用,而是被 Django 的 URL 调度机制调用。这允许视图从触发它们的特定 URL 中分离出来,并且对于大多数项目来说,这两个方面如何联系的细节可以安全地忽略。但是因为视图并不总是简单的函数,所以了解 Django 如何从 URL 到视图是很重要的,这样才能确定视图真正能够做什么。

将 URL 映射到视图是一个简单的、有良好文档记录的过程,但是值得在这里介绍一些基础知识以供参考。典型的 URL 模式由几个不同的项目组成:

  • 与请求的传入 URL 匹配的正则表达式
  • 对要调用的视图的引用
  • 每次访问视图时传递的参数字典
  • 反向查找期间用于引用视图的名称

由于 URL 模式是用正则表达式表示的,正则表达式可以捕获字符串的某些部分以备后用,Django 将此作为从 URL 中提取参数的自然方式,以便将它们传递给视图。有两种方法可以指定这些组,这决定了它们的捕获值如何传递到视图中。

如果指定的组没有名称,它们将被放入一个元组中,作为多余的位置参数传递。这种方法使正则表达式变得更小,但是它有一些缺点。这不仅降低了正则表达式的可读性,还意味着视图中参数的顺序必须始终与 URL 中组的顺序相匹配,因为 Django 将它们作为位置参数发送。这比通常更好地将 URL 耦合到视图;在某些情况下,例如本章后面描述的基于对象的视图,它仍然非常有用。

如果组被给定了名称,Django 将创建一个字典,将这些名称映射到从 URL 中提取的值。这种替代方法通过将捕获的值作为关键字参数传递给视图,有助于鼓励 URL 和视图之间的松散耦合。注意,Django 不允许在同一模式中同时使用命名组和未命名组。

将视图解析为 URL

正如上一节提到的,Django 提供了另一个 URL 解析过程,如果应用得当,这个过程会更有用。应用通常需要提供链接或重定向到应用的其他部分或网站上的其他地方,但直接硬编码这些链接通常不是一个好主意。毕竟,即使是专有应用也可以改变它们的 URL 结构,而分布式应用可能根本不知道 URL 结构是什么样子。

在这些情况下,将 URL 放在代码之外很重要。Django 提供了三种不同的方法来指定一个位置,而不需要事先知道它的 URL。从本质上来说,它们都以相同的方式工作,因为它们都使用相同的内部机制,但是每个接口都适合特定的用途。

permalink 装饰公司

代码引用 URL 最明显的地方之一是在大多数模型的get_absolute_url()方法中。提供此方法是一种常见的约定,因此模板可以轻松地提供到对象详细信息页面的直接链接,而不必知道或关心使用什么 URL 或视图来显示该页面。它不带任何参数,返回一个包含要使用的 URL 的字符串。

为了适应这种情况,Django 提供了一个装饰器,位于django.db.models.permalink,它允许函数返回一组描述要调用的视图的值,将它转换成调用视图的 URL。这些值作为函数(如get_absolute_url()方法)的返回值提供,并遵循特定的结构——一个最多包含三个值的元组。

  • 第一个值是要调用的视图的名称。如果视图已命名,则此处应该使用该名称。否则,应该使用视图的导入路径。这是必须的。
  • 第二个值是应该应用于视图的一组位置参数。如果没有任何参数要应用于视图,则不需要提供该值,但是如果需要关键字,这应该是一个空元组。
  • 这个元组中的第三个值是一个将关键字参数映射到它们的值的字典,所有这些值都将被传递给指定的视图。如果不需要关键字参数,这个值可以不包含在元组中。

给定以下 URL 配置:

from django.conf.urls.defaults import *

from django.views.generic.detail import DetailView

from library.import models

class LibraryDetail(DetailView):

queryset = models.Article.objects.all()

urlpatterns = patterns('django.views.generic'

url(r'^articles/(?P<object_id>\d+)/$', LibraryDetail.as_view()

name='library_article_detail')

)

相应的模型(位于library应用中)可能如下所示:

from django.db import models

class Article(models.Model):

title = models.CharField(max_length=255)

slug = models.SlugField()

pub_date = models.DateTimeField()

def get_absolute_url(self):

return ('library_article_detail'

(), {'object_id': self.id})

get_absolute_url = models.permalink(get_absolute_url)

url 模板标记

另一个常见的需求是让模板提供到不是基于模型的视图的链接,但是不应该有硬编码的 URL。例如,一个联系表单的链接不一定与数据库或任何模型有任何联系,但是仍然需要以一种能够适应未来变化或分布的方式进行链接。

这个模板的语法看起来非常类似于permalink decorator,因为它将值传递给同一个实用函数。有一些细微的区别,因为作为一个模板标签,它不使用真正的 Python 代码。

{% url library_article_detail object_id=article.id %}

reverse()实用函数

Django 还提供了一个 Python 函数,该函数提供了从视图描述及其参数到触发指定视图的 URL 的转换。生活在django.core.urlresolversreverse()函数正是这样做的。它采用前面两种技术描述的所有相同的参数,但也有一个参数,允许它指定应该使用哪个 URL 配置模块来解析 URL。这个函数由permalink装饰器和url模板标签在内部使用。reverse()函数最多接受四个参数。

  • viewname—要调用的视图的名称或导入路径(如果未指定名称)。这是必须的。
  • urlconf—用于查找的 URL 配置模块的导入路径。这是可选的,如果它不存在或None,该值取自ROOT_URLCONF设置。
  • args—将传递给视图的任何位置参数的元组。
  • kwargs—将传递给视图的任何关键字参数的字典。

使用与上一节相同的例子,下面是如何使用reverse()来获取特定对象的 URL。

>>> from django.core.urlresolvers import reverse

>>> reverse('library_article_detail', kwargs={'object_id': 1})

'/articles/1/'

请记住,argskwargs是分开的、不同的参数。reverse()效用函数不使用第二章中描述的任何形式的参数展开。

POSITIONAL VS. KEYWORD ARGUMENTS

为了说明最佳实践,本节中的示例都在 URL 的正则表达式中使用命名组,这允许——实际上要求——使用关键字指定参数的反向解析。这极大地提高了代码的可读性和可维护性,这是编写 Python 的主要目标。不过,也可以指定 URL 而不命名捕获组,这需要反向解析来仅使用位置参数。

例如,如果 URL 模式被定义为r'^articles/(d+)/$',为了正常工作,前面的例子必须这样编写:

  • permalink装饰者— return ('library_article_detail', (self.id,), {})
  • url模板标签— {%url library_article_detail article.id %}
  • reverse()功能— reverse('library_article_detail', args=(1,))

因为 URL 配置只允许位置参数或关键字参数,而不是两者都允许,所以没有必要在同一个反向解析调用中同时指定这两种类型。

基于功能的视图

来自其他环境的程序员感到困惑的一点是,Django 使用的术语“视图”与其他人有点不同。传统上,模型-视图-控制器(MVC)体系结构中的视图指的是向用户显示信息——本质上是用户界面的输出部分。

网络不是这样的。查看数据通常是用户操作的直接结果,对该视图的更新只是对后续操作的响应。这意味着输出过程不可避免地与用户输入过程联系在一起,这可能会导致一些困惑,甚至是传统的 MVC 模式应该如何定义视图。

因此,对于 Django 的观点与其他环境相比如何的问题,没有简单的答案,因为没有任何可靠的东西可以比较。不同背景的人可能对视图有不同的期望。坏消息是 Django 可能和他们中的任何一个都不一致。好消息是,一旦您开始使用 Django,视图的概念就有了明确的定义,所以在与其他 Django 开发人员交流时不会有什么困惑。

模板稍微打破了它

Django 的视图执行输出接口的基本功能,因为它们负责发送给浏览器的响应。从严格意义上来说,这个响应就是整个输出,它包含了用户将会看到的所有信息。在保持可读性的同时,这在 Python 中要做的工作通常太多了,所以大多数视图依赖模板来生成大部分内容。

最常见的做法是让每个视图调用一个单独的模板,这可以利用许多工具来最小化为特定视图使用而必须编写的模板代码的数量。第六章包括了关于模板语言和可用工具的更多细节,但是对于这一节来说,重要的是要知道模板是一种从整体上简化编码过程的好方法。它们有助于减少必须编写的代码量,同时使代码在将来更具可读性和可维护性。

虽然第一章将模板列为一个单独的层,但是请记住,它们实际上只是 Django 提供给应用其他部分的一个工具,包括视图。最终,无论是否使用模板来生成内容,视图单独负责生成最终的响应。Django 的模板系统没有请求或响应的概念;它只是生成文本。剩下的就由视图来处理了。

视图的剖析

视图是一个接受 HTTP 请求并返回 HTTP 响应的函数。考虑到视图的潜在力量,这有点过于简单了,但这确实就是全部了。一个视图总是接收 Django 创建的HttpRequest作为它的第一个参数,并且它应该总是返回一个HttpResponse,除非出错。关于这些物体的全部细节,它们的用途和属性在第七章中有所介绍。

该定义的第一个方面是视图必须是标准函数的概念。这个定义有点灵活,因为在现实中,任何 Python 可调用对象都可以用作视图;只是基本功能很容易使用,并且提供了大多数情况下所需的一切。方法——包括类和实例上的方法——和可调用对象,使用第二章中描述的协议,都完全可以用作视图。这打开了各种其他的可能性,其中一些将在本章后面描述。

下一点是在视图方面不可改变的。每当调用一个视图时,不管传递什么其他参数,第一个参数总是一个HttpRequest对象。这也意味着所有视图必须至少接受这一个对象,甚至那些没有任何显式参数的视图。一些简单的视图,比如显示服务器当前时间的视图,甚至可能不使用请求对象,但是无论如何都必须接受它,以满足视图的基本协议。

关于参数,另一点是视图必须能够接受传递给它的任何参数,包括从 URL 捕获的参数和传递到站点的 URL 配置中的参数。这似乎是显而易见的,但是一个常见的混淆点是假设 Django 使用某种魔法来允许 URL 配置指定应该使用哪个模板,而不需要视图中的任何支持代码。

Django 的通用视图都允许您指定模板名称,许多用户认为 Django 会以某种方式直接传递给模板系统,以覆盖视图默认使用的名称。事实是,通用视图对这个参数有特殊的处理,视图本身负责告诉模板系统使用哪个模板。Django 依赖于标准 Python,所以在幕后没有试图解释你的参数应该是什么意思的魔法。如果您计划为函数提供一个参数,请确保视图知道如何处理它。

最初的视图描述中的最后一个概念是视图必须返回一个HttpResponse对象,即使这样也不完全准确。返回响应无疑是所有视图的主要目标,但是在某些情况下,引发一个异常更合适,这将通过其他方式来处理。

请求和响应之间发生的事情在很大程度上是不受限制的,并且视图可以用于需要满足的许多目的。可以构建视图来服务于特定的目的,或者可以使它们足够通用以用于分布式应用。

将视图编写为通用视图

Django 开发中的一个常见主题是使代码尽可能地可重用和可配置,以便应用和代码片段在多种情况下都有用,而不必为每一种需求重写代码。这就是 DRY 的全部意义:不要重复自己。

视图对于 DRY 来说是一个挑战,因为它们只被传入的请求调用。看起来似乎不可能编写一个视图,除了它最初打算用于的请求之外,它还可以被调用。然而,Django 本身有很多通用视图的例子,这些视图可以用于各种各样的应用和情况,每次新的使用只需要少量的配置。

有一些准则可以极大地帮助视图的重用,使它们足够通用,可以在各种应用中使用。视图甚至可以如此通用,以至于可以分发给其他人,并包含在原作者没有概念的项目中。

使用大量的论据

通常,一个视图可以执行很多不同的任务,所有的任务组合起来解决一个特定的问题。这些任务中的每一个通常都必须对它应该如何工作做出假设,但是这些假设通常可以使用参数提取到一个可配置的选项中。考虑下面的视图,该视图旨在检索一篇博客文章并将其传递给模板。

from django.shortcuts import render_to_response

from django.template import RequestContext

from blog.models import Post

def show_post(request, id):

post = Post.objects.get(id=id)

context = RequestContext(request, {'post': post})

return render_to_response('blog/detail.html', context)

这个视图将非常适合它的预期目的,但是它与一个特定的博客应用紧密相连。它仍然是松散耦合的,因为它不需要处理如何检索博客文章或呈现模板的细节,但仍然依赖于特定于博客应用的细节,如模型和模板。

相反,有可能将这些假设转移到可以在其他情况下替换的论点中。虽然最初这将涉及到一些额外的工作,但是如果这种视图在很多情况下被使用,它可以节省很多时间。更重要的是,视图越复杂,使用这种技术可以重用的代码就越多。一旦这些选项被移到参数中,特定的值就可以通过 URL 配置传入,所以不必为每个目的编写视图。

对于这个特殊的视图,一些事情可以这样分解。不需要预先知道模型,视图也应该能够使用 QuerySet,以便特定的 URL 可以对有限的数据集进行操作。此外,字段名称不应该是硬编码的,模板名称应该在视图之外提供。

from django.shortcuts import render_to_response

from django.template import RequestContext

def show_object(request, id, model, template_name):

object = model._default_manager.get(pk=id)

context = RequestContext(request, {'object': object)})

return render_to_response(template_name, context)

然后,当需要使用这个视图时,通过使用 URL 配置提供这些细节,就可以很容易地进行定制。只需在 URL 配置中提供参数值作为额外的字典,每次从该 URL 模式调用视图时都会传递这些参数值。

from django.conf.urls.defaults import *

from blog.models import Post

urlpatterns = patterns(''

(r'^post/(?P<id>\d+)/$', 'blog.views.show_object', {

'model': Post

'template_name': 'blog/detail.html'

})

)

这种方法甚至可以用于使用其他类型 id 的模型,例如使用 DJNG-001 格式的目录号的音乐数据库;任何可以保证在所有对象中唯一的东西都可以用作对象的主键。因为我们新的通用视图只是将 ID 直接传递给数据库 API,所以只需适当地调整 URL 模式,就可以很容易地支持其他类型的 ID。

r'^album/(?P<id>[a-z]+-[0-9])/$'

这个特殊的视图不应该写在第一位,因为 Django 为此提供了一个现成的视图DetailView,它甚至比这里显示的例子更加通用。它使用了十几个不同的参数,所有这些参数都应该在 URL 配置中进行定制。

一旦您有了一个接受大量参数进行定制的视图,就很容易要求在每个 URL 配置中指定太多的参数。如果每次使用视图都需要指定所有的配置选项,那么使用通用视图的工作量很快就会变得和每次从头开始编写视图一样多。显然,需要有一种更好的方式来处理所有这些争论。

提供合理的默认值

因为函数可以为任何使用它们的参数定义默认值,所以管理这种复杂性的最合理的方法是尽可能提供合适的默认值。确切地说,每个视图可以提供什么样的缺省值以及它们看起来是什么样的会有所不同,但是通常有可能为它们提供一些合理的值。

有时您有许多视图,每个视图服务于不同的目的,但是可能有一些共同的代码。这通常是样板文件,每个视图都需要使用,但并不适合任何单个视图的真正功能。

例如,个人页面的视图必须始终验证用户是否登录以及他们是否拥有适当的权限。一个应用可能有十几种不同类型的视图,但是如果它们都是私有的,那么它们每次都必须使用相同的代码。幸运的是,我们正在使用 Python,这提供了一个有用的选择。

视图装饰者

视图中的大多数样板文件要么在最开始,要么在最末尾。通常,它处理诸如初始化各种对象、测试标准先决条件、优雅地处理错误或在响应到达浏览器之前定制响应之类的任务。视图的真正核心是位于中间的部分,这是写起来有趣的部分。在第二章的中描述,装饰器是一种很好的方式,可以将几个函数封装在一些只需编写一次就可以轻松测试的公共代码中,这样可以减少 bug 和程序员的疲劳。因为视图通常只是标准的 Python 函数,所以这里也可以使用 decorators。

第二章展示了如何使用 decorators 编写原始函数的包装器,该包装器可以访问该函数的所有参数,以及函数本身的返回值。就视图而言,这意味着 decorators 总是可以访问传入的请求对象和传出的响应对象。在某些情况下,装饰器可以是特定应用的特例,这将允许它预期特定于该应用的大量参数。

decorators 可以提供很多视图,其中一些很常见,足以保证包含在 Django 本身中。位于django.views.decorators的是一些包含装饰器的包,你可以在任何应用的任何视图上使用它们。下面列出的包只提供了完整导入路径的尾部,因为它们都位于同一位置。

  • cache.cache_page—将视图的输出存储到服务器的缓存中,以便以后有类似的请求时,不必每次都重新创建页面。
  • cache.never_cache—防止缓存特定视图。如果您设置了站点范围的缓存,但某些视图不能过时,这是很有用的。
  • gzip.gzip_page—压缩视图的输出并添加适当的 HTTP 头,以便 Web 浏览器知道如何处理它。
  • http.conditional_page—仅当自浏览器上次获取副本以来页面发生了更改时,才将整个页面发送到浏览器。
  • http.require_http_methods—接受一个 HTTP 方法列表(在第七章的中有详细描述),该视图仅限于这些方法。如果用任何其他方法调用视图,它会发送一个响应告诉浏览器这是不允许的,甚至不调用视图。包含的两个快捷方式变体是http.require_GEThttp.require_POST,它们不带任何参数,分别针对 GET 和 POST 请求进行硬编码。
  • vary.vary_on_header—根据传递给装饰器的头的值,通过指示页面内容的变化,帮助控制基于浏览器的页面缓存。特定于Cookie割台的简单变体可在vary.vary_on_cookie获得。

位于django.contrib的捆绑应用提供了额外的装饰器。这些装饰器都位于该路径之下,所以和前面的列表一样,只提供了相关的路径:

  • admin.views.decorators.staff_member_required—一个简单的装饰器,检查当前用户是否有人员访问权。这将自动用于 Django 内置管理中的所有视图,但也可以用于您站点上任何其他员工专用的视图。如果用户没有 staff 权限,装饰者会将浏览器重定向到管理员的登录页面。
  • auth.decorators.user_passes_test—接受单个参数,这是一个针对某些任意条件测试当前用户的函数。所提供的函数应该只接受User对象,如果测试通过,则返回True,如果测试失败,则返回False。如果测试通过,用户将被授予访问页面的权限,但如果测试失败,浏览器将重定向到网站的登录页面,这由LOGIN_URL设置决定。
  • auth.decorators.login_requireduser_passes_test的特殊版本,这个装饰器在允许访问视图之前简单地检查用户是否登录。
  • auth.decorators.permission_requireduser_passes_test的另一个专门化,它在视图加载之前检查用户是否有给定的权限。装饰器只接受一个参数:要检查的权限。

这些只是 Django 本身捆绑的装饰器。decorators 还有许多其他用途,第三方应用也可以提供它们自己的用途。然而,为了让这些装饰器有用,它们必须应用于视图。

应用视图装饰器

第二章描述了装饰器如何应用于标准 Python 函数。将 decorators 应用于视图的工作方式是相同的,但是有一个显著的区别:视图并不总是在您的控制之下。

第二章描述的技术假设你修饰的功能是你自己的。虽然这是经常发生的情况,但分布式应用的数量意味着许多 Django 支持的网站将使用其他来源的代码,并有自己的视图。如前所述应用 decorators 需要修改第三方代码。

目标是将 decorators 应用于第三方视图,而不实际修改第三方代码。做到这一点的关键在于 Python 2.3 和更早版本的旧式装饰语法。请记住,新语法允许在函数定义之上应用装饰器,但是旧语法依赖于将函数直接传递给装饰器。因为 Python 函数可以从任何地方导入,并且可以随时作为参数传入,所以这是从第三方代码创建修饰视图的一种极好的方式。

还要记住,URL 配置是在 Python 模块中定义的,当它被读取时就会被执行。这使得这种配置可以使用大量的 Python,包括将函数传递给 decorators 来创建新函数的能力。

from django.conf.urls.defaults import *

from django.contrib.auth.decorators import login_required

from thirdpartyapp.views import special_view

urlpatterns = patterns(''

(r'^private/special/$', login_required(special_view))

)

编写视图装饰器

第二章讲述了装饰者本身是如何工作的,以及如何在各种情况下编写它们,尽管视图的装饰者有一些具体的细节需要注意。这些与编写 decorators 的技术方面关系不大,更多的是在具体使用视图时如何实现某些有用效果的细微差别。

decorators 用于视图的最常见的任务是在原始视图周围创建一个包装函数。这允许装饰者执行视图本身通常会做的额外工作,包括

  • 基于传入的请求执行额外的工作或改变其属性
  • 改变传递给视图的参数
  • 修改或替换传出响应
  • 处理视图内部发生的错误
  • 分支到其他代码,甚至不执行视图

编写装饰器时要考虑的第一件事是,它接收所有针对视图本身的参数。前几节讨论了这一点,但只是在使用*args**kwargs接收参数并将它们直接传递给包装函数的常见上下文中。对于视图,您预先知道第一个参数将始终是传入的请求对象,因此包装器函数可以预见到这一点,并与其他参数分开接收请求。

通过在执行视图之前与请求对象进行交互,decorators 可以做两件重要的事情:根据传入的请求做出决策,并对请求进行更改以改变视图的操作方式。这些任务并不互相排斥,许多装饰者两者都做,比如下面来自 Django 的例子。

from django.utils.functional import wraps

def set_test_cookie(view):

"""

Automatically sets the test cookie on all anonymous users

so that they can be logged in more easily, without having

to hit a separate login page.

"""

def wrapper(request, *args, **kwargs):

if request.user.is_anonymous():

request.session.set_test_cookie()

return view(request, *args, **kwargs)

return wraps(view)(wrapper)

PRESERVING A VIEW’S NAME AND DOCUMENTATION

内置的管理接口使用视图函数本身的名称和 docstring 为应用的视图生成文档。通过使用 decorators 包装函数,我们实际上是用包装器替换了原始的视图函数。这导致管理界面看到包装器,而不是视图。

通常,这将导致视图的名称和文档字符串在混乱中丢失,因此管理员的文档特性不能正确地处理这些视图。为了获得正确的文档,函数的这些属性必须在整个包装过程中保持不变。

Django 提供了一个额外的装饰器,位于django.utils.functional.wraps,它被设计成将这些属性复制到包装的函数上,这样看起来更像原始视图。这个过程在第九章中有更详细的描述,但是本节中的所有例子都用它来说明修饰视图的最佳实践。

装饰器的另一个常见用途是从一组视图的开头或结尾提取一些公共代码。这在查看传入参数时特别有用,因为 decorators 可以在调用视图之前执行任何查找和初始化。然后,装饰者可以简单地将完全准备好的对象传递给视图,而不是从 URL 获取原始字符串。

from django.utils.functional import wraps

from django.shortcuts import get_object_or_404

from news.models import Article

def get_article_from_id(view):

"""

Retrieves a specific article, passing it to the view directly

"""

def wrapper(request, id, *args, **kwargs):

article = get_object_or_404(Article, id=int(id))

return view(request, article=article, *args, **kwargs)

return wraps(view)(wrapper)

像这样的装饰器的优点在于,尽管它包含的逻辑相当少,但它确实减少了为视图复制的代码量,这些视图都根据 URL 中提供的 ID 获得一个Article对象。这不仅使视图本身更具可读性,而且任何时候您都可以减少必须编写的代码,这有助于减少 bug。

此外,通过访问响应,装饰者可以对响应应该如何表现做出一些有趣的决定。在第七章中描述的中间件类,在访问响应方面有更多的用途,但是装饰者仍然可以做一些有用的事情。

值得注意的是能够设置响应的内容类型,这可以控制浏览器在收到内容后如何处理它。第七章更详细地描述了这一点,以及在创建响应时如何设置它。然而,也可以在响应已经被创建并从视图返回之后设置它。

这种技术是为特定类型的视图覆盖内容类型的好方法。毕竟,如果没有指定内容类型,Django 会从DEFAULT_CONTENT_TYPE设置中提取一个值,默认为'text/html'。对于某些类型的视图,尤其是那些面向 Web 服务的视图,最好使用另一种内容类型,比如'application/xml',同时仍然能够使用通用视图。

from django.utils.functional import wraps

def content_type(c_type):

"""

Overrides the Content-Type provided by the view.

Accepts a single argument, the new Content-Type

value to be written to the outgoing response.

"""

def decorator(view):

def wrapper(request, *args, **kwargs):

response = view(request, *args, **kwargs)

response['Content-Type'] = c_type

return response

return wraps(view)(wrapper)

return decorator

然后,这个装饰器可以在将内容类型应用到视图时接受它。

@content_type('application/json')

def view(request):

...

视图装饰器的一个很少使用的特性是捕捉由视图或它执行的任何代码引发的任何异常的能力。视图通常只是直接返回一个响应,但是在很多情况下视图可能会选择引发一个异常。Django 自己的通用视图中常见的一个例子是抛出Http404异常来表示找不到某个对象。

第九章涵盖了 Django 在其标准发行版中提供的例外情况,其中许多可以由视图出于这样或那样的原因提出。此外,许多标准 Python 异常可能会在各种情况下出现,捕捉这些异常可能会很有用。当出现异常时,装饰器可以执行各种额外的任务,从简单地将异常记录到数据库,到在某些异常的情况下返回不同类型的响应。

考虑一个具有如下日志条目模型的自定义日志记录应用:

from datetime import datetime

from django.db import models

class Entry(models.Model):

path = models.CharField(max_length=255)

type = models.CharField(max_length=255, db_index=True)

date = models.DateTimeField(default=datetime.utcnow, db_index=True)

description = models.TextField()

提供这个模型的应用也可以为项目提供一个装饰器,应用到它们自己的视图中,自动记录这个模型的异常。

from django.utils.functional import wraps

from mylogapp.models import Entry

def logged(view):

"""

Logs any errors that occurred during the view

in a special model design for app-specific errors

"""

def wrapper(request, *args, **kwargs):

try:

return view(request, *args, **kwargs)

except Exception as e:

# Log the entry using the application’s Entry model             Entry.objects.create(path=request.path

type='View exception'

description=str(e))

# Re-raise it so standard error handling still applies

raise

return wraps(view)(wrapper)

所有这些例子反复出现的主题是,视图装饰者可以封装一些公共代码,否则这些代码必须在视图的每个实例中重复。本质上,视图装饰器是在原始代码之前或之后扩展视图代码的一种方式。为了认识到视图装饰器有多大的潜力,概括这些例子是很重要的。您发现自己在视图的开头或结尾复制的任何样板文件都可以放在装饰器中,以节省时间、精力和麻烦。

基于类的视图

视图不必局限于函数。最后,对 Django 来说,重要的是它得到了一个可调用函数;如何创建这个可调用函数仍然取决于您。您还可以将视图定义为类,这提供了一些优于传统函数的关键优势。

  • 更高的可配置性
  • 更轻松地定制专业应用
  • 重复使用可能用于其他目的的对象

尽管最终结果必须是可调用的,但创建类的方法比创建函数的方法还要多。Django 自己的通用视图遵循特定的结构,本着尽可能保持相似性的精神,尝试与之匹配是个好主意。请记住,如果这种格式不像其他格式那样容易满足您的需求,您可以编写不同的类。

django.views.generic.base.View

将基础知识加入到您的类中的最简单的方法是子类化 Django 自己的View类。当然,它不能满足您开箱后的所有需求,但它提供了基本功能:

  • 验证传入视图配置的参数
  • 防止使用以 HTTP 方法命名的参数
  • 收集在 URL 配置中传递的参数
  • 将请求信息保存在方便方法访问的地方
  • 验证视图是否支持请求的 HTTP 方法
  • 自动处理选项请求
  • 根据请求的 HTTP 方法调度视图方法

其中一些功能是特定于它是一个类的,比如使方便的请求信息可以方便地用于各种视图方法。其他的,比如强制执行特定的 HTTP 方法和直接处理选项请求,实际上都是很好的 HTTP 实践,因为类可以提供这样的功能,而您不必记住在自己的代码中遵从它,所以变得更加容易。

类为多个方法提供了交互的机会,所以 Django 使用一些标准方法来处理常见的项目,同时为您提供了一种添加其他方法的方式,现在只需要担心您的应用的细节。而且因为它只是一个类,你也可以添加你认为合适的其他方法。Django 不会对它们做任何事情,所以您必须自己调用它们,但是它确实给了您一个机会,让您比使用原始函数更容易地抽象出公共代码。

Django 的所有通用视图都继承自一个共同的祖先来提供所有这些挂钩,您的视图也很容易做到这一点。让我们看看 Django 在默认通用视图上提供的一些方法。

init(自我,**夸格)

作为一个设计用来创建对象的类,__init__()显然是类的实例开始的地方。它的关键字参数是在 URL 中定义的选项,但是实际上在任何时候都不会直接调用它。相反,您的 URL 配置将使用as_view(),它做一些事情,包括初始化类。

__init__()的默认实现只是将所有提供的关键字参数设置为视图对象上的实例变量。

BE CAREFUL OVERRIDING INIT()

正如您将在下面几节中看到的,直到请求到达并被发送到生成的视图函数,视图类才被实例化。这意味着__init__()会为每个传入的请求触发,而不是在 Django 处理您的 URL 配置时只触发一次。

因此,如果您需要对配置选项执行任何更改,或者以不需要访问来自实际请求的任何信息的任何方式对它们做出反应,您将想要覆盖as_view()并在那里添加您的逻辑。实际上,__init__()甚至看不到请求对象本身;它只接收从 URL 捕获的参数。

因此,虽然__init__()通常是提供额外配置特性的好地方,但在这种情况下,它往往不会工作得很好。如果需要处理配置,最好重写as_view(),如果需要处理与传入请求相关的任何事情,最好重写dispatch()

as_view(cls,**initkwargs)

这个类方法是视图的主要入口点。当您配置一个 URL 来使用这个视图时,您将调用这个方法,它将返回一个供 Django 使用的视图函数。您还将把配置选项传递到方法调用中,而不是把它们放在视图本身旁边的字典中。例如:

from django.views.generic.base import View

urlpatterns = patterns(''

(r'^example/', View.as_view(template_name='example.html'))

)

as_view()被召唤时,负责几件事:

  • 它验证所提供的选项都不匹配 HTTP 方法的名称。如果发现任何错误,它会立即引发一个TypeError,而不是等待请求的到来。
  • 它还验证所有提供的选项是否与类中现有的命名属性相匹配。这实施了一种模式,其中默认选项被建立为类属性,然后根据需要被单独的 URL 配置覆盖。例如,前面的例子会引发一个TypeError,因为template_name没有被命名为内置View类的一个属性。
  • 然后,它创建一个简单的视图函数,该函数将返回到 URL 配置中,供实际请求进入时使用。然后这个视图用来自类和任何应用的装饰器的一些属性更新,使它在以后自省时更有用。
  • 最后,它返回新创建的视图函数,所以当请求开始进来时,Django 就有东西可处理了。

as_view()创建的视图功能甚至更简单。它接受一个request,以及*args**kwargs,因此它可以根据 URL 配置中的正则表达式接收从 URL 捕获的任何内容。这与任何其他视图的工作方式相同;Django 的 URL 调度处理基于类的视图的方式没有什么特别的。

一旦有了这些信息,它只负责一点点记录和调用更有用的东西:

  • 首先,它创建 view 类的一个实例,传递提供给as_view()的配置选项。这就是__init__()最终发挥作用的地方,因为视图的实例只适用于单个请求。每个后续请求将获得视图类的一个新实例。
  • 接下来,它检查视图是否有get()head()方法。如果它有get()而没有head(),它会设置视图,这样 HEAD 请求就会被发送到get()方法。一般来说,HEAD 应该像 GET 一样工作,但是不返回内容,所以这是一个合理的默认行为。
  • 然后,它将请求和 URL 捕获的信息设置到对象上,作为名为requestargskwargs的实例属性。您可能不需要将这些信息作为对象的属性来访问,但是如果您确实需要它们,它们就在那里。
  • 最后,它将执行委托给dispatch()方法,传递请求和所有捕获的参数,就像它们被传递给视图本身一样。
派遣(自身,请求,*参数,*夸尔格斯)

这是正确处理请求的地方。像任何视图一样,它负责接受请求并返回响应。它的默认实现处理不同 HTTP 方法的一些复杂性,同时允许您在附加的视图方法中编写代码。

  • 它首先检查所请求的 HTTP 方法是否有效,并在类上有一个匹配的视图方法来处理请求。如果没有,它将返回一个状态代码为 405 Method Not Allowed 的响应,而不是尝试以任何额外的能力为其提供服务。
  • 如果类确实有匹配的视图方法,dispatch()只是遵从它,将所有参数传递给它。

HTTP 方法的第一个测试是对照已知方法的列表检查方法字符串的小写副本,该列表存储为名为http_method_names的类属性:

  • get
  • post
  • put
  • delete
  • head
  • options
  • trace

请注意,较新的选项(如修补程序)不在此列表中。如果您确实需要一个不同的方法,并且您所在的环境会将它传递给 Django,那么您可以覆盖这个列表来添加您需要的任何其他方法。如果一个 HTTP 方法不在这个列表中,Django 不会允许,即使你有一个同名的视图方法。Django 在这里的行为通常是首选的,但是如果您愿意,您可以覆盖dispatch()来提供其他功能。例如,如果您有一个可以以各种格式返回数据的 API,那么您可以使用dispatch()方法根据需要格式化输出,让各个方法只检索和返回原始数据。

个人视图方法

dispatch()确定视图可以处理请求后,它将请求发送到几个可能的函数中的一个,根据 HTTP 方法命名。例如,GET 请求将被路由到get(),POST 请求将被路由到post(),等等。这些函数的行为就像一个标准的视图函数,接受一个请求和额外的参数并返回一个响应。

为了演示这在实践中如何帮助您的代码,请考虑以下在传统的基于函数的视图中处理表单的示例:

def view(request, template_name='form.html'):

if request.method == 'POST':

form = ExampleForm(request.POST)

if form.is_valid():

# Process the form here

return redirect('success')

else:

return render(request, template_name, {'form': form})

else:

form = ExampleForm()  # no data gets passed in

return render(request, template_name, {'form': form})

这个视图服务于 GET 和 POST 请求,因此它必须处理形成需要处理的数据的请求,同时还要管理没有任何数据的请求,以便首先显示表单。下面是使用基于类的视图时该视图的样子。

class FormView(View):

template_name = 'form.html'

def get(self, request):

form = ExampleForm()

return render(request, self.template_name, {'form': form})

def post(self, request):

form = ExampleForm(request.POST)

if form.is_valid():

# Process the form here

return redirect('success')

else:

return render(request, self.template_name, {'form': form})

这是一个更加清晰的关注点分离,作为一个额外的好处,基于类的版本将自动正确地处理 HEAD 和 OPTIONS 请求,同时拒绝 PUT、DELETE 和 TRACE 请求。

Django 还提供了一个简单的options()方法,它指明了 URL 可以提供什么特性。默认行为使用可用的视图方法来指示允许哪些 HTTP 方法,并在响应的Allow头中提供这些方法。如果您有更多的特性需要包含在这里,比如跨源资源共享所必需的特性, 2 您可以简单地覆盖options()来提供这些信息。

装饰视图方法

当涉及到装饰者时,这些基于类的视图的结构使它们有些有趣。一方面,它们是类,不能用函数所用的装饰器来装饰。事实上,在 Python 3 之前,类根本不能被修饰。另一方面,as_view()方法返回一个简单的函数,可以像其他函数一样进行修饰。

最简单的解释技巧是修饰as_view()的输出。因为它返回一个函数,所以它可以像任何其他函数一样被修饰。因此,如果您需要要求用户登录,您可以像往常一样简单地使用标准的login_required装饰器。

from django.contrib.auth.decorators import login_required

urlpatterns = patterns(''

(r'^example/'login_required(FormView.as_view(template_name='example.html')))

)

另一方面,如果你知道它们总是需要的东西,你可以直接在你的类中修饰单独的方法。有两件事比典型的函数情况更复杂。首先,这些是实例方法,而不是简单的函数,这意味着它们接受一个self参数,这在传统的基于函数的视图中是没有的。就像装饰者经常遇到的问题一样,解决方案是另一个装饰者,在这种情况下是由 Django 自己提供的。method_decorator可以用来包装一个普通的装饰器,让它忽略self,只处理它期望的参数。

from django.utils.decorators import method_decorator

class FormView(View):

@method_decorator(login_required)

def get(request):

# View code continues here

第二个问题是,现在涉及到了多个函数,而不仅仅是一个可以直接修饰的函数。您可以修饰任何您喜欢的函数,但是基于类的视图的调度过程的一个有趣的事实是,dispatch()是唯一一个与传统函数具有相同目的的方法。所有请求都要经过它,它还可以访问关于类、实例和传入请求的所有可用信息。

因此,这也是应用任何视图装饰器的最佳地方。如果您对dispatch()应用一个装饰器,它将修改每个请求的行为,不管后来使用了什么其他方法。如果有充分的理由,您可以修饰单个方法,但最有用的是使用dispatch()并让它像在传统的基于函数的视图中一样工作。

将对象用作视图

正如在第二章中所描述的,Python 提供了一种定义类的方法,它的实例可以像函数一样被调用。如果定义在一个类上,那么当对象被传入一个期望函数的地方时,__call__()方法将被调用。与任何其他可调用对象一样,这些对象也可以用作 Django 视图。

有多少种方法定义对象本身,就有多少种方法使用对象作为视图。除了使用__call__()接收每个传入的请求之外,对象内部发生的事情也是公开的。在典型的情况下,请求将被分派给单独的方法,类似于 Django 自己的基于类的视图,但是您可以做您需要的任何事情。

应用技术

通过允许自定义对象和装饰器用于 URL 模式和视图,几乎任何有效的 Python 代码都可以自定义 URL 如何映射到视图以及视图本身如何执行。以下只是一个尝试的可能性;其余的取决于您的应用的需要。

跨产地资源共享(CORS)

从一个域跨到另一个域的请求存在安全风险,因为它们可能会将敏感数据暴露给不应该访问这些数据的站点。想象一下,如果一个随机的博客可以对你的银行网站进行 AJAX 调用。如果你登录后浏览器没有任何保护措施,这个电话可能会把你的银行账户信息发送到一个你一无所知、只是碰巧访问过的博客上。

幸运的是,现代浏览器确实有针对这类事情的保护措施。默认情况下,在您的浏览器中,从一个站点向另一个站点发出的请求将被禁止。不过,像这样的跨源请求也有合法的用途,比如在可信站点之间,或者在为一般用途提供公共数据文件时。跨源资源共享(CORS)规范允许一个站点指示哪些其他站点可以访问某些资源。

CORS 室内设计师

在传统的基于函数的视图中,这种功能可以作为装饰添加。以下是如何装饰视图,使其从任何请求它的站点公开可用的方法:

@cross_origin(allow_origin=['*'])

def public_data(request):

# Data retrieval goes here

就装饰者而言,实现非常简单:

def cross_origin(allow_credentials=False, allow_headers=None

allow_methods=None, allow_headers=None

allow_origin=None, expose_headers=None, max_age=None):

def decorator(func):

@functools.wraps(func)

def wrapper(request, *args, **kwargs):

headers = {}

if access_control_allow_credentials:

headers['Allow-Credentials'] = allow_credentials

if access_control_allow_headers:

headers['Allow-Headers'] = ', '.join(allow_headers)

if access_control_allow_methods:

headers['Allow-Methods'] = ', '.join(allow_methods)

if access_control_allow_origin:

headers['Allow-Origin'] = ' '.join(allow_origin)

if access_control_expose_headers:

headers['Expose-Headers'] = ', '.join(expose_headers)

if access_control_max_age:

headers['Max-Age'] = self.max_age

response = func(request, *args, **kwargs)

for name, value in headers:

response.headers['Access-Control-%s' % name] = value

return response

return wrapper

return decorator

没有必要支持使用不带参数的装饰器,因为如果你不提供任何参数,它不会做任何事情。所以它只支持参数,当响应从修饰视图返回时,只需添加所有正确的头。如您所见,其中一些接受列表,而另一些只接受单个值。

Mixin 合唱团

这个装饰器可以使用method_decorator直接应用于基于类的视图,但是为了使它更容易配置,我们可以使用 mixin。下面是它在基于类的类似视图中的样子:

class PublicData(View, CrossOrigin):

access_control_allow_origin = ['*']

def get(self, request):

# Data retrieval goes here

实现比简单的装饰器稍微复杂一些,但是仍然非常简单:

class CrossOrigin(object):

"""

A view mixin that provides basic functionality necessary to add the necessary

headers for Cross-Origin Resource Sharing

"""

access_control_allow_credentials = False

access_control_allow_headers = None

access_control_allow_methods = None

access_control_allow_origin = None

access_control_expose_headers = None

access_control_max_age = None

def get_access_control_headers(self, request):

headers = {}

if self.access_control_allow_credentials:

headers['Allow-Credentials'] = self.access_control_allow_credentials

if self.access_control_allow_headers:

headers['Allow-Headers'] = ', '.join(self.access_control_allow_headers)

if self.access_control_allow_methods:

headers['Allow-Methods'] = ', '.join(self.access_control_allow_methods)

if self.access_control_allow_origin:

headers['Allow-Origin'] = ' '.join(self.access_control_allow_origin)

if self.access_control_expose_headers:

headers['Expose-Headers'] = ', '.join(self.access_control_expose_headers)

if self.access_control_max_age:

headers['Max-Age'] = self.access_control_max_age

return headers

def dispatch(self, request, *args, **kwargs):

response = super(CORSMixin, self).dispatch(request, *args, **kwargs)

for name, value in self.get_access_control_headers(request):

response.headers['Access-Control-%s' % name)] = value

return response

这里值得注意的是,header 功能已经转移到一个单独的方法中,该方法接收请求作为参数。这允许您在子类中覆盖该方法,以防您需要根据传入请求的细节对 CORS 头进行更改。

例如,如果您有许多需要访问资源的不同域,您可以根据这些域检查传入的请求,只将该域添加为允许的源,而不必在每个响应中包含整个列表。这是一个很好的例子,说明了类比装饰器能够更好地定制内部细节,装饰器倾向于以一种您无法修改的方式隐藏那些实现。

提供装饰器和混合器

如果您想将它作为一个可重用的助手来提供,您甚至可以同时提供函数 decorator 和类 mixin。这很容易做到,只需将公共代码提取到一个单独的函数中,可以从每种不同的方法中调用该函数。

def cors_headers(allow_credentials=false, allow_headers=None, allow_methods=None,                 allow_origin=None, expose_headers=None, max_age=None):

headers = {}

if allow_credentials:

headers['Access-Control-Allow-Credentials'] = allow_credentials

if allow_headers:

headers['Access-Control-Allow-Headers'] = ', '.join(allow_headers)

if allow_methods:

headers['Access-Control-Allow-Methods'] = ', '.join(allow_methods)

if allow_origin:

headers['Access-Control-Allow-Origin'] = ' '.join(allow_origin)

if expose_headers:

headers['Access-Control-Expose-Headers'] = ', '.join(expose_headers)

if max_age:

headers['Access-Control-Max-Age'] = self.max_age

return response

def cross_origin(allow_credentials=false, allow_headers=None, allow_methods=None

allow_origin=None, expose_headers=None, max_age=None):

def decorator(func):

@functools.wraps(func)

def wrapper(request, *args, **kwargs):

response = func(request, *args, **kwargs)

headers = cors_headers(response, allow_credentials, allow_headers

allow_methods, allow_origin, expose_headers, max_age)

response.headers.update(headers)

return response

return wrapper

return decorator

class CrossOrigin(object):

"""

A view mixin that provides basic functionality necessary to add the necessary

headers for Cross-Origin Resource Sharing

"""

access_control_allow_credentials = false

access_control_allow_headers = None

access_control_allow_methods = None

access_control_allow_origin = None

access_control_expose_headers = None

access_control_max_age = None

def get_access_control_headers(self, request):

return cors_headers(self.access_control_allow_credentials

self.access_control_allow_headers

self.access_control_allow_methods

self.access_control_allow_origin

self.access_control_expose_headers

self.access_control_max_age):

def dispatch(self, request, *args, **kwargs):

response = super(CORSMixin, self).dispatch(request, *args, **kwargs)

headers = self.get_access_control_headers(request)

response.headers.update(headers)

return response

现在,decorator 和 mixin 唯一要做的事情就是为每种技术适当地收集参数,把实际的头应用到一个公共函数的细节留下来。这并不是一个突破性的技术,但它有助于了解装饰者和混合者到底有什么不同。它们的配置稍有不同,但最终还是要接受请求并返回响应。

现在怎么办?

URL 构成了站点架构的基础,定义了用户如何访问您提供的内容和服务。Django 不参与 URL 方案的设计,所以你可以随心所欲地构建它。一定要花适当的时间,记住 URL 配置仍然是网站设计的一种形式。

视图是任何应用的真正主力,接收用户输入并将其转化为有用的输出。虽然视图可以使用整个 Python,但是 Django 确实提供了一个非常重要的工具来处理 Web 上最常见的用户输入任务之一:表单。

Footnotes 1

http://prodjango.com/cool-uris-dont-change/

  2

http://prodjango.com/cors/

五、表单

Abstract

现代 Web 应用的关键要素之一是交互性——接受用户输入的能力,这有助于塑造他们的体验。输入可以是任何内容,从简单的搜索词到用户提交的整部小说。关键是能够处理这些输入,并将其转化为有意义的功能,丰富网站所有用户的体验。

现代 Web 应用的关键要素之一是交互性——接受用户输入的能力,这有助于塑造他们的体验。输入可以是任何内容,从简单的搜索词到用户提交的整部小说。关键是能够处理这些输入,并将其转化为有意义的功能,丰富网站所有用户的体验。

该过程首先向 Web 浏览器发送一个 HTML 表单,用户可以在其中填写表单并将其提交回服务器。当数据到达时,必须对其进行验证,以确保用户没有忘记任何字段或输入任何不适当的内容。如果提交的数据有任何问题,必须将其发送回用户进行更正。一旦知道所有的数据都是有效的,应用最终就可以使用这些数据执行有意义的任务。

没有框架也可以做到这一切,但是如果涉及到多个表单,那么这样做将会涉及大量的重复工作。手动管理表单也给程序员带来了走捷径的高风险。表单跳过必要的验证是很常见的,要么是因为缺少时间,要么是觉得没有必要。许多被利用的安全漏洞可以直接归因于这种类型的疏忽。

Django 通过提供一个管理这些细节的框架来解决这个问题。一旦定义了表单,Django 就会处理生成 HTML、接收输入和验证数据的细节。之后,应用可以对收到的数据做任何想做的事情。像 Django 中的其他东西一样,您也可以绕过这种表单处理,在必要时手动处理。

声明和标识字段

Django 的表单和它的模型一样,使用声明性语法,其中字段作为属性分配给表单的类定义。这是 Django 最明显的特征之一,在这里也用得很好。它允许将一个窗体声明为一个简单的类,同时在幕后提供大量的附加功能。

模型和表单的第一个区别是它们识别字段的方式。模型实际上根本不识别字段;它们只是检查属性是否有一个contribute_to_class()方法并调用它,而不管它附加到什么类型的对象。表单实际上会检查类中每个属性的类型,以确定它是否是一个字段,特别是寻找django.forms.fields.Field的实例。

与模型一样,表单保留了对所有已声明字段的引用,尽管表单的做法略有不同。根据表单所处的阶段,表单上可能会有两个单独的字段列表,每个列表都有自己的用途。

第一个是base_fields,是元类执行时找到的所有字段的列表。它们存储在 form 类本身中,并且对所有实例都可用。因此,只有在极端情况下才应该编辑这个列表,因为这样做会影响表单的所有未来实例。当查看表单类本身或识别那些实际上直接在类上声明的字段时,作为参考总是有用的。

所有表单实例都有一个fields属性,其中包含实际用于生成表单 HTML 以及验证用户输入的字段。大多数时候,这个列表与base_fields相同,因为它只是它的一个副本。但是,有时一个表单需要根据一些其他信息来定制它的字段,这样各个实例在不同的情况下会有不同的行为。

例如,联系人表单可以接受一个User对象来确定用户是否登录。如果没有,表单可以添加另一个字段来接受用户名。

from django import forms

class ContactForm(forms.Form):

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

super(ContactForm, self).__init__(*args, **kwargs)

if not user.is_authenticated():

# Add a name field since the user doesn't have a name

self.fields['name'] = forms.CharField(label='Full name')

绑定到用户输入

因为表单是专门用来接受用户输入的,所以该活动必须在其他任何活动之前执行。这非常重要,实例化的表单被认为处于两种状态之一:绑定或未绑定。绑定窗体给定用户输入,然后用户可以使用它来做进一步的工作,而非绑定窗体没有与之相关联的数据,通常只用于向用户询问必要的数据。

这两者的区别是在实例化表单时根据是否传入了数据字典来确定的。这个字典将字段名映射到它们的值,如果它被传入,它总是表单的第一个位置参数。即使传递一个空字典也会导致表单被认为是绑定的,尽管它的用处是有限的,因为没有数据,表单就不太可能被验证。一旦一个表单被实例化,通过检查它的布尔is_bound属性,很容易确定它是否被绑定到数据。

>>> from django import forms

>>> class MyForm(forms.Form):

...     title = forms.CharField()

...     age = forms.IntegerField()

...     photo = forms.ImageField()

...

>>> MyForm().is_bound

False

>>> MyForm({'title': u'New Title', 'age': u'25'}).is_bound

True

>>> MyForm({}).is_bound

True

还要注意,所有值都是作为字符串传递的。有些字段可能接受其他类型,如整数,但字符串是标准的,所有字段都知道如何处理它们。这是为了支持实例化表单的最常见方式,使用视图中可用的request.POST字典。

from my_app.forms import MyForm

def my_view(request):

if request.method == 'POST':

form = MyForm(request.POST)

else:

form = MyForm()

...

有时,表单也可能接受文件,这与其他类型的输入略有不同。文件可以作为传入请求对象的FILES属性来访问,这通过接受该属性作为第二个位置参数来实现。

from my_app.forms import MyForm

def my_view(request):

if request.method == 'POST':

form = MyForm(request.POST, request.FILES)

else:

form = MyForm()

...

不管以何种方式实例化,表单的任何实例都有一个data属性,它包含传递给它的任何数据的字典。对于未绑定的表单,这将是一个空字典。单独使用data是不安全的,因为不能保证用户提交的数据适合表单的需要,事实上它可能会带来安全风险。这些数据在使用前必须经过验证。

验证输入

一旦表单被绑定到一组传入数据,它就可以检查该数据的有效性,并且应该在继续之前一直这样做。这可以防止您的代码对数据质量做出无效的假设,从而防止许多安全问题。

从表面上看,验证用户输入的过程非常简单,只需调用表单的is_valid()方法。这将返回一个 Boolean 值,指示根据表单字段设置的规则,数据是否确实有效。仅这一点就足以确定是继续处理表单还是重新显示表单以供用户更正错误。

def my_view(request):

if request.method == 'POST':

form = MyForm(request.POST, request.FILES)

if form.is_valid():

# Do more work here, since the data is known to be good

else:

form = MyForm()

...

NEVER TRUST USER INPUT

在 Web 开发领域有一句古老的谚语,经常被这样表述:“用户输入是邪恶的。”这有点极端,但基本思想是 Web 应用不是在真空中运行,而是暴露在外部世界中,供各种各样的用户交互。这些用户中的大多数都是正直的网络公民,他们只希望按照预期的方式使用网站。然而,其他人最想做的就是让你宝贵的应用屈服。

任何基于用户输入采取行动的应用都有潜在的风险。因为决策是基于用户提供的内容做出的,所以用户对应用的行为有很大的控制权。在某些情况下,用户输入直接传递给数据库或文件系统操作,并假设输入将在某个已知值的既定范围内。

一旦有人心怀恶意,他可以利用这一事实,将其他数据推入应用,希望说服它做一些它不应该做的事情,例如读取用户不应该访问的内容,写入应该只读的区域,关闭应用,这样就没有人可以使用它,或者最糟糕的是,在您不知情的情况下获得对系统的完全访问权。这些类型的攻击通常分为几类,如 SQL 注入、跨站脚本、跨站请求伪造和表单操纵,但有一个主题将它们联系在一起:它们都依赖于应用过于信任传入的数据。

对这类攻击的解决方案是通过仔细验证所有输入的内容,有力地保护您的应用免受恶意输入。Django 的表单有多种方法来控制这种验证,但是is_valid()方法确保它们都运行,这样应用就可以知道是否应该使用输入。永远不要跳过这一步,因为这样做会使您的应用容易受到这些攻击。

同样重要的是要认识到,无论用户的 Web 浏览器内部发生了什么,都必须通过form.is_valid()的方式在服务器上进行验证。在这个 Web 2.0 和富 Web 应用的时代,很多工作都是在浏览器中用 JavaScript 完成的,很容易认为这足以在数据到达服务器之前确保输入数据的质量。

然而,在浏览器和服务器之间可以发生很多事情,有很多免费的工具可以帮助用户在 JavaScript 处理完提交的数据后对其进行操作。HTTP 也是一个容易使用的协议,所以完全绕过浏览器是很容易的。再多的客户端验证都不足以保护应用免受攻击;一切都必须在服务器上检查。

在幕后,is_valid()通过间接调用表单的full_clean()方法做了更多的工作,该方法填充了另外两个属性。第一个是cleaned_data,它是一个类似于前面提到的data属性的字典,除了它的值已经被表单的字段处理并转换成适当的 Python 数据类型。第二个是errors,这是一个字典,包含了输入数据遇到的所有问题的信息。

这两个属性在某种程度上相互关联,因为不应该同时在两个属性中标识任何字段。也就是说,如果一个字段的名称在cleaned_data中,它就不在errors中,反之亦然。因此,在理想情况下,cleaned_data将包含每个字段的数据,而errors将为空。

哪些数据被认为是有效的以及哪些错误将被返回的确切细节通常由每个字段使用其clean()方法来指定。对于大多数表单来说,这已经足够了,但是有些表单可能需要超出单个字段的额外验证。为了支持这一点,Django 提供了一种将附加验证规则注入表单的方法。

可以在表单上定义特殊的方法来帮助这个过程,并根据它们关联的字段来命名。例如,设计用来验证和清理title字段的方法将被称为clean_title()。以这种方式定义的每个方法负责在cleaned_data中查找它的值,根据适合表单的任何规则验证它。如果该值需要额外清理,该方法还必须用适当清理的值替换cleaned_data中的值。

使用基于类的视图

查看到目前为止显示的视图,您会注意到它们倾向于遵循一个共同的模式。事实上,您将遇到的大多数表单处理视图看起来很像这样:

from django.shortcuts import render, redirect

def my_view(request):

if request.method == 'POST':

form = MyForm(request.POST, request.FILES)

if form.is_valid():

form.save()

return redirect('/success/')

return render(request, 'form.html', {'form': form})

else:

form = MyForm()

return render(request, 'form.html', {'form': form})

如第四章中的所示,通过在基于类的视图中分别处理 GET 和 POST 情况,这可以变得更容易管理。

from django.shortcuts import render, redirect

from django.views.generic.base import View

class MyView(View):

def get(self, request):

form = MyForm()

return render(request, 'form.html', {'form': form})

def post(self, request):

form = MyForm(request.POST, request.FILES)

if form.is_valid():

form.save()

return redirect('/success/')

return render(request, 'form.html', {'form': form})

这当然是一个进步,但是这里仍然有很多样板文件。您几乎总是以相同的方式实例化和验证表单,并且在最初显示表单和显示错误时,模板呈现是相同的。归根结底,这个视图唯一真正有趣的部分是你用一个有效的表单做什么。在这种情况下,它只是调用form.save(),但你可以用它来发送电子邮件,传输一些文件,触发支付交易或任何其他事情。

为了避免所有这些重复,Django 提供了另一个基于类的视图,称为FormView。它抽象出了这些共性,所以您只需提供一些基本的细节和一个名为form_valid()的方法,该方法接收一个有效的表单作为唯一的参数。

from django.shortcuts import render, redirect

from django.views.generic.edit import FormView

class MyView(FormView):

form_class = MyForm

template_name = 'form.html'

success_url = '/success/'

def form_valid(self, form):

form.save()

return super(MyView, self).form_valid(form)

这让事情简单多了。事实上,您甚至不需要提供form_valid(),但是考虑到默认情况下它只是重定向到success_url,根本不用对表单做任何事情,您几乎总是希望至少提供这么多。根据需要,您还可以定义许多其他方法来控制其行为的各个方面。

  • get_form_class(self)—返回在整个过程中使用的表单类。默认情况下,它只返回form_class属性的内容,如果不提供属性,则返回None
  • get_initial(self)—返回一个字典,以传递到表单的initial参数中。就其本身而言,这只是返回视图的initial属性的内容,默认情况下这是一个空字典。
  • get_form_kwargs(self)—返回一个字典,在为每个请求实例化表单时用作关键字参数。默认情况下,这包括get_initial()的结果,如果请求是 POST 或 PUT,它还会添加request.POSTrequest.FILES
  • get_form(self, form_class)—通过将从get_form_kwargs()检索到的参数传递给从get_form_class()返回的类,返回一个完全实例化的表单。假设您可以通过get_form_kwargs()控制进入表单实例的所有参数,那么这只对在表单创建之后、有效性测试之前对表单进行修改有意义。
  • form_valid(self, form)—主要工作,当表单被验证时触发,允许您采取适当的行动。默认情况下,这会将用户重定向到get_success_url()方法的结果。
  • form_invalid(self, form)—一个自然对应物form_valid(),当表单被认为无效时被调用,将被赋予无效表单本身。默认情况下,这只是用表单重新呈现模板。
  • get_success_url(self)—这将返回成功验证表单后用户将被发送到的 URL。默认情况下,它返回success_url属性的值。

如您所见,FormView让您有机会定制表单流程的各个方面。不必为表单的每次使用都编写单独的视图,您可以只控制特定于您需要的部分。

如果您需要使用特定模型的表单,有另一个通用视图可以为您做更多的事情。Django 提供了几个其他的类供您使用,而不是重写其中的几个方法来创建一个ModelForm实例。这些都住在django.views.generic.edit里,因为它们允许你编辑数据。

  • CreateView用于帮助创建新对象。
  • UpdateView在编辑现有对象时使用。
  • DeleteView用于删除已有的对象。

所有这三种视图的工作方式都相似。要获得基本的功能,您真正需要的只是提供一个模型供他们使用。然后视图处理剩下的工作,包括设置表单、验证用户输入、保存数据以及将用户重定向到适当的 URL。

from django.views.generic import edit

from my_app.models import MyModel

class CreateObject(edit.CreateView):

model = MyModel

class EditObject(edit.UpdateView):

model = MyModel

class DeleteObject(edit.DeleteView):

model = MyModel

success_url = '/'

这里唯一令人惊讶的是,除了模型之外,DeleteView实际上还需要指定一个success_urlCreateViewUpdateView都产生一个有效的对象,数据与之相关联,因此它们的默认实现可以简单地调用修改后的对象上的get_absolute_url()

DeleteView的情况下,当视图完成工作时,被访问的对象不再存在,所以get_absolute_url()不是一个选项。由于没有标准的方法来描述对象列表的 URL,Django 无法猜测将用户发送到哪里。因此,为了正确使用DeleteView,你总是需要声明一个success_url

自定义字段

虽然 Django 包含的字段适用于大多数任务,但并不是每个应用都适合其他人认为常见的情况。对于那些现有字段不够用的应用,很容易为表单定义自定义字段,就像创建模型字段一样。创建表单域甚至比创建模型域更容易,因为它们不需要与数据库交互。

模型字段和表单字段的主要区别在于,表单只需要处理字符串输入,这大大简化了处理过程。不需要担心支持多个后端,每个后端都有自己的复杂性,更不用说添加到大量模型字段中的所有不同的查找类型和关系。

如前所述,所有表单字段都继承自Field,位于django.forms.fields。因为表单使用这一事实来区分字段与方法或其他属性,所以所有自定义字段都必须是这个继承链的一部分,以便正常工作。令人欣慰的是,Field提供了许多有用的特性,使得实现特定类型的字段变得更加容易。

像许多其他类一样,字段定义了一些属性和方法来控制特定的行为,例如使用什么小部件和显示什么错误消息,以及如何验证和清除传入的值。它们中的任何一个或全部都可以被覆盖,以自定义特定字段的功能。

确认

也许字段最重要的行为是它如何验证和清理用户输入。毕竟,字段是危险的传入数据和安全的 Python 环境之间的桥梁,所以正确地完成这种转换是非常重要的。字段的clean()方法主要负责这一点,既针对不正确的数据引发异常,又在输入有效时返回干净的值。

该方法的签名只是简单的clean(self, value),接受字段对象本身以及传入的值。然后,如果根据字段的要求,该值被认为是不合适的,那么它应该产生一个django.forms.util.ValidationError的实例,并显示一条消息,指出哪里出错了。否则,它应该将该值转换为适合该字段的任何本机 Python 数据类型,并将其返回。

除了确保错误消息尽可能具有描述性之外,保持错误消息的维护简单也很重要,同时仍然允许单个实例覆盖它们。Django 通过一对名为error_messagesdefault_error_messages的属性以及一个名为error_messages的参数来提供便利。这看起来像是一个价值纠结的巢,但它的工作方式相当简单。

字段类在名为default_error_messages的类级属性中定义其标准错误消息。这是一个将易于识别的键映射到实际错误信息字符串的字典。由于字段通常会从其他字段继承,这些字段可能会定义自己的default_error_messages属性,所以当字段被实例化时,Django 会自动将它们合并到一个字典中。

除了使用default_error_messages,Django 还允许单个字段实例通过error_messages参数覆盖其中的一些消息。该字典中的任何值都将替换指定键的默认值,但仅限于该特定的字段实例。该字段的所有其他实例将不受影响。

这意味着错误消息可能来自三个不同的地方:字段类本身、字段的父类和用于实例化字段的参数。当希望将异常作为clean()方法的一部分时,需要一种简单的方法来检索特定的错误消息,而不管它实际上是在哪里定义的。为此,Django 填充每个字段实例的一个error_messages属性,该属性包含以所有三种方式定义的所有消息。这样,clean()可以简单地在self.error_messages中查找一个键,并使用它的值作为ValidationError的参数。

from django.forms import fields, util

class LatitudeField(fields.DecimalField):

default_error_messages = {

'out_of_range': u'Value must be within -90 and 90.'

}

def clean(self, value):

value = super(LatitudeField, self).clean(value)

if not -90 <= value <= 90:

raise util.ValidationError(self.error_messages['out_of_range'])

return value

class LongitudeField(fields.DecimalField):

default_error_messages = {

'out_of_range': u'Value must be within -180 and 180.'

}

def clean(self, value):

value = super(LatitudeField, self).clean(value)

if not -180 <= value <= 180:

raise util.ValidationError(self.error_messages['out_of_range'])

return value

注意这里使用了super()来调用父DecimalField类的clean()方法,它首先确保该值是有效的小数,然后才检查它是否是有效的纬度或经度。由于无效值会导致异常,如果对DecimalField.clean()的调用允许代码继续执行,那么就可以确保该值是有效的十进制数。

控制小部件

字段类中定义的另外两个属性指定了在某些情况下使用哪些小部件为字段生成 HTML。第一个是widget,它定义了当 field 实例没有明确指定小部件时使用的默认小部件。这被指定为一个小部件类,而不是一个实例,因为小部件与字段本身同时被实例化。

第二个属性叫做hidden_widget,控制当字段应该输出到 HTML 中,但不向用户显示时使用哪个小部件。这不应该被覆盖,因为默认的HiddenInput小部件对于大多数字段来说已经足够了。有些字段,比如MultipleChoiceField,需要指定多个值,所以在这些情况下使用了特殊的MultipleHiddenInput

除了为这些情况指定单独的小部件类之外,字段还可以定义一个widget_attrs()方法来指定一组属性,这些属性应该添加到用于在 HTML 中呈现字段的任何小部件中。它接收两个参数,通常的selfwidget,这是一个完全实例化的小部件对象,任何新的属性都将附加到它上面。widget_attrs()应该返回一个字典,包含应该分配给小部件的所有属性,而不是直接附加属性。这是内置的CharField用来给 HTML 输入字段分配一个maxlength属性的技术。

定义 HTML 行为

如前一节所述,窗口小部件是字段在 Web 页面中以 HTML 表单表示的方式。虽然字段本身更多地处理数据验证和转换,但是小部件关心的是显示表单和接受用户输入。每个字段都有一个关联的小部件,用于处理与用户的实际交互。

Django 提供了各种小部件,从基本的文本输入到复选框和单选按钮,甚至是多选列表框。Django 提供的每个字段都有一个最适合该字段最常见用例的小部件,作为它的widget属性,但是有些情况可能需要不同的小部件。这些小部件可以在单个字段的基础上被覆盖,只需向字段的构造函数提供一个不同的类作为widget参数。

自定义小部件

像字段一样,Django 提供的小部件对于大多数常见的情况都很有用,但是不能满足所有的需求。有些应用可能需要提供额外的信息,如度量单位,以帮助用户准确地输入数据。其他人可能需要集成客户端 JavaScript 库来提供额外的选项,比如用于选择日期的日历。这些类型的附加特性是由定制的小部件提供的,它们满足了相关领域的需求,同时在 HTML 中提供了很大的灵活性。

虽然没有像字段那样严格执行,但所有的小部件都应该从django.forms.widgets.Widget继承,以便从一开始就获得最常见的功能。然后,每个定制小部件可以覆盖最适合它需要执行的任务的任何属性和方法。

呈现 HTML

定制小部件最常见的需求是通过 HTML 的方式为用户提供定制的字段显示。例如,如果一个应用需要一个字段来处理百分比,如果它的小部件可以在输入字段后输出一个百分号(%),那么用户使用这个字段会更容易。这可以通过覆盖小部件的render()方法来实现。

除了正常的selfrender()方法接收三个额外的参数:HTML 元素的name,当前与之关联的valueattrs,一个应该应用于元素的属性字典。其中,只有attrs是可选的,如果没有提供,它将默认为一个空字典。

>>> from django import forms

>>> class PriceInput(forms.TextInput):

...     def render(self, name, value, attrs=None):

...         return '``$ %s

...

>>> class PercentInput(forms.TextInput):

...     def render(self, name, value, attrs=None):

...         return '``%s %%

...

>>> class ProductEntry(forms.Form):

...     sku = forms.IntegerField(label='SKU')

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

...     price = forms.DecimalField(decimal_places=2, widget=PriceInput())

...     tax = forms.IntegerField(widget=PercentInput())

...

>>> print ProductEntry()

<tr><th><label for="id_sku">SKU:</label></th><td><input type="text" name="sku" i

d="id_sku" /></td></tr>

<tr><th><label for="id_description">Description:</label></th><td><textarea id="i

d_description" rows="10" cols="40" name="description"></textarea></td></tr>

<tr><th><label for="id_price">Price:</label></th><td> $ <input type="text" name="

price" id="id_price" /> </td></tr>

<tr><th><label for="id_tax">Tax:</label></th><td> <input type="text" name="tax" i

d="id_tax" /> % </td></tr>

从发布的数据中获取值

由于小部件都与处理 HTML 有关,并且值是使用 HTML 指定的格式提交给服务器的,在 HTML 元素规定的结构中,小部件提供了在传入数据和数据映射到的字段之间进行转换的额外功能。这不仅将字段与 HTML 输入如何工作的细节隔离开来,也是管理使用多个 HTML 输入的小部件的唯一方法,并且允许小部件在 HTML 输入没有提交任何内容的情况下填充默认值,如None

负责这个任务的小部件方法是value_from_datadict(),除了标准的self之外,它还接受三个参数。

  • data—提供给表单构造器的字典,通常为request.POST
  • files—传递给表单构造器的文件,使用与request.FILES相同的格式
  • name—小部件的名称,实际上就是字段的名称加上添加到表单中的任何前缀

该方法使用所有这些信息来检索从浏览器提交的值,进行任何必要的更改,并返回适合字段使用的值。这应该总是返回一个值,如果找不到合适的值,默认为None。默认情况下,所有 Python 函数都返回None,如果它们不返回其他任何东西的话,所以只要确保value_from_datadict()不引发任何异常,就很容易遵循这条规则,但是为了可读性,最好总是显式返回None

跨多个小部件拆分数据

由于小部件是字段和 HTML 之间的桥梁,它们对使用什么 HTML 以及如何向字段报告有很大的控制权。事实上,以至于有可能将一个单独的字段分割成多个 HTML 字段控件。由于render()value_from_datadict()钩子放置在流程中的位置,这甚至可以在现场不知道的情况下完成。

具体如何工作很大程度上取决于小部件将使用什么样的 HTML 输入,但总体思路很简单。一个字段将其值传递给小部件的render()方法,该方法将其分解成多个 HTML 输入,每个输入包含一部分原始值。一个例子是为DateTimeField的每个日期和时间部分设置一个单独的文本框。

然后,当小部件通过它的value_from_datadict()方法接收回数据时,它将这些片段重新组合成一个值,然后将这个值返回给字段。无论小部件做什么,字段都不需要处理多个值。

不幸的是,这都要求每个小部件负责所有的 HTML 标记,并在收到值时重新组装。有时候,简单地组合两个或更多现有的字段,依靠它们的小部件来完成这项工作也是一样有用的。因为有一个实用程序来帮助解决这个问题非常方便,所以 Django 提供了一个。

准确地说,Django 提供了两个实用程序:一个字段MultiValueField和一个小部件MultiWidget,它们被设计为协同工作。就其本身而言,它们在现实世界中并不十分有用。相反,它们提供了大量必要的特性,同时允许子类填充特定用例的细节。

在字段方面,在清理数据时,MultiValueField通过对组成组合的每个单独的字段进行验证来处理细节。它留给子类的唯一两件事是定义哪些字段应该合并,以及它们的值应该如何压缩成适合其他 Python 代码使用的单个值。例如,在 Django 中,SplitDateTimeField组合了一个DateField和一个TimeField,并将它们的值压缩到一个单独的datetime对象中。

定义应该使用哪些字段的过程很简单,在新字段类的__init__()方法中处理。所要做的就是用应该组合的字段实例填充一个元组。然后,简单地将这个元组作为第一个参数传递给父类的__init__()方法,该方法从那里处理其余的部分。这使得特定字段的方法定义非常简单,通常只有几行。

压缩由这些多个字段生成的值发生在compress()方法中。除了通常的self之外,这还需要一个值,?? 是一个值序列,应该组合成一个本地 Python 值。然而,内部发生的事情可能会更复杂一些,因为有一些情况需要考虑。

首先,对于字段的任何部分,可能根本没有提交任何值,这意味着传入的数据将是一个空列表。默认情况下,字段是必需的,在这种情况下,在调用compress()之前会抛出一个异常。如果一个字段用required=False声明,这是一个非常可能的场景,在这种情况下,该方法应该返回None

此外,很有可能只提交一部分值,因为它被分割在多个 HTML 输入中。同样,如果该字段是必需的,这是自动处理的,但是如果该字段是可选的,compress()仍然必须做一些额外的工作,以确保如果任何值被提交,所有的值都会被提交。这通常通过对照标准的EMPTY_VALUES元组检查值序列中的每一项来处理,该元组也位于django.forms.fields。包含空值的字段的任何部分都应该引发一个异常,通知用户字段的哪个部分缺少值。

然后,如果所有的值都被提交并且是有效的,compress()执行它真正的工作,在处理表单时返回一个适合 Python 使用的值。这个返回值的确切性质将完全取决于所创建的字段的类型,以及它的预期用途。考虑以下字段示例,该字段接受纬度和经度坐标作为单独的小数,并将它们组合成一个简单的元组。

from django.forms import fields

class LatLonField(fields.MultiValueField):

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

flds = (LatitudeField(), LongitudeField())

super(LatLonField, self).__init__(flds, *args, **kwargs)

def compress(self, data_list):

if data_list:

if data_list[0] in fields.EMPTY_VALUES:

raise fields.ValidationError(u'Enter a valid latitude.')

if data_list[1] in fields.EMPTY_VALUES:

raise fields.ValidationError(u'Enter a valid longitude.')

return tuple(data_list)

return None

解决了字段方面的问题后,下一步是创建一个小部件来分别捕获这两个元素。因为想要显示的只是两个文本框,所以让定制小部件由两个TextInput小部件简单组合是有意义的,这解决了识别要使用的小部件的第一个挑战。base MultiWidget在呈现输出和从传入数据中检索值方面做得很好,所以剩下的唯一挑战是将单个压缩值转换成由各个小部件呈现的值列表。

如您所料,与字段的compress()方法相对应的是小部件的decompress()方法。它的签名非常相似,只接受一个值,但它的任务是将该值分成尽可能多的小部件来呈现它们。通常,这是从单个值中取出一些比特和片断,并把它们放入一个序列中,比如一个元组或一个列表。由于前面显示的LatLonField直接将其值输出为一个元组,所以剩下的唯一事情就是提供一个空值元组(如果没有提供的话)。

from django.forms import fields, widgets

class LatLonWidget(widgets.MultiWidget):

def __init__(self, attrs=None):

wdgts = (widgets.TextInput(attrs), widgets.TextInput(attrs))

super(LatLonWidget, self).__init__(wdgts, attrs)

def decompress(self, value):

return value or (None, None)

class LatLonField(fields.MultiValueField):

widget = LatLonWidget

# The rest of the code previously described

自定义表单标记

除了定义自定义小部件,还可以自定义表单本身如何呈现为 HTML。与前面的例子不同,在 Django 的模板语言中使用了下面的技术,这使得针对单个表单进行修改变得更加容易。

可以定制的最明显的东西是实际的<form>元素,因为 Django 表单根本不输出它。这主要是因为没有办法假设表单应该使用 GET 还是 POST,以及应该发送到哪个 URL。任何需要提交回服务器的表单都需要手动指定,因此这是一些专门化的绝佳机会。例如,当使用包含一个FileField的表单时,<form>元素需要包含一个属性,比如enctype="multipart/form-data"

除了表单的提交行为之外,需要配置的一个常见内容是使用级联样式表(CSS)来呈现表单。用 CSS 引用元素有很多方法,但最有用的两种方法是分配一个 ID 或一个类,这两种方法通常都放在<form>元素本身上。因为必须定义该元素,所以添加这些额外的属性也很容易。

此外,根据站点整体外观的实现方式,还经常需要配置表单字段的显示方式。不同的站点可能会使用表格、列表甚至简单的段落来呈现表单,所以 Django 试图尽可能简单地适应这些不同的场景。

在模板中输出表单时,有几种方法可以选择使用哪种输出格式。默认情况下,as_table将每个字段包装在一行中,适合在标准表中使用,而as_ul()将字段包装在列表项中,as_p()将字段包装在段落中。然而,这些都没有输出所有字段周围的任何类型的元素;这就留给了模板,这样就可以添加额外的属性,比如 CSS 引用的 id 和类,就像 form 元素一样。

虽然提供的这三种方法对于它们自己的目的是有用的,但是它们并不一定适合每种情况。为了与 DRY 保持一致,它们实际上都是围绕一个公共方法定制的包装器,该方法将任何类型的标记包装在表单的所有字段周围。这个常见的方法_html_output()不应该直接从表单外部调用,但是非常适合由另一个为更具体的目的而设计的定制方法使用。它有许多参数,每个参数指定 HTML 输出的不同方面。

  • normal_row—用于标准行的 HTML。它被指定为将接收字典的 Python 格式字符串,因此这里可以放置几个值:errorslabelfieldhelp_text。这些应该是不言自明的,除了field实际上包含由字段的小部件生成的 HTML。
  • error_row—用于仅包含错误消息的行的 HTML,主要用于与特定字段无关的表单级错误。根据列表末尾描述的errors_on_separate_row选项,它还用于配置为在独立于字段本身的行上显示字段错误的表单。它也是一个 Python 格式的字符串,带有一个未命名的参数,即要显示的错误。
  • row_ender—用于标识行尾的标记。由于前面的行必须直接指定它们的结尾,所以不用将它追加到行中,而是将任何隐藏字段插入到最后一行,就在它的结尾之前。因此,始终确保以下情况成立:normal_row.endswith(row_ender)
  • help_text_html—写出帮助文本时使用的 HTML。这个标记将直接放在小部件之后,并将帮助文本作为这个格式字符串的一个未命名参数。
  • errors_on_separate_row—一个布尔值,指示在呈现字段本身之前是否应使用error_row呈现字段错误。这不会影响传递给normal_row的值,所以如果表单希望错误出现在单独的行上,一定要将错误排除在格式字符串之外。否则,错误将被打印两次。

访问单个字段

除了能够在 Python 中定制表单的整体标记之外,在表单本身上,直接在模板中指定表单的标记也非常简单。这样,表单尽可能地可重用,同时仍然允许模板对呈现的标记进行最终控制。

使用第二章中描述的技术,表单对象是可迭代的。这意味着模板可以使用for block 标签简单地遍历它们,每次迭代都是表单上的一个字段,它已经被绑定到一个值。然后,这个绑定的字段对象可以用来显示字段的各个方面,在任何对模板最有意义的标记中。它有很好的属性和方法选择来帮助这个过程。

  • field—原始字段对象及其所有相关属性
  • data—绑定到字段的当前值
  • errorsErrorList(如下一节所述)包含该字段的所有错误
  • is_hidden—一个布尔值,指示默认小工具是否为隐藏输入
  • label_tag()—HTML<label>元素及其内容,用于字段
  • as_widget()—字段的默认呈现,使用为其定义的小部件
  • as_text()—使用基本TextInput而不是自己的小部件呈现的字段
  • as_textarea()—使用Textarea而不是为其定义的小部件呈现的字段
  • as_hidden()—使用隐藏输入而不是任何可见小部件呈现的字段

自定义错误的显示

默认情况下,用于显示错误的标记由一个名为ErrorList的特殊 Python 类指定,它位于django.forms.util。这就像一个标准的 Python 列表,除了它有一些额外的方法以 HTML 的表单输出它的值。特别是,默认情况下,它有两个方法,as_ul()as_text(),分别将错误输出为无序列表或未加修饰的文本。

通过创建一个定制的错误类,作为ErrorList的子类,很容易在显示错误时覆盖这些方法来提供定制的标记。该标记包括任何包含元素,如<ul>,因为无论是作为默认标记的一部分,还是通过直接访问字段的errors属性,整个标记都将被放置在显示字段错误的地方。

默认情况下,as_ul()方法用于呈现错误,尽管希望进行进一步定制的模板可以调用对模板最有意义的方法。事实上,可以添加全新的方法,甚至可以通过覆盖__unicode__()方法来覆盖默认使用的方法。模板也可以简单地遍历列表中的错误,并根据情况用任何有意义的标记包装每个错误。

编写一个定制的ErrorList子类是不够的;它还必须以某种方式传递到表单中,以确保它被使用。这也很简单:只需将自定义类作为error_class参数传递给表单的构造函数。

除了显示单个字段的错误,表单的clean()方法还允许显示整个表单验证失败的错误。在模板中显示它需要访问表单的non_field_errors()方法。

应用技术

虽然 Django 的表单主要是为处理相当常见的用户输入需求而设计的,但是也可以让它们做一些复杂的跑腿工作。它们既可以单独使用,也可以成组使用,以进一步扩展用户界面。几乎任何形式的用户输入都可以用 Django 表单来表示;以下只是可用内容的一个示例。

挂起和恢复表单

表单通常是用来一次接收所有的输入,处理输入并相应地运行。这是一个一次性的循环,表单必须重新显示的唯一原因是显示验证错误,允许用户修复错误并重新提交。如果用户需要暂时停止处理表单,稍后再回来,这意味着从头开始。

虽然这是普遍接受的方法,但对于复杂的表单或用户可能需要提供需要时间收集的信息(如税务信息)的表单来说,这也是一种负担。在这些情况下,如果能够以部分填充的状态保存表单,并在以后的某个时间点返回该表单,将会更加有用。这不是表单通常的工作方式,所以显然有一些工作要做,但这真的没有那么难。

因为表单被声明为类,所以没有理由违反这一假设,此后开发的类将可以作为父类使用,就像forms.Form一样。事实上,从所有的意图和目的来看,它应该是标准类的替代物,简单地给它的子类注入额外的功能。考虑在properties申请中提供房子的以下表格,这通常不会被轻易接受。通过允许表单被挂起并在以后恢复,用户可以在承诺这样的投资之前花必要的时间来查看报价。

from django import forms

from django_localflavor_us import forms as us_forms

from pend_form.forms import PendForm

class Offer(PendForm):

name = forms.CharField(max_length=255)

phone = us_forms.USPhoneNumberField()

price = forms.IntegerField()

注意,除了切换到PendForm之外,这是像任何其他标准 Django 表单一样定义的。这一简单改变的优点将在下面的章节中描述,这些章节概述了一个新的pend_form应用。

为以后存储值

为了保存处于部分完成状态的表单,它的当前值必须以某种方式存储在数据库中。它们还必须与字段名相关联,以便以后可以用来重新创建表单。这听起来像是动态模型的工作,它可以根据表单的定义自动创建,以有效地存储值。然而,由于一些原因,它们不适合这个用例。

首先,表单字段没有直接等价的模型字段。因为动态模型必须填充与表单字段包含相同数据的字段,所以必须有某种方法来基于表单字段确定模型字段。模型字段确实定义了可以与它们一起使用的表单字段,但不是相反。

从技术上讲,可以手动提供表单域到模型域的映射,这样无论如何都可以创建这样的模型。这也存在一些问题,因为它不能支持自定义表单域。本质上,任何不存在于映射中表单字段都没有匹配的模型字段,这种技术就会失败。

此外,在基于表单字段类型的模型字段中存储字段值需要首先将这些值转换为 Python 对象,这意味着它们都必须是有效值。应该可以挂起一个表单,即使有无效的值,以便以后可以更正。如果必须用特定的数据类型(包括数据验证或类型检查)将值填充到模型字段中,这是完全不可能的。

相反,我们可以相信,所有表单数据在提交回服务器时都是以字符串的形式到达的。作为表单验证过程的一部分,必须将这些字符串转换为原生 Python 对象,因此字符串本身是从提交的表单中获取实际原始数据的最后机会。更好的是,由于它们都是字符串,Django 提供了一种简单的方法来存储它们以备后用:TextField。一个TextField是必要的,因为不同的表单值提供不同长度的数据,其中一些可能会超过CharField的 255 个字符的限制。

有了存储值的可靠方法,下一步就是确定数据库中还必须存储哪些信息,以便重新构建表单。显然,应该包括字段的名称,这样就可以将值放回正确的位置。此外,由于不同的表单可能有不同的结构,具有不同数量的字段,因此最好在数据库中为每个字段的值指定自己的行。这意味着需要有一种方法将字段作为表单的一部分保存在一起。

这里的技巧是表单没有唯一的标识符。毕竟,通常不希望它们存在于特定的请求/响应周期之外,除了验证更正,在验证更正中,整个表单作为新请求的一部分被重新提交。根本没有内置的方法来标识表单的实例,所以必须使用不同的方法。

识别这种复杂结构的一种非常常见的方法是根据数据创建一个散列。虽然不能保证散列是唯一的,但对于大多数目的来说,它们已经足够接近了,而且有些东西可以和散列一起包含,以获得更好的唯一性。

在表单的情况下,这个哈希可以从完整的字段数据集合中获取,因此任何名称或值的更改都会导致数据产生的哈希发生变化。可以与哈希一起存储的另一条信息是表单的导入路径,如果有多个表单具有相同的字段集合,这可以区分多组数据。

现在有一些信息需要存储,考虑它们应该如何相互关联。这里本质上有两个层次:表单和它的值。这可以看作是两个独立的模型,通过标准的外键关系将多个值关联到一个表单。表单端包含表单的路径及其所有值的散列,而值端包含每个字段的名称和值,以及对它所属表单的引用。

pend_form应用的models.py模块如下所示:

class PendedForm(models.Model):

form_class = models.CharField(max_length=255)

hash = models.CharField(max_length=32)

class PendedValue(models.Model):

form = models.ForeignKey(PendedForm, related_name='data')

name = models.CharField(max_length=255)

value = models.TextField()

这个简单的结构现在能够存储任何形式的任何数量的数据。如果应用需要对表单数据进行复杂的查询,效率可能会很低,但是因为它只是用来一次性保存和恢复表单的内容,所以它会工作得很好。

既然已经有了包含表单数据的模型,就需要有一种方法来实际存储这些数据,以便以后检索。幸运的是,表单只是标准的 Python 类,所以只需编写一个额外的方法来直接处理这项任务就足够简单了。然后,当编写需要这种能力的特定表单时,它可以简单地继承下面的表单,而不是通常的forms.Form。这被放在我们的pend_form应用的一个新的forms.py模块中。

try:

from hashlib import md5

except:

from md5 import new as md5

from django import forms

from pend_form.models import PendedForm

class PendForm(forms.Form):

@classmethod

def get_import_path(cls):

return '%s.%s' % (cls.__module__, cls.__name__)

def hash_data(self):

content = ','.join(%s:%s' % (n, self.data[n]) for n in self.fields.keys())

return md5(content).hexdigest()

def pend(self):

import_path = self.get_import_path()

form_hash = self.hash_data()

pended_form = PendedForm.objects.get_or_create(form_class=import_path

hash=form_hash)

for name in self.fields:

pended_form.data.get_or_create(name=name, value=self.data[name])

return form_hash

注意这里对get_or_create()的自由使用。如果一个表单的实例已经存在,并且具有完全相同的值,那么将整个表单保存两次是没有意义的。相反,它只是依赖于这样一个事实,即前一个副本在功能上是相同的,所以它对两者都适用。

重构一个表单

现在,表单可以不经过完全处理甚至验证就放在数据库中,如果以后用户不能检索它们来继续使用它们,它们的用处仍然有限。数据以这样一种方式存储,它可以重新组合成一种表单,剩下的就是实际这样做了。

根据定义,执行此操作的代码必须在使用表单实例之前被调用,因此看起来它必须在模块级函数中。请记住,如果需要的话,可以声明方法在类上使用,而不是在实例上使用。因为这里的目标是将所有这些功能封装在一个子类中,而不必担心所有的机制本身是在哪里编写的,所以一个类方法就可以很好地完成这个任务。

这个新类方法中实际发生的事情更有趣一些。为了实例化一个表单,它将一个字典作为它的第一个参数,这个参数通常只是request.POST,对所有视图都可用。当以后加载表单时,新的请求与表单完全无关,更不用说它包含适当的数据,因此必须从先前存储在数据库中的数据手动构建字典。

这些数据可能会被前面描述的表单散列以及正在使用的表单的导入路径所引用。这两条信息是从数据库中正确定位和检索所有字段值所需的全部信息。由于表单已经知道如何获得它的导入路径,由于前面描述的方法之一,剩下的就是手动提供表单的散列。这最有可能在 URL 模式中被捕获,尽管不同的应用可能有不同的方式来实现这一点。

一旦知道了散列,恢复表单的方法应该能够接受它,将它与自己的导入路径结合起来,从数据库中检索值,根据这些值填充字典,用这些值实例化表单的新副本,并返回新表单供其他代码使用。这听起来工作量很大,但比看起来容易得多。

这里需要解决的一个问题是如何实例化 Python 自己的字典。内置的dict()可以接受各种不同的参数组合,但是其中最有用的是一个 2 元组序列,每个元组包含目标字典中一个条目的名称和值。因为 QuerySets 已经返回了序列,而且像 list comprehensions 和 generator expressions 这样的工具可以很容易地基于它们创建新的序列,所以创建合适的序列是很容易的。

获取导入路径和查找保存的表单很容易,并且该对象的data属性提供了对其所有值的轻松访问。使用生成器表达式,数据的名称/值对可以很容易地传递到内置的dict()中,创建一个可以传递到表单对象的构造函数中的字典。所有这些都由法典明确规定。

@classmethod

def resume(cls, form_hash):

import_path = cls.get_import_path()

form = models.PendForm.objects.get(form_class=import_path, hash=form_hash)

data = dict((d.name, d.value) for d in form.data.all())

return cls(data)

当使用表单生成的哈希值调用这个简单的方法时,它将返回一个完整的表单对象,准备好进行验证并呈现给用户以供进一步查看。事实上,验证和演示将是这种情况下的典型工作流,让用户有机会在决定提交表单或稍后再次挂起表单之前,查看是否有任何需要添加或更正的内容。

完整的工作流程

如前所述,正常的工作流是相当标准的,在野外使用的各种表单之间几乎没有变化。通过允许挂起或恢复表单,工作流中增加了一个可选的额外步骤,这需要在视图中进行一些额外的处理。将这个新的部分添加到拼图中,整个工作流程看起来有点像这样:

Display an empty form.   User fills in some data.   User clicks Submit.   Validate data submitted by the user.   Display the form with errors.   User clicks Pend.   Save form values in the database.   Validate data retrieved from the database.   Display the form with errors.   Process the completed form.  

为了维护整个工作流,视图变得有点复杂。现在有四条不同的路径可供选择,这取决于在任何给定时间处理工作流的哪一部分。请记住,这只是采取必要的步骤来处理表单。它没有考虑特定应用所需的任何业务逻辑。

  • 用户请求一个没有任何数据的表单。
  • 用户使用 Pend 按钮发布数据。
  • 用户使用表单哈希请求表单。
  • 用户使用提交按钮发布数据。

从这里开始,典型的工作流步骤仍然适用,例如检查输入数据的有效性,并采取特定于应用功能的适当步骤。一旦在一个视图中将这些内容汇总在一起,它看起来就像这样:

from django import http

from django.shortcuts import render_to_response

from django.template.context import RequestContext

from properties import models, forms

def make_offer(request, id, template_name='', form_hash=None):

if request.method == 'POST':

form = forms.Offer(request.POST)

if 'pend' in request.POST:

form_hash = form.pend()

return http.HttpRedirect(form_hash)

else:

if form.is_valid():

# This is where actual processing would take place

else:

if form_hash:

form = forms.Offer.resume(form_hash)

else:

form = forms.Offer()

return render_to_response(template_name, {'form': form}

context_instance=RequestContext(request))

这里发生了很多事情,但很少与房子报价有关。绝大多数代码的存在只是为了管理表单在任何给定时间可能处于的所有不同状态,并且每次视图使用PendForm子类时都必须重复这些代码,这是没有效率的。

使其通用化

虽然很容易看出视图的哪些方面是重复的,因此应该将其分解成可重用的东西,但是决定如何做有点棘手。主要的问题是,特定于这个特定视图的代码部分不仅仅是一个字符串或一个数字,就像前面的大多数例子中显示的那样,而是一个代码块。

这是一个问题,因为前面的例子已经展示了如何使用通用视图来提取共性,同时允许在 URL 模式中指定具体的差异。这对于基本的数据类型,如字符串、数字、序列和字典来说很有效,但是代码的处理方式不同。这些代码不能在 URL 模式中直接指定值,而是必须在一个单独的函数中定义,然后传递给模式。

虽然这肯定是可能的,但它使 URL 配置模块变得有点麻烦,因为在每个 URL 模式块上面可能声明了许多顶级函数。Lambda 风格的函数可以解决这个问题,但是由于它们仅限于执行简单的表达式,没有循环或条件,它们会严重限制可以使用的代码类型。

一种替代方法是装饰器,它可以应用于标准函数,在包装器中提供所有必要的功能。这样,任何函数都可以用来包含实际处理表单的代码,Python 的全部功能都由它支配。这些代码也不必处理挂起或恢复表单所需的任何样板文件,因为装饰器甚至可以在视图代码本身执行之前完成所有这些工作,只需将表单作为参数传入即可。如果使用装饰器来移除样板文件,那么前面的视图可能是这样的。

from pend_forms.decorators import pend_form

@pend_form

def make_offer(request, id, form):

# This is where actual processing would take place

现在剩下的就是编写装饰器本身,封装从上一个示例中移除的功能,将它包装在将要传入的视图周围。这将被放在一个新的decorators.py模块中。

from django import http

from django.shortcuts import render_to_response

from django.template.context import RequestContext

from django.utils.functional import wraps

def pend_form(view):

@wraps(view)

def wrapper(request, form_class, template_name

form_hash=None, *args, **kwargs):

if request.method == 'POST':

form = form_class(request.POST)

if 'pend' in request.POST:

form_hash = form.pend()

return http.HttpRedirect(form_hash)

else:

if form.is_valid():

return view(request, form=form, *args, **kwargs)

else:

if form_hash:

form = form_class.resume(form_hash)

else:

form = form_class()

return render_to_response(template_name, {'form': form}

context_instance=RequestContext(request))

return wrapper

现在,所有需要做的就是设置一个 URL 配置,提供一个表单类和一个模板名。这个装饰器将处理剩下的工作,只在表单完成并提交处理时调用视图。

基于班级的方法

既然您已经看到了如何使用传统的基于函数的视图来实现这一点,请记住 Django 新的基于类的视图为许多问题提供了一种不同的方法,这也不例外。在这一章的前面,你已经看到了FormView类是如何提供你使用表单所需的大部分功能的,我们也可以扩展它来使用我们的待定功能。事实上,因为我们可以向视图类添加新方法,所以不再需要提供自定义的Form子类。可以使用代码中的任何股票表单来完成。让我们从检索一个以前挂起的表单开始。以前放在Form子类中的一些实用方法可以原封不动地在这里重用,但是我们还需要一种方法将现有的值传递到新的表单中,这对于get_form_kwargs()来说是一个完美的任务。

from django.views.generic.edit import FormView

from pend_form.models import PendedValue

class PendFormView(FormView):

form_hash_name = 'form_hash'

def get_form_kwargs(self):

"""

Returns a dictionary of arguments to pass into the form instantiation.

If resuming a pended form, this will retrieve data from the database.

"""

form_hash = self.kwargs.get(self.form_hash_name)

if form_hash:

import_path = self.get_import_path(self.get_form_class())

return {'data': self.get_pended_data(import_path, form_hash)}

else:

return super(PendFormView, self).get_form_kwargs()

# Utility methods

def get_import_path(self, form_class):

return '%s.%s' % (form_class.__module__, form_class.__name__)

def get_pended_data(self, import_path, form_hash):

data = PendedValue.objects.filter(import_path=import_path, form_hash=form_hash)

return dict((d.name, d.value) for d in data)

因为get_form_kwargs()的目的是为表单的实例化提供参数,所以我们在这里真正需要做的是检索适当的值并返回它们,而不是默认值。如果在 URL 中提供了表单散列,这将足以填充填充的表单。

还要注意的是,form_hash_name是作为一个类级别的属性包含进来的。这允许该视图的用户覆盖指示表单被挂起的参数。您所需要做的就是将它作为一个类属性提供,Django 将允许对它进行定制,返回到您定义的默认值。

下一阶段将允许用户实际保存表单值以备后用。和以前一样,这将需要在数据库中存储表单及其值,以及该信息的散列,以便以后检索。除了一些额外的实用程序,大部分工作必须在post()方法中完成,因为这是我们提交表单时的入口点。

保存表单的原始功能包括相当多的部分,其中一些可以从前面的步骤中重用。以下是保存表单以备后用所需的内容,因此我们可以在一起展示所有代码之前讨论一下。

from django.views.generic.edit import FormView

from pend_form.models import PendedForm, PendedValue

class PendFormView(FormView):

pend_button_name = 'pend'

def post(self, request, *args, **kwargs):

"""

Handles POST requests with form data. If the form was pended, it doesn't follow

the normal flow, but saves the values for later instead.

"""

if self.pend_button_name in self.request.POST:

form_class = self.get_form_class()

form = self.get_form(form_class)

self.form_pended(form)

else:

super(PendFormView, self).post(request, *args, **kwargs)

# Custom methods follow

def get_import_path(self, form_class):

return '%s.%s' % (form_class.__module__, form_class.__name__)

def get_form_hash(self, form):

content = ','.join('%s:%s' % (n, form.data[n]) for n in form.fields.keys())

return md5(content).hexdigest()

def form_pended(self, form):

import_path = self.get_import_path(self.get_form_class())

form_hash = self.get_form_hash(form)

pended_form = PendedForm.objects.get_or_create(form_class=import_path

hash=form_hash)

for name in form.fields.keys():

pended_form.data.get_or_create(name=name, value=form.data[name])

return form_hash

post()方法通常在form_valid()form_invalid()方法之间调度,但是由于挂起的表单不一定有效或无效,所以需要覆盖它以提供第三个调度选项。第三个分派由form_pended()处理,它的名字与 Django 自己的表单有效性方法一致。它完成保存表单及其相关数据的工作,重用 Django 的一些工具,以及显示挂起表单的前一次迭代。

这是所有这些看起来的样子:

from django.views.generic.edit import FormView

from pend_form.models import PendedForm, PendedValue

class PendFormView(FormView):

form_hash_name = 'form_hash'

pend_button_name = 'pend'

def get_form_kwargs(self):

"""

Returns a dictionary of arguments to pass into the form instantiation.

If resuming a pended form, this will retrieve data from the database.

"""

form_hash = self.kwargs.get(self.form_hash_name)

if form_hash:

import_path = self.get_import_path(self.get_form_class())

return {'data': self.get_pended_data(import_path, form_hash)}

else:

return super(PendFormView, self).get_form_kwargs()

def post(self, request, *args, **kwargs):

"""

Handles POST requests with form data. If the form was pended, it doesn't follow

the normal flow, but saves the values for later instead.

"""

if self.pend_button_name in self.request.POST:

form_class = self.get_form_class()

form = self.get_form(form_class)

self.form_pended(form)

else:

super(PendFormView, self).post(request, *args, **kwargs)

# Custom methods follow

def get_import_path(self, form_class):

return '{0}.{1}'.format(form_class.__module__, form_class.__name__)

def get_form_hash(self, form):

content = ','.join('{0}:{1}'.format(n, form.data[n]) for n in form.fields.keys())

return md5(content).hexdigest()

def form_pended(self, form):

import_path = self.get_import_path(self.get_form_class())

form_hash = self.get_form_hash(form)

pended_form = PendedForm.objects.get_or_create(form_class=import_path

hash=form_hash)

for name in form.fields.keys():

pended_form.data.get_or_create(name=name, value=form.data[name])

return form_hash

def get_pended_data(self, import_path, form_hash):

data = PendedValue.objects.filter(import_path=import_path, form_hash=form_hash)

return dict((d.name, d.value) for d in data)

现在,您可以像使用任何其他基于类的视图一样使用它。您需要做的就是为它提供一个表单类,并覆盖这里或FormView中指定的任何默认值。模板、按钮名称和 URL 结构可以通过简单地子类化PendFormView并从那里开始工作来定制。除此之外,您唯一需要做的就是在模板中添加一个按钮,允许用户挂起表单。

现在怎么办?

为了在现实世界中真正有用,表单必须作为 HTML 页面的一部分呈现给用户。Django 没有尝试直接在 Python 代码中生成 HTML 内容,而是提供了模板作为一种对设计人员更友好的替代方式。