Django-高级教程-四-

97 阅读1小时+

Django 高级教程(四)

原文:Pro Django

协议:CC BY-NC-SA 4.0

九、常用工具

Abstract

虽然 Django 的目标是为您构建自己的 Web 应用提供一个基础,但是该框架有自己的基础,将所有这些联系在一起。这些通用工具和特性有助于一切保持一致并更易于维护,您自己的应用也可以利用这些好处。毕竟,Django 中可用的东西对任何使用它的人都是可用的。

虽然 Django 的目标是为您构建自己的 Web 应用提供一个基础,但是该框架有自己的基础,将所有这些联系在一起。这些通用工具和特性有助于一切保持一致并更易于维护,您自己的应用也可以利用这些好处。毕竟,Django 中可用的东西对任何使用它的人都是可用的。

核心异常(django.core.exceptions)

虽然 Python 自带了一组可以在各种情况下引发的异常,但 Django 在此基础上引入了足够的复杂性,值得进一步研究。由于 Django 服务于特定的受众,这些异常更加专门化,但是它们仍然可以被核心代码之外的其他代码使用。前面已经提到了其中一些异常,因为它们更具体地处理了一个特定的 Django 特性,但是它们在其他情况下也很有用,下面几节将对此进行解释。

配置不正确

这是大多数新用户遇到的第一个异常,因为当应用的模型设置不正确、找不到视图或发生许多其他常见配置错误时,就会出现这个异常。它通常在执行manage.py validation的过程中出现,帮助用户识别和纠正发现的任何错误。

并非所有的应用都需要任何特定的配置,但是那些需要的应用可以很好地利用这个例外,因为大多数用户以前都见过它。这可能有用的常见情况包括缺少或不正确的设置、使用的 URL 配置没有附带的INSTALLED_APPS条目、自定义模型字段的参数无效以及缺少必需的第三方库。

要记住的最重要的事情是,不仅要指出出错的地方,还要指出用户应该如何修复它。通常情况下,异常表明某些代码出现了错误,并且几乎没有办法通知用户如何修复它。然而,对于一个应用的配置,有有限数量的可接受的方式来设置它,并且这个错误应该被用作将用户引向正确方向的一种方式。

例如,如果一个应用被设计为处理音频文件,它可能需要诱变剂的存在, 1 一个完善的 Python 库,用于从这样的文件中提取信息。在models.py的顶部简单导入这个库,可能会用到它,可以识别这个库是否安装正确,如果不正确,指导用户如何操作。

from django.core.exceptions import ImproperlyConfigured

try:

import mutagen

except ImportError:

raise ImproperlyConfigured("This application requires the Mutagen library.")

未使用的中间件

第七章描述了如何使用中间件来调整 HTTP 的处理方式,但是一个有趣的副作用是并不是所有的中间件都是有用的。虽然每个项目都可以选择通过MIDDLEWARE_CLASSES设置来设置那些必要的中间件,但是开发和生产之间或者不同开发人员的计算机之间仍然存在差异。

每个中间件都有能力决定其环境是否适合使用,并指出是否存在问题。当第一次需要时,中间件类被自动实例化,在第一个请求开始时,这是检查发生的地方。通过覆盖该类的__init__()方法,中间件可以立即检查是否一切都设置好了,以正常工作并做出相应的反应。

具体来说,这种反应是,如果一切正常,不做任何事情就返回,或者提高MiddlewareNotUsed。如果被抛出,Django 将总是捕捉到这个异常,并认为这个异常意味着这个类应该从每个请求所应用的中间件列表中删除。

这是一个很重要的区别,因为不能告诉 Django 完全不要使用中间件,而是由每个单独的方法来决定它是否应该执行。虽然这可以工作,但它会在每个请求上占用宝贵的时间和内存,检查一些只能确定一次的东西。通过将中间件完全排除在列表之外,它根本不会消耗任何额外的周期或内存。

返回了多个对象

当从数据库中检索对象时,通常希望只返回一行。每当查询是主键时,情况总是如此,但是在某些应用中,slugs(甚至可能是日期)可以是唯一的。Django 用 QuerySet 的get()方法支持这种情况,如果它匹配多个结果,它就可以中断应用的整个执行。

Note

Django 的SlugField几乎总是被设置为unique=True,因为它用于标识 URL 中的对象。

由于get()应该从数据库中返回一条记录,所以匹配多条记录的查询会被标记为异常MultipleObjectsReturned。其他类型的查询不会出现这种情况,因为在大多数情况下会出现多条记录。捕捉此异常在许多方面都很有用,从显示更有用的错误信息到移除意外的重复项。

ObjectDoesNotExist

get()期望的另一面是总是返回一行;也就是说,要想成功,总要有一个行。如果一个期望某行存在的查询发现没有这样的行,Django 相应地用ObjectDoesNotExist响应。它的工作方式与MultipleObjectsReturned非常相似,不同之处仅在于它被举起的位置。

简称为DoesNotExist,这个子类避免了额外的导入,因为当调用get()方法时,使用它的类通常已经被导入了。此外,通过被称为DoesNotExist并作为模型类的属性,它看起来像是完全可读的英语:Article.DoesNotExist

权限被拒绝

大多数应用都有某种形式的权限来防止对受限资源的访问;这遵循规则的模式,但有例外。规则是试图访问资源的用户将确实拥有正确的权限,因此任何没有正确权限的用户都将导致一个异常—这次是PermissionDenied。这是一种指出问题并停止处理视图其余部分的便捷方式,因为如果用户没有正确的权限,视图本身可能会做出无效的更改。

Django 还在其请求处理程序中自动捕获这个异常,将它用作返回 HTTP 403 Forbidden响应而不是通常的200 OK的指令。这将向客户端表明所提供的凭证没有足够的权限来请求资源,并且用户不应该在没有纠正这种情况的情况下重试。Django 自己的管理应用默认提供了这种行为,但也可以在任何其他应用中使用。

像其他异常一样,PermissionDenied既可以被引发也可以被捕获,尽管返回一个特殊 HTTP 响应代码的默认行为在大多数情况下是合适的。如果需要一些其他行为,很容易创建一个中间件,在process_view()阶段捕获这个异常,可能会将用户重定向到一个表单,在那里他们可以联系站点管理员,请求访问页面的权限。

from django.core.exceptions import PermissionDenied

from django.http import HttpResponseRedirect

from django.core.urlresolvers import reverse

class PermissionRedirectMiddleware(object):

def __init__(self, view='request_permission', args=None, kwargs=None):

self.view = view

self.args = args or ()

self.kwargs = kwargs or {}

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

try:

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

except PermissionDenied:

url = reverse(self.view, args=self.args, kwargs=self.kwargs)

return HttpResponseRedirect(url)

如第七章所述,在MIDDLEWARE_CLASSES中添加一个对此的引用或使用decorator_from_middleware()创建一个装饰器,当用户的权限对原始请求无效时,只需将用户重定向到另一个页面。即使没有这个异常的自定义处理程序,但是在用户不满足适当权限的任何视图中提出这个异常也是非常有用的。这种反应会导致所有其他类似情况下的处理方式,帮助你的网站尽可能的有凝聚力和一致性。

可疑操作

虽然用户通常会遵守规则,按照预期的方式使用你的网站,但任何合理的开发者都会为那些不遵守规则的人做好准备。Django 采取了许多预防措施来防止对管理接口之类的东西的未经授权的访问,并提供了装饰器来限制对应用视图的访问,但是还需要考虑一些更微妙的事情。

例如,sessions 框架需要担心用户为了劫持另一个用户的会话而修改会话 ID。这些类型的事情本身不属于身份验证或权限范围,而是用户试图规避这些通常的保护。识别这种情况何时发生很重要,这样就可以适当地处理它。

为了全面识别这些问题,Django 提供了一个SuspiciousOperation异常,这种异常可以在任何时候发生。在许多情况下,这是在同一个应用中被抛出和捕获的,但是提供它是为了能够进入应用并只使用引发异常的部分。在其他情况下,它被暴露给其他应用,以最有意义的方式进行处理。

第七章中的签名 cookies 应用是一个很好的例子,说明可疑活动可以很容易地被识别和处理。如果一个 cookie 没有有效的签名,很明显有可疑的事情发生,签名验证代码会产生一个SuspiciousOperation来表示它。因为它被设计成一个无需干预的中间件,所以它还提供了代码来捕捉这个异常,并通过在请求到达视图之前从请求中删除有问题的 cookie 来执行一个更有用的功能。但是由于其他应用有可能在中间件之外签名和验证值,所以提出一个准确识别正在发生什么的异常是有用的。

验证错误

模型和表单都可以在进一步处理数据之前验证数据,当数据无效时,Django 会抛出一个ValidationError。这在任何需要验证数据的时候都很有用,即使是在那些上下文之外。例如,如果您有一个处理 JSON 数据的应用,您可能希望提供不同于模型和表单工作方式的验证,并且除了单个字段之外,您还可能希望验证整个对象。您可以通过重用其他领域中使用的相同的ValidationError来保持与 Django 的一致性。

当实例化一个ValidationError时,可以传入一些不同种类的对象,通常是指无效的数据。通常,您会传入一个可打印的对象,比如一个字符串或者可以使用__str__()方法转换成字符串的东西。您还可以传入一个这样的对象列表,或者一个键和值都可以打印的字典,这允许您将几个错误合并到一个异常中。当在这些情况下打印ValidationError时,它的内部代码会自动执行必要的强制,以确保得到字符串。

Note

字典列表的特殊处理仅限于列表和字典。其他类型的序列和映射将被视为标准的可打印对象,而不需要查看它的单个值。此外,它只会查看第一层的数据,所以如果您将数据嵌套在一个列表中,例如,Django 只会将值从外部列表中取出;任何内部列表都将被强制转换为字符串。

视图不存在

在解析 URL 时,很可能传入的 URL 匹配 URL 配置中的模式,但不匹配任何已知的视图。这可能有多种原因,包括一个真正丢失的视图,但也经常是由于一个错误导致视图没有被正确加载。毕竟,Django 只有在 Python 可以解析视图并将其作为函数加载的情况下,才能识别出合适的视图。当这些情况发生时,Django 会抛出ViewDoesNotExist来尽可能地指出哪里出错了。

通常不需要手动捕捉这个错误或对它做任何特殊处理,因为 Django 已经尽可能好地处理了它。在开发中,使用DEBUG=True,它会显示一个有用的错误页面,详细说明尝试了哪个视图,以及一条 Python 错误消息,指出为什么不能加载它。在生产中,这种详细程度是不安全的,所以它退回到标准的 HTTP 500 错误,通知后台的管理员。

Text Modification (django.utils.text)

从本质上讲,网络是一种书面媒体,使用文本来传达绝大多数的想法。通常,这些文本是作为模板和数据库内容的组合提供的,但是在发送给用户之前,通常需要进行一些处理。在标题中使用时可能需要大写,在电子邮件中使用时需要换行,或者进行其他修改。

压缩字符串

这个简单的实用程序使用 gzip 格式压缩输入字符串。这允许您以浏览器能够在另一端解压缩的格式传输内容。

>>> from django.utils.text import compress_string

>>> compress_string('foo')

'\x1f\x8b\x08\x00s={Q\x02\xffK\xcb\xcf\x07\x00!es\x8c\x03\x00\x00\x00'

很明显,这个例子看起来没有被压缩,但这仅仅是压缩如何处理小字符串的一个假象。在这些情况下,压缩算法所需的头和簿记足以使字符串更长。当您提供一个更长的字符串时,比如一个文件或一个呈现的模板,您将在这个函数的输出中看到一个更小的字符串。

Note

如果您使用它向浏览器发送内容,您还需要发送一个头来告诉浏览器如何处理它。

Content-Encoding: gzip

压缩序列(序列)

这与compress_string()非常相似,但是它将按照提供的顺序压缩单个项目。与其简单地返回所有压缩内容的字符串,compress_sequence()实际上是一个生成器,逐段生成内容。输出中的第一项是 gzip 头,接下来是每个输入字符串的压缩版本,最后是 gzip 页脚。

>>> for x in text.compress_sequence(['foo', 'bar', 'baz']):

...     print repr(x)

...

'\x1f\x8b\x08\x00\x16={Q\x02\xff'

'J\xcb\xcf\x07\x00\x00\x00\xff\xff'

'JJ,\x02\x00\x00\x00\xff\xff'

'JJ\xac\x02\x00\x00\x00\xff\xff'

"\x03\x00\xaa'x\x1a\t\x00\x00\x00"

get_text_list(items,last_word='or ')

向用户显示项目列表有多种方式,每种方式适用于不同的情况。不要在每一行中列出每一项,用简单的英语将列表显示为逗号分隔的列表,例如“红、蓝、绿”,通常是有用的这似乎是一项艰巨的任务,但是get_text_list()大大简化了它。只需传入一个条目列表作为第一个参数,并传入一个可选的连接词作为第二个参数,它将返回一个字符串,其中包含由逗号分隔的条目,并在末尾加上连接词。

>>> from django.utils.text import get_text_list

>>> 'You can use Python %s' % get_text_list([1, 2, 3])

u'You can use Python 1, 2 or 3'

>>> get_text_list(['me', 'myself', 'I'], 'and')

u'me, myself and I'

javascript_quote(s,quote_double_quotes=False)

当将字符串写出到 JavaScript 时,无论是在源代码中还是在 JavaScript 对象表示法(JSON)、 2 的响应代码中,对于特殊字符都必须考虑某些因素。这个函数以 JavaScript 可以理解的方式正确地转义这些特殊字符,包括 Unicode 字符。

>>> from django.utils.text import javascript_quote

>>> javascript_quote('test\ning\0')

'test\\ning\x00'

normalize_newlines(文本)

当应用需要处理来自未知来源的文本内容时,很可能会在 Windows、Apple 和 Unix 风格的系统上生成输入。这些不同的平台对于使用什么字符来编码行尾有不同的标准,当应用需要对它们进行任何文本处理时,这会导致问题。给定这样的输入,normalize_newlines()寻找常见的行尾替换,并将它们全部转换成 Python 期望的 Unix 风格的\n

>>> from django.utils.text import normalize_newlines

>>> normalize_newlines(u'Line one\nLine two\rLine three\r\nLine four')

u'Line one\nLine two\nLine three\nLine four'

电话 2 数字(电话)

企业通常将电话号码作为单词来提供,以便于记忆。如果像这样的电话号码作为应用的输入提供,它们通常只有在直接显示给用户时才有用。如果应用必须将这些数字作为自动化系统的一部分使用,或者向经常打电话的员工显示这些数字,那么将它们作为原始数字而不是营销文本使用会更有用。通过用phone2numeric()传递电话号码,你可以确保你总是得到一个真实的电话号码来工作。

>>> from django.utils.text import phone2numeric

>>> phone2numeric(u'555-CODE')

u'555-2633'

资本重组(文本)

给定一个可能已经转换为小写的字符串,可能是为了搜索或其他比较,通常需要在向用户显示之前将其转换回常规的大小写混合。recapitalize()函数就是这样做的,将句尾标点符号后面的字母大写,比如句号和问号。

>>> from django.utils.text import recapitalize

>>> recapitalize(u'does this really work? of course it does.')

u'Does this really work? Of course it does. '

Caution

尽管 Django 为国际观众提供了许多功能,但recapitalize()功能只适用于基本的英文文本。其他语言中使用的标点符号可能无法正确识别,导致大写输出不正确。

slugify(值)

Slugs 是一种适合在 URL 中使用的字符串,通常是文章标题的精简版本。Slugs 由小写字母、代替空格的连字符以及缺少标点符号和其他非单词字符组成。slugify()函数接受一个文本值,并执行必要的转换,使其适合用作 URL slug。

>>> from django.utils.text import slugify

>>> slugify(u'How does it work?')

u'how-does-it-work'

智能拆分(文本)

最初是作为一种解析模板标签参数的方法开发的,smart_split()获取一个字符串并在空格处将其分开,同时仍然完整地保留引用的段落。这是为任何其他应用解析参数的好方法,因为它提供了很大的灵活性。它可以识别单引号和双引号,安全地处理转义引号,并在遇到任何引用段落的开头和结尾保持引号不变。

>>> from django.utils.text import smart_split

>>> for arg in smart_split('arg1 arg2 arg3'):

...     print arg

arg1

arg2

arg3

>>> for arg in smart_split('arg1 "arg2\'s longer" arg3'):

...     print arg

arg1

"arg2's longer"

arg3

unescape_entities(文本)

HTML 可以包含一些实体,这些实体可以更容易地表示某些国际字符和其他特殊字形,这些字符很难在大多数英文键盘上键入或使用本地英文字符编码进行传输。这些在手动编辑 HTML 时很有用,但是如果您使用像 UTF-8 这样的宽文本编码,您可以通过网络发送原始字符,而不是依赖浏览器在事后转换它们。通过将字符串传递给这个函数,任何 HTML 实体都将被转换为适当的 Unicode 码位。

>>> from django.utils.text import unescape_entities

>>> unescape_entities('“Curly quotes!”')

u'\u201cCurly quotes!\u201d'

unescape_string_literal

当编写包含撇号或引号的字符串时,您通常需要通过在它们前面放置反斜杠来转义这些字符,以避免意外地使用它们来终止字符串。因为为此目的使用了反斜杠,所以还需要对字符串中要包含的任何文字反斜杠进行转义。

通常,Python 会直接解释这些,并为您提供一个包含原始字符的字符串,没有额外的反斜杠。在某些情况下,比如在模板中,字符串不是由 Python 直接处理的,而是作为包含反斜杠的字符串传递到代码中。您可以使用unescape_string_literal()获得 Python 通常会提供的等价字符串。

>>> from django.utils.text import unescape_string_literal

>>> unescape_string_literal("'string'")

'string'

>>> unescape_string_literal('\'string\'')

'string'

换行(文本,宽度)

这将获取指定的文本,并根据需要插入换行符,以确保没有一行超出所提供的宽度。它确保不分解单词,并且保持现有的换行符不变。不过,它希望所有的换行符都是 Unix 风格的,所以如果您不能控制文本的来源,最好先运行文本,以确保它正常工作。

>>> from django.utils.text import wrap

>>> text = """

... This is a long section of text, destined to be broken apart.

... It is only a test.

... """

>>> print wrap(text, 35)

This is a long section of text

destined to be broken apart.

It is only a test.

截断文本

另一个常见的需求是截断文本以适应更小的空间。无论您是限制字数还是字符数,也无论您是否需要在截断时考虑 HTML 标签,Django 都有一个Truncator类可以完成这项工作。您可以通过简单地传递您想要截断的文本来实例化它。

>>> from django.utils.text import Truncator

为了这个例子,我们必须首先配置 Django 不要使用它的国际化系统。如果您使用的是manage.py shell,这已经为您完成了,但是如果您只是在项目之外使用 Python,您将需要对此进行配置。在实际的应用中,您不需要执行这个步骤。

>>> from django.conf import settings

>>> settings.configure(USE_I18N=False)

现在我们有了一个能够像这样处理文本转换的环境。

>>> truncate = Truncator('This is short, but you get the idea.')

从那里,实际的操作由任何可用的方法提供。

Truncator.chars(num,truncate='…')

此方法将文本限制为不超过所提供的数量,而不考虑原始文本中的任何单词或句子。

>>> truncate.chars(20)

u'This is short, bu...'

Note

结果字符串有 20 个字符长,包括省略号。如您所见,truncate参数可以改变字符串末尾使用的字符数,而chars()在决定保留多少字符串时会考虑到这一点。对于truncate值的不同设置将改变原始字符串的剩余部分。这个行为是chars()方法独有的。

truncate参数指定结果字符串应该如何格式化。默认情况下,它会在后面附加三个句点,起到省略号的作用。您可以提供任何其他字符串,它将被追加到截断的字符串,而不是句点。

>>> truncate.chars(20, truncate='--')

u'This is short, but--'

您还可以通过使用名为truncated_text的占位符来指定格式字符串,从而更加灵活地控制文本输出。这允许您将截断的文本放在字符串中的任何位置。

>>> truncate.charts(20, truncate='> %(truncated_text)s...')

u'> This is short, ...'

Truncator.words(num,truncate='…',html=False)

此方法将字符串的长度限制为指定的字数,而不是单个字符。这通常是更可取的,因为它避免了在单词中间断开。因为单词可以有不同的长度,所以产生的字符串比使用chars()时更难预测。

>>> truncate.words(5)

u'This is short, but you...'

>>> truncate.words(4, truncate='--')

u'This is short, but--'

还要注意,truncate参数不再改变字符串被截断的方式。您的文本将减少到指定的字数,之后将应用truncate参数。

html参数控制该方法是否应该避免将 HTML 属性作为单独的单词,因为它们由空格分隔。对于普通文本,默认的False更好,因为它需要做的工作更少,但是如果你要输出一个可能包含 HTML 标签的字符串,你会想要使用True来代替。

>>> truncate = Truncator('This is <em class="word">short</em>, but you get the idea.')

>>> truncate.words(4)

u'This is <em class="word">short</em>,...'

>>> truncate.words(4, html=True)

u'This is <em class="word">short</em>, but...'

>>> truncate.words(3)

u'This is <em...'

>>> truncate.words(3, html=True)

u'This is <em class="word">short</em>,...'

使用html=True的另一个优点是它会小心地关闭标签,否则当字符串被截断时标签会保持打开。

>>> truncate = Truncator('This is short, <em>but you get the idea</em>.')

>>> truncate.words(5)

u'This is short, <em>but you...'

>>> truncate.words(5, html=True)

u'This is short, <em>but you...</em>'

数据结构(django.utils.datastructures)

在处理任何复杂的系统时,经常需要处理非常特殊结构的数据。这可能是一个项目的顺序列表,一个键到值的映射,一个类别的层次树,这些的任意组合或者其他完全不同的东西。虽然 Django 并没有假装为应用可能需要的每种数据安排提供对象,但是框架本身需要一些特定的东西,并且这些东西也对基于它的所有应用可用。

字典包装

这是一个为特定目的设计的数据结构的好例子,在现实世界中可能有其他用途。这种特殊类型的字典的目标是,如果请求的键与一个基本标准匹配,就提供一种在检索时转换值的方法。

在实例化字典时,可以提供一个函数和一个前缀字符串。每当您请求一个以前缀开头的键时,DictWrapper将去掉前缀,并在返回它之前对相关的值调用提供的函数。除此之外,它就像一个标准的字典。

>>> from django.utils.datastructures import DictWrapper

>>> def modify(value):

...     return 'Transformed %s' % value

>>> d = DictWrapper({'foo': 'bar'}, modify, 'transformed_')

>>> d['foo']

'bar'

>>> d['transformed_foo']

'Transformed: bar'

不变列表

列表和元组之间的区别通常用它们的内容来描述。列表可以包含任意数量的对象,所有对象都应该是相同的类型,这样您就可以遍历它们,并像处理所有其他项目一样处理每个项目。本质上,列表是值的集合。

另一方面,元组本身是一个整数值,其中的每一项都有特定的含义,由它的位置来表示。任何特定类型的元组都有相同数量的值。例如,空间中的三维点可以由包含 x、y 和 z 坐标的 3 项元组来表示。每个这样的点都有相同的三个值,并且总是在相同的位置。

两者之间的一个关键技术区别是元组是不可变的。为了改变一个元组,实际上需要用改变后的值创建一个新的元组。这种不变性是一个有用的安全网,可以确保序列不会在您的控制下发生变化,而且它还可以略微提高性能,因为元组是更简单的数据结构。但是,它们并不打算用作不可变的列表。

对于那些既有列表语义,又想获得不变性好处的情况,Django 提供了一个替代方案:ImmutableList。它是 tuple 的一个子类,但是它也包含了列表中所有可变的方法。唯一的区别是这些方法都引发一个AttributeError,而不是改变值。这是一个微妙的区别,但它确实给你机会利用元组,同时仍然使用列表的语义。

MergeDict

当需要一起访问多个字典时,典型的方法是创建一个新字典,其中包含这些字典的所有键和值。这适用于简单的应用,但是可能有必要保持底层字典的可变性,以便对它们的更改反映在组合字典中。下面展示了标准字典是如何分解的。

>>> dict_one = {'a': 1, 'b': 2, 'c': 3}

>>> dict_two = {'c': 4, 'd': 5, 'e': 6}

>>> combined = dict(dict_one, **dict_two)

>>> combined['a'], combined['c'], combined['e']

(1, 4, 6)

>>> dict_one['a'] = 42

>>> combined['a']

1

这说明了一种简单的合并字典的方法,利用了dict()可以接受字典和关键字参数的事实,将它们合并成一个新的字典。多亏了在第二章中详细描述的**语法,这使得它成为了一种获得期望结果的便捷方式,但是这个例子也显示了它从哪里开始失败。

第一,它只接受两本词典;添加更多将需要调用dict()不止一次,每次添加一个新的字典。也许更重要的是,对源字典的更新不会反映在组合结构中。清楚地说,这通常是一件好事,但是在像组合了request.GETrequest.POSTrequest.REQUEST这样的情况下,对底层字典所做的更改也应该显示在组合输出中。

为了方便这一切,Django 使用了自己的类,该类在许多方面都像字典一样,但是在后台透明地访问多个字典。通过这种方式可以访问的词典数量没有限制。在实例化对象时,只需根据需要提供尽可能多的字典,它们将按照提供的顺序被访问。因为它存储对真实字典的引用并访问它们,而不是创建一个新的字典,所以对底层字典的修改会反映在组合中。

>>> from django.utils.datastructures import MergeDict

>>> dict_one = {'a': 1, 'b': 2, 'c': 3}

>>> dict_two = {'c': 4, 'd': 5, 'e': 6}

>>> combined = MergeDict(dict_one, dict_two)

>>> combined['a'], combined['c'], combined['e']

(1, 3, 6)

>>> dict_one['a'] = 42

>>> combined['a']

42

因为键在内部字典中的检查顺序与它们被传递给MergeDict的顺序相同,所以在第二个例子中combined['c']3,而在第一个例子中是4

多元价值预测

在另一个极端,有时让字典中的每个键潜在地引用多个值是有用的。由于 Web 浏览器将数据作为一系列名称/值对发送到服务器,没有任何更正式的结构,因此一个名称可能会被发送多次,每次可能会有不同的值。字典被设计成将一个名字映射到一个值,所以这是一个挑战。

从表面上看,解决方案似乎很简单:只需在每个键下存储一个值列表。再深入一点,一个问题是绝大多数应用对每个键只使用一个值,所以总是使用一个列表会让每个人做更多的工作。相反,大多数情况下应该能够使用单个键来访问单个值,同时仍然允许那些需要它们的应用访问所有的值。

Django 使用MultiValueDict来处理这种情况,基于大多数其他框架在这种情况下的默认行为。默认情况下,访问一个MultiValueDict中的键会返回以该名称提交的最后一个值。如果所有的值都是必需的,那么一个单独的getlist()方法可以返回完整的列表,即使它只包含一项。

>>> from django.utils.datastructures import MultiValueDict

>>> d = MultiValueDict({'a': ['1', '2', '3'], 'b': ['4'], 'c': ['5', '6']})

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

('3', '4', '6')

>>> d.getlist('a')

['1', '2', '3']

>>> d.getlist('b')

['4']

>>> d.getlist('c')

['5', '6']

Caution

这不会自动将每个值强制转换为一个列表。如果为任何值传入单个项,该值将按预期返回,但getlist()将返回传入时的原始值。这意味着getlist()将只返回单个项目,而不是包含单个项目的列表。

>>> d = MultiValueDict({'e': '7'})

>>> d['e']

'7'

>>> d.getlist('e')

'7'

排序直接

Python 字典的一个更难理解的特性是它们在技术上是无序的。检查各种各样的字典可能看起来会产生一些模式,但是它们是不可靠的,因为它们在 Python 实现之间会有所不同。这有时会是一个相当大的绊脚石,因为很容易意外地依赖字典的隐式排序,只在你最意想不到的时候发现它从你的下面改变了。

需要一个可靠有序的字典是很常见的,这样 Python 代码和模板在遇到字典时就能知道会发生什么。在 Django 中,这个特性是由SortedDict提供的,它跟踪它的键被添加到字典中的顺序。利用该功能的第一步是传入一个有序的键/值对序列。然后保留这个顺序,以及任何后续键被赋予新值的顺序。

>>> from django.utils.datastructures import SortedDict

>>> d = SortedDict([('c', '1'), ('d', '3'), ('a', '2')])

>>> d.keys()

['c', 'd', 'a']

>>> d.values()

['1', '3', '2']

>>> d['b'] = '4'

>>> d.items()

[('c', '1'), ('d', '3'), ('a', '2'), ('b', '4')]

功能实用程序(django.utils.functional)

Python 将函数视为一级对象。它们有一些明显不同于其他对象的属性和方法,但是核心语言对待它们就像对待其他对象一样。这种处理允许函数的一些非常有趣的用法,比如在运行时设置属性和在一个列表中组装函数,以便按顺序执行。

cache _ property(func)

属性是最简单的描述符之一,因为在访问属性时,通常情况下只需调用一个方法。如果它依赖于其他属性或外部因素,这对于确保其值始终是最新的非常有用。每次访问属性时,都会调用方法并产生一个新值。

>>> class Foo(object):

...     @property

...     def bar(self):

...         print('Called the method!')

...         return 'baz'

...

>>> f = Foo()

>>> f.bar

Called the method!

'baz'

>>> f.bar

Called the method!

'baz'

不过,有时候,你有一个不会改变的价值,但它的生产成本可能会很高。如果不需要,你不想产生这个值,但是你也不想产生一次以上。为了解决这种情况,您可以使用@cached_property装饰器。将此应用于方法将导致该方法在第一次访问属性时被调用,但它会将结果存储在对象上,以便每次后续访问都将只获得存储的值,而不是再次调用该方法。

>>> from django.utils.functional import cached_property

>>> class Foo(object):

...     @cached_property

...     def bar(self):

...         print('Called the method!')

...         return 'baz'

...

>>> f = Foo()

>>> f.bar

Called the method!

'baz'

>>> f.bar

'baz'

咖喱(func)

通常有必要采用一个带有复杂参数集的函数并对其进行简化,这样调用它的代码就不需要总是提供所有的参数。最明显的方法是尽可能提供默认值,如第二章所述。然而,在许多情况下,在编写函数时没有合理的默认值,或者默认值可能不适合情况的需要。通常,您可以使用您需要的任何参数值来调用该函数,这对于大多数需求来说都很好。

不过,有时函数的参数是在与实际需要调用时不同的时间确定的。例如,传递一个函数以便以后使用是很常见的,无论是作为实例方法还是回调,甚至是模块级的函数。当使用的函数接受的参数多于以后提供的参数时,必须提前指定剩余的参数。

从 Python 2.5 开始,这个功能通过functools.partial函数在标准库中提供。虽然与 Python 捆绑在一起很方便,但它只对后续安装有用,而 Django 支持已经存在很久的 Python 版本。相反,Django 在django.utils.functional.curry提供了自己的实现。

curry 的第一个参数总是一个 callable,它不会马上被调用,而是被藏起来以后用。除此之外,所有的位置和关键字参数也被保存,并在适当的时候应用于提供的 callable。返回值是一个新的函数,当被调用时,它将使用原始参数和随后调用中提供的任何参数来执行原始的可调用函数。

>>> from django.utils.functional import curry

>>> def normalize_value(value, max_value, factor=1, comment='Original'):

...     """

...     Normalizes the given value according to the provided maximum

...     scaling it according to factor.

...     """

...     return '%s (%s)' % (float(value) / max_value * factor, comment)

>>> normalize_value(3, 4)

'0.75 (Original)'

>>> normalize_value(3, 4, factor=2, comment='Double')

'1.5 (Double)'

>>> percent = curry(normalize_value, max_value=100, comment='Percent')

>>> percent(50)

'0.5 (Percent)'

>>> percent(50, factor=2, comment='Double')

'1.0 (Double)'

>>> tripled = curry(normalize_value, factor=3, comment='Triple')

>>> tripled(3, 4)

'2.25 (Triple)'

惰性(func,*resultclasses)

根据环境的不同,有些值可以用不同的方式表示。一个常见的例子是可翻译的文本,其中的内部值通常是英语,但也可以使用用户选择的不同语言来表示。具有这种行为的对象被认为是懒惰的,因为它们不会立即被填充,而是在以后需要的时候被填充。

您可以使用这个lazy()函数创建一个惰性对象。它接受的主要参数是一个可以产生最终值的函数。该函数不会被立即调用,而是简单地存储在一个Promise对象中。然后这个承诺可以在整个框架代码中传递,比如 Django,它不关心对象是什么,直到它最终到达关心对象的代码。当试图访问承诺时,将调用该函数并返回值。事实上,每次访问对象时都会调用该函数,每次都有机会使用环境来改变返回值。

这个过程中有趣的部分是承诺如何决定它是否被访问。当简单地传递一个对象时,该对象本身无法访问保存对它的引用的代码。但是,当它的属性被访问时,它可以做出反应。因此,当您的代码试图访问一个承诺的属性时,这将成为生成承诺值的新表示的线索。

lazy()函数的其余参数有助于这部分过程。您指定的resultclasses应该包含您的函数可以返回的所有不同类型的对象。每个类都有一组属性和方法,promise 可以监听这些属性和方法。当其中任何一个被访问时,promise 将调用它的存储函数返回一个新值,然后返回最初请求的那个值的属性。

如果没有例子,这可能特别难以理解。翻译是一个常见的例子,但是另一个有用的例子是在处理日期和时间时。具体来说,社交网络通常会根据事件发生的时间来显示特定事件的日期和时间,而不是绝对日期。Django 有一个实用程序可以立即计算这个值,但是您也可以用它来创建一个 lazy 对象。然后,每次显示时,您的代码可以根据需要计算时差。

就像我们之前看到的,这个例子要求我们首先配置 Django 不要使用它的国际化系统,如果你没有使用manage.py shell

>>> from django.conf import settings

>>> settings.configure(USE_I18N=False)

现在系统被配置为使用timesince()功能。位于django.utils.timesince中,您可以简单地传入一个datedatetime对象,它将返回一个字符串,该字符串包含从现在到您传入的日期之间的时间长度的可读表示。

>>> import datetime

>>> from django.utils.timesince import timesince

>>> then = datetime.datetime.now() - datetime.timedelta(minutes=1)

>>> since = timesince(then)

>>> since

u'1 minute'

>>> print(since)

1 分钟

这就是它通常的工作方式,立即返回持续时间。然后剩下一个只有在函数被调用时才有效的字符串。惰性对象在需要的时候会像字符串一样工作,但是在需要产生值的时候会计算函数。

>>> from django.utils.functional import lazy

>>> lazy_since = lazy(timesince, str)(then)

>>> lazy_since

<django.utils.functional.__proxy__ at 0x...>

>>> print(lazy_since)

1 minute

# Wait a few minutes...

>>> print(lazy_since)

5 minutes

allow_lazy(func,* resultclasses)

这个装饰器提供了另一种处理懒惰选项的方法,就像上一节中描述的那样。大多数函数在实际对象上操作,不知道任何关于懒惰对象的延迟加载行为,并且将直接访问对象的属性。如果向这样的函数提供一个惰性对象,它会立即触发值,如果函数只是简单地转换值,这可能不是很有用。

>>> def bold(value):

...     return u'<b>%s</b>' % value

...

>>> bold(lazy_since)

u' 10 分钟'如果新的函数调用也可以是惰性的就更好了,如果你能在不改变函数代码的情况下做到这一点就更好了。这就是allow_lazy()发挥作用的地方。您可以将此应用于任何函数,这样当您调用该函数时,它将检查是否有任何传入的参数是惰性的。如果它们中的任何一个实际上是懒惰对象,包装器将介入并返回一个由原始函数支持的新的懒惰对象。否则,原始函数将立即在提供的非惰性参数上运行。

>>> from django.utils.functional import allow_lazy

>>> lazy_bold = allow_lazy(bold, str)

>>> lazy_bold(lazy_since)

<django.utils.functional.__proxy___ at 0x...>

>>> lazy_bold(since)

u'<b>1 minute</b>'

>>> print lazy_bold(lazy_since)

u'<b>2 minutes</b>

lazy_property(fget=None,fset=None,fdel=None)

属性是围绕简单属性访问包装自定义行为的一种非常有用的方式。例如,您可以使用属性按需生成属性值,或者在属性值更改时更新相关信息。然而,它们的一个潜在问题是,当它们第一次被添加到一个类中时,它们包装了特定的函数。子类可以继承每个属性的行为,但是它总是使用提供给原始装饰者的函数。子类可以覆盖属性行为的唯一方法是创建一个全新的属性,完全替换属性的每个方面。

>>> class Foo(object):

...     def _get_prop(self):

...         return 'foo'

...     prop = property(_get_prop)

...

>>> class Bar(Foo):

...     def _get_prop(self):

...         return 'bar'

...

>>> Foo().prop

'foo'

>>> Bar().prop

'foo'

为了允许子类更容易地覆盖特定的属性行为,您可以使用lazy_property()函数创建您的属性。这将自动查看哪个子类正在访问该属性,并使用您添加的任何被覆盖的函数,否则将返回到原始函数。

>>> from django.utils.functional import lazy_property

>>> class Foo(object):

...     def _get_prop(self):

...         return 'foo'

...     prop = lazy_property(_get_prop)

...

>>> class Bar(Foo):

...     def _get_prop(self):

...         return 'bar'

...

>>> Foo().prop

'foo'

>>> Bar().prop

'bar'

内存(func、cache、num_args)

当处理大量信息时,函数通常需要进行某些基本计算,其中唯一的真实变量(即,从一个调用到下一个调用发生变化的值)是传入的参数。重用第七章提到的一个术语,这个行为使得函数幂等;给定相同的参数,无论函数被调用多少次,结果都是一样的。事实上,这是该术语最初的数学含义,它被借用来与 HTTP 方法一起使用。

幂等性在人类和计算机之间提供了一种有趣的分离。虽然人类可以很容易地识别函数何时是幂等的,并学会记住结果,而不是每次都继续执行该函数(还记得学习乘法表吗?),电脑就没那么幸运了。他们会高兴地一次又一次地使用这个函数,从来没有意识到它花费了多少不必要的时间。在数据密集型应用中,这可能是一个大问题,在这种情况下,一个函数可能需要很长时间来执行,或者用相同的参数执行数百次或数千次。

一个程序有可能走我们人类小时候学过的捷径,但这需要一点帮助。Django 通过同样位于django.utils.functionalmemoize()功能提供这种帮助。它只接受任何一个标准函数,并返回一个包装器,记录正在使用的参数,并将它们映射到函数为这些参数返回的值。然后,当这些相同的参数再次传入时,它只需查找并返回之前计算的值,而无需再次运行原始函数。

除了要调用的函数之外,memoize()还有另外两个参数,用来决定如何管理返回值的缓存。

  • cache—存储值的字典,关键字是传递给函数的参数。任何类似字典的对象都可以在这里工作,因此,例如,可以围绕 Django 的低级缓存编写一个字典包装器——在第八章中有所描述——并让多个线程、进程甚至整个机器共享同一个内存化缓存。
  • num_args—字典缓存中组合形成关键字的参数的数量。这通常是函数接受的参数总数,但如果有不影响返回值的可选参数,这个数字可能会更低。

>>> from django.utils.functional import memoize

>>> def median(value_list):

...     """

...     Finds the median value of a list of numbers

...     """

...     print 'Executing the function!'

...     value_list = sorted(value_list)

...     half = int(len(value_list) / 2)

...     if len(value_list) % 2:

...         # Odd number of values

...         return value_list[half]

...     else:

...         # Even number of values

...         a, b = value_list[half - 1:half + 1]

...         return float(a + b) / 2

>>> primes = (2, 3, 5, 7, 11, 13, 17)

>>> fibonacci = (0, 1, 1, 2, 3, 5, 8, 13)

>>> median(primes)

Executing the function!

7

>>> median(primes)

Executing the function!

7

>>> median = memoize(median, {}, 1)

>>> median(primes)

Executing the function!

7

>>> median(primes)

7

>>> median(fibonacci)

Executing the function!

2.5

>>> median(fibonacci)

2.5

NOTE ABOUT MEMOIZING ARGUMENTS

因为函数的参数将在字典中用于映射返回值,所以它们必须是可哈希的值。通常,这意味着任何东西都是不可变的,但是某些其他类型的对象也可能是可散列的。例如,如果传递的是一个列表而不是一个元组,本节描述的median()函数将会抛出一个错误。因为列表的内容可以改变,所以它们不能用作字典键。

分区(谓词,值)

这是一个简单的实用函数,根据将每个值传递给predicate函数的结果,将一个values序列分成两个列表。返回值是一个 2 元组,元组中的第一项是False响应,而第二项包含True响应。

>>> from django.utils.functional import partition

>>> partition(lambda x: x > 4, range(10))

([0, 1, 2, 3, 4], [5, 6, 7, 8, 9])

谓词应该返回TrueFalse,但是在内部partition()实际上利用了这样一个事实:当TrueFalse被用作序列的索引时,它们分别等价于10。这意味着如果你有一个已经返回10的谓词,你不需要转换它来使用TrueFalse来代替。

>>> even, odd = parittion(lambda x: x % 2, range(10))

>>> even

[0, 2, 4, 6, 8]

>>> odd

[1, 3, 5, 7, 9]

包装(功能)

第二章详细描述了 decorator,但是有一个方面在某些情况下会引起问题,因为 decorator 经常返回一个原始函数的包装器。事实上,这个包装器是一个与源文件中所写的完全不同的函数,所以它也有不同的属性。当自省函数时,如果几个函数通过同一个装饰器传递,这会导致混淆,因为它们共享相似的属性,包括它们的名字。

>>> def decorator(func):

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

...         return func(*args, **kwargs)

...     return wrapper

>>> def test():

...     print 'Testing!'

>>> decorated = decorator(test)

>>> decorated.__name__

'wrapper'

为了帮助缓解这种情况,Django 包含了 Python 自己的wraps()函数的副本,该函数是在 Python 2.5 中首次引入的。wraps()实际上是另一个装饰器,它将原始函数的细节复制到包装器函数上,所以当一切完成时,它看起来更像原始函数。只需将原始函数传递给wraps(),像使用包装器上的其他装饰器一样使用它,剩下的工作就交给它了。

>>> from django.utils.functional import wraps

>>> def decorator(func):

...     @wraps(func)

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

...         return func(*args, **kwargs)

...     return wrapper

>>> def test():

...     print 'Testing!'

>>> decorated = decorator(test)

>>> decorated.__name__

'test'

Caution

遗憾的是,wraps()无法让包装器与原函数完全一致。特别是,它的函数签名将总是反映包装函数的签名,所以试图自省修饰函数的参数可能会导致一些混乱。尽管如此,出于自动化文档和调试的目的,让wraps()更新名称和其他信息是非常有用的。

信号

大型应用的一个重要方面是知道应用的其他部分何时会发生某些事情。更好的是有能力在事件发生的瞬间做一些事情。为此,Django 包含了一个信号调度器,它允许代码广播事件的发生,同时提供一个方法让其他代码在事件发生时监听这些广播并做出相应的反应。它通过允许代码定义要调度的唯一信号来识别正在广播的事件的类型。

调度的概念和实现它的代码并不是 Django 独有的,但是它的实现是为 Web 应用的需求定制的。这个实现位于django.dispatch.dispatcher,尽管它被设计成通过简单的Signal对象使用,该对象在django.dispatch可用。Django 在很多地方使用信号,其中很多在本书的其他地方都有记录,在使用信号的地方。接下来的部分将更全面地讨论信号和调度是如何工作的,以及如何为特定事件注册侦听器。

它是如何工作的

基本过程相当简单。每一步都将在单独的章节中详细解释,但是下面的内容应该是一个很好的概述。

首先,一些 Python 代码定义了一个信号。如下一节所述,这是一个放置在可靠位置的Signal对象。该对象表示预期在某个时间点发生的事件,可能会发生多次。调度员不使用任何信号的集中登记;由您自己的代码决定在任何给定的时间使用哪个信号。

当您的代码触发了您希望其他代码知道的事件时,您的代码会向信号发送一些信息,包括表示事件来源的“sender”对象和描述事件其他细节的任何参数。信号本身只识别事件的类型;这些额外的参数描述了在特定时间发生的事情。

然后,该信号查看其注册侦听器列表,以查看是否有任何侦听器匹配所提供的信号和发送者,并依次调用它找到的每个函数,传递事件触发时该信号给出的任何参数。侦听器的注册可以在任何时候发生,当添加新的侦听器时,信号将更新其注册表,以便将来的事件将包括新的侦听器。

定义信号

信号不需要实现任何类型的协议,甚至不需要提供任何属性。它们实际上只是事件发生时用来做广告的工具;它们只是Signal的实例。定义一个成功信号的真正关键是确保它不会被取代。信号对象必须始终可以从同一导入位置获得,并且必须始终是同一对象。调度程序需要这样做,因为它使用对象作为标识符,将被调度的事件与已注册的适当侦听器相匹配。

>>> from django.dispatch import Signal

>>> signal = Signal()

>>> signal

<django.dispatch.dispatcher.Signal object at 0x...>

发送信号

每当您想要通知其他代码一个事件发生时,signals 提供了一个send()方法来将该信号发送给任何注册的侦听器。这个方法需要一个sender,它代表负责分派信号的对象,这允许监听器响应来自特定对象的事件。通常,Django 使用一个类——比如一个模型——作为sender,这样监听器可以在创建任何实例之前注册,同时也允许监听器响应该类的所有实例上的事件。

除了发送者之外,send()还接受任意数量的附加关键字参数,这些参数将直接传递给监听器。如下一节所示,侦听器必须总是接受所有的关键字参数,不管它们实际使用的是什么。这允许发送代码稍后向信号添加新信息,而不会给尚未更新以使用新信息的侦听器带来任何问题。发送信号的代码很可能会在以后添加一些特性,这种关键字参数支持使得将这些特性合并到现有信号中变得很容易。

一旦调用了所有的监听器,send()返回一个由注册的监听器返回的响应列表。该列表包含格式为(listener, response)的 2 元组序列。Django 自己的信号通常不使用任何返回值,但是它们对于支持将信息发送回应用本身的插件非常有用。

>>> from django.dispatch import Signal

>>> signal = Signal()

>>> sender = object()

>>> signal.send(sender=sender, spam='eggs')

[]

捕获返回值

函数通常会返回值,信号可以充分利用这一点。当用信号的参数调用每个监听器时,Django 捕获它的返回值并将它们收集在一个列表中。一旦所有的监听器都被调用,返回值的完整列表就会从Signal.send()返回,允许调用代码访问监听器提供的任何信息。这允许信号不仅仅用于额外的动作;它们也可以用于数据处理和相关任务。

定义监听器

发送时,信号将发送者和所有适当的参数传递给用该信号注册的每个侦听器函数。侦听器只是一个 Python 函数,和其他函数一样;唯一的区别是已经被注册为特定信号的收听者。由于信号只是简单地将监听器作为一个函数调用,它实际上可以是任何有效的 Python 可调用函数,其中许多在第二章中有所描述。实际上,标准函数是最常见的。

虽然允许侦听器有很大的灵活性,但信号确实对如何定义它们做了一个重要的假设:所有侦听器都必须接受传入的任何关键字参数。实际使用哪些参数完全取决于特定侦听器打算如何使用信号,但它必须正确无误地接受未使用的参数。如前所述,可以用任意数量的关键字参数发送信号,这些参数都将被传递给所有的侦听器。

这种方法的价值在于听众不需要知道信号负责的所有事情。可以为一个目的附加一个侦听器,期望一组特定的参数。然后,可以将附加参数添加到信号调度中,所有先前定义的侦听器将继续正常工作。与任何其他函数调用一样,如果侦听器期望一个信号没有提供的参数,Python 将引发一个TypeError

def listener(sender, a, **kwargs):

return a * 3

注册侦听器

一旦有了一个要处理的信号和一个要处理它的监听器,连接它们就是简单地调用信号的connect()方法。除了一个必需的参数之外,在注册信号时还可以指定几个选项,定制稍后调度信号时应该如何处理侦听器。

  • receiver—将接收信号及其相关参数的可调用函数。这显然是所有注册所必需的。
  • sender—观察信号的特定对象。由于每个信号都必须包含一个发送者,这就允许一个监听器只响应那个发送者。如果省略,将为发出给定信号的所有发送方调用侦听器。
  • weak—一个布尔值,指示是否应该使用弱引用,这个主题将在下一节中更详细地描述。默认为True,默认使用弱引用。
  • dispatch_uid—用于识别给定信号上的收听者的唯一字符串。由于模块有时可以不止一次地被导入,侦听器有可能被注册两次,这通常会导致问题。在这里提供一个唯一的字符串将确保侦听器只注册一次,不管模块被导入多少次。如果省略,将基于侦听器本身生成一个 ID。

强制强引用

虽然弱引用是一个相当复杂的话题,远远超出了本书的范围, 3 信号的使用在某些情况下会导致混乱,所以有必要给出这个问题及其解决方案的基本概述。当使用弱引用引用一个对象时,就像 Django 的 dispatcher 所做的那样,这个引用本身不会阻止对象被垃圾收集。它必须在别的地方还有一个强引用,否则 Python 会自动销毁它,释放它所占用的内存。

虽然 Python 中的标准引用是强引用,但默认情况下,dispatcher 使用弱引用来维护其注册侦听器列表。对于信号来说,这通常更好,因为这意味着属于不再使用的代码的侦听器函数不会因为被调用而耗尽宝贵的时间和精力。

然而,Python 中的一些情况通常会导致对象被破坏,这些情况在使用信号时需要特别注意。特别是,如果在另一个函数中定义了一个侦听器函数,可能是为了为特定对象定制一个函数,那么当侦听器的容器函数执行完毕并且其作用域被移除时,该侦听器将被销毁。

>>> from django.dispatch import Signal

>>> signal = Signal()

>>> def weak_customizer():

...     def weak_handler(sender, **kwargs):

...        pass

...     signal.connect(weak_handler)

...

>>> def strong_customizer():

...     def strong_handler(sender, **kwargs):

...        pass

...     signal.connect(strong_handler, weak=False)

...

>>> weak_customizer()

>>> strong_customizer()

>>> signal.send(sender="sender")

[(<function <strong_handler> at 0x...>, None)]

如您所见,注册侦听器的默认形式允许在定制函数执行完毕后销毁该函数。通过显式地指定weak=False,当信号在稍后的时间点被发送时,它仍然被调用。

现在怎么办?

本章中介绍的工具不会为您的应用提供主要的新功能,但是它们可以帮助您完成许多应用需要的更简单的任务。这些小事情真的可以帮助你把所有事情联系在一起。应用实际上如何使用是另一个问题,一些更有趣的选项将在下一章描述。

Footnotes 1

http://prodjango.com/mutagen/

  2

http://prodjango.com/json/

  3

http://prodjango.com/weak-references/

十、协调应用

Abstract

为企业编写软件是一项艰苦的工作。没有单一的规则手册来概述应该编写哪些应用,应该如何编写,它们应该如何相互交互,或者它们应该如何定制。所有这些问题的答案最好留给每个项目的开发人员,但是本章和第十一章给出的例子可以帮助你决定项目的最佳方法。

为企业编写软件是一项艰苦的工作。没有单一的规则手册来概述应该编写哪些应用,应该如何编写,它们应该如何相互交互,或者它们应该如何定制。所有这些问题的答案最好留给每个项目的开发人员,但是本章和第十一章的例子可以帮助你决定项目的最佳方法。

网站的大部分功能都是面向外部的,向组织外部的用户提供功能。很多时候,更多的功能集中在内部,旨在帮助员工更有效地执行日常任务。考虑一个需要跟踪其客户和可用房产的基本房地产网站。除了向外界显示属性之外,代理还需要管理这些属性以及帮助流程向前发展的人员。

与其构建一个面向特定需求的大型应用,不如尝试将这些需求分开,让多个应用协同工作以实现最终目标。这样做在开始时需要多做一些工作,但是随着新功能的不断增加,应用的清晰分离将有助于确定什么应该放在哪里以及一切应该如何协同工作。

联系人

虽然看起来房地产世界的一切都围绕着房地产,但人仍然是最基本的一块拼图。例如,一个给定的房产可能有一个所有者、一个房地产经纪人和几个潜在的买家。这些人在房地产过程中分别扮演不同的角色,但是无论他们扮演什么角色,表示他们所需的数据都是相同的。它们都可以概括为一个“联系人”,该联系人只包含识别它们并与之通信所必需的数据。

这种抽象为我们提供了一个简单的模型,可以用于与特定房产相关的人、尚未对房产表示兴趣的其他人、我们虚构的房地产办公室本身的员工,甚至像质量检查员和价值评估员这样的第三方联系人。每个人所扮演的角色可以通过将他们与另一个模型(比如一个属性)相关联来定义。

联系人.模型.联系人

联系信息通常包括姓名、地址、电话号码和电子邮件地址,其中一些已经可以被 Django 捕获。来自django.contrib.authUser模型包含一个人的姓和名以及电子邮件地址的字段,所以剩下的就是包含一些更真实的联系信息。将它与User相关联允许一个联系人包含两种类型的数据,同时也为以后可以登录的联系人提供了可能性。

因为我们的房地产公司将在美国运营,所以有一些特定的联系信息字段需要根据当地习俗验证数据。为了提供对区域数据类型的支持,有多种本地风格的包可供使用。每个包,包括我们将要使用的django-localflavor-us,都包含了一个模型和表单的字段选择,这些字段特定于该区域的公共数据类型。我们的Contact型号可以特别利用PhoneNumberFieldUSStateField

from django.db import models

from django.contrib.auth.models import User

from django_localflavor_us import models as us_models

class Contact(models.Model):

user = models.OneToOneField(User)

phone_number = us_models.PhoneNumberField()

address = models.CharField(max_length=255)

city = models.CharField(max_length=255)

state = us_models.USStateField()

zip_code = models.CharField(max_length=255)

class Meta:

ordering = ('user__last_name', 'user__first_name')

def __unicode__(self):

return self.user.get_full_name()

WHY NOT MODEL INHERITANCE?

第三章解释了一个 Django 模型如何直接从另一个继承,自动创建一个类似于这里使用的引用。因为这也增加了一些额外的易用选项,你可能会奇怪为什么Contact不直接从User继承。

模型继承最适合不直接使用基本模型的情况,因为 Django 不提供向现有模型添加继承实例的方法。在我们的例子中,这意味着如果数据库中已经存在一个User,我们将不能基于它创建一个新的Contact。因为有许多其他应用,包括 Django 的管理应用,可能会直接创建用户,所以我们需要能够毫无困难地为新用户或现有用户创建联系人。

通过显式使用OneToOneField,我们定义了模型继承将使用的完全相同的关系,但是没有在这种情况下限制我们的不同语法。我们失去了真正继承提供的一些语法优势,但是这些可以通过另一种方式来适应。

因为联系人本质上只是添加了一些属性的用户,所以在一个对象上拥有所有可用的属性是很有用的。否则,模板作者不仅要知道给定属性来自哪个模型,还要知道如何引用另一个模型来检索这些属性。例如,给定一个名为contactContact对象,下面的列表显示了它的许多属性和方法:

  • contact.user.username
  • contact.user.get_full_name()
  • contact.user.email
  • contact.phone_number
  • contact.address
  • contact.zip_code

这给模板作者带来了不必要的负担,他们不需要知道联系人和用户之间存在什么类型的关系。模型继承通过将所有属性直接放在接触上直接缓解了这种情况。这里,只需使用一组属性就可以实现相同的行为,这些属性将各种属性映射到后台的相关用户对象。

@property

def first_name(self):

return self.user.first_name

@property

def last_name(self):

return self.user.last_name

def get_full_name(self):

return self.user.get_full_name()

不是所有的User方法在Contact上都有意义。例如,is_anonymous()is_authenticated()方法最好留给User。视图和模板不会使用联系人来确定身份验证或权限,因此联系人将作为个人身份信息各个方面的中心位置。

contacts.forms.UserEditorForm

与其要求用户通过管理界面管理他们的联系人,不如有一个单独的表单专门用于联系人管理。这对于联系人来说甚至比大多数其他模型更重要,因为联系人实际上包括两个独立的模型。Django 提供的ModelForm助手 1 将一个表单映射到一个模型,要求contacts应用使用两个单独的表单来管理一个人。

一个表单可以包含两个模型所需的所有字段,但这不适用于ModelForm,因为表单必须包含手动填充和保存模型所需的所有逻辑。相反,可以使用两种独立的形式,以便将它们联系在一起。详见contacts.views.EditContact的描述。

因为 Django 自己提供了User模型,所以重用 Django 用于管理用户的任何东西似乎都是合乎逻辑的。不幸的是,为用户管理提供的表单是为与联系人管理非常不同的用例设计的。有两种形式可用,都住在django.contrib.auth.forms,各有不同的目的:

  • UserCreationForm—该表格旨在用于最基本的用户创建,仅接受一个用户名和两份密码(用于验证)。联系人所需的字段(姓名和电子邮件)不可用。
  • UserChangeForm—用于管理界面,该表单包含User模型上可用的每个字段。虽然这包括姓名和电子邮件,但它也包括一系列用于身份验证和授权的字段。

因为这些表单都不适合联系人管理的用例,所以为这个应用创建一个新表单更有意义。使这变得容易,允许一个表单只指定那些不同于缺省值的东西。对于联系人管理,这意味着只包括用户名、名、姓和电子邮件地址等字段。

from django import forms

from django.contrib.auth.models import User

class UserEditorForm(forms.ModelForm):

class Meta:

model = User

fields = ('username', 'first_name', 'last_name', 'email')

有了这些信息,ModelForm就可以根据底层模型提供的细节来管理表单的其余行为。剩下的工作就是提供一个补充表单来管理新的联系人级别的详细信息。

contacts . forms . contacteditorform

管理联系人的表单与用户表单非常相似,使用ModelForm来处理大部分细节。唯一的区别是,用于联系人的字段比已经定义的Contact模型中列出的字段有更具体的验证要求。例如,电话号码以纯文本的形式存储在模型中,但是它们遵循特定的格式,可以被表单验证。这些验证已经由模型中使用的同一个django-localflavor-us包提供了。这个ContactEditorForm可以使用四个类:

  • USStateField根据当前状态验证两个字母的代码
  • USStateSelect显示包含所有有效状态的列表框
  • USPhoneNumberField验证十位数电话号码,包括破折号
  • 验证五位数或九位数的邮政编码

Note

USStateField还包括美国领土:美属萨摩亚群岛、哥伦比亚特区、关岛、北马里亚纳群岛、波多黎各和美属维尔京群岛。

还有其他的类,但是这四个类足以定制联系人的验证。唯一剩下的可编辑字段“地址”和“城市”没有可以通过编程验证的既定格式。应用这些覆盖,ContactEditorForm如下所示:

from django import forms

from django_localflavor_us import forms as us_forms

from contacts.models import Contact

class ContactEditorForm(forms.ModelForm):

phone_number = us_forms.USPhoneNumberField(required=False)

state = us_forms.USStateField(widget=us_forms.USStateSelect, required=False)

zip_code = us_forms.USZipCodeField(label="ZIP Code", required=False)

class Meta:

model = Contact

exclude = ('user',)

注意这里使用了exclude而不是字段,就像在UserEditorForm中使用的一样。这告诉ModelForm使用模型中除了那些明确列出的字段之外的所有字段。因为用户将由UserEditorForm提供,所以没有必要在这里将其作为一个单独的选项。地址和城市不需要作为显式字段声明提供,因为ModelForm会自动使用标准文本字段。

contacts . views . editcontact 联络人。检视.编辑联络人

联系人由两种模型组成——因此也有两种表单——但是管理这些联系人的用户应该只需要处理一个包含所有适当字段的表单。Django 的特定于表单的通用视图在这里并没有真正帮助我们,因为它们只是为每个视图一个表单而设计的。我们将两种形式结合起来,这要么需要对UpdateView进行相当大的修改,要么需要一个结合两种形式的新类。这两个选项都不令人愉快,所以我们将采用一种更实用的方法,用稍微通用一些的视图,手工组装表单处理行为。

首先要做的选择是接受什么样的论点。因为这个视图将在模板中呈现表单,所以最好在 URL 配置中接受一个模板名称,所以我们将依赖 Django 的TemplateView,它会自己处理这个问题。通过简单地子类化django.views.generic.TemplateView,我们的新视图将在其配置中自动接受一个template_name参数,并提供一个将模板呈现给响应的方法。

为了调出单个联系人进行编辑,视图还必须能够识别应该使用哪个联系人。这个标识符必须是唯一的,并且应该是用户可以合理理解的。因为每个联系人都与一个用户相关,并且每个用户都有一个唯一的用户名,所以这个用户名非常适合这个目的。

from django.views.generic import TemplateView

class EditContact(TemplateView):

def get(self, request, username=None):

pass

注意用户名是可选的。拥有一个可选的标识符允许这个视图用于添加新的联系人以及编辑现有的联系人。这两种情况需要本质上相同的行为:接受用户的联系信息,检查它们是否是有效数据,并将它们保存在数据库中。添加和编辑的唯一区别是Contact对象是否已经存在。

考虑到这个目标,视图必须准备好创建一个新的Contact对象,甚至可能创建一个新的User,如果它们都不存在的话。必须处理四种不同的情况:

  • 提供了一个用户名,并且对于该用户名存在一个User和一个Contact。视图应该继续编辑两个现有记录。
  • 提供了一个用户名并且存在一个User,但是没有Contact与之相关联。应该创建一个新的Contact并与User相关联,这样两者都可以被编辑。
  • 提供了一个用户名,但不存在它的User,这也意味着不存在Contact。请求用户名意味着一个已存在的用户,因此请求一个不存在的用户应该被认为是错误的。在这种情况下,这是 HTTP 404(未找到)错误代码的适当用法。
  • 没有提供用户名,这意味着现有用户和联系人是不相关的。应该创建新的UserContact对象,忽略任何可能已经存在的对象。该表格将要求提供一个新的用户名。

Tip

使用 404 错误代码并不总是意味着您必须提供通用的“未找到页面”页面。您可以向HttpResponseNotFound类提供您喜欢的任何内容,而不是默认的HttpResponse类。为了简单起见,这些例子仅仅依赖于标准的 404 错误页面,但是对于您的站点来说,显示类似“您请求的联系人尚不存在”这样的 404 页面可能更有意义这允许您利用已知的 HTTP 状态代码,同时仍然向用户显示更有用的消息。

这些情况可以用get_objects()方法轻松处理。它被分解到自己的方法中,因为我们最终会从get()post()方法中都需要它。

from django.shortcuts import get_object_or_404

from django.views.generic import TemplateView

from django.contrib.auth.models import User

from contacts.models import Contact

class EditContact(TemplateView):

def get_objects(self, username):

# Set up some default objects if none were defined.

if username:

user = get_object_or_404(User, username=username)

try:

# Return the contact directly if it already exists

contact = user.contact

except Contact.DoesNotExist:

# Create a contact for the user

contact = Contact(user=user)

else:

# Create both the user and an associated contact

user = User()

contact = Contact(user=user)

return user, contact

def get(self, request, username=None):

pass

一旦知道这两个对象都存在,视图就可以显示这两个对象的表单,这样用户就可以填写信息。这是通过get()方法处理的。

from django.shortcuts import get_object_or_404

from django.views.generic import TemplateView

from django.contrib.auth.models import User

from contacts.models import Contact

from contacts import forms

class EditContact(TemplateView):

def get_objects(self, username):

# Set up some default objects if none were defined.

if username:

user = get_object_or_404(User, username=username)

try:

# Return the contact directly if it already exists

contact = user.contact

except Contact.DoesNotExist:

# Create a contact for the user

contact = Contact(user=user)

else:

# Create both the user and an associated contact

user = User()

contact = Contact(user=user)

return user, contact

def get(self, request, username=None):

user, contact = self.get_objects()

return self.render_to_response({

'username': username

'user_form': forms.UserEditorForm(instance=user)

'contact_form': forms.ContactEditorForm(instance=contact)

})

然后视图可以继续处理表单,并用适当的信息填充这些对象。它必须独立于其他表单实例化、验证和保存每个表单。这样,每个表单只需要知道它要管理的数据,而视图可以将两者联系在一起。

如果两个表单都保存正确,视图应该重定向到一个新的 URL,在那里可以查看编辑过的联系信息。这对于新的联系人特别有用,因为在处理表单之前不会给他们分配 URL。在任何其他情况下,包括第一次查看表单时(即尚未提交任何数据),以及提交的数据未能通过验证时,视图应该返回可以显示适当表单的呈现模板。

from django.core.urlresolvers import reverse

from django.http import HttpResponseRedirect

from django.shortcuts import get_object_or_404

from django.views.generic import TemplateView

from django.contrib.auth.models import User

from contacts.models import User

from contacts import forms

class EditContact(TemplateView):

def get_objects(self, username):

# Set up some default objects if none were defined.

if username:

user = get_object_or_404(User, username=username)

try:

# Return the contact directly if it already exists

contact = user.contact

except Contact.DoesNotExist:

# Create a contact for the user

contact = Contact(user=user)

else:

# Create both the user and an associated contact

user = User()

contact = Contact(user=user)

return user, contact

def get(self, request):

user, contact = self.get_objects()

return self.render_to_response({

'username': user.username

'user_form': forms.UserEditorForm(instance=user)

'contact_form': forms.ContactEditorForm(instance=contact)

})

def post(self, request):

user, contact = self.get_objects()

user_form = forms.UserEditorForm(request.POST, instance=user)

contact_form = forms.ContactEditorForm(request.POST, instance=contact)

if user_form.is_valid() and contact_form.is_valid():

user = user_form.save()

# Attach the user to the form before saving

contact = contact_form.save(commit=False)

contact.user = user

contact.save()

return HttpResponseRedirect(reverse('contact_detail'

kwargs={'slug': user.username}))

return self.render_to_response(self.template_name, {

'username': user.username

'user_form': user_form

'contact_form': contact_form

})

管理配置

因为这个应用有自己的添加和编辑联系人的视图,所以不太需要使用管理界面。但是由于后面描述的Property模型既与Contact相关,又大量使用 admin,所以配置一个管理联系人的基本界面是个好主意。

from django.contrib import admin

from contacts import models

class ContactAdmin(admin.ModelAdmin):

pass

admin.site.register(models.Contact, ContactAdmin)

它没有提供同时编辑UserContact模型的便利,但是为通过管理员管理的相关模型提供了价值。

URL 配置

除了添加和编辑联系人,该应用还必须提供一种方式来查看所有现有的联系人和任何特定联系人的详细信息。这些特性反过来要求在contact应用的 URL 配置中考虑四种不同的 URL 模式。其中两个将映射到上一节描述的edit_contact视图,而另外两个将映射到 Django 自己的通用视图。

  • /contacts/—所有现有联系人的列表,带有指向各个联系人详细信息的链接
  • /contacts/add/—可以添加新联系人的空表单
  • /contacts/{username}/—给定用户的所有联系信息的简单视图
  • /contacts/{username}/edit/—填充有任何现有数据的表单,可以在其中更改数据和添加新数据

这些 URL 开头的/contacts/部分不是任何联系人视图本身的组成部分;这是站点级的区别,指向整个contacts应用。因此,它不会包含在应用的 URL 配置中,而是包含在站点的配置中。剩下的是一组 URL 模式,它们可以移植到站点需要的任何 URL 结构中。

第一个模式是所有现有联系人的列表,表面上很简单。一旦从 URL 中删除了/contacts/,就什么都没有了——更确切地说,剩下的只是一个空字符串。空字符串确实很容易与正则表达式匹配,但是我们将为它使用的视图django.views.generic.ListView需要一些额外的定制才能正常工作。

首先,它需要一个queryset和一个template_name,控制在哪里可以找到对象以及它们应该如何显示。对于此应用,所有联系人都是可用的,没有任何过滤。根据您的风格,模板名称可以是最合适的名称;我就叫它"contacts/contact_list.html"

通过显示所有联系人,列表可能会变得很长,因此如果需要的话,将结果分成多个页面会更有用。ListView也通过它的paginate_by参数提供了这一点。如果提供的话,它会在溢出到下一页之前提供单个页面上应该显示的最大数量的结果。然后,该模板可以控制页面信息和相关页面链接的显示方式。

from django.conf.urls.defaults import *

from django.views.generic import ListView

from contacts import models

urlpatterns = patterns(''

url(r'^$'

ListView.as_view(queryset=models.Contact.objects.all()

template_name='contacts/list.html'

paginate_by=25)

name='contact_list')

)

接下来是添加新联系人的 URL,使用自定义的EditContact视图。像联系人列表一样,这个 URL 模式的正则表达式非常简单,因为它不包含任何要捕获的变量。除了匹配 URL 的add/部分,这个模式只需要指向正确的视图并传递一个模板名。

from django.conf.urls.defaults import *

from django.views.generic import ListView

from contacts import models , views

urlpatterns = patterns(''

url(r'^$'

ListView.as_view(queryset=models.Contact.objects.all()

template_name='contacts/list.html'

paginate_by=25)

name='contact_list')

url(r'^add/$', views.EditContact.as_view(

template_name='contacts/editor_form.html'

), name='contact_add_form')

)

其余的 URL 模式都需要从 URL 本身捕获用户名,然后将其传递给相关的视图。用户名遵循相当简单的格式,允许字母、数字、破折号和下划线。这可以用正则表达式[\w-]+来表示,这是一种通常用于识别文本标识符的模式,通常被称为“slugs”

Note

就像 Django 本身一样,Slugs 也植根于新闻行业。slug 是一篇文章在付印前给新闻机构内部交流使用的名称。就在印刷之前,文章会有一个合适的标题,但 slug 仍然是唯一引用特定文章的方式,不管它是否可供公众查看。

要编写的第一个视图,即基本的 contact detail 页面,将使用 Django 提供的另一个通用视图django.views.generic.DetailView,因此必须注意用户名所分配到的变量的名称。自定义的EditContact视图称其为username,但是DetailView不知道要寻找具有该名称的东西。相反,它允许一个 URL 模式捕获一个slug变量,其功能相同。另一个要求是提供一个slug_field参数,它包含与 slug 匹配的字段的名称。

通常,这个slug_field参数是模型上可以找到slug值的字段的名称。不过,像大多数通用视图一样,DetailView需要给一个queryset参数一个有效的 QuerySet,从中可以检索到一个对象。视图然后向 QuerySet 添加一个get()调用,使用slug_field / slug组合来定位一个特定的对象。

这个实现细节很重要,因为它为 URL 模式提供了额外的灵活性,如果视图将slug_field与模型上的实际字段相匹配,这种灵活性是不可用的。更具体地说,slug_field可以包含一个跨越相关模型的查找,这一点很重要,因为联系人是由两个不同的模型组成的。URL 模式应该通过查询相关的User对象的用户名来检索一个Contact对象。为此,我们可以将slug_field设置为"user__username"

from django.conf.urls.defaults import *

from django.views.generic import ListView, DetailView

from contacts import models, views

urlpatterns = patterns(''

url(r'^$'

ListView.as_view(queryset=models.Contact.objects.all()

template_name='contacts/list.html'

paginate_by=25)

name='contact_list')

url(r'^add/$', views.EditContact.as_view(

template_name='contacts/editor_form.html'

), name='contact_add_form')

url(r'^(?P<slug>[\w-]+)/$'

DetailView.as_view(queryset=models.Contact.objects.all()

slug_field='user__username'

template_name='contacts/list.html')

name='contact_detail')

)

最后一个 URL 模式是编辑单个联系人,它严格遵循用于添加新联系人的模式。两者之间唯一的区别是用于匹配 URL 的正则表达式。前面的模式没有从 URL 中捕获任何变量,但是这个模式需要捕获用户名来填充表单的字段。用于捕获用户名的表达式将使用与细节视图相同的格式,但是将使用名称username而不是slug

from django.conf.urls.defaults import *

from django.views.generic import ListView, DetailView

from contacts import modelsviews

urlpatterns = patterns(''

url(r'^$'

ListView.as_view(queryset=models.Contact.objects.all()

template_name='contacts/list.html'

paginate_by=25)

name='contact_list')

url(r'^add/$', views.EditContact.as_view(

template_name='contacts/editor_form.html'

), name='contact_add_form')

url(r'^(?P<slug>[\w-]+)/$'

DetailView.as_view(queryset=models.Contact.objects.all()

slug_field='user__username'

template_name='contacts/list.html')

name='contact_detail')

url(r'^(?P<username>[\w-]+)/edit/$', views.EditContact.as_view(

template_name='contacts/editor_form.html'

), name='contact_edit_form')

)

这个应用现在唯一缺少的是 URL 模式中提到的四个模板。由于这本书的目标是开发,而不是设计,这些留给读者作为练习。

房地产属性

房地产公司的主业当然是房地产。单个建筑或一块土地通常被称为属性,但是这个术语不应该与 Python 的属性概念混淆,在第二章中有描述。这种名称冲突是不幸的,但并不意外;完全不同的人群使用相同的术语表达不同的意思是很常见的。

当这种情况普遍出现时,最好使用你的听众最容易理解的术语。在与房地产经纪人会面时,你应该能够使用“财产”来指代一块房地产,而不会有任何混淆或解释。当与程序员交谈时,“属性”可能指的是模型、对象或内置函数。

Python 的property装饰器在很多情况下都很有用,但是本章的大部分内容将集中在其他 Python 技术上。鉴于此,除非另有说明,否则术语“财产”将指不动产。

属性.模型.属性

物业管理应用中最基本的项目是一个Property。在房地产术语中,房产只是一块土地,通常附有一栋或多栋建筑物。这包括房屋、零售店、工业区和未开发的土地。虽然这涵盖了广泛的选项,但也有许多事情是跨领域共享的。这些共享功能中最基本的是所有属性都有一个地址,该地址由几个部分组成:

from django.db import models

from django_localflavor_us import models as us_models

class Property(models.Model):

slug = models.SlugField()

address = models.CharField(max_length=255)

city = models.CharField(max_length=255)

state = us_models.USStateField()

zip = models.CharField(max_length=255)

class Meta:

verbose_name_plural = 'properties'

def __unicode__(self):

return u'%s, %s' % (self.address, self.city)

这个模型还包括一个 slug,它将用于识别 URL 中的属性。

Note

这种模式只使用一个地址字段,而许多地址表单使用两个字段。两个地址行总是适合于邮寄地址,因为它们允许在一个建筑物内进行划分,例如公寓或办公室套房。房地产往往专注于建筑本身和它所坐落的土地,而不是建筑如何分割,所以一个领域就足够了。共管公寓是单独出售的建筑物的子部分,因此在进行共管公寓交易的市场中,需要一个额外的地址字段来唯一标识建筑物内的属性。

除了能够定位物业之外,还可以添加更多的字段来描述物业的大小和占用它的建筑。包含这些信息的方法有很多种,而Property将利用多种方法,所有这些方法都是可选的。通常,所有这些都将在列表公开之前填写,但数据库应该支持管理具有不完整信息的属性,以便代理可以在信息可用时填充它。

from django.db import models

from django_localflavor_us import models as us_models

class Property(models.Model):

slug = models.SlugField()

address = models.CharField(max_length=255)

city = models.CharField(max_length=255)

state = us_models.USStateField()

zip = models.CharField(max_length=255)

square_feet = models.PositiveIntegerField(null=True, blank=True)

acreage = models.FloatField(null=True, blank=True)

class Meta:

verbose_name_plural = 'properties'

def __unicode__(self):

return u'%s, %s' % (self.address, self.city)

square_feet字段是指建筑物内的可用面积。当设计或改造一栋建筑时,有必要将其分解为单个房间的尺寸,但对于买卖房产的任务来说,总量本身就很好。acreage字段代表房产所占的总土地面积,以英亩为单位,相当于 43560 平方英尺。

Tip

如果代理确实获得了酒店内各个房间的尺寸,则可以使用本章后面的“properties.models.Feature”一节中描述的Feature模型将这些尺寸作为单独的酒店特征包括在内。

到目前为止,Property模型的大部分内容都集中在描述酒店本身,但也包括销售过程的一些方面。价格可能是属性列表中最重要的方面,尽管它不是一个物理属性,但每个属性一次只能有一个价格,因此在这里将其作为一个字段仍然是有意义的。下一章将解释我们如何跟踪过去的价格,但是这个模型将只存储当前价格。

另一个这样的属性是资产的status——它当前在销售过程中的位置。对于数据库中的新条目,可能根本没有任何状态。也许正在为正在考虑出售但尚未决定在市场上上市的房主记录一些房产信息。一旦业主决定出售,它可以上市供公众考虑,其余的过程开始。

from django.db import models

from django_localflavor_us import models as us_models

class Property(models.Model):

LISTED, PENDING, SOLD = range(3)

STATUS_CHOICES = (

(LISTED, 'Listed')

(PENDING, 'Pending Sale')

(SOLD, 'Sold')

)

slug = models.SlugField()

address = models.CharField(max_length=255)

city = models.CharField(max_length=255)

state = us_models.USStateField(max_length=2)

zip = models.CharField(max_length=255)

square_feet = models.PositiveIntegerField(null=True, blank=True)

acreage = models.FloatField(null=True, blank=True)

status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES

null=True, blank=True)

price = models.PositiveIntegerField(null=True, blank=True)

class Meta:

verbose_name_plural = 'properties'

def __unicode__(self):

return u'%s, %s' % (self.address, self.city)

除了可以在模型上存储一次的属性之外,还有其他属性特征可以多次出现或以多种不同的组合出现。这些便利设施,如壁炉、地下室、车库、阁楼和电器,并不是每个房产都有的或没有的功能清单的一部分。这使得很难(如果不是不可能的话)为每个特征创建一个字段,而不必在每次出现不符合先前假设的新属性时修改模型的结构。

相反,特征应该存储在另一个模型中,指出哪些特征存在于属性中,并详细描述它们。另一个模型可以介入,将这些特性归纳为通用类型,以便可以浏览和搜索。例如,用户可能对查找所有带壁炉的房产感兴趣。拥有一个专门定义壁炉的模型,以及一个描述各个壁炉的相关模型,有助于实现这种类型的行为。请参阅后面的“properties.models.Feature”和“properties . models . property feature”部分,了解有关其工作原理的更多详细信息。

房产也有许多相关的人,如业主、房地产经纪人、建筑师、建筑商,可能还有几个潜在的买家。这些都有资格作为联系人,并使用已经定义的Contact模型进行存储。为了使其尽可能通用,他们将被称为“利益相关方”,因为每个人都在有关财产的交易中有一些利益。

from django.db import models

from django_localflavor_us import models as us_models

class Property(models.Model):

LISTED, PENDING, SOLD = range(3)

STATUS_CHOICES = (

(LISTED, 'Listed')

(PENDING, 'Pending Sale')

(SOLD, 'Sold')

)

slug = models.SlugField()

address = models.CharField(max_length=255)

city = models.CharField(max_length=255)

state = us_models.USStateField()

zip = models.CharField(max_length=255)

square_feet = models.PositiveIntegerField(null=True, blank=True)

acreage = models.FloatField(null=True, blank=True)

status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES

null=True, blank=True)

price = models.PositiveIntegerField(null=True, blank=True)

features = models.ManyToManyField('Feature', through='PropertyFeature')

interested_parties = models.ManyToManyField(Contact

through='InterestedParty')

class Meta:

verbose_name_plural = 'properties'

def __unicode__(self):

return u'%s, %s' % (self.address, self.city)

不是所有的房产都应该公开上市。在财产上市之前,以及在出售之后,应当对公众保密,只让工作人员管理。与其每次需要公开显示一个属性时都为此键入一个查询,不如创建一个自定义管理器,用一种方法来缩小列表范围。

class PropertyManager(models.Manager):

def listed(self):

qs = super(PropertyManager, self).get_query_set()

return qs.filter(models.Q(status=Property.LISTED) | \

models.Q(status=Property.PENDING))

这可以通过简单的赋值附加到模型上;任何名字都可以,但是约定俗成是称呼标准管理器objects,所以这个会这么做。

from django.db import models

from django_localflavor_us import models as us_models

class Property(models.Model):

LISTED, PENDING, SOLD = range(3)

STATUS_CHOICES = (

(LISTED, 'Listed')

(PENDING, 'Pending Sale')

(SOLD, 'Sold')

)

slug = models.SlugField()

address = models.CharField(max_length=255)

city = models.CharField(max_length=255)

state = us_models.USStateField()

zip = models.CharField(max_length=255)

square_feet = models.PositiveIntegerField(null=True, blank=True)

acreage = models.FloatField(null=True, blank=True)

status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES

null=True, blank=True)

price = models.PositiveIntegerField(null=True, blank=True)

features = models.ManyToManyField('Feature', through='PropertyFeature')

interested_parties = models.ManyToManyField(Contact

through='InterestedParty')

objects = PropertyManager()

class Meta:

verbose_name_plural = 'properties'

def __unicode__(self):

return u'%s, %s' % (self.address, self.city)

属性.模型.功能

一个特征仅仅是酒店提供的一些值得注意的东西。它可以是一个普通的必需品,如地下室或洗衣房,但也可以是非常独特的,如壁炉或阳光房。这些功能经常被列出来,试图将一个房产与另一个区别开来,因为买家经常会列出他们想要的功能。

Feature模型仅包含定义特定类型特征所需的信息。Feature不是描述一个具体的壁炉,而是简单地定义了什么是壁炉,为各个壁炉提供了一个定位点。这样,可以使用这个模型作为起点,通过特性来搜索属性。

class Feature(models.Model):

slug = models.SlugField()

title = models.CharField(max_length=255)

definition = models.TextField()

def __unicode__(self):

return self.title

属性.模型.属性特征

在查看特定属性时,特定的细节远比在高层次上定义一个特性更有用。PropertyFeature模型在PropertyFeature之间架起了一座桥梁,提供了一种描述某一特定物业的个人特征的方式。

class PropertyFeature(models.Model):

property = models.ForeignKey(Property)

feature = models.ForeignKey(Feature)

description = models.TextField(blank=True)

def __unicode__(self):

return unicode(self.feature)

属性.模型.兴趣方

对某一特定房产感兴趣的联系人多种多样,从业主和买家到房地产经纪人和安全检查员。这些人中的每一个都可以通过关系的方式连接到特定的资产,该关系包括关于关系性质的一些细节。

from contacts.models import Contact

class InterestedParty(models.Model):

BUILDER, OWNER, BUYER, AGENT, INSPECTOR = range(5)

INTEREST_CHOICES = (

(BUILDER, 'Builder')

(OWNER, 'Owner')

(BUYER, 'Buyer')

(AGENT, 'Agent')

(INSPECTOR, 'Inspector')

)

property = models.ForeignKey(Property)

contact = models.ForeignKey(Contact)

interest = models.PositiveSmallIntegerField(choices=INTEREST_CHOICES)

class Meta:

verbose_name_plural = 'interested parties'

def __unicode__(self):

return u'%s, %s' % (self.contact, self.get_interest_display())

Note

这些角色可以重叠,例如所有者同时是建筑商和房地产经纪人。一些数据库允许将字段用作位掩码,您可以切换单个位来指示联系人履行的角色。由于 Django 不支持创建或搜索这些类型的字段,我们改为每行只存储一个角色;具有多个角色的联系人可以简单地使用多行来描述情况。

管理配置

房产列表是供普通公众浏览的,但只能由房地产代理公司的员工编辑,这些员工在该领域受过广泛的培训并有丰富的经验,可以被信任来完成这项任务。该描述与 Django 内置管理应用的目标受众相同。

使用 admin 提供的功能,用户可以很容易地将界面放在一起,以便能够编辑和维护 properties 应用中的所有各种模型。不需要单独的编辑器视图,只需要做一些小的修改就可以定制管理,以一种用户友好的方式使用这些模型。

“THE ADMIN IS NOT YOUR APP”

如果你花很多时间在 Django 社区,你可能会遇到这样一句话,“管理不是你的应用。”这里传达的普遍观点是,管理员的关注点相当有限,远比大多数网站有限。它预计将由可信的工作人员使用,他们可以使用更基本的数据输入界面。当你发现自己在努力寻找让管理员做你想做的事情的方法时,很可能你需要开始写自己的观点,不再依赖管理员。

这并不意味着管理员只在开发过程中有用。如果一个基本的编辑界面适合工作人员使用,它可以节省时间和精力。通过一些简单的定制,管理员可以执行这种编辑界面所需的大多数常见任务。本章前面描述的 contacts 应用不能依赖于 admin,因为它需要组合两个表单,这超出了 admin 的预期范围。

对于属性,管理员完全有能力生成一个合适的界面。因为只有员工需要编辑属性数据,所以没有必要创建与站点其他部分集成的自定义视图。您可以花更多的时间来构建面向公众的应用。

要设置的第一个模型是Property,但是由于相关模型的工作方式,需要先对PropertyFeatureInterestedParty进行一些配置。这些都是使用一个简单的类配置的,该类告诉管理员将它们作为页面末尾的表添加到属性编辑器中。除了任何现有的关系,管理员应该显示一个空记录,可用于添加新的关系。

from django.contrib import admin

from properties import models

class InterestedPartyInline(admin.TabularInline):

model = models.InterestedParty

extra = 1

class PropertyFeatureInline(admin.TabularInline):

model = models.PropertyFeature

extra = 1

为了在Property模型的管理页面上定制一些更专业的字段,需要一个定制的ModelForm子类。这允许表单指定应该为其statezip字段使用什么小部件,因为它们遵循一种比自由格式文本字段更具体的格式。所有其他字段可以保持原样,因此不需要在该表单中指定它们。

from django.contrib import admin

from django import forms

from django_localflavor_us import forms as us_forms

from properties import models

class InterestedPartyInline(admin.TabularInline):

model = models.InterestedParty

extra = 1

class PropertyFeatureInline(admin.TabularInline):

model = models.PropertyFeature

extra = 1

class PropertyForm(forms.ModelForm):

state = us_forms.USStateField(widget=us_forms.USStateSelect)

zip = us_forms.USZipCodeField(widget=forms.TextInput(attrs={'size': 10}))

class Meta:

model = models.Property

现在我们终于可以为Property本身配置管理界面了。第一个定制是使用PropertyForm而不是通常使用的普通ModelForm

class PropertyAdmin(admin.ModelAdmin):

form = PropertyForm

admin.site.register(models.Property, PropertyAdmin)

在该表单中,并非所有字段都应该显示在一个从上到下的简单列表中。通过将citystatezip字段放在一个元组中,完整的地址可以以更熟悉的格式显示,这样它们都在同一行结束。slug 放在地址旁边,因为它将根据该信息进行填充。销售字段可以放在一个单独的分组中,与大小相关的字段也可以放在一个单独的分组中,并用一个标题将每个分组区分开。

class PropertyAdmin(admin.ModelAdmin):

form = PropertyForm

fieldsets = (

(None, {'fields': (('address', 'slug')

('city', 'state', 'zip'))})

('Sales Information', {'fields': ('status'

'price')})

('Size', {'fields': ('square_feet'

'acreage')})

)

相关的模型被添加到一个名为inlines的元组中,该元组控制其他模型如何附加到现有的管理界面。因为它们已经在自己的类中配置好了,所以我们需要做的就是将它们添加到PropertyAdmin中。

class PropertyAdmin(admin.ModelAdmin):

form = PropertyForm

fieldsets = (

(None, {'fields': (('address', 'slug')

('city', 'state', 'zip'))})

('Sales Information', {'fields': ('status'

'price')})

('Size', {'fields': ('square_feet'

'acreage')})

)

inlines = (

PropertyFeatureInline

InterestedPartyInline

)

最后,生成 slug 的声明需要一个分配给prepopulated_fields属性的字典。本词典中的关键字是自动生成的SlugField的名称。关联的值是一个字段名元组,slug 的值应该从这个元组中提取。根据地址和邮政编码,所有属性都应该是唯一的,所以这两个字段可以组合起来形成属性的一个 slug。

class PropertyAdmin(admin.ModelAdmin):

form = PropertyForm

fieldsets = (

(None, {'fields': (('address', 'slug')

('city', 'state', 'zip'))})

('Sales Information', {'fields': ('status'

'price')})

('Size', {'fields': ('square_feet'

'acreage')})

)

inlines = (

PropertyFeatureInline

InterestedPartyInline

)

prepopulated_fields = {'slug': ('address', 'zip')}

Note

在管理应用中编辑模型实例时,使用 JavaScript 预先填充了 Slug 字段。这是一个有用的便利,只要缺省的 slug 是合适的,就节省了必须访问单独字段的时间和麻烦。在 Python 中创建对象时,不使用显式值填充字段的唯一方式是通过作为其default参数传入的函数或通过字段的pre_save()方法。

有了这些,剩下的唯一需要建立的模型就是Feature。因为它是比Property更简单的模型,所以管理声明也相当简单。有三个字段需要安排,还有一个SlugField需要配置。

class FeatureAdmin(admin.ModelAdmin):

fieldsets = (

(None, {

'fields': (('title', 'slug'), 'definition')

})

)

prepopulated_fields = {'slug': ('title',)}

admin.site.register(models.Feature, FeatureAdmin)

URL 配置

因为属性的实际管理是由管理界面处理的,所以只需为用户配置 URL 来查看属性列表。这些类型的只读视图最好由 Django 自己的通用视图来处理,这些视图被配置为与所讨论的模型一起工作。具体来说,这些 URL 将使用我们在本章前面用于联系人的相同的通用列表和详细视图。

可使用ListView设置房产列表的视图。这个视图需要一个 QuerySet 来定位条目,这就是PropertyManager有用的地方。它的listed()方法将查询范围缩小到应该向公众显示的项目。

from django.conf.urls.defaults import *

from django.views.generic import ListView, DetailView

from properties import models

urlpatterns = patterns(''

url(r'^$'

ListView.as_view(queryset=models.Property.objects.listed()

template_name='properties/list.html'

paginate_by=25)

name='property_list')

)

尽管详细视图需要较少的配置选项——因为它不需要paginate_by参数——正则表达式变得有点复杂。在 URL 中查找属性最好由 slug 处理,但是 slug 通常可以由字母、数字和基本标点符号的任意组合组成。属性的 slug 是一种更具体的格式,以地址中的街道号开始,以邮政编码结束。中间的街道名称仍然可以是任何东西,但它总是被数字包围。

这个简单的事实有助于形成用于从 URL 中捕获 slug 的正则表达式。这里的想法是尽可能的具体,这样一个 URL 模式不会干扰其他可能寻找相似模式的模式。单个Property对象的详细视图的 URL 配置如下所示:

from django.conf.urls.defaults import *

from django.views.generic import ListView, DetailView

from properties import models

urlpatterns = patterns(''

url(r'^$'

ListView.as_view(queryset=models.Property.objects.listed()

template_name='properties/list.html'

paginate_by=25)

name='property_list')

url(r'^(?P<slug>\d+-[\w-]+-\d+)/$'

DetailView.as_view(queryset=models.Property.objects.listed()

slug_field='slug'

template_name='properties/detail.html')

name='property_detail')

)

此正则表达式为段的开头和结尾的数字添加了显式规则,用破折号与中间部分隔开。这将像通常的[\w-]+一样匹配属性 slugs,但有一个重要的额外好处:这些 URL 现在可以放在站点的根目录下。拥有更具体的正则表达式允许更小的 URL,如 http://example.com/123-main-st-12345/ 。这是一个保持 URL 小而整洁的好方法,同时不会妨碍其他可能使用 slugs 的 URL 配置。

现在怎么办?

一些应用就位并准备好协同工作后,一个基本的站点就成形了。下一章将展示如何把你到目前为止学到的所有工具结合起来,为这样的应用添加重要的新特性。

Footnotes 1

http://prodjango.com/modelform/