Grok-1-0-Web-开发指南-全-
Grok 1.0 Web 开发指南(全)
原文:
zh.annas-archive.org/md5/c9fdc46646d0594ac4be07c1fd791801译者:飞龙
前言
目前有许多 Web 开发框架可供选择。Grok 是许多用 Python 编程语言编写的框架之一,但它可能是最不为人知的。本书是让更多人知道 Grok 可以非常适合许多类型的 Web 开发项目的第一步。
例如,Grok 基于一个名为 Zope Toolkit(ZTK)的软件库,这是一个用于 Python Web 开发的庞大库集合。ZTK 本身是超过十年工作的结果,始于 Zope Web 框架,这是 Python Web 框架中的第一个。
Grok 也是一个现代的 Web 框架,这意味着它也从像 Django 或 Ruby on Rails 这样的流行 Web 框架的创新中吸取了经验。总的来说,随着我们阅读本书,你会发现 Grok 是一个足够敏捷的框架,适用于小型应用程序,同时对于真正复杂的项目来说也足够强大。
本书涵盖的内容
第一章,了解 Grok 深入探讨了为什么 Grok 是 Python Web 开发的吸引人选择。你将了解 Grok 如何利用 Zope Toolkit 以及为什么这使得 Grok 强大且灵活。然后,将介绍 Grok 的一些最重要的概念。最后,你将简要了解 Grok 与其他 Web 开发框架的比较。
第二章,开始使用 Grok 展示了如何在不同的平台上安装 Python,以防你对它不熟悉。你将了解 Python 包索引(PyPI)是什么以及如何使用 EasyInstall 从网络上快速安装包。接下来,你将使用名为 grokproject 的工具创建并运行你的第一个项目。
第三章,视图解释了视图是什么以及在哪里定义 Grok 应用程序代码中的视图。对于模板,Grok 使用 ZPT 模板语言,因此你将学习如何使用它并看到最常见语句的实际应用示例。为了测试这一知识,我们将看到如何仅使用视图编写一个完整的 Grok 应用程序。其他涵盖的主题包括如何从网络请求中获取表单参数,如何向 Grok 应用程序添加静态资源,以及如何创建和使用额外的视图。
第四章,模型介绍了模型的概念以及它们与视图之间的关系。在其他关键主题中,你将学习如何在 ZODB 上持久化模型数据,以及如何结构化你的代码以保持显示逻辑与应用逻辑的分离。接下来,我们将了解容器是什么以及如何使用它。最后,我们将解释如何使用多个模型并将特定的视图关联到每个模型。
第五章,表单,将从自动表单的快速演示开始。我们将简要介绍接口和模式的概念,并展示它们如何用于自动生成表单。在其他方面,你将学习如何过滤字段并防止它们在表单中显示,以及如何更改表单模板和展示。
第六章,目录:面向对象的搜索引擎,将讨论如何使用名为目录的工具在数据库中搜索特定对象。我们将介绍目录是什么以及它是如何工作的,索引是什么以及它们是如何工作的,以及如何在目录中存储数据。接下来,我们将学习如何在目录上执行简单查询,以及如何利用这些知识为我们的应用创建一个搜索界面。
第七章,安全,将涵盖认证和授权。Grok 安全基于主体(用户)、权限和角色的概念。它有一个默认的安全策略,你将学习如何修改它。Grok 有一个可插拔的认证系统,允许我们设置自定义安全策略。我们将创建一个并学习如何管理用户。
第八章,应用展示和页面布局,处理基于视图组件概念的 Grok 布局和展示机制。我们将学习视图组件管理器和视图组件是什么,以及如何使用它们来定义布局。然后,我们将介绍层和皮肤的概念,这些概念允许 Grok 应用提供不同的展示和样式。最后,我们将为应用定义一个替代皮肤。
第九章,Grok 和 ZODB,将告诉我们更多关于 ZODB 的信息,包括如何利用其其他功能,如 blob 处理。我们还将了解一些关于 ZODB 维护和需要频繁打包数据库的需求。最后,我们将尝试将 ZODB 作为常规 Python 库在 Grok 外部使用。
第十章,掌握关系数据库,将帮助我们了解 Grok 在关系数据库访问方面提供了哪些功能。以下是一些我们将要涉及的具体内容:为什么 Grok 允许开发者轻松使用关系数据库很重要,什么是对象关系映射器,如何使用 SQLAlchemy 与 Grok 结合,以及如何更改我们的认证机制以使用关系数据库而不是 ZODB。
第十一章,Grok 背后的关键概念对 Grok 的支柱——Zope 组件架构的主要概念进行了更深入的探讨。我们将从解释主要的 ZCA 概念,如接口、适配器和工具,并使用前十章的代码进行说明开始。我们将通过使用一些模式来扩展我们的应用程序,了解 ZCA 的主要好处之一。最重要的是,我们将介绍如何在不修改其代码的情况下扩展一个包。
第十二章,Grokkers、火星人和敏捷配置将介绍 grokkers。Grokker 是一段代码,允许开发者通过在代码中进行声明而不是使用配置文件来使用框架功能。Grok 使用一个名为 Martian 的库来创建自己的 grokkers,我们也将看到如何创建自己的 grokkers。
第十三章,测试和调试简要说明了测试的重要性,然后解释了如何在 Grok 中进行测试。我们将从扩展 grokproject 提供的功能测试套件开始,然后继续介绍其他类型的测试。最后,我们将介绍一些调试工具,包括实时网络调试器。
第十四章,部署讨论了如何使用标准 paster 服务器来部署我们的应用程序。然后,我们将了解如何在 Apache 服务器后面运行应用程序,首先是通过使用简单的代理配置,然后是使用 mod_wsgi。最后,我们将探讨 ZEO 如何为我们提供应用程序的水平可伸缩性,并简要讨论如何通过添加缓存和负载均衡来使网站支持高流量负载。
你需要这本书什么
为了让这本书中的示例 Grok 应用程序运行,你需要的只是一个运行 Python 编程语言(版本 2.4 或 2.5)的联网计算机。在 Unix 环境中,你还需要一个 C 编译器。
本书面向对象
这本书是为那些想要创建网络应用程序但几乎没有网络开发经验的 Python 开发者准备的。如果你已经使用过其他网络框架,但正在寻找一个能够让你在不失去敏捷性的情况下创建更复杂应用程序的框架,你也将从这本书中受益。读者应大致了解网络应用程序是如何工作的。
习惯用法
在这本书中,你会找到许多不同风格的文本,用于区分不同类型的信息。以下是一些这些风格的示例及其含义的解释。
文本中的代码单词如下所示:"现在我们有了 title 属性,我们可以修改 index.pt 模板,使其显示的不是旧文本"。
代码块是这样设置的:
class SetTitle(grok.View):
def update(self,new_title):
self.context.title = new_title
def render(self):
return self.context.title
当我们希望引起你对代码块中特定部分的注意时,相关的行或项目将以粗体显示:
# Comment this line to disable developer mode. This should be done in
# production
# devmode on
任何命令行输入或输出都按如下方式编写:
$ bin/paster serve --stop-daemon parts/etc/deploy.ini
新术语和重要词汇以粗体显示。你在屏幕上看到的,例如在菜单或对话框中的单词,在文本中显示如下:“在基本类部分下有一个提供的接口部分。”。
注意
警告或重要提示以如下框的形式出现。
注意
小技巧和技巧以如下形式出现。
读者反馈
我们读者的反馈总是受欢迎的。让我们知道你对这本书的看法,你喜欢的或可能不喜欢的。读者反馈对我们开发你真正从中获得最大收益的标题非常重要。
要向我们发送一般反馈,只需发送一封电子邮件到 <feedback@packtpub.com>,并在邮件主题中提及书名。
如果你有一本书需要我们出版,请通过 www.packtpub.com 上的建议标题表单或通过电子邮件 <suggest@packtpub.com> 发送给我们。
如果你在一个主题上具有专业知识,并且你对撰写或为关于该主题的书籍做出贡献感兴趣,请参阅我们的作者指南 www.packtpub.com/authors。
客户支持
现在你已经是 Packt 图书的骄傲拥有者,我们有许多事情可以帮助你从购买中获得最大收益。
小贴士
下载本书的示例代码
访问www.packtpub.com/files/code/7481_Code.zip 直接下载示例代码。
勘误
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果你在我们的书中发现错误,可能是文本或代码中的错误,如果你能向我们报告,我们将不胜感激。这样做可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果你发现任何勘误,请通过访问 www.packtpub.com/support,选择你的书,点击让我们知道链接,并输入你的勘误详情。一旦你的勘误得到验证,你的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的现有勘误列表中。任何现有勘误都可以通过从 www.packtpub.com/support 选择你的标题来查看。
盗版
在互联网上对版权材料的盗版是所有媒体中持续存在的问题。在 Packt,我们非常重视我们版权和许可证的保护。如果你在网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以追究补救措施。
请通过以下链接联系我们 <copyright@packtpub.com>,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们作者方面的帮助,以及我们为您提供有价值内容的能力。
问题
如果您在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。
第一章:了解 Grok
Grok 是用 Python 编程语言编写的 Web 应用程序框架。目前,对于 Python Web 开发框架有很多不错的选择,Grok 可能是其中最不为人知的一个。本书通过开发一个完整的 Web 应用程序来详细介绍 Grok。在这个过程中,本书将让我们了解为什么 Grok 是构建 Web 应用程序的一个非常好的选择,无论它们是小型简单还是大型复杂。在本章中,我们将从以下内容开始:
-
使 Grok 成为 Python Web 开发有吸引力的选项的原因
-
Grok 如何利用 Zope 工具包以及为什么这使得 Grok 强大且灵活
-
Grok 最重要的概念是什么以及为什么它们有用
-
Grok 与其他 Web 开发框架的比较
为什么选择 Grok?
Grok 是一个现代 Web 框架,这意味着:
-
它提供了一种敏捷的开发流程,强调快速生成可工作的代码,但始终关注清晰性和可测试性。它不需要广泛的配置。
-
它提供了一套开发应用程序的约定,包括文件系统布局和代码结构。Grok 的设计鼓励开发者共享标准,并为他们在组织代码时提供了一套良好的实践。这使得经验不足的开发者可以从一开始就变得高效。
Grok 支持与关系数据库的强大集成,包括使用许多对象关系映射器,并且通过使用自己的面向对象数据库提供了一种透明的方式来存储 Python 对象。Grok 为开发强大、可扩展和安全的 Web 应用程序提供了坚实的基础,而不会过于复杂。这在很大程度上是因为它在其核心使用了Zope 工具包,这是一个旨在供项目重用以开发 Web 应用程序或框架的库集合。这个库集合使用了一种称为Zope 组件架构(ZCA)的通用组件开发模型。
ZCA 是在 Zope 社区中开发的,该社区已经使用 Python 开发 Web 应用程序超过十年。Grok 基于这一经验,提供了一种强大的经验和敏捷的结合。当使用 Grok 时,您将获得:
-
一个核心框架已经发展了十多年,始终在学习新的经验教训并寻求改进,基于在所有类型的生产环境中部署的项目的大量经验。
-
一种灵活、前瞻性的架构,允许开发者在长时间内创建可以演变和扩展的应用程序,甚至可以由其他开发者进行,有时甚至不需要触及原始代码。
-
一个实用、知识渊博的开发社区,这个社区已经存在很长时间,并且愿意提供关于如何最好地学习和利用框架的建议。
-
一系列集成功能,例如国际化、表单生成和验证、安全性、编目和模板化。
-
一大批现成的组件,可以用作构建您自己应用程序的基石。与 Zope 3 本身一样,这些组件通常经过彻底测试,具有良好的 API 文档。
Grok 为 Web 开发者提供了我们应用程序的一系列有用功能,这些功能可以轻松地作为依赖项集成到我们的代码中。
区分 Grok 概念
Grok 与其他现代 Web 开发框架有许多共同之处,但它也提供了一些非常独特的功能,使其与众不同。在这里,我们将简要介绍其中三个:
-
组件架构
-
对象数据库
-
对象发布和遍历的概念
Zope 组件架构
Grok 的一个显著特点是它使用组件架构,旨在使开发者能够创建独立组件,这些组件可以自由重用和交换。
组件仅仅是执行特定功能的对象。在 ZCA 中,这种功能通过接口正式声明,接口是描述对象各种方法和属性的类。实现与某个其他组件相同接口的组件可以轻松地替换它,这为开发者提供了以不同方式组合组件和用较新或更好的实现替换任何特定组件的选项。
ZCA 包括两种类型的组件:适配器和实用工具。适配器 是扩展另一个组件以提供额外数据、功能或展示能力的组件。实用工具 是提供某种服务的独立组件,例如数据库连接、加密或邮件投递。在 Grok 中,术语 内容组件 用于定义与这些适配器和实用工具交互的代码。
例如,用于描述现代 Web 框架的常见范例被称为 模型/视图/控制器,或简称 MVC。模型 是内容本身,通常存储在关系型数据库中,以及定义如何修改它的业务规则;视图 是用户看到的 HTML 页面,以及与之交互的页面;控制器 是从视图中收集动态数据并将其推送到模型进行处理的代码,以组装包含结果的新 HTML 页面。
在 MVC 术语中,模型是提供数据的组件,在 Grok 中被称为内容组件。视图和控制器都是作用于该组件的适配器,前者提供展示能力,后者提供额外的功能。
使用 ZCA 为 Grok 开发者提供了几个优势,我们得到了一个扩展其他对象的标准方式,甚至可以用来从外部添加功能,而无需触及它们的代码。此外,我们能够创建可重用的组件,并使用在其他地方开发的现有组件。除此之外,我们还得到了一个全局注册表,我们可以用它轻松地创建自己的组件注册表,从而避免代码重复。
这可能听起来有些抽象,所以想象一个实际的 Web 应用程序,其中我们有一些类型的对象代表商店目录中的项目。假设这个应用程序是由第三方开发的,但我们需要添加一个网页来显示每个项目的特定信息,具体取决于浏览目录的用户位于世界的哪个地方。在 Grok 中,我们可以通过创建一个独立包中的新视图并使用 ZCA 通过一行代码将其与原始对象连接起来,轻松地做到这一点。根本不需要触及第三方应用程序。
对象数据库
大多数 Python Web 框架使用关系数据库来存储它们的对象数据,对于复杂的数据结构来说,这可能意味着数据分散在不同的表中,然后通过使用复杂的查询将它们连接起来。由于 Python 是一种面向对象的语言,因此直接存储对象更有意义,Zope 3 通过使用其自己的对象数据库 ZODB 来实现这一点。
ZODB是一个具有负载均衡能力的事务性对象存储,并且是 Zope 从最初就定义的概念之一。它允许开发者几乎透明地处理他们的对象并将它们持久化(只需从持久化类继承即可)。无需担心在读取时组合对象以及在写入时拆分它们。
ZODB 是 Grok 中可以独立在任何 Python 环境中使用的组件之一。
对象发布和遍历
在其他 Web 框架中,URL 通常被映射到代码中,这些代码调用正确的模板或返回请求的结果。在 Grok 中,URL 被映射到对象上,这意味着任何对象都可以有一个相应的视图。
如果你将 ZODB 存储视为一棵树,你可以可视化 Grok 如何解析 URL,从树的根节点开始,然后到分支,再到子分支,直到达到要显示的对象实例。这个过程称为“遍历”。
遍历机制“只需工作”,这意味着在实践中,你可以有任意长度的嵌套层次结构,只要为你的所有对象类提供默认视图(在 Grok 中称为索引视图),就无需担心。
相比之下,许多 Python 网络框架使用“URL 路由”的概念,这需要使用某种正则表达式语法将已知的路径分配给数据库对象,该语法涉及数据库中的对象属性,如 ID,以及一系列可能的操作。从应用程序外部修改这些路由并不容易。
除了默认的遍历机制外,Grok 还允许使用自定义遍历方法,这些方法可以返回任意对象实例和视图。这可以用于遍历关系映射器查询的结果,例如。
其他 Grok 功能
Grok 为网络应用开发提供了许多其他有用的功能。这里仅列举其中的一小部分。
集成安全模型
Grok 拥有一个非常细粒度的安全模型,基于权限和角色。它允许开发者将特定的权限分配给特定的视图,然后将这些权限映射到不同的角色。它使用 ZCA 允许可插入的组件用于身份验证和授权。
简单使用现有的 Python 库和模块
当然,Python 网络框架非常强大,因为它们允许轻松访问可用的众多 Python 库和模块,从 Python 的标准库开始。Grok 允许开发者利用这一软件库,当然,但这还不是最好的部分。使用 ZCA,我们可以在不修改其代码的情况下向从这些模块实例化的对象添加视图和功能,从而使我们更容易地重用它们。
静态资源
Grok 为所有静态文件(如图像、CSS 样式表或 JavaScript 文件)保留了一个特殊的目录名称。您可以使用这些资源,就像在其他任何网络开发环境中一样。
关系数据库访问
Grok 包含强大的 ZODB,用于透明的对象持久化,但当然,在某些情况下,关系数据库更适合存储某些类型的数据。Grok 还通过使用对象关系映射器(如 SQLAlchemy)提供轻松访问关系数据库的功能,这样我们就可以兼得两者之长了。
Python 网络服务器网关接口兼容性
Grok 可以配置为 WSGI 应用程序,并与其他 Python 应用程序一起插入到管道中。这使得开发者能够将不同的网络框架和应用程序组合成一个单一站点,从而在可能的情况下使用最佳的应用程序。
Grok 与其他网络框架的比较
使用过或熟悉其他网络框架的开发者可能想知道 Grok 与它们相比的情况。尽管完整的比较超出了本书的范围,但关于流行框架的几句话可能有所帮助。
PHP
PHP 允许快速的开发速度,但不鼓励结构化和安全的网络开发实践。Grok 提供了敏捷但结构化程度更高的体验,从一开始就鼓励良好的实践。Grok 还具有清晰的关注点分离。其模板语言 ZPT 专注于展示,与 PHP 不同,PHP 本身将所有内容组合到一个模板中,避免了过多的应用程序逻辑。
Java 框架
与 Java 框架类似,Grok 是为了开发大型和复杂的应用程序而设计的。来自 Java 网络框架的开发者会发现 Grok 的约定将大大减少配置文件的使用。Grok 使用动态语言,没有编译周期大大加快了开发周期。Python 语言使得从命令行测试事物变得更加容易。
Ruby on Rails
Grok 受 Ruby on Rails 的启发很大。Rails 是最早将敏捷哲学阐述为两个一般原则的框架之一:不要重复自己 (DRY) 和 约定优于配置。DRY 意味着避免在不同地方重复相同的代码或信息,Rails 在其开发实践中强烈执行这一点。约定优于配置意味着许多框架方面和代码结构有合理的默认值,而不是依赖于显式的配置来工作。Grok 将这些概念铭记在心,并将它们与 Zope 工具包和 Zope 组件架构相结合,提供了一种强大而独特的开发平台。
Django
Django 是最受欢迎的 Python 网络开发框架之一。其关键卖点之一是为其应用程序自动生成管理界面,这是 Grok 所不具备的。Django 在使用 约定优于配置 和 不要重复自己 原则方面与 Grok 类似。它还提供了 Python 编程语言的全部功能,并希望成为解决网络开发问题的全栈解决方案。与 Grok 不同,其架构专注于构建基于关系数据库的应用程序,并将第三方组件集成到其堆栈中,同时仍然保持管理界面等功能的运行,这并不容易。
Pylons
Pylons 更像是一个最小化的网络框架,而 Grok 正在尝试提供一套完整的组件。使用 Pylons 的开发者可能会在 Grok 中找到一个更强大的框架,但会牺牲一些最小化框架所提供的灵活性。
摘要
在本章中,我们介绍了 Grok 并说明了为什么它是现代网络应用程序开发的绝佳选择。在下一章中,我们将看到如何将 Grok 应用到实践中,并将构建我们的第一个简单应用程序并使其运行。
第二章:开始使用 Grok
现在我们已经对 Grok 及其历史有所了解,让我们开始使用它吧。当然,首先要做的是安装 Grok 所依赖的程序。幸运的是,大部分工作将自动为我们完成,但有三项关键程序你可能需要手动安装:Python、一个 C 编译器(Windows 系统上不需要),以及 EasyInstall。另外,请注意 Grok 会通过网络安装自身,因此需要互联网连接。
在本章中,我们将涵盖:
-
如何在不同的平台上安装 Python
-
Python 软件包索引(PyPI)是什么
-
如何使用 EasyInstall 通过网络快速安装 PyPI 上的软件包
-
virtualenv如何让我们为开发工作设置干净的 Python 环境 -
如何使用
grokproject创建项目 -
如何使用 paster 运行应用程序
-
Grok 管理员用户界面是什么,以及如何使用它
由于 Python 支持许多不同的平台,Grok 几乎可以在任何地方安装。在本章中,我们将提供在三个特定平台上安装它的说明:Unix/Linux、Mac OS X 和 Windows。
获取 C 编译器
如果你使用 Linux 或 Mac OS X,你的第一步是获取一个编译器。Grok 依赖于 Zope 工具包,该工具包在其源代码中包含一些 C 扩展,因此我们需要一个编译器来构建这些扩展,在大多数平台上。
Windows 用户无需担心这个问题,因为 Grok 安装过程使用的是预编译包,但其他系统确实需要安装编译器,以便构建 Grok。
许多 Linux 发行版在默认设置中包含编译器,因此对于这些发行版不需要采取任何行动,但 Ubuntu 需要安装一个包含编译器和其他开发工具的特殊软件包。在这种情况下,软件包名称是 build-essential,你可以通过以下命令安装它:
# sudo apt-get install build-essential
在 Mac OS X 系统中,系统 DVD 上的开发者工具包中包含了一个编译器。更多信息请参阅developer.apple.com/tools/xcode/。
安装 Python
Grok 最伟大的优势之一是它是用 Python 编写的。Grok 需要 Python 版本 2.4 或 2.5 来运行(在撰写本文时,2.6 版本的支持即将到来),但请注意,它不能在最近发布的 3.0 版本上运行,因为该版本与旧版本的 Python 不兼容,因此大多数库和框架仍然不支持它。
在 Unix/Linux 上安装 Python
Unix/Linux 发行版通常已经预装了 Python,所以你的系统可能已经安装了合适的 Python 版本。要查看你的版本,请在 shell 提示符下输入命令 python V;你将得到类似以下的结果:
# python -V
Python 2.5.2
如果您遇到错误,这意味着 Python 没有安装到您的机器上,因此您必须自行安装它。这在任何主流 Linux 发行版中都不太可能发生,但可能在某些 Unix 变体中发生。尽管如此,这种情况非常罕见,因此您应该得到一个类似于上一个示例的版本号。
如果您的版本号是 2.4 或 2.5,您可以使用系统上的 Python 安装来开发 Grok。然而,根据您的 Linux 发行版,您可能需要额外的包。一些发行版将 Python 的开发库和头文件捆绑在单独的包中,因此可能需要在您的系统上安装这些包。
例如,如果您使用 Ubuntu 或 Debian,您还需要安装 python-dev 包。您可以通过使用命令行轻松完成此操作。
# sudo apt-get update
# sudo apt-get install python-dev
其他发行版可能需要安装不同的包。请参阅您系统的文档以获取说明。
如果您没有安装 Python,您应该能够使用系统包管理器轻松安装它,类似于我们设置附加包的方式:
# sudo apt-get install python
如果您有 Python,但不是 2.4 或 2.5 版本,那么您的 Linux 发行版很可能包含了 2.5 版本的包。在 Ubuntu 或 Debian 中,您可以使用以下命令:
# sudo apt-get install python2.5
您不一定必须使用系统版本的 Python。一些开发者更喜欢从源代码手动编译自己的 Python。这可以给您带来更多的灵活性,并且还可以避免包冲突,当不同的 Python 框架或工具在同一系统上安装时可能会发生包冲突(请注意,在不编译自己的版本的情况下,也可以通过使用本章其他地方描述的 virtualenv 工具实现类似的目标)。
要从源代码安装,您需要从 www.python.org/download/ 下载所需的版本,并完成 Unix/Linux 软件包所需的常规 configure-build-install 循环。请记住使用 Python 2.4 或 2.5。
在 Mac OS X 上安装 Python
Mac OS X 总是预装了 Python,但由于 Mac 发布周期,安装到您计算机上的版本可能是一年甚至两年前的版本。因此,在许多情况下,Python-Mac 社区成员建议您安装自己的版本。
如果您的 OS X 版本是 10.5,您应该可以使用已安装的 Python 版本。对于更早的版本,您可能需要查看 www.python.org/download/mac/ 以获取可用的安装程序和建议。
Grok 与 Mac OS X 最新版本中包含的网络包Twisted存在已知的依赖冲突,因此最好为 Grok 使用单独的 Python 环境。当然,您可以按照本章末尾的在 Unix/Linux 上安装 Python部分所述构建自己的版本。如果您出于某种原因不习惯从源代码构建自己的 Python,建议的方法是使用虚拟环境。这将在本章后面进行介绍。
在 Windows 上安装 Python
除了源代码下载外,Python 发行版还附带了一个非常好的 Windows 安装程序。要在 Windows 上安装 Python(我们建议您使用至少 XP 或 Vista),您只需从www.python.org/download/选择一个 Python 版本,然后从提供的选项中选择下载相应的.msi安装程序。
下载安装程序后,双击安装文件,选择一个合适的目录来安装 Python(默认设置应该没问题),然后您就可以开始使用 Python 了。由于您需要从命令行使用 Grok,您可能希望将 Python 安装路径添加到系统路径中,这样您就可以从任何目录轻松访问 Python 解释器。
要这样做,请转到 Windows 控制面板,单击系统图标,然后选择高级选项卡。从那里,单击环境变量按钮,并从系统变量窗口(位于底部)中选择Path。单击编辑,将显示一个带有文本框的窗口,您可以在其中编辑当前值。请确保将当前值保持原样,并在末尾添加 Python 安装路径。路径是您运行安装程序时选择的路径,通常形式为C:\PythonXX,其中 XX 代表不带点的 Python 版本(例如,C:\Python25,如以下截图所示)。使用分号将此路径与系统路径中已存在的路径分开。您还可能希望在后面添加C:\PythonXX\Scripts,这样我们将在本章中安装的 Python 脚本也可以从任何位置调用。

要使用 Grok,您还需要安装win32all包,该包包括 Win32 API、COM 支持和 Pythonwin。此包还附带安装程序,因此应该很容易设置。只需访问sourceforge.net/projects/pywin32/files,下载与您已安装的 Python 版本对应的win32all版本。下载后,只需运行安装程序,一切就绪。
EasyInstall 和 Python 包索引(PyPI)
Python 包索引(PyPI)是 Python 软件的仓库,其中提供了数千个可供下载的软件包。您可以在那里找到许多种类的库和应用程序,Zope 和 Grok 都有很好的代表,您有数百个软件包可供使用。
使 PyPI 变得更有力的一个因素是名为 easy_install 的 Python 脚本,它允许 Python 开发者通过网络安装 PyPI 上索引的任何软件包,同时跟踪依赖关系和版本。可以轻松安装的软件包要么打包为压缩文件,要么使用特殊格式,即 .egg 扩展名,并被称为 Python eggs。
easy_install 模块是 setuptools 软件包的一部分,因此您需要安装它才能获取它。在 setuptools PyPI 页面上有 Windows 安装程序和 Unix/Linux/Mac 的 .egg 文件可供下载,网址为 pypi.python.org/pypi/setuptools。
要在 Windows 上安装 setuptools,只需运行安装程序。对于 Unix/Linux/Mac 系统,将 .egg 文件作为 shell 脚本运行,如下例所示(您的版本可能不同):
# sh setuptools-0.6c11-py2.4.egg
许多 Linux 发行版都包含 setuptools 的软件包。例如,在 Ubuntu 或 Debian 中,您可以使用 apt-get 来安装它:
# sudo apt-get install python-setuptools
然而,我们建议您手动安装最新版本,即使您的系统有可用的软件包也是如此,因为这样您可以确保获得最新版本。
之后,easy_install 脚本将出现在系统 Python 的路径上,从那时起,您可以使用以下命令在您的系统上安装 PyPI 上的任何软件包:
# sudo easy_install <package name>
可能您已经安装了 setuptools,但您想要轻松安装的某些软件包可能需要更近期的版本。在这种情况下,您将收到一个错误信息,告知您这一事实。要快速更新您的 setuptools 版本,请使用以下命令:
# sudo easy_install -U setuptools
-U 开关告诉 easy_install 获取软件包的最新版本,并就地更新之前的版本。在 Windows 系统上,easy_install 命令是相同的。只需在开头省略 sudo 这个词即可。
如前所述,Grok 和您可以使用 Grok 和纯 Zope 一起使用的数十个软件包,可在 PyPI 上找到,因此我们将使用 easy_install 安装 Grok 所需的软件包。但首先,我们将学习如何为我们的 Grok 开发工作设置一个干净的环境。
Grok 和 Python 环境
Grok 使用相当多的软件包。如果其他大型 Python 软件包,甚至是一系列较小的 Python 软件包,在相同的 Python 安装下安装,有时可能会出现依赖关系问题或版本冲突。这是因为,通过 easy_install 或其他 Python 安装方法安装的 Python 软件包的代码通常存储在 Python 库的 site-packages 目录中。
Grok 通过将它们放置在 .buildout/eggs 目录中来隔离其包,但 Python 解释器仍然会查找 site-packages 以找到所需的包,这意味着如果另一个 Python 工具安装了 Grok 所用库的不同版本,可能会发生冲突。
这可能不会给你带来问题,除非你使用其他基于 Zope 的技术,例如 Plone。如果你是从 Grok 开始的,最简单的做法可能是直接安装它,但如果在那里遇到任何版本冲突,有一个工具可以帮助你摆脱混乱;它被称为 virtualenv。
Virtualenv
virtualenv 是一个 Python 包,允许创建独立的 Python 环境。这是一种避免 site-packages 目录内的冲突包干扰你的 Grok 应用程序的方法。
可以使用 easy_install: 安装 virtualenv。
# sudo easy_install virtualenv
安装完成后,你可以通过为任何新项目创建环境来使用它。例如,为了为 Grok 创建一个测试环境,前往你主目录下的任意目录,并输入:
# virtualenv --no-site-packages no-site-packages no-site-packages testgrok
此命令将创建一个名为 testgrok 的目录,其中包含子目录 bin 和 lib(Windows 下的脚本和 Lib)。在 bin 目录中,你可以找到 python 和 easy_install 命令,它们将在你刚刚创建的虚拟环境上下文中运行。这意味着 python 将使用 testgrok 下的 lib 目录来运行 Python 解释器,而 easy_install 将在该 site-packages 目录下添加新包。
--no-site-packages 选项告诉 virtualenv,系统 Python site-packages 目录下的现有包都不应该在新 virtualenv 中可用。建议在为 Grok 构建环境时使用此选项。然而,如果你有多个不同的环境,这些环境倾向于使用相同的通用库,你可以在主 Python 环境下安装这些库,只需在虚拟环境中添加每个应用程序所需的包即可。在这种情况下,不应使用 no-site-packages 选项,但你需要非常仔细地规划你的设置。
在 virtualenv 内工作的时候,你必须记得使用 python 和 easy_install 命令的完整路径,否则你可能会无意中在主 Python 环境中安装包,或者使用与你预期不同的包集运行你的应用程序。为了防止这个问题,virtualenv 的 bin 目录(Windows 下的脚本目录)中包含了一个名为 activate 的批处理脚本。一旦运行它,所有后续的 python 和 easy_install 命令都将使用它们的 virtualenv 版本,直到你使用相应的 deactivate 脚本结束会话。
使用以下命令在 Unix/Linux/Mac 下激活 virtualenv 的 testgrok 环境:
# source testgrok/bin/activate
在 Windows 上:
> testgrok\scripts\activate
由于buildout本身负责这一点,因此对于 Grok 不需要activate和deactivate脚本;这里只是提及以示完整。
使用 grokproject 安装 Grok
我们终于准备好安装 Grok 了。为了便于创建项目以及基本目录结构,Grok 使用了grokproject包,可以使用easy_install来安装。前往你想要创建应用程序的目录(如果你使用virtualenv,请进入我们在上一节中创建的testgrok virtualenv内部)。现在输入以下命令:
# easy_install grokproject
你现在已经安装了它,但请记住easy_install的-U开关,它允许你在原地更新一个包,因为grokproject正在持续开发中,频繁更新是个好主意。现在我们可以创建我们的第一个 Grok 项目。
创建我们的第一个项目
如前所述,我们刚刚安装的grokproject包是一个用于创建项目的工具。Grok 项目是一个目录,它构成了一个工作环境,其中可以开发 Grok 应用程序。它是一个模板,包括一个简单的可执行应用程序,可以用作开发的基础,并作为 Grok 中事物通常去向的指南。
创建项目非常简单。让我们使用 Grok 创建传统的 hello world 示例:
# grokproject helloworld
grokproject将作为参数接受将要创建项目的目录名称。运行命令后,你将立即被要求输入管理员用户名和密码。请注意这一点,因为稍后你需要它来运行应用程序的第一次。
一旦它有了所需的信息,grokproject将下载并安装 Grok 所需的 Zope Toolkit 包,以及 Grok 本身。这可能需要几分钟,具体取决于你的网络连接速度,因为 Grok 由许多包组成。下载完成后,grokproject将配置 Grok,并设置 Grok 应用程序模板,使其准备好使用。
运行默认应用程序
要运行与项目工作空间一起创建的默认应用程序,我们需要启动 Grok。让我们将当前目录更改为我们的 hello world 示例所在的目录,并执行该操作。
# cd helloworld
# bin/paster serve parts/etc/deploy.ini
注意,该命令需要直接在项目目录中运行。请不要切换到bin目录并尝试在那里运行paster。一般来说,所有项目脚本和命令都旨在从主项目目录运行。
此命令将在 8080 端口启动 Grok,这是它的默认端口。现在你可以通过打开网页浏览器并将它指向http://localhost:8080来最后看到 Grok 的实际运行情况。
如果出于某种原因,您必须在系统上使用不同的端口,您需要编辑parts/etc/目录内的deploy.ini文件。此文件包含 Grok 的部署配置。您将找到设置端口的行(文件底部附近)。只需将其更改为您想要的任何数字,然后再次运行paster。以下是文件相关部分的示例:
[server:main]
use = egg:Paste#http
host = 127.0.0.1
port = 8888
当您访问该 URL 时,您将看到一个登录提示。在这里,您必须使用您在上一节创建项目时选择的登录名和密码。之后,您将进入 Grok 管理界面(参考以下截图)。

要创建默认应用程序的副本,您只需在标签为命名您的应用程序的文本框中输入名称,然后点击创建按钮。例如,可以命名为hello。之后,您将在页面上看到一个新部分,显示已安装的应用程序,其中hello应该是列表中唯一的,如下一张截图所示。您可以从那里点击名称,或者将浏览器指向http://localhost:8080/hello,以查看正在运行的应用程序。

此时,您的浏览器应显示一个非常简单的 HTML 页面,其中有一条消息告诉您 Grok 正在运行,如下面的截图所示:

要停止服务器,您需要按Ctrl + C,这将让您重新控制 shell。要重新启动它,只需再次运行paster serve命令。
Grok 项目内部有什么?
如我们之前提到的,我们在上一节中创建项目时使用的grokproject命令(幕后)使用了一个名为zc.buildout的工具,这是一个用于管理可重复的开发和生产环境的系统。buildout负责下载 Grok 的所有依赖项,并在项目目录下构建和安装它们。它还安装了运行 Grok 所需的所有脚本,例如我们之前使用的paster命令。
我们将在本书的后面部分更详细地探讨buildout及其目录结构。现在,只需注意主项目目录中的文件是buildout的一部分。实际的 Grok 应用程序将存储在src目录下。
让我们来看看与我们所创建的“hello world”Grok 应用程序特别相关的目录和文件。
应用程序结构概述
“hello world”应用程序代码存储在grokproject在本书早期为我们创建的helloworld目录下的src子目录中。让我们来看看那里存储的文件:
| 文件 | 描述 |
|---|---|
app.py |
包含应用程序的模型和视图 |
app_templates |
存储应用程序模板的目录 |
app.txt |
应用程序的功能测试 |
configure.zcml |
Zope 3 XML 配置文件 |
ftesting.zcml |
功能测试的 XML 配置 |
__init__.py |
此文件存在是为了使目录成为一个包 |
startup.py |
WSGI 应用程序工厂 |
static |
静态资源目录,例如图像和 CSS |
tests.py |
包含应用程序测试代码 |
我们将在后续章节中详细介绍所有这些文件,但到目前为止,最重要的要点是:
-
文件
app.py包含实际的应用程序代码,在这个例子中是最小的。 -
当我们执行应用程序时在浏览器窗口中显示的消息来自存储在
app_templates目录中的index.pt模板。 -
一个 XML 配置文件,主要用于加载 Grok 的配置。
创建我们的第一个模板
在上一节中,我们展示了一个仅包含 hello world 应用程序可见部分的模板。这个模板可以在我们的 Grok 项目的 src/app_templates 目录中找到。为了让我们对 Grok 有所了解,让我们更改这个模板并添加我们自己的消息。
从 helloworld 目录中,使用你喜欢的文本编辑器打开 src/app_templates/index.pt 文件。该文件具有以下内容:
<html>
<head>
</head>
<body>
<h1>Congratulations!</h1>
<p>Your Grok application is up and running.
Edit <code>testdrive/app_templates/index.pt</code> to change
this page.</p>
</body>
</html>
将文件更改为如下所示:
<html>
<head>
</head>
<body>
<h1>Hello World!</h1>
<p>Grok the caveman says hi.</p>
</body>
</html>
实际上,你可以更改消息以显示你喜欢的任何内容;只需注意 Grok 模板需要 XHTML。
最后,保存模板并再次使用 paster 运行服务器实例:
# bin/paster serve parts/etc/deploy.ini
如果你之前让服务器运行过,由于页面模板、图像和 CSS 的修改可以立即查看,因此无需重新启动服务器以进行此更改。
在你的浏览器中打开 URL http://localhost:8080/hello。你应该看到以下截图类似的内容:

Grok 说你好。现在欢呼吧,你已经完成了你的第一个 Grok 应用程序。
Grok 管理员用户界面
在你命名并启动 hello world 应用程序之前,你已经简要地与 Grok 管理应用程序管理器进行了交互。让我们更详细地看看这个管理员用户界面中的三个标签页。
应用程序
在此标签页中,你可以管理所有你的应用程序实例。对于你定义的任何应用程序,你将看到其点状类路径,例如在我们的例子中是 helloworld.app.Helloworld,以及一个用于创建和命名实例的文本框和按钮。
一旦创建了一个应用程序实例,你将在页面顶部看到它,以及你之前可能创建的任何其他应用程序。从那里,你可以通过点击其名称来启动应用程序,或者可以通过使用其左侧的复选框和相应的按钮来删除它或重命名它。
您还可以使用应用程序名称旁边的对象浏览器链接来检查实例,并查看其基类、属性、属性和方法。例如,您可以探索类或模块的 docstrings 中包含的文档。您可以在运行默认应用部分的第二个截图下查看 Grok 管理界面中的对象浏览器链接。
服务器控制
服务器控制标签页允许您查看服务器进程信息,例如运行时间、平台和软件版本。它还允许您启动、停止或重启服务器进程,前提是您以守护进程方式运行它,而不是从命令行运行。请参阅下一张截图,以查看此标签页中找到的信息示例。

由于 Grok 用于持久化的 ZODB 是事务性的并且支持撤销,因此随着越来越多的对象被添加并不断修改,它的大小往往会变得很大。为了保持您项目数据库的整洁有序,建议您定期“打包”它。打包过程删除了您对象的旧版本,只保留最新信息,从而减小文件大小。
您可以从服务器控制面板中打包 Grok 的 ZODB。只需选择您希望保留对象信息的天数,然后点击打包按钮。0的值会清除所有之前的对象修订版本,只保留所有对象的最新版本。打包过程在后台运行,因此您的应用程序在打包过程中仍然可以处理请求。
您可以从此标签页使用的一个最后功能是管理消息,它允许您输入一条消息,该消息将在 Grok 管理界面的每一页上显示给所有管理员,直到有人重置文本。
文档
文档标签页提供了到 DocGrok 包和对象浏览器的链接,这些链接允许您查看运行中的 Grok 进程下大多数事物的所有信息。这包括但不限于对象、类、模块、函数和文本文件。
摘要
在本章中,我们安装了 Grok 及其依赖项,并创建了我们的第一个 Grok 项目和应用。
现在我们已经可以创建自己的 Grok 项目了,是时候学习如何定义和创建不同类型的视图,并使用这些知识开发我们的第一个工作应用了。
第三章。视图
在上一章中,我们创建了我们的第一个 Grok 项目,并学习了如何启动一个简单的 hello world 应用程序。现在我们将学习关于视图和模板的内容,它们构成了我们 Web 应用程序的表现层。为此,我们将创建一个简单的应用程序来管理待办事项列表。目标是到本章结束时,创建一个可以允许用户创建和管理任意数量列表、向其添加条目并标记为完成的应用程序。
为了达到我们的目标,我们将在本章中探讨以下主题:
-
视图是什么以及在哪里定义 Grok 应用程序代码中的视图
-
如何使用 ZPT 模板引擎,以及最常见的语句在行动中的示例
-
如何仅使用视图编写完整的 Grok 应用程序
-
如何从 Web 请求中获取表单参数
-
如何向 Grok 应用程序添加静态资源
-
如何创建和使用额外的视图
-
如何使用不需要关联模板的视图
Grok 视图
Web 应用程序只是一系列网页,通过允许用户输入一些数据、以各种方式处理这些数据并向用户展示一些结果或确认,帮助用户完成一个或多个任务。在 Web 应用程序框架中,允许用户查看正在发生的事情的网页和允许他捕获信息的 Web 表单被称为视图。
视图通常通过使用某种页面模板来实现,但在 Grok 中,我们也可以有独立的 Python 代码来处理更复杂的逻辑,使其更容易。实际上,我们甚至可以使用仅使用 Python 代码而不使用模板来渲染视图。这给开发者提供了更多的权力(实际上是 Python 的全部权力),并允许视图表现和视图逻辑的清晰分离,从而在模板和 Python 两侧都产生更易读的代码。
我们在第二章的末尾已经处理了视图的模板部分,当时我们修改了helloworld项目中的index.pt文件。除了这个页面模板之外,如果你查看该项目src/helloworld目录中的app.py文件,你将看到视图的 Python 部分。以下是helloworld应用程序的完整源代码:
import grok
class Helloworld(grok.Application, grok.Container):
pass
class Index(grok.View):
pass # see app_templates/index.pt
在这种情况下,视图的 Python 代码仅由一个类声明组成,我们从这个类继承自grok.View,这是基本的 Grok 视图。由于我们只是将要展示模板,我们不需要更多的代码,但如果我们查看类的名称,我们可以看到 Grok 的一个约定正在起作用:Index。正如类定义之后的注释所说,这个类的模板将在application目录下的app_templates/index.pt中找到。Grok 不需要在代码或配置中被告知这一点,因为按照约定,模板名称将与类名相同,模板名称为小写,并附加.pt扩展名。
要向应用程序添加新的视图,我们只需要定义一个从grok.View继承的类,并在app_templates文件夹内创建相应的模板。然后我们可以通过使用模板名称在 URL 中引用这个视图。注意,名称index.pt是一个特殊情况,它代表另一个约定,因为具有该名称的视图将被视为应用程序的默认视图,这意味着在 URL 中不需要指定其名称。
Zope 页面模板 (ZPT)
对于 HTML 生成,Grok 使用Zope 页面模板(ZPT)。它们很好地符合 Grok 的哲学,因为它们设计背后的一个驱动原则是严格分离逻辑和展示。尽可能的情况下,视图模板应只包含展示和结构逻辑,以及视图类中的名称和方法调用。
ZPT 的另一个重要设计原则是与编辑工具良好地协同工作,从而允许设计师从开发者那里取回模板代码,同时仍然能够看到并处理页面的完整 HTML 表示,而不会丢失任何逻辑。这是通过使用有效的 HTML/XHTML 作为页面模板来实现的。
模板属性语言 (TAL)
为了实现与编辑工具良好协作的目标,ZPT 使用模板属性语言(TAL)。这种语言中的语句使用带有 XML 命名空间的 HTML 属性,这基本上意味着它们以“tal:”和冒号作为前缀,例如tal:content或tal:replace。编辑工具通常会忽略不是常规 HTML 的一部分的语句,因此它们会保留 TAL 语句不变。
让我们快速概述一下最重要的 TAL 语句和结构。这里我们将介绍基础知识,但完整的参考可以在docs.zope.org/zope2/zope2book/AppendixC.html找到。
让我们通过一个简单的例子开始我们的 TAL 介绍:
<h1 tal:content="python:5*5">Me Grok can multiply</h1>
content是一个 TAL 语句,它保持标签不变,但用引号中的表达式结果替换其内容。在这种情况下,当页面渲染时,我们将得到以下内容:
<h1>25</h1>
注意,标签仍然是一个<h1>标签,但标签的内容会变为表达式的结果,每次页面渲染时都会动态计算。使用所见即所得(WYSIWYG)工具的设计师会看到“一个简单的乘法”这样的文字,但仍然能够正确地看到页面的预期结构。
按设计,ZPT 不是一个通用编程语言,因此复杂的代码最好放在视图类中的 Python 代码中。然而,仍然可以根据某些条件重复标签、省略或显示它们,甚至包括其他页面模板的部分。这通常足以处理大多数页面结构,即使不是这样,也可以在 Python 代码中生成 HTML 的一部分,并将其插入模板中。
注意
ZPT 不是唯一可以与 Grok 一起使用的模板引擎,因为它被精心设计为允许可插拔的引擎。在撰写本文时,有两个这样的引擎的包可用:megrok.genshi 和 megrok.chameleon。开发者可以选择更适合他风格的模板引擎,并使用它而不是 ZPT。
表达式类型
在上一个示例中,我们使用了表达式 python:5*5 来获取乘法的结果。这些被称为 Python 表达式,可以在冒号之后包含任何有效的 Python 表达式。当然,Grok 的一个原则是明确分离表现和逻辑,我们希望避免使用非常长的表达式。但 Python 表达式有时非常有用,尤其是在处理条件或列表、字典等 Python 对象时。
有几个原因说明将大部分逻辑保留在模板之外是一个好主意。首先,如果我们能使用 Python 开发工具,调试和测试代码会容易得多。此外,通过使用这种策略,我们可以潜在地改变应用程序的表现(模板)而不必触及主代码。
虽然还有其他类型的表达式。ZPT 中的默认表达式类型被称为路径表达式。以下是一个示例:
<p tal:content="request/URL">Me Grok want current URL</p>
它被称为路径表达式的原因是因为它以变量名开头,并使用斜杠来分隔对该变量子对象的调用,返回路径中最后一个对象的调用结果,或者如果它不可调用,则返回该对象本身。在前面的示例中,我们通过从 request 到 URL 的路径从特殊名称 request 获取当前 URL。
如果路径的某个组件未找到,将发生错误,但在这种情况下,可以回退到其他对象或值。为此,我们使用 | 符号来分隔可能的表达式值。例如,要从请求中获取一些参数:
<span tal:content="request/param1|request/param2|nothing">Me Grok will settle for any value here</span>
在这个路径表达式中,我们寻找请求参数 param1;如果它未定义,我们使用 param2 的值,如果两个都没有定义,则使用特殊值 nothing。此值等同于 Python 的 None 值,因此如果发生这种情况,span 标签内将没有任何内容,但不会发生错误。
除了路径和 Python 表达式之外,还有一种称为字符串表达式的第三种类型。当你需要将任意字符串与路径表达式的结果组合时,这些表达式非常有用。以下是一个示例:
<p tal:content="string:Me Grok web page is at this URL: ${request/URL}"></p>
冒号之后的所有内容都被视为要显示的字符串,除了字符串中带有 $ 标记的路径表达式和这些表达式的结果会被替换。当路径表达式由多个部分组成(当我们必须使用 / 分隔符时)或者它与其他字符在字符串中未分隔时,必须将表达式括在 {} 大括号内,就像前面的示例一样。要插入 $ 符号,使用两个 $ 符号,如下所示:
<p tal:content="string: Me Grok is richer by $$ $amount"></p>
如果变量amount的值为 45,前面的表达式将输出以下 HTML:
<p>Me Grok is richer by $ 45</p>
插入文本
我们已经看到content语句会替换一个标签的所有内容,包括任何嵌套的标签。还有一个replace语句,它可以完全删除标签,并在其位置插入所需的文本。
<span tal:replace="string:Me Grok need no tag here">text</span>
在这种情况下,<span>标签只是一个占位符标签,因为当模板渲染时它不会被输出。
除了插入文本外,replace语句还可以用于在页面上包含占位内容,这对于一些想要使用更详细的模拟网页进行演示的设计师来说可能很有帮助,而最终渲染的页面上不会出现这些内容。为此,我们只需将 HTML 替换为特殊名称nothing:。
<p tal:replace="nothing">Me Grok will not use this content</p>
重复标签
当处理 HTML 表格和列表时,我们通常会需要为 Python 序列中的每个项目添加一个表格行或列表项,例如列表或元组。例如,我们可能有一个用于显示为 HTML 无序列表的武器列表,名为'Grok the caveman'。repeat语句会导致标签及其所有内容在序列中的每个元素上重复一次。为了了解它是如何工作的,假设我们有一个作为视图属性传递给模板的武器列表,名称为weapon:。
<h1>Grok's Arsenal</h1>
<ul>
<li tal:repeat="weapon python:view.weapons" tal:content="weapon">Weapon</li>
</ul>
如果weapon包含列表['Rock','Club','Spear'],模板将渲染如下:
<h1>Grok's Arsenal</h1>
<ul>
<li>Rock</li>
<li>Club</li>
<li>Spear</li>
</ul>
repeat语句接受两个参数。第一个是循环变量的名称,它将按顺序分配列表中每个元素的值。第二个是一个返回元素存储序列的表达式。注意我们如何使用选定的名称weapon,结合content语句,在<li>标签内插入当前武器的名称。
可以嵌套多个repeat语句,这就是为什么我们需要给循环变量命名。这个名称也可以用来确定我们在列表中的位置,通过结合使用特殊的repeat变量。例如,表达式repeat/weapon/number将返回 1 作为第一个元素,2 作为第二个元素,依此类推。表达式repeat/weapon/index做同样的事情,但起始值为 0。
条件元素
condition语句用于在渲染时决定是否在页面上显示一个标签及其内容。它评估传递给它的表达式,如果结果为假,则删除该标签。当表达式为真时,标签会正常显示。
为了帮助“穴居人”更好地跟踪他的武器库,我们可以将列表转换成表格,同时显示他每种武器的数量。当物品数量被认为过少时,可以在每一行添加一个有用的提示。在这种情况下,view变量将包含一个字典列表,例如这个:[{'name':'Rock','quantity':10},{'name':'Club','quantity':1},{'name':'Spear','quantity':3}]。要显示表格,我们可以使用以下标记:
<table>
<tr>
<th>Weapon</th>
<th>Quantity</th>
<th>Notes</th>
</tr>
<tr tal:repeat="weapon view/weapons">
<td tal:content="weapon/name">Weapon</td>
<td tal:content="weapon/quantity">Quantity</td>
<td tal:condition="python:weapon['quantity']>=3">OK</td>
<td tal:condition="python:weapon['quantity']<3">Need to get more!</td>
</tr>
</table>
这将生成以下 HTML:
<table>
<tr>
<th>Weapon</th>
<th>Quantity</th>
<th>Notes</th>
</tr>
<tr>
<td>Rock</td>
<td>10</td>
<td>
OK
</td>
</tr>
<tr>
<td>Club</td>
<td>1</td>
<td>
Need to get more!
</td>
</tr>
<tr>
<td>Spear</td>
<td>3</td>
<td>
OK
</td>
</tr>
</table>
关于这段代码,有几个需要注意的地方。首先,注意我们是如何使用路径表达式来引用name和quantity列中的字典键的。路径表达式对于字典键、对象属性和方法的工作方式相同。相比之下,在condition语句的 Python 表达式中,我们必须使用字典语法。其他需要注意的事情是,我们实际上需要重复相同的<span>标签两次,条件不同。渲染的模板只显示那些为真的标签。在这种情况下,我们具有互斥的条件,但在其他情况下,我们可能有多个条件,具有独立的真或假值。
变量
有时候,我们需要在整个页面模板中重复使用表达式。我们可以在多个地方重复表达式,但这会导致它被多次评估,这是低效的。对于这些情况,ZPT 提供了一个define语句,允许我们将表达式的结果分配给一个变量。以下是一个模板片段的例子,其中变量定义会有所帮助:
<ul>
<li tal:repeat="weapon view/weapons">
<span tal:replace="weapon">weapon</span> is weapon
<span tal:replace="repeat/weapon/number">number</span> of
<span tal:replace="python:len(view.weapons)">total number of weapons </span>
</li>
</ul>
在这个例子中,表达式view/weapons在repeat语句中计算一次,然后对于列表中的每个项目再计算一次。对于weapons列表中的每个项目,列表的长度也只计算一次。如果我们使用define语句,这可以避免:
<ul tal:define="weapons view/weapons;total_weapons python:len(weapons)">
<li tal:repeat="weapon weapons">
<span tal:replace="weapon">weapon</span> is weapon
<span tal:replace="repeat/weapon/number">number</span> of
<span tal:replace="total_weapons">total number of weapons</span>
</li>
</ul>
define语句接受变量的名称和用于其值的表达式,两者之间用空格分隔。请注意,我们可以在单个语句中具有多个定义,由分号分隔。现在,假设我们需要在列表顶部添加一个标题,显示武器总数:
<h1>Grok's Arsenal (<span tal:replace="python:len(view.weapons)"> number</span> total weapons)
</h1>
我们不能仅在<h1>标签中定义长度,因为define语句的作用域仅限于使用它的标签及其内容。尽管如此,我们仍然希望有一个可以重复使用的定义。在这种情况下,可以使用global关键字:
<h1 tal:define="global weapons view/weapons;total_weapons python:len(weapons)">Grok's Arsenal (<span tal:replace="python: total_weapons">number</span> total weapons)
</h1>
一旦使用global关键字定义了一个变量,它就可以在模板的任何地方重复使用,不受页面标签结构的限制。
关于定义的一个重要注意事项是,路径表达式始终返回路径中最后一个元素的调用结果。因此,当我们与可调用对象一起工作时,我们可能会将错误的东西分配给变量,并弄乱页面。这有时很难调试,所以尽可能使用 Python 表达式是个好主意,因为它们更明确。
这并不意味着我们不能使用路径表达式(毕竟,它们是默认类型),但我们必须确保我们得到我们想要的价值。当我们需要获取一个对象而不是调用该对象的结果时,我们可以使用特殊的nocall路径表达式来获取它:
<p tal:define="object nocall:view/object">Me Grok need object</p>
特殊变量
Grok 还为每个页面模板提供了一些特殊变量,这样模板作者就可以引用不同的应用程序对象和视图。
-
view:使用这个特殊名称,可以访问与模板关联的视图类的所有方法和属性。 -
context:上下文名称指的是正在查看的模型,并且允许访问其方法和属性。 -
request:请求对象包含当前网络请求的数据。 -
static:这个特殊名称允许创建指向静态资源的 URL。
修改 HTML 标签属性
除了在页面中插入文本外,通常还需要在渲染时动态定义 HTML 标签的属性。attributes语句允许我们做到这一点。例如,让我们为 Grok 这个穴居人的武器库中的每件武器添加描述页面的链接:
<h1>Grok's Arsenal</h1>
<ul>
<li tal:repeat="weapon view/weapons">
<a tal:content="weapon" href="" tal:attributes="href string:${request/URL}/${weapon}">Weapon</a>
</li>
</ul>
在这个例子中,我们修改链接的href属性,使其使用当前页面的 URL,并附加武器的名称,假设列表中的每件武器都存在一个具有该名称的武器描述页面。
与define语句一样,attributes语句允许使用分号分隔的多个属性定义。
插入结构
由于安全原因,使用content和replace标签插入的字符串会被引号括起来,以便转义任何 HTML 标签,所以<括号变成<;而>括号变成>;。这导致 HTML 标签在渲染的页面上显示,而不是被解释为 HTML。这对于防止一些跨站脚本攻击很有用,但也会妨碍从 Python 代码生成 HTML。在表达式之前使用structure关键字,告诉 ZPT 返回的文本应该被解释为 HTML,并像页面上的其他标签一样渲染。
例如,如果我们假设变量 text 包含以下 HTML:
<p>This is the text</p>
这个标签将转义 HTML:
<div tal:content="text">The text</div>
生成的 HTML 将看起来像这样:
<div><p>This is the text</p></div>
让我们使用structure关键字:
<div tal:content="structure text">The text</div>
现在我们得到以下结果:
<div><p>This is the text</p></div>
一个标签中的多个语句
在一些早期的例子中,我们在单个标签内使用了多个 TAL 语句。由于 XML 不允许在标签内重复属性,我们可以在给定的标签中只使用每种类型的 TAL 语句中的一个,并且我们可以根据需要将它们组合起来,除了content和replace语句,它们是互斥的,并且不能在同一个标签中使用。
当在一个标签中使用多个语句时,需要知道的最重要的事情是它们执行的顺序是固定的。我们如何放置它们在标签中无关紧要,它们将按照以下顺序执行:
-
define -
condition -
repeat -
content/replace -
attributes
因此,如果我们需要在这个列表中较低位置的语句在较高位置的语句之前执行,我们必须添加所需的<div>或<span>标签来绕过固定的顺序。例如,假设我们想在repeat语句的每个元素中定义一些变量。我们不能在repeat语句本身所在的标签中添加define语句,因为define在repeat之前执行,所以我们会因为循环变量在那个点未定义而得到错误。
一个可能的解决方案是在repeat语句之后使用<span>标签来定义变量,如下面的示例所示:
<h1>Grok's Arsenal</h1>
<ul>
<li tal:repeat="weapon view/weapons">
<span tal:define="weapon_uppercase python:weapon.upper()" tal:content="weapon_uppercase">
Me Grok want uppercase letters
</span>
</li>
</ul>
一个稍微更好的解决方案是使用具有 XML 命名空间的标签tal,它将作为结构标记但不会出现在最终的页面渲染中:
<h1>Grok's Arsenal</h1>
<ul>
<li tal:repeat="weapon view/weapons">
<tal:weapon define="weapon_uppercase python:weapon.upper()" content="weapon_uppercase">
Me Grok want uppercase letters
</tal:weapon>
</li>
</ul>
我们不使用<span>标签,而是使用<tal:weapon>。这并不是因为 ZPT 有一个weapon标签;技巧在于名字中的tal部分。在这里我们可以用任何其他东西代替weapon。请注意,由于标签本身明确使用了tal命名空间,当使用 ZPT 时,同一标签中的define和content语句不需要以它为前缀。
宏和插槽
许多网站在每一页上使用标准元素,例如标题、页脚、侧边栏等。ZPT 的一个不错之处在于它允许我们重用这些元素,而无需在各个地方重复它们。用于执行此操作的机制称为宏和插槽。
宏是一个声明为单个实体并赋予名称的页面或页面的一部分。一旦完成此声明,所命名的标记就可以在不同的页面中重用。这样,我们可以有一个单页宏来定义我们网站的外观和感觉,并且让其他每一页看起来都像它,最重要的是,当宏更改时,它会自动更改。
宏的定义使用 HTML 属性,就像 TAL 语句一样。宏定义语言被称为宏扩展模板属性语言,或简称METAL。以下是一个示例:
<div metal:define-macro="about">
<h1>About Grok</h1>
<p>Grok is a friendly caveman who likes smashing websites</p>
</div>
define-macro语句创建了一个带有引号中给定名称的宏,在这个例子中是about。这个名字被添加到页面上定义的宏列表中,这个列表被恰当地命名为macros。我们可以在页面模板中定义任意数量的宏,只要我们为每个宏使用不同的名称。要使用宏,我们需要通过使用所需的名称来访问创建它的页面的macros属性。假设about宏定义在名为main.pt的页面模板中,我们就可以在任意其他页面中使用这个宏,如下所示:
<p metal:use-macro="context/main.pt/macros/about">
About
</p>
当这个其他模板被渲染时,整个<p>标签会被包含宏的<div>标签所替换。这就像是将宏中的 HTML 复制并粘贴到包含use-macro语句的标签位置一样。
通过使用宏,你可以轻松地在多个页面中重用大块或小块的 HTML,但使这一概念更加有用的是插槽的概念。将插槽想象为现有宏内部自定义 HTML 的占位符。尽管 HTML 的一般结构保持不变,但使用宏的模板可以在渲染时填充这些占位符,从而比简单的宏提供更多的灵活性。
最有效的使用插槽的方法是在全页宏中定义部分,这些部分可以通过不同的模板使用宏来填充。这样我们就可以在一个地方定义我们网页的结构,并且让整个网站都能从中受益。
让我们定义一个页面宏来展示这些概念的实际应用:
<html metal:define-macro="page">
<head>
<title tal:content="context/title">title</title>
</head>
<body>
<div metal:define-slot="header">
<h1 tal:content="context/title">Grok's Cave</h1>
</div>
<div metal:define-slot="body">
<p>Welcome to Grok's cave.</p>
</div>
<div metal:define-slot="footer">
<p>Brought to you by Grok the caveman.</p>
</div>
</body>
</html>
注意我们是如何在第一个标签内部定义宏的,这样整个页面就是一个大宏。然后,使用define-slot语句,我们定义了三个插槽,分别对应于页眉、页脚和主体。其他模板可以重用这个宏,并填充一个或多个这些插槽来定制最终的页面渲染。以下是实现方法:
<html metal:use-macro="context/main.pt/page">
<div fill-slot="body">
Me Grok likes macros!
</div>
</html>
fill-slot语句接受一个插槽的名称,并用使用该语句的标签开始的节的内容替换其内容。main.pt模板中的其他内容将完全按照其出现的方式使用。这是渲染后的 HTML 看起来像这样:
<html>
<head>
<title>example</title>
</head>
<body>
<div>
<h1>example</h1>
</div>
<div>
<p>Me Grok likes macros!</p>
</div>
<div>
<p>Brought to you by Grok the caveman.</p>
</div>
</body>
</html>
超越 ZPT 基础
虽然我们确实介绍了一些概念,包括在模板中插入文本、重复标签、条件、变量、属性和宏,但这只是一个对 ZPT 的简要介绍。更多详细信息,可以参考 Zope 书籍,该书籍可在docs.zope.org/zope2/zope2book/在线获取。
待办事项列表应用程序
现在我们已经了解了 ZPT,让我们从待办事项应用程序的代码开始。想法是用户将有一个网页,他可以通过列表来管理他的任务。他不仅可以使用单个列表,还可以创建多个列表来处理高级任务,每个列表包含多个较小的任务。应用程序将允许用户创建带有描述的列表,向其中添加任务,并在完成时勾选它们。他可以添加他需要的任何数量的列表,并在任何时候删除它们。
这一切都非常简单,所以现在列表管理器将使用一个带有相关模板的单个视图。随着我们进入本章的后续内容,我们将添加更多功能。
当然,第一步是创建一个新的 Grok 项目:
# grokproject todo
现在,我们将使用 Python 列表来保存我们的待办事项列表。每个列表将是一个包含标题、描述和项目的字典。我们的下一步将是创建一个模板来显示所有列表及其项目。我们将用以下代码替换 app_templates 中 index.pt 模板的内 容:
<html>
<head>
<title>To-Do list manager</title>
</head>
<body>
<h1>To-Do list manager</h1>
<p>Here you can add new lists and check off all the items that you complete.
</p>
<tal:block repeat="todolist context/todolists">
<h2 tal:content="todolist/title">List title</h2>
<p tal:content="todolist/description">List description</p>
<ul>
<li tal:repeat="item todolist/items" tal:content="item">item 1</li>
<li tal:replace="nothing">item 2</li>
<li tal:replace="nothing">item 3</li>
</ul>
</tal:block>
</body>
</html>
检查代码。确保你理解我们使用的所有 ZPT 语句。如有必要,再次查看 ZPT 部分。我们假设特殊名称 context 将包含在名为 todolists 的属性中的列表,并且它通过使用 repeat 语句遍历所有列表。我们还在其中嵌套了一个 repeat 语句,以列出每个待办事项列表中的所有项目。
在做任何事情之前,使用你的网络浏览器的打开文件选项打开 index.pt 文件。注意我们如何得到一个页面一旦有真实数据的预览(见以下截图)。这是使用 ZPT 的一大好处。我们明确利用了这一特性,通过在列表中使用 replace="nothing" 技巧添加了两个虚拟列表项。

现在我们来看看它在 Grok 中的工作情况。请记住,模板假设在 context 中将有一个名为 todolists 的列表可用。这实际上将指向应用程序模型,但为了现在,我们只需在那里硬编码一些值来查看模板是如何渲染的。我们稍后会使用一个真实模型。打开 src 目录中的 app.py 文件,并将其修改如下:
import grok
class Todo(grok.Application, grok.Container):
todolists = [{
'title' : 'Daily tasks for Grok',
'description' : 'A list of tasks that Grok does everyday',
'items' : ['Clean cave',
'Hunt breakfast',
'Sharpen ax']
}]
class Index(grok.View):
pass
现在启动应用程序:
# bin/paster serve etc/deploy.ini
通过使用管理控制面板创建一个应用程序,并点击其链接。将显示一个类似于以下截图的屏幕。注意它与之前我们打开的预览模板的相似性。

处理表单数据
现在我们可以正确地显示待办事项列表及其项目,但我们需要一种方法来添加新的列表和项目,我们还需要能够以某种方式勾选它们。我们需要表单来处理这些操作,所以让我们修改模板,使其看起来像这样:
<html>
<head>
<title>To-Do list manager</title>
</head>
<body>
<h1>To-Do list manager</h1>
<p>Here you can add new lists and check off all the items that you complete.
</p>
<tal:lists repeat="todolist context/todolists">
<form method="post" tal:attributes="action view/url">
<fieldset>
<legend tal:content="todolist/title">
title</legend>
<p tal:content="todolist/description">
description</p>
<div tal:repeat="item todolist/items">
<span tal:content="item/description">
</span>
</div>
</fieldset>
</form>
</tal:lists>
</body>
</html>
我们现在为每个列表使用一个表单,并且每个表单都有相同的操作,该操作在渲染时通过使用attributes语句定义。Grok 视图始终有一个url方法,它返回视图 URL,因此我们在这里所做的就是将表单提交给自己。我们将在添加视图的 Python 代码时稍后处理表单提交。
为了在视觉上分隔列表,每个列表都包含在一个fieldset中,其中包含其标题和描述。为了列出单个项目,我们丢弃了第一个模板中的<ul>,并使用了一个<div>,因为我们现在需要有一个复选框来标记已完成的项目。让我们添加复选框,同时提供一个在添加项目的同时从列表中删除项目的方法:
<div tal:repeat="item todolist/items">
<input type="checkbox" tal:attributes="name string:item_{repeat/item/index}; checked item/checked"/>
<span tal:content="item/description"></span>
<input type="submit" tal:attributes="name string:delete_${repeat/item/index}" value="Delete"/>
</div>
<br/>
<input type="hidden" name="list_index" tal:attributes="value repeat/todolist/index"/>
<input type="submit" name="update_list" value="Update list"/>
<br />
注意使用repeat/item/index变量来命名每个项目,根据其在列表中的位置,这样我们就可以在视图中单独引用它们。我们还需要知道项目属于哪个列表,这就是为什么每个列表表单都有一个隐藏的输入字段,其中包含其索引,使用repeat/todolist/index来获取该值。最后,我们有一个更新列表按钮,用于一次性检查和取消选中多个列表项。
现在所需的就是一种向列表中添加新项目的方法。我们只需在之前的代码中添加一个用于项目描述的文本区域和一个用于添加项目的提交按钮。这里并没有什么特别之处:
<label for="item_description">Description:</label><br/>
<textarea name="item_description"></textarea>
<input type="submit" name="new_item" value="Add item"/>
在显示所有列表之后,我们想要另一个表单来添加新的列表。这个表单也提交给自己。将此代码放置在上述</tal:lists>标签之后。
<form method="post" tal:attributes="action view/url">
<fieldset>
<legend>Create new list</legend>
<label for="list_title">Title:</label>
<input type="text" name="list_title"/><br/>
<label for="list_description">Description:</label>
<br/>
<textarea name="list_description"></textarea><br/>
<input type="submit" name="new_list" value="Create"/>
</fieldset>
</form>
视图模板已经准备好了。现在让我们创建视图类。记住,在 Grok 中,视图通常由一个用于 UI 的模板和一个用于将计算数据传递给模板并处理任何表单输入的类组成。由于我们现在有各种将表单数据发送到视图的提交按钮,我们需要一种处理这些数据的方法。最简单的方法是定义一个update方法,Grok 知道在渲染页面模板之前调用该方法。
要获取表单数据,我们将使用在每个视图类中都可用的request变量。该变量包含当前HTTP请求的所有数据,包括表单字段。使用request.form,我们将得到一个包含所有可用表单字段的字典,字段的name属性用作键。每个提交按钮都有自己的名称,因此我们可以检查其是否存在,以查看需要执行哪个操作。
将app.py修改为以下代码:
import grok
class Todo(grok.Application, grok.Container):
todolists = []
class Index(grok.View):
def update(self):
form = self.request.form
if 'new_list' in form:
title = form['list_title']
description = form['list_description']
self.context.todolists.append({'title':title, 'description':description, 'items':[]})
return
if 'list_index' in form:
index = int(form['list_index'])
items = self.context.todolists[index]['items']
if 'new_item' in form:
description = form['item_description']
items.append({'description':description, 'checked':False})
return
elif 'update_list' in form:
for item in range(len(items)):
if 'item_%s' % item in form:
items[item]['checked'] = True
else:
items[item]['checked'] = False
return
else:
for item in range(len(items)):
if 'delete_%s' % item in form:
items.remove(items[item])
首先,在应用程序定义中,我们删除之前添加的测试数据,将todolists属性定义为空列表,这样当我们启动应用程序时,我们就有一个干净的起点。
在update方法中,我们通过使用self.request.form将表单数据分配给form变量,正如之前所讨论的。然后我们有三种可能的情况。
-
第一种情况:提交了添加列表的表单,在这种情况下,
new_list的名称将在表单中。 -
情况二:按下了添加项目、删除项目或更新特定列表的按钮之一,因此
list_index隐藏字段将在表单中。 -
情况三:没有发生表单提交,例如当第一次访问视图时,因此不应采取任何操作。
在新列表的情况下,我们获取标题和描述字段的值,然后简单地将一个包含空项目列表的新字典附加到应用程序的todolists变量中。
如果按下了特定的列表按钮之一,那么我们就从表单中获取列表索引,并使用它来获取受影响列表的列表项。如果按下了new_item按钮,我们获取项目描述,并将一个包含此描述和选中键的字典附加到以跟踪其开/关状态的字典上。如果按下了update_list,我们遍历每个列表项,如果它在表单上存在,则将其状态设置为True,否则设置为False。最后可能的情况是当按下列表项的删除按钮时,因此我们遍历项目并删除任何在表单上出现的名称。
运行应用程序并稍微玩一下。它看起来不太美观,但它具有我们在本章开头概述的完整功能。以下截图显示了测试运行的外观:

添加静态资源
现在我们有一个运行中的应用程序,所以下一步将是让它看起来更好。显然,我们需要使用 CSS 添加一些样式。此外,列表项右侧的删除按钮太分散注意力了,所以让我们使用一个小型垃圾箱图标代替。
在这种情况下,我们将处理静态资源,如图像和样式表,而不是动态模板和类。为了处理这些类型的资源,Grok 按照惯例使用一个名为static的目录,你将在与项目源相同的目录中找到它。我们可以简单地将我们的静态文件放在这个目录中,Grok 将能够找到它们并为它们生成链接。
我们在这里将使用的图标可以在书的代码中找到,位于项目的static目录下。如果你要跟随这个示例,你需要在你项目的该目录中放置一些图标。对于样式表,在那个目录中创建一个名为styles.css的文件,并添加以下 CSS 代码:
body {
background-color: #e0e0e0;
padding: 15px;
}
h1 {
background-color : #223388;
font-size: 1.6em;
color : #ffcc33;
padding : 10px;
margin: 0px;
}
h2 {
background-color: white;
margin: 0px;
padding: 10px;
font-size: 0.8em;
}
.todolist {
background-color: white;
padding: 10px;
margin: 0px;
}
.todolist fieldset {
border: none;
padding: 0px;
}
.todolist legend {
color: #223388;
font-weight: bold;
font-size: 1em;
}
.todolist p {
padding: 0px;
margin: 2px 0px 5px 3px;
color: gray;
font-size: 0.8em;
}
这只是一个示例。完整的 CSS 文件可以在本书的示例代码中找到。为了使样式真正生效,我们需要对index.pt模板进行一些修改,添加一些类,并将删除按钮修改为使用图像。以下是带有一些解释和更改高亮的代码:
<html>
<head>
<title>To-Do list manager</title>
<link rel="stylesheet" type="text/css" tal:attributes="href static/styles.css" />
</head>
首先,注意文档头部中的链接声明是如何使用 TAL 的attributes语句来设置href属性的。这里的单词static并不指代文件系统目录本身,而是指代一个特殊的视图,该视图在该目录中查找文件并生成指向相应文件的链接。以这种方式创建链接是为了确保无论应用程序安装在哪里,即使我们使用虚拟主机,它都能正确工作。
<body>
<h1>To-Do list manager</h1>
<h2>Here you can add new lists and check off all the items that you complete .</h2>
<tal:block repeat="todolist context/todolists">
<form class="todolist" method="post" tal:attributes="action view/url">
<fieldset>
<legend tal:content="todolist/title">title</legend>
<p tal:content="todolist/description">description</p>
<div tal:repeat="item todolist/items" tal:attributes="class python:item['checked'] and 'done' or 'pending'">
<input type="checkbox" tal:attributes="name string:item_${repeat/item/index}; checked item/checked"/>
<span tal:content="item/description"></span>
<input type="image" tal:attributes="name string:delete_${rep eat/item/index}; src static/bin_closed.png" value="Delete"/><br/>
</div>
<input type="hidden" name="list_index" tal:attributes="value repeat/todolist/index"/>
<input type="submit" class="update_button" name="update_list" value="Update list"/><br/>
<label for="item_description">New item:</label><br/>
<input type="text" size="60" name="item_description"><br/>
<input type="submit" class="new_button" name="new_item" value="Add to list"/>
</fieldset>
</form>
</tal:block>
<form class="add" method="post" tal:attributes="action view/url">
<fieldset>
<legend>Create new list</legend>
<label for="list_title">Title:</label>
<input type="text" name="list_title" size="40"/><br/>
<label for="list_description">Description:</label><br/>
<textarea name="list_description" rows="3" cols="50"></textarea><br/>
<input type="submit" class="new_button" name="new_list" value="Create"/>
</fieldset>
</form>
</body>
</html>
我们添加的类属性相当直接,但在列表项的情况下,我们希望已选中的项与未选中的项显示不同,因此我们再次使用attributes语句在渲染时动态决定每个项是否被选中(class done)或未选中(class pending),并在每种情况下使用不同的类。
最后,我们将删除按钮的输入类型从submit更改为image,并再次使用特殊的static名称添加了一个指向图标的链接。
就这样。我们不需要停止服务器来查看效果。只需重新加载页面即可查看。Python 代码甚至都没有被修改,模板也只做了最小限度的更改,但当它使用 CSS 时,应用程序看起来却完全不同,如下面的截图所示:

添加一些 JavaScript 的华丽效果
我们的任务应用现在看起来好多了,但我们还可以做一些改进,让它感觉更好。每次勾选一个任务时都必须按下更新按钮,这有点尴尬,也容易忘记。如果列表在点击复选框时自动更新,那就太好了。
从视觉上看,添加列表项的输入框和按钮有点分散注意力,所以我们可能需要将它们隐藏,直到需要时再显示。我们还可以添加的一个最终视觉触感是将所有已完成的项移动到列表底部,这样就可以清楚地看到哪些已经完成,哪些还没有完成。
对于隐藏表单控件,我们必须修改模板,使新项目标签可点击,并在用户点击时调用 JavaScript 中的切换函数。我们将动态分配onclick属性,以便引用包含控件的<div>的唯一 ID。
<label for="item_description" tal:attributes="onclick string:toggleAddControls('add_controls_${repeat/todolist/index}');"> New item</label><br/>
当然,为了实现这一点,我们必须将控件包裹在一个<div>标签中,并给它分配相同的 ID:
<div class="controls" tal:attributes="id string:add_controls_${repeat/todolist/index}">
<input type="text" size="60" name="item_description"><br/>
<input type="submit" class="new_button" name="new_item" value="Add to list"/>
</div>
就这样。实际上,这非常简单,我们可以为创建列表控件做同样的操作。唯一需要注意的是,这里我们不需要动态 ID,因为只有一个“创建列表”表单:
<legend onclick="toggleAddControls('new_list_controls');">Create new list</legend>
<div class="controls" id="new_list_controls">
<label for="list_title">Title:</label>
<input type="text" name="list_title" size="40"/><br/>
<label for="list_description">Description:</label><br/>
<textarea name="list_description" rows="3" cols="50"></textarea><br/>
<input type="submit" class="new_button" name="new_list" value="Create"/>
</div>
将已完成的项目移动到底部不需要我们在模板中做任何修改。这一切都是使用 JavaScript 和 DOM 完成的。然而,跳过使用更新按钮则有点困难,因为我们需要使用XMLHTTPRequest对象(现在更常被称为 AJAX)从 JavaScript 中调用 Grok 视图。
之前我们提到,视图通常由一个模板和一个类组成。然而,有些情况下模板并不是必需的。我们这里需要的是一个简单的视图,它会设置用户点击的复选框的选中状态。这个视图应该只返回受影响项目的 ID,这样我们就可以动态地更改其 CSS 类以反映变化。
在 Grok 中添加视图非常简单,我们只需要定义一个从grok.View继承的类,然后添加所需的行为。在这种情况下,我们甚至不需要模板,因为我们只将返回项目 ID 作为纯文本。Grok 允许我们在不想使用模板或需要返回除 HTML 之外的内容时,在视图中使用render方法。我们将新的视图添加到app.py中:
class Check(grok.View):
def update(self,list_index,item_index):
self.div_id = 'div_item_%s_%s' % (list_index,item_index)
list_index=int(list_index)
item_index=int(item_index)
items = self.context.todolists[list_index]['items']
items[item_index]['checked'] = not items[item_index] ['checked']
def render(self):
return self.div_id
新的视图类名为Check,因此视图 URL 将使用小写的单词check。这个视图期望传递一个列表索引和一个项目索引来切换列表中某个项目的状态。在这种情况下,我们不需要检查表单,因为我们知道我们始终需要相同的参数。Grok 可以很容易地通过在update方法中指定它们来为我们检索参数,如果其中一个或两个参数不存在,将会发生错误。
注意,首先调用的是update方法,因此我们在视图属性div_id中存储项目 ID,以便在模板实际渲染时能够返回它。然后,我们使用索引来找到正确的项目,并通过使用not运算符切换其状态。
如我们之前提到的,我们将返回一个简单的字符串 ID,因此不需要模板。这就是为什么这个视图有一个render方法,在这种情况下,它只是返回我们之前存储的 ID。在其他情况下,render方法可以做更多的事情,例如生成页面的 HTML,或者发送特定类型的内容,如图片或 PDF 文件。为了做到这一点,除了渲染所需文档的代码外,render方法还必须通过使用特殊的response对象来设置Content-Type,就像返回 XML 的示例一样:
self.response.setHeader('Content-Type','text/xml; charset=UTF-8')
当我们使用render方法时,不需要模板,所以如果 Grok 找到一个应该与这个视图关联的模板,它将发出错误信号以避免歧义。在这种情况下,如果我们把名为check.pt的模板放在app_templates目录中,Grok 将会因为错误而停止。
现在我们有了新的视图,我们需要在用户点击复选框时从模板中调用它。我们也会在这里使用onclick事件处理器:
<div tal:repeat="item todolist/items" tal:attributes="class python:item['checked'] and 'done' or 'pending'; id string:div_item_${repeat/todolist/index}_${repeat/item/index}">
<input type="checkbox" tal:define="check_url python:view.url('check');" tal:attributes="name string:item_${repeat/item/index}; checked item/checked; onclick string:checkItem( '${check_url}?list_index=${repeat/todolist/index}& item_index=${repeat/item/index}')"/>
<span tal:content="item/description"></span>
<input type="image"
tal:attributes="name string:delete_${repeat/item/index}; src static/bin_closed.png" value="Delete"/>
<br/>
</div>
首先,我们需要为每个项目分配一个唯一的 ID,我们通过在 <div> 标签中使用 list 和 item 索引来实现这一点。然后,我们通过使用 TAL 的 attributes 语句来分配 onclick 属性,这样我们就可以动态地构造带有 check 视图名称以及列表和项目索引作为查询参数的 URL。为了避免硬编码 URL 信息,我们使用视图类中的 url 方法,当它用一个字符串作为参数调用时,会返回具有该名称的视图。我们使用 define 语句来做到这一点。
现在,当用户点击复选框时,JavaScript 会通过 GET 请求调用 check 视图,并且获取返回值的 JavaScript 回调函数将使用它来设置正确的 CSS 类以表示选中状态,并重新排列底部的已完成项。
完成这项工作所需的 JavaScript 代码包含在这本书的源代码中。如果您对细节感兴趣,应该学习它。解释这段代码超出了本章的目标。从这个例子中,我们得到的关键点是 Grok 视图能够返回我们想要的任何内容,而不仅仅是模板。
摘要
利用我们新获得的对 ZPT 的知识,我们现在拥有了一个简单但完整的 Grok 应用程序,包括一个很好的设计和 JavaScript 装饰。正如我们设计的,我们学习了 Grok 视图是什么以及如何与之合作的基础知识。在下一章中,我们将看到视图数据来自哪里:定义 Grok 应用的内容对象或模型。
第四章:模型
在第三章中,我们学习了如何通过使用视图在网页上显示信息。实际上,正如我们所看到的,视图机制足够强大,可以创建完整的应用程序,就像我们的待办事项列表管理器。现在,我们将注意力转向这些视图的数据来源:内容对象,或定义 Grok 应用程序的模型。以下是本章我们将涵盖的内容列表:
-
模型是什么以及它与视图有什么关系
-
如何在 ZODB 上持久化模型数据
-
如何结构化我们的代码以保持显示逻辑与应用逻辑的分离
-
容器是什么以及如何使用它
-
如何使用多个模型并将特定视图关联到每个模型
-
如何使用 Grok 内省器导航模型
模型和应用程序
尽管视图执行显示数据和处理用户交互的实际工作,但它们只是显示数据的一种可能方式。在前一章中,我们从一个非常简单的视图开始,没有任何样式或客户端脚本,到本章结束时,我们有了完全不同的应用程序。始终不变的是我们正在处理的东西:待办事项列表。
每当我们添加列表或列表项并勾选项目时,我们都在与应用程序的模型进行交互,在这个例子中,它只由两行代码定义:
class Todo(grok.Application, grok.Model):
todolists = []
Grok 应用程序由一个或多个模型组成。这些模型可以是简单的 Python 对象,但通常使用grok.Model作为基类。应用程序的主要模型也应该继承自grok.Application,这正是前面代码所发生的情况。
模型包含应用程序数据和所有与如何显示这些数据直接无关的逻辑。在我们的待办事项列表管理器中,模型仅由一个todolists类属性组成,它包含所有列表及其项目。
尽管这个模型如此简单,视图是为模型工作而不是相反。如果我们查看index和check视图中的代码,我们会看到每次发生重要事件时,self.context.todolists的值都会被修改。正如我们之前提到的,所有视图都有一个context属性,它指向它们关联的模型。通过它,视图可以访问这个模型的所有属性。
存储模型数据
到目前为止,我们可以成功创建和管理列表,但一旦我们重新启动应用程序,我们就会丢失所有数据。我们需要一种方法来持久化信息。
对于 Web 应用程序,这通常意味着在关系型数据库中创建一些表,并通过直接 SQL 查询或对象关系映射器(ORM)来存储信息。ORM 是一个很好的解决方案,因为它将我们的对象透明地存储在相应的表中。每次我们需要处理我们的对象时,ORM 都会再次从数据库中重新组装它们,而无需我们担心 SQL。
Grok 可以使用纯 SQL 以及多种 Python ORM(如 SQLAlchemy www.sqlalchemy.org 或 Storm storm.canonical.com)在关系数据库中存储数据。然而,Grok 为我们提供了另一个更有趣的选项,它更适合我们的 Python 对象,并且可以比 ORM 更透明地工作:Zope 对象数据库(ZODB)。
ZODB
我们在第一章中讨论了 ZODB,其中我们提到它与 Grok 集成得非常好。现在我们将展示一个存储数据的简单示例,稍后我们将回到细节。
目前,待办事项列表管理应用程序的标题定义在index模板中的<h1>标签内。让我们给每个应用程序实例一个title属性,这样用户就可以自己设置标题。首先,我们将用更现实的代码替换到目前为止我们所使用的简单模型定义:
class Todo(grok.Application, grok.Model):
def __init__(self):
super(Todo, self).__init__()
self.title = 'To-Do list manager'
self.todolists = []
我们添加了一个在应用程序创建时被调用的__init__方法。在这个方法内部,我们确保调用超类__init__方法,这样我们的方法扩展而不是替换原始代码。然后我们定义了两个实例属性,title和todolists。
在 Python 中,有两种类型的对象:可变对象,其值可以更改,和不可变对象,其值不能更改。对于不可变对象,如字符串或数字,我们只需通过从grok.Model继承即可在 ZODB 中获得自动持久性。在标题的情况下,现在我们可以透明地将其存储在 ZODB 中。todolists(因为它们存储在一个列表中,一个可变对象)需要做更多的工作,但稍后我们会讨论。
动态更改标题
现在我们有了title属性,我们可以修改index.pt模板,以显示新文本而不是旧文本。<title>和<h1>标签都需要一个tal:content语句,如下例所示:
<h1 tal:content="context/title">To-Do list manager</h1>
现在,模板将使用存储在应用程序实例中的任何标题。现在我们需要一种让用户设置标题的方法。让我们创建一个简单的 Ajax 视图来完成这个任务:
class SetTitle(grok.View):
def update(self,new_title):
self.context.title = new_title
def render(self):
return self.context.title
所有这些只是检查请求中的键new_title,并将title属性设置为它的值。正如我们在第三章中与Check视图所做的那样,响应不使用模板,而是一个简单的 Python 代码,返回纯文本。我们只是返回在update方法中更改的title属性,以便 JavaScript 回调函数可以在设置后立即更改它。
在客户端,我们使用几个 JavaScript 函数来产生一个原地编辑效果,这样用户就可以点击标题并立即更改它。我们不会在这里深入代码,但你可以找到它在该书的代码包的第四章目录中。你可以在以下屏幕截图中查看结果:

如果我们现在运行应用程序,将能够编辑标题、重新启动应用程序,并看到其值在服务器重启后仍然被保存。不过有一个警告:因为我们已经在 __init__ 方法中向应用程序实例添加了一个属性,所以在尝试这段代码之前,必须删除任何现有的应用程序实例。这是因为 title 属性是在应用程序创建时添加的,当时 __init__ 方法被调用,而在上一章创建应用程序时并没有发生这种情况。如果我们尝试使用较旧的应用程序实例,当我们尝试访问 title 属性时,将会得到一个属性错误。
持久性规则
正如我们之前提到的,todolists 属性不会那么容易持久化到 ZODB。原因是每当对可变属性进行更改时,应用程序需要通知 ZODB 已发生更改。这是通过将实例的特殊 _p_changed 属性设置为 True 来实现的。
在 todolists 属性的情况下,我们只需在修改属性后将其 _p_changed 设置为 True。我们只需在索引和检查视图的 update 方法末尾添加此代码:
self.context.p_changed_ = True
幸运的是,这是我们与 Grok 一起使用 ZODB 时必须记住的唯一规则。好吧,还有一些其他规则,但 Grok 已经遵循了它们,所以这是唯一一个需要我们修改代码的规则。让我们看看 ZODB 的持久性规则:
-
从
persistent.Persistent(ZODB 代码中定义的一个类)或其子类继承。Grok 类grok.Model和grok.Container已经为我们做了这件事,所以通过扩展它们,我们将自动持久化我们的类。 -
类实例必须以层次结构相互关联。ZODB 有一个根对象,该对象包含其他对象,这些对象反过来又可以包含一些其他对象,形成一个树状结构。
-
当修改不是持久化的持久化对象的可变属性时,持久化机制必须通过将实例的特殊
_p_changed属性设置为True来通知。
正如我们所说的,Grok 遵循这些规则,所以通过使用 Grok 的模型和容器,我们自动为我们的应用程序提供了持久数据存储。这样,我们只需考虑类和属性,而无需在心中(和代码中)将它们来回转换成表和列。
我们只需这样做就可以在 ZODB 中存储待办事项列表。我们几乎已经完成了一个完整的应用程序,所以现在让我们关注如何更好地组织我们的代码。
显示逻辑和应用逻辑
我们一直在谈论显示逻辑和应用逻辑的分离,但我们的代码到目前为止显然没有强制执行这一规则。模型只持有列表,而所有其他操作都在视图中进行。
这种方法的缺点在于我们的视图需要了解太多关于模型实现的方式,因此在不修改某些或所有视图的情况下,改变它变得非常困难。
例如,当我们在我们索引视图的update方法中添加一个列表项时,我们必须知道列表项是以description和check键存储为字典的。我们还得知道,项目列表存储在代表列表本身的字典中的items键下。这些都是太多的内部细节,如果模型实现发生变化,依赖于这些知识的视图代码可能需要进行重大修改。
更糟糕的是,依赖于这些知识使得我们的代码比应有的重复性更高,并且在有修改时,我们不得不在多个地方进行相同的更改。看看这个来自待办事项管理器的代码行:
items = self.context.todolists[list_index]['items']
我们只有两个视图,并且这一行出现在它们两个中。如果我们添加更多视图并且需要查看它们中的某些列表项,我们就必须再次重复这段代码。现在假设我们向应用程序添加项目,并希望将列表存储在项目中。在这种情况下,我们必须在每个视图中更改这一行的每个出现,以反映新的结构。
这就是为什么分离显示和应用逻辑如此重要的原因。它有助于结构化我们的应用程序,并允许我们更改数据的显示方式,而无需修改其内部表示,反之亦然。
分离显示和应用逻辑
让我们思考一下如何重构应用程序,以便考虑到这个原则。记住,模型不应该了解任何关于它如何显示的信息,而视图不应该依赖于模型中的任何实现细节。
我们可以从向模型添加一些方法开始,用于添加列表和从列表中获取所有项,这样我们就可以从视图中调用这些新方法,并停止依赖于列表数据在待办事项列表主列表中的存储方式。我们可能会想写点像这样东西:
class Todo(grok.Application, grok.Model):
def __init__(self):
super(Todo, self).__init__()
self.title = 'To-Do list manager'
self.todolists = []
def createList(self, title, description):
self.todolists.append({'title':title, 'description':description, 'items':[]})
self._p_changed = True
def getListItems(self, index):
items = self.todolists[index]['items']
return items
然而,我们必须抵制这种诱惑。如果我们走这条路,我们的代码将充满self._p_changed行,并且我们会像疯狂一样传递列表索引。将待办事项列表表示为简单的列表真的不是一条好路。我们应该使用更精心设计的模型,并充分利用 Grok 的模型类。
使用容器和多个模型
到目前为止,我们正在使用grok.Model作为我们应用程序的基类,这基本上为我们提供了我们看到的几乎透明的 ZODB 存储。然而,大多数非平凡的应用程序都需要多种类型的对象才能有用。此外,使用父级和子级关系组织应用程序数据相当常见,这样主要对象就是父级,并包含多个子对象。在我们的待办事项列表管理应用程序中,主要应用程序是待办事项列表的容器,而每个列表又可以包含多个项目。
因为这是一个非常常见的模式,Grok 提供了一个grok.Container类,它允许我们存储其他模型,并处理 ZODB 持久化。使用它,我们可以更好地组织我们的代码,简化它,并且还可以消除每次对列表或其项目进行更改时向框架发出信号的需求(不再需要self._p_changed行)。我们不再需要处理一个临时的列表结构并跟踪索引,我们可以考虑列表对象和项目对象。
将容器添加到我们的应用程序中
从概念上讲,我们的应用程序将包含一个列表的容器。列表对象也将是一个容器,其中将存储项目。让我们首先定义主要模型:
class Todo(grok.Application, grok.Container):
def __init__(self):
super(Todo, self).__init__()
self.next_id = 0
self.title = 'To-Do list manager'
这与我们之前所拥有的并没有太大区别。重要的变化是我们现在从grok.Container继承,而不是从grok.Model继承。这将允许应用程序存储列表对象。我们还定义了一个next_id属性来为列表创建标识符。好的,现在让我们定义列表模型:
class TodoList(grok.Container):
def __init__(self,list_title,list_description):
super(TodoList, self).__init__()
self.next_id = 0
self.title = list_title
self.description = list_description
列表有title和description属性,两者都是实例创建时的必填参数。与列表模型一样,我们也定义了一个next_id属性来跟踪单个项目。请注意,grok.Application在这里不是一个基类,因为这个是一个将用于之前定义的应用程序中的模型。在某些情况下,我们可能需要在项目中使用多个应用程序,技术上我们可以在同一个文件中定义这两个应用程序,但通常建议我们为不同的应用程序使用单独的文件。
TodoList类也是一个grok.Container,因为它将包含待办事项。这些项目不会包含其他类型的模型,因此列表项目类定义将简单地是一个模型:
class TodoItem(grok.Model):
def __init__(self,item_description):
super(TodoItem, self).__init__()
self.description = item_description
self.checked = False
TodoItem类仅从grok.Model继承。它只有一个description属性,这是实例创建时的一个必填参数。
向模型添加行为
现在我们已经将应用程序模型结构化,我们应该考虑哪个模型将执行哪些操作。我们正在讨论应用程序执行的不同操作,例如列表和项目的创建。然而,在之前的版本中,它们都拥挤在index视图的update方法中。有了干净的模式结构,我们现在可以分离这些操作,并将每个操作放在它更适合的位置。
主应用程序是列表管理器,因此应该将列表创建和删除的方法添加到 Todo 类中。让我们开始编写代码:
def addList(self,title,description):
id = str(self.next_id)
self.next_id = self.next_id+1
self[id] = TodoList(title,description)
def deleteList(self,list):
del self[list]
addList 方法接受 title 和 description,并简单地创建一个 TodoList 实例。新的列表使用键存储在容器中,就像 Python 字典一样工作(实际上,它支持相同的方法,如 keys、values 和 items)。键是使用我们之前讨论的 next_id 属性生成的,然后为下一个列表创建递增。
deleteList 方法甚至更简单,因为我们只需要使用 del 语句从字典中删除所需的键。正如我们承诺的那样,请注意,没有直接处理持久性的代码。列表将在 ZODB 中正确存储,无需显式通知。
TodoList 模型
现在,让我们将注意力转向 TodoList 模型。我们需要一种方法来添加和删除项目,类似于我们在主应用程序中所做的。此外,如果我们想保持非 JavaScript 启用的应用程序版本正常工作,我们需要一种方法来同时更改多个项目的 checked 状态。以下是我们要添加到 TodoList 类中的代码:
def addItem(self,description):
id = str(self.next_id)
self.next_id = self.next_id+1
self[id] = TodoItem(description)
def deleteItem(self,item):
del self[item]
def updateItems(self, items):
for name,item in self.items():
if name in items:
self[item].checked = True
else:
self[item].checked = False
addItem 方法几乎是我们之前看到的 addList 方法的逐字逐句的复制。我们使用 next_id 模式来创建 ID,并创建一个新的 TodoItem 实例。deleteItem 方法与之前讨论的 deleteList 方法相同。updateItems 方法不同,因为它期望一个包含要标记为已检查的项目 ID 的列表。我们遍历列表中的所有项目,如果它们在接收到的列表中,则将它们的 checked 属性设置为 True,否则设置为 False。
TodoItem 模型是三个中最简单的。我们可以直接将 checked 属性设置为 True 或 False,所以我们可能不需要单独的方法来做这件事。我们将只添加一个方便的方法来切换项目的 checked 状态,而无需我们知道当前状态。这将对我们之前创建的 Ajax 启用的检查视图很有用:
def toggleCheck(self):
self.checked = not self.checked
toggleCheck 方法简单地将 TodoItem 的 checked 属性的值设置为当前值的相反,从而起到切换的作用。
现在我们有一个完整的模型,它使用了 Grok 的特性,并包含了所有应用程序逻辑。视图现在将能够执行显示工作,而不会干扰应用程序的内部结构。
重新组织视图
说到视图,我们也需要重构它们。到目前为止,我们一直只用一个视图做所有事情,但这一点需要改变。我们希望保持应用程序只有一个单一的主视图,并在同一页面上显示对不同模型的全部修改,但每个模型的操作调用应来自与该模型关联的视图。这是因为这样 Grok 将正确地设置视图的上下文,我们就不必担心在代码中确保我们作用于正确的模型。记住,我们之前使用了一个隐藏的<input>标签和列表索引来告诉我们的update方法我们想要在哪个列表上操作。如果我们把视图与正确的模型关联起来,我们就不需要这样做。
对于主应用程序,我们将保留index视图和setTitle视图。我们还将添加调用addList和deleteList方法的视图。新的TodoList模型将为它的三个操作(addItem、deleteItem和updateItem)各自提供一个视图。对于TodoItem模型,我们将重用现有的check视图,但与这个模型相关联,而不是主应用程序。
就这样。所以,之前我们有一个单一模型和三个视图,但现在我们有三个模型,每个模型都需要几个视图。Grok 将如何知道哪些视图与哪个模型相关联?
介绍类注解
Grok 的一个约定是,在一个模块中定义了单个模型的情况下,所有定义的视图都将与这个模型相关联。Grok 知道这是唯一可能性,因此自动将所有视图的context属性设置为这个模型。现在我们有三个模型,我们创建的每个视图都必须明确告诉 Grok 它属于哪个模型。
Grok 用来做这件事的机制被称为类注解。Grok 有许多约定,有助于它在没有信息的情况下做出决定,但当我们需要时,我们当然可以告诉 Grok 该怎么做。类注解只是告诉 Grok 关于类的一些信息的声明性方式。
为了明确地将一个视图与一个模型关联,我们使用grok.context类注解。这个类将我们想要关联视图的模型作为参数。让我们重新定义我们的index视图:
class Index(grok.View):
grok.context(Todo)
我们使用grok.context类注解来告诉 Grok 这个视图将使用Todo模型作为上下文。实际上,这就是我们为这个视图需要的所有代码,因为我们已经将模型责任分成了三个不同的视图。不再有满是if语句的巨大update方法。
在继续其他视图之前,让我们稍微思考一下index视图是如何工作的。正如我们在上一章所看到的,如果一个视图的名称是index,它将被视为该模型的默认视图。我们还了解到,视图的名称是自动由其类名的小写版本给出的。现在,对于我们有多个模型并且我们希望每个模型都有一个默认视图的情况,我们该如何命名视图类?在同一个模块中不能定义两个Index类。
明确设置视图名称
如果你猜到类注解会再次发挥作用,你完全正确。对于这类情况,我们有grok.name类注解,它可以用来显式设置视图的名称。目前,我们只需要在我们的代码中有一个index视图,但以后我们可能需要为其他模型添加默认视图,所以我们可以在这个时候修改代码:
class TodoIndex(grok.View):
grok.context(Todo)
grok.name('index')
我们将视图类名更改为TodoIndex,这将导致名为todoindex的 URL。然后我们使用grok.name注解将视图名称设置为index,这样我们就可以调用默认视图而不使用其名称。
现在,让我们看看这个模型的动作视图。首先,是addlist视图,它将被用来向应用程序添加新的列表:
class TodoAddList(grok.View):
grok.context(Todo)
grok.name('addlist')
def update(self,title,description):
self.context.addList(title,description)
def render(self):
self.redirect(self.url('index'))
TodoAddList视图也与Todo模型相关联,但使用不同的名称。当用户填写创建列表表单并点击提交按钮时,将调用此视图。
注意在这段代码中,update方法只有一行长。我们只是调用context的addList方法,并将两个必需的参数传递给它。不需要处理表单参数或进行if检查以查看所需的操作。
视图重定向
在这个应用程序中,我们希望索引页面能够立即显示所有更改。因此,在render方法中,我们不是使用模板生成 HTML,或者像我们之前所做的那样发送一个纯文本字符串,而是使用视图可用的redirect方法将视图重定向到索引视图。
deletelist视图与addlist视图非常相似。我们最初的应用程序版本中没有实现这个功能,但现在你会看到,有了适当的结构,实现这个功能真的很简单:
class TodoDeleteList(grok.View):
grok.context(Todo)
grok.name('deletelist')
def update(self,list):
self.context.deleteList(list)
def render(self):
self.redirect(self.url('index'))
我们只是使用不同的名称并调用模型的deleteList方法,但除此之外代码是相同的。
Todo模型的最后一个视图是我们在本章开头添加的setTitle视图。以下是它的样子:
class TodoSetTitle(grok.View):
grok.context(Todo)
grok.name('settitle')
def update(self,title):
self.context.title = title
def render(self):
return self.context.title
在这种情况下,我们保留了在渲染时返回新标题的旧行为。这里唯一的改变是,与模型的关联是明确的,并且我们给它一个不同于其类名的名称。
与TodoList模型关联的三个视图几乎与Todo模型的addlist和deletelist视图完全相同。让我们看看并讨论一下有什么不同:
class TodoListAddItem(grok.View):
grok.context(TodoList)
grok.name('additem')
def update(self,description):
self.context.addItem(description)
def render(self):
self.redirect(self.url(self.context.__parent__,'index'))
class TodoListDeleteItem(grok.View):
grok.context(TodoList)
grok.name('deleteitem')
def update(self,item):
self.context.deleteItem(item)
def render(self):
self.redirect(self.url(self.context.__parent__,'index'))
class TodoListUpdateItems(grok.View):
grok.context(TodoList)
grok.name('updateitems')
def update(self,items):
self.context.updateItems(items)
def render(self):
self.redirect(self.url(self.context.__parent__,'index'))
注意这三者如何使用grok.context注解将自己与TodoList模型关联。这三个的update方法与其他方法非常相似,只是调用context的正确update方法,在这种情况下我们知道是一个TodoList实例。
在render方法上的redirect调用略有不同。因为上下文是TodoList,而我们想显示Todo模型的索引视图,所以我们需要获取这个列表的容器,以便将其传递给视图的url函数,这样它就可以生成正确的 URL。为此,我们使用 Grok 模型对象的另一个特性,即__parent__属性,它指向其容器。正如我们所知,列表被Todo模型包含,我们可以这样访问它。模型对象还有一个__name__属性,它存储用于访问容器内对象的键。我们将在稍后看到索引模板如何改变以适应新的模型结构时使用它。
至于TodoItem模型,我们只需要之前版本中已经使用的check视图:
class TodoItemCheck(grok.View):
grok.context(TodoItem)
grok.name('check')
def update(self):
self.div_id = 'div_item_%s_%s' % (self.context.__parent__.__name__,self. context.__name__)
self.context.toggleCheck()
def render(self):
return self.div_id
有一些差异。现在视图明确地使用grok.context与TodoItem模型关联,这是显而易见的。现在我们调用TodoItem的新toggleCheck方法,而不是在视图中进行更改。最后,我们通过使用模型的__parent__和__name__属性来构造视图将返回的<div>标签的 ID。这样,我们就避免了需要将列表和项目索引作为视图参数传递的需要。
现在,在我们的待办事项列表管理应用程序中,应用程序和显示逻辑的分离要好得多。让我们看看这对index视图代码的影响,现在它必须显示来自三个模型而不是一个模型的数据。看看显示列表及其项目的循环代码。首先,注意我们现在使用context/values而不是旧模型中的列表元素来遍历列表:
<tal:block repeat="todolist context/values">
<div class="todolist">
之后,我们定义了todo列表表单。注意我们如何使用view.url()生成表单的正确 URL。记住,这个函数通过传递我们想要生成 URL 的对象以及可选的附加到它上面的视图名称来工作。在下面的代码中,第一个表单的动作是通过调用view url方法定义的,将当前的todolist对象和updateitems视图名称传递给它。
<form method="post" tal:attributes="action python:view.url(todolist,'updateitems')">
<fieldset>
接下来,我们有删除列表的代码。看看我们如何使用__name__属性来创建包含我们感兴趣的对象的名称的 URL。这样,期望接收键的方法将直接从这个视图中接收它。我们不需要进行任何列表和项目索引的计算,也不需要添加包含列表数据的隐藏字段:
<legend><span tal:content="todolist/title">title</span>
<a tal:define="url python:view.url('deletelist')" tal:attributes="href string:${url}?list=${todolist/__name__}">
<img border="0" tal:attributes="src static/bin_closed.png" />
</a>
</legend>
表单的其余部分有非常相似的变化,主要是使用__name__和view.url:。
<p tal:content="todolist/description">description</p>
<div tal:repeat="item todolist/values" tal:attributes="class python:item.checked and 'done' or 'pending'; id string:div_item_${todolist/__name__} _${item/__name__}">
<input type="checkbox" name="items:list" tal:define="check_url python:view.url(item,'check');" tal:attributes="checked item/checked; onclick string:getRequest('{check_url}', processReqChange)"/>
<span tal:content="item/description"></span>
<a tal:define="url python:view.url(todolist,'deleteitem')" tal:attributes="href string:${url}?item=${item/__name__}">
<img border="0" tal:attributes="src static/bin_closed.png" />
</a>
<br/>
</div>
<input type="submit" class="update_button" name="update_list" value="Update list"/>
</fieldset>
</form>
最后,添加列表项的表单现在也使用view.url来生成表单操作:
<form method="post" tal:attributes="action python:view.url(todolist,'add item')">
<label for="description"tal:attributes="onclick string:toggleAddControls('add_controls_${repeat/todolist/index}');; this.form.description.focus();">New item</label><br/>
<div class="controls" tal:attributes="id string:add_controls_${repeat/todolist/index}">
<input type="text" size="60" name="description">
<br/>
<input type="submit" class="new_button" name="new_item" value="Add to list"/>
</div>
</form>
</tal:block>
人们可能会认为需要使用三个模型可能会使视图中的事情变得复杂,但实际上这次我们使用的代码更少。它也更为简洁。我们不再使用一个包含多个提交按钮的大表单,这些按钮都指向同一视图中的相同update方法,我们现在使用几个表单,并通过使用view.url()生成每个情况下的正确 URL,将表单提交直接导向相应的视图。
除了这些更改之外,模板的代码基本上与我们已有的相同,因此不需要进行大的更改来使其工作。
Grok 内省器
因此,我们现在有一个完全工作的应用程序。玩一玩它,创建一些列表和任务。所有内容都将安全地存储在 ZODB 中。
现在我们快速浏览一下 Grok 提供的一个工具,这个工具在处理他人开发的应用程序(或回顾我们多年前编写的应用程序)时,可以帮助我们理解对象关系和职责。
Grok 内省器是一个可以从http://localhost:8080/applications的应用程序列表访问的工具。前往那里,在已安装的应用程序下,你会看到Todo应用程序的实例。应用程序名称是一个链接,可以运行它,但在名称的右侧有一个链接,上面写着对象浏览器。点击这个链接,你会看到类似于下一张截图的内容:

在这种情况下,我们可以看到Todo模型当前实例的数据。注意用于__parent__属性的父信息就在顶部。下面我们可以看到该对象的基本类,我们知道它们是grok.Application和grok.Container。在基类部分下面有一个提供的接口部分。我们稍后会更多地讨论接口,所以现在让我们跳过那个部分。
在下面你可以看到一个名为属性和属性的部分。在属性下,你可以找到Todo模型的title和next_id属性及其当前值。然后是映射部分,其中显示了存储在此容器中的所有对象的值。如果你点击这些值中的任何一个,你将得到一个页面,显示相应模型实例的数据,这样你可以轻松地导航应用程序产生的整个数据结构并查看其中的信息。
最后,显示了该对象所有方法的名称和文档字符串。尝试在我们的应用程序中添加一个方法的文档字符串,你将看到它在那个部分中反映出来(当然,在服务器重启后)。
摘要
在本章中,我们通过使用模型扩展了我们的演示应用。代码量与上一章相比并没有大幅增加,但我们得到了一个更加优雅且结构更优的应用。代码的维护性也更强,并且具有很好的扩展性,这一点我们将在处理 Grok 中的表单时学到。
第五章。表单
我们已经看到创建一个小型应用程序是多么容易,比如我们在过去几章中开发的待办事项管理器。现在,我们将探讨 Grok 如何帮助我们开发更复杂应用程序的多种方法之一。
到目前为止,我们一直在处理简单的一到两个字段的表单。当我们上一章更改我们的模型时,我们必须回去编辑表单的 HTML。对于几个字段来说,这需要很少的工作,但是当我们有可能有十几个字段或更多的复杂模型时,如果我们每次更改都不必修改两个文件,那就太好了。
幸运的是,Grok 有一个自动化创建和处理表单的机制。我们将在本章中了解它是如何工作的,以及一些其他与表单相关的主题:
-
什么是接口
-
什么是模式
-
如何使用 Grok 的表单组件自动生成表单,以及接口和模式是如何被使用的
-
如何创建、添加和编辑表单
-
如何过滤字段并防止它们出现在表单中
-
如何更改表单模板和展示
自动表单的快速演示
让我们先看看它是如何工作的,然后再深入了解细节。为了做到这一点,我们将向我们的应用程序添加一个项目模型。一个项目可以与任何数量的列表相关联,这样相关的待办事项列表就可以一起分组。现在,让我们单独考虑项目模型。将以下行添加到app.py文件中,在Todo应用程序类定义之后。我们稍后再考虑它是如何融入整个应用程序的。
class IProject(interface.Interface):
name = schema.TextLine(title=u'Name',required=True)
kind = schema.Choice(title=u'Kind of project',
values=['personal','business'])
description = schema.Text(title=u'Description')
class AddProject(grok.Form):
grok.context(Todo)
form_fields = grok.AutoFields(IProject)
我们还需要在文件顶部添加几个导入:
from zope import interface
from zope import schema
保存文件,重新启动服务器,然后访问 URL localhost:8080/todo/addproject。结果应该类似于以下截图:

好吧,表单的 HTML 是从哪里来的?我们知道AddProject是一种视图,因为我们使用了grok.context类注解来设置其上下文和名称。此外,类的名称(小写)被用于 URL 中,就像之前的视图示例一样。
重要的新事物是如何创建和使用表单字段。首先,定义了一个名为IProject的类。该接口定义了表单上的字段,而grok.AutoFields方法将它们分配给Form视图类。这就是视图知道在表单渲染时生成哪些 HTML 表单控件的原因。
我们有三个字段:name, description和kind。在代码的后面,grok.AutoFields行将这个IProject类转换成表单字段。
就这些。不需要模板或render方法。grok.Form视图负责生成显示表单所需的 HTML,从grok.AutoFields调用生成的form_fields属性值中获取信息。
接口
类名中的I代表接口。我们在文件顶部导入了zope.interface包,我们用作IProject基类的Interface类就来自这个包。
接口示例
接口是一个用于指定和描述对象外部行为的对象。从某种意义上说,接口就像一份合同。当一个类包含接口类中定义的所有方法和属性时,我们说这个类实现了接口。让我们看看一个简单的例子:
from zope import interface
class ICaveman(interface.Interface):
weapon = interface.Attribute('weapon')
def hunt(animal):
"""Hunt an animal to get food"""
def eat(animal):
"""Eat hunted animal"""
def sleep()
"""Rest before getting up to hunt again"""
在这里,我们正在描述穴居人的行为。穴居人将拥有武器,他可以狩猎、进食和睡眠。请注意,武器是一个属性,它是属于对象的某个东西,而狩猎、进食和睡眠是方法。
一旦定义了接口,我们就可以创建实现它的类。这些类承诺包括它们接口类中的所有属性和方法。因此,如果我们说:
class Caveman(object):
interface.implements(ICaveman)
然后,我们承诺Caveman类将实现ICaveman接口中描述的方法和属性:
weapon = 'ax'
def hunt(animal):
find(animal)
hit(animal,self.weapon)
def eat(animal):
cut(animal)
bite()
def sleep():
snore()
rest()
注意,尽管我们的示例类实现了所有接口方法,但 Python 解释器并没有做出任何类型的强制执行。我们可以定义一个不包含任何定义的方法或属性的类,它仍然可以工作。
Grok 中的接口
在 Grok 中,一个模型可以通过使用grok.implements方法来实现接口。例如,如果我们决定添加一个项目模型,它可以如下实现IProject接口:
class Project(grok.Container):
grok.implements(IProject)
由于它们的描述性,接口可以用作文档。它们还可以用于启用组件架构,但我们会稍后讨论这一点。对我们来说,现在更有兴趣的是,它们可以用于自动生成表单。
模式
定义表单字段的方法是使用zope.schema包。这个包包括许多种字段定义,可以用来填充表单。
基本上,模式允许详细描述使用字段的类属性。在形式方面,这是我们在这里感兴趣的内容,模式表示当用户提交表单时将传递给服务器的数据。表单中的每个字段对应于模式中的一个字段。
让我们更仔细地看看我们在上一节中定义的模式:
class IProject(interface.Interface):
name = schema.TextLine(title=u'Name',required=True)
kind = schema.Choice(title=u'Kind of project',
required=False,
values=['personal','business'])
description = schema.Text(title=u'Description',
required=False)
我们为 IProject 定义的方案有三个字段。有多种类型的字段,如下表所示。在我们的示例中,我们定义了一个 name 字段,它将是一个必填字段,并且旁边将有一个标签 Name。我们还定义了一个 kind 字段,它是一个用户必须从中选择一个选项的选项列表。请注意,required 的默认值是 True,但通常最好明确指定它,以避免混淆。您可以通过使用 values 参数来静态地传递可能的值列表。最后,description 是一个文本字段,这意味着它将有多行文本。
可用的模式属性和字段类型
除了 title、values 和 required 之外,每个模式字段还可以有多个属性,如下表详细说明:
| 属性 | 描述 |
|---|---|
title |
简短的摘要或标签。 |
description |
字段的描述。 |
required |
表示字段是否需要存在值。 |
readonly |
如果为 True,则字段的值不能被更改。 |
default |
字段的默认值可以是 None,或者一个有效的字段值。 |
missing_value |
如果此字段的输入缺失,并且这是可以接受的,那么这就是要使用的值。 |
order |
order 属性可以用来确定模式中字段的定义顺序。如果一个字段在另一个字段之后(在同一线程中)创建,则其顺序将更大。 |
除了前面表格中描述的字段属性之外,一些字段类型还提供了额外的属性。在前面的示例中,我们看到了各种字段类型,如 Text、TextLine 和 Choice。还有其他几种字段类型可用,如下表所示。我们可以通过这种方式定义一个方案,并让 Grok 生成它们,从而创建非常复杂的表单。
| 字段类型 | 描述 | 参数 |
|---|---|---|
Bool |
布尔字段。 | |
Bytes |
包含字节字符串的字段(如 python 的 str)。值可能被限制在长度范围内。 |
|
ASCII |
包含 7 位 ASCII 字符串的字段。不允许任何大于 DEL(chr(127))的字符。值可能被限制在长度范围内。 | |
BytesLine |
包含没有换行符的字节字符串的字段。 | |
ASCIILine |
包含没有换行符的 7 位 ASCII 字符串的字段。 | |
Text |
包含 Unicode 字符串的字段。 | |
SourceText |
包含对象源文本的字段。 | |
TextLine |
包含没有换行符的 Unicode 字段的字段。 | |
Password |
包含没有换行符的 Unicode 字符串的字段,该字符串被设置为密码。 | |
Int |
包含整数值的字段。 | |
Float |
包含浮点数的字段。 | |
Decimal |
包含十进制数的字段。 | |
DateTime |
包含日期时间的字段。 | |
Date |
包含日期的字段。 | |
Timedelta |
包含 timedelta 的字段。 | |
时间 |
包含时间的字段。 | |
URI |
包含绝对 URI 的字段。 | |
Id |
包含唯一标识符的字段。唯一标识符可以是绝对 URI 或点分名称。如果是点分名称,它应该有一个模块或包名称作为前缀。 | |
| 字段类型 | 描述 | 参数 |
选择 |
值包含在预定义集合中的字段。 | values: 字段文本选择的列表。vocabulary: 将动态生成选择的词汇对象。source: 生成动态选择的不同、较新的方式。注意:三者中只能提供其一。关于来源和词汇的更多信息将在本书后面提供。 |
元组 |
包含实现传统 Python 元组 API 的值的字段。 | value_type: 字段值项必须符合通过字段给出的类型,通过字段表达。Unique。指定集合的成员是否必须是唯一的。 |
列表 |
包含实现传统 Python 列表 API 的值的字段。 | value_type: 字段值项必须符合通过字段给出的类型,通过字段表达。Unique。指定集合的成员是否必须是唯一的。 |
集合 |
包含实现传统 Python 标准库 sets.Set 或 Python 2.4+ 集合 API 的值的字段。 |
value_type: 字段值项必须符合通过字段给出的类型,通过字段表达。 |
冻结集合 |
包含实现传统 Python 2.4+ frozenset API 的值的字段。 | value_type: 字段值项必须符合通过字段给出的类型,通过字段表达。 |
对象 |
包含对象值的字段。 | Schema: 定义对象所包含字段的接口。 |
字典 |
包含传统字典的字段。key_type 和 value_type 字段允许指定字典中键和值的限制。 |
key_type: 字段键必须符合通过字段给出的类型,通过字段表达。value_type: 字段值项必须符合通过字段给出的类型,通过字段表达。 |
表单字段和部件
模式字段非常适合定义数据结构,但在处理表单时有时它们并不足够。实际上,一旦使用模式作为基础生成表单,Grok 将模式字段转换为表单字段。表单字段类似于模式字段,但具有扩展的方法和属性集。它还有一个默认关联的部件,负责在表单内字段的外观。
渲染表单需要不仅仅是字段及其类型。表单字段需要一个用户界面,这正是小部件提供的。例如,Choice 字段可以渲染为表单上的 <select> 框,但它也可以使用一组复选框,或者可能是单选按钮。有时,字段可能不需要在表单上显示,或者可写字段可能需要以文本形式显示,而不是允许用户设置字段的值。
表单组件
Grok 提供了四个不同的组件,可以自动生成表单。我们已经与这些组件中的第一个 grok.Form 一起工作过。其他三个是这个组件的专门化:
-
grok.AddForm用于添加新的模型实例。 -
grok.EditForm用于编辑已存在的实例。 -
grok.DisplayForm简单地显示字段的值。
Grok 表单本身是 grok.View 的专门化,这意味着它获得与视图相同的方法。这也意味着如果模型已经有了表单,实际上不需要为模型分配视图。实际上,简单的应用程序可以通过将表单用作对象的视图来避免这种情况。当然,有时需要更复杂的视图模板,或者甚至需要在同一视图中显示来自多个表单的字段。Grok 也可以处理这些情况,我们将在稍后看到。
在站点的根目录添加一个项目容器
要了解 Grok 的表单组件,让我们将我们的项目模型正确集成到待办事项列表应用程序中。我们需要对代码进行一点重构,因为目前待办事项列表容器是应用程序的根对象。我们需要有一个项目容器作为根对象,然后向其中添加一个待办事项列表容器。
幸运的是,我们在上一章中已经正确地结构化了代码,所以我们现在不需要做很多修改。首先,让我们修改 app.py 的顶部,在 TodoList 类定义之前,使其看起来像这样:
import grok
from zope import interface, schema
class Todo(grok.Application, grok.Container):
def __init__(self):
super(Todo, self).__init__()
self.title = 'To-Do list manager'
self.next_id = 0
def deleteProject(self,project):
del self[project]
首先,我们导入 zope.interface 和 zope.schema。注意我们如何保持 Todo 类作为根应用程序类,但现在它可以包含项目而不是列表。我们还省略了 addProject 方法,因为 grok.AddForm 实例将负责这一点。除此之外,Todo 类几乎相同。
class IProject(interface.Interface):
title = schema.TextLine(title=u'Title',required=True)
kind = schema.Choice(title=u'Kind of project',values=['personal', 'business'])
description = schema.Text(title=u'Description',required=False)
next_id = schema.Int(title=u'Next id',default=0)
然后我们有 IProject 的接口定义,其中我们添加了 title, kind, description 和 next_id 字段。这些是我们之前在产品初始化时调用 __init__ 方法时添加的字段。
class Project(grok.Container):
grok.implements(IProject)
def addList(self,title,description):
id = str(self.next_id)
self.next_id = self.next_id+1
self[id] = TodoList(title,description)
def deleteList(self,list):
del self[list]
在 Project 类定义中需要注意的关键点是,我们使用 grok.implements 类声明来查看这个类将实现我们刚刚定义的模式。
class AddProjectForm(grok.AddForm):
grok.context(Todo)
grok.name('index')
form_fields = grok.AutoFields(Project)
label = "To begin, add a new project"
@grok.action('Add project')
def add(self,**data):
project = Project()
self.applyData(project,**data)
id = str(self.context.next_id)
self.context.next_id = self.context.next_id+1
self.context[id] = project
return self.redirect(self.url(self.context[id]))
实际的表单视图是在之后定义的,通过使用grok.AddForm作为基类。我们使用grok.context注解将此视图分配给主Todo容器。目前使用index这个名字,这样应用程序的默认页面就是“添加表单”本身。
接下来,我们通过调用grok.AutoFields方法创建表单字段。请注意,这次这个方法调用的参数是Project类本身,而不是接口。这是因为我们在之前使用grok.implements时将Project类与正确的接口关联起来了。
在我们分配字段之后,我们将表单的label属性设置为文本:“开始,添加一个新项目”。这是将在表单上显示的标题。
除了这段新代码之外,文件中所有grok.context(Todo)的实例都需要更改为grok.context(Project),因为待办事项列表及其视图现在属于一个项目,而不是主Todo应用程序。有关详细信息,请参阅本书第五章的源代码。
表单操作
如果你仔细查看“自动表单快速演示”部分中显示的截图,你会看到表单没有提交按钮。在 Grok 中,每个表单都可以有一个或多个操作,并且对于每个操作,表单都会有一个提交按钮。Grok 的action装饰器用于标记表单类中将用作操作的方法。在这种情况下,add方法被装饰了,并且文本参数的值,在这种情况下是Add project,将用作按钮上的文本。要更改按钮上的文本,只需修改传递给装饰器的字符串即可:
@grok.action('Add project')
def add(self,**data):
project = Project()
self.applyData(project,**data)
id = str(self.context.next_id)
self.context.next_id = self.context.next_id+1
self.context[id] = project
return self.redirect(self.url(self.context[id]))
add方法接收data参数中所有填写好的表单字段,然后创建一个Project实例。接下来,它通过调用表单的applyData方法将project的属性设置为表单中的值。最后,它将新项目添加到Todo实例中,并将用户重定向到项目页面。
尝试应用程序
当你尝试应用程序时,有两点需要注意。首先,当添加项目表单显示时,用于命名项目的next_id字段会显示出来。如果我们愿意,甚至可以编辑它。显然,我们不希望这种行为。
第二,一旦项目创建完成并且我们被重定向到项目页面,一切都会像以前一样工作,即使我们没有触摸模板。如果我们使用第三章中尝试的方法,即使用隐藏值和列表索引,我们就需要在index.pt模板的许多地方进行修改。现在我们有一个基于模型的架构,为它们注册的视图和方法不需要改变,即使包含层次结构不同。
另一个需要注意的重要事情是,添加项目表单没有任何设计或样式。这是因为表单构建机制使用了一个通用的模板,我们还没有对其进行样式设计。

过滤字段
记住,目前我们在表单上显示了项目的next_id字段。我们不想让它在那里,所以我们如何移除它?幸运的是,grok.AutoFields方法生成的字段列表可以很容易地进行过滤。
我们可以使用select方法精确选择我们需要的字段:
form_fields = grok.AutoFields(Project).select('title','kind', 'description')
或者,我们可以通过使用omit方法省略特定的字段:
form_fields = grok.AutoFields(Project).omit('next_id')
在这两种情况下,我们将字段的 ID 作为字符串传递给选择的方法。现在,next_id字段已经不再存在了,正如您可以在下一张屏幕截图中所看到的。
过滤字段不仅对于移除不想要的字段(如next_id)有用。我们还可以为编辑模式下的部分模式创建专门的表单,或者根据用户信息或输入显示特定字段。
使用 grok.EditForm
我们刚才制作的表单是为了向应用程序添加一个新项目,但如果我们需要编辑现有的项目怎么办?在这种情况下,我们需要一个知道如何获取表单上所有字段的现有值并在编辑时显示它们的表单。另一个区别是,添加项目表单被分配给了主 Todo 应用程序,但编辑表单将使用它将要修改的实际项目作为上下文。
这就是为什么 Grok 有另一种形式的编辑组件。使用grok.EditForm比grok.AddForm更容易。以下是我们需要添加到我们的应用程序中的所有代码,以便能够编辑项目:
class EditProjectForm(grok.EditForm):
grok.context(Project)
grok.name('edit')
form_fields = grok.AutoFields(Project).omit('next_id')
label = "Edit the project"
如前所述,此表单的上下文是Project类,我们通过使用grok.context类注解来设置它。我们给这个表单命名为edit,这样就可以直接将这个词添加到项目 URL 后面,以获取其编辑视图。正如我们在上一节中讨论的,从表单中删除next_id字段的显示是个好主意,所以我们使用omit方法来实现这一点。最后,我们为表单设置了一个标签,然后就可以准备测试它了。
启动应用程序。如果您还没有创建项目,请先创建一个。然后,转到 URL:localhost:8080/todo/0/edit。编辑项目字段,然后点击应用按钮。您应该会看到一个类似于以下屏幕截图的屏幕:

注意我们在渲染表单后没有包含重定向,所以当我们点击应用按钮时,我们会回到同一个表单,但会显示一条消息告诉我们对象已更新,以及修改的日期。如果我们想的话,我们可以通过使用action装饰器和重定向来添加一个edit操作,就像我们对添加表单所做的那样。
修改单个表单字段
由 Grok 自动创建添加和编辑表单很方便,但在某些情况下,我们可能需要以某种方式对表单的渲染进行小修改。Grok 允许我们轻松修改特定字段属性,以便实现这一点。
记住,每个表单字段都将由一个控件渲染,这可以被视为特定字段的视图。这些视图通常接受多个参数,以便用户以某种方式自定义表单的外观。
在渲染之前,Grok 的表单组件总是调用一个名为setUpWidgets的方法,我们可以覆盖它来对字段及其属性进行修改。
在添加和编辑项目表单中,项目标题,其类型为TextLine,有一个显示用于捕获其值的<input>标签的控件,长度为 20 个字符。许多项目名称可能比这更长,因此我们希望将其长度扩展到 50 个字符。此外,描述文本区域对于项目摘要来说太长了,所以我们将它截断为五行。让我们使用setUpWidgets方法来完成这个任务。将以下行添加到AddProjectForm和EditProjectForm类中:
def setUpWidgets(self, ignore_request=False):
super(EditProjectForm,self).setUpWidgets(ignore_request)
self.widgets['title'].displayWidth = 50
self.widgets['description'].height = 5
在将方法添加到适当类时,请注意在超类调用中用EditProjectForm替换AddProjectForm。setUpWidgets方法相当简单。我们首先调用超类,以确保在尝试修改之前我们得到表单中的正确属性。接下来,我们修改我们想要的任何属性。在这种情况下,我们访问widgets属性来获取我们定义的字段的控件,并更改我们想要的值。
另一个需要解释的是传递给setUpWidgets方法的ignore_request参数。如果将其设置为False,正如我们定义的那样,这意味着任何存在于HTTP请求中的字段值都将应用于相应的字段。True的值表示在此调用期间不应更改任何值。
重新启动应用程序,您将看到编辑和添加表单现在使用我们修改过的属性显示控件。
表单验证
现在我们已经有了用于添加和编辑项目的有效表单。实际上,这些表单可以做得比我们迄今为止展示的更多。例如,如果我们转到添加表单并尝试提交它而不填写必需的title字段,我们将得到表单,而不是被重定向。不会创建任何项目,屏幕上将显示一个错误消息,警告我们该字段不能为空。
这种验证是自动发生的,但我们可以通过在定义模式中的字段时使用constraint参数来添加我们自己的约束。例如,假设我们绝对需要在标题中有多于两个单词。我们可以非常容易地做到这一点。只需在接口定义之前添加以下行:
def check_title(value):
return len(value.split())>2
接下来,修改标题字段定义,使其看起来像这样:
title = schema.TextLine(title=u'Title', required=True, constraint=check_title)
我们首先定义了一个函数,该函数将接收字段值作为参数,如果值有效则返回True,否则返回False。然后,在定义接口中的字段时,我们将此函数分配给constraint参数。这一切都非常简单,但它允许我们为表单数据中需要满足的任何条件添加验证。
有时候,简单的约束不足以验证字段。例如,想象一下,我们需要的是每当项目的类型是“business”时,描述不能为空。在这种情况下,约束是不够的,因为描述字段是否有效取决于另一个字段的值。
在 Grok 中,涉及多个字段的约束被称为不变量。要定义一个,使用@interface.invariant装饰器。对于之前描述的假设情况,我们可以使用以下定义,并将其添加到Interface定义中:
@interface.invariant
def businessNeedsDescription(project):
if project.kind=='business' and not project.description:
raise interface.Invalid( "Business projects require a description")
现在,当我们尝试添加一个类型为“business”的项目时,如果描述为空,Grok 将会抱怨。请参考下一张截图以获取参考信息:

自定义表单模板
之前,我们评论了我们的新项目表单没有应用样式或设计,因此它们与我们的应用程序的其他部分明显不同。现在是时候改变这一点了。
使用自动表单生成的缺点是默认模板必须相当通用,才能在多个应用程序中发挥作用。然而,Grok 允许我们为编辑表单设置自定义模板。我们所需做的只是设置表单中的template属性:
template = grok.PageTemplateFile('custom_edit_form.pt')
当然,为了让这起作用,我们还需要在我们的应用程序目录内(而不是在app_templates内)提供命名模板。现在,让我们先为 Grok 附带的一般编辑模板添加一个样式表和类。这里没有什么特别之处,所以我们只需采用默认的编辑表单模板,并添加之前定义的相同样式表。如果您想查看模板的其余部分,请查看本书的源代码。
<html>
<head>
<title tal:content="context/title">To-Do list manager</title>
<link rel="stylesheet" type="text/css" tal:attributes="href static/styles.css" />
</head>
为了拥有我们自定义的表单编辑模板,所需的就是这些。请查看下一张截图以了解其外观。当然,我们可能需要进一步修改它,以使其看起来正好符合我们的要求。我们甚至可以只留下我们想要的字段,以不规则的方式排列,但使用原始模板并稍作修改的原因是,您可以查看它,并小心处理显示验证消息和生成操作的章节。我们将在未来的章节中对此有更多讨论。

摘要
我们已经了解了如何使用模式自动生成表单,以及如何自定义它们的渲染方式。此外,我们还了解了一些 Zope 框架库,例如 zope.schema 和 zope.interface。
第六章:目录:面向对象的搜索引擎
现在我们有一个包含多个项目和列表的应用程序。随着我们开始添加更多的列表,将会有一个时刻我们需要找到特定的列表项。我们可能想要找到所有包含特定单词的项目,或者可能想要找到所有在特定日期完成的项目。由于所有应用程序数据都存储在 ZODB 中,我们需要一个工具来帮助我们查找其中包含的特定对象。这个工具是 Grok 默认提供的,被称为目录。
在本章中,我们将学习如何使用目录。特别是,我们将探讨以下概念:
-
目录是什么以及它是如何工作的
-
索引是什么以及它们是如何工作的
-
在目录中存储数据
-
在目录上执行简单查询
-
为我们的应用程序创建一个搜索界面
目录和索引
当我们处理少量数据时,我们可以总是查看列表中的所有元素,例如,以找到我们想要的元素。然而,当我们处理成千上万的对象时,这种方法显然无法扩展。解决这个问题的常见方法之一是使用某种类型的查找表,它将允许我们通过使用对象的一个属性快速轻松地找到特定的对象。这被称为索引。
目录是一个工具,它允许我们管理一组相关的索引,并通过使用一个或多个索引来对目录进行查询。我们可以向目录添加索引,以跟踪对象的特定属性。从那时起,每次我们创建一个新的对象时,我们都可以调用目录来索引它,它将包括所有具有索引设置的属性,并将它们包含在相应的索引中。一旦它们被包含在内,我们就可以通过使用特定的属性值来查询目录,并获取与查询匹配的对象。
向应用程序添加一个简单的搜索功能
目录包含对存储在 ZODB 中的实际对象的引用,每个索引都与这些对象的一个属性相关。
要在目录中搜索对象,对象需要被目录索引。如果它在对象的生命周期中的特定事件发生,这将更有效,这样它就可以在创建时和修改时被索引。
由于在使用 ZODB 时,通过目录处理搜索是最佳方式,因此 Grok 附带了一个类,允许我们轻松地连接到目录、创建索引和执行搜索。这个类被称为grok.Indexes,允许我们定义索引并将我们的应用程序对象与适当的生命周期事件挂钩,以实现自动索引。
定义一个简单的索引
让我们定义一个简单的索引,用于项目的title属性,并展示如何通过使用它来在目录上执行搜索:
class ProjectIndexes(grok.Indexes):
grok.site(ITodo)
grok.context(IProject)
title = grok.index.Text()
我们将创建一个针对Project类的索引,因此我们将我们的类命名为ProjectIndexes。名称并不重要,因为关键是使用grok.Indexes作为类的基类。grok.site类注解用于通知 Grok 在我们的应用程序中哪种类型的对象将使用这里定义的索引。
接下来,我们需要告诉 Grok 哪些对象在修改时将被自动索引。这是通过使用grok.context并使用类或接口作为参数来完成的。在这种情况下,我们选择IProject作为将标记要索引的对象的接口。
最后,我们定义索引本身。在这个例子中,我们希望title属性的全部文本都是可搜索的,因此我们将使用Text索引。我们很快就会详细介绍索引的类型。现在,只需注意将要被索引的属性与索引的名称相同,在这种情况下意味着项目模型中的title属性将在这个目录中被索引。也有可能将索引命名为与要索引的属性不同的名称,但那时我们需要使用关键字参数attribute来指定实际的属性名称,如下所示:
project_title = grok.index.Text(attribute='title')
就这样。仅仅通过在这个类中声明索引,Grok 就会自己创建目录并将索引附加到它上面,同时还会跟踪何时需要重新索引对象。
创建搜索视图
现在,我们将创建一个搜索视图,以便我们可以看到目录的实际应用。首先,让我们看一下视图代码:
class TodoSearch(grok.View):
grok.context(Todo)
grok.name('search')
def update(self,query):
if query:
catalog = getUtility(ICatalog)
self.results = catalog.searchResults(title=query)
这是一个主应用程序的视图,命名为 search。重要的是update方法。我们接收一个作为参数的查询,它代表了用户在项目标题中寻找的文本。在我们能够执行搜索之前,我们必须先获取实际的目录。请注意,此时的目录已经包含了我们之前为Project定义的索引。我们不需要做任何其他事情来将它们与目录连接起来;Grok 会处理所有管道。
在我们获取到catalog对象之后,我们可以通过使用searchResults方法来搜索它,该方法接受键/值对,其中包含索引名称和查询值。在这种情况下,我们将请求中传入的查询传递给title索引,这样我们就能得到所有标题中包含此查询文本的项目,作为结果。
你可能还记得,我们之前提到过,接口除了对文档和属性自省有用之外,对于处理组件架构也非常有帮助。在底层,Grok 包含一个注册表,它跟踪所有对象的接口声明,这样就可以通过查询接口来找到对象。目录总是实现ICatalog接口,该接口位于 Grok 附带zope.app.catalog.interfaces包中。
由于 Grok 通过不强制我们实例化目录并手动添加和填充索引来简化我们的生活,我们无法控制代码执行此操作的部分。那么我们如何找到它呢?通过使用注册表,我们可以查询实现ICatalog接口的对象,这将是我们正在寻找的目录。
这正是zope.component包中的getUtility方法所做的事情。因此,在调用此方法之后,我们将拥有由我们的catalog变量引用的目录。这看起来可能是一种绕弯子的机制来获取目录。为什么不直接让 Grok 定义一个全局目录并直接使用它呢?好吧,我们可以用另一个或两个问题来回答:如果我们需要多个目录怎么办?或者如果我们决定用我们自己的目录替换 Grok 创建的目录怎么办?当使用接口和组件注册时,我们可以处理这些情况,并且代码几乎不需要更改。
创建一个模板来显示搜索结果
一旦我们在update方法的最后一行将结果放置在视图中,我们需要一个模板来显示这个结果。在app_templates目录内创建一个名为todosearch.pt的模板。首先是标题,包括我们的样式表:
<html>
<head>
<title tal:content="context/title">To-Do list manager</title>
<link rel="stylesheet" type="text/css" tal:attributes="href static/styles.css" />
</head>
接下来,我们将在页面的顶部标题内添加一个搜索框。我们使用tal:attributes将文本输入值设置为之前搜索的内容,或者如果之前没有搜索,则留空。
表单操作设置为调用我们之前定义的搜索视图:
<body>
<div id="appheader">
<form id="search" tal:attributes="action python:view.url('search')">
<input type="text" name="query" tal:attributes="value request/query|nothing" />
<input class="new_button" type="submit" value="search" />
</form>
<h1 id="apptitle" tal:content="string:${context/title}: search"> To-Do list manager </h1>
</div>
<p class="create">
<a tal:attributes="href python:view.url(context)"> Go back to main page </a>
</p>
现在我们来到了问题的关键。回想一下,在视图中我们通过标题进行了搜索,并定义了一个名为results的属性。现在我们可以在模板中使用这些结果。首先,我们需要确保我们有东西可以展示,如果没有,我们将显示一条消息说明这一点:
<h2 tal:condition="not:view/results|nothing">There were no results.</h2>
接下来,如果有结果,我们将准备一个带有正确标题的表格,并使用tal:repeat来遍历结果。视图变量results包含所有标题与查询匹配的项目,因此我们遍历这些结果,并在与仪表板相似的表格中简单地显示它们的所有属性。
<div class="projects" tal:condition="view/results|nothing">
<h2>These are the results for "<i tal:content="request/query"></i>":</h2 >
<table>
<tr>
<th>Project name</th>
<th>Kind</th>
<th>Description</th>
<th>Created on</th>
<th>Last modified</th>
<th>Owner</th>
</tr>
<tr tal:repeat="project view/results">
<td>
<a href="" tal:attributes="href python:view.url(project)" tal:content="project/title">title</a>
</td>
<td tal:content="project/kind">type</td>
<td tal:content="project/description">type</td>
<td tal:content="project/creation_date">type</td>
<td tal:content="project/modification_date">type</td>
<td tal:content="project/creator">type</td>
</tr>
</table>
</div>
</body>
</html>
现在我们可以创建一个新的应用程序实例在 Grok 管理 UI 中,并定义一些项目,以便我们可以看到搜索是如何工作的。我们需要定义一个新的应用程序的原因是,索引只有在安装应用程序时才会创建。如果我们的ProjectIndexes类在应用程序创建后被添加,实际上将不会做任何事情。请看下面的截图,以了解搜索结果是如何显示的:

简短的偏离:为搜索结构化我们的应用程序
现在我们已经尝试了目录搜索的基本操作,我们需要对我们的应用程序进行一些重构,以便与目录良好地协同工作,因为我们的待办事项没有所有我们可能需要的属性来使搜索足够强大。例如,我们肯定会通过日期来搜索它们,最终通过用户来搜索。
让我们稍作停顿,思考一下我们的应用程序想要走向何方。如果我们将要管理“项目”,我们需要在我们的模型中添加一些更多的属性。我们之前没有关心这个问题,但随着我们应用程序的复杂性增加,有一个清晰的计划变得更加重要。
我们应用程序的最高级单元将是一个项目。就我们的目的而言,一个项目是一系列相关待办事项的集合。我们需要存储项目的创建日期以及最后一次修改日期。当一个项目的所有列表中的所有事项都被勾选时,该项目即为“完成”。
项目可以有所有者和成员。成员是分配了一个或多个事项的用户;所有者是成员,他们还可以添加、编辑或删除列表和事项。我们还没有看到 Grok 中用户管理的工作方式,但我们将看到它将在下一章中,所以现在我们只需存储项目的创建者。
一个待办事项列表可以有任意数量的事项。我们还将存储列表的创建日期。一个事项将有一个完成日期以及一个创建日期。我们还将跟踪谁执行了这些操作。
在上一章中,我们使用接口模式作为基础自动构建了表单。当时,我们只为Project类添加了一个接口。让我们完成这项工作,并为其他类做同样的事情。
我们正在处理不同类型的对象,但肯定有一些属性在它们中的大多数都会用到。例如,每个项目、列表和事项都将有一个创建者和创建日期。我们希望避免在每个接口定义中重复这些属性,因此我们将为它们创建一个通用接口,并让所有其他类实现这个接口。
我们之前在处理表单时简要讨论了接口,并看到一个类可以通过使用grok.implements类注解来承诺实现一个接口。然而,一个类并不局限于实现单个接口。实际上,一个类可以实现的接口数量是没有限制的。这对我们来说将是有用的,因为我们的所有类都可以实现通用的元数据接口以及它们自己的特定接口。
我们重构后的模型将类似于下面的代码。首先,我们的共享属性接口:
class IMetadata(interface.Interface):
creator = schema.TextLine(title=u'Creator')
creation_date = schema.Datetime(title=u'Creation date')
modification_date = schema.Datetime(title=u'Modification date')
主要应用程序类不需要使用元数据模式;只使用它自己的模式:
class ITodo(interface.Interface):
title = schema.TextLine(title=u'Title',required=True)
next_id = schema.Int(title=u'Next id',default=0)
class Todo(grok.Application, grok.Container):
grok.implements(ITodo)
title = u'To-do list manager'
next_id = 0
def deleteProject(self,project):
del self[project]
我们在grok.implements调用之后向模式中添加了title和next_id,并为类设置了默认值。
现在看看Project类:
class IProject(interface.Interface):
title = schema.TextLine(title=u'Title', required=True, constraint=check_title)
kind = schema.Choice(title=u'Kind of project', values=['personal', 'business'])
description = schema.Text(title=u'Description', required=False)
next_id = schema.Int(title=u'Next id', default=0)
class Project(grok.Container):
grok.implements(IProject, IMetadata)
next_id = 0
description = u''
def addList(self, title, description):
id = str(self.next_id)
self.next_id = self.next_id+1
self[id] = TodoList(title, description)
def deleteList(self, list):
del self[list]
在这个情况下,我们定义了Project模式,然后告诉 Grok 这个类将使用这个模式和之前定义的元数据模式。这相当简单:我们只需将两个接口定义作为参数传递给grok.implements。
当向应用程序添加新项目时,我们将使用新属性,如下所示:
@grok.action('Add project')
def add(self,**data):
project = Project()
self.applyData(project,**data)
id = str(self.context.next_id)
self.context.next_id = self.context.next_id+1
self.context[id] = project
project.creator = self.request.principal.title
project.creation_date = datetime.datetime.now()
project.modification_date = datetime.datetime.now()
return self.redirect(self.url(self.context[id]))
在创建新项目并将表单数据应用于它之后,我们为日期和创建者设置值。请记住,接口是信息性的。我们从不要求使用接口模式中的所有字段,但能够为了文档目的而引用模式是非常方便的。在某些情况下,它被用来使用一个或多个字段生成表单。顺便提一下,日期分配发生的事情可能非常清楚,但self.request.principal.title这一行可能看起来有点奇怪。principal是 Grok 中的一个用户,其标题是这个用户的字符串描述。
关于我们模型重构的介绍到此为止。以下是最后的两个模型:
class ITodoList(interface.Interface):
title = schema.TextLine(title=u'Title',required=True, constraint=check_title)
description = schema.Text(title=u'Description',required=False)
next_id = schema.Int(title=u'Next id',default=0)
class TodoList(grok.Container):
grok.implements(ITodoList, IMetadata)
next_id = 0
description = u''
def __init__(self,title,description):
super(TodoList, self).__init__()
self.title = title
self.description = description
self.next_id = 0
def addItem(self,description):
id = str(self.next_id)
self.next_id = self.next_id+1
self[id] = TodoItem(description)
def deleteItem(self,item):
del self[item]
def updateItems(self, items):
for name,item in self.items():
if name in items:
self[item].checked = True
else:
self[item].checked = False
class ITodoItem(interface.Interface):
description = schema.Text(title=u'Description',required=True)
checked = schema.Bool(title=u'Checked',default=False)
class TodoItem(grok.Model):
grok.implements(ITodoItem, IMetadata)
checked = False
def __init__(self,item_description):
super(TodoItem, self).__init__()
self.description = item_description
self.checked = False
def toggleCheck(self):
self.checked = not self.checked
现在我们已经添加了所需的属性和接口,让我们创建一个主页模板,我们可以一眼看到所有项目及其属性,以及每个项目的链接。这将是我们应用程序的仪表板。在此过程中,我们还在顶部添加了一个搜索框。我们将首先添加仪表板的视图:
class DashBoard(grok.View):
grok.context(Todo)
grok.name('index')
现在,对于模板,调用dashboard.pt文件并将其放置在app_templates中。注意我们使用名称index,这样它将成为应用程序的默认视图:
<html>
<head>
<title tal:content="context/title">To-Do list manager</title>
<link rel="stylesheet" type="text/css" tal:attributes="href static/styles.css" />
</head>
<body>
<div id="appheader">
<form id="search" tal:attributes="action python:view.url('search')">
<input type="text" name="query" />
<input class="new_button" type="submit" value="search" />
</form>
<h1 id="apptitle" tal:content="context/title"> To-Do list manager</h1>
</div>
<p class="create"><a href="add">Create a new project</a></p>
<h2 tal:define="projects context/values" tal:condition="projects"> These are your available projects</h2>
<div class="projects">
<table>
<tr>
<th>Project name</th>
<th>Kind</th>
<th>Description</th>
<th>Created on</th>
<th>Last modified</th>
<th>Owner</th>
<th>Delete</th>
</tr>
<tr tal:repeat="project context/values">
<td>
<a href="" tal:attributes="href python:view.url(project)" tal:content="project/title">title</a>
</td>
<td tal:content="project/kind">type</td>
<td tal:content="project/description">type</td>
<td tal:content="project/creation_date">type</td>
<td tal:content="project/modification_date">type</td>
<td tal:content="project/creator">type</td>
<td>
<a tal:define="url python:view.url('deleteproject')" tal:attributes="href string:${url}?project=${project/__name__}">
<img border="0" tal:attributes="src static/bin_closed.png" />
</a>
</td>
</tr>
</table>
</div>
</body>
</html>
模板非常直接。我们只需使用context/values调用获取所有项目,然后遍历结果,在表格中显示所有属性。模板可以在以下屏幕截图中看到:

回到搜索:使用多个索引
让我们简要回顾一下在开始重新组织代码之前我们在搜索方面的位置。在使用 Grok 中的目录时,要理解的关键点是索引定义了我们能执行哪些类型的搜索。在我们的例子中,我们使用了Text索引来搜索title属性,而这个索引的 Grok 上下文是Project模型。这意味着即使一个项目有多个属性,我们目前也只能在标题中进行搜索。
当然,我们不仅限于使用一个索引。我们可以添加任意数量的索引,甚至可以为对象的每个属性添加一个。让我们在ProjectIndexes类中立即在标题之后添加一个用于description的索引:
description = grok.index.Text()
注意,唯一改变的是索引名称,它指向要索引的实际属性。为了保持简单,我们可以使用以下查询:
self.results = catalog.searchResults(title=query, description=query)
我们将 query 参数传递给两个索引,因为我们只有一个可以容纳一个参数的文本框。结果将包括所有标题和描述与查询中指定的值匹配的项目。如果我们有几个参数,我们可以为每个索引传递不同的值,并且我们会得到所有索引都匹配的所有条目。
让我们考虑在这个应用程序中我们希望搜索框如何工作。理想的情况是尽可能全面,这样我们就可以输入一个或两个词,让目录查看不同的索引以找到结果。例如,如果可以对 description 或 title 索引执行文本搜索,并且目录返回所有在查询中匹配这些索引的条目,那就很好了。由于这是一个常见的情况,Grok 提供了一个通常很有用且易于实现的解决方案。基本上,我们可以定义一个方法,它可以收集所有属性的信息,并将收集到的信息作为单个索引返回。
首先,我们在接口中添加一个空的方法定义。这样做既是为了记录方法,也是为了让 grok.Indexes 类在我们声明其名称为索引时找到该属性。记住,项目索引与 IProject 接口相关联,因此这里定义的每个属性或方法都可以用作搜索索引。
class IProject(interface.Interface):
title = schema.TextLine(title=u'Title', required=True, constraint=check_title)
kind = schema.Choice(title=u'Kind of project', values=['personal', 'business'])
description = schema.Text(title=u'Description', required=False)
next_id = schema.Int(title=u'Next id', default=0)
def searchableText():
"""return concatenated string with all text fields to search"""
注意方法定义内部缺少 self 参数。这个参数不是必需的,因为我们不是定义一个类,而是一个接口。通常,我们会在定义体的注释中包含对方法的描述。
然后,我们在 ProjectIndexes 类中将此方法的名称作为一个索引添加,如下所示:
searchableText = grok.index.Text()
Grok 使用接口定义来查找具有该名称的方法或属性。如果是一个方法,它将在索引时被调用,因此这里发生的情况是我们正在通知 Grok,通过调用其自身的 searchableText 方法,一个项目的全文条目将可用。这个方法在 Project 类中定义如下:
def searchableText(self):
return self.title+self.description
这个方法非常简单。我们只需将 title 和 description 属性作为单个字符串返回,这样索引实际上将包括这两个字段。通过使用新的 searchableText 索引查询目录,我们可以同时搜索这两个字段:
self.results = catalog.searchResults(searchableText=query)
这就完成了对这个要求的覆盖,但正如我们所看到的,这种方法可以用来让 index 方法返回任何类型的内容,这给了我们足够的自由来创建我们可能需要的任何数量的组合索引。
第七章:索引不同类型的对象
到目前为止,我们只能搜索Project类的标题和描述字段,没有更多。列表和项目也有描述字段,所以如果我们的简单搜索查询也能在列表及其项目内部进行搜索,那就很好了。
目前,我们使用IProject作为搜索的上下文。context类注解不接受超过一个参数,所以我们不能只是传递ITodoList和ITodoItem就完成了。这正是接口概念可以发挥作用的地方。我们可以定义一个通用的搜索接口,并让所有我们的对象实现它。它们甚至不需要提供每个属性:
class ISearchable(interface.Interface):
title = interface.Attribute('title')
kind = interface.Attribute('kind')
description = interface.Attribute('description')
creator = interface.Attribute('creator')
creation_date = interface.Attribute('creation date')
modification_date = interface.Attribute('modification date')
checked = interface.Attribute('checked')
def searchableText():
"""return concatenated string with all text fields to search"""
注意,我们不是使用模式来定义属性,而是简单地使用interface包中的Attribute类。我们不会从这个接口生成表单,因此只需描述其属性而无需担心字段属性即可轻松完成。
一旦我们定义了这个接口,并将我们希望在目录中索引的所有属性包含在内,我们只需声明每个模型实现它即可。例如:
class Project(grok.Container):
grok.implements(IProject, IMetadata, ISearchable)
然后,我们将context注解参数替换掉,以便也能使用它。这非常强大,意味着任何通过grok.implements(ISearchable)类注解声明自己可搜索的对象都将被 Grok 自动索引,或者catalog.searchResults的调用将考虑它们。
class ProjectIndexes(grok.Indexes):
grok.site(ITodo)
grok.context(ISearchable)
title = grok.index.Text()
description = grok.index.Text()
searchableText = grok.index.Text()
creator = grok.index.Field()
modification_date = grok.index.Field()
creation_date = grok.index.Field()
checked = grok.index.Field()
注意我们现在使用ISearchable作为上下文,并创建了所有剩余的我们可以使用的索引。由于这些索引将包含对不同类型模型的引用,我们在每个模型中添加了一个content_type属性来保存它所代表的模型类型。例如,以下是改进的TodoList类定义。我们将ISearchable添加到grok.implements声明中,这样我们的搜索视图就可以同时处理待办事项和项目。
class TodoList(grok.Container):
grok.implements(ITodoList, IMetadata, ISearchable)
next_id = 0
description = u''
content_type = 'list'
def __init__(self,title,description,creator):
super(TodoList, self).__init__()
self.title = title
self.description = description
self.next_id = 0
self.creator = creator
self.creation_date = datetime.datetime.now()
self.modification_date = datetime.datetime.now()
def searchableText(self):
return self.title+self.description
再次,我们实现了ISearchable,这标志着TodoList模型可索引。然后我们定义content_type为list,这样当搜索结果包含多种类型的对象时,我们可以通过查看此属性来找出其中是否有一个是列表。注意,其余的元数据属性现在在创建时通过__init__方法分配了一些值,以便结果显示完整。
最后,定义了全文搜索的searchableText方法。在这种情况下,代码与Project类中同名方法的代码相同,但它可能不同,正如在未在此处显示的TodoItem类中所示。
目录管理界面
当你的目录开始增长时,找出它有多少对象以及每个索引的填充情况可能很有用。Grok 包含一个用于对目录进行简单内省的包,这允许我们查看内部情况。
目录 UI 未集成到 Grok 管理 UI 中。但是,我们可以通过使用直接 URL 从 Zope 端访问目录。让我们这样做。在todo应用程序中创建一些项目和列表,然后通过浏览器访问:http://localhost:8080/todo/++etc++site/Catalog/@@advanced.html。浏览器中会显示项目索引列表和一些统计数据,如下面的截图所示:

这是目录 UI 的统计屏幕,所以我们在这里不能做太多,除了查看数字,但它确实让我们对我们的应用程序数据在目录中的存储有了很好的认识。
有其他标签页,其中最重要的是内容标签页,从这里我们可以访问每个索引的信息屏幕。
索引类型
如我们从ProjectIndexes类的代码中可以看到,存在多种索引类型。到目前为止,我们已经使用了一个文本索引,它允许我们进行全文搜索。然而,并非所有搜索都是平等的;文本搜索允许我们在字段值内部查找单词或单词片段,但在某些情况下,我们需要精确匹配字段值或完全不匹配。
Grok 提供了三个现成的索引类,以下表格中进行了总结:
| 索引 | 描述 |
|---|---|
字段 |
与整个字段匹配。用于索引可排序的值和查询范围。支持排序和限制结果数量。 |
文本 |
支持字段的全文本搜索。查询可以包括'and'和'or'布尔运算符。还可以通过使用星号(通配符)来搜索字符串片段。 |
集合 |
支持字段的关键词搜索。该索引允许搜索包含任何一组值、一组值的所有值或一组值之间的文档。 |
我们现在不会在我们的应用程序中添加其他索引的搜索选项,但这里有一些使用它们的示例。为了完整性,让我们从文本字段开始:
在标题中查找包含单词'caveman'的对象:
results = catalog.searchResults(title='caveman')
在描述中查找包含'spear'或'club'的所有对象:
results = catalog.searchResults(description='spear or club')
在标题中查找以'cave'开头的任何单词的对象(如 caveman、caved、caveat 等):
results = catalog.searchResults(title='cave*')
字段索引的工作方式不同。查询时必须始终使用元组,即使我们只对单个值感兴趣。如果我们想找到由用户'Manager'创建的所有对象:
results = catalog.searchResults(creator=(u'Manager', u'Manager'))
2009 年 3 月 31 日至今天创建的所有对象:
from datetime import datetime
results = catalog.searchResults(creation_date=(datetime(2009,3,31), datetime.now())
集合索引允许我们在值列表中找到匹配项,这对于查找具有一些关键词或标签的对象很有用,但这些对象不一定应用了相同的标签列表。
摘要
在本章中,我们学习了如何使用目录进行内容的搜索和分类。现在我们能够为我们的应用程序添加索引和自定义搜索查询。在下一章中,我们将关注安全性问题。
第七章:安全
目前,我们的待办事项管理应用程序可以被任何人访问。任何能够访问 Grok 服务器上的 8080 端口的用户都将能够创建、更新和删除项目和任务。
如果我们考虑应用程序想要执行的操作,那么最初只允许经过身份验证的用户访问它将会很理想。例如,应该可以在公司内部安装应用程序,并且只有单个部门作为应用程序的用户。拥有正确权限的用户可以创建项目和将任务分配给团队成员。
Grok 包括完成此任务所需的工具,正如我们将在本章中看到的那样。以下是我们将学习的核心概念:
-
身份验证和授权
-
实体、权限和角色
-
默认安全策略是什么以及如何在
site.zcml配置文件中定义它 -
使用
site.zcml声明设置身份验证 -
设置自定义安全策略
-
添加和管理用户
身份验证和授权
在讨论 Web 应用程序安全时,在开始之前有两个重要概念需要正确理解:身份验证和授权。
在一个特定的应用程序中,我们可能会有许多被允许登录的用户。为了这样做,他们通常会提供一个用户名和一个密码。如果用户名和密码匹配,则用户已经通过身份验证,因此系统假设他就是他所说的那个人。在本章中,我们将学习如何通过使用插件在 Grok 中执行身份验证。
一旦用户登录,他可能想要执行多项操作,从仅查看应用程序到管理它。通过给予他执行特定操作的特定权限,用户被授权执行一些或所有可用的操作。
实体、权限和角色
在 Grok 中,用户被称为“实体”。一个实体代表与应用程序交互的任何实体,无论是用户还是任何类型的代理,例如远程客户端程序。一个组是一种特殊的实体,可以包含其他实体。在本章的其余部分,当我们谈论实体时,我们将主要使用更熟悉的术语“用户”。
Grok 中的视图可以通过权限进行保护,这样只有拥有正确权限的用户才能访问每个视图。然而,默认情况下,Grok 允许包括经过身份验证的用户在内的每个人都无限制地访问所有视图。
与为单个用户分配和跟踪权限相比,将相关权限分组并将此组分配给用户更有效率。这就是 Grok 中角色的作用。例如,我们一直在使用的用于访问 Grok 管理 UI 的 admin 用户具有zope.Manager角色,该角色授予此用户所有现有权限。
在介绍本章的“设置自定义安全策略”部分中的安全策略之后,我们将了解权限和角色。
安全策略
因此,Grok 应用程序将具有许多主体、权限和角色。这些集合被称为安全策略,代表特定应用程序的全局安全决策。
如前所述,Grok 应用程序有一个默认的安全策略,它给予所有人查看权限。它还定义了认证和未认证的用户组。
默认的 Grok 安全策略定义
Grok 默认使用的安全策略定义在site.zcml文件中,该文件位于etc目录内。此文件中有几个声明,我们将逐一讨论。
以下声明是系统中未认证用户的用户表示:
<unauthenticatedPrincipal id="zope.anybody" title="Unauthenticated User" />
所有未认证访问应用程序的用户将共享相同的 ID。
未认证组被分配给未认证主体:
<unauthenticatedGroup id="zope.Anybody" title="Unauthenticated Users" />
对于分组操作来说,定义这一点是有用的。
接下来,我们有一个包含所有已认证用户的动态组,无论他们的权限或角色如何:
<authenticatedGroup id="zope.Authenticated" title="Authenticated Users" />
最后,有一个包括所有用户(认证或不认证)的组:
<everybodyGroup id="zope.Everybody" title="All Users" />
现在我们来到了定义用户的部分。在这种情况下,只有一个用户,即“站点管理员”,登录名为“admin”:
<principal id="zope.manager"
title="Manager"
login="admin"
password_manager="Plain Text"
password="admin"
/>
如您所见,密码以纯文本形式分配给此管理员。您可能还记得这里定义的标题是我们列出仪表板中的项目时在创建者列中显示的内容。
下面的两个声明将查看权限授予zope。任何代表之前定义的未认证主体的Anybody用户。
<grant permission="zope.View"
principal="zope.Anybody" />
<grant permission="zope.app.dublincore.view"
principal="zope.Anybody" />
注意,在这种情况下,没有单独的权限定义,因为权限只是一个名称。这些声明允许所有未认证用户查看,因此默认情况下,应用程序对查看是开放的。
接下来是角色定义:
<role id="zope.Manager" title="Site Manager" />
<role id="zope.Member" title="Site Member" />
定义了一个“站点管理员”角色用于管理站点。然后,定义了一个“站点成员”角色用于常规站点用户,尽管在默认配置中此角色未使用。
最后,通过使用grantAll将所有权限授予zope.Manager角色。
<grantAll role="zope.Manager" />
<grant role="zope.Manager"
principal="zope.manager" />
这意味着具有管理角色的用户将获得所有定义的权限。然后,该角色被分配到我们在主体、权限和角色部分中定义的用户。
回顾一下,安全策略由以下内容组成:
-
一群可以登录应用程序的用户。
-
用户组,可以包含任意数量的用户。一个用户也可以属于多个组。
-
允许这些用户与应用程序的部分进行工作的特定权限。
-
可以分配多个权限的角色,这样用户或组可以分配一个包含所有相关权限的任务角色。这大大简化了权限管理。
修改安全策略
到目前为止,我们一直是通过使用在默认 Grok 安全策略定义部分中定义的“manager”用户来访问应用程序的。现在,关闭所有浏览器窗口,重新打开浏览器,直接访问todo应用程序的 URL,无需登录。你未经过身份验证,但不会显示登录窗口。添加一个项目或进行其他更改,看看应用程序是如何顺从地执行的。

等一下,我们不是说只将查看权限分配给了未经身份验证的用户吗?那么 Grok 为什么允许我们更改项目呢?
嗯,问题是,如果我们想保护它们,视图需要通过权限进行特别保护。在我们这样做之前,用户是否经过身份验证并不重要,更不用说如果他拥有所需的权限。再次强调,所有这些都是 Grok 应用程序非常开放默认安全策略的意图。
修改默认视图权限
让我们稍微修改一下默认安全策略,看看它是如何工作的。在待办事项列表管理员的特定情况下,我们希望阻止未经身份验证的用户,因此让我们首先将grant permission声明更改为以下代码:
<grant permission="zope.View"
principal="zope.Authenticated" />
<grant permission="zope.app.dublincore.view"
principal="zope.Authenticated" />
我们不是将查看权限授予每个人,而是授予我们之前定义的zope.Authenticated组,这样只有经过身份验证的用户才能访问应用程序。
添加新用户
为了测试这些声明,我们需要向网站添加另一个用户,因为“manager”用户无论如何都会获得查看权限。在site.zcml中添加以下行,在 manager 定义之后:
<principal id="todo.user"
title="User"
login="user"
password_manager="Plain Text"
password="user"
/>
我们现在有一个todo.user,用户名为“user”,密码为“password”。保存文件并重新启动 Grok。由于安全策略是在创建应用程序时应用的,因此有必要删除并重新创建应用程序。
现在应该可以以todo.user的身份登录。试试看:前往网站的根目录以获取登录窗口,然后输入新用户的登录名和密码。你会看到一个错误消息,因为新用户没有权限管理 Grok 应用程序。忽略错误并转到应用程序 URL。项目仪表板将在浏览器中显示。
当然,我们的新用户可以创建和删除项目以及任务,因为视图没有受到保护。不仅如此,未经身份验证的用户仍然可以查看和编辑一切。
保护视图
是时候保护一个视图并阻止未经身份验证的用户访问它了,正如预期的那样。只需将仪表板视图更改为以下内容:
class DashBoard(grok.View):
grok.context(Todo)
grok.name('index')
grok.require('zope.View')
为了使用权限保护一个视图,我们使用grok.require类注解。注意我们是如何将权限名称传递给它,正如在site.zcml配置文件中定义的那样。重新启动应用程序(这次不需要重新创建它),然后关闭并重新打开浏览器以丢失当前的认证信息。尝试在不登录的情况下访问应用程序 URL,你应该会看到登录窗口。未经认证的用户不能再查看仪表板了。输入todo.user凭证,仪表板将再次出现。
设置自定义安全策略
现在我们已经了解了安全机制的工作原理,我们准备将我们自己的安全策略添加到应用程序中。让我们从创建一些权限并将它们附加到我们的视图中开始。
创建权限
Grok 提供了一个非常简单的机制来定义权限和限制访问。可以通过从grok.Permission类派生并添加一个名称来简单地定义一个权限。
为了保持简单,让我们只为我们的应用程序定义四个权限:一个通用的查看权限,以及添加项目和列表或修改列表项的特定权限:
class ViewTodos(grok.Permission):
grok.name('todo.view')
class AddProjects(grok.Permission):
grok.name('todo.addprojects')
class AddLists(grok.Permission):
grok.name('todo.addlists')
class ChangeItems(grok.Permission):
grok.name('todo.changeitems')
通过使用grok.require指令将权限应用于视图,因此为了保护我们的每个视图,我们需要遍历我们的应用程序代码,并为每个视图添加适当的grok.require语句。例如:
class DashBoard(grok.View):
grok.context(Todo)
grok.name('index')
grok.require('todo.view')
class ProjectIndex(grok.View):
grok.context(Project)
grok.name('index')
grok.require('todo.view')
class TodoAddList(grok.View):
grok.context(Project)
grok.name('addlist')
grok.require('todo.addlists')
class TodoDeleteProject(grok.View):
grok.context(Todo)
grok.name('deleteproject')
grok.require('todo.addprojects')
我们使用视图权限保护仪表板,这意味着匿名用户将无法访问它。TodoAddList视图将需要addlists权限,而要删除项目,则需要addprojects权限。这样,我们可以使用我们想要的权限来保护所有视图。
角色
视图通过权限进行保护,但将权限分配给实际用户最好是通过使用角色。让我们为应用程序定义三个简单的角色。项目成员将只能查看和更改列表项。项目经理可以执行所有这些操作,还可以创建列表。应用程序管理员是唯一可以创建项目的人。
grok.Role类对于定义这些角色很有用,如下面的代码所示:
class ProjectMemberRole(grok.Role):
grok.name('todo.ProjectMember')
grok.permissions('todo.view','todo.changeitems')
class ProjectManagerRole(grok.Role):
grok.name('todo.ProjectManager')
grok.permissions('todo.view','todo.changeitems','todo.addlists')
class AppManagerRole(grok.Role):
grok.name('todo.AppManager')
grok.permissions('todo.view','todo.changeitems', 'todo.addlists','todo.addprojects')
在这里,我们已经为我们的应用程序中不同用户级别创建了四个不同的角色。每个角色分别通过使用grok.name和grok.permissions声明分配了一个名称和一个或多个权限。
如您所见,角色只是一个具有名称的权限集合。使用此类集合而不是单独分配每个权限的好处是,一旦将角色分配给用户,就可以在不为每个用户单独授予或撤销权限的情况下,向角色添加或删除权限。
添加认证
我们现在已经保护了所有的视图,但到目前为止还没有用户,所以没有人可以被分配我们创建的角色。我们需要添加一个创建和管理用户以及验证他们并分配角色的机制,这样他们就可以使用不同的视图。
可插拔认证工具
可插拔认证工具(PAU)是一个用于用户认证的框架。它是 Zope 工具包的一部分,目前 Grok 没有提供内置机制来与之工作,但我们将看到它并不复杂。
PAU 使用插件来完成其工作,因此可以有不同的认证源插件,并且可以轻松地替换它们,甚至让它们协同工作。
PAU 有两种类型的插件:“凭证”插件从请求中提取凭证(例如用户名和密码),“认证”插件检查这些凭证是否有效,并在凭证有效的情况下为应用程序生成一个用户。
在我们的应用程序中注册 PAU
要能够在我们的应用程序中使用 PAU,我们首先需要将其注册为一个本地实用程序。实用程序是一个应用程序可以提供的服务。本地意味着它可以存储特定于应用程序每个实例的信息和配置。
下面是如何在我们的主todo应用程序定义中注册 PAU 的方法。首先,我们导入PluggableAuthentication,这是一个工厂,将创建实际的 PAU 对象。我们还导入了IAuthentication,这是我们的 PAU 实用程序必须提供的接口,以便与认证机制集成。
from zope.app.authentication.authentication import PluggableAuthentication
from zope.app.security.interfaces import IAuthentication
from auth import setup_authentication
当我们注册一个 PAU 实用程序时,我们需要配置它使用适当的插件,这就是前面代码中setup_authentication import语句的目的。
现在我们通过使用grok.local_utility指令并传递给该指令PluggableAuthentication工厂、它将要提供的接口以及setup函数来注册这个实用程序。请注意,这个指令并不局限于 PAU,任何类型的服务都可以这样注册:
class Todo(grok.Application, grok.Container):
grok.implements(ITodo)
grok.local_utility(
PluggableAuthentication, provides=IAuthentication,
setup=setup_authentication,
)
目前在主app.py文件中我们只需要做这么多。让我们添加setup_authentication方法,以及其他所有安全类和视图,到另一个模块中。创建auth.py文件,并添加以下行到其中:
def setup_authentication(pau):
pau.credentialsPlugins = ['credentials']
pau.authenticatorPlugins = ['users']
这是一个非常简单的方法,它只是将一个名为credentials的插件分配给 PAU 的credentialsPlugins,并将另一个名为users的插件分配给authenticatorPlugins。第一个将负责从浏览器请求中提取用户的凭证并将其提供给应用程序。users插件将用于认证。当然,这些插件目前还不存在;我们需要创建它们。
添加凭证插件
对于凭据提取服务,我们将使用一个名为 SessionCredentialsPlugin 的插件,该插件包含在 Zope 工具包中。正如其名称所暗示的,此插件通过请求会话存储提取的凭据,以便应用程序可以轻松使用这些信息。由于我们不需要在插件中存储任何特定于应用程序的信息,这次让我们使用一个全局实用工具。
from zope.app.authentication.session import SessionCredentialsPlugin
from zope.app.authentication.interfaces import ICredentialsPlugin
class MySessionCredentialsPlugin(grok.GlobalUtility, SessionCredentialsPlugin):
grok.provides(ICredentialsPlugin)
grok.name('credentials')
loginpagename = 'login'
loginfield = 'login'
passwordfield = 'password'
全局实用工具 简单来说是一个不存储在应用程序数据中,但位于可供 Grok 中所有应用程序实例访问的注册表中的服务。我们将在第十一章中更详细地解释实用工具。
注意我们如何从 grok.GlobalUtility 和我们之前提到的 SessionCredentialsPlugin 继承。这里的 grok.name 指令非常重要,因为它为我们之前在 setup_authentication 方法中为我们的 PAU 配置的插件分配了一个名称。
之后是一些用于配置插件工作方式的类变量。loginpagename 是当用户尝试访问受保护页面时将显示的视图的名称。通常,这指向一个登录表单。loginfield 和 passwordfield 是表单中包含用户凭据的字段名称。它们将由认证插件用于使用应用程序对用户进行认证。
登录表单
现在我们已经配置了凭据插件,当用户想要访问受保护视图时查找登录表单,我们不妨立即创建该表单。
首先,我们通过使用接口定义一个表单模式。login 和 password 字段应该与我们配置在凭据插件中的名称完全相同。我们添加了一个 camefrom 参数,该参数将用于在用户登录之前将其重定向到他想要查看的页面。
class ILoginForm(Interface):
login = schema.BytesLine(title=u'Username', required=True)
camefrom = schema.BytesLine(title=u'', required=False)
password = schema.Password(title=u'Password', required=True)
SessionCredentialsPlugin 在将用户重定向到登录表单时自动将此变量添加到请求中,因此名称必须相同。这就是为什么我们在以下代码中将表单字段的 prefix 赋值为空字符串,以保持名称不变。
注意,我们使用 grok.require 声明将权限 zope.Public 分配给视图。此权限在驱动 Grok 的 Zope 工具包中定义,并分配给所有人都可以看到的视图。我们使用此权限来明确表示,每位访问我们应用程序的访客都可以访问登录表单。
class Login(grok.Form):
grok.context(Interface)
grok.require('zope.Public')
label = "Login"
template = grok.PageTemplateFile('custom_edit_form.pt')
prefix = ''
form_fields = grok.Fields(ILoginForm)
我们需要在这个类中实现一个 setUpWidgets 方法,以确保 camefrom 字段不会在表单上显示,该表单使用我们在第五章中创建的自定义模板。这样做是为了使其看起来与我们所使用的布局融为一体。
def setUpWidgets(self, ignore_request=False):
super(Login,self).setUpWidgets(ignore_request)
self.widgets['camefrom'].type = 'hidden'
最后,handle_login 动作将用户重定向到 camefrom URL,或者如果 camefrom 不可用,则重定向到网站根目录。如果用户输入无效凭据,登录表单将再次显示。
@grok.action('login')
def handle_login(self, **data):
self.redirect(self.request.form.get('camefrom', self.url(grok.getSite())))
就这样。看看下一个截图中的表单在实际操作中的样子:

注销视图
我们有一个登录机制,所以我们需要一种方法来结束会话,也许登录为不同的用户。我们将添加一个注销视图来处理这个需求:
from zope.app.security.interfaces import IAuthentication, IUnauthenticatedPrincipal, ILogout
class Logout(grok.View):
grok.context(Interface)
grok.require('zope.Public')
def update(self):
if not IUnauthenticatedPrincipal.providedBy( self.request.principal):
auth = component.getUtility(IAuthentication)
Ilogout(auth).logout(self.request)
首先,我们需要确定当前用户是否已登录,我们通过使用 self.request.principal 从请求中获取此信息,并检查它是否提供了 IUnauthenticatedPrincipal 接口。如果是,那么我们就知道这个用户未认证。如果结果显示他已经认证,我们将使用 component.getUtility 来查找我们的 PAU 并调用 logout 方法。
这个视图需要一个模板,我们现在将保持它相当简单。请看下一张截图以查看此视图的实际操作。
<html>
<head>
<title>Logged out</title>
</head>
<body>
<p>You are now logged out.</p>
<p><a tal:attributes="href python:view.url(context)"> Log in again</a>.
</p>
</body>
</html>

UserAuthenticator 插件
对于认证部分,我们需要另一个本地实用工具,因为我们需要在那里存储用户信息。我们以类似 PAU 实用工具的方式注册该实用工具,使用 grok.local_utility 指令。实际上,这段代码位于我们的主应用程序文件 app.py 中的 PAU 声明下方:
from auth import UserAuthenticatorPlugin
grok.local_utility(
UserAuthenticatorPlugin, provides=IAuthenticatorPlugin,
name='users',
)
这里的唯一区别是我们传递了一个 name 参数,它必须与我们在 setup_authentication 方法中用于插件的名称相匹配。
我们将创建一个 Grok 容器来存储用户账户,因此我们的插件将知道如何创建新用户,并决定登录尝试是否为现有用户提供了有效的凭证。让我们一步一步地查看认证插件代码。
首先,我们需要做一些导入,然后是插件定义:
from zope.app.authentication.session import SessionCredentialsPlugin
from zope.app.authentication.interfaces import ICredentialsPlugin
from zope.app.authentication.interfaces import IAuthenticatorPlugin
from zope.app.authentication.interfaces import IprincipalInfo
class UserAuthenticatorPlugin(grok.LocalUtility):
grok.implements(IAuthenticatorPlugin)
grok.name('users')
def __init__(self):
self.user_folder = UserFolder()
注意我们如何从 grok.LocalUtility 继承并实现 IAuthenticatorPlugin 接口。当实用工具初始化(__init__ 方法)时,我们创建一个用户文件夹并将其存储在那里。用户文件夹是一个简单的 Grok 容器:
class UserFolder(grok.Container):
pass
现在我们来看插件本身的方法。authenticateCredentials 方法在每次登录尝试时被调用。它接收凭证插件从请求中提取的凭证,然后使用 getAccount 方法尝试获取一个有效账户。接下来,它调用账户的 checkPassword 方法来验证密码与用户凭证中的密码是否一致。
def authenticateCredentials(self, credentials):
if not isinstance(credentials, dict):
return None
if not ('login' in credentials and 'password' in credentials):
return None
account = self.getAccount(credentials['login'])
if account is None:
return None
if not account.checkPassword(credentials['password']):
return None
return PrincipalInfo(id=account.name,
title=account.real_name,
description=account.real_name)
注意,authenticateCredentials 方法返回一个包含账户名称、标题或显示名称和用户描述的 PrincipalInfo 对象。此对象实现了在代码顶部导入的 IPrincipalInfo 接口,这意味着你可以在其他认证插件中期待找到类似的行为。
这是此插件中使用的 PrincipalInfo 类代码。除了主体信息外,它还持有用于授权用户的凭证和认证插件。
class PrincipalInfo(object):
grok.implements(IPrincipalInfo)
def __init__(self, id, title, description):
self.id = id
self.title = title
self.description = description
self.credentialsPlugin = None
self.authenticatorPlugin = None
我们在这个插件中实现的 IAuthenticatorPlugin 接口需要有一个 principalInfo 方法,该方法应返回我们刚刚定义的 PrincipalInfo 对象:
def principalInfo(self, id):
account = self.getAccount(id)
if account is None:
return None
return PrincipalInfo(id=account.name,
title=account.real_name,
description=account.real_name)
插件最重要的方法是 getAccount,它试图在 user_folder 中找到指定的用户账户,并返回该账户,如果用户未找到,则返回 None。目前我们使用 Grok ZODB 存储用户,但我们可以通过修改此方法轻松访问关系数据库或外部认证系统。
实现相当直接。我们使用布尔表达式检查传递给方法的方法登录是否在我们的 user 文件夹中,如果是,则返回文件夹内的用户对象。否则,我们返回 None:
def getAccount(self, login):
return login in self.user_folder and self.user_folder[login] or None
添加用户
我们认证插件中的另一个重要方法是 addUser。它创建一个具有给定用户名的账户对象,然后使用此名称为用户分配一个角色。
def addUser(self, username, password, real_name, role):
if username not in self.user_folder:
user = Account(username, password, real_name, role)
self.user_folder[username] = user
role_manager = IPrincipalRoleManager(grok.getSite())
if role==u'Project Manager':
role_manager.assignRoleToPrincipal ('todo.ProjectManager',username)
elif role==u'Application Manager':
role_manager.assignRoleToPrincipal ('todo.AppManager',username)
else:
role_manager.assignRoleToPrincipal ('todo.ProjectMember',username)
在我们的 user 文件夹内创建用户账户后,这个方法中最重要的部分是我们根据 addUser 表单传递的角色分配适当的角色给新用户的部分。
观察我们首先如何获取站点的 RoleManager,如下所示:
role_manager = IprincipalRoleManager(grok.getSite())
然后,当我们知道要应用什么角色时,我们使用其 assignRoleToPrincipal 方法:
role_manager.assignRoleToPrincipal('todo.ProjectMember',username)
这里是我们使用的账户类:
from zope.app.authentication.interfaces import IpasswordManager
class Account(grok.Model):
def __init__(self, name, password, real_name, role):
self.name = name
self.real_name = real_name
self.role = role
self.setPassword(password)
def setPassword(self, password):
passwordmanager = component.getUtility(IPasswordManager, 'SHA1')
self.password = passwordmanager.encodePassword(password)
def checkPassword(self, password):
passwordmanager = component.getUtility(IPasswordManager, 'SHA1')
return passwordmanager.checkPassword(self.password, password)
账户对象需要包含 checkPassword 和 setPassword 方法,这些方法与密码管理实用程序一起使用。实用程序执行所有繁重的工作,因为 checkPassword 方法只是获取账户密码并将其与用户输入的密码一起传递给 passwordManager 的 checkPassword 方法。
setPassword 方法使用 passwordManager 实用程序的 encodePassword 方法来设置密码。你可能还记得,当我们在本章开头处理 site.zcml 声明时,我们看到了一个 'plain-text' 密码管理器。在这种情况下,我们使用 SHAI 密码管理器,以便能够存储加密密码。
基本用户管理
当然,我们需要一种方法将用户添加到我们的应用程序中。UserAuthenticator 的 addUser 方法仅由我们使用第五章中讨论的接口和模式机制定义的表单自动呈现机制调用。首先,我们为表单字段定义一个接口:
class IAddUserForm(Interface):
login = schema.BytesLine(title=u'Username', required=True)
password = schema.Password(title=u'Password', required=True)
confirm_password = schema.Password(title=u'Confirm password', required=True)
real_name = schema.BytesLine(title=u'Real name', required=True)
role = schema.Choice(title=u'User role', values=[u'Project Member', u'Project Manager', u'Application Manager'], required=True)
然后,我们定义实际的 AddUser 表单,该表单使用先前定义的接口中的字段来构建表单。handle_add 方法使用 Grok 的 action 装饰器向表单添加一个按钮,该按钮将调用认证插件中的 addUser 方法:
class AddUser(grok.Form):
grok.context(Interface)
grok.require('zope.ManageApplication')
label = "Add user"
template = grok.PageTemplateFile('custom_edit_form.pt')
form_fields = grok.Fields(IAddUserForm)
@grok.action('add')
def handle_add(self, **data):
users = component.getUtility(IAuthenticatorPlugin,'users')
users.addUser(data['login'],data['password'], data['real_name'],data['role'])
self.redirect(self.url(grok.getSite(),'userlist'))
注意我们如何添加一个角色字段,允许管理员将我们定义的其中一个角色分配给每个用户。值得注意的是,这个用户管理视图受到zope.ManageApplication权限的保护,这个权限仅分配给 Zope 管理员。如果我们使用我们自己的权限,我们根本无法首先创建用户。完成后的表单可以在以下截图查看:

一个简单的用户列表视图完成了我们用户管理应用程序的用户界面:
class UserList(grok.View):
grok.context(Interface)
grok.require('zope.ManageApplication')
def update(self):
users = component.getUtility(IAuthenticatorPlugin,'users')
self.users = users.listUsers()
这里没有新内容。我们只是从UserAuthenticator插件获取用户列表,并将其传递给视图模板,模板简单地以表格形式列出用户。你现在应该熟悉一些这段代码。首先,我们使用静态视图插入我们的样式表定义,以正确显示模板中的 URL:
<html>
<head>
<title tal:content="context/title">To-Do list manager</title>
<link rel="stylesheet" type="text/css" tal:attributes="href static/styles.css" />
</head>
在模板的主体中,我们有一个作为页眉一部分的站点搜索表单,其中包含应用程序的标题。然后有一个链接用于添加新用户,它指向我们刚刚定义的表单。
<body>
<div id="appheader">
<form id="search" tal:attributes="action python:view.url('search')">
<input type="text" name="query" />
<input class="new_button" type="submit" value="search" />
</form>
<h1 id="apptitle" tal:content="context/title"> To-Do list manager</h1>
</div>
<p class="create"><a href="adduser">Add a new user</a></p>
只有当存在用户时,用户列表才会显示,它由一个表格组成,该表格遍历在UserList视图中定义的用户列表(视图/users)。
<h2 tal:condition="view/users">These are the existing users</h2>
<div class="projects">
<table>
<tr>
<th>Login</th>
<th>Real name</th>
<th>Role</th>
</tr>
<tr tal:repeat="user view/users">
<td tal:content="user/name">type</td>
<td tal:content="user/real_name">type</td>
<td tal:content="user/role">type</td>
</tr>
</table>
</div>
</body>
</html>
就这样。现在我们可以在应用程序中拥有多个具有不同配置文件的用户,如下一张截图所示。创建一些用户并测试权限和角色,以了解它们是如何协同工作的。

摘要
在本章中,我们学习了 Grok 的安全功能,并将身份验证和授权添加到我们的应用程序中。
第八章。应用程序展示和页面布局
到目前为止,我们已经开发了一个相当简单但完整的应用程序。显然,还有一些粗糙的边缘需要抛光。例如,我们使用的模板都有不同的布局,尽管它们使用或多或少相同的样式和颜色,但缺乏统一性。
在本章中,我们将学习如何布局应用程序。我们将涵盖的一些重要点包括:
-
视图小部件管理器和视图小部件
-
使用视图小部件进行布局定义
-
将表单插入视图小部件
-
层和皮肤
-
定义应用程序的替代皮肤
视图小部件和视图小部件管理器
在典型的 Web 应用程序中,其布局的许多部分几乎需要在每个页面上重复。有时这些部分需要以不同的组合出现。例如,如果用户已经登录,则不应显示登录提示,但搜索框应始终可见。
Grok 通过允许开发者将网页拆分成称为视图小部件的小块 HTML,然后将它们按要求组装在给定的视图中来解决此问题。
视图小部件
视图小部件提供了一种将页面不同组件分离成独立部分(如页眉、页脚和导航)的机制。可以将这些部分进一步分解成 HTML 片段,这些片段可以根据上下文信息显示在页面上或不显示。这个概念使我们能够在组装页面时具有很大的灵活性。
与旨在显示完整页面的视图不同,视图小部件是 HTML 片段的表示,通常具有一个清晰的功能。当使用视图小部件时,我们可以将页面视为这些片段的集合。
视图小部件管理器
为了避免创建视图,跟踪所有可能的视图小部件组合。视图小部件分配给特定的视图小部件管理器。视图小部件管理器可以表示页面布局的一部分,例如,例如标题。视图小部件向此管理器注册,以便它负责它们的顺序和渲染。在我们的标题视图小部件管理器中,例如,我们可以有标题、登录、搜索框和主要导航视图小部件。
这意味着视图小部件永远不会直接从模板中调用。相反,它们的视图小部件管理器被调用,然后它依次调用其注册的每个视图小部件,并按所需顺序在页面上渲染它们。视图小部件管理器还有其他一些职责:
-
聚合注册到管理器的所有视图小部件。
-
应用一组过滤器以确定视图小部件的可用性。
-
根据实施的政策对视图小部件进行排序。默认情况下,按它们声明的顺序显示它们,Grok 还可以根据视图的
grok.order([number])指令对它们进行数字排序。 -
提供视图小部件渲染的环境。
-
渲染本身,包含视图小部件的 HTML 内容。
视图小部件也可以与特定的视图、上下文或权限相关联,因此整个系统非常灵活,并且比带有条件标签的宏集合更容易管理。
理解各个部分如何组合
我们现在有了 Grok 构建页面布局的所有组成部分。当一个 Grok 应用程序服务一个页面时,幕后发生的事情如下:
-
浏览器发出请求,其中包含所需的 URL 以及任何表单输入,以及通常的
HTTP头信息,发送给 Grok。 -
根据 URL 的部分,Grok 从根开始遍历(遍历)网站,继续到 URL 的下一部分,直到它到达最后一个模型对象。这个模型对象被称为上下文。
-
当找到模型对象时,Grok 会使用 URL 的剩余部分并将其用作视图的名称。如果没有 URL 剩余,则使用名称 "index"。
-
一旦有了视图的名称,Grok 就会找到它并初始化视图,将上下文和用于找到视图的请求传递给它。
-
视图通常分配了一个模板,该模板用于将响应渲染到浏览器。
-
模板可能包含对多个视图管理器的调用,这些视图管理器反过来调用它们包含的视图小部件来组装将返回的响应中的 HTML。
模板是调用 Grok 应用程序后的最终结果,它代表了视图的渲染。视图反过来与一个上下文相关联,这是分配给该视图的模型对象。
Grok 向模板传递大量信息,以便让开发者使用所有这些部分。这些信息是以变量形式存在的,这些变量指向以下对象,开发者可以使用这些对象来构建模板:
-
request:浏览器发送的HTTP请求,包括所有头信息。 -
context:由请求的 URL 指向的模型对象。在我们的应用程序中,这可能是一个项目或待办事项列表项。 -
view:根据其名称配置用于上下文的视图。这是与当前模板相关联的代码。 -
viewlet:在视图小部件模板内部,这个变量代表视图小部件对象。 -
viewletmanager:在视图小部件模板内部,这个变量指向负责当前视图小部件的视图管理器。
视图方法和属性
这些方法和属性是view变量的一个部分,并且可以在模板内部被开发者使用。
| 视图方法和属性 | 描述 |
|---|---|
context |
视图所呈现的对象。这通常是一个grok.Model类的实例,但也可以是一个grok.Application、一个grok.Container对象或任何类型的 Python 对象。 |
request |
HTTP 请求对象。 |
response |
与请求相关联的HTTP 响应对象。这也可以作为self.request.response提供,但response属性提供是为了方便。 |
static |
包含视图包静态文件的目录资源。 |
redirect(url) |
重定向到指定的 URL。 |
url(obj=None, name=None, data=None) |
构建 URL。如果没有提供任何参数,则构建到视图本身的 URL。如果只提供了obj参数,则构建到obj的 URL。如果只提供了第一个参数name,则构建到上下文/名称的 URL。如果同时提供了对象和名称参数,则构建到obj/name的 URL。可选地,您可以传递一个作为cgi查询字符串添加到 URL 中的data关键字参数。 |
default_namespace() |
返回模板实现期望始终可用的命名空间字典。此方法不打算被应用程序开发者重写。 |
namespace() |
返回一个字典,该字典将注入到模板命名空间中,除了默认命名空间之外。此方法打算由应用程序开发者重写。 |
| 视图方法和属性 | 描述 |
update(**kw) |
此方法旨在由grok.View子类实现。它将在渲染视图的关联模板之前被调用,并可用于为模板预计算值。update()可以接受任意关键字参数,这些参数将从请求中填充(在这种情况下,它们必须存在于请求中)。 |
render(**kw) |
视图可以通过关联的模板进行渲染,或者它可以实现此方法以从 Python 本身进行渲染。如果视图的输出不是 XML/HTML,而是在 Python 中计算(例如纯文本、PDF 等),则这很有用。render()可以接受任意关键字参数,这些参数将从请求中填充(在这种情况下,它们必须存在于请求中)。 |
application_url(name=None) |
返回层次结构中最接近的应用程序对象的 URL,或相对于最接近的应用程序对象的命名对象(name 参数)的 URL。 |
flash(message, type='message') |
向用户发送简短的消息。 |
视图小部件方法和属性
在视图小部件模板内部,以下方法和属性可供开发者使用:
| 视图小部件方法和属性 | 描述 |
|---|---|
context |
通常,这是在此视图小部件的上下文中渲染的模型对象。 |
request |
Request对象。 |
view |
对视图的引用,视图小部件在其中提供。 |
viewletmanager |
对渲染此视图小部件的ViewletManager的引用。 |
update() |
在渲染视图小部件之前调用此方法,可用于执行预计算。 |
render(*args, **kw) |
此方法渲染由该视图小部件提供的内容。 |
视图小部件管理器方法和属性
viewletmanager变量在视图小部件内部对开发者可用。这些是它包含的方法和属性:
| 视图管理器方法和属性 | 描述 |
|---|---|
context |
这通常是 ViewletManager 在上下文中渲染的模型对象。 |
request |
Request 对象。 |
view |
对 ViewletManager 提供的视图的引用。 |
update() |
在 ViewletManager 渲染之前调用此方法,可以用来执行预计算。 |
render(*args, **kw) |
此方法渲染由该 ViewletManager 提供的内容。通常这意味着渲染和连接由该 ViewletManager 管理的所有视图。 |
除了这些方法外,视图管理器内部包含的视图可以使用标准的 Python 字典语法访问。
待办事项管理器布局
对于我们的应用程序,我们将使用视图来生成布局。首先,让我们定义我们的布局结构,它将大致是我们现在的样子。看看下面的截图:

这是一个非常简单的布局。三个部分(标题、主要和页脚)分别代表一个视图管理器以及这些部分内的事物列表,它们指的是所需视图。要在页面模板中实现这一点,我们需要使用一种特殊的模板表达式,称为提供者。我们的主模板将如下所示:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xml:lang="en" lang="en">
<head>
<tal:headslot content="structure provider:headslot" />
</head>
<body>
<div id="header">
<tal:header content="structure provider:header" />
</div>
<div id="main">
<tal:main content="structure provider:main" />
</div>
<div id="footer">
<tal:footer content="structure provider:footer" />
</div>
</body>
</html>
在考虑我们应用程序的最终展示时,首先要注意的是我们插入了一个正确的 <DOCTYPE> 声明。我们还从标题中移除了 <title> 和 <style> 标签,因为它们现在将放入视图内。
我们定义了四个布局区域:headslot、header、main 和 footer。每个区域的内容将由一个单独的视图管理器提供。provider:name 表达式告诉 Grok 查找一个具有此名称的视图管理器,渲染其所有已注册的视图,并按配置的顺序返回它们。如您所回忆的,structure 前缀意味着将函数调用返回的内容解释为要渲染的 HTML。
在应用程序代码中定义视图管理器
我们首先查看模板,以了解它如何与所需的布局相关联,但实际上我们需要定义一些视图管理器和视图来使这成为可能。幸运的是,Grok 有 Viewlet 和 ViewletManager 类,我们可以使用这些类。首先,让我们定义我们的视图管理器:
class HeadSlot(grok.ViewletManager):
grok.context(Interface)
grok.name('headslot')
class Header(grok.ViewletManager):
grok.context(Interface)
grok.name('header')
class Main(grok.ViewletManager):
grok.context(Interface)
grok.name('main')
class Footer(grok.ViewletManager):
grok.context(Interface)
grok.name('footer')
就这些了。我们只需从 grok.ViewletManager 继承,Grok 就会为我们做大部分工作。关键部分是 grok.name 类声明,因为我们在这里使用的名称将用于模板中的提供者表达式。grok.context 指令使用 Interface,因为它是最通用的声明,所以这样做将使所有应用程序视图和模型中的管理器生效。
注册视图
要使视图小部件运行,我们需要对我们的应用程序进行一些修改。到目前为止,我们一直在使用单独的视图来定义每个页面上发生的事情以及谁可以看到它。从我们刚刚定义的主模板中可能很明显,这现在将是视图小部件本身的职责。
要将我们的应用程序转换为使用视图小部件,第一步是从dashboard.pt模板中提取我们想要转换为视图小部件的部分,并将它们放入它们自己的模板中。我们将首先以标题为例。在项目中创建一个名为apptitle.pt的文件,并将以下代码输入其中:
<div id="apptitle">
<h1 tal:content="context/title">To-Do list manager</h1>
</div>
目前,这仅包含标题,但我们可以最终包括标志和其他类似元素。
要将此视图小部件注册到视图小部件管理器中,使用Viewlet类作为基类,并调用grok.viewletmanager类:
class AppTitle(grok.Viewlet):
grok.viewletmanager(Header)
grok.context(Interface)
grok.order(2)
就这样。此视图小部件将与之前定义的Header视图小部件管理器注册。视图小部件还需要一个上下文,以便视图小部件管理器知道它们是否要在当前上下文中显示。与视图小部件管理器的情况一样,我们使用Interface作为上下文,这样视图小部件就可以在应用程序的任何地方启用。还要注意grok.order指令,它在指定视图小部件管理器在渲染视图小部件时应使用的顺序时很有用。如果不使用grok.order,视图小部件将按照它们定义的顺序渲染。
Viewlet类与视图非常相似。它有一个update方法来准备它进行渲染,以及一个render方法来进行实际的渲染。如果省略了这些方法,Grok 将简单地渲染相应的模板,在这种情况下是apptitle.pt。
现在让我们来处理其他简单的视图小部件。头部部分的模板head.pt看起来是这样的:
<meta tal:attributes="http-equiv string:Content-Type; content string:text/html;;charset=utf-8" />
<title tal:content="context/title">To-Do list manager</title>
<link rel="stylesheet" type="text/css" tal:attributes="href static/styles.css" />
搜索框的模板searchbox.pt::
<div id="searchbox">
<form id="search" tal:attributes= "action python:view.url('search')">
<input type="text" name="query" />
<input class="new_button" type="submit" value="search" />
</form>
</div>
登录信息和注销链接loggedin.pt如下所示:
<div id="loggedin">
<p>
<span tal:content="string:Logged in as ${request/principal/title}"> </span>
<a tal:attributes="href python:view.url('logout')">(Log out)</a>
</p>
</div>
以及一个简单的导航辅助工具navigation.pt,定义为:
<div id="navigation">
<a tal:attributes="href python:view.url('index')">Go back to main page</a>
</div>
对于页脚,我们将使用一个简单的由 Grok 提供支持的消息,以及 Grok 的标志,该标志将在grokpowered.pt:中定义:
<div id="grokpowered">
<a href="http://grok.zope.org">
<img border="0" tal:attributes="src static/groklogo.png" />
</a>
<span id="poweredtext">Powered by Grok!</span>
</div>
现在我们将把这些视图小部件与它们的管理器注册。我们只需在app.py中添加适当的类声明,如下所示:
class Head(grok.Viewlet):
grok.viewletmanager(HeadSlot)
grok.context(Interface)
class SearchBox(grok.Viewlet):
grok.viewletmanager(Header)
grok.context(Interface)
grok.order(1)
class LoggedIn(grok.Viewlet):
grok.viewletmanager(Header)
grok.context(Interface)
grok.order(4)
class Navigation(grok.Viewlet):
grok.viewletmanager(Header)
grok.context(Interface)
grok.order(3)
class GrokPowered(grok.Viewlet):
grok.viewletmanager(Footer)
grok.context(Interface)
这样,Header视图小部件管理器已按特定顺序注册了四个视图小部件。目前Footer只有一个视图小部件。
修改现有视图以使用主模板
我们的应用程序中已经有了几个工作的视图。为了使它们使用在主模板中定义的新布局,我们需要做两件事:
-
让它们使用主模板
-
创建显示旧模板主要部分的视图小部件
第一部分很简单;我们只需使用grok.template指令强制每个视图使用主模板,如下所示:
class DashBoard(grok.View):
grok.context(Todo)
grok.name('index')
grok.require('todo.view')
grok.template('master')
视图小部件本身与我们定义的其他视图非常相似,增加了几行:
class DashBoardViewlet(grok.Viewlet):
grok.viewletmanager(Main)
grok.context(Todo)
grok.template('dashboard_viewlet')
grok.view(DashBoard)
我们使用grok.template指令来强制视图组件使用dashboard_viewlet模板。接下来使用grok.view指令,以使此视图组件仅在DashBoard视图中显示。我们必须这样做,以防止注册到Main视图组件管理器的多个视图组件同时出现在每个视图中。换句话说,这意味着当用户浏览应用程序的默认视图时,DashBoardViewlet将仅在Main视图组件管理器中渲染,而这个默认视图恰好是DashBoard视图。
要使这生效,我们需要额外的步骤。目前,我们在app_templates目录中有模板dashboard.pt。我们不能保留这个名称,因为这样DashBoard视图将有两个可能的模板可以使用,Grok 将拒绝猜测使用哪一个。因此,我们将名称更改为dashboard_viewlet.pt,这是我们放在grok.template指令中的名称。
最后一步是更改模板本身,并从中删除所有结构。这些结构现在包含在master.pt模板中。这是结果:
<div id="dashboard">
<p class="create"><a href="add">Create a new project</a></p>
<h2 tal:define="projects context/values" tal:condition="projects">These are your available projects</h2>
<div class="projects">
<table>
<tr>
<th>Project name</th>
<th>Kind</th>
<th>Description</th>
<th>Created on</th>
<th>Last modified</th>
<th>Owner</th>
<th>Delete</th>
</tr>
<tr tal:repeat="project context/values">
<td>
<a href="" tal:attributes="href python:view.url(project)" tal:content="project/title">title</a>
</td>
<td tal:content="project/kind">type</td>
<td tal:content="project/description">type</td>
<td tal:content="project/creation_date">type</td>
<td tal:content="project/modification_date">type</td>
<td tal:content="project/creator">type</td>
<td>
<a tal:define="url python:view.url('deleteproject')" tal:attributes="href string:${url}?project=${project/__name__}">
<img border="0" tal:attributes="src static/bin_closed.png" />
</a>
</td>
</tr>
</table>
</div>
</div>
我们必须执行相同的步骤来使TodoSearch和UserList视图工作。我们将得到TodoSearchViewlet和UserListViewlet声明,以及todosearch_viewlet.pt和userlist_viewlet.pt模板。ProjectIndex视图需要额外的工作,因为它使用了 JavaScript。我们将在本章后面看到如何解决这个问题。
目前,您可以查看下一张截图,看看布局是如何工作的:

将表单插入到视图组件中
由于“添加用户”和“添加项目”表单是自动生成的,它们仍然没有使用我们新的布局。我们必须将这些表单放入视图组件中,以便我们可以利用布局。这需要更多的工作,因为视图组件需要渲染表单。
class AddProject(grok.View):
grok.context(Todo)
grok.template('master')
class AddProjectViewlet(grok.Viewlet):
grok.viewletmanager(Main)
grok.context(Todo)
grok.view(AddProject)
def update(self):
self.form = getMultiAdapter((self.context, self.request), name='add')
self.form.update_form()
def render(self):
return self.form.render()
我们之前定义了AddProjectForm并给它命名为“add”。此代码直接使用该表单来渲染视图组件。首先,我们定义一个将使用主模板的视图。我们保持与实际表单相同的上下文,在这个例子中是Todo。
接下来,我们创建一个视图组件,并在相同上下文中将其注册到Main视图组件管理器中。我们将此视图组件分配给刚刚定义的视图。这里的技巧是获取表单,我们通过使用从zope.component导入的getMultiAdapter方法来实现。我们将在第十一章中更多地讨论适配器,但就目前而言,请相信这个调用将使我们得到当前上下文中的名为“add”的表单。一旦我们有了表单,我们就将其存储在视图组件中,并在视图组件的render方法中使用它,以便从实际表单中提取渲染。
那就是全部了,我们只需要将dashboard_viewlet.pt中的链接更改为指向“addproject”而不是“add”,我们就会得到我们想要的结果,如下面的截图所示:

使用视图组件插入不同的静态资源
我们几乎已经将整个应用程序转换为使用我们新的布局。唯一缺少的是项目视图。这个视图的问题在于它使用了一些在其他地方没有使用的 JavaScript 代码,因此我们需要以某种方式将其包含在标题中。
由于我们已经有了一个headslot视图组件管理器,完成这个任务的最简单方法是通过这个管理器注册一个视图组件,该视图组件只应用于项目视图。我们之前已经这样做过了。技巧是使用grok.view指令使视图组件只对选定的视图显示:
class ProjectJavascript(grok.Viewlet):
grok.viewletmanager(HeadSlot)
grok.context(Project)
grok.view(ProjectIndex)
我们现在可以添加一个名为projectjavascript.pt的新模板,该模板将包括我们的 JavaScript 调用:
<script type="text/javascript" tal:attributes = "src static/todo.js">
</script>
<script tal:define="url python:view.url('settitle')"
tal:content="string:window.onload = hideControls;; settitleurl = '${url}';;">
</script>
现在这个视图中的 JavaScript 代码将正常工作,并且项目视图组件可以无问题地显示。
层和皮肤
我们已经展示了如何通过使用视图组件来构建一个灵活的布局。Grok 通过使用层和皮肤,为我们提供了更多的灵活性。
我们应用程序的外观和感觉是通过结合一些视图组件并使用适当的 CSS 样式创建的。最终的组合可以被认为是应用程序的“主题”。在 Grok 中,我们为这个主题使用的名字是“皮肤”。
皮肤代表我们应用程序的外观和感觉。应该能够重新排列所有的视图组件并编辑样式,以创建一个完全不同的外观和感觉,而无需改变应用程序的工作方式。因此,有一个处理皮肤的机制,使我们能够轻松地为我们的应用程序创建主题或为其他用途创建特殊的皮肤,例如移动浏览器展示、响应特定 HTTP 动词的视图(称为 REST 视图,实际上在 Grok 内部就是这样构建的),或者根据登录用户的不同而创建的特殊高级用户皮肤。
为了简化皮肤的制作和使用,它们由多个层组成,每个层将只包含它们特有的外观和感觉部分。这使得重用应用程序的大部分 UI 变得容易,只需关注特定皮肤的特定部分。
Grok 中的皮肤
在 Grok 中,所有视图和视图组件都必须分配给一个层。到目前为止,这已经在后台发生了,因为默认情况下有一个所有视图都使用的层。这被称为默认浏览器层。Grok 中的所有应用程序都有一个默认皮肤,它没有名字,只由一个层组成。
然而,皮肤和层之间的区别是微妙的。实际上它们非常相似,但皮肤有一个用户将知道的名字,而层则是匿名的。目的是让皮肤由多个层组成,并使用名字来引用结果。
将主题添加到我们的应用程序中
让我们通过使用皮肤为我们的应用程序添加不同的主题。现在我们将使其非常简单。我们已经有了一个包含我们的样式表 <link> 标签的视图组件。为了设计一个主题,我们将简单地创建一个覆盖此视图组件并使用不同样式表的层。这样,我们只需添加七行代码、一个模板和一个样式表,就可以创建一个新的主题。
我们正在使用蓝色为我们应用程序的页眉。我们将添加第二个主题,它使用红色,还有一个使用绿色。由于这些名称将为人所知,我们将使用花哨的名称:红色主题将被称为“martian”,绿色主题将被称为“forest”。
在 Grok 中,一个层需要从 zope.publisher 中定义的 IDefaultBrowserLayer 继承。因此,我们首先需要做的是导入它:
from zope.publisher.interfaces.browser import IDefaultBrowserLayer
我们现在可以在 app.py 中定义皮肤,如下所示:
class MartianLayer(IDefaultBrowserLayer):
grok.skin('martian')
class ForestLayer(IDefaultBrowserLayer):
grok.skin('forest')
对于每一层皮肤,我们只需基于 IDefaultBrowserLayer 定义一个类,然后使用 grok.skin 指令为其命名。这个名称将被用来浏览皮肤。
覆盖视图组件
现在我们有了皮肤,我们可以覆盖定义样式表的 Head 视图组件。这个视图组件由 HeadSlot 视图组件管理器管理。要覆盖一个视图组件,我们只需创建另一个具有相同名称的组件:
class HeadMartian(grok.Viewlet):
grok.viewletmanager(HeadSlot)
grok.context(Interface)
grok.name('head')
grok.template('head_martian')
grok.layer(MartianLayer)
class HeadForest(grok.Viewlet):
grok.viewletmanager(HeadSlot)
grok.context(Interface)
grok.name('head')
grok.template('head_forest')
grok.layer(ForestLayer)
注意 HeadMartian 和 HeadForest 视图组件与它们覆盖的 Head 视图组件具有完全相同的视图组件管理器、上下文和名称。区别在于它们将使用另一个模板。为了使 HeadMartian 视图组件仅在 MartianLayer 皮肤上工作,我们添加了 grok.layer 指令并将其传递给皮肤。请记住,grok.layer 可以用于我们定义的任何视图组件或视图,因此我们可以覆盖应用程序中的任何内容,或者创建仅在特定皮肤上使用的新视图或视图组件。
在这种情况下,这就是我们需要添加到这两个主题中的所有代码。我们只需添加模板和样式,然后我们就完成了。对于 martian 主题,我们指导视图组件使用 head_martian 模板,因此我们创建一个名为 head_martian.pt 的文件,并向其中添加以下代码:
<meta tal:attributes="http-equiv string:Content-Type; content string:text/html;;charset=utf-8"/>
<title tal:content="context/title">To-Do list manager</title>
<link rel="stylesheet" type="text/css" tal:attributes="href static/styles_martian.css" />
唯一的改变是 <link> 标签中样式表的名称。
对于 forest 主题,我们将创建 head_forest.pt 文件,并添加相同的文本,但我们必须将 CSS 文件名更改为 styles_forest.css 以使用正确的样式表。
现在剩下的只是添加新的样式表。为了使事情简单,我们只需将 styles.css 文件复制到 styles_martian.css 和 styles_forest.css,然后进行一些颜色替换。使用您喜欢的文本编辑器,将 martian 中的所有 #223388 替换为 #883322,在 forest 中替换为 #338822。除此之外,主题将完全相同,但我们将能够轻松地看到差异。如果您喜欢,可以自由地进行更多样式更改。
使用新的皮肤
要真正看到新皮肤,我们需要告诉 Grok 使用默认皮肤以外的其他皮肤。最简单的方法是在 URL 中使用特殊的++skin++名称,并附带皮肤名称。Grok 会看到这个,并使用正确的皮肤。
例如,如果我们把浏览器指向以下 URL:localhost:8080/++skin++martian/todo,我们就能看到火星皮肤。注意++skin++名称紧接在主机名之后。如果我们把++skin++放在 URL 的其他位置,Grok 将会发出错误信号。要查看森林皮肤,只需在上面的 URL 中将单词martian改为forest。查看下一张截图,看看火星皮肤的样子:

我们已经完成了。如果我们浏览应用程序,我们会看到现在所有页面都显示了带有相应背景色的标题。
当然,要求用户在 URL 中输入皮肤名称并不适合生产使用。在第十一章中,我们将看到如何通过偏好设置菜单让用户选择皮肤,并让 Grok 自动应用正确的皮肤。
摘要
在本章中,我们学习了 Grok 的布局和展示功能,包括视图组件、层和皮肤。我们通过使用这些工具修改了我们的应用程序,使其外观和感觉保持一致。接下来,我们将学习 ZODB 以及 Grok 如何使用它。
第九章. Grok 和 ZODB
正如我们在第一章就强调的,Grok 最重要的组成部分之一是 ZODB。能够透明地存储对象的能力使我们能够创建一个完整的应用程序,而无需考虑其数据的持久化。
虽然 Grok 在幕后处理我们应用程序需要的 ZODB 交互,但 ZODB 还能做更多的事情,我们将在本章中回顾它们。特别是,我们将看到:
-
ZODB 是如何工作的
-
ZODB 存储是什么
-
如何通过使用 Blob 支持将文件附件添加到我们的应用程序中
-
如何打包数据库以及为什么推荐这样做
-
如何在 Grok 之外使用 ZODB
ZODB 是如何工作的
在我们深入探讨如何使用其他 ZODB 功能之前,让我们再谈谈 ZODB 是如何工作的。
简而言之,ZODB 是一个 Python 对象的持久化系统。其目标是使持久化尽可能透明,以便任何对象更改都可以自动保存到磁盘并在需要时再次读取。
ZODB 是一个对象数据库,这意味着不是根据数据模式将对象分区到一个或多个表中,而是将对象以当前状态写入数据库。在关系型数据库系统中,一旦从数据库中读取了一行数据,程序仍然需要重新组装信息列以获取一个真正的对象实例。在对象数据库系统中,当你从数据库中检索对象时,对象实例立即存在。
透明性
由于可以直接将对象以当前状态直接存储到数据库中,因此使对象成为 ZODB 意识所需的努力非常小。换句话说,对象是以透明的方式存储的。因此,尽管在某些情况下可能需要做更多的工作,但通常只需满足一个简单的要求:从 ZODB 提供的persistent.Persistent类派生子类,就可以在数据库中持久化对象。
当持久化对象的实例被修改时,它们会被 ZODB 机制标记,如果用户请求这样做,这些更改可以写入数据库。当然,可能同时会有多个修改请求。一组对存储对象的单个或多个更改被称为事务,当对象被写入时,我们说事务已经提交。
事务也可以被中止,这样自上次提交以来对对象的修改就会被遗忘。还可以回滚已提交的事务,使数据库回到之前的状态。
ACID 属性
如果你曾经使用过关系型数据库,那么关于事务的所有讨论都应该很熟悉。你可能也知道,事务性系统需要确保数据库永远不会进入不一致的状态,它们通过支持四个属性来实现这一点,这些属性以首字母缩写词ACID闻名:
-
原子性:
要么将事务中分组的所有修改都写入数据库,要么如果某些事情使这成为不可能,则整个事务将被中止。这确保了在发生写错误或硬件故障的情况下,数据库将保持之前的状态,避免不一致性。
-
一致性:
对于写事务,这意味着如果事务会导致数据库处于不一致状态,则不允许任何事务。对于读事务,这意味着读操作将看到事务开始时的数据库一致状态,而不管当时是否正在进行其他事务。
-
隔离:
当两个不同的程序对数据库进行更改时,它们将无法看到彼此的事务,直到它们提交自己的事务。
-
耐用性:
这仅仅意味着一旦事务提交,数据就会被安全存储。之后的软件或硬件崩溃不会导致任何信息丢失。
其他 ZODB 功能
除了符合 ACID 标准外,ZODB 还提供了其他一些功能,使其成为工业强度数据管理的真正好选择。
-
内存缓存:
每次从数据库读取对象时,它都会保存在内存中的缓存中,这样后续访问该对象时就会消耗更少的资源和时间。ZODB 透明地管理缓存,并自动删除长时间未访问的对象。缓存的大小可以配置,以便具有更多内存的机器可以更好地利用它。
-
撤销:
ZODB 提供了一个非常简单的机制来回滚任何已提交的事务,这是因为它跟踪每个事务之前和之后的数据库状态。这使得即使在之后提交了更多事务的情况下,也可以撤销事务中的更改。当然,如果涉及此事务且需要撤销的对象在后续事务中已更改,则由于一致性要求,将无法撤销。
-
历史:
由于每个事务都保存在数据库中,因此可以查看对象在之前事务中的状态,并将其与当前状态进行比较。这允许开发者快速实现简单的版本控制功能。
-
保存点:
由于在单个事务中进行的更改在事务提交之前都保存在内存中,因此在同时修改大量对象(例如,一个修改 100,000 个对象属性的自循环)的事务中,内存使用量可能会急剧增加。保存点允许我们在事务完成之前提交事务的一部分,这样更改就会被写入数据库,并且释放更改所占用的内存。这些保存点中的更改实际上只有在整个事务完成时才会真正提交,因此如果事务被中止,任何保存点也将回滚。
-
二进制大对象(Blob):
二进制大对象,如图像或办公文档,不需要 ZODB 提供的所有版本控制功能。实际上,如果它们被当作常规对象属性处理,blob 会大大增加数据库的大小,并通常减慢速度。这就是为什么 ZODB 使用特殊的 blob 存储器,这使得处理高达几百兆字节的大文件而不会出现性能问题成为可能。
-
打包:
正如我们所见,ZODB 保留了存储在其中的所有对象的版本。这意味着数据库随着每个对象的修改而增长,可以达到非常大的大小,这可能会减慢速度。ZODB 允许我们通过称为打包数据库的程序来删除存储对象的旧版本。打包例程足够灵活,可以只删除超过指定天数的老对象,保留较新的版本。
-
可插拔存储:
默认情况下,ZODB 将数据库存储在一个单独的文件中。管理此文件的程序称为文件存储。然而,ZODB 是以这种方式构建的,可以无需修改即可插入其他存储。这个特性可以用来在其他媒体或格式中存储 ZODB 数据,我们将在后面更详细地看到。
ZEO
ZEO 代表 Zope Enterprise Objects,是 ZODB 的网络存储。通过使用 ZEO,任何数量的 Grok 实例都可以连接到同一个 ZODB。ZEO 可以用来提供可伸缩性,因为负载可以在多个 ZEO 客户端之间分配,而不仅仅是单个客户端。
我们将在第十四章中了解更多关于 ZEO 的内容。
使用 ZODB
要存储对象,ZODB 使用一个根对象,这基本上是其他对象的容器。包含的对象反过来也可以作为容器,这意味着 ZODB 的结构可以用树来表示。
持久化规则
并非所有对象修改都可以自动检测并透明地写入数据库。正如我们在本书前面所看到的,有一些规则关于如何使对象持久化以及需要额外工作的条件:
-
从
persistent.Persistent(ZODB 代码中定义的一个类)或其子类继承。 -
类实例必须以层次结构相互关联。ZODB 有一个根对象,该对象包含其他对象,这些对象又可以包含进一步的对象,从而形成一个树。
-
当修改非持久化自身的持久化对象的可变属性时,持久化机制必须通过将实例的特殊
_p_changed属性设置为True来通知。
遍历
要访问数据库中的特定对象,ZODB 总是从根对象开始,递归地进入任何容器,直到找到该对象。这被称为 遍历,因为访问的对象路径上的每个包含对象都必须被触及才能到达它。
这意味着可以通过其在数据库中的路径来识别唯一的对象。按照设计,URL 路径可以很好地映射到 ZODB 结构,因此当我们有 Grok 中的 URL 时,其路径的每个元素通常代表一个必须遍历以到达特定对象的对象。例外情况有时是路径的最后一个元素,它也可以代表一个视图。
Grok 如何使用 ZODB
到目前为止,我们还没有直接与 ZODB 打交道,因为 Grok 已经设置好了一切,以便利用 ZODB 的透明性。Grok 的模型和容器已经继承自persistence.Persistent,因此对我们模型对象的任何修改都会自动保存到数据库中。
此外,Grok 的 URL 解析机制使用遍历来获取持久化对象,因此我们不需要跟踪我们放置了什么,以便 Grok 能为我们提供正确的对象。
然而,Grok 无法帮助我们避免 ZODB 对可变对象更改进行持久化机制信号的要求。记住,每当对可变属性进行更改时,应用程序需要通知 ZODB 已发生更改。这是通过将实例的特殊_p_changed属性设置为True来完成的:
self.context.p_changed_ = True
如果您使用常规 Python 可变对象,如列表和字典,您必须记住这个规则,这不是很多要求,但仍可能容易忘记。正是出于这个原因,ZODB 包包括这些内置 Python 类型的几个替代品:
From persistent.list import PersistentList
From persistent.dict import PersistentDict
这两种类型与内置 Python 类型完全等价,除了它们在需要时负责设置p_changed。如果您在应用程序中需要列表或字典,请考虑使用它们。
向我们的应用程序添加 Blob 支持
许多应用程序需要允许上传和存储外部文件,无论是办公文档、图像、多媒体还是其他类型的文件。通过使用 ZODB 处理这些文件可能会在资源和带宽方面造成高昂的成本,因此现在展示如何利用 ZODB Blob 支持会更好。
向项目中添加消息
目前,我们应用程序中的项目只有任务。当我们的应用程序是单用户应用时,这已经足够了,但现在多个用户可以登录并具有不同的角色,一个用于与其他用户沟通任务状态的机制将很有用。
让我们在项目视图中添加一个消息标签,任何人都可以发布消息。消息将包含标题、消息文本,并且可选地允许用户上传文件。
megrok.form 包
我们可以添加一些代码来使 Grok 表单字段能够使用 blob,但有时,找到已经做了我们需要的包并集成到我们的项目中要容易得多。
由于 Grok 使用 Zope 工具包,有数百个包可供选择。还有许多专门为 Grok 创建的包。只需访问 Python 包索引(PyPI)页面,你就会看到有很多包可供下载。
在这种情况下,有一个包正好符合我们的需求,那就是使 blob 的使用变得简单。这个包叫做megrok.form,我们将将其集成到我们的项目中。它可以在pypi.python.org/pypi/megrok.form找到。
将外部包添加到我们的项目中
使用 Grok 集成 PyPI 上的包相当简单。第一步是将它添加到项目安装需求中,这些需求在项目根目录下的setup.py文件中指定。找到这个文件,并将install_requires赋值更改为如下所示:
install_requires=['setuptools',
'grok',
'grokui.admin',
'z3c.testsetup',
'megrok.form',
# Add extra requirements here
],
接下来,从命令行运行bin/buildout。megrok.form包及其依赖项将被下载并安装到项目中。在这种情况下,我们添加了megrok.form,但当然,我们也可以从 PyPI 中选择任何其他包并以相同的方式将其添加到我们的项目中。
在 Grok 中如何配置 blob 支持
默认情况下,Grok 预先配置为使用 blob 存储,因此利用它很简单。如果你查看我们项目parts/etc/目录中的zope.conf文件,你可以看到如何完成此配置。相关部分看起来像这样:
<blobstorage>
<filestorage>
path /<your directory>/todo/parts/data/Data.fs
</filestorage>
blob-dir /<your directory>/todo/parts/data/blobs
</blobstorage>
消息类和模式
现在我们已经拥有了所需的支撑包和配置,让我们开始设置消息标签。我们将快速浏览代码,因为大多数概念已经在我们的应用中之前使用过了。
首先,我们添加一个Message类来存储消息及其附件。这个类的上下文将是Project类,因为我们希望消息按项目存储。当然,我们首先定义类的接口:
class IMessage(interface.Interface):
subject = schema.TextLine(title=u'Subject')
message = schema.Text(title=u'Message text')
attachment = BlobFile(title=u'Attachment',required=False)
我们保持简单,只添加subject, message和attachment字段。请注意,我们将存储附件的字段声明为BlobFile。为了使此声明生效,我们当然需要在文件顶部包含以下导入:
from megrok.form.fields import BlobFile
现在,找到实际的类定义,我们只是实现接口,并添加一个searchableText方法,以便消息出现在搜索结果中:
class Message(grok.Model):
grok.implements(IMessage, IMetadata, ISearchable)
subject = u''
message = u''
content_type = 'message'
def searchableText(self):
return self.subject+self.message
我们需要一个用于创建消息和上传附件的表单,所以这是我们下一步要做的事情。这将创建一个add_message表单,使用它需要todo.changeitems权限。字段来自之前定义的接口,我们使用第五章中创建的定制编辑表单模板。
class AddMessageForm(grok.AddForm):
grok.context(Project)
grok.name('add_message')
grok.require('todo.changeitems')
form_fields = grok.AutoFields(IMessage)
label = "Add a new message"
template = grok.PageTemplateFile('custom_edit_form.pt')
表单的关键部分是添加操作,在这里创建消息并使用表单中的值设置其属性。请注意,消息和列表都使用相同的 next_id 计数器作为它们的名称,因此 content_type 类属性变得非常重要。这将用于根据视图获取仅消息或仅列表:
@grok.action('Add message')
def add(self,**data):
message = Message()
message.creator = self.request.principal.title
message.creation_date = datetime.datetime.now()
message.modification_date = datetime.datetime.now()
message.project = self.context
message.project_name = self.context.__name__
self.applyData(message,**data)
id = str(self.context.next_id)
self.context.next_id = self.context.next_id+1
self.context[id] = message
return self.redirect(self.url('messages'))
def setUpWidgets(self, ignore_request=False):
super(AddMessageForm,self).setUpWidgets(ignore_request)
self.widgets['subject'].displayWidth = 50
self.widgets['message'].height = 12
您可以在以下屏幕截图中看到这个表单的实际效果:

消息视图小部件
接下来,我们创建用于显示消息的视图和视图小部件。视图通过使用内容类型执行目录搜索。当然,我们首先需要将 ProjectIndexes 类(来自第六章)添加到我们的类中,以便它被索引,因为现在项目内部可以存储两种不同类型的对象(消息和列表)。我们还必须将 todo 列表的目录搜索更改为使用这个新索引。以下是视图代码:
class ProjectMessages(grok.View):
grok.context(Project)
grok.name('messages')
grok.require('todo.view')
grok.template('master')
def update(self):
catalog = getUtility(ICatalog)
self.messages = catalog.searchResults (content_type=('message','message'), project_name=(self.context.__name__,self.context.__name__))
对于视图小部件,我们只需定义一个模板并将视图设置为刚刚创建的那个:
class ProjectMessagesViewlet(grok.Viewlet):
grok.viewletmanager(Main)
grok.context(Project)
grok.template('projectmessages_viewlet')
grok.view(ProjectMessages)
消息列表模板
项目消息视图小部件使用一个名为 projectmessages_viewlet 的新模板。我们将在顶部显示几个链接,它们将大致像标签一样工作,用于在列表视图和消息视图之间切换:
<div id="project">
<h1 id="apptitle" onclick="editTitle();" tal:content="context/title">To-Do list manager</h1>
<ul id="project-tabs">
<li><a tal:attributes="href python:view.url('index')" title="Project lists">Lists</a></li>
<li><a tal:attributes="href python:view.url('messages')" title="Project messages">Messages</a></li>
</ul>
<h2>Here you can add new messages relating to this project.</h2>
<p class="create"><a href="add_message">Create a new message</a></p>
之后,我们通过使用“重复”结构来显示消息及其内容。对于本章的目的,模板的关键部分是下载附件的链接,它使用 view.url 方法指向下载视图。请注意,一条消息可能不包含附件,这就是为什么带有链接的段落有一个条件来决定是否显示它:
<tal:block repeat="message view/messages">
<div class="message">
<h3><span tal:replace="message/subject">subject</span>
<a tal:define="url python:view.url('deletemessage')" tal:attributes="href string:${url}?message=${message/__name__}"> <img border="0" tal:attributes="src static/bin_closed.png" /></a>
</h3>
<p class="message_info" tal:content="string:By ${message/creator}, on ${message/creation_date}"> info </p>
<p tal:content="message/message">text</p>
<p tal:condition="message/attachment"> <a tal:attributes="href python:view.url(message,'download')"> Download attachment </a> ( <span tal:replace="message/attachment/filename">filename </span>, <span tal:replace="message/attachment/size">size</span> bytes ) </p>
</div>
</tal:block>
</div>
此外,还向项目中添加了一些 CSS 样式。这里没有显示。请检查这本书的源代码以查看它们是什么。
下载附件
最后一步是添加一个用于下载附件的视图。这里我们不需要显示模板;我们必须返回文件。这是如何操作的:
class Download(grok.View):
grok.context(Message)
grok.require('todo.changeitems')
def update(self):
self.data = self.context.attachment.data
self.filename = self.context.attachment.filename
def render(self):
self.response.setHeader('Content-Disposition', 'attachment; filename=%s;' % self.filename)
return self.data
首先,我们在 update 方法中获取附件的文件名和文件数据。然后,在 render 方法中,我们将 Content-Disposition 响应头设置为 attachment 并将其传递给文件名,这样浏览器就会知道通过使用其原始名称直接下载哪个文件。最后,我们返回文件数据。
测试 Blob 支持
我们现在可以运行应用程序了。以下屏幕截图显示了它应该看起来像什么。尝试添加一些消息和文件,然后查看您在 zope.conf 文件中指定的 blob 目录的内容。您应该看到一个包含每个上传文件目录的 blobs 目录。

利用事务的优势
我们从本章开始就说过 ZODB 是事务性的,但到目前为止,我们还没有看到我们从中获得的好处。可能最重要的好处是能够回滚或撤销事务。
使用 zope.app.undo
由于 Grok 为我们处理事务提交,我们还没有机会展示它,但现在我们将看到如何撤销和重做事务。展示这一点最简单的方法是使用 PyPI 上的 zope.app.undo 包。我们将以与本章早期插入 megrok.form 相同的方式将其添加到项目中。编辑项目根目录下的 setup.py 文件,并添加以下代码:
install_requires=['setuptools',
'grok',
'grokui.admin',
'z3c.testsetup',
'megrok.form',
'zope.app.undo',
# Add extra requirements here
],
现在重新运行 bin/buildout,让 Grok 下载并安装该包。无需其他操作即可启用它,只需重新启动应用程序。
测试撤销功能
创建一个 todo 应用程序实例,并添加一个项目。然后在项目中添加一个列表,标题为 test undo。现在删除您刚刚创建的列表。您将再次看到一个空的项目。
要撤销事务,我们必须使用之前在第六章学习目录时使用的 Grok 管理界面。将您的浏览器指向以下 URL:localhost:8080/todo/@@undoMore.html。
您应该会看到一个事务列表,类似于下一张截图所示。屏幕显示了访问 todo 应用程序实例的最后十个事务。顶部的事务是最后提交的事务。要撤销它,只需选中左侧的复选框,然后点击屏幕底部的撤销按钮。
现在回到您的项目。您删除的列表神奇地又回到了那里。如果您回到列表,您会注意到撤销操作现在被列为一个事务,因此您可以通过撤销现在顶部的事务来“重做”旧事务。
“撤销”是一个强大的功能,它可以使您在应用程序用户眼中看起来像英雄。同时撤销多个事务也是可能的。然而,只有当事务操作的对象没有被后续事务修改时,才能撤销事务。这意味着在新的事务使事情复杂化之前,必须快速撤销错误。

ZODB 维护
与任何其他数据库系统一样,ZODB 需要定期进行一些维护。可能发生的主要情况是数据库大小会增长,占用大量磁盘空间,导致某些任务变慢。
文件存储
如我们之前提到的,ZODB 跟踪存储对象的全部版本,这使得每次对象更改时 ZODB 都会增长。打包允许 ZODB 丢弃旧版本的对象,从而减小数据库大小。
数据库通常包含在一个名为Data.fs的文件中,该文件位于我们的project目录下的parts/data文件夹中。
打包是一个可能需要一些时间的操作,因此它在单独的线程上运行。在打包开始之前,会创建数据库文件的备份副本,以防万一出现问题,因此请注意,您至少需要与 ZODB 当前大小一样多的空闲磁盘空间,才能打包它。
在 Grok 中,打包数据库最简单的方法是进入管理界面并点击服务器控制链接。这将带您到一个控制面板,其中显示了打包当前数据库的选项(参见下一张截图)。要打包数据库,只需选择超过多少天应该删除对象修订的数字。打包过程将开始,完成后,控制面板顶部将有一条消息通知您。

自动打包
在大多数情况下,频繁地进行打包将是一件好事,除非出于某种原因你绝对需要跟踪每个对象的每个修订版本。例如,每周打包一次数据库可能是一个保持数据库大小可控并使备份等任务更容易、更快的好方法。
当然,每次手动打包数据库可能会给管理员带来负担,并且很容易被遗忘,因此自动执行打包操作会很有用。
打包是一个昂贵的操作,并且从 Grok 外部执行需要单独的数据库连接,这就是为什么即使我们的可扩展性需求不需要它,使用 ZEO 也是一个好主意。因为 ZEO 允许多个连接,所以可以从另一个 ZEO 客户端进行打包,而无需停止常规服务。
这个任务如此必要,以至于 Grok 安装已经提供了一个名为zeopack的脚本,用于通过 ZEO 打包 ZODB。它位于 Grok 主安装的bin目录下。要使用它,只需确保 ZEO 服务器正在运行,然后使用站点的主机和端口调用脚本:
$ bin/zeopack -h localhost -p 8080
可以将此脚本调用添加到 UNIX cron 脚本中,以每周或所需频率执行任务。
备份
与所有重要的数据处理服务一样,在使用 ZODB 时,备份被强烈推荐。具体多久进行一次备份可以根据应用程序的类型而有所不同,但应该定期进行。
Grok 安装包括一个名为repozo的脚本,用于简化备份。此脚本允许进行增量或完整备份,并且可以用于备份和恢复Data.fs文件。为了备份我们的数据库,我们可以在 Grok 主目录内创建一个名为backups的目录,然后使用:
$ bin/repozo -B -r backups -f todo/parts/data/Data.fs
-B 选项表示执行备份操作。-f 选项给出了我们想要备份的 Data.fs 文件的路径。第一次,repozo 将在 backups 目录(由 -r 选项指定)中进行完整备份。进一步的调用将导致增量备份,除非在最后一次备份后数据库已被打包。
要从 repozo 备份中恢复 Data.fs 文件,我们使用 -R 选项:
$ bin/repozo -R -r backups -o RecoveredData.fs
此命令将恢复最新的备份并将恢复的文件输出到 RecoveredData.fs(由 -o 选项指定)。也可以通过使用 -D 选项并指定 "yyyy-mm-dd" 格式的日期来恢复指定日期的备份。
在没有 Grok 的情况下使用 ZODB
ZODB 是一个非常强大的包,我们没有理由不在 Grok 之外的正常用作应用程序中使用它。事实上,许多通常使用关系型数据库(主要是因为大多数开发者都习惯了)开发的应用程序,如果使用 ZODB,甚至可以更简单。
为了展示从 Python 使用 ZODB 的简便性,我们将展示一个简单的示例。要跟随示例,只需安装 Python 并使用 easy_install 获取 ZODB3 egg,或者从 pypi.python.org/pypi/ZODB3 下载 ZODB 包。
创建一个名为 zodb_demo.py 的文件。首先,让我们创建一个小的类来处理打开和关闭到 FileStorage 的连接。在您的 zodb_demo.py 文件开始处添加以下代码:
from ZODB import FileStorage, DB
import transaction
class ZODBHandler(object):
def __init__(self, path):
self.storage = FileStorage.FileStorage(path)
self.db = DB(self.storage)
self.connection = self.db.open()
self.dbroot = self.connection.root()
def close(self):
self.connection.close()
self.db.close()
self.storage.close()
首先,我们进行一些导入。FileStorage 用于定义数据库存储的文件,DB 是实际的 ZODB 库,而 transaction 用于将更改提交到数据库。
接下来,我们创建一个名为 ZODBHandler 的类,它将接受一个文件路径并为我们的 ZODB 初始化一个文件存储。如果传入路径的文件存在,它将被用作数据库;如果不存在,它将被创建。无论如何,我们不必担心这一点,因为 FileStorage 会为我们处理。有了这个,我们就有了一个可以传递给下一行的 DB 类的存储。之后,我们可以打开一个连接,一旦完成,我们就获取数据库的根对象并将其存储在 dbroot 中。从那里,我们可以像我们将要看到的那样与数据库一起工作。
我们的 ZODB 处理器做的另一件事是在我们完成使用后通过一个 close 方法来关闭连接和存储。
我们现在可以初始化一个数据库,并开始向其中写入数据:
if __name__ == _'_main__':
db = ZODBHandler('./Data.fs')
dbroot = db.dbroot
dbroot['pi'] = 3.14159265358979323
dbroot['planet'] = 'Earth'
dbroot['primes'] = [1, 2, 3, 5, 7, 11]
dbroot['pycon'] = { 2009: 'Chicago', 2010: 'Atlanta' }
transaction.commit()
我们将一个路径传递给我们的处理器,该路径将在当前目录中创建一个名为 Data.fs 的文件。接下来,我们获取存储在那里的数据库的根对象。然后我们添加几个对象,只是为了展示任何可 pick 的 Python 对象都可以存储在数据库中。最后,我们需要提交事务,以便实际保存更改。
要从数据库中获取一个对象,我们只需通过其键来引用它,就像字典的工作方式一样:
print dbroot['primes']
删除一个对象也非常简单:
del dbroot['planet']
transaction.commit()
当然,大多数应用程序不会使用内置的 Python 对象,而是会创建自己的类,这些类从 persistent.Persistent 继承。在上述 if 语句之前插入以下类定义:
from persistent import Persistent
class Project(Persistent):
def __init__(self, title, kind, description):
self.title = title
self.kind = kind
self.description = description
我们现在可以透明地将项目存储到我们的数据库中。在程序末尾追加以下行:
dbroot['project 1'] = Project('test project', 'business', 'A simple test project')
dbroot['project 2'] = Project('another project', 'personal', 'a personal project')
transaction.commit()
print dbroot['project 1'].title
dbroot['project 1'].title = 'new title'
transaction.commit()
这是一个非常简单的例子,但希望你能看到创建有趣的 ZODB 支持的应用程序的潜力。不需要 SQL,只需普通的 Python 对象。
摘要
在本章中,我们更多地了解了 ZODB,并学习了如何利用其功能,例如 blob 处理。我们还了解了一些关于 ZODB 维护和需要频繁打包数据库的内容。最后,我们尝试在 Grok 之外使用 ZODB,作为一个常规的 Python 库。
第十章. Grok 和关系数据库
到目前为止,我们一直在使用 ZODB 进行数据存储。正如我们在上一章中看到的,这是一个很好的解决方案。Grok 开发者真正喜欢 ZODB,并希望它在 Python 世界中得到更广泛的应用。此外,Grok 充分利用了它的力量和特性。
话虽如此,关系数据库目前是网络应用程序中最常用的持久化机制。一方面,它们是可靠的、高效的和可扩展的。它们也相当为人所熟知,许多新手网络开发者已经对 SQL 有了一些了解。
关系数据库在非网络开发项目中的一致使用也使得它们更有可能在需要访问现有信息的网络应用程序中被需要。
换句话说,ZODB 是很棒的,但一个好的网络框架需要提供足够的支持来处理关系数据库。当然,Grok 就是这样一种框架,所以在本章中,我们将了解 Grok 为关系数据库访问提供了哪些设施。以下是我们将涵盖的一些具体内容:
-
为什么 Grok 允许开发者轻松使用关系数据库很重要
-
对象关系映射器(ORM)是什么
-
如何使用 SQLAlchemy 与 Grok 结合
-
如何将我们的认证机制更改为使用关系数据库而不是 ZODB
对象关系映射器
Python 是一种面向对象的编程语言,Grok 严重依赖这种面向对象。在实践中,这意味着我们使用属性和方法定义模型,每个模型实例代表一个对象。因此,我们有项目或待办事项列表对象,我们的视图与它们一起工作,自由地访问它们的属性和调用它们的方法。
当我们保存对象的时候,ZODB 就派上用场了,因为我们只需抓取整个对象并将其放入其中,这就是为什么它被称为对象数据库。另一方面,关系数据库以非常不同的方式工作。它们使用表格和列来存储所有内容,通常将一个对象分成几个相关的表格。
显然,我们不能直接将我们的待办事项列表对象放入关系数据库中;即使只涉及一个表格,也需要进行一些转换。关系数据库使用 SQL 语言来接收对表格或表格的读取和写入命令,因此我们可以逐个生成对象的属性,将它们作为字符串生成一个 SQL 语句,并将其发送到数据库。然后,我们会逆向这个过程,从数据库列中重新组装对象。这实际上并不容易扩展,因此通常的解决方案是使用专门设计用于将对象拆分成表格并在查询时重新组装它们的库,透明地生成所需的 SQL 以使其工作。这些库被称为对象关系映射器,或简称ORM。
ORMs(对象关系映射)在保持代码与所使用的数据库独立方面也非常出色,因为开发者以对象的形式执行操作,ORMs 在幕后生成针对特定数据库的 SQL 语句。这使得在项目中切换数据库时,无需进行耗时的 SQL 语法更改,从而变得容易得多。
SQLAlchemy
对于 Python 来说,有众多 ORM,但可能最受欢迎的一个是SQLAlchemy。其受欢迎的原因之一是,SQLAlchemy 除了是一个强大的 ORM 之外,还提供了一个数据抽象层,以平台无关的方式构建 SQL 表达式。这为开发者提供了足够的灵活性,可以在不担心数据库或 SQL 细节的情况下与模型对象一起工作,但在需要时仍然能够深入到 SQL 级别,无论是出于性能或其他原因。
SQLAlchemy 支持包括 SQLite、Postgres、MySQL、Oracle 和 MS-SQL 在内的十多种数据库。它将待处理的操作组织到队列中,并一次性批量刷新它们,提供效率和事务安全性。可以通过使用 Python 函数和表达式构建 SQL 子句,从而允许使用语言构造的完整范围。它还负责连接池管理,有助于优化系统资源的使用。
将 SQLAlchemy 包含到我们的 Grok 项目中
我们在上一章中已经看到了如何将来自 PyPI 的 Python 包包含到我们的项目中。只需将包添加到项目setup.py文件中的install_requires变量即可:
install_requires=['setuptools',
'grok',
'grokui.admin',
'z3c.testsetup',
'megrok.form',
'SQLAlchemy',
# Add extra requirements here
],
之后,重新运行 buildout,包应该被包含在内:
$ bin/buildout
完成这些操作后,SQLAlchemy 就准备好使用了。
使用 SQLAlchemy
为了感受 SQLAlchemy 独立运行的方式,让我们首先直接从 Python 提示符尝试。前往项目的顶层目录,并输入以下命令:
$ bin/python-console
这将在命令行中启动 Python 解释器。由于我们已经运行了带有 SQLAlchemy 包的 buildout,我们应该能够从中导入:
>>> from sqlalchemy import create_engine
>>> engine = create_engine('sqlite:///:memory:',echo=True)
create_engine方法用于告诉sqlalchemy与哪个数据库进行交互。第一个参数被称为连接字符串,包含连接到数据库所需的所有信息,如数据库名称、用户名和密码。在这个例子中,我们使用 SQLite,它是从 Python 2.5 版本开始包含的一个轻量级数据库。SQLite 允许我们在内存中工作,而不是在磁盘上创建数据库。由于我们只是在测试,我们可以使用这个特性。
将echo参数传递值为True,这样我们就可以在控制台输出中看到 SQLAlchemy 生成的 SQL。
现在,我们将进行更多的导入操作:
>>> from sqlalchemy import Column, Integer, String
>>> from sqlalchemy.ext.declarative import declarative_base
>>> Base = declarative_base()
Column类用于定义表列。Integer和String是列数据类型。我们将使用它们来定义我们的表。
接下来,我们导入declarative_base,这允许我们创建一个用于与我们的对象模型一起使用的基类。要使用它,我们必须调用它并将结果赋值给将作为我们模型基类的变量。
我们现在准备好创建一个模型:
>>> class User(Base):
... __tablename__ = 'users'
... id = Column(Integer, primary_key=True)
... name = Column(String)
... realname = Column(String)
... role = Column(String)
... password = Column(String)
... def __init__(self, name, real_name, role, password):
... self.name = name
... self.real_name = real_name
... self.role = role
... self.password = password
在这个例子中,我们创建了一个User类来存储用户数据。我们必须使用我们刚刚用declarative_base创建的Base类,以便 SQLAlchemy 能够透明地与这个模型一起工作。还需要一个__tablename__属性来指定数据库中存储模型信息的表名。
接下来,使用Column类和之前导入的类型定义列。注意使用primary_key参数将id设置为该表的键。
最后,我们需要定义一个__init__方法来在创建时设置列值。完成此操作后,SQLAlchemy 可以创建表:
>>> metadata = Base.metadata
>>> metadata.create_all(engine)
2009-06-30 03:25:36,368 INFO sqlalchemy.engine.base.Engine.0x...5ecL PRAGMA table_info("users")
2009-06-30 03:25:36,368 INFO sqlalchemy.engine.base.Engine.0x...5ecL ()
2009-06-30 03:25:36,381 INFO sqlalchemy.engine.base.Engine.0x...5ecL
CREATE TABLE users (
id INTEGER NOT NULL,
name VARCHAR,
realname VARCHAR,
role VARCHAR,
password VARCHAR,
PRIMARY KEY (id)
)
表元数据存储在Base类的metadata属性中,并且可以通过create_all方法用来创建表。此方法需要传递给我们之前创建的引擎,以便知道使用哪种 SQL 方言以及如何连接到实际的数据库。注意生成的 SQL 是如何在调用 SQLAlchemy 方法后立即显示的。你可以看到表名是我们之前使用__tablename__属性定义的那个。
一旦表被创建,我们就可以用我们的User对象来填充它。为了这样做,我们首先需要创建一个会话:
>>> from sqlalchemy.orm import sessionmaker
>>> Session = sessionmaker(bind=engine)
>>> session = Session()
sessionmaker方法的工作方式与declarative_base类似,它产生一个我们可以调用的类来创建实际的会话。以这种方式创建会话是为了让 SQLAlchemy 能够为引擎定义创建一个特定的Session类,这个类通过bind参数传递给sessionmaker。一旦我们有一个针对我们引擎定制的sessionmaker,我们就可以创建数据库会话,并且我们准备好创建第一个对象实例:
>>> grok_user = User('grok','Grok the Caveman','todo.ProjectMember','secret')
>>> session.add(grok_user)
我们创建了一个User类的实例,代表用户'grok'。为了将其放入会话队列中,我们使用了add方法。此时,User对象已经准备好写入数据库:
>>> session.commit()
2009-06-30 03:30:28,873 INFO sqlalchemy.engine.base.Engine.0x...5ecL BEGIN
2009-06-30 03:30:28,874 INFO sqlalchemy.engine.base.Engine.0x...5ecL INSERT INTO users (name, realname, role, password) VALUES (?, ?, ?, ?)
2009-06-30 03:30:28,874 INFO sqlalchemy.engine.base.Engine.0x...5ecL ['grok', None, 'todo.ProjectMember', 'secret']
2009-06-30 03:30:28,875 INFO sqlalchemy.engine.base.Engine.0x...5ecL COMMIT
我们使用commit方法将更改保存到数据库。再次,我们可以在控制台输出中查看生成的 SQL。现在我们可以使用 Python 构造函数查询数据库,并按需使用数据:
>>> for user in session.query(User):
... print user.name, user.real_name
...
2009-06-30 03:32:18,286 INFO sqlalchemy.engine.base.Engine.0x...5ecL BEGIN
2009-06-30 03:32:18,287 INFO sqlalchemy.engine.base.Engine.0x...5ecL SELECT users.id AS users_id, users.name AS users_name, users.realname AS users_realname, users.role AS users_role, users.password AS users_password
FROM users
2009-06-30 03:32:18,288 INFO sqlalchemy.engine.base.Engine.0x...5ecL []
grok Grok the Caveman
在前面的例子中,我们使用会话的query方法来获取所有存储的User对象,目前正好有一个。然后我们打印出结果的name和real_name属性值。
一旦我们完成,我们应该关闭会话:
>>> session.close()
当然,SQLAlchemy 可以做更多的事情,但深入的解释超出了本书的目的。在项目网站上提供了丰富的文档,包括教程。
使用关系型数据库进行身份验证
我们已经看到了如何单独使用 SQLAlchemy,因此现在我们可以在我们的项目中尝试使用它。对于网络应用程序来说,关系数据库连接通常在认证过程中很有用,因为大多数情况下,在实际的企业环境中,网络应用程序并不是孤立的,而是构成公司日常工作中可以使用的一系列工具的一部分。通常有一个单独的数据库来存储所有公司用户,而不是为每个应用程序都有一个单独的数据库。
我们将展示如何使用 SQLAlchemy 将我们的认证数据库从 ZODB 文件夹转换为关系数据库表。首先,将以下行添加到 auth.py 的顶部,紧接在导入之后:
from sqlalchemy import create_engine
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
engine = create_engine('sqlite:///todo.db',echo=True)
Base = declarative_base()
Session = sessionmaker(bind=engine)
我们在上一节中使用了这些导入和引擎设置语句。唯一的区别是,我们不再使用内存数据库,而将使用一个文件来保存我们的数据。SQLite 允许我们在连接字符串中传递一个相对路径来创建一个数据库文件。在这种情况下,文件将命名为 todo.db 并存储在项目根目录中。
由于我们现在将用户数据存储在数据库中,我们不再需要用户文件夹,因此我们从代码中删除其定义。移除以下两行:
Class UserFolder(grok.Container):
pass
在完成这些之后,下一步是修改 Account 类的定义:
class Account(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
real_name = Column(String)
role = Column(String)
password = Column(String)
def __init__(self, name, password, real_name, role):
self.name = name
self.real_name = real_name
self.role = role
self.setPassword(password)
def setPassword(self, password):
passwordmanager = component.getUtility(IPasswordManager, 'SHA1')
self.password = passwordmanager.encodePassword(password)
def checkPassword(self, password):
passwordmanager = component.getUtility(IPasswordManager, 'SHA1')
relational databaserelational databaseAccount class definition, modifyingreturn passwordmanager.checkPassword(self.password, password)
我们不再使用 grok.Model 作为基类,而是切换到 declarative_base 中的 Base 类。然后,我们定义列,使用与在 __init__ 方法中声明的现有属性相同的名称。请注意,类中的所有其他方法都保持与基于 grok.Model 的 Account 类相同。
现在我们有了基于 SQLAlchemy 的 Account 类,我们可以在代码中使用我们可能需要的任何 SQLAlchemy 功能。在这种情况下,我们必须更改 UserAuthenticatorPlugin 的实现以访问数据库进行用户列表、用户创建和用户删除。
这是新的实现。请注意,authenticateCredentials 和 principalInfo 方法与之前的定义没有变化,因此不包括在内:
class UserAuthenticatorPlugin(grok.LocalUtility):
grok.implements(IAuthenticatorPlugin)
grok.name('users')
def __init__(self):
metadata = Base.metadata
metadata.create_all(engine)
__init__ 方法使用预定义的引擎调用 metadata.create_all 方法来创建数据库表,当插件初始化时。
def getAccount(self, login):
session = Session()
result = session.query(Account).filter_by(name=login).first()
return result
要获取一个账户,我们首先启动一个会话,然后通过使用filter_by方法查询Account类,该方法只返回与传入的登录信息匹配的数据库行。SQLAlchemy 允许查询结果的链式调用,因此我们使用过滤结果上的第一个方法来获取第一个(也是唯一一个)匹配项,或者如果没有这样的用户,则返回None。
def addUser(self, username, password, real_name, role):
session = Session()
result = session.query(Account).filter_by(| name=username).first()
if result is None:
user = Account(username, password, real_name, role)
session.add(user)
session.commit()
role_manager = IPrincipalRoleManager(grok.getSite())
if role==u'Project Manager':
role_manager.assignRoleToPrincipal( 'todo.ProjectManager',username)
elif role==u'Application Manager':
role_manager.assignRoleToPrincipal( 'todo.AppManager',username)
else:
role_manager.assignRoleToPrincipal( 'todo.ProjectMember',username)
要添加用户,我们首先检查登录是否存在,使用与 getAccount 方法中的相同 filter_by 调用。如果结果是 None,我们创建用户账户并将其添加到会话中。我们立即提交以保存结果。
def deleteUser(self, username):
session = Session()
result = session.query(Account).filter_by( name=username).first()
if result is not None:
session.delete(result)
session.commit()
在第七章中我们讨论认证时,没有为管理员提供删除用户的能力。上面的deleteUser方法正是为了实现这一点。我们再次使用filter_by调用以检查是否存在传递的登录用户。如果存在,我们调用session.delete将其从数据库中删除并提交。
def listUsers(self):
session = Session()
results = session.query(Account).all()
return results
最后,在listUsers中,我们简单地调用all方法以返回表中的每一行。
这就是将认证从使用 ZODB 中的grok.Container改为使用关系数据库中的表所需的所有内容(参考下一张截图)。要使用不同的数据库,我们只需更改引擎定义为我们想要使用的数据库。但请注意,你必须在你系统中安装数据库所需的任何驱动程序,并将相应的 Python 驱动程序添加到项目的setup.py中。有关更多信息,请参阅 SQLAlchemy 文档。

安全处理数据库事务
因此,我们现在只需做很少的工作就可以实现关系数据库认证。然而,我们的代码中存在一个微妙的问题,尤其是在addUser方法中。正如我们所见,Grok 有一个事务机制,但关系数据库也有。
目前我们是在将用户添加到数据库后立即调用session.commit()。在那个时刻,用户数据被保存到磁盘。问题是,在保存这个更改后,我们立即使用 Grok 的基于 ZODB 的权限机制为用户设置适当的角色。
现在,如果在调用assignRoleToPrincipal时发生错误,Grok 事务将被中止,这意味着新用户将不会设置角色。同时,数据库事务已经提交,所以我们最终得到一个存在于数据库中但无法访问应用程序功能的用户,因为它没有设置任何角色。
这就是我们所说的数据库和用户的不一致状态,用户可能会称之为一个 bug。在这种情况下,我们可以在角色更改后移动session.commit()调用,这样至少可以保证数据库错误会导致 Grok 中的事务中止。但显然可能存在更复杂的情况,在这些情况下,仅仅放置session.commit()调用可能不足以解决问题。
需要的是一个同步 Grok 和 SQLAlchemy 事务的方法,这样我们就不必分别控制它们。幸运的是,通过 Zope Toolkit 提供的 Grok 的庞大包集合中,有适合这项工作的正确工具。
我们需要的包叫做zope.sqlalchemy,可以在 PyPI 上找到,这意味着我们可以通过setup.py轻松将其添加到我们的项目中。你现在应该知道了这个流程,将其添加到install_requires中,并重新运行 buildout:
install_requires=['setuptools',
'grok',
'grokui.admin',
'z3c.testsetup',
'megrok.form',
'SQLAlchemy',
'zope.sqlalchemy',
# Add extra requirements here
],
zope.sqlalchemy 包只有一个目的:为集成 Grok 和 SQLAlchemy 事务提供一个事务管理器。要使用它,我们首先必须在 auth.py 的顶部添加几个导入:
from sqlalchemy.orm import scoped_session
from zope.sqlalchemy import ZopeTransactionExtension
scoped_session 是一个特殊的 SQLAlchemy 会话管理器,它确保在同一个事务中,对 Session() 的任何调用都将返回相同的会话对象。ZopeTransactionExtension 将创建负责绑定 Grok 和 SQLAlchemy 会话的对象。
模块顶部的引擎和 Base 声明保持不变,但我们必须删除定义 Session 类的那一行,并使用以下代码行来替换它:
Session = scoped_session(sessionmaker(bind=engine, extension=ZopeTransactionExtension()))
这将初始化一个作用域会话,并通过使用 ZopeTransactionExtension 集成两个事务。现在所需做的就是将所有的 session.commit() 调用替换为 transaction.commit() 调用,我们的应用程序将享受安全的交易处理。以下是对结果 addUser 方法的查看:
def addUser(self, username, password, real_name, role):
session = Session()
result = session.query(Account).filter_by( name=username).first()
if result is None:
user = Account(username, password, real_name, role)
session.add(user)
role_manager = IPrincipalRoleManager(grok.getSite())
if role==u'Project Manager':
role_manager.assignRoleToPrincipal( 'todo.ProjectManager',username)
elif role==u'Application Manager':
role_manager.assignRoleToPrincipal( 'todo.AppManager',username)
else:
role_manager.assignRoleToPrincipal( 'todo.ProjectMember',username)
如你所见,现在提交 db 事务不再必要,因为我们现在使用的是 Grok 事务管理器,而不是 SQLAlchemy 会话对象。当 Grok 为我们提交事务时,数据库事务将自动提交,并且 Grok 机制将确保两个事务要么都成功,要么都失败。更少的工作,没有数据不一致。
创建数据库后端模型和容器
我们已经展示了如何轻松访问和使用关系型数据库中的数据。程序中只需要修改很少的代码行,并且没有使用任何 SQL 代码。当然,认证是主应用程序的一个相对外围的功能。如果我们需要将应用程序生成的所有数据存储在关系型数据库中怎么办?
Grok 的 Model 和 Container 类将信息存储在 ZODB 中,所以如果我们想使用关系型数据库,我们需要创建自己的模型和容器实现,对吧?这似乎是很多工作。幸运的是,有人已经为我们做了这些工作。
megrok.rdb 包
结果表明,megrok.rdb 包为 Grok 开发者提供了与 Grok 自身类相似工作的 Container 和 Model 类,但将所有信息存储在关系型数据库中。更重要的是,megrok.rdb 还使用了 SQLAlchemy,因此它与我们在本章中迄今为止所做的工作相得益彰。
如你所猜,这个包可以从 PyPI 获取。希望到这个时候你已经知道,应该将 megrok.rdb 添加到 setup.py 的 install_requires 部分,并且需要再次运行 buildout。这样,megrok.rdb 包就准备好使用了。
创建一个数据库后端应用
将整个应用程序转换为使用 megrok.rdb 需要的工作量比本介绍章节中现实可行的工作量要多,所以让我们假设,如果我们从一开始就计划使用关系数据库进行存储,我们会如何设计这个应用程序。
由于这只是一个练习,我们将只展示使 Project 类及其包含的待办事项列表工作的代码,首先,我们需要从 sqlalchemy 中导入一些内容,类似于我们之前使用的。当然,我们必须从 megrok.rdb 中导入 rdb 模块:
from sqlalchemy import Column, ForeignKey
from sqlalchemy.types import Integer, String
from sqlalchemy.orm import relation
from megrok.rdb import rdb
目前,我们将省略数据库连接设置。假设我们已经在本章的早期创建了数据库。然而,我们需要一个特殊的 metadata 对象,而不是 SQLAlchemy 提供的那个。我们将从刚刚导入的 rdb 模块中获取它:
metadata = rdb.MetaData()
我们现在可以创建模型了。记住,这段代码只是为了说明目的,不要将其添加到我们的项目中。
class TodoLists(rdb.Container):
pass
class Project(rdb.Model):
id = Column(Integer, primary_key=True)
title = Column(String(50))
kind = Column(String(50))
description = Column(String(140))
todolists = relation('TodoList', backref='project', collection_class=TodoLists)
class TodoList(rdb.Model):
id = Column(Integer, primary_key=True)
title = Column(String(50))
description = Column(String(140))
project = Column('project_id', Integer, ForeignKey('project.id'))
我们首先定义待办事项列表容器。它只是一个空类。然后,我们使用常规 SQLAlchemy 声明来定义 Project 模型和其列。我们在项目上有一个 todolists 容器属性,通过使用 SQLAlchemy 关系声明与之前定义的容器连接。这对于在我们的关系数据库应用程序中拥有 Grok 风格容器是必要的。
对于 TodoList 类,我们使用 rdb.Model 基类,并添加其列,但在这里,我们使用外键将待办事项列表与项目相关联。
完成这些后,我们可以生成数据库结构,这通常在应用程序创建时完成。我们也在前面的部分中做了这件事,所以让我们假设我们已经准备好了所有东西,并开始创建内容:
session = rdb.Session()
project_x = Project(title='Project X',kind='personal', description='My secret project')
session.add(project_x)
首先,我们创建一个会话。然后,我们创建一个新的 Project 并将其添加到会话中,就像常规 SQLAlchemy 使用一样。接下来,我们定义一些待办事项列表:
planning = TodoList(title='Planning',description='Define steps for master plan')
execution = TodoList(title='Execution',description='Perform plan flawlessly')
world_domination = TodoList(title='World Domination', description='Things to do after conquering the world')
这仍然是常规 SQLAlchemy 语法,但现在我们来到了使用我们在 Project 类内部定义的容器来存储待办事项列表的部分。这就是我们能够在我们的关系数据库应用程序中拥有 Grok 风格容器功能的原因:
project_x.todolists.set(planning)
project_x.todolists.set(execution)
project_x.todolists.set(world_domination)
set 方法负责将列表添加到容器中,并让数据库为每一行设置键。现在我们可以调用 Grok 容器方法,例如在 project_x.todolists 容器上使用 items(),以获得预期的结果,就像使用 grok.Container 一样。
使用 megrok.rdb 还可以完成其他事情,所以如果你对拥有类似 Grok 的数据库后端应用程序感兴趣,你应该查阅文档。
何时使用 ZODB 而不是关系数据库
由于使用 Grok 操作关系型数据库相对容易,那些习惯于使用关系型应用的人可能会 wonder 为什么他们应该使用 ZODB。尽管使用我们最熟悉的技术很舒适,但没有任何数据库技术是适合所有类型项目的。
关系型数据库是一个扁平实体。当使用像 Python 这样的面向对象语言时,通常需要将复杂的相关对象层次结构“扁平化”以适应几个表。有时简单地以它们本来的样子存储对象,就像面向对象数据库所做的那样,会更容易和更快。
哪个最适合您的应用?这里的关键词是扁平和层次结构。如果您的应用将处理扁平的信息结构,例如客户、产品、记录、订单等,或者如果您需要进行大量的报告和数据操作,关系型数据库很可能是您最好的选择。在线商店就是一个非常适合使用关系型数据库的应用示例。我们刚刚完成的应用的用户管理部分也非常适合这里。
如果您有复杂、层次化的对象结构,使用 ZODB 并以它们自然的形式持久化层次结构可能更好。内容管理应用,其中您需要模拟类似于文件夹和可以嵌套多级深度的页面结构,非常适合面向对象数据库。
最后,这通常只是个人偏好的问题,但无论如何,Grok 都会以简单的方式让您使用其中一种或两种机制,这为您提供了最大的灵活性。
摘要
在本章中,我们看到了:
-
Grok 如何使使用现有的 Python 关系型数据库包和 ORM 变得非常容易。我们还学习了如何使用更高级的
megrok.rdb包将我们的模型透明地转换为 ORM 映射。 -
如何将关系型数据库事务与 ZODB 的事务支持集成。
-
如何使用
megrok.rdb将我们的模型转换为 ORM 映射。
第十一章. Grok 背后的关键概念
在本书的第一章中,我们讨论了 Grok 的特点,强调了Zope 工具包(ZTK)和Zope 组件架构(ZCA)的重要性。我们还提到 Grok 提供了一种敏捷的方式来使用这些工具。
在前面的章节中,我们使用了几个 ZTK 包,并采用了许多 ZCA 模式,在需要时介绍和解释它们。我们还看到了 Grok 如何敏捷,因为我们逐步增长我们的应用程序,同时从一开始就拥有一个功能齐全的应用程序。
就目前为止我们所覆盖的材料而言,我们可以创建相当复杂的应用程序,但要真正充分利用 Grok,就必须在更详细的层面上解释 ZCA 背后的概念以及提供 Grok 灵活性的工具。
尽管 ZCA 及其模式的完整解释超出了本书的范围,但本章至少将讨论其更重要的概念,并通过对我们待办事项应用的小幅增加和扩展来展示它们的实用性。作者认为,一旦我们看到了这些概念在实际应用中的使用实例,我们就可以在解释它们时参考。
在本章中,我们将详细探讨这些概念。特别是,我们将讨论:
-
ZCA 是什么
-
接口
-
适配器、实用工具和事件
-
通过事件扩展应用程序
-
通过适配器扩展应用程序
Zope 组件架构
许多敏捷的 Web 应用程序框架被设计成专门允许从想法到工作应用程序的快速转换。这是好事,但有时对快速开发周期的强调会导致对可扩展性和代码重用等事物的关注减少。相比之下,Grok 基于一系列模式,其主要关注点是应用程序的可维护性和可扩展性。实际上,Grok 应用程序甚至可以从外部扩展,而无需接触它们的代码。允许这种扩展的模式集被称为Zope 组件架构。
尽管 Zope 这个词出现在它的名字中,但 ZCA 是一组通用的 Python 包,可以用来创建基于组件的应用程序,独立于 Zope 网络应用程序服务器或 ZTK。实际上,我们可以使用 ZCA 来创建任何类型的 Python 应用程序,并且它特别适合于大型系统的开发。
ZCA 通过将 Python 对象的功能封装到一个称为组件的实体中来工作,该组件具有由一个称为接口的单独对象定义的良好行为,该接口有助于开发者了解如何使用给定的组件,甚至可以通过实现其中表达的所有功能来完全替换它。
由于将系统的功能拆分为多个组件的概念需要开发者跟踪大量可能的组件,ZCA 提供了一个注册表,可以根据它们的接口注册和检索组件。这确实是 Zope 组件架构的关键部分,因为与之交互主要涉及以各种方式与注册表交互。
接口
我们在第五章中介绍了“接口”,当时我们使用它们来自动生成表单。在第五章中,我们解释了接口用于记录对象的外部行为。
一个在接口中定义其功能的对象被称为 提供 该接口。接口定义了一个对象可以做什么,但对象如何内部遵守此协议完全由实现者决定。
ZCA 中的接口用于在注册表中注册和查找组件。这使得我们能够根据组件的功能来查找它们,并轻松地替换、扩展或覆盖应用程序中的特定功能。
当一个类包含接口中描述的功能时,它就被说成是 实现 该接口。尽管如此,接口是独立于类的。一个特定的接口可能被任何数量的类实现。此外,一个类可以实现任何数量的接口。
让我们看看第六章中定义的接口,以便使项目、列表和条目可搜索:
class ISearchable(interface.Interface):
title = interface.Attribute('title')
kind = interface.Attribute('kind')
description = interface.Attribute('description')
creator = interface.Attribute('creator')
creation_date = interface.Attribute('creation date')
modification_date = interface.Attribute('modification date')
checked = interface.Attribute('checked')
content_type = interface.Attribute('content type')
project_name = interface.Attribute('project name')
def searchableText():
"""return concatenated string with all text fields to search"""
接口是一个类,它继承自 zope.interface 包中定义的 Interface 类。ISearchable 接口描述了一个可搜索对象必须提供的属性和方法。类似于我们的待办事项列表应用程序,我们可能有几种不同的内容类型,但只要它们的类定义承诺实现 ISearchable,我们就可以在它们上面使用相同的搜索代码而不会出现问题。
注意到 searchableText 方法定义中不包含 Python 类中使用的 self 参数。这是因为尽管接口记录了方法,但它永远不会被实例化,因此在这里不需要 self。
一旦我们定义了一个接口,我们就可以创建实现它的类。在我们的应用程序中,ISearchable 接口由几个类实现。例如,以下是 Project 内容类型如何声明其遵守 ISearchable 协议的意图:
class Project(grok.Container):
grok.implements(IProject, IMetadata, ISearchable)
在 Grok 中,我们使用 implements 类注解来声明一个类实现了一个或多个接口。我们可以看到,除了 ISearchable,Project 类还实现了 IProject 和 IMetadata。
一旦我们创建了一个实际的 Project 对象,它就被说成是 提供 ISearchable,这意味着类实现接口,而这些类的实例提供了它们。
接口也可以用来定义表单生成的模式。我们在整个应用程序代码中大量使用了这种方法。以下是一个示例:
class ITodoList(interface.Interface):
title = schema.TextLine(title=u'Title', required=True, constraint=check_title)
description = schema.Text(title=u'Description', required=False)
next_id = schema.Int(title=u'Next id' ,default=0)
我们已经看到,通过使用 grok.AutoFields 指令,我们可以将此模式定义转换为表单上的 HTML 控件。
由于接口在组件注册表中用作键,因此可以找到实现该接口的所有对象。这对于找到属于某些类型的对象非常有用,但也可以用于更有趣的事情,例如创建仅与提供接口的对象一起工作的方法,或者扩展实现它的任何类的功能。
这通常在不需要为某些接口定义特殊功能时很有用,因此有时我们可能会遇到空接口定义,称为标记接口。它们基本上将对象标记为属于某个类型,这使得注册表可以找到它们并将其他组件注册为与它们一起工作。到目前为止,在我们的应用程序中我们还没有需要它们,但它们可以是我们工具箱中非常有用的补充。
适配器
ZCA 的一个原则是使用组件而不是继承来扩展应用程序的行为,这样我们就可以将不同的组件相互连接,以获得所需的结果。
这种方法需要三件事:
-
明确定义的组件,这就是我们使用接口的原因。它们将组件标记为提供特定的行为,并为该功能提供一种蓝图或合同。
-
跟踪多个组件的简单方法。我们已经提到 ZCA 有一个注册表,接口被用作键来根据其定义的功能检索组件。这也使得可以完全替换一个组件为完全不同的一个,只要它实现了相同的接口。
-
一种使不同接口的组件协同工作的方式,即使它们具有完全不同的接口。这就是适配器发挥作用的地方。
适配器简单地说是一段代码,它接受具有特定接口的对象,并使其提供额外的接口。换句话说,它适应了组件,使其提供新的行为,而无需对其代码进行任何更改。
现实世界的适配器
将其视为现实世界中的物体可能会有所帮助。如今,手机无处不在,许多人依赖它们来满足他们的通信需求。然而,它们的电池寿命相当短,需要不断充电。当我们购买一部新手机时,我们通常会得到一个交流适配器,这允许我们将手机插入任何墙壁插座并为其充电。在某些情况下,我们可能无法使用墙壁插座,例如,在长途汽车旅行期间。在这种情况下,我们当然可以使用车载适配器,通过使用车辆的电源插座来为手机充电。
ZCA 的适配器与这些电话适配器非常相似。它们适配电源,无论是汽车还是插座,并使其以不同的接口呈现,以便手机可以充电。电源和手机都不会有任何改变,甚至不需要了解所使用的特定适配器的任何信息,只要它们符合既定的电源和手机连接接口(现在你明白术语的来源了)。
定义和使用适配器
到目前为止,我们尚未明确定义或使用适配器,所以让我们快速看一下在 Grok 中如何进行此操作。假设我们想要显示待办事项列表应用程序中任何内容上次修改过去的天数。如果我们为每种内容类型添加方法,我们将有很多重复的代码,并且任何对某个方法逻辑的更改都需要对所有内容类型进行更改。通过使用适配器,我们可以在一个地方获得所有代码,如下所示:
import datetime
class IDayCalculations(Interface):
def daysOld():
"number of days since the content was created"
def daysFromLastModification():
"number of days since the last modification"
class DaysOld(grok.Adapter):
grok.implements(IDayCalculations)
grok.adapts(ISearchable)
def daysOld(self):
return (datetime.datetime.now() - self.context.creation_date).days
def daysFromLastModification(self):
return (datetime.datetime.now() - self.context.modification_date).days
首先,我们定义我们的“天”计算方法的接口。为了演示目的,我们将保持其简单性。grok.Adapter组件用于定义适配器。我们创建了一个适配器,并使用grok.implements指令来表示它将实现我们在接口中刚刚定义的方法。
我们应用程序中的所有内容已经实现了ISearchable接口,因此为了使每个待办事项应用程序对象都提供此接口,我们使用grok.adapts指令。因此,DaysOld是一个适配器,它接受任何提供ISearchable的对象,并为它提供IDayCalculations。
适配器实例将同时具有context和request属性。这是因为适配器总是接收它所适配的对象作为参数,以便能够访问它们的属性并在实现中使用它们。在这个例子中,self.context指的是被适配的context对象,它可以是TodoList、Project、TodoListItem等。由于所有这些对象都实现了ISearchable接口,我们知道modification_date和creation_date将可用于计算。
我们可以在以下方式中使用此适配器在我们的内容类型之一的任何视图中:
class ProjectView(grok.View):
grok.context(Project)
def update(self):
self.days_old = IDayCalculations(self.context).daysOld()
self.days_modified = IDayCalculations( self.context).daysFromLastModification()
在这个例子中,我们使用update方法将days_modified和days_old属性插入到视图中,以便在渲染时它们将可用于视图模板。要从注册表中获取适配器,我们调用IDayCalculations接口本身,并将context对象作为参数,在这种情况下是一个Project。一旦我们有了适配器,我们就可以简单地调用它的方法,它将表现得好像这些方法是Project组件的一部分。
当刚开始了解它们时,适配器可能看起来是一种绕弯路的方式来获取我们需要的组件,但请记住,整个系统都是为了易于扩展和进化而设计的。通过使用适配器,我们可以在其他包内部覆盖组件,同时系统仍然可以正常工作,无需以任何方式修改原始代码。
我们在应用程序中使用的适配器
为了在 Grok 中使用适配器,它们首先需要注册到 ZCA 中。Grok 本身在应用程序启动时会执行一系列注册。实际上,我们已经在代码中使用了这些。在第七章中,我们向我们的应用程序添加了身份验证,并决定为每个新创建的用户添加特定的角色,以简化权限管理。
Grok 站点对角色分配一无所知,因此为了能够为某个主体定义角色,它使用了一个角色管理器。这个角色管理器是一个适配器,它使得站点能够提供角色管理器接口。看看我们使用的代码:
def addUser(self, username, password, real_name, role):
session = Session()
result = session.query(Account).filter_by(name = username).first()
if result is None:
user = Account(username, password, real_name, role)
session.add(user)
role_manager = IPrincipalRoleManager(grok.getSite())
if role==u'Project Manager':
role_manager.assignRoleToPrincipal('todo.ProjectManager', username)
elif role==u'Application Manager':
role_manager.assignRoleToPrincipal('todo.AppManager', username)
else:
role_manager.assignRoleToPrincipal('todo.ProjectMember', username )
transaction.commit()
在这里,我们正在添加一个新用户,紧接着我们就为该用户分配了一个角色。正如我们之前提到的,站点将这项责任委托给角色管理器。角色管理器是一个适配器,它注册为实现了 Grok 站点的IPrincipalRoleManager接口,因此我们需要使用这些信息查询注册表以获取实际的经理。执行此操作的行是:
role_manager = IPrincipalRoleManager(grok.getSite())
该接口本身执行注册查找,请求一个已注册的组件,并将其提供给实现 Grok 站点接口的对象。请注意,Grok 站点本身作为参数传递,而不是其接口,但 ZCA 足够智能,能够找到正确的组件,如果它已注册。
在这种情况下,查询为我们提供了一个实现IPrincipalRoleManager接口的角色管理器对象,因此我们知道assignRoleToPrincipal方法将可用于为我们的新用户分配正确的角色。
适配器可以被命名,这样我们就可以通过它的名字来获取一个特定的IPrincipalRoleManager组件。此外,一个适配器同时适配多个组件也是可能的,在这种情况下,它被称为多适配器。例如,Grok 中的所有视图都是请求和上下文的多适配器:
class TodoListUpdateItems(grok.View):
grok.context(TodoList)
grok.name('updateitems')
grok.require('todo.changeitems')
上下文通过grok.context指令在类体中指定,请求指的是当前的浏览器请求。在前面的代码中,TodoListUpdateItems是一个请求和一个TodoList的多适配器。它使用名称updateitems。这个多适配器/视图只有在用户在浏览TodoList对象时请求updateitems视图时,才会被 Grok 调用。
我们还在我们的待办事项列表管理器代码中明确使用了多适配器:
class AddProjectViewlet(grok.Viewlet):
grok.viewletmanager(Main)
grok.context(Todo)
grok.view(AddProject)
def update(self):
self.form = getMultiAdapter((self.context, self.request), name='add')
self.form.update_form()
def render(self):
在第八章中,我们展示了如何获取在视图组件中渲染的表单,并看到我们需要获取创建该特定表单的特定组件。在这里,注册表查找是通过从zope.component导入的getMultiAdapter方法执行的。我们特别想要一个项目的“添加表单”,因为我们想要渲染AddProjectViewlet。由于上面的grok.context指令,我们知道我们有一个请求,上下文是一个Todo应用程序,因此如果我们调用getMultiAdapter并传递请求、上下文以及所需表单的名称,ZCA 机制将为我们找到它。
实用程序
正如我们所见,组件注册表主要是由注册到特定接口的适配器组成。在某些情况下,注册不适应任何东西但提供我们需要能够覆盖或替换的某种服务也是很有用的。数据库连接、身份验证后端和用户源就是这类服务的例子。
ZCA 有概念来覆盖这种情况。实用程序只是一个具有声明接口的组件,它也可能有一个名称。ZCA 中的实用程序可以是本地的也可以是全局的。
全局实用程序
全局实用程序是在 Grok 启动时创建和注册的,但它不是持久的(也就是说,其状态没有保存在 ZODB 中)。我们使用过的全局实用程序的例子包括数据库连接和会话凭证。
让我们看看第七章中我们集成到应用程序中的身份验证服务时添加的实用程序:
class MySessionCredentialsPlugin(grok.GlobalUtility, SessionCredentialsPlugin):
grok.provides(ICredentialsPlugin)
grok.name('credentials')
loginpagename = 'login'
loginfield = 'login'
passwordfield = 'password'
要定义全局实用程序,我们继承自grok.GlobalUtility组件,并通过使用grok.provides指令声明组件提供的接口。命名实用程序也需要使用grok.name指令来注册它们的名称。
要从注册表中获取全局实用程序,我们使用zope.component中定义的getUtility函数。在这种情况下,credentials实用程序不是直接由我们的代码调用,而是由身份验证机制本身调用。然而,如果我们想使用这个实用程序,我们会像以下这样获取它:
from zope.component import getUtility
credentials_plugin = getUtility(ICredentialsPlugin, 'credentials')
本地实用程序
本地实用程序与全局实用程序非常相似,但它会被保存在数据库中,因此其状态和配置是持久的。Zope Toolkit 目录和我们在应用程序中使用的可插拔身份验证机制都是本地实用程序的例子。
我们可以查看第七章中定义的UserAuthenticatorPlugin,以了解如何定义本地实用程序:
class UserAuthenticatorPlugin(grok.LocalUtility):
grok.implements(IAuthenticatorPlugin)
grok.name('users')
我们可以看到,这与全局实用程序的工作方式完全相同,只是我们继承自grok.LocalUtility而不是它。然而,我们实际上不能使用本地实用程序,除非我们明确将其添加到grok.Container组件中。看看Todo应用程序的主应用程序组件:
class Todo(grok.Application, grok.Container):
grok.implements(ITodo)
grok.local_utility(
UserAuthenticatorPlugin, provides=IAuthenticatorPlugin,
name='users',
)
grok.local_utility(
PluggableAuthentication, provides=IAuthentication,
setup=setup_authentication,
)
我们的应用程序包含两个本地实用工具。有一个名为 UserAuthenticatorPlugin 的实用工具,因为我们可能需要与多个用户源一起工作。还有一个名为 PluggableAuthentication 的实用工具,它将处理我们网站的认证需求,因此不需要通过名称与其他类似实用工具区分开来。
实际上,我们的应用程序还包含第三个本地实用工具,即“目录”,它在应用程序创建时由 Grok 自动添加。目录不是一个命名实用工具。
要使用这些实用工具之一,我们使用与我们的全局实用工具相同的 getUtility 函数:
users = component.getUtility(IAuthenticatorPlugin, 'users')
auth = component.getUtility(IAuthentication)
关于本地实用工具的一个重要注意事项是,由于它们是在应用程序创建时添加并存储在数据库中的,因此更改一个实用工具的初始化代码在应用程序创建后不会有任何效果。在这种情况下,使修改后的本地实用工具工作最简单的方法是删除应用程序实例并重新创建它。
事件
Zope 工具包定义了一系列生命周期事件,每当对对象执行某些操作时(如创建或修改)都会触发这些事件。一个事件可以有一个或多个订阅者,每当订阅的事件发生时都会调用这些订阅者。这些订阅者被称为事件处理器,Grok 提供了一种简单的方式来定义它们。
这里是我们可以通过使用 Grok 订阅的一些事件:
| 事件 | 描述 | 事件属性 |
|---|---|---|
IObjectModifiedEvent |
一个对象已被修改。这是一个通用事件,涵盖了持久对象的所有更改,如添加、移动、复制或删除对象。 | object descriptions |
IContainerModifiedEvent |
容器已被修改。容器修改是针对子对象的添加、移除或重新排序的特定操作。继承自 grok.IObjectModifiedEvent。 |
object descriptions |
IObjectMovedEvent |
一个对象已被移动。 | object oldParent oldName newParent newName |
| 事件 | 描述 | 事件属性 |
IObjectAddedEvent |
一个对象已被添加到一个容器中。 | object oldParent oldName newParent newName |
IObjectCopiedEvent |
一个对象已被复制。 | object original |
IObjectCreatedEvent |
一个对象已被创建。此事件旨在在对象被持久化之前发生,即其位置属性(__name__ 和 __parent__)通常为 None。 |
object |
IObjectRemovedEvent |
一个对象已被从一个容器中移除。 | object oldParent oldName |
IBeforeTraverseEvent |
发布者即将遍历一个对象。 | object request |
我们可以通过使用 grok.subscriber 装饰器在 Grok 中定义事件处理器:
@grok.subscribe(Project, grok.IObjectAddedEvent)
def handler(obj, event):
"New project added: %s." % obj.title
每次向容器添加新的 Project 时,都会执行此代码。处理程序接收两个参数 obj,它包含涉及的对象,以及 event,它包含前面表格中列出的属性。
由于订阅实际上是一种适配器,grok.subscribe 装饰器的第一个参数可以是任何接口,因此我们可以使订阅尽可能通用或具体。在早期示例中,我们传递了 Project 作为参数,因此只有当添加 Project 时,处理程序才会执行,但我们可以传递 Interface,以便获取所有事件的实例,无论对象类型如何。可以添加额外的订阅者来处理同一事件,但它们被调用的顺序无法预先知道,因此不要依赖它。
从外部扩展 Grok 应用程序
毫无疑问,Zope 组件架构最令人愉悦的特性之一是它使得在不触及代码的情况下扩展或覆盖应用程序的功能变得非常容易。
许多 Grok 组件,如视图和实用工具,都可以在不进行任何特殊操作的情况下被覆盖。其他对象,如视图小部件,可能需要一些小的修改来添加新功能。
在本节中,我们将展示如何通过创建一个独立的包来扩展 Grok 应用程序,该包为我们的待办事项应用程序添加新的功能,从而使其变得非常容易。我们刚刚获得的关于 ZCA 的知识将在这个任务中派上用场,因此希望当我们的附加组件准备好时,我们能对其有更好的理解。
准备原始应用程序
当然,如果我们尽量减少对原始应用程序的修改,这个演示将更有说服力。幸运的是,我们只需要对 app.py 模块进行一个小小的添加,并对 navigation.pt 模板进行简单的修改。
在 app.py 的顶部添加以下两行,位于 import 语句之下:
master_template = grok.PageTemplateFile('app_templates/master.pt')
form_template = grok.PageTemplateFile('custom_edit_form.pt')
这将允许我们通过使用简单的 import 语句来使用新应用程序中的主页面和表单模板。
我们将做出的唯一其他更改是在导航视图小部件中添加额外的视图管理器,以便其他应用程序可以轻松地在那里插入导航选项。为此,我们需要将视图管理器定义添加到 app.py 中,如下所示:
class ExtraNav(grok.ViewletManager):
grok.context(Interface)
grok.name('extranav')
我们还需要修改导航模板,以考虑新的视图管理器。将 app_templates/navigation.pt 修改如下:
<div id="navigation">
<a tal:attributes="href python:view.application_url('index')"> Go back to main page</a>
<tal:extranav content="structure provider:extranav" />
</div>
我们在这里添加的唯一内容是“提供者”行,用于在链接之后插入新的视图小部件,以便返回主页。
为了使待办事项应用程序准备好由第三方包扩展,我们只需要做这些。当然,我们本可以在一开始就添加这段代码,但展示你需要添加多少代码是有教育意义的,即使你一开始没有考虑可扩展性。
待办事项+包
想象一下,我们在网上某个地方找到了待办事项应用程序,我们认为它几乎满足了我们对列表管理器的所有要求,只要它有更多一些功能。
代码是可用的,因此我们可以直接扩展应用程序。然而,由于我们无法控制其开发,我们基本上是在分叉代码,这意味着任何由原始开发者添加到应用程序中的新功能都需要合并到我们的代码中,以便我们能够利用它们。此外,原始应用程序的代码风格可能与我们习惯的不同,我们不希望有混合的风格。
对我们来说最好的做法是创建一个完全独立的包,该包扩展原始应用程序,并且完全在我们控制之下。如果我们真的不需要大量修改原始应用程序,这就是我们应该采取的方法。让我们创建一个“待办事项+”包,该包向现有的“待办事项”包添加一些功能,我们将将其声明为依赖项。
创建新包
在现实生活中,待办事项包将作为一个“egg”提供,我们可以在新的 Grok 项目中使用它,但由于我们仍然处于开发阶段,我们只需将一个新的包添加到我们的项目中,而不是创建另一个。
我们用来初始化项目的 grokproject 脚本创建的buildout有一个src目录,我们可以向其中添加我们的新包。进入该目录,并输入以下命令:
$ ../bin/paster create -t basic_package todo_plus
此脚本将提出一些问题,但到目前为止,你可以在每个问题后只需按下Enter键,以接受默认值。将创建一个名为todo_plus的新目录。此目录将包含我们的新包。
首先,我们需要确保这个包在启动时被解析,因此我们包含grok,并解析当前包。由于这个包依赖于原始的todo应用程序的存在,我们必须确保其依赖项也被解析。在todo_plus/todo_plus目录内创建一个configure.zcml文件,内容如下:
<configure >
<include package="grok" />
<includeDependencies package="." />
<grok:grok package="." />
</configure>
注意,与为我们原始项目创建的todo包不同,todo_plus包有两个子目录级别,因此请务必在第二级todo_plus目录内创建此文件。
接下来,创建 Grok 应用程序使用的app_templates和static目录:
$ mkdir todo_plus/todo_plus/app_templates
$ mkdir todo_plus/todo_plus/static
此外,你必须在todo_plus/setup.py文件的requires部分添加todo包,如下所示:
install_requires=[
'todo',
]
最后,需要更新buildout.cfg文件以包含新包。在将todo_plus包添加到所需部分后,文件顶部应如下所示(将在第十四章中更详细地讨论 buildout):
[buildout]
develop = .
src/todo_plus
parts = eggbasket app i18n test data log
newest = false
extends = versions.cfg
# eggs will be installed in the default buildout location
# (see .buildout/default.cfg in your home directory)
# unless you specify an eggs-directory option here.
find-links = http://download.zope.org/distribution/
versions = versions
[app]
recipe = zc.recipe.egg
eggs = todo
todo_plus
z3c.evalexception>=2.0
Paste
PasteScript
PasteDeploy
interpreter = python-console
site.zcml = <include package="todo_plus" />
在develop行中,我们正在告诉buildout,除了当前目录中的包(即todo包本身)之外,我们还将添加我们的新todo_plus开发包。这将创建一个todo_plus的开发“egg”,应该添加到[app]部分的 eggs 行下方,紧挨着原始的待办事项包。
现在,我们可以重新运行buildout,我们的包将准备好工作:
$ bin/buildout
将皮肤选择表单添加到应用程序中
在第八章中,我们向应用程序添加了主题,尽管查看默认主题之外的主题的唯一方法是在 URL 中使用丑陋的++skin++遍历助手。正如我们当时提到的,我们不希望用户这样做,因此从表单中选择所需皮肤的方法会很方便。
让我们将此作为todo_plus包将添加到待办事项列表应用程序中的第一个功能。目标是有一个表单,我们可以从中获取可用屏幕的列表,选择一个,然后保存它。之后,Grok 应该在我们导航通过待办事项应用程序时自动使用所选皮肤。
添加新皮肤
首先,我们的包将包含自己的皮肤层,以添加到原始应用程序中可用的三个皮肤中。当然,我们需要从待办事项应用程序中导入一些东西,但这一切都非常直接。在src/todo_plus/todo_plus目录内创建一个名为app.py的文件,并向其中添加以下代码行:
import grok
from zope.interface import Interface
from zope.publisher.interfaces.browser import IBrowserSkinType, IDefaultBrowserLayer
from todo.app import ITodo, IProject, Todo, HeadSlot, ExtraNav, Main, master_template, form_template
class PlusLayer(IDefaultBrowserLayer):
grok.skin('plus')
class HeadPlus(grok.Viewlet):
grok.viewletmanager(HeadSlot)
grok.context(Interface)
grok.name('head')
grok.template('head_plus')
grok.layer(PlusLayer)
因为我们需要为此皮肤层向HeadSlot添加一个视图组件,我们必须从todo.app中导入它。我们需要的其他所有东西,我们已经在第八章中完成了。我们添加了一个新层,并使用grok.skin指令将其转换为皮肤。之后,我们添加了一个视图组件,并使用名称head将其注册到该层,覆盖了默认皮肤中的原始视图组件。当然,我们需要为新视图组件创建一个模板。将以下代码添加到app_templates/head_plus的顶部:
<meta tal:attributes="http-equiv string:Content-Type; content string:text/html;; charset=utf-8" />
<title tal:content="context/title">To-Do list manager</title>
<link rel="stylesheet" type="text/css" tal:attributes="href static/styles_plus.css" />
最后,我们可以将原始应用程序中的styles.css文件复制并保存为styles_plus.css。为了保持简单,就像我们在第八章中所做的那样,做一些明显的修改,例如更改页眉背景颜色,然后暂时保持这样。
正常重启待办事项应用程序,使用 paster 然后导航到localhost/++skin++plus/todo/index,你将看到新皮肤。就这样。对于一个简单的热身来说,还不错。从一个完全独立的包开始,我们已经添加了一个新皮肤,该皮肤可以透明地与待办事项应用程序集成。
皮肤选择表单
现在,我们已经准备好添加表单。我们需要一个包含所有可用皮肤名称的下拉列表,目前这些皮肤包括基本(默认)、火星、森林和加号。我们可以使用一个schema.Choice字段并将这些值传递给它,但这样,如果原始应用程序添加了新的皮肤或者第三方推出了包含新皮肤的另一个附加包,我们的皮肤列表就会过时。我们需要一种动态指定皮肤名称值的方法,这样我们就不必在代码中跟踪所有皮肤名称。
幸运的是,正如我们将在想要向 Grok 应用程序添加新功能时经常发现的那样,Zope Toolkit 已经有一个名为zc.sourcefactory的包可以帮助我们。我们可以通过将其添加到src/todo_plus/setup.py文件的install_requires部分来使用此包,如下所示:
install_requires=[
'todo',
'zc.sourcefactory',
]
重新运行buildout,包将像往常一样被下载和安装。现在我们可以用它来定义一个动态选择字段:
from zope import schema
from zope.component import getAllUtilitiesRegisteredFor, getMultiAdapter, getUtility
from zc.sourcefactory.basic import BasicSourceFactory
class SkinNameSource(BasicSourceFactory):
def getValues(self):
values = ['Basic']
skin_tag = 'grokcore.view.directive.skin'
skin_names = [s.getTaggedValue(skin_tag)
for s in getAllUtilitiesRegisteredFor(IBrowserSkinType) if skin_tag in s.getTaggedValueTags()]
values.extend(skin_names)
return values
class ISkinChooser(Interface):
skin_name = schema.Choice(source=SkinNameSource(), title=u'Skin Name', description=u'Name of the new default skin')
源只是一个返回用于方案字段内部值的方法,而不是静态列表。BasicSourceFactory子类用于定义源。
我们之前已经创建了一个表单方案,所以在ISkinChooser方案定义中唯一的新事物是Choice字段中的source=SkinNameSource()参数。这告诉 Grok 使用名为SkinNameSource的源工厂来提供皮肤名称值。
要创建一个源,我们只需从BasicSourceFactory派生,并添加一个getValues方法,该方法将返回一个皮肤名称列表。要获取皮肤名称本身,我们必须使用组件注册表。在 Zope 中,皮肤被注册为具有IBrowserSkinType接口和皮肤名称的命名实用工具。
我们如何获取注册为此接口的所有实用工具?zope.component包包含一个名为getAllUtilitiesRegisteredFor的函数,它可以完成这项工作。为了将它们与 Zope 皮肤区分开来,Grok 为其皮肤组件添加了一个标签,以标识它们为 Grok 层。这个标签是grokcore.view.directive.skin,在前面代码中分配给skin_tag。要获取 Grok 皮肤名称,我们从一个包含值'Basic'的列表开始,以考虑到默认皮肤,它没有被 Grok 标记。然后我们遍历每个注册的皮肤,检查它是否有skin_tag。如果有,我们就将它添加到列表中。
以这种方式,每次从 Grok 包添加新的皮肤层时,它将自动列在我们的“皮肤选择器”表单中。组件注册表为我们处理可用的皮肤。
现在我们需要添加表单,以及一个包含表单的SkinChooser视图和视图组件。我们在第八章中已经完成了所有这些,所以应该看起来很熟悉:
from zope.app.session.interfaces import Isession
class SkinChooserForm(grok.Form):
grok.context(ITodo)
grok.name('skin_chooser_form')
form_fields = grok.AutoFields(ISkinChooser)
label = "Set the default skin for the To-Do list manager"
template = form_template
def setUpWidgets(self, ignore_request=False):
super(SkinChooserForm,self).setUpWidgets(ignore_request)
session = ISession(self.request)['todo_plus']
self.widgets['skin_name'].setRenderedValue(session.get( 'skin_name''Basic'))
@grok.action('Choose skin')
def choose(self, **data):
session = ISession(self.request)['todo_plus']
session['skin_name'] = data['skin_name']
return self.redirect(self.url('skin_chooser'))
class SkinChooser(grok.View):
grok.context(ITodo)
grok.name('skin_chooser')
grok.require('todo.changeitems')
def render(self):
return master_template.render(self)
class SkinChooserViewlet(grok.Viewlet):
grok.viewletmanager(Main)
grok.context(ITodo)
grok.view(SkinChooser)
def update(self):
self.form = getMultiAdapter((self.context, self.request), name='skin_chooser_form')
self.form.update_form()
def render(self):
return self.form.render()
SkinChooserForm从grok.Form继承,因为我们想要一个与内容无关的表单。其上下文是ITodo,使其仅在根应用程序级别显示。我们使用我们在前面创建的ISkinChooser接口中定义的字段。
我们需要一种方法来存储皮肤选择,以便在用户在应用程序中导航时可用。为此,我们将使用在zope.app.session包中定义的会话机制(是的,Zope 工具包再次救命)。会话是请求的适配器,因此我们可以通过发出ISession(self.request)来获取它。我们使用todo_plus包名作为会话键,以避免与其他会话对象发生命名空间冲突。
在表单的updateWidgets方法中,我们尝试从会话中获取skin_name的值(如果skin_name尚未初始化,则返回'Basic')。有了这个值,我们可以通过在选项小部件上调用setRenderedValue来显示当前所选的皮肤。
最后,在表单操作中,我们将会话的skin_name键设置为用户提交的表单数据返回的值,并将用户重定向到'皮肤选择'视图。

视图和视图小部件定义没有新增内容,但请注意我们如何使用从原始待办应用导入的组件来执行注册。视图被分配ITodo作为其上下文,视图小部件则注册到该包的 Main 视图小部件管理器。
使用事件来显示所选皮肤
到目前为止,我们有一个可以保存所选皮肤名称到当前会话的skin_chooser表单,但我们还需要在每个页面视图中自动使用该名称设置皮肤。为此,我们将利用 Grok 的事件功能,这在本章的Zope 组件架构部分有讨论。
ZCA 提供了一个注册了IBeforeTraverseEvent接口的事件,我们可以使用它来在每个请求上设置皮肤。Grok 有一个非常方便的grok.subscribe装饰器,它允许我们轻松添加此事件的处理器:
from zope.app.publication.interfaces import IbeforeTraverseEvent
from zope.publisher.browser import applySkin
@grok.subscribe(ITodo, IBeforeTraverseEvent)
def choose_skin(obj, event):
session = ISession(event.request)['todo_plus']
skin_name = session.get('skin_name','Basic')
skin = getUtility(IBrowserSkinType,skin_name)
applySkin(event.request, skin)
我们将ITodo接口与IBeforeTraverse事件注册,这意味着在 Grok 显示待办应用实例内的视图之前,我们的choose_skin处理程序将被调用。
在处理程序内部,我们获取会话对象并找到当前浏览器会话所选的皮肤名称。然后我们使用getUtility函数通过此名称获取实际的皮肤对象,并使用从zope.publisher.browser导入的applySkin函数设置皮肤。结果是所选的皮肤将在页面显示之前及时设置。
在内容创建时发送电子邮件通知
在多用户系统中,通常请求的一个功能是用户可以对其他用户的内容进行操作,即更改的电子邮件通知。如果我们向待办加号包中添加此功能,它肯定会赢得其'plus'名称。
我们需要一个表单来设置通知属性,例如目标电子邮件和消息主题。当然,为了保持网站布局的一致性,我们还需要进行我们通常的表单在视图组件中的注册舞蹈。这些是我们已经知道如何做到的事情。更有趣的是,我们将如何通过事件订阅来设置我们的电子邮件处理程序,以及实际的电子邮件将如何发送。让我们开始吧。
目前,此功能将在创建项目时向指定的电子邮件列表发送通知消息。为此,我们首先需要一个表单模式的接口定义:
class ISetNotifications(Interface):
enabled = schema.Bool(title=u'Enable notifications', description=u'Email will only be sent if this is enabled')
sender = schema.TextLine(title=u'Sender email', description=u'Email address of sender application')
emails = schema.TextLine(title=u'Notification emails', description=u'One or more emails separated by commas')
subject = schema.TextLine(title=u'Message subject')
message = schema.Text(title=u'Message introduction')
enabled字段将允许管理员打开或关闭通知。如果它们被打开,将向emails字段中指定的电子邮件列表发送包含指定消息和subject字段信息的电子邮件。
对象注释
在我们继续进行表单定义之前,我们需要决定如何存储通知属性。我们不能使用会话,就像我们在皮肤选择器中做的那样,因为通知属性将是应用程序的全局属性。实际上,它们可以被认为是Todo实例本身的一部分,因此理想的做法是将它们存储在那里。但这需要更改原始代码以添加这些属性,对吗?
嗯,不是的。Grok 提供了一个名为annotation的特殊组件,可以用于在特定上下文对象内部存储信息,而无需直接修改它。这是通过为上下文对象注册持久适配器来实现的。也就是说,在幕后,有一个IAnnotation接口为任何实现IAnnotatable接口的注册表中的对象提供这项写入服务。
如往常一样,Grok 通过一个方便的组件简化了设置,我们可以通过它进行子类化,称为grok.Annotation。如前所述,让我们将我们的annotation组件注册到主应用的ITodo接口:
class SetNotificationsAnnotation(grok.Annotation):
grok.implements(ISetNotifications)
grok.context(ITodo)
sender = 'grokadmin@example.com'
emails = ''
subject = 'New project created'
message = ''
enabled = False
grok.context指令表明,将向具有ITodo接口的组件添加注释,我们知道这是主应用对象。grok.implements指令告诉 Grok 将此注释适配器注册到ISetNotifications接口,这是我们将在注册表中找到它的方式。注意,此接口与我们将在“通知属性”表单中使用的接口相同。之后定义的变量代表存储属性的默认值。
创建表单
如我们现在知道属性将存储在哪里,我们可以继续创建表单。表单定义的代码看起来像这样:
class SetNotificationsForm(grok.Form):
grok.context(ITodo)
grok.name('set_notifications_form')
form_fields = grok.AutoFields(ISetNotifications)
label = 'Set email notification options'
template = form_template
def setUpWidgets(self, ignore_request=False):
super(SetNotificationsForm,self).setUpWidgets(ignore_request)
todo_annotation = ISetNotifications(self.context)
self.widgets['sender'].displayWidth = 80
self.widgets['emails'].displayWidth = 80
self.widgets['subject'].displayWidth = 50
self.widgets['message'].height = 7
self.widgets['emails'].setRenderedValue( ','.join(todo_annotation.emails))
self.widgets['enabled'].setRenderedValue( todo_annotation.enabled)
self.widgets['sender'].setRenderedValue( todo_annotation.sender)
self.widgets['message'].setRenderedValue( todo_annotation.message)
self.widgets['subject'].setRenderedValue( todo_annotation.subject)
@grok.action('Set notification options')
def set_options(self, **data):
todo_annotation = ISetNotifications(self.context)
todo_annotation.emails = data['emails'].split(',')
todo_annotation.enabled = data['enabled']
todo_annotation.subject = data['subject']
todo_annotation.message = data['message']
return self.redirect(self.url('set_notifications'))
我们使用grok.AutoFields指令通过使用我们在ISetNotifications接口中先前定义的字段自动构建表单。与skin_chooser表单一样,我们使用从原始应用导入的表单模板,以保持网站的外观和感觉相同。
在setUpWidgets方法中,我们通过调用ISetNotifications(self.context)获取annotation对象,该调用在注册表中搜索实现ISetNotification并注册到当前上下文(即todo应用程序)的适配器。一旦我们有了这个适配器,我们就使用每个小部件的setRenderedValue方法来使表单在显示时显示当前存储的值。我们还改变了各种文本字段的大小(尽管,目前这并不重要)。
在set_options表单提交处理程序中,我们再次获取注释对象,但这次我们将提交的值存储在其对应的属性中。之后,我们只需将用户重定向到相同的表单。
剩下的工作是将表单插入到视图中,然后将该视图添加到视图中的Main视图管理器。相应的代码如下:
class SetNotifications(grok.View):
grok.context(ITodo)
grok.name('set_notifications')
grok.require('todo.addprojects')
def render(self):
return master_template.render(self)
class SetNotificationsViewlet(grok.Viewlet):
grok.viewletmanager(Main)
grok.context(ITodo)
grok.view(SetNotifications)
def update(self):
self.form = getMultiAdapter((self.context, self.request), name='set_notifications_form')
self.form.update_form()
def render(self):
return self.form.render()
这里没有新的内容。只需注意,要编辑“电子邮件通知”属性,需要添加项目的权限。以下截图显示了表单的实际应用:

发送电子邮件
要发送电子邮件,我们需要一个邮件投递机制。当然,Zope Toolkit 有一个名为zope.sendmail的包可以完成这个任务,所以我们只需将其添加到我们的setup.py文件中,然后重新运行buildout,以便使用它。编辑setup.py文件,并将其添加到install_requires行:
install_requires=[
'todo',
'zc.sourcefactory',
'zope.sendmail',
]
重新运行buildout。现在,我们必须配置邮件投递服务。与迄今为止我们使用的 Grok 包不同,这个 Zope Toolkit 包需要使用基于 XML 的 Zope 配置语言 ZCML 进行配置。打开todo_plus包中的configure.zcml文件,并修改它以看起来像这样:
<configure xmlns:mail=http://namespaces.zope.org/mail >
<include package="grok" />
<includeDependencies package="." />
<grok:grok package="." />
<mail:smtpMailer
name="todoplus.smtp"
hostname="mail.example.com"
port="25"
username="cguardia"
password="password"
/>
<mail:queuedDelivery
name="mailer"
permission="zope.Public"
mailer="todoplus.smtp"
queuePath="mailqueue"
/>
</configure>
注意文件顶部添加了mail命名空间。这允许我们在正常的 Grok 配置之后使用邮件指令。smtpMailer指令代表一个命名的 SMTP 服务器。其参数是host, port, username和password。如果你的邮件主机不需要密码,只需省略username和password参数。
queuedDelivery指令设置了一个队列,用于发送邮件消息。这在一个独立的线程中完成,以便在发送大量电子邮件时应用程序仍能继续工作。permission参数指的是发送电子邮件所需的权限。请确保使用与上面 SMTP 邮件器定义中使用的mailer参数相同的名称。
现在,我们准备注册发送电子邮件的事件。我们将使用grok.subscribe指令将我们的处理程序注册到Project对象的IObjectAddedEvent。
import email.MIMEText
import email.Header
from zope.sendmail.interfaces import IMailDelivery
@grok.subscribe(IProject, grok.IObjectAddedEvent)
def send_email(obj, event):
todo_annotation = ISetNotifications(obj.__parent__)
if not todo_annotation.enabled:
return
sender = todo_annotation.sender
recipient = todo_annotation.emails
subject = todo_annotation.subject
body = todo_annotation.message
body = body.replace('${title}',obj.title)
body = body.replace('${description}',obj.description)
body = body.replace('${creator}',obj.creator)
msg = email.MIMEText.MIMEText(body.encode('UTF-8'), 'plain', 'UTF-8')
msg["From"] = sender
msg["To"] = ','.join(recipient)
msg["Subject"] = email.Header.Header(subject, 'UTF-8')
mailer = getUtility(IMailDelivery, 'todoplus')
mailer.send(sender, recipient, msg.as_string())
处理程序首先找到annotation对象并检查enabled属性的值。如果是False,该方法将直接返回而不做其他任何事情;如果是True,我们将获取属性值并借助 Python 电子邮件模块来组合消息。我们在这里使用的一个简单技巧是允许消息通过简单的字符串替换插入新创建项目的标题、创建者和描述。查看本章“创建表单”部分的截图,看看这是如何工作的。
zope.sendmail包通过名为IMailDelivery的接口注册了邮件工具,因此我们导入这个接口并使用zope.component中的getUtility函数来找到它,最终发送电子邮件。
显示额外的导航链接
到目前为止,待办事项应用程序有“用户管理”和“皮肤选择器”表单,但它们没有显示在网站导航中。我们在原始应用程序中为此设置了一个视图管理器,所以我们只需将视图小部件注册到该管理器并添加一个模板,就完成了。这将演示在 Grok 中插入任意页面片段是多么容易,前提是原始视图计划了这一点。
让我们添加两个视图小部件,一个用于常规用户选项,另一个用于仅管理员选项:
class ManagerOptions(grok.Viewlet):
grok.viewletmanager(ExtraNav)
grok.context(Interface)
grok.require('zope.ManageApplication')
class UserOptions(grok.Viewlet):
grok.viewletmanager(ExtraNav)
grok.context(Interface)
grok.require('todo.changeitems')
这两个视图小部件都注册了来自待办事项应用程序的ExtraNav视图小部件管理器,但一个只需要todo.changeitems权限,而另一个需要zope.ManageApplication权限,这通常分配给网站管理员。页面模板只包含几个链接。首先,在manageroptions.pt内部,我们有:
| <a tal:attributes="href python:view.application_url('userlist')">Manage users</a>
确实如此,只需一个指向userlist视图的链接,视图管理器会将其插入正确的位置。
另一个模板useroptions.pt几乎同样简单:
| <a tal:attributes="href python:view.application_url('skin_chooser')">Choose skin</a>
| <a tal:attributes="href python:view.application_url('set_notifications')"> Set notifications</a>
这就是所有需要的。你可以在本章前两个截图的导航部分看到结果。这个功能的好处是,其他第三方包也可以添加导航链接,并且它们将与现有的导航选项透明地集成,甚至无需了解这些其他包的存在。
摘要
本章讨论了 Zope 组件架构背后的主要概念,并展示了如何使用 ZCA 的一些模式来扩展我们的应用程序。最重要的是,我们展示了如何在不接触其代码的情况下扩展一个包。在下一章中,我们将看到 Grok 如何使用一个名为 Martian 的库来实现敏捷配置,并学习如何从中受益,用于我们的工作。
第十二章。Grokkers,火星和敏捷配置
敏捷性在 Grok 中非常重要,并且为了使应用程序运行而需要做更少的配置是敏捷的关键。在 Grok 术语中,grokker是一段代码,允许开发者通过在代码中声明而不是使用 ZCML 配置文件来使用框架功能。在本章中,我们介绍了用于创建 grokkers 的库火星,并演示了如何为我们自己的应用程序创建一个简单的 grokker。我们将涵盖的主题包括:
-
什么是火星
-
为什么需要它以及 Grok 如何使用它
-
什么是 grokker
-
如何创建一个 grokker
敏捷配置
正如我们在本书一开始所解释的,当我们不使用 Grok 而使用 Zope Toolkit 时,我们必须使用 ZCML 来配置一切。这意味着我们必须为代码中的每个视图、视图组件、适配器、订阅者和注释添加 ZCML 指令。这里有很多标记,所有这些都必须与代码一起维护。当我们想到这一点时,敏捷性并不是首先想到的。
Grok 的开发者从经验中知道,Zope Toolkit 和Zope 组件架构(ZCA)使开发者能够创建高级面向对象系统。这种力量是以新开发者进入门槛提高为代价的。
另一个证明是 Zope Toolkit 采用问题的是,它对显式配置的强调。ZCML 允许开发者在其应用程序配置中非常明确和灵活,但它需要为配置单独的文件,并且需要更多的时间来创建、维护和理解。你只需要更多的时间来理解一个应用程序,因为你必须查看不同的代码片段,然后查阅 ZCML 文件以了解它们是如何相互关联的。
Grok 被设计成这样的方式,如果开发者在其代码中遵循某些约定,则不需要配置文件。相反,Grok 分析 Python 代码以使用这些约定,然后“理解”它们。幕后,一切连接正如如果配置是用 ZCML 编写的,但开发者甚至不需要考虑这一点。
由于这个过程被称为“理解”,因此 Grok 应用程序的代码干净且统一。整个配置都在代码中,以指令和组件的形式存在,因此更容易遵循,开发起来更有趣。
Grok 确实比单独的 Zope Toolkit 更敏捷,但它不是它的子集或“简化版”。Zope Toolkit 的所有功能都对开发者可用。甚至在需要时,可以使用 ZCML 进行显式配置,就像我们在上一章配置 SMTP 邮件器时所看到的那样。
火星库
Grok 中执行代码 'grokking' 的部分已被提取到一个名为 Martian 的独立库中。这个库提供了一个框架,允许以 Python 代码的形式表达配置,形式为声明性语句。想法是,通常,可以检查代码的结构,并且大多数它所需的配置步骤都可以从这个结构中推断出来。火星人通过使用指令来注释代码,使配置要求更加明显。
Martian 被发布为一个独立库,因为尽管它是 Grok 的关键部分,但它可以为任何类型的框架添加声明性配置。例如,repoze.bfg(bfg.repoze.org),一个基于 Zope 概念的最小化 Web 框架,使用 Martian 可选地允许在没有 ZCML 的情况下进行视图配置。
在程序启动时,火星人读取模块中的 Python 代码并分析所有类,以查看它们是否属于一个 'grokked' 基类(或其子类)。如果是,火星人将从类注册信息中检索信息以及其中可能包含的任何指令。然后,这些信息被用于在 ZCA 注册表中执行组件注册,这与 ZCML 机制类似。这个过程被称为 'grokking',正如你所见,它允许在框架内快速注册插件。Grokkers 允许我们再次在同一个句子中写出 "agility" 和 "Zope Toolkit",而无需带有讽刺意味。
理解 grokkers
Grokker 是一个包含要 grokked 的基类、一系列配置该类的指令以及使用 Martian 执行注册过程的实际代码的包。
让我们看看一个常规的 Grok 视图定义:
class AddUser(grok.View):
grok.context(Interface)
grok.template('master')
在此代码中,grok.View 是一个已 grokked 的类,这意味着在程序启动时的 "Grok 时间" 来临时,它将被火星人找到,'grokked' 并注册到 ZCA。grok.context 和 grok.template 声明是该类可用的配置指令。实际的 'grokking' 是通过与已 grokked 类关联的一段代码来完成的,该代码将一个命名适配器注册到 ZCA 注册表中,该适配器是通过 grok.context 指令传入的接口。注册是通过使用类名来命名视图,以及将作为 grok.template 指令参数传递的任何字符串值来命名相关模板来完成的。
这就是 grokking 的全部意义,所以如果我们有三个必需的部分,我们就可以轻松地制作出自己的 grokkers。
已 grokked 的类
任何类都可以被 grokked;没有特殊要求。这使得开发者更容易开始,并且与它们一起工作要少得多。想象一下,我们有一些 Mailer 类想要 grok。它可以像这样简单:
class Mailer(object):
pass
当然,它可以像需要的那样复杂,但重点是它不需要那么复杂。
指令
一旦我们有一个想要解析的类,我们就定义可能需要的指令来配置它。再次强调,这里没有强制要求。我们可能不需要指令就能完成配置,但大多数情况下我们可能需要几个指令。
class hostname(martian.Directive):
scope = CLASS
default = 'localhost'
class port(martian.Directive):
scope = CLASS
default = 25
指令确实需要继承自martian.Directive子类。此外,它们至少需要指定一个作用域,可能还需要一个默认值。在这里,我们定义了两个指令hostname和port,这些指令将被用来配置邮件发送器。
类 grokker
我们 grokker 的最后一部分是执行实际注册的部分,它以继承自martian.ClassGrokker的类的形式出现:
class MailGrokker(martian.ClassGrokker):
martian.component(Mailer)
martian.directive(hostname)
martian.directive(port)
def execute(self, class_, hostname, port, **kw):
register_mailer(class_, hostname, port)
grokker 类将解析的类与其指令连接起来,并执行解析或注册。它必须包含一个execute方法,该方法将负责任何配置操作。
martian.component指令将 grokker 与要解析的类(在这种情况下为Mailer)连接起来。martian.directive指令用于将我们之前定义的各种指令与这个 grokker 关联起来。
最后,execute方法接收基类和代码中声明的指令值,并执行最终的注册。请注意,register_mailer方法(实际上在这里执行工作)在前面代码中不存在,因为我们只想展示 grokker 的结构。
你将永远需要的唯一 ZCML
一旦 grokker 可用,它必须在启动时由 Grok 注册机制进行配置以初始化和使用。为此,我们必须在名为meta.zcml的文件中使用一些 ZCML:
<configure >
<grok:grok package=".meta" />
</configure>
如果我们的MailGrokker类位于meta.py文件中,它将由 Grok 机制初始化。
为 zope.sendmail 配置创建我们自己的 grokker
现在我们知道了 grokker 的结构,让我们创建一个用于 SMTP 邮件发送器的 grokker,这个 SMTP 邮件发送器是我们之前在第十一章关于添加电子邮件通知的部分所使用的。
我们想要的是一个简单的MailGrokker类声明,包含hostname、port、username、password和delivery type指令。这将使我们能够避免使用 ZCML 来配置邮件发送器,正如我们在上一节所要求的那样。
我们需要创建一个新的包,这样我们的 grokker 就可以独立于todo_plus代码,并且可以在其他地方自由使用。
创建包
我们在第十一章的创建新包部分执行了这些步骤。如果您有任何疑问,请参阅该部分以获取详细信息。
要创建包,请进入我们主要todo应用的src目录,并输入:
$ ../bin/paster create -t basic_package mailgrokker
这将创建一个mailgrokker目录。现在,导航到这个目录,并将grok、martian以及zope.sendmail包添加到install_requires声明中:
install_requires=[
'grok',
'martian',
'zope.sendmail',
],
这样,我们确保在安装mailgrokker后,所需的包是存在的。我们还必须将我们的新mailgrokker包添加到项目顶层的主buildout.cfg文件中,紧接在todo_plus下面。在 egg 和 develop 部分都这样做。
编写我们的解析器
首先,我们将添加一个configure.zcml文件,它就像todo_plus包中的那个一样。实际上,我们可以从那里复制它:
<configure >
<include package="grok" />
<includeDependencies package="." />
<grok:grok package="." />
</configure>
我们解析的类将位于component.py文件中。在这里,我们只使用一个基类,但一个解析器项目可以包含多个基类,并且按照惯例,它们在这里定义:
import grok
class Mailer(object):
grok.baseclass()
这是一个没有方法的简单基类。使用grok.baseclass指令将其标记为基类,尽管这不是强制性的。
配置指令存储在一个名为directives.py的文件中。
import martian
class name(martian.Directive):
scope = martian.CLASS
store = martian.ONCE
class hostname(martian.Directive):
scope = martian.CLASS
store = martian.ONCE
default = 'localhost'
class port(martian.Directive):
scope = martian.CLASS
store = martian.ONCE
default = '25'
class username(martian.Directive):
scope = martian.CLASS
store = martian.ONCE
default = None
class password(martian.Directive):
scope = martian.CLASS
store = martian.ONCE
default = None
class delivery(martian.Directive):
scope = martian.CLASS
store = martian.ONCE
default = 'queued'
class permission(martian.Directive):
scope = martian.CLASS
store = martian.ONCE
default = 'zope.Public'
这非常直接。我们只需定义我们需要的所有指令,然后添加一个martian.CLASS作用域。每个指令都有自己的默认值,这取决于其目的。通过查看代码,每个指令的意图应该是显而易见的,除了delivery指令。这个指令是必需的,因为zope.sendmail包括两种不同的交付机制direct和queued。
接下来是主要的解析器类,我们将将其添加到meta.py文件中。首先,是import语句。注意这里我们导入了martian以及GrokError,这是一个异常,如果解析失败,我们可以抛出它。我们还导入了我们将从zope.sendmail库中使用的所有内容。
import martian
from martian.error import GrokError
from zope.component import getGlobalSiteManager
from zope.sendmail.delivery import QueuedMailDelivery, DirectMailDelivery
from zope.sendmail.delivery import QueueProcessorThread
from zope.sendmail.interfaces import IMailer, IMailDelivery
from zope.sendmail.mailer import SMTPMailer
from zope.sendmail.zcml import _assertPermission
from mailgrokker.components import Mailer
from mailgrokker.directives import name, hostname, port, username, password, delivery, permission
register_mailer函数创建一个zope.sendmail SMTP 邮件发送对象,并将其注册为名为IMailer的命名实用工具,名称来自name指令。注意使用getGlobalSiteManager函数,这实际上是一个获取组件注册表的华丽名称。我们使用注册表的registerUtility函数添加我们新创建的SMTPMailer实例。
def register_mailer(class_, name, hostname, port, username, password, delivery, permission):
sm = getGlobalSiteManager()
mailer = SMTPMailer(hostname, port, username, password)
sm.registerUtility(mailer, IMailer, name)
继续使用register_mailer代码,我们现在使用传递的参数选定的交付机制来决定是否初始化一个DirectMailDelivery实例或一个QueuedMailDelivery实例。无论哪种方式,我们都将结果注册为一个实用工具。
在queue交付机制的情况下,一个将负责从主应用程序代码中单独发送电子邮件的线程被启动。
if delivery=='direct':
mail_delivery = DirectMailDelivery(mailer)
_assertPermission(permission, IMailDelivery, mail_delivery)
sm.registerUtility(mail_delivery, IMailDelivery, name)
elif delivery=='queued':
mail_delivery = QueuedMailDelivery(name)
_assertPermission(permission, IMailDelivery, mail_delivery)
sm.registerUtility(mail_delivery, IMailDelivery, name)
thread = QueueProcessorThread()
thread.setMailer(mailer)
thread.setQueuePath(name)
thread.start()
else:
raise GrokError("Available delivery methods are 'direct' and 'queued'. Delivery method %s is not defined.",class_)
MailGrokker类声明了添加到directives模块的所有指令,并将其与它将要解析的Mailer类关联起来。然后它定义了execute方法,该方法将调用register_mailer函数以执行所需的zope.sendmail注册。
class MailGrokker(martian.ClassGrokker):
martian.component(Mailer)
martian.directive(name)
martian.directive(hostname)
martian.directive(port)
martian.directive(username)
martian.directive(password)
martian.directive(delivery)
martian.directive(permission)
def execute(self, class_, config, name, hostname, port, username, password, delivery, permission, **kwds):
config.action(
discriminator = ('utility', IMailer, name),
callable = register_mailer,
args = (class_, name, hostname, port, username, password, delivery, permission),
order = 5
)
return True
上述代码与我们之前展示的代码的唯一区别是,我们不是直接调用register_mailer函数,而是将其包裹在config.action对象中。这样做是为了让 Grok 在代码加载后以任意顺序执行注册,而不是在初始化每个包时执行。这防止了任何配置冲突,并允许我们具体指定注册条件。
例如,discriminator参数,可能是空的,在这种情况下,是一个包含字符串utility、接口IMailer和name指令值的元组。如果任何其他 grokker 包使用这个相同的判别器,Grok 将发出冲突错误条件。
action的order参数用于指定调用动作的顺序,尽管这里只是添加了用于演示的目的。callable参数是执行注册的函数,而args参数包含传递给它的参数。
我们现在在meta模块中有了我们的 grokker,需要告诉 Grok 在这里找到它,我们通过添加之前讨论过的小的meta.zcml文件来完成:
<configure >
<grok:grok package=".meta" />
</configure>
最后,编辑现有的__init__.py文件,该文件位于src/mailgrokker/mailgrokker目录中,使其看起来像以下代码:
from mailgrokker.directives import name, hostname, port, username, password, del ivery, permission
from mailgrokker.components import Mailer
这将允许我们通过导入主要的mailgrokker模块来简单地使用指令,就像grok.*指令那样工作。
使用 mailgrokker
现在我们已经完成了我们的 grokker,唯一缺少的是展示如何在应用程序中使用它。我们将将其添加到todo_plus包中。在该文件的底部插入以下行:
import mailgrokker
class TodoMailer(mailgrokker.Mailer):
mailgrokker.name('todoplus')
mailgrokker.hostname('smtp.example.com')
mailgrokker.username('cguardia')
mailgrokker.password('password')
显然,你应该用你的smtp服务器的实际值替换这里显示的值。你可能还想删除我们在之前的configure.zcml文件中放置的邮件发送器配置。
完成。我们现在创建了一个小的 grokker 包,可以在我们的任何应用程序中使用,以便轻松配置电子邮件提交。
摘要
在本章中,我们学习了关于 Martian 库以及它是如何使 Grok 成为一个敏捷框架的。我们现在准备好讨论如何调试我们的应用程序。
第十三章:测试和调试
在整本书中,我们一直在讨论 Grok 如何提供一种敏捷的方式来与 Zope 协同工作。然而,到目前为止,我们还没有忽视一个被认为在敏捷项目中最重要的活动:测试。
Grok 提供了一些测试工具,实际上,由grokproject(即我们一直在扩展的那个)创建的项目包括一个功能测试套件。在本章中,我们将讨论测试,然后为我们的应用程序迄今为止的功能编写一些测试。
测试帮助我们避免错误,但当然不能完全消除它们。有时我们不得不深入代码以找出其中的问题。在这种情况下,一套好的调试辅助工具变得非常有价值。我们将看到有几种调试 Grok 应用程序的方法,并尝试其中的一些。
我们将涵盖的一些内容包括:
-
测试的需求
-
Grok 中的测试
-
扩展
grokproject提供的功能测试套件 -
其他类型的测试
-
调试工具
测试
理解测试不应被视为事后之想是很重要的。只是在像这样一本大量关注测试的书中,在阅读的初期阶段,可能更难理解解释和代码。
如前所述,敏捷方法非常重视测试。实际上,甚至有一种名为测试驱动开发(TDD)的方法,它不仅鼓励为我们的代码编写测试,还鼓励在编写任何代码之前编写测试。
有各种类型的测试,但在这里我们只简要描述两种:
-
单元测试
-
集成或功能测试
单元测试
单元测试的想法是将程序分解为其组成部分,并单独测试每一个。每个方法或函数调用都是单独测试的,以确保它返回预期的结果并正确处理所有可能的输入。
一个拥有覆盖大部分代码行数的单元测试的应用程序,允许其开发者不断在更改后运行测试,并确保代码的修改不会破坏现有的功能。
功能测试
功能测试关注的是应用程序的整体行为。在一个 Web 应用程序中,这意味着它如何响应浏览器请求,以及它是否为给定的调用返回预期的 HTML。
理想情况下,客户自己参与定义这些测试,通常是通过明确的功能需求或验收标准。客户的要求越正式,定义适当的函数测试就越容易。
Grok 中的测试
Grok 强烈鼓励使用这两种测试,实际上,每个项目都自动配置了一个强大的测试工具。在 Grok 起源的 Zope 世界中,一种称为“doctest”的测试被赋予了很高的价值,因此 Grok 附带了一个此类测试的示例测试套件。
Doctests
doctest是一种以文本文件编写的测试,其中包含代码行和解释代码正在做什么的说明。代码以模拟 Python 解释器会话的方式编写。由于测试会锻炼代码的大部分内容(理想情况下 100%),它们通常提供了一种很好的方法来了解应用程序做什么以及如何做。因此,如果一个应用程序没有书面文档,其测试将是了解其功能的下一个明显的方法。Doctests 通过允许开发者在文本文件中精确地解释每个测试正在做什么,进一步发展了这个想法。
Doctests 在功能测试中特别有用,因为它更有意义地记录了程序的高级操作。另一方面,单元测试则预期逐步评估程序,为每一小段代码编写文本说明可能会很繁琐。
Doctests 的一个可能的缺点是它们可能会让开发者认为他不需要为他的项目提供其他文档。在几乎所有情况下,这都不是真的。记录一个应用程序或包可以立即使其更易于访问和有用,因此强烈建议不要将 doctests 用作良好文档的替代品。我们将在本章的“查看测试代码”部分展示使用 doctests 的示例。
Grok 项目的默认测试设置
如前所述,使用grokproject工具启动的 Grok 项目默认已经包含了一个简单的功能测试套件。让我们详细地来考察一下。
测试配置
默认测试配置会查找名称中包含“tests”一词的包或模块,并尝试在其中运行测试。对于功能测试,任何以.txt或.rst结尾的文件都被认为是测试文件。
对于需要模拟浏览器的功能测试,需要一个特殊的配置来告诉 Grok 除了 Grok 基础设施外还需要初始化哪些包(这些通常是正在工作的包)。package目录中的ftesting.zcml文件包含这个配置。此文件还包括一些用户定义,这些定义被某些测试用来检查特定于某个角色(如经理)的功能。
测试文件
除了已经提到的ftesting.zcml文件外,在同一目录下,grokproject还添加了一个tests.py文件,该文件基本上加载 ZCML 声明并注册包中的所有测试。
默认项目文件中包含的实际测试包含在app.txt文件中。这些是通过加载整个 Grok 环境和模拟浏览器来执行功能测试的 doctests。我们很快就会查看文件的内容,但首先让我们运行测试。
运行测试
作为项目构建过程的一部分,当你创建一个新项目时,bin目录中会包含一个名为test的脚本。这是测试运行器,不带参数调用它将找到并执行配置中包含的包中的所有测试。
到目前为止,我们还没有添加任何测试,所以如果我们在我们项目的目录中输入bin/test,我们会看到与在新项目中执行此操作大致相同的结果:
$ bin/test
Running tests at level 1
Running todo.FunctionalLayer tests:
Set up
in 12.319 seconds.
Running:
...2009-09-30 15:00:47,490 INFO sqlalchemy.engine.base.Engine.0x...782c PRAGMA table_info("users")
2009-09-30 15:00:47,490 INFO sqlalchemy.engine.base.Engine.0x...782c ()
Ran 3 tests with 0 failures and 0 errors in 0.465 seconds.
Tearing down left over layers:
Tear down todo.FunctionalLayer ... not supported
我们输出与新建 Grok 包输出的唯一区别在于sqlalchemy行。当然,输出中最重要的部分是倒数第二行,它显示了运行了多少个测试以及是否有失败或错误。失败意味着测试没有通过,这意味着代码没有按照预期执行,需要检查。错误表示代码在某个点意外崩溃,测试甚至无法执行,因此有必要在担心测试之前找到错误并纠正它。
测试运行器
测试运行器程序会寻找包含测试的模块。测试可以是三种不同类型之一:Python 测试、简单的 doctests 和全功能的 doctests。为了让测试运行器知道哪个测试文件包含哪种类型的测试,文件顶部会放置一个类似于以下的注释:
Do a Python test on the app.
:unittestt:
在这种情况下,我们将使用 Python 单元测试层来运行测试。我们还将使用 doctest,当我们学习如何编写 doctests 时。
测试运行器随后会找到所有的测试模块,并在相应的层中运行它们。尽管单元测试在常规开发中被认为非常重要,但我们可能会发现对于 Grok Web 应用来说,功能测试更为必要,因为我们通常会测试视图和表单,这需要加载完整的 Zope/Grok 堆栈才能工作。这就是为什么我们在默认设置中只找到功能性的 doctests 的原因。
测试层
测试层是一种特定的测试设置,用于区分执行的测试。默认情况下,测试运行器处理的三种测试类型中,每种类型都有一个测试层。可以运行一个测试层而不运行其他层。也可以创建新的测试层,以便将需要特定设置的测试聚集在一起。
调用测试运行器
如上所示,运行bin/test将使用默认选项启动测试运行器。也可以指定多个选项,其中最重要的选项如下总结。在下表中,命令行选项显示在左侧。大多数选项可以使用短形式(一个连字符)或长形式(两个连字符)来表示。每个选项的参数以大写形式显示。
| 选项 | 描述 |
|---|---|
-s 包, --package=包, --dir=包 |
在指定的包目录中搜索测试。这可以指定多次,以在源树的不同部分运行测试。例如,在重构接口时,你不想看到其他包中测试设置的破坏方式。你只想运行接口测试。包以点分隔的名称提供。为了与旧测试运行器兼容,包名称中的前后斜杠被转换为点。 (在包的特殊情况下,这些包分布在多个目录中,只有测试搜索路径内的目录将被搜索。) |
-m 模块, --module=模块 |
指定一个测试模块过滤器作为正则表达式。这是一个区分大小写的正则表达式,用于搜索(而非匹配)模式,以限制搜索哪些测试模块。正则表达式将与点分隔的模块名称进行匹配检查。在 Python 正则表达式符号的扩展中,一个前导的"!"将被移除,并导致剩余正则表达式的意义被否定(因此"!bc"匹配任何不匹配"bc"的字符串,反之亦然)。此选项可以指定多个测试模块过滤器。匹配任何测试过滤器的测试模块将被搜索。如果没有指定测试模块过滤器,则使用所有测试模块。 |
-t 测试, --test=测试 |
指定一个测试过滤器作为正则表达式。这是一个区分大小写的正则表达式,用于搜索(而非匹配)模式,以限制运行哪些测试。在 Python 正则表达式符号的扩展中,一个前导的"!"将被移除,并导致剩余正则表达式的意义被否定(因此"!bc"匹配任何不匹配"bc"的字符串,反之亦然)。此选项可以指定多个测试过滤器。匹配任何测试过滤器的测试将被包含。如果没有指定测试过滤器,则执行所有测试。 |
--layer=LAYER |
指定要运行的测试层。此选项可以多次提供,以便指定多个层。如果没有指定,将执行所有层。运行脚本通常为此选项提供默认值。层是用于搜索模式中对象点名称的正则表达式。在 Python 正则表达式记法的一个扩展中,一个前导的 "!" 被移除,导致剩余正则表达式的意义被否定(因此 "!bc" 匹配任何不匹配 "bc" 的字符串,反之亦然)。名为 'unit' 的层保留用于单元测试;然而,请注意 unit 和 non-unit 选项。 |
-u, --unit |
仅执行单元测试,忽略任何层选项。 |
-f, --non-unit |
执行除单元测试之外的其他测试。 |
-v, --verbose |
使输出更详细。 |
-q, --quiet |
通过覆盖任何详细程度选项提供最小输出。 |
查看测试代码
让我们看看 Grok 项目中的三个默认测试文件,看看每个文件的作用。
ftesting.zcml
如我们之前所解释的,ftesting.zcml 是测试运行器的配置文件。其主要目的是帮助我们设置带有用户的测试实例,以便我们可以根据需要测试不同的角色。
<configure
i18n_domain="todo"
package="todo"
>
<include package="todo" />
<include package="todo_plus" />
<!-- Typical functional testing security setup -->
<securityPolicy
component="zope.securitypolicy.zopepolicy.ZopeSecurityPolicy"
/>
<unauthenticatedPrincipal
id="zope.anybody"
title="Unauthenticated User"
/>
<grant
permission="zope.View"
principal="zope.anybody"
/>
<principal
id="zope.mgr"
title="Manager"
login="mgr"
password="mgrpw"
/>
<role id="zope.Manager" title="Site Manager" />
<grantAll role="zope.Manager" />
<grant role="zope.Manager" principal="zope.mgr" />
如前述代码所示,配置简单地包含了一个安全策略,包括用户和角色,以及实例应该加载的包,以及除了常规 Grok 基础设施之外的内容。如果我们运行需要经过身份验证的用户才能工作的任何测试,我们将使用这些特殊用户。
文件顶部的包含确保在运行测试之前执行我们应用程序所需的整个 Zope 组件架构设置。
tests.py
默认测试模块非常简单。它定义了功能层并注册了我们包的测试:
import os.path
import z3c.testsetup
import todo
from zope.app.testing.functional import ZCMLLayer
ftesting_zcml = os.path.join(
os.path.dirname(todo.__file__), 'ftesting.zcml')
FunctionalLayer = ZCMLLayer(ftesting_zcml, __name__, 'FunctionalLayer', allow_teardown=True)
test_suite = z3c.testsetup.register_all_tests('todo')
在导入之后,第一行获取 ftesting.zcml 文件的路径,然后将其传递给层定义方法 ZCMLLayer。模块中的最后一行告诉测试运行器找到并注册包中的所有测试。
这将足够满足我们本章的测试需求,但如果我们需要为我们的应用程序创建另一个非 Grok 包,我们就需要在其上添加类似于最后一行的行,以便测试运行器可以找到所有测试。这基本上是样板代码,因为只需要更改包名。
app.txt
我们最终来到了整个配置的原因所在,即测试运行器将要执行的实际测试。默认情况下,测试被包含在 app.txt 文件中:
Do a functional doctest test on the app.
========================================
:doctest:
:layer: todo.tests.FunctionalLayer
Let's first create an instance of Todo at the top level:
>>> from todo.app import Todo
>>> root = getRootFolder()
>>> root['app'] = Todo()
Run tests in the testbrowser
----------------------------
The zope.testbrowser.browser module exposes a Browser class that
simulates a web browser similar to Mozilla Firefox or IE. We use that to test how our application behaves in a browser. For more
information, see http://pypi.python.org/pypi/zope.testbrowser.
Create a browser and visit the instance you just created:
>>> from zope.testbrowser.testing import Browser
>>> browser = Browser()
>>> browser.open('http://localhost/app')
Check some basic information about the page you visit:
>>> browser.url
'http://localhost/app/@@login?camefrom=%2Fapp%2F%40%40index'
>>> browser.headers.get('Status').upper()
'200 OK'
文本文件有一个标题,紧接着是一个 :doctest: 声明,这是一个告诉测试运行器这些测试需要加载一个功能层来执行声明的声明。然后是一个 :layer: 声明,它是一个指向我们在 tests.py 中之前定义的层的路径。之后是测试代码。以三个括号开头的行代表被测试的 Python 代码。其他任何内容都是注释。
当使用 Python 解释器时,一行代码可能会返回一个值,在这种情况下,预期的返回值必须立即写在那行代码下面。这个预期值将与测试代码的实际返回值进行比较,如果值不匹配,将报告失败。同样,如果一行代码后面跟着一个空行,当代码执行并返回结果时,也会产生失败,因为在这种情况下,预期返回值被认为是 None。
例如,在下面的 Python doctest 的最后一行中,表达式 browser.headers.get('Status').upper() 预期返回值 200 OK。如果返回任何其他值,测试将失败。
添加我们自己的测试
现在,让我们添加一些针对我们应用程序的特定功能测试。为此,我们需要模拟一个浏览器。zope.testbrowser 包包含一个浏览器模拟器。我们可以通过使用 browser.open 将任何有效的 URL 传递给这个浏览器,它将像浏览器一样向我们的应用程序发送请求。然后,我们的应用程序的响应将作为 browser.contents 可用,这样我们就可以在上面执行我们的测试比较。
浏览器类
在编写我们的测试之前,查看我们的 testbrowser 能做什么将非常有用。当然,任何依赖于 JavaScript 的功能在这里都不会工作,但除此之外,我们可以以非常直接的方式与链接甚至表单进行交互。以下是 Browser 类提供的主要功能:
| 初始化 | >>> from zope.testbrowser.testing import Browser>>> browser = Browser() |
|---|---|
| 页面内容 | >>> print browser.contentsSimple Page>>> 'Simple Page' in browser.contentsTrue |
| 头部信息 | >>> print browser.headersStatus: 200 OKContent-Length: 123Content-Type: text/html;charset=utf-8X-Powered-By: Zope (www.zope.org), Python (www.python.org)>>> browser.headers['content-type']'text/html;charset=utf-8' |
| Cookies | >>> browser.cookies['foo']'bar'>>> browser.cookies.keys()['foo']>>> browser.cookies.values()['bar']>>> browser.cookies.items()[('foo', 'bar')]>>> 'foo' in browser.cookiesTrue>>> browser.cookies['sha'] = 'zam' |
| 链接 | >>> link = browser.getLink('Link Text')>>> link<Link text='Link Text'url='http://localhost/@@/testbrowser/navigate.html?message=By+Link+Text'>>>> link.text'Link Text'>>> link.tag # 链接也可以是图像映射.'a'>>> link.url # 它已被标准化'http://localhost/@@/testbrowser/navigate.html?message=By+Link+Text'>>> link.attrs{'href': 'navigate.html?message=By+Link+Text'}>>> link.click()>>> browser.url'http://localhost/@@/testbrowser/navigate.html?message=By+Link+Text' |
| 表单控件 | >>> control = browser.getControl('Text Control')>>> control |
现在我们知道了我们可以做什么,让我们尝试编写一些测试。
我们的第一批待办应用测试
理想情况下,我们应该在每次向我们的应用程序添加新功能时,在app.txt文件中添加几个 doctests。我们已经讨论了我们为什么没有这样做的原因,但让我们弥补一些失去的地盘。至少,我们将对 doctests 的工作方式有一个感觉。
我们将把新的测试添加到现有的app.txt文件中。我们看到的最后一个测试将我们留在了待办实例的 URL 上。我们没有登录,所以如果我们打印浏览器内容,我们会得到登录页面。让我们为这个情况添加一个测试:
Since we haven't logged in, we can't see the application. The login page appears:
>>> 'Username' in browser.contents
True
>>> 'Password' in browser.contents
True
如我们之前提到的,当使用testbrowser访问 URL 时,页面的整个 HTML 内容都存储在browser.contents中。现在我们知道我们的登录页面有一个用户名字段和一个密码字段,所以我们只需使用几个in表达式来检查这些字段是否评估为True。如果是,这意味着浏览器实际上正在查看登录页面。
让我们为登录添加一个测试。当我们开始在测试中启动应用程序时,用户数据库是空的,因此,最经济的登录方式是使用基本认证。这可以通过更改请求头轻松完成:
让我们使用在ftesting.zcml中定义的管理员用户登录。为了使事情简单,让我们使用基本认证头来登录:
>>> browser.addHeader('Authorization', 'Basic mgr:mgrpw')
>>> browser.open('http://localhost/app')
>>> 'Logged in as Manager' in browser.contents
True
就这些。我们只需添加标题,“重新加载”主页,我们应该就登录了。我们通过寻找登录为消息来验证它,我们知道在成功登录后必须在那里。
一旦我们登录,我们就可以最终正确地测试我们的应用程序了。让我们先添加一个项目:
我们现在已经登录了。让我们创建一个项目:
>>> browser.getLink('Create a new project').click()
>>> browser.getControl(name='form.title').value='a project'
>>> browser.getControl(name='form.description').value='The description.'
>>> browser.getControl('Add project').click()
>>> browser.url
'http://localhost/app/0'
>>> 'Create new list' in browser.contents
True
首先,我们在主页上找到将带我们转到 '添加表单' 项目的链接。这可以通过 getLink 方法和链接文本轻松完成。我们点击链接,然后应该有一个可以填写的表单。然后我们使用 getControl 通过名称找到每个字段并更改其值。最后,我们通过获取提交按钮控件并点击它来提交表单。结果是项目被创建,并且我们被重定向到其主视图。我们可以通过比较 browser url 与在这种情况下我们预期的 URL 来确认这一点。
向项目中添加列表同样简单。我们获取表单控件,分配一些值,然后点击提交按钮。列表和添加新项到其中的链接应该出现在浏览器内容中:
我们已经添加了一个项目。现在,我们将向其中添加一个列表。如果我们成功,我们将看到一个用于添加列表新项的链接:
>>> browser.getControl(name='title').value='a list'
>>> browser.getControl(name='description').value='The list description.'
>>> browser.getControl(name='new_list').click()
>>> 'New item' in browser.contents
True
好的。让我们看看到目前为止我们做得怎么样:
$ bin/testRunning tests at level 1
Running todo.FunctionalLayer tests:
Set up
in 3.087 seconds.
Running:
.......2009-09-30 21:35:44,585 INFO sqlalchemy.engine.base.Engine.0x...69ec PRAGMA table_info("users")
2009-09-30 21:35:44,585 INFO sqlalchemy.engine.base.Engine.0x...69ec ()
Ran 7 tests with 0 failures and 0 errors in 0.428 seconds.
Tearing down left over layers:
Tear down todo.FunctionalLayer ... not supported
还不错。我们现在比开始时多了四个工作测试。
注意,测试浏览器优雅地处理 HTTP 错误,返回一个类似于真实浏览器在遇到错误时返回的字符串。例如,看看以下测试:
>>> browser.open('http://localhost/invalid')
Traceback (most recent call last):
...
HTTPError: HTTP Error 404: Not Found
这是默认行为,因为这是真实浏览器的行为方式,但有时,当我们调试时,查看由我们的应用程序引起的原始异常会更好。在这种情况下,我们可以让浏览器停止自动处理错误并抛出原始异常,这样我们就可以处理它们。这是通过将 browser.handleErrors 属性设置为 False 来实现的:
>>> browser.handleErrors = False
>>> browser.open('http://localhost/invalid')
Traceback (most recent call last):
...
NotFound: Object: <zope.site.folder.Folder object at ...>,
name: u'invalid'
添加单元测试
除了功能测试之外,我们还可以创建纯 Python 测试用例,测试运行器可以找到它们。而功能测试覆盖应用程序行为,单元测试则关注程序的正确性。理想情况下,应用程序中的每个 Python 方法都应该经过测试。
单元测试层不会加载 Grok 基础设施,因此测试不应该理所当然地接受它所附带的一切,而只接受基本的 Python 行为。
要添加我们的单元测试,我们将创建一个名为 unit_tests.py 的模块。记住,为了让测试运行器找到我们的测试模块,它们的名称必须以 'tests' 结尾。以下是我们将放入此文件的内容:
"""
Do a Python test on the app.
:unittest:
"""
import unittest
from todo.app import Todo
class InitializationTest(unittest.TestCase):
todoapp = None
def setUp(self):
self.todoapp = Todo()
def test_title_set(self):
self.assertEqual(self.todoapp.title,u'To-do list manager')
def test_next_id_set(self):
self.assertEqual(self.todoapp.next_id,0)
顶部的 :unittest: 注释非常重要。没有它,测试运行器将不知道你的测试应该在哪个层执行,它将简单地忽略它们。
单元测试由测试用例组成,理论上,每个测试用例都应该包含与应用程序功能特定区域相关的几个测试。测试用例使用 Python 的 unittest 模块中的 TestCase 类。在这些测试中,我们定义了一个包含两个非常简单的测试的单个测试用例。
我们在这里不深入细节。只需注意,测试用例可以包括一个setUp方法和一个tearDown方法,这些方法可以用来执行任何必要的公共初始化和销毁任务,以便使测试工作并干净地完成。
测试用例中的每个测试都需要在其名称中包含前缀'test',所以我们恰好有两个测试满足这个条件。这两个测试都需要Todo类的一个实例来执行,因此我们将其分配给测试用例作为类变量,并在setUp方法中创建它。这些测试非常简单,它们只是验证在实例创建时设置了默认属性值。
这两个测试都使用assertEqual方法来告诉测试运行器,如果传递的两个值不同,测试应该失败。为了看到它们在行动中的样子,我们只需再次运行bin/test命令:
$ bin/test
Running tests at level 1
Running todo.FunctionalLayer tests:
Set up
in 2.691 seconds.
Running:
.......2009-09-30 22:00:50,703 INFO sqlalchemy.engine.base.Engine.0x...684c PRAGMA table_info("users")
2009-09-30 22:00:50,703 INFO sqlalchemy.engine.base.Engine.0x...684c ()
Ran 7 tests with 0 failures and 0 errors in 0.420 seconds.
Running zope.testing.testrunner.layer.UnitTests tests:
Tear down todo.FunctionalLayer ... not supported
Running in a subprocess.
Set up zope.testing.testrunner.layer.UnitTests in 0.000 seconds.
Ran 2 tests with 0 failures and 0 errors in 0.000 seconds.
Tear down zope.testing.testrunner.layer.UnitTests in 0.000 seconds.
Total: 9 tests, 0 failures, 0 errors in 5.795 seconds
现在,功能测试层和单元测试层都包含一些测试,并且它们是依次运行的。我们可以在这些测试的每个层的末尾看到该层的总计,以及当测试运行器完成其工作时九个通过测试的总计。
扩展测试套件
当然,我们只是触及了表面,关于哪些测试应该添加到我们的应用程序中。如果我们继续添加测试,到我们完成的时候可能会有数百个测试。然而,这一章并不是进行这项工作的地方。
如前所述,如果我们边编码边添加测试,那么为应用程序的每个部分编写测试要容易得多。测试确实是一项大量工作,但拥有一个完整的测试套件对我们的应用程序来说价值巨大。尤其是当第三方可能独立使用我们的产品时,这一点更为重要。
调试
现在,我们将快速查看 Grok 提供的调试功能。即使我们有一个非常全面的测试套件,我们也有可能在我们应用程序中发现相当数量的错误。当这种情况发生时,我们需要一种快速有效的方法来检查代码的运行情况,并轻松找到问题所在。
通常,开发者会在代码中(放置在关键行)使用print语句,希望找到问题所在。尽管这通常是一个开始定位代码中的痛点的好方法,但我们往往需要某种方式逐行跟踪代码,以便真正找出问题所在。在下一节中,我们将看到如何使用 Python 调试器逐步执行代码并找到问题所在。我们还将快速看一下如何在 Grok 中执行死后调试,这涉及到在异常发生后立即跳入调试器来分析程序状态。
Grok 中的调试
对于常规调试,我们需要逐步执行代码以查看其内部情况,Python 调试器是一个出色的工具。要使用它,你只需在你希望开始调试的点添加下一行:
import pdb; pdb.set_trace()
让我们试试。打开 app.py 模块,并将 AddProjectForm 类的 add 方法(第 108 行)改为以下内容:
@grok.action('Add project')
def add(self,**data):
import pdb; pdb.set_trace()
project = Project()
project.creator = self.request.principal.title
project.creation_date = datetime.datetime.now()
project.modification_date = datetime.datetime.now()
self.applyData(project,**data)
id = str(self.context.next_id)
self.context.next_id = self.context.next_id+1
self.context[id] = project
return self.redirect(self.url(self.context[id]))
注意,我们在方法的开头调用了调试器。现在,启动实例,转到“添加项目”表单,填写它,然后提交。您不会看到新的项目视图,浏览器将停留在“添加表单”页面,并显示 等待中... 的信息。这是因为控制权已经转移到控制台,以便调试器可以操作。您的控制台将看起来像这样:
> /home/cguardia/work/virtual/grok1/todo/src/todo/app.py(109)add()
-> project = Project()
(Pdb) |
调试器现在处于活动状态并等待输入。注意,调试开始时的行号就出现在我们所在的模块路径旁边。行号之后是方法名,add()。下面显示的是将要执行的下一行代码。
调试器命令很简单。要执行当前行,点击 n:。
(Pdb) n
> /home/cguardia/work/virtual/grok1/todo/src/todo/app.py(110)add()
-> project.creator = self.request.principal.title
(Pdb)
如果您输入 h:,您可以看到可用的命令。
(Pdb) h
Documented commands (type help <topic>):
========================================
EOF break condition disable help list q step w
a bt cont down ignore n quit tbreak whatis
alias c continue enable j next r u where
args cl d exit jump p return unalias
b clear debug h l pp s up
Miscellaneous help topics:
==========================
exec pdb
Undocumented commands:
======================
retval rv
(Pdb)
列表命令 id 用于获取我们目前在代码中的鸟瞰图:
(Pdb) list
105
106 @grok.action('Add project')
107 def add(self,**data):
108 import pdb; pdb.set_trace()
109 project = Project()
110 -> project.creator = self.request.principal.title
111 project.creation_date = datetime.datetime.now()
112 project.modification_date = datetime.datetime.now()
113 self.applyData(project,**data)
114 id = str(self.context.next_id)
115 self.context.next_id = self.context.next_id+1
(Pdb)
如您所见,当前行由一个箭头标识。
在当前执行上下文中,我们可以输入对象的名称并找出它们的值:
(Pdb) project
<todo.app.Project object at 0xa0ef72c>
(Pdb) data
{'kind': 'personal', 'description': u'Nothing', 'title': u'Project about nothing'}
(Pdb)
我们当然可以继续逐行检查应用程序中的所有代码,包括 Grok 的代码,在检查过程中检查值。当我们完成审查后,我们可以点击 c 返回控制权到浏览器。此时,我们将看到项目视图。
Python 调试器非常易于使用,并且对于查找代码中的隐蔽错误非常有价值。
默认的 Ajax 调试器
另一种调试方式被称为 事后调试。在上一个章节中,我们在应用程序处于稳定和运行状态时随意地逐步执行代码。然而,很多时候,我们可能会遇到一个错误条件,这会停止程序,我们需要分析错误发生时程序的状态。这就是事后调试的内容。
我们现在将故意在我们的代码中引入一个错误。从 add 方法中删除 import pdb 行,并将之后的第 一行改为以下内容:
project = Project(**data)
Project 类的 __init__ 方法不期望这个参数,因此会引发 TypeError。重新启动实例并添加一个项目。而不是一个视图,将显示一个空白屏幕和错误信息 系统错误发生。
回想一下,到目前为止,我们一直使用 deploy.ini 文件通过以下命令启动实例:
$ bin/paster serve parts/etc/deploy.ini
要运行一个事后调试会话,我们必须使用调试配置启动实例:
$ bin/paster serve parts/etc/debug.ini
再次尝试添加一个项目。现在,屏幕上会显示完整的错误回溯,而不是简单的错误信息,如下截图所示:

关于回溯信息的一个优点是它可以展开。点击左侧的括号将显示发生错误的那行代码周围的代码行,而点击模块和行消息右侧的加号将显示额外的回溯信息。在此信息之上,您还将看到一个文本框,可以用来在当前上下文中评估表达式(参见图表)。

使用 Python 调试器进行事后调试
Ajax 调试器很棒,但如果您真的习惯了 Python 调试器,您可能会希望在使用它进行事后分析时使用 Python 调试器。这没问题;Grok 已经为此做好了准备。
为了快速测试,请编辑项目中的parts/etc/debug.ini文件,并在[filter-app:main]部分将单词ajax更改为pdb。完成时,它应该看起来像这样:
[filter-app:main]
# Change the last part from 'ajax' to 'pdb' for a post-mortem debugger
# on the console:
use = egg:z3c.evalexception#pdb # <--- change here to pdb
next = zope
现在,使用调试配置重启实例并尝试添加项目。您将不会看到 Ajax 屏幕,控制权将转移到控制台上的 Python 调试器。
请记住,我们只是修改了一个文件,当我们再次运行buildout时,该文件将被重写,所以像我们刚才做的那样,只进行快速测试,永远不要依赖于parts/etc目录中文件的更改。为了使此更改永久,请记住编辑etc/debug.ini.in文件而不是parts/etc目录中的文件。然后您需要再次运行buildout。
摘要
本章讨论了如何测试 Grok 应用程序以及为什么这样做很重要。我们还介绍了调试,并查看了一些对 Grok 开发者有用的调试工具。现在我们已经添加了测试,在下一章中,我们将看到如何部署我们的应用程序。
第十四章。部署
到目前为止,我们一直在开发环境中工作,以“前台”模式在控制台运行我们的应用程序,以便我们可以看到输出以进行调试和确认。
现在我们有一个或多或少完整的应用程序,我们可能希望部署它。即使是为了有限的受众测试,我们也希望部署的应用程序在后台运行。稍后,我们可能希望使用完整的网络服务器,如 Apache,来为我们提供应用程序服务,也许还有其他应用程序。最后,对于预期会有大量流量和众多访客的应用程序,我们可能希望在我们的应用程序的多个实例之间平衡负载。
本章将讨论如何使用标准 paster 服务器部署我们的应用程序。然后我们将了解如何在 Apache 服务器后面运行应用程序,首先是通过使用简单的代理配置,然后是使用mod_wsgi。最后,我们将探讨 ZEO 如何为我们提供应用程序的水平扩展性,并简要讨论如何通过添加缓存和负载均衡来使网站支持高流量负载。
我们将涵盖的一些特定主题包括:
-
使用 paster 进行简单部署
-
使用代理传递在 Apache 服务器后面运行
-
使用
mod_wsgi在 Apache 服务器后面运行 -
设置 ZEO 集群
-
缓存和负载均衡
将应用程序迁移到生产模式
在考虑我们将为部署使用哪个网络服务器之前,我们需要为每个生产应用程序执行一个步骤。Grok 为应用程序服务器提供了一个“开发者模式”,该模式启用了一些旨在帮助开发者更容易地调试和测试其应用程序的功能。
开发者模式最明显的影响是,对模板所做的任何更改都会自动由服务器考虑,无需重启。这会对应用程序性能造成惩罚,因为服务器必须轮询文件以确定是否对模板进行了更改。
建议在准备发布应用程序时关闭此功能。对模板的进一步更改将需要重启,但在网络开发中,任何性能提升都应受到欢迎。
要关闭开发者模式,编辑由grokproject生成的包中包含的etc/zope.conf.in文件。找到显示devmode on的行,并将其修改为如下所示:
# Comment this line to disable developer mode. This should be done in
# production
# devmode on
通过注释掉粗体显示的行,将应用默认设置,即开发模式关闭。然而,请注意,这并不是实际的配置文件,而是一个用于生成它的模板。
要使更改生效,请重新运行 buildout,以便实际配置文件parts/etc/zope.conf被重写。当你下次启动应用程序时,它将在生产模式下运行。
在后台运行 paster 服务器
到目前为止,部署我们的应用程序最简单的方法是使用我们在整本书的开发过程中一直使用的相同的 paster 服务器。我们唯一需要做的是以“守护进程”模式启动服务器,这样我们的应用程序就可以在后台运行:
$ bin/paster serve --daemon parts/etc/deploy.ini
然后进程将在后台启动。该进程的 PID 将存储在paster.pid文件中,可以用来获取运行中服务器的状态信息。默认情况下,其他paster serve命令假定paster.if文件名,因此例如,要获取进程状态,您可以输入:
$ bin/paster serve --status parts/etc/deploy.ini
$ Server running in PID 11482
当我们需要停止服务器时,我们使用stop-daemon选项,如下所示:
$ bin/paster serve --stop-daemon parts/etc/deploy.ini
还有其他一些选项可能很有用。我们可能希望服务器在出现任何原因导致服务器死亡的情况下自动重启;这正是monitor-restart选项的作用:
$ bin/paster serve --daemon --monitor-restart parts/etc/deploy.ini
最后,我们可能更喜欢在后台持续运行服务器,并在我们更改某些文件时自动重启。这可以通过reload选项实现:
$ bin/paster serve --daemon --reload parts/etc/deploy.ini
这种设置可以被认为是 Grok 部署架构的最小版本,其部分如下所示:

在 Apache 网络服务器后面运行 Grok
使用默认 paster 配置进行生产的问题在于,我们网站的网络地址必须包含应用程序名称。在某些情况下这可能可以接受,但几乎总是不够好。克服这个问题的最简单方法是将应用程序放在一个网络服务器后面,比如 Apache,并使用大多数网络服务器中可用的强大 URL 重写工具从所需的任何 URL 提供我们的应用程序。
此外,如果我们的网站将运行除我们开发的以外的其他应用程序,通常让 Apache 或其他网络服务器负责集中处理网站上的多个应用程序的请求是个好主意。
使用 mod_rewrite 从 Apache 服务器提供我们的应用程序
要设置此配置,您需要安装 Apache 网络服务器,它适用于所有平台。大多数 Linux 发行版都允许您通过使用它们的包管理工具来安装它。
例如,在 Ubuntu 或 Debian 上,您可以简单地输入:
$ sudo apt-get install apache2
一旦 Apache 准备就绪,下一步就是配置它以使用mod_rewrite模块。通常,这样做的方法是编辑httpd.conf文件,该文件应位于服务器/etc目录中的apache2或httpd子目录中。
要加载所需的模块以使mod_rewrite工作,需要以下一般配置:
LoadModule rewrite_module /usr/lib/apache2/modules/mod_rewrite.so
LoadModule proxy_module /usr/lib/apache2/modules/mod_proxy.so
LoadModule proxy_http_module /usr/lib/apache2/modules/mod_proxy_http.so
ProxyRequests off
前三条加载所需的模块,而ProxyRequests行确保服务器不能被第三方用作非自愿的代理。不要省略此行。
现在已经启用了“重写”功能,我们需要创建一个虚拟主机来为我们应用程序的请求提供服务。这个虚拟主机将包括“重写”规则,允许我们的应用程序由 Apache 提供。一个示例虚拟主机定义如下:
<VirtualHost *:80>
ServerName grok.example.com
RewriteEngine On
RewriteRule ^/(.*) http://localhost:8080/todo/++vh++http:grok.example.com:80/++/$1 [P,L]
</VirtualHost>
在这个简单的示例中,我们只设置了服务器名称,开启了重写引擎,并设置了一个重写规则。在执行任何其他操作之前,请确保服务器名称是正确的。
重写规则是配置中的重要部分。Apache 的 mod_rewrite 使用正则表达式语法来匹配将触发规则的 URL 部分。在上面的配置中,规则的第一部分告诉引擎匹配以斜杠开头的任何 URL,这当然会匹配由这个虚拟主机提供的任何 URL。
在正则表达式中,当你用括号括起一个子表达式时,这意味着与括号内文本匹配的任何内容都必须保存到一个变量中。匹配此文本的第一个括号内的表达式存储在变量 $1 中,第二个在 $2 中,依此类推。在这种情况下,斜杠之后的所有内容,即请求的完整路径,将被存储在 $1 中。
规则的第二部分依赖于 Grok 的虚拟主机工具。这将是由规则查询以获取实际内容的 URL,以便在匹配的位置提供服务。对于虚拟主机,Grok 期望我们发布的应用的完整 URL(http://localhost:8080/todo),后面跟着特殊的虚拟主机语法,这将包括协议、服务器名称和端口号,Grok 将使用这些信息来转换响应中存在的所有 URL,以便其中的每个链接都指向正确的宿主。注意,在 Grok ++vh++ 规则之后,通过使用之前解释的 $1 变量,将请求的完整路径附加到末尾。
规则的第三和最后一部分指示 Apache 这是一个代理请求(P),并且当匹配时(L),这个规则应该是最后一个应用的规则。
这就是我们需要设置我们的应用程序与 mod_rewrite 一起使用的所有内容。为了测试这一点,首先确保 paster 进程正在运行,并且 Apache 正在使用我们添加的配置。通常,服务器在安装时会自动启动,所以你可能需要告诉它重新加载配置。在 Ubuntu 或 Debian 中,这样做的方式是:
$ /etc/init.d/apache2 reload
现在,您可以访问配置中定义的 URL(在我们的示例中是 http://grok.example.com),并查看您的应用程序在 Apache 后面运行。
在 mod_wsgi 下安装和设置 Grok
在 Apache 后面提供 Grok 应用程序的另一个优秀选项是 mod_wsgi,这是一个在 WSGI 协议下提供应用程序的 Apache 模块。在本节中,我们将学习 WSGI 是什么,以及如何使用 mod_wsgi 模块设置 Apache 以提供我们的应用程序。
WSGI:Python 网络服务器网关接口
WSGI 是一个 Python 标准,用于指定网络应用程序如何与网络服务器和应用程序服务器通信。其目标是提供一个简单的接口,以支持网络服务器和 Web 框架(如 Grok)之间的大多数交互。
WSGI 还支持“中间件”组件,这些组件可以用于预处理或后处理请求。这意味着它被用来创建“插入”到我们的应用程序中的 Python WSGI 工具,并执行诸如性能分析、错误处理等功能。
Grok 可以在任意 WSGI 服务器后面运行。现在我们将探讨如何在全新的 Linux 虚拟服务器上安装 Grok,以及 mod_wsgi。
Grok 和 WSGI
对于 Python 网络开发者来说,WSGI 是 Python 网络开发未来的关键。由于存在许多重要的网络开发框架,Python 的强大功能使得快速创建新框架变得非常容易,因此与多个框架中开发的最佳应用程序交互可能会很快成为创建新 Python 网站的最佳方式。
直到相对较近的时期,Zope 3 及其一些衍生应用程序,如 Grok,还有可能错过 WSGI 的聚会,但现在已经不是这样了。Grok 1.0 与 WSGI 兼容,因此可以与当今 Python 世界中广泛可用的各种基于 WSGI 的技术集成。
为什么使用 Apache 和 mod_wsgi 为 Grok?
可用的 WSGI 服务器有很多,但本章将重点介绍使用 mod_wsgi,这是一个 Apache 的 WSGI 适配器模块。这样做有几个原因。
首先,Apache 是最受欢迎的网页托管平台,因此许多网络开发人员和网站管理员已经熟悉它。例如,Grok 就是使用 mod_rewrite 在 Apache 后面安装用于生产服务器的。
其次,还有许多 Python 应用程序已经在 Apache 下通过使用 mod_python 运行。为此模块也有几个 WSGI 适配器,但 mod_wsgi 是用 C 语言编写的,与这些适配器相比,内存开销更低,性能更好。
此外,mod_wsgi 的一个目标就是打入低成本的商品化网络托管市场,这对 Python、Grok 和 Zope 都是有益的。
设置干净的 Linux 服务器
在接下来的讨论中,我们将使用 Linux 作为操作系统,因为它是迄今为止部署网络应用程序最受欢迎的方式。我们将介绍 Ubuntu 发行版,但这些步骤同样适用于任何基于 Debian 的发行版。其他发行版使用不同的软件包管理器,可能还有不同的系统路径,但无论如何,你应该能够轻松地找出你需要的东西。
我们可以从安装最新版本的 Ubuntu GNU/Linux 的干净安装开始。第一步是安装必要的软件包,以支持正确的 Python 版本(Grok 目前需要 Python 2.5)和 Apache 服务器。
在此之前,需要安装编译和构建软件所需的软件包,以便使用 Ubuntu(其他发行版通常不需要此步骤)。请注意,软件包安装和 Apache 模块添加通常需要 root 权限。在命令块中,带有$的提示符是用户提示符,带有#的提示符是 root 提示符,您可以通过执行sudo -s命令获得。在本部分中,您将使用 root 终端,这样就不需要在每个命令前加上sudo。在其他部分中,您将使用用户终端,如果需要以 root 身份执行某些操作,请在命令前加上sudo。您可以将一个终端作为 root 打开,另一个终端作为用户打开。
$ sudo -s
# apt-get install build-essential
接下来,安装 Python 和 Apache 的软件包。与大多数打包的 Linux 发行版一样,Ubuntu 需要为每个软件组件的开发库进行单独安装:
# apt-get install python2.5 python2.5-dev
# apt-get install apache2
apache2软件包通常安装apache2-mpm-worker,但您可能已安装了另一个版本,即apache2-mpm-prefork。要检查已安装哪个版本,您可以执行以下命令:
# dpkg -l|grep apache2
接下来,安装相应的开发包,如果已安装apache2-mpm-worker,则安装apache2-threaded-dev;如果已安装apache2-mpm-prefork,则安装apache2-prefork-dev:
# apt-get install apache2-threaded-dev
Grok 使用 Python 的setuptools,因此也需要这个包:
# apt-get install python-setuptools
有可能 Ubuntu 软件包提供的版本不是最新的。如果您想对安装的setuptools版本有更多控制,并在新版本可用时自行更新它,可以使用以下方法。手动下载setuptools-0.6c9-py2.5.egg(或最新版本,选择 py2.5)并执行以下命令:
# sh setuptools-0.6c9-py2.5.egg
您可以稍后使用以下命令进行更新:
sudo easy_install-2.5 -U setuptools
安装和配置 mod_wsgi
现在,服务器已准备好安装mod_wsgi。在 Ubuntu 中有一个名为libapache2-mod-wsgi的软件包,但建议您构建最新版本,部分原因是因为mod_wsgi必须使用与 Grok 相同的 Python 编译。如果您之前已安装该软件包,请将其删除。我们需要直接从下载站点获取源代码,然后进行构建:
$ wget http://modwsgi.googlecode.com/files/mod_wsgi-2.6.tar.gz
$ tar xzf mod_wsgi-2.6.tar.gz
$ cd mod_wsgi-2.6
$ ./configure --with-python=/usr/bin/python2.5
$ make
$ sudo make install
再次提醒,必须使用您将用于运行网站的相同 Python 编译mod_wsgi。由于 Grok 需要 2.5 版本,因此使用了--with-python选项来指向我们需要的 Python 版本。
一旦安装了mod_wsgi,Apache 服务器就需要知道这一点。在 Apache 2 中,这是通过将加载声明和任何配置指令添加到/etc/apache2/mods-available/目录来完成的。
模块的加载声明需要放在一个名为wsgi.load的文件中(位于/etc/apache2/mods-available/目录),该文件只包含以下一行代码:
LoadModule wsgi_module /usr/lib/apache2/modules/mod_wsgi.so
配置指令位于名为wsgi.conf的文件中,该文件位于wsgi.load文件旁边。我们现在不创建它,但如果您有多个 WSGI 应用程序需要服务,它可能会很有用。
然后,你必须使用以下命令激活wsgi模块:
# a2enmod wsgi
注意,a2enmod代表“apache2 enable mod”,这是一个为你创建符号链接的可执行文件。实际上,a2enmod wsgi等同于:
# cd /etc/apache2/mods-enabled
# ln -s ../mods-available/wsgi.load
# ln -s ../mods-available/wsgi.conf # if it exists
对于 Apache 1.3 或使用旧目录布局的 Apache 2,你可能需要在 Apache 的/etc目录中的httpd.conf文件内放置LoadModule行和配置指令(你将在后面看到),在这种情况下,上述软链接将不再必要。
在 mod_wsgi 下配置 Grok 站点
Grok 可以使用setuptools安装,并且可以使用包含的grokproject工具轻松创建 Grok 站点。如前所述,Grok 可以在任何 WSGI 服务器后面运行,而不仅仅是默认使用的 paster 服务器。现在我们有了工作的mod_wsgi,我们将看看如何在它后面运行 Grok。
准备待办事项应用程序以供 mod_wsgi 使用
WSGI 应用程序使用入口点来让 WSGI 服务器知道如何运行程序。入口点通常是一个简单的 Python 脚本,它提供了一个用于调用应用程序并传递适当的初始化文件给服务器的函数。一些服务器,例如 paster 服务器,只需要.ini文件的路径,这是我们通常用来启动 Grok 应用程序的方式。这并不意味着 paster 没有入口点脚本。实际上,入口点是在setup.py文件中定义的,这是在初始化新项目时由grokproject创建的。看看文件的最后几行:
[paste.app_factory]
main = grokcore.startup:application_factory
debug = grokcore.startup:debug_application_factory
标题paste.app_factory告诉服务器在哪里找到.ini文件每个部分的工厂函数。在 Grok 中,一个通用应用程序工厂函数定义在grokcore.startup包中,这是 paster 用来启动应用程序的。
然而,mod_wsgi需要一个指向工厂的路径,这在我们配置中可能会很麻烦,因为这意味着它需要指向grokcore.startup egg 内的一个文件。由于 egg 包含版本号,简单的更新可能会导致如果删除旧 egg,我们的站点崩溃。最好在我们的应用程序包内部定义自己的工厂。
由于工厂代码对于不同的项目几乎可以完全相同,因此在我们创建项目时自动包含它会更好,以避免每次都需要重新创建相同的脚本。幸运的是,对于 Grok 使用 buildout 的情况,这非常有帮助,因为有一个 buildout 配方可以为我们创建 WSGI 应用程序工厂。
该食谱名为 collective.recipe.modwsgi。要使用它,只需在 buildout 中添加一个名为 wsgi_app 的部分。该食谱需要两个参数,第一个是必须提供给运行 WSGI 应用程序的 Python 进程的 eggs,第二个是用于站点的配置文件路径。最后一个参数 value 是我们一直在使用的 parts/etc/deploy.ini 路径,用于在 paster 下运行应用程序。就是这样。按照以下方式编辑 buildout.cfg 文件的 parts 列表:
parts =
eggbasket
app
i18n
test
mkdirs
zpasswd
zope_conf
site_zcml
zdaemon_conf
wsgi_app
deploy_ini
debug_ini
接下来,在文件的任何位置添加以下部分:
[wsgi_app]
recipe = collective.recipe.modwsgi
eggs = ${app:eggs}
config-file = ${buildout:directory}/parts/etc/deploy.ini
注意,eggs 参数只是简单地指向在 buildout 开始处定义的主要 egg 部分,以避免重复。
当再次运行 buildout 时,我们会找到一个 parts/wsgi_app 目录(或我们为 buildout 部分使用的任何名称)。在该目录内,将有一个 wsgi 文件,可以直接由 mod_wsgi 使用来运行应用程序。
配置 Apache 站点以使用 mod_wsgi
最后一步是将站点添加到使用 mod_wsgi 服务的 Apache 服务器。这是标准的 mod_wsgi 配置;我们只需添加我们之前创建的应用程序工厂的路径。
要设置虚拟主机,在 /etc/apache2/sites-available 目录中创建一个文件,例如命名为 "grok"。向其中添加以下代码,假设你的待办事项应用位于 /home/cguardia/grok/todo:。
WSGIPythonHome /usr
WSGIDaemonProcess grok user=cguardia group=cguardia threads=4 maximum-requests=10000
<VirtualHost *:80>
ServerName wsgi.example.com
WSGIScriptAlias /todo /home/cguardia/grok/todo/parts/wsgi_app/wsgi
WSGIProcessGroup grok
WSGIPassAuthorization On
WSGIReloadMechanism Process
SetEnv HTTP_X_VHM_HOST http://wsgi.example.com/todo
</VirtualHost>
这将使 mod_wsgi 以 'daemon' 模式运行,这意味着它将启动多个进程以运行配置的 WSGI 应用程序,而不是使用 Apache 进程。如果你使用 virtualenv,运行它的虚拟 Python 的 site-packages 目录需要传递给 WSGIPythonHome 变量。为了告诉 mod_wsgi 要运行哪个 WSGI 应用程序,我们使用 WSGIScriptAlias 指令,并传递我们之前创建的应用程序工厂的路径。
注意,我们分配了一个用户和组来运行进程。必须要求此用户有权访问应用程序目录。
PYTHON_EGG_CACHE 目录
注意,当应用程序启动时,所有 eggs 将会自动提取到 PYTHON_EGG_CACHE 目录中,通常是 ~/.python-eggs。此目录依赖于 HOME 环境变量。HOME apache 用户 www-data 是 /var/www。如果你没有在 WSGIDaemonProcess 指令中配置用户或 python-eggs 变量,你可能会在 error.log apache 文件中遇到错误 [Errno 13] Permission denied: '/var/www/.python-eggs'。你也可以添加一个 python-eggs 参数来告诉 mod_wsgi 使用一个替代目录作为 egg 缓存:
WSGIDaemonProcess grok threads=4 maximum-requests=10000 python-eggs=/tmp/python-eggs
在这个例子中,该进程属于 www-data。www-data 和 python-eggs 缓存目录将是 /tmp/python-eggs。
运行应用程序
一旦配置就绪,我们需要在 Apache 中启用站点,因为我们刚刚创建了它。这仅在第一次运行时是必要的:
$ sudo a2ensite grok
此命令将在 Apache 的sites-enabled目录中创建一个指向我们刚刚配置的“grok”站点的链接,这足以使其生效。然后,我们可以通过简单地重新加载服务器的配置来从 Apache 开始服务我们的应用程序:
$ sudo /etc/init.d/apache2 reload
当你在浏览器中访问网站(wsgi.example.com/todo)时,你应该看到 Grok 管理 UI。你应该能够使用管理员登录名和密码登录。
添加 ZEO 服务器
默认情况下,mod_wsgi将使用单个进程来运行应用程序。由于此配置旨在用于生产环境,可能希望有更多的进程可供应用程序服务。Grok 使用的 ZODB 附带一个名为Zope 企业对象(ZEO)的服务器,它允许我们将尽可能多的进程添加到我们的配置中,提供无限的水平扩展性。通常,建议的进程数是系统处理器的每个核心一个。让我们设置一个 ZEO 服务器并配置 Grok 进程连接到它。
ZEO 配置的 Buildout 配方
再次强调,让 ZEO 运行的最简单方法就是使用现有的 buildout 配方。这次我们将使用名为zc:zodbrecipes的配方。将zeo_server部分添加到您的buildout.cfg文件中,如下所示:
parts =
eggbasket
app
i18n
test
mkdirs
zpasswd
zope_conf
site_zcml
zdaemon_conf
zeo_server
wsgi_app
deploy_ini
debug_ini
接下来,添加一个zeo_server部分,如下所示:
[zeo_server]
recipe = zc.zodbrecipes:server
zeo.conf =
<zeo>
address 8100
</zeo>
<blobstorage 1>
blob-dir ${buildout:directory}/var/blobstorage
<filestorage 1>
path ${buildout:directory}/var/filestorage/Data.fs
</filestorage>
</blobstorage>
<eventlog>
level info
<logfile>
path ${buildout:directory}/parts/log/zeo.log
</logfile>
</eventlog>
这将添加 ZEO 服务器,并将其配置为监听端口 8100。其余的配置基本上是样板,所以只需在需要 ZEO 的新项目中复制它。blobstorage部分设置了一个启用 blob 处理的文件存储;logfile部分告诉服务器在哪里存储日志文件。
接下来,我们需要 buildout 添加启动和停止 ZEO 的脚本。这可以通过将ZODB3 egg 添加到我们的应用程序部分轻松实现:
[app]
recipe = zc.recipe.egg
eggs = gwsgi
z3c.evalexception>=2.0
Paste
PasteScript
PasteDeploy
ZODB3
配置 ZEO 客户端
目前,我们使用的 Grok 应用程序正在与常规的 Zope 服务器一起工作。要使用 ZEO,我们需要更改配置以连接到端口 8100 的服务器。幸运的是,所需更改已经包含在 Grok 项目内部创建的常规zope.conf文件中,所以我们只需要取消注释那些行。在 Grok 项目的etc目录中的zope.conf.in文件内取消注释以下行:
# Uncomment this if you want to connect to a ZEO server instead:
<zeoclient>
server localhost:8100
storage 1
# ZEO client cache, in bytes
cache-size 20MB
# Uncomment to have a persistent disk cache
#client zeo1
</zeoclient>
这里重要的一行是带有 ZEO 服务器地址的行。在这种情况下,我们使用与 Grok 应用程序相同的宿主,以及我们在上一节中定义的 ZEO 配置中的端口。
启动 ZEO 服务器
再次运行 buildout 后,我们将准备好在后台启动 ZEO 服务器。为此,我们只需要运行为我们自动创建的服务器脚本。该脚本的名称与 buildout 中配置服务器的部分名称相同:
$ bin/zeo_server start
我们的可视化应用现在正在运行!要停止它,你可以使用以下命令:
$ bin/zeo_server stop
增加进程数量
回想一下,我们之前提到 mod_wsgi 默认情况下在一个进程中运行应用程序。为了真正利用 ZEO,我们希望有更多的进程可用。我们需要对我们的 mod_wsgi Apache 虚拟主机配置进行一些小的修改。将 WSGIDaemonProcess 行,靠近顶部,修改如下代码:
WSGIDaemonProcess grok user=cguardia group=cguardia processes=2 threads=4 maximum-requests=10000
记得重新加载 Apache 配置,以便能够看到新设置的效果。在这个例子中,我们将有两个进程运行,每个进程有四个线程。通过使用 ZEO 和 mod_wsgi,我们现在有一个可扩展的网站。
更高的可扩展性:添加缓存和负载均衡
我们刚才讨论的配置对于大多数类型的网站来说已经足够了。然而,有时一个网站需要处理高流量负载,并且需要采取措施确保它在压力下不会崩溃。
缓存代理
可以做的一件非常有效的事情是在 Apache 服务器前添加一个缓存代理。一个缓存代理通过保存网站最常使用的页面的副本并在后续请求中直接提供该副本,从而减少带宽并提高响应时间,而不是再次连接到服务器。
两个非常流行的缓存代理是 Squid (www.squid-cache.org) 和 Varnish (www.varnish-cache.org)。如何设置这些代理的详细讨论超出了本章的目标,但有趣的是,有自动设置它们的 buildout 脚本可用。查看 Python 包索引,看看这些脚本如何帮助构建和配置缓存代理。对于 Squid,有 plone.recipe.squid (pypi.python.org/pypi/plone.recipe.squid/),而对于 Varnish,相应的 plone.recipe.varnish (pypi.python.org/pypi/plone.recipe.varnish)。
负载均衡器
负载均衡器位于应用程序的两个或多个实例之前,并将传入的请求均匀地分配给它们。当你有多个机器提供服务时,这是一个非常强大的解决方案。
有几个开源的负载均衡器可用;例如,Apache 本身就有一个负载均衡模块。然而,与基于 Zope Toolkit 的应用程序一起使用最流行的负载均衡器之一是 "Pound" (www.apsis.ch/pound/)。
Pound 不仅仅只是分配负载。它可以保持会话信息,并将来自同一来源浏览器的请求发送到第一次请求时使用的同一目标服务器。Pound 还可以根据其 URL 在服务器之间分配请求。最后,它可以作为一个故障转移服务器,因为它足够智能,能够注意到后端服务器何时失败,并且会在服务器恢复之前停止向其发送请求。
当然,有一个 buildout 脚本可用于设置 Pound,其名称恰当地命名为 plone.recipe.pound (pypi.python.org/pypi/plone.recipe.pound)。
Grok 的高流量架构
现在我们已经了解了一些提高我们应用程序性能的机制,可视化不同部分之间的关系可能会有所帮助。在我们的案例中,组织一个支持高流量负载的系统的好方法是在前端放置一个缓存代理,例如 Varnish,并将其配置为将请求传递给 Pound 负载均衡器。
Pound 将被配置为在多个单独的服务器之间分配负载,每个服务器都运行配置了 mod_wsgi 的 Apache,并运行多个进程,这些进程将与运行我们的 Grok 应用程序的多个 ZEO 客户端进行通信。使用以下图中所示的这种架构,我们可以扩展我们的应用程序以处理非常高的流量。

摘要
本章讨论了如何处理应用程序部署,并探讨了部署我们的示例应用程序的各种可能性,从简单的设置到具有 ZEO 的可扩展配置。
我们与 Grok 的旅程已经结束。希望你会觉得 Grok 是你网络工具箱中一个很好的补充,并且会考虑其优势如何帮助你实现目标,无论何时你需要开发一个网络应用程序。在这个互联网时代,联系并与人沟通以获得各种技术的帮助非常容易。Grok 的开发者和用户也不例外,所以请不要犹豫,通过使用 Grok 的邮件列表或 IRC 频道寻求建议。我们将很高兴帮助你,并且会为你在 Grok 上尝试而感到高兴。


浙公网安备 33010602011771号