编写 Zope 产品

作者:Reuven M. Lerner

上个月,我们继续讨论了 Zope Web 开发环境,安装并检查了 Zope 产品。正如我们所见,每个产品实际上都是一个 Python 对象模块,可以在 Zope 服务器上实例化一次或多次。Zope 有数百种产品可用,范围从小型轻量级的算命先生到大型且令人印象深刻的内容管理框架 (CMF)。

许多 Zope 管理员满足于安装和使用他们可以从 Web 下载的产品。 实际上,现有产品足以满足大多数基本站点;无论您无法使用现有产品做什么,通常都可以在 DTML(Zope 的动态模板标记语言,在 2002 年 2 月的 LJ 期刊中描述)中轻松创建。

但是,尽管在 DTML 中执行许多任务可能很容易和直接,但它既不如 Python 完整也不如 Python 灵活。虽然在 Zope 中添加 PythonScripts(和 PerlScripts!)肯定减少了为许多中型任务编写产品的需求,但大多数 Zope 黑客最终还是会发现自己正在编写某种产品。

本月,我们将了解如何构建一个简单的 Zope 产品,然后将其集成到我们的站点中。正如您将看到的,创建一个可以很好地集成到其余环境中的 Zope 产品非常容易。

一个极其简单的产品

Zope 产品的核心是一个 Python 模块。产品(正如我们上个月所见)安装在主 Zope 目录下的 lib/python/Products 目录中。Zope 在启动(或重启)时查找产品,这意味着您必须在安装新产品后启动或重启 Zope。

我们可以根据需要多次实例化已安装的产品,并将每个实例放置在 Web 层次结构中的某个位置。每个实例都有一个唯一的“id”属性,该属性既在文件夹中唯一标识它,又允许我们在对象上调用方法。

如果这听起来已经令人困惑,请记住 URL /foo/bar 通常意味着 Web 服务器应检索目录“foo”中的文件“bar”。但是在 Zope 中,URL foo/bar 意味着系统应在对象“foo”上调用方法“bar”。换句话说,foo/bar 变成了 foo.bar。当我们说“对象 foo”时,我们实际上是指“ID 为 foo 的对象”。如果我们要成功地在我们的对象上调用方法,则设置 ID 至关重要。

我们还需要为我们的产品定义一个 meta_type。meta_type 名称是文本字符串,它将出现在 /manage 屏幕右上角的 Zope 产品“添加”下拉列表中。通常,您可以将 meta_type 命名为与您的类相同,或者命名为更容易理解的名称。请记住,“添加”菜单中的项目按 ASCII 顺序显示,这意味着大写字母“Z”在小写字母“a”之前。

要创建我们自己的产品,我们需要执行以下操作

  • 在其自己的模块中定义一个类,并将其安装在 lib/python/Products 中。

  • 定义一个 __init__ 方法,我们在其中为“id”实例变量赋值。

  • 定义一个或多个方法,其返回值是包含 HTML 的文本字符串。

  • 定义一个 meta_type 类变量,该变量为我们产品的所有实例设置 meta_type。

事实证明,这非常简单,正如您在清单 1 中看到的那样,清单 1 定义了 helloworld.py,这是一个简单的 Zope 产品,您可以在您的站点中安装并几乎可以实例化。(我们很快就会看到如何绕过这些限制。)

清单 1. helloworld.py,一个简单的 Zope 产品

在我们的 helloworld 类中,有几个重要的项目需要注意。首先,类及其方法包含文档字符串。编写文档字符串始终是一个好主意,Python 包含这样一个功能是一个罕见且令人耳目一新的提醒,即程序员可以并且应该在源代码中包含文档。Zope 通过强制规定类和方法必须包含文档字符串(如果您希望在系统中使用它们)来强制执行此限制。

我们的类还定义了两个方法,__init__ 和 index_html。__init__ 方法在 Zope 创建我们对象的实例时自动调用,通常用于初始化实例变量和定义稍后需要的其他行为。在本例中,__init__ 定义了一个实例变量 (self.id),该变量允许我们的对象跟踪其身份。正如您可能期望的那样,__init__ 并非旨在从外部世界调用,而是从 Zope 自身内部调用。

相比之下,index_html 方法旨在通过 URL 调用。如果我们将“helloworld”的实例放置在 Zope 服务器的根 (/) 目录中,我们可以使用 URL /helloworld/index_html 在其上调用 index_html 方法。但是 index_html 很特殊;就像许多 Apache 服务器上的 index.html 文件一样,如果没有显式命名其他方法,则默认调用它。

最后,请注意 index_html 将 HTML 返回给其调用者。它不返回状态代码或除 HTML 以外的任何内容。

缺少什么?

helloworld.py 是一个完全合法的 Zope 产品;我们可以将其安装在 lib/python/Products 中,Zope 不会介意。不幸的是,Zope 也将无法注意到 helloworld.py 在那里,不会将其添加到“添加”选择列表中,并且通常会忽略我们在编写产品时所做的工作。显然,如果我们希望我们的骨架产品与 Zope 交互,我们将需要加强它。我们将此增强版本称为“smallhello”产品。

首先,我们必须将我们产品的结构从单个独立的模块文件 (helloworld.py) 更改为成熟的 Python 包。包是 Python 搜索路径(由变量 sys.path 定义)中的一个目录 (smallhello),其中包含一个或多个 Python 源文件。在我们的例子中,smallhello 目录将包含两个文件:一个与 helloworld.py 非常相似的 smallhello.py 文件(参见清单 2)和一个 __init__.py 文件(参见清单 3),它初始化并帮助注册我们的对象。

清单 2. smallhello/smallhello.py

清单 3. smallhello/__init__.py

__init__.py 文件首先导入 smallhello.smallhello,定义模块的方法和属性。但是 __init__.py 的关键部分(至少就 Zope 而言)是 initialize 方法。在 Zope 发现并导入 smallhello 后,它会调用 smallhello.initialize,并将 ProductContext 对象(称为“context”)传递给它。换句话说,初始化对象会导致该对象在服务器上注册自身。

initialize 例程本身非常简单,尽管我们的版本做了一些基本的错误捕获(使用 try/except)以确保事情正常工作。我们的 smallhello 产品仅将两个参数传递给 context.registerClass:我们想要添加的 finalhello.finalhello 对象,然后是一个构造函数元组,当我们想要创建我们产品的新实例时应该调用这些构造函数。如果将单个项目传递给构造函数,请记住包含尾随逗号;否则,Zope 将无法加载产品。

constructors 参数只是我们可以传递给 context.registerClass 的许多命名参数之一,以自定义我们的对象在 Zope 中注册的方式。例如,我们还可以传递一个 icon 参数,该参数告诉 Zope 哪个图形(我们包目录中的文件名)应该放置在我们包的实例旁边。

我们对象的更改

将 helloworld.py 转换为 smallhello.py(参见清单 2)需要进行一些小的更改。我们首先添加一个方法,允许 Zope 创建我们产品的新实例。按照惯例,此类管理相关的方法以 manage_ 开头,因此我们的方法称为 manage_smallhello。这与传递给 context.registerClass 的 constructors 元组中命名的方法相同。

对我们的 smallhello 类最重要的更改也是最不明显的更改之一:我们已将其设为 OFS.SimpleItem.SimpleItem 的子类,OFS.SimpleItem.SimpleItem 是作为 Zope 包一部分提供的 Zope 产品基类之一(在 OFS.SimpleItem 包中)。如果不从 SimpleItem 继承,许多事情(从对象的剪切和粘贴到获取)将无法按我们的预期工作,甚至根本无法工作。您的产品可以继承自几个可能的基类;SimpleItem,顾名思义,旨在成为最简单和最直接的一个。

在修改 smallhello.py 时,我决定添加另外两个生成内容的方法。其中一个方法 other_html 生成的输出与 index_html 类似,当然,index_html 在未指定其他方法时默认显示,而 other_html 仅在 URL 中显式命名时才调用。

我还添加了一个 foo_file 方法,该方法演示了如何从磁盘返回 HTML(或 DTML)文件的内容。将所有 HTML 放在 Python 模块文件中可能很烦人且令人沮丧;通过这种方式,您可以将 DTML 文件保留在包目录中,但独立于程序本身对其进行修改。请注意,我们必须从 Globals 包中导入 HTMLFile 方法才能使其工作。

我修改了 smallhello.py 中的 __init__ 函数以接受三个参数:self、id 和 title。(以前,它只接受 self 和 id。)每次创建 smallhello.py 的新实例时都会调用 __init__ 函数,这通过调用 manage_smallhello 完成。在 manage_smallhello 内部,我们对 self._setObject 的调用将对象 ID 设置为通用的 smallhello_id,标题为 smallhello_title。由于我们在示例中硬编码了 ID,并且由于 ID 在文件夹中必须是唯一的,这意味着我们在给定文件夹中只能有一个 smallhello 产品实例。这里没有足够的空间来描述如何读取和写入参数,但是快速查看“资源”部分中提到的示例应该可以清楚地了解如何执行此操作。

在调用 self._setObject 之后,manage_smallhello 然后将用户的浏览器重定向到主(index_html)方法。我们可以改为显示一些输出,将用户的浏览器重定向到我们对象中的另一个方法,但我选择了简单的方法,并决定将用户发送到我们站点上的 /index.html。

安装 smallhello 产品后,您可以停止 Zope 并再次重启它。您应该在“添加”菜单的底部附近看到“smallhello”;选择它会将您发送到您的 Zope 站点上的 index.html 页面。因为我们还没有使我们的产品像它可能的那样用户友好,所以您必须在浏览器中手动输入 URL(index_html、other_html 或 foo_file)。但是没有任何理由说明这些页面不能包含彼此的链接,或者站点上的其他页面不能链接到它们。

您知道吗?我们创建了一个 Zope 产品!

现在缺少什么?

如果我们要发布我们当前的 simplehello 项目,那么没有人真正想要使用它。除了上面提到的问题(例如,单个实例缺少唯一的 ID)之外,我们的产品还缺少管理选项卡,这些选项卡使 Zope 对管理员如此用户友好。它也未能以标准或简单的方式处理安全权限。

安装这些功能几乎与我们迄今为止看到的其他功能一样容易。例如,每个选项卡都由一个字典表示,其中包含两个名称-值对,label 和 action。与 label 关联的值是用户在屏幕上看到的内容,而与 action 关联的值告诉 Zope 当有人单击相应的选项卡时应调用哪个方法。要在 Zope 中安装您的选项卡,请在您的对象中定义一个 manage_options 元组,其成员是描述选项卡的字典。

我们到目前为止尚未解决的最重要项目之一是用户输入。这实际上是一个非常容易解决的问题,因为 Zope 将 HTML 表单输入视为方法的标准参数。例如,考虑以下 HTML 表单

<form action="manage_edit" method="POST">
    <p>id: <input type="text"
name="id"></p>
    <p>Title: <input type="text"
name="title"></p>
    <p><input type="submit"></p>
</form>

单击“提交”按钮会将 id 和 title 的名称-值对提交到我们产品的 manage_edit 方法。我们可以使用如下签名定义该方法

def manage_edit(self, id, title):
在此方法中,我们可以使用同名变量检索 id 和 title HTML 表单元素的值。
结论

与 DTML 文件相比,Zope 产品是构建 Zope 应用程序的一种更高级和更复杂的方式,它提供了更大的灵活性,但也需要更高的纪律和对底层机制的理解。了解如何编写 Zope 产品就像了解如何为 Apache 编写 mod_perl 模块一样;这意味着底层系统完全由您支配。

不幸的是,尽管程序员可以利用丰富的 API 来创建他们自己的 Zope 产品,但缺乏良好的入门文档吓退了许多人尝试。我们的 simplehello 产品表明,只需少量代码,您就可以在短时间内获得令人印象深刻且有用的应用程序。

资源

电子邮件:reuven@lerner.co.il

Reuven M. Lerner 是一位顾问,专门从事 Web/数据库应用程序和开源软件。他的著作《Core Perl》于 2002 年 1 月由 Prentice-Hall 出版。Reuven 与他的妻子和女儿住在以色列的莫迪因。

加载 Disqus 评论