通过b-org项目来理解Zope3技术在Plone中的应用
通过b-org项目来理解Zope3技术在Plone中的应用
http://www.315ok.org/blogfolder/263
http://www.315ok.org/logo.png
通过b-org项目来理解Zope3技术在Plone中的应用
通过b-org项目来理解Zope3技术在Plone中的应用
[align=left]本文为b-org教程英汉对照翻译,本人随着翻译的深入,越来越觉得 b-org教程实在是了解zope 3体系 最好,最易理解的教材。[/align][align=left]本文的翻译重点在理解zope 3部件体系架构,其他显而易懂的东西不作深入。[/align][align=left]翻译整理:adam 三人行网络服务有限公司 ,如需转载,请保留此行。[/align]Plone 2.5 brings us closer to the promised land of Zope 3. Zope 3 brings us a new way of working.This tutorial will show how to marry the old and the new, to make Plone products that are more
extensible, better tested and easier to maintain.
Plone2.5的到来使得我们更加靠近zope3的乐土。zope3带给我们全新的工作方式。本教程将新旧技术结合在一起,使得
plone产品有更好的扩展性,更易于测试和维护。
Introduction 介绍What is b-org, and what will you learn here?
b-org是什么,从这我们能学到什么?
b-org stands for "base-organisation". The name had nothing whatsoever to do with my desire to get an svn URL of http://svn.plone.org/svn/collective/borg. Promise. In fact, it used to be called company, which some people rightly pointed out is a bit too generic and opens up the possibility of conflicts with other people's code.
It just proves that naming generic components is difficult.
b-org顾名思义是指"基本组织"。你能通过svn http://svn.plone.org/svn/collective/borg取得该产品。
Generic is the key word here. Functionally, b-org provides infrastructure to help you manage Departments,Employees and Projects in a natural way. Departments are containers for employees, employees are linked to projects by references. Using membrane, these objects become sources for users and groups, so that a department is a group for all the employees in it, and employees become real users of the system, with usernames and passwords. Projects manage local roles, so that employees that have been associated with the project are able to add and modify content in it. Other users may or may not be able to view content in a project, depending on its workflow state.
However, b-org makes no assumptions about which metadata you want to associate with departments,employees or projects. For that, it expects you to plug in your own content schema. It also delegates almost all its functionality to smaller components, so that if you, for example, want to store authentication details via LDAP or change the way in which users are employees to projects, you can do so by implementing small, isolated components rather than sub-classing and re-implementing large chunks of the three basic content types.
That's all well and good, but you're probably not going to want to read a lengthy tutorial just about how great b-org is. As the title promises, this tutorial is about leveraging new technologies available in Plone 2.5 to write better content types and other software in Plone. Hopefully, you will find the techniques described here useful whether you are writing a member management module using membrane (mmmm), or other code. I for one, want to go and rewrite several of my products (like Poi) to make them more extensible and flexible after having adopted these techniques. Hopefully, you will also learn something about the development process, in particular test-driven development, that I followed, and how the future of Plone is entangled in Zope 3.
This tutorial should be viewed as complementary to, rather than superceding, my earlier tutorial entitled RichDocument - Creating Content Types the Plone 2.1 way. The techniques of RichDocument, in particular relating to extending ATContentTypes, are still valid in Plone 2.5. What Plone 2.5 allows us to do, however, is to achieve better separation of concerns between content storage, business logic and view logic, due to the added spices of Zope 3. For RichDocument, the gain wouldn't be that great since it's relatively simple (and focuses on doing as little as possible by re-using as much as possible from ATContentTypes). Hence, I didn't update the RichDocument tutorial, nor do I feel as compelled to update RichDocument itself (yet). b-org is a more ambitious example which allows us to illustrate the new techniques more fully.
One thing to note is that this tutorial is still centered on Archetypes, and assumes you know the basics of Archetypes development on the filesystem. Archetypes is rooted in a pre-Zope 3 world, and there are times when we have to accommodate it in ways that make our clean patterns a bit messier - luckily, not too often. There are ways of managing content in Zope 3 that can be applied to Plone, for example by way of zope.formlib, but these are generally not quite ready to replace what we can do today with Archetypes. In the future, they may be, but more likely Archetypes will converge a bit more with its Zope 3 equivalents and blur the lines between the two approaches. The upshot is that what you know about Archetypes today continues to be relevant, and is augmented by the Zope 3-inspired techniques you will find here.
A whirlwind tour of Zope 3Zope 3 is still fairly new. After reading this tutorial, it should hopefully start to feel a bit more familiar. In this section, we will give a brief overview of what is different in Zope 3 and how it fits into Plone.
The name Zope 3 is a lie. True - it is brought to you by many of the same clever people who built Zope 2, one of the most advanced open source app servers of its day. True, it is still Python, it still publishes things over the web, and there are still Zope Page Templates. However, Zope 3 is about small, re-usable components orchestrated into a flexible framework. It is this flexibility that allows us to use Zope 3 technologies in Zope 2 applications like Plone.
A piece of wizardry called Five (Zope 2 + Zope 3 = Five, geddit?) makes a number of Zope 3 components directly available in Zope 2, and since Zope 2.8, almost all of Zope 3 has shipped with Zope 2 as a python library. Plone 2.5's primary purpose was to lay the foundations for taking advantage of Zope 3 technologies in Plone.
Zope 3 may seem a bit alien at first, because it uses strange concepts such as adapters and utilities. Luckily, these are not so difficult to understand, and once you do, you will find that they help you focus your development on smaller and more manageable components. You will also find that these basic concepts underpin most of the innovative parts of Zope 3.
Interfaces接口
Everything in Zope 3 starts with interfaces. Unlike Java or C#, say, Python does not have a native type for an interface, so an interface in Zope 3 is basically a class that contains only empty methods and attributes, and inherits from Interface. Here is a basic example:
接口是Zope 3体系结构的开始。python没有一个固有类型来定义接口,在Zope 3中通过定义一个包含空的属性和方法的类,并且该类继承自Interface,下面是接口定义例子:
接口提供主要的文档,每一件东西都有文档字符串,你也注意到了这个wear()方法没有实现的语句(即便是空的pass语句也没有,但这个语法有效),也没有self参数。这时因为你从来不需要直接实例化一个接口,仅仅用它来描述一个对象应当有的行为(属性、方法等)。
An object can be associated with an interface in a few different ways. The most common way is via its class. We say that the class implements an interface, and objects of that class provide that interface:
一个对象能有多种方法对接一个接口。最通常的是从他的类定义中获得,这种情况下,我们说这个类实现了该接口,该类所有对象能提高该接口。
implements(IShoe)这行 意味这这个类的对象将提供iIshoe接口。说得更明白点,我们通过设置两个属性来实现这个接口(我们能实现它们作为属性或方法)。IShoeWearing 接口将在下文关于适配器段 讨论。
We use interfaces to model components. Interfaces are normally the first stage of design, in that you should define clear interfaces and write actual classes to fulfill those interfaces. This formalism makes for great documentation - interfaces are conventionally found in an interfaces module, and this is typically the first place you look after browsing a package's documentation. It also underpins the adapter and utility system - otherwise known as the Component Architecture - as described below.
我们用接口模型化部件。接口通常是设计的第一步,应该设计清晰的接口,然后定义实际的类来实现这些接口。按照惯例,这样形式上提供了文档化的接口在interface模块中。它也加强了适配和应用系统,另外也称此为部件体系架构。
Note that you can use common OOP techniques in designing interfaces. If one interface describes a component that has an "is-a" or "has-a" relationship to another component, you can let interfaces subclass or reference each other. An object will provide the interfaces of its class, and all its base-classes, and all base-interfaces of those interfaces. Don't worry about untangling that - it works the way you would expect.
注意,你可以用通常的OOP技术设计接口。如果一个接口描述一个部件 “是一个”或“有一个”关联和其他部件,你能子类化该接口后让它们相互引用。一个对象将提供它的类实现的接口,也将提供它的所有基类实现的接口,还将提供那些接口的基 接口。
You can also apply interfaces directly to an object. Of course, if that interface has methods and attributes, they must be provided by the object, and unless you resort to crazy dynamic programming, the object will get those from its class, which means that you may as well have applied the interface to the class. However, some interfaces don't have methods or attributes, but are used as markers to distinguish a particular feature of an object. Such marker interfaces may be used as follows:
你也能直接应用一个接口到一个对象。当然,如果这个接口有方法或属性,它们必须由这个对象提供。然而,某些接口没有方法也没有属性,它们仅仅用于标记以区分一个对象特别的特性。即marker interfaces :
Marker interfaces是很有用的对于在运行时响应的某些事件(例如 某些用户操作),而不能预先估计到。在下面,你将学习关于适配器以及适配器构造器将应用到 marker interface.通过应用不同的marker interfaces可以改变哪个适配器构造器被调用。
It's also possible to apply interfaces directly to classes (that is the class itself provides the interface, as opposed to the more usual case where the class implements the interface so that objects of that class provides it - this is useful because it allows you to group those classes together and describe the type of class they are) and to modules (where you want to describe the public methods and variables of a module). These constructs are less common, so don't worry about them for now. Look at the documentation and interfaces (!) in the zope.interface package for more.
也可能应用接口直接到类(类自己提供接口,暂不明白,不翻译……)
Adapters适配器
The most important thing that Zope 3 promises is separation of concerns. In Zope 2, almost everything has a base class that pulls in a number of mix-in classes, such as SimpleItem (surely, the most ironically named class in Zope 2) and its plethora of base classes that include RoleManager, Acquisition.Implicit and many others. This means that a class written for Zope 2 is nearly impossible to re-use outside of Zope.
Zope 3最重要的概念是承诺松散耦合。在Zope 2中,通过基类拉入很多混合类,例如SimpleItem 就包括RoleManager, Acquisition.Implicit 和很多其他类。这意味着为zope 2写的类不可能在zope 2之外重用。
Furthermore, in Zope 2 we are tightly wedded to the context (aka here) because it is so convenient to use in page templates, workflow scripts etc. For example, people often write an Archetypes class that contains a schema (storage logic), methods for providing various operations (business logic) and methods for preparing things to display in a page template (view logic). Often, people do this simply because they can't think of a better place to put things, but it does mean that re-using any part of the functionality becomes impossible without importing the whole class - and its base classes, which include Archetypes' BaseObject, CMF's DynamicType, and Zope's SimpleItem - to name a few!
此外,在zope 2中紧密绑定context (here) ,因为这个能方便地应用在ZPT WORKFLOW 脚本中。例如,人们开发一个Archetypes类通常包含一个schema (存储逻辑),一些方法为各种操作(商业逻辑),一些方法为显示页面模版(表现逻辑),所有这一切都在一起。但如果要重用其中功能的任何一部分,就必须导入整个类,包括它的基类,也包括Archetype的基对象,CMF的动态类型和ZOPE的simpleitem。所以,重用几乎不可能。
Think about the example above. The Shoe class is well-contained and only concerned with one thing - storing the attributes of shoes. It can be used as an abstraction of shoe anywhere, and is very lightweight. Now let's consider that we may want to wear shoes as well. We can create a pair of shoes easily enough:
考虑上面的例子。这个shoe类仅仅关心一件事情——存储鞋子的属性。它能用作任何抽象的鞋子。我们可以很容易地创建一双鞋子:
现在,我们想某人能穿这些鞋子,让我们假定有一个人:
在zope 2中,要实现这个要求这个Person要混合某些shoewearingMixin类以指明鞋子该怎样穿。这不得不制造很肥的接口而难于理解。在zope 3中我们只需用一个适配器。
An adapter is a glue component that can adapt an object providing one interface (or a particular combination of interfaces, in the case of a multi-adapter) to another interface. We already have a specification for something that wears shoes, in the form of IShoeWearing. Here is a snippet of code that may use this interface:
适配器是一个胶合部件,能调节一个对象提供一个接口(或一组接口,多接口适配器环境下)到另一个接口。 在我们的例子中,特殊的事情是wears shoes,有 IShoeWearing 要求的形式。 下面是 用这个接口的代码:
问题是怎样实现"wearing=…",怎样包含一个提供IshoeWearing接口的对象?这里代码操作的context,应当是Person.如果Person实现了IshoeWearing接口(或者至少实现了wear()方法),问题就能解决。但是,显然,我们对于Person作出了过分的要求。我们需要一个简单的方法以调节Iperson接口,使得它具备IshoeWearing接口的某些东东。为实现这个,我们需要写个适配器:
这里,我们实现了 IShoeWearing接口。注意这个wear()方法有了个self参数,这是因为这儿是个真实的对象。还有__init__()方法,它取得的一个context参数,这个就是被适配的对象,本例中,指提供Iperson接口的对象。我们通过__init__保存这个作为一个实例变量,以便在后面引用。
We could now do something like this:
我们现在能实现:
然而。仍旧有个问题:我们怎样精确地知道为一个特定的对象调用哪一个适配器?怎样有效地将适配器、被适配的对象、使用适配器的代码捆绑在一起?
Luckily, the Zope 3 Component Architecture knows how to find the right adapter if you only tell it about the available adapters. We do that using ZCML, the Zope Configuration Markup Language. This is an XML dialect that is used to configure many aspects of Zope 3 code, such as permissions and component registration. You can do what ZCML does in Python code as well, but typically it's more convenient to use ZCML because it allows you to separate your logic from your configuration.
幸运的是,zope 3部件体系知道怎样找到正确的适配器。通过用ZCML。这是一个XML格式用于配置zope 3代码的样式,诸如权限、部件注册等。python代码能做的配置相关的事情,ZCML都能做。并且ZCML更好,因为可以从应用中分离出逻辑。
ZCML directives are stored in file called configure.zcml, which itself may include other files. A configure.zcml file in your product directory (Products/myproduct/configure.zcml) will be picked up automatically by Five. Here is a snippet that will register the above adapter:
ZCML 语句保持在configure.zcml文件中,该文件位于product目录(Products/myproduct/configure.zcml),能够被Five自动提取,下面是注册adapter的程序片段
<adapter factory=".shoes.PersonWearingShoes" />
注册完整形式如下:
这里我们指定了一个完全.分割的名称到接口。这和我们定义adapter时调用adapts() 和 implements() 的形式完全一样。注意adapts() 在ZOPE 2.9之前不被支持 (因此,在ZCML中应为for这个属性强制指定),如果你的adapter类因为某些原因(如:继承了另外一个adapter,同时又有自己的implements())实现了多个接口,你应该ZCML中需要指定provides ,以便让zope 3知道你适配的是哪一个接口。
Notice here that the dotted names begin with dot. This means "relative to the current package". You can write "..foo.bar" to reference the parent package as well. You could specify an absolute path instead, e.g. Products.Archetypes.interfaces.IBaseObject or zope.app.annotation.interfaces.IAttributeAnnotatable. Typically, you use the full dotted name for things in other packages and the relative name for things in your own package.
注意,这儿“.”开始的命名方式意味路径这“相对于当前包”。也即能用"..foo.bar" 去引用父级包。当然,也能指定绝对路径,例如Products.Archetypes.interfaces.IBaseObject 或 zope.app.annotation.interfaces.IAttributeAnnotatable。通常我们采用"."开始并分割的名称。
The factory attribute normally references a class. In Python, a class is just a callable (taking the parameters specified in its __init__() method) that returns an instance of itself. You can reference another callable as well if you need to, such as a function that takes the same parameters (only context in this case - obviously there is no self for functions), finds or constructs and object (which must provide IShoeWearing) and then returns it. This is rarely used, but can be very powerful (for example, it could find an object providing the given interface in the adapted object's annotations - but don't worry if you don't understand that for now).
这个factory 属性通常引用一个 class,一个可以调用的class(通过他的__init__()方法来取得参数)被调用时将返回该class的一个实例。后文理解不透暂不翻译。
With this wiring in place, we can now find an adapter for an IPerson to IShoeWearing. The Component Architecture will ensure that we find the correct adapter:
用这个贯穿始终,我们我们找到一个adapter为适配Iperson到IShoeWearing。部件体系将确保我们找到正确的adapter。
我们调用接口来方便地查找adapter。如果adapter找不到,将有个ComponentLookupError 错误。在zope.component 模块中有大量发现adapter以及其他部件,详情参见zope.component.interfaces 。
It is important to realise that the adapter lookup is essentially a search. The Component Architecture will look at the interfaces provided by person and look for a suitable adapter to IShoeWearing. As mentioned before, it's possible for an object to provide many interfaces, e.g. inherited from its base classes, implemented explicitly by the object (by declaring implements(IFoo, IBar)), via ZCML or because an object directly provides an interface. It is therefore possible that there are multiple adapters that could be applicable. In this case, Zope 3 will use the interface resolution order (IRO) to find the most specific adapter. The IRO is much like you would expect of polymorphism in traditional OOP:
明白adapter查询过程实际上是一个搜索过程。部件体系将查看由person提供的接口,并查找一个为IShoeWearing配套的适配器。正如前面提到的,一个对象可能提供多个接口,因此可能有多个adapter可以被应用。在这种情况下,zope3将用interface resolution order (IRO)来找到最优先的adapter.这个IRO类似于传统OOP设计的多态。
还记得 marker interface吗?marker interface的作用暗示一个特别的adapter。考虑这种情况 当你有特定的adpter为适配某些marker interface IAmputee 到IShoeWearing。如果你标记一个person作为一个IAmputee,这个IShoeWearing adapter不会去修改apparel列表,而会发出一个警告。
All of this may seem a little roundabout and unfamiliar, but you'll get to grips with it soon enough. Let's re-cap how we arrived at this:
所有这些看起来很曲折难以理解。但是你将马上要认真处理这些。现在,让我们来回顾总结下:
We modelled an aspect of a person (or other object) for wearing shoes - IShoeWearing We wrote some simple classes that implemented the domain interfaces IPerson and IShoe We wrote and registered a simple adapter that could adapt an IPerson to IShoeWearing
然后,我们来说明怎样应用。假定有某些客户代码,这些客户代码仅仅需要知道IPerson和IShoeWearing,而不必关心一个人穿鞋子是怎样实现的。部件体现确保找到恰当的适配器,无论这个Person是一个vanilla IPerson,还是一个带特殊子接口的子类,或者是一个带marker interface的实例。
Multi-adapters, named adapters and views多适配器,命名适配器和视图
In the example above, we used an adapter with a single context. That is the most common form of adapter, but sometimes there is more than one object that forms the context of an adapter. As a rule of thumb, if you find yourself passing a particular parameter into every method of an adapter, it should probably be a multi-adapter.
在上面的例子中,我们演示了带简单上下文的适配器。这是适配器最通常的用法,但是,在某些情况下,一个适配器的上下文可能有多个对象。单凭经验,你能发现传送一个特定的产生到一个适配器的每个方法,这也许应当称为多适配器。
The most common example of a multi-adapter that you will come across is that of a view, which incidentally is also how Zope 3 solves the "where do I put my view logic" code. We will cover views in detail later, but for now think of them as a python class that is automatically instantiated and bound to a page template when it's rendered. In the template, the variable view refers to the view instance and can be used in TAL expressions to gain things to render or loop on.
多适配器最通常的例子是视图。也附带说明zope 3怎样解决"表现逻辑放在哪儿"。稍后,我们将详细讨论视图,现在仅仅将视图 看作一个python class,当呈现时,这种class 是自动实例化并被绑定到页面模版。在模版中,变量view引用视图view实例,能用在TAL表达式中,获得需要呈现的东东。
When dealing with a view, there are two things that make up its context - the context content object (conventionally called context) and the current request (conventionally called request). Thus, a view class is a multi-adapter from the tuple (context, request) to IBrowserView. As it happens, there are ZCML directives called browser:page and browser:view that make it easier to register a view and bind a page template to it, handle security etc. However, abstractly a view looks like this:
当和视图交换时,有两个事情特别重要,一个是对象的上下文(通常叫context),另一个是当前request(通常叫request)。因而,一个视图类是一个多适配器,适配元组(context, request)到IBrowserView.由于有这个,ZCML语句调用browser:page 和browser:view ,并容易地注册一个视图,并绑定一个页面模版到视图上,还能操作安全关系等等。然而,抽象层面来看,视图就是这么回事。
注意怎样适配IPerson和 IHttpRequest,因此在__init__()中提供两个参数。稍后,你能学到,视图通常继承BrowserView 基类,但两者的原理是一样的。
To obtain a multi-adapter, you can't use the "calling an interface" syntax that you use for a regular adapter. Instead, you must use the getMultiAdapter() method:
为包括一个多适配器,不能用普通适配器"调用接口"的方法,而应该用 getMultiAdapter() 方法。
如果你想在找不到adapter的情况下,返回一个none而不报错,应该用queryMultiAdapter() 代替getMultiAdapter()。
The above code has a problem, however (apart from being an incomplete example) - what if you have more than one view on the same object, say for two different tabs? To resolve this ambiguity, views are actually named multi-adapters. The names correspond to the names used as part a URL, and are registered using the name attribute in ZCML. This is used in browser:page and browser:view directives, but can also be used in the standard adapter directive:
然而,上面的例子是不完整的,如果同一个对象有多个视图,比如说对象提供两个不同的tab。为解决这种问题,要采用命名的多适配器,视图名称对应为URL的一部分,通过ZCML用name属性注册。通常是放在browser:page and browser:view语句里面完成注册,但也能用在标准的适配器 配置语句里面。
<adapter factory=".sampleviews.PersonView" name="index.html" />To get this particular view, we can write:
为取得这个特定的视图,我们代码如下:
通常,我们用命名适配器时,停用要求的接口。
Multi-adapters are useful for other things as well. If you have an adapter and find that every method takes at a common parameter, it's a good candidate for a multi-adapter. Also observe that in the case above, we could register a different adapter for a different type of request as well as for a different type of object. Again, the Component Arhictecture will find the most specific one looking at both interfaces.
Named adapters do not have to be multi-adapters, of course. They are typically used in cases where something (e.g. the user) is making a selection from a set of possible choices (such as choosing the particular view among many possible views).
命名适配器不一定是多适配器,当一个用户从多个视图中选择一个时,就用到这种情况。
Utilities工具应用
In the CMF, we have tools, which are essentially singletons. They contain various methods and attributes and may be found using the ubiquitous getToolByName() function. The main problem with tools is that they live in content space, as objects in the ZODB, and require a lot of Zope 2 specific things.
在CMF中,tools本质上是独立的。这些tools包含各种方法和属性,可以用gettoolbyName()取到。这些tools都对象上下文空间中有效,并且要求大量zope 2特性。
Let's say we had a shoe locating service (very useful when you can't find your shoes):
我们设计个查找shoe的服务:
The Component Architecture contains a very flexible utility registry, which lets you look up things by interface and possibly by name. Unlike adapters, utilities do not have context, and they are instantiated only once, when Zope starts up. Global utilities are not persistent (but local utilities are - see below).
部件体系包含灵活的工具应用注册,可以通过接口和名称准确找到工具。工具和适配器不同,不需要context(上下文),他们仅仅当zope启动时,实力化一次。
As with adapters, we register utilities with ZCML:
向用适配器一样,我们用ZCML注册工具应用:
另外,可以不在工具应用的factory中调用implements(),而将其放在ZCML中。这种情况在当你的工具应用要支持多个接口时是必要的。
现在,通过getUtility() 找到工具应用:
The utility registry turns out to be a very useful generic registry, because like the adapter registry, it can manage named utilities. Let's say that you had a few different shoes you wanted to keep around:
工具应用的注册湘适配器注册一样,可以采用命名的工具应用。假定你周围有各式各样的shoe:
我们通过用name参数,用getUtility() 能找到这些工具应用:
当然,上例只是短暂的全局应用注册,当zope重启时,就会消失。我们能用本地部件代替(下文),或者通过ZCML注册。假定我们在shoes.py模块中定义了shoes left 和shoes right,在ZCML中能如下配置:
另外一种方法,可以定义两个classes LeftShoe and RightShoe 用ZCML语句的factory属性取代部件(引用一个实力代替引用一个class/factory)
Local components本地部件
The examples above all use global, transient registries that are reloaded each time Zope is restarted. That is certainly what you want for code and functionality. Sometimes, you would like for utilities to be a bit more like their CMF cousins and also manage persistent state. To achieve that you need to use local components, which are stored in the ZODB.
上文所有例子都是采用全局的、短暂的注册,必须在zope每次重启后,重新调入。你也许想能象CMF一样,能够管理这些工具应用一个可持续的状态。这时,你需要保存在ZODB中的本地部件。
Prior to Zope 3.3, which is included in Zope 2.10, local components were a bit of a black art. Then came the jim-adapter branch and everything was greatly simplified. The theory is still the same, the API is just much more sane. Each time Zope executes a request (or if you implicitly invoke zope.component.setSite(), for example in a test), it discovers which is the nearest site to the context. In Plone, the site is normally the root of the Plone instance, but in theory any folder could be turned into a site.
每次zope执行一个request(或者如果你在测试中调用了zope.component.setSite(), )系统将最近的站点作为context.在Plone中,最近的站点通常是Plone实例的根,但原理上任何文件夹都可视作一个站点。
A site has a local component registry, where local utilities and adapters may be defined. This means that a particular utility or adapter can be specific to a particular Plone site, not affecting other Plone instances in the same Zope instance. You cannot use ZCML to register local components, since ZCML is inherently global (at least for now) - it does not know anything about your particular sites. However, you can register them with Python code, e.g. in an Install.py or a GenericSetup profile, using calls like provideUtility() (and its equivalent, provideAdapter()) called on a local site manager instance:
站点有本地部件注册功能,能注册本地工具应用、适配器。这意味着一个特别的工具应用或适配器能够指定到一个特别的plone站点,而不会影响同一个zope实例中的其他Plone实例。不能用ZCML注册本地部件,因为ZCML继承了global,ZCML不知道任何关于特定站点的东西。然而,可以用python代码来注册他们,例如在install.py或GenericSetup配置文件中,用类似于provideUtility() (或provideAdapter() )来调用在一个本地站点实例中。
不幸的是,Plone2.5没有运行在zope2.10上,所以,我们在此不深入介绍本地部件。本地部件在Plone3中将变得更加重要。
b-org does not use local components yet, and we will see how the extension mechanism would benefit from local components so that you could have one b-org extension installed in one Plone instance and another extension installed in another Plone instance, without the two interfering. Luckily, to code that uses adapters and utilities, it is completely transparent whether they are global or local.
b-org项目也不用本地部件,但我们将看到来源于本地部件的扩展机制。因此我们可以一个b-org扩展安装在一个plone实例,另一个扩展安装到另一个plone实例,他们之间不会相互影响。幸运的是,工具应用和适配器的代码是完全透明的,无论是global 还是local。
Conclusion结论
That's it! If you can master the concepts of interfaces, adapters and utilities you will go far in a Zope 3 world. They will become much more natural as you use them a few times, and you'll probably wonder how you ever managed without them. Hopefully, that point will come before the end of this tutorial, which is largely focused on showing how the principle of separation of concerns can be imposed upon your Archetypes and Plone code.
这就是 zope 3 部件体系结构的介绍。如果你理解了接口、适配器、工具应用的概义,你就可以去到(扬名)zope 3世界。
这些东西,在你多次使用后,将变得很自然。我们着重在将松散耦合原理强加在Archetypes 和Plone code之上。
Overview of b-org b-org项目概览The big picture
To the user, b-org presents itself as three content types:
b-org定义了三种内容类型:
Department A container for employees, and a source of groups. That is, each department becomes a group, and the employees within that department become group members. 一个包含员工的父对象,用户组的来源,每一个部门可以是一个组,从属于这个部门的员工自动成为组的成员。 Employee Information about employees, and a source of users. That is, each active employee object becomes a user who can log in and interact with the portal. 员工相关信息 ,用户的一个来源。也即每一个员工都将成为一个能登陆该项目的用户。Project A project workspace - a folder where employees can collaborate on content. Content inside the project folder has a custom workflow, and employees who are related to the project (by reference) have elevated permissions over this content. 项目空间,员工能够协作处理里面的内容,并且这些内容都定制了工作流,每个员工对其相关的内容有严格的权限。
Out of the box, these are not terribly interesting, because they have only the minimum of metadata required to function. The task of providing actual schema fields, view templates, content type names (if Department, Employee and Project are not appropriate) and other application-specific facets is left up to simpler third-party products that plug into b-org. One example of such a product is included, which models a hypothetical charity use case and is called charity.
脱离本文,上述这些没有多大意思,因为仅仅为了满足功能而定义了最小的元数据。提供实际schema字段的任务、视图模版,内容
类型类型名称以及面向应用的一些特别的东西都被提到可以插入b-org的第三方产品当中。b-org附带的一个样例产品,该产品定义了
一个charity模型,这也是charity名称的由来。
This seemingly innocuous orchestration of functionality is achieved by a variety of means:
这个项目达到的功能很显然是由很多成员完成的:
Archetypes Used to build the actual content types and their schemata. 用于建立实际的内容类型和他们的schemate(元数据) The Zope 3 Component Architecture Is used to make all this exensibility possible - you will see lots of examples of interfaces, adapters and utilities. 这个部件体系是该项目具备扩展功能的原因。你将看到大量的接口、适配器和工具应用。 Membrane The content types are registered with membrane to be able to act as groups and users 内容类型用membrane 注册能够被视为组和用户。PAS and PlonePAS The Pluggable Authentication Service is used by membrane to actually provide user sources. A custom
PAS plug-in is also used to manage local roles for members and managers within projects and
departments. 由membrane采用的可插拔认真服务 提供用户源。定制的PAS插件用来管理成员的本地角色和项目内的managers和部门。 GenericSetup The next-generation set-up and installation framework is used to install and configure b-org. charity demonstrates how GenericSetup XML profiles can be used directly, without depending on the actual GenericSetup import mechanism. 用来安装配置b-org的下一代安装配置框架。charity证明GenericSetup XML profiles 能够直接使用,而不必依靠实际的GenericSetup import 机制。Zope 3 events
Zope 3's event dispatch mechanism is used to ensure employee users actually own their own Employee
objects, among other things. zope3的事件分发机制用于确保员工用户在众多东西当中拥有他们自己的对象。Zope 3 views The charity demo uses views for its display templates. charity应用将演示视图以及视图的显示模版。 Annotations Employees' passwords are hashed and stored in an annotation 员工口令被hash并存储在一个annotatin. Placeful workflow To let content inside projects have a different workflow to that of the rest of the site, each project uses a CMFPlacefulWorkflow policy. 为让该项目内的内容有不同于站点其他部分的工作流,每个项目采用一个定制的CMFPlacefulWorkflow 策略。On the following pages, you will learn about each of these components and how it fits together. Meanwhile,
you can follow along the code by looking in the subversion repository, or getting b-org from its
product page.
在下面的章节将解释每一个这样的部件怎样集成在一起。
To Archetype or not to ArchetypeArchetypes is still the most complete framework for building content types quickly. With the advent of Zope 3, there is an alternative in Zope 3 schemas. Here's why b-org doesn't use them.
There is a growing consensus that Archetypes has grown a little too organically. On the one hand, Archetypes has given us a lot of flexibility, and made many of us more productive than we would ever have thought possible (for those who remember the heady days of plain Zope 2, and then plain CMF development). On the other hand, Archetypes has become fairly monolithic. The reference engine, for example, is woven tightly into the field type machinery, and the way that views are composed from widgets makes these almost impossible to re-use outside of Archetypes.
In practical terms, the biggest headache that arises from Archetypes' evolution is the very same problem we identified when introducing Zope 3 concepts - it's hard to re-use Archetypes-based components without sub-classing and repeating a large portion of a type's configuration. Take the Poi issue tracker, for example - I frequently get requests from people who want to add a few use-case specific fields to each issue, or add some new functionality such as having private issues or issues submitted on behalf of someone else. The problem is that I don't want to put all this functionality in Poi itself, because this would increase the complexity of the product and thus the maintenance burden and probably impact the intuitiveness of the UI, when in reality not everyone would benefit from such new features.
Ideally, someone would be able to plug in their own schema fields and add some logic in well-defined places without having to re-invent all of Poi. However, this is difficult, because, for example, the "add issue" button assumes you are adding a PoiIssue object, which has a schema defined wholly in Products/Poi/content/PoiIssue.py. There are custom form controller scripts to handle saving of issues, and a lot of methods are found in the various content classes to do things like send mail notifications or perform issue searches for various lists. Again, changing the logic of who gets an email notification or how a particular list of open issues is calculated may involve subclassing one or all of Poi's content types, re-registering view templates and other content type information, and possibly customise a number of templates and scripts to reference the new subclassed types. Of course, when Poi itself changes, keeping these customisations up-to-date becomes difficult.
Zope 3 has, in keeping with its philosophy, approached these problems by promising separation of concerns. In Zope 3, you would typically define an interface that specifies the schema of a content type, and then create a class that is only concerned with holding and persisting the data for this schema:
Zope 3有自己的哲学(设计理念),那就是通过松散耦合来解决这些问题。在Zope 3中,通常通过定义一个接口来描述一个内容类型的schema,然后创建一个class,该class仅仅关心怎样操纵和处理数据为该schema。
象发送通知等一些具体的功能将由适配器来完成。表单能用zope.formlib从schema接口中提取信息来创建。这能恰当地添加表单()、验证输入、编辑表单等等。每个表单、适配器、菜单条(比如"add"菜单条等)被独立注册,意味着所有这些都能被独立的覆盖和定制。这里有一
个很好的formlib教程:how to use formlib in a Plone context 。
There are voices that say we should dump Archetypes entirely in favour of Zope 3-style content objects. Other voices (including my own) say that this may be a bit premature. Certainly, Zope 3 schemas and content objects are not yet fully integrated into CMF and Plone, so you end up depending on some CMF base classes at the very least. Moreover, the number and richness of widgets available for Zope 3 forms does not yet match that of Archetypes. Fundamentally, Archetypes has been around for a long time and has grown to meet a wide variety of use cases, whereas in the context of Plone at least, Zope 3 schemas are a new kid on the block.
The point is - Archetypes is not going to go away, not for a long time anyway, and are still the right choice for many types of applications. Almost all of Plone's add-on products use Archetypes, and it is well-understood by our developer community. The more likely scenario is that Archetypes will evolve in the same way that Zope 2 is evolving, by seeing its internals refactored piecemeal and pragmatically to take advantage of Zope 3 equivalents and concepts, until theoretically an Archetypes schema and content object is just a different spelling for what Zope 3 is doing, and Zope 3's content type story offers the same richness as Archetypes does (and more).
In the meantime, Archetypes is the right choice for b-org (and for other membrane-based systems). What we will try to do, however, is to alleviate the aforementioned problems by making use of Zope 3 design techniques, in order to make b-org extensible and flexible.
The extension story 扩展样例One of the main drivers behind the componentisation of b-org is that it should be easy to extend and customise for third party developers. We'll take a look at how such customisations may look, before considering how we made it possible.
利用b-org部件体系的一个主要动力是其能被第三方开发人员容易地扩展和定制。下面,我们来看下怎样定制。
b-org ships with an example called charity, found in the examples/charity directory, which demonstrates one use-case specific implementation of b-org. This is quite simple, consisting of the following top-level files and directories:
b-org附带一个charity样例,在examples/charity目录,该例子b-org的一个具体应用实现。他的目录结构如下:
configure.zcmlRegisters the schema extension adapters (see below) and references the browser package 注册扩展schema的适配器,并引用browser包 Extensions/ Contains an Install.py script that configures the Factory Type Information for the Department, Employee and Project content types. It does so by using GenericSetup XML files, but invokes the import handlers explicitly rather than through a GenericSetup profile. 包含一个install.py安装脚本,用来为 Department, Employee and Project内容类型配置FTI。通过调用import handlers 来采用GenericSetup XML 代替GenericSetup profile。 browser/Contains Zope 3 views for the charity department, employee and project content types, and a configure.zcml to register these. More on views in a later section. 包含为Department, Employee and Project内容类型 的视图,以及注册这些视图的ZCML文件。schema/ Contains adapters that extend the schemas for Departments, Employees and Projects with use-case specific fields. 包含为 Department, Employee and Project内容类型 扩展schema的适配器和用户特别的字段。
To use charity you should copy or symlink it from Products/borg/examples/charity to Products/charity. It can be
installed as normal, but you must install b-org first. See borg/README.txt for the full install instructions!A key
aim is to make it possible to meaningfully extend b-org without needing to subclass all its types. Of course,
you can do that, but in most cases it's not necessary. Unfortunately, the mechanisms and techniques
described here will be "global" in nature. That is, you will not be able to have two different modes of
ustomisation for two different Plone instances in the same Zope instance. This is because prior to Zope
2.10 (which Plone 2.5 does not support - it wasn't out until several months after Plone 2.5 was released),
the "local" components story in Zope 3 was not fully developed. There is also a specific problem with the way
the schema extension mechanism works which makes it inherently global.
对于使用charity应用,你应该copy或符号链接 Products/borg/examples/charity 到Plone的产品目录Products/charity,并且
在安装它之前,安装好b-org.charity应用最大的看点是能扩展b-org而不需要subclass所有类型。不幸的是由于这里采用的机制和技
术是"global",所以,对于同一个zope实例下的不同plone实例不能产生两个不同的模型。
When Plone 3.0 rolls around, it will support local components much better, and Archetypes 1.5, in conjunction with a third-party product called ContentFlavors (or possibly another similar tool), will enable the kind of extension story described here to work on almost any type. At that point, the forerunner you see in b-org now will be obsolete.
Plone 3到来后将支持本地部件和Archetypes 1.5再联合象contentFlavors这样的三方产品,理论上讲可以扩展任何类型。
Of course, if you don't need two different b-org customisations for two different Plone sites in the same Zope instance (which I suspect most people can work around - having two separate Zope instances of course isolates you from all of this), you should be fine.
当然,如果你不需要在同一zope下为两个不同的plone实例定制两个不同的b-org,则丝毫不受影响。
The schemas extendersIf you look at charity/configure.zcml you will see the following registrations:
detail later, but here is how they look from the point of view of the extending product:
这些schema扩展是挂钩到b-org特定部分的适配器,我们稍后将详细描述:
calculating the schema of a content type, the b-org types (by virtue of Products.borg.content.schema.ExtensibleSchemaSupport, a mix-in class that all the b-org types uses, and which
the aforementioned changes to Archetypes should make obsolete) will look up an adapter from the content
object (which is marked with IEmployeeContent, in this case), to ISchemaExtender. This will be given the chance
to extend (and modify) the schema of the type.
为了计算(组建)一个内容类型的schema,b-org类型将查找一个适配该内容对象到ISchemaExtender的适配器,从而给出机会扩展和修
该该类型的schema。
The returned value is cached (to avoid an expensive re-calculation each time the schema is used). This cache
can be invalidated upon an event, which you will see in charity/Extensions/Install.py:
schema计算返回的值被缓存(避免每次使用该schema时重新计算)。这个缓存值能通过一个事件来声明无效,具体参见
charity/Extensions/Install.py
argument to know which class the schema is being invalidated for.
该事件是实现ISchemaInvalidatedEvent 接口的类的一个势力,它取得一个类名作为参数,以明确将被通知无效的schema是哪一个类的。
Defining new views and type information定义新的视图和类型信息
We have now managed to add new schema fields to Department, Employee and Project. The auto-generated
edit form will pick these up for editing, but we probably also want some custom views. We may also want to
change other aspects of the Factory Type Information (FTI) which controls how the type is presented within
Plone's UI (an FTI is an object in portal_types).
我们已经添加了新的schema字段到Department, Employee and Project 内容类型,自动生成的表单将提取所有这些字段来实现
编辑。我们也想定制某些视图,改变某些FTI的样式,通过这些来来控制内容类型在Plone UI的表现。
First, we define some views in the browser package. These are described in a later section, but lookin
at charity/configure.zcml, you will see:
首先,我们定义某些视图在browser包中,请看charity/configure.zcml:
<include package=".browser" />This will bring in charity/browser/configure.zcml, which contains several directives like:
这个语句将调用charity/browser/configure.zcml ,里面的主要语句如下:
to disambiguate views from content objects, for example) available on any employee (or rather, any object
providing IEmployeeContent).
用 Products.charity.browser.employee.EmployeeView 和charity/browser/employee.pt 模版将生成一个视图
@@charity_employee_view (@@是可选的,在此使用仅仅为消除来自内容对象视图的歧义)对任何employee有效。
We then need to tell Plone that this view should be invoked when you view an Employee object or click its
'View' tab. This is done by setting the (Default) and view method aliases for the Employee type. See
this page of the RichDocument tutorial for some background.
然后,当我们查看员工对象或点击他的"view" tab时,我们也要告诉plone这个视图将被调用。
To achieve this, we could modify portal_types/Employee in Python during the Install.py script. However, to make
it easier to define the FTI, we use a GenericSetup XML file instead. Take a look at
charity/Extensions/setup/types/Employee.py, for example:
为达到这个目的,我们能在install.py中用python修改 portal_types/Employee ,但是,我们这儿有更简单的办法来定义FTI,
我们用一个GenericSetup XML 文件即可:
这样就定义了FTI的各项,下面仔细观察我们在install.py中怎样调用更新这个配置:
This will update the FTIs by examing Products/charity/Extensions/setup/types. Each file there is named corresponding to the name of the FTI it modifies.
Adding new functionality扩展新的功能
Extending the schema and modifying the FTI to support different views is probably enough for a large number
of use cases. If you find yourself thinking "I wish I could add a method to the Employee class to support …",
take your left hand, hold it out, raise you right hand and slap your left wrist sternly, then read the section
on adapters again.
扩展schema和修改FTI以支持不同的视图,对于大多数应用情况,这两点就够了。如果你在考虑怎样添加新的的功能,这个
能方便地由适配器来做,请看下文:
For example, let's say you wanted to send an email to administrators when a particular button in the view
was clicked. You could do that in an adapter. For examples, in your interfaces module, you could could have:
举例,假设你想通过点击视图上某个按钮,发送一个邮件到管理员。你能用一个适配器来实现这个。在接口模块,增加一个接口
定义:
Then, an adapter from IEmployee in module nag.py:
然后,在nag.py中定义一个适配器 适配IEmployee 到IAdministratorNagging
And finally, in your configure.zcml:
Then, in the form handler that is about to nag the employee, you would do:
然后,在表单界面的动作触发句柄中:
Modifying workflow and other configuration修改工作流和其他配置
The b-org workflows are not special. In your Install.py, you could modify them or change the workflow assignments as you would any other content type. You can also use CMFPlacefulWorkflow to assign different
workflows depending on context, if need be.
b-org工作流没有什么特别,你定制它的工作流就像定制其他内容类型一样。你也能采用CMFPlacefulWorkflow 指派取决于上下文的
不同工作流。
Similarly, if you need to modify the behaviour of the Department, Employee and Project types in other ways,
for example by modifying settings in portal_properties, you are of course free to do so. The intended pattern
is that your b-org customisation product encapsulates the various settings and extensions that describe your
use case.
类似的你也可以用其他方法来修改Department, Employee and Project 类型的行为。如 可以通过ZMI修改Portal_properties设置。理想的模式是由b-org定制产品来封装各种设置和扩展以满足你的应用要求。
Changing fundamental b-org behaviour变更基本的b-org行为
Lastly, as you learn about b-org you will see how it uses adapters to hook into membrane. If you need to
override its behaviour, you can add an overrides.zcml to your product, which is otherwise identical to a
configure.zcml in format, but is able to override earlier registrations (such s those in b-org). For example, you
could override the adapter from IEmployeeContent to IUseRelated to change the way in which user ids is
assigned, or the adapter to IUserAuthentication to change the way in which authentication is performed.
最后,将学习b-org怎样用适配器来插入membrane。如果你要覆盖某些行为,你可以在产品包中加一个overrides.zcml ,这个文件的格式和configure.zcml 一样,但是它能覆盖先前的注册。例如你可以覆盖一个适配 IEmployee到 IuseRelated的适配器 ,以改变当一个用户id被指派时的行为。或者覆盖为 IUserAuthentication 适配的适配器,以改变当一个认证过程被执行时的行为。
Filesystem organisation 文件系统组织b-org attempts to ahere to modern ideal about how code should be laid out on the filesystem.
b-org在此将演示怎样以理想的模式组织在文件系统中。
In the Zope 3 world, the Products pseudo-namespace is frowned upon. In Zope 2, every extension module
lives in the Products/ folder. This raises some obvious namespace clash concerns, but also separates Zope
modules further from plain-Python modules. In Zope 3, you can install a module anywhere in your
PYTHONPATH. For example, in Plone 3.0, there will be a module called plone.portlets, normally installed in lib/python/plone/portlets.
在Zope 2的世界,所有产品扩展模块都在Products文件夹下。这可能导致某些名称空间冲突,但也能隔离zope模块从平面的pyton模块到更深的路径模块。在Zope 3中,你可以将模块安装到PYTHONPath路径的任何地方。例如,在plone3中,有个模块叫Plone.portlets,通常可安装在lib/python/plone/portlets
For module that need to act like Zope products (i.e. they need an initialize() method, they install content types,
they register a GenericSetup profile or CMF skins or use an Extensions/Install.py method, say), this works in
Zope 2.10 and later. It can also be made to work in earlier version of Zope using a product (ironically) called
pythonproducts.
For the purposes of borg, we stick with the traditional Products/ installation. It's nice to have imports like
from borg import …, but fundamentally, b-org is very closely tied to Zope (2) and Plone, so the re-use argument
goes away, and that nice import syntax is not really worth the extra dependency and configuration.
One thing you may notice, though, is that the borg product is named in lowercase, in keeping with Zope 3 and
Python naming conventions. Looking inside it, you will see the following key files and directories:
对于b-org,我们严格地用传统的Products/intallation模式。
__init__.py Initialises the Zope 2 product machinery, registers content types, the skin layer and the GenericSetup
extension profile that is used to install b-org. zope2产品机制初始化,注册部件内容类型、皮肤和需要安装b-org的genericeSetup扩展配置文件。 config.py Holds various constants 定义各种常量。 configure.zcml Starts the Zope 3 snowball going. This references other packages with their own configure.zcml files. zope 3技术,用以引用其他包和这些包自己的configure.zcml配置文件。 content/ Contains the Archetypes content types for Department, Employee and Project. Also contains some utilities,
like EmployeeLocator, an adapter to find employees, two utilities used to provide vocabularies AddableTypesProvider and ValidRolesProvider, and the the schema extension mechanism in schema.py. 包含Department, Employee and Project 的Archetpes内容类型,和某些工具应用。如EmployeeLocator 寻找员工的适配器,AddableTypesProvider and ValidRolesProvider 两个工具应用。还有个schema扩展。events/ Contains event subscribers which modify ownership of an Employee object so that the employee user
owns it (and can thus edit their own profiles, for example), as well as to set up the local workflow when a Project is created. 包含修改员工对象的从属关系以便员工用户拥有它的事件预定,以及当一个项目创建时,为其设置工作流。interfaces/ Contains all the interfaces that b-org defines, in various sub-modules like interfaces/employee.py for the
Employee-related interfaces. All of these are imported into interfaces/__init__.py, so that you can write
from Products.borg.interfaces import …. 包含b-org项目的所有接口,所有这些接口被导入到interfaces/__init__.py中,以便通过from Products.borg.interfaces import …. 导入membership/Contains various adapters for plugging into membrane which enable b-orgs user-and-group functionality. 包含各种为使用用户组功能而嵌入membrane的各种适配器。 pas/ Contains a custom PAS plug-in which is used to manage the local roles for Project members 包含用于管理本地项目成员角色而定制的PAS插件。 permissions.py Contains custom add-content permissions, so that the ability to add Department, Employee and Project
content objects can be controlled by different permissions. 包含定制添加的内容权限,以便能添加部门、员工和项目内容对象。 profiles/ Contains the GenericSetup extension profile that sets up b-org. This is registered in the borg/__init__.py. 包含用于设置b-org的GenericeSetup的扩展配置文件,该文件在 borg/__init__.py注册。setuphandlers.py Defines a custom GenericSetup "import step" which configures aspects of b-org that cannot be expressed
in the existing GenericSetup XML formats. 定制GenericSetup "import step",配置b-org样式,弥补某些不能在已存在的 GenericSetup XML表述的部分。skins/ Contains the borg skin layer, which is registered in borg/__init__.py. This contains only the b-org icons. These could potentially have been defined in a browser package using Zope 3 resources, but are included in a traditional skin layer to make them easier to customise using conventional methods. See the section on Zope 3 views for more details.定义b-org皮肤层,目前仅仅包含b-org图标。 tests/Contains unit and integration tests. 包含单元测试和集成测试。 zmi/ Defines a ZMI page for adding the PAS plug-in, for completeness' sake. 为通过ZMI添加PAS插件而定义的一个zmi页。 You will notice that there are many directories, and many of these directories contain the same set of files -
employee.py, department.py and project.py. This is a side-effect of the finer-grained components and increased
separation of concerns that stem from Zope 3 design concepts. For products that act less as framework, the
degree of separation may be lower, and thus the product may appear smaller. However, as you browse b-org's
source code, it should become obvious why things are placed where they are, and how code is grouped
together by logical functionality rather than a tight coupling to Archetypes content types.
Interfaces 接口In Zope 3, everything is connected to an interface in some way. Sure enough, b-org has a slew of them.
Getting the interface design right is often more than half the battle, so pay attention to this part.
在ZOPE 3中,任何东西都通过某些方法连接到接口。b-org中有很多这样的接口。设计正确的接口是成功的一半,请充分注重接口这部分。
If you were trying to understand b-org without a comprehensive tutorial to hand, you would do well to look at the interfaces package. You will notice that this is subdivided into various files
如果没有一个合适的教程,要理解b-org,最好认真看看interfaces包,你可以发现这里再细分出很多文件。
interfaces/department.py Contains a description of a department (IDepartment) and a marker interface for the content object that stores the department (IDepartmentContent). 包含一个department接口和一个为保存部门数据的对象的marker interface。 interfaces/employee.py Contains the equivalent interfaces, IEmployee and IEmployeeContent, as well as the definition of a specific
event interface, IEmployeeModified. 包含 两个对等的接口,一个IEmployee,一个IEmployeeContent,另外还有一个事件接口IEmployeeModified。interfaces/project.py Again contains IProject and IProjectContent, as well ILocalWorkflowSelection, which is used to denote a utility that defines the placeful workflow policy that projects will use. 包含IProject 和IProjectContent 以及一个用于定义工作流的工具应用的ILocalWorkflowSelection 。interfaces/workspace.py Holds the interface IWorkspace, which is used by the local-role PAS plug-in to extract which users should
have which local roles in a project. interfaces/schema.py Contains interfaces relevant to the custom schema extension mechanism - ISchemaExtender, IExtensibleSchemaProvider and ISchemaInvalidatedEvent. 包含定制扩展shema机制的相关接口,如ISchemaExtender, IExtensibleSchemaProvider and ISchemaInvalidatedEvent 。interfaces/utils.py Defines interfaces that are used as input to various vocabularies - IEmployeeLocator, IAddableTypesProvider and IValidRolesProvider. 定义用于输入各种词汇的接口:IEmployeeLocator, IAddableTypesProvider and IValidRolesProvider 。
In order to understand what each of these interfaces describes in more detail, look at the files above. Recall that interfaces are mainly documentation - these interfaces are accompanied by docstrings and generally self-documenting code.
为了更详细了解各种接口,请查看这些接口源文件。
The various interfaces intended for public consumption are imported to interfaces/__init__.py, so that client code can write, e.g.:
为了让其他模块方便调用这些接口,它们通过interfaces/__init__.py导入,其他模块可以这样引用接口:
from Products.borg.interfaces import IEmployee
This is a common idiom. If you find yourself with too many interfaces to manage in interfaces/__init__.py, you
don't necessarily need to do this, but it's probably a sign that you should be breaking your code into smaller
packages!
Remember that unless you have a particular need to depend on Zope 2, then you don't need to pollute the
Products namespace with such components! (and even if you do, with PythonProducts or Zope 2.10, you can do
without the Products/ namespace too). For example, we could have placed the employee functionality in a
package borg.employee, found in lib/python/borg/employee as a plain-python library, possibly depending on Zope
3 components (i.e. packages in the zope.* namespace).
除非你有特别的要求,否则不要破坏zope2这种产品命名空间。
Conversely, if you have relatively few interfaces, you can simply have an interfaces.py module without a directory.
如果接口很少,我们可以用一个简单的interfaces.py来代替一个接口目录。
Separating Archetypes from real components从部件中分离ArcheTypes
One thing you may notice is that we have split the interface describing the concept of e.g. an employee
(IEmployee) from the interface that describes the employee content object in the ZODB (IEmployeeContent).
Whether this is always the right thing to do is debatable, but the reasoning goes something like this:
有件事情需要注意,我们分离描述员工概义的接口IEmployee 和描述员工内容对象的接口IEmployeeContent。是否这总是正确的有待商讨,但我们这里这样做的原因是:
Archetypes objects contain a very large API. Archetypes schemas and the infamous ClassGen generate
methods on the content objects corresponding to schema fields, so that a field name gets an accessor called
getName() and a mutator called setName(). This is all rather Archetypes-specific, and in Zope 3 schemas, we
typically prefer simple properties (a name attribute) to pairs of methods. To avoid being constrained by the
Archetypes when defining interfaces (Archetypes is just one implementation choice), we created IEmployee as
follows:
Archetypes对象包含一个巨大的API。Archetypes schemas和生成schema 字段的ClassGen 生成方法。以便一个命名为name的字段自动获得一个getName() 和一个setName() 。这些是Archetypes 的独有的特性。而在zope 3 schemas中通常只有简单属性和某些方法,当定义接口时(Archetypes只是其中一种实现方式),为了避免被Archetypes限制,我们创建IEmployee:
为配合这个,我们放置相关的属性到Archetypes内容对象,但是,令人遗憾的是用于转换methods 到properties的property()申明仅仅当那些methods确实存在时,才有效,而不能由ClassGen创建。
Instead, we mark the content object with a marker interface, IEmployeeContent and then register an adapter to
IEmployee. Strictly speaking, this is cheating, since the adapter makes assumptions about its context (such as
which methods are available, and the fact that it uses Archetypes) that are not formally defined in the interface.
To save excessive typing and retain some sanity in the interface definitions, it's not a terrible compromise though.
Here's the adapter, from membership/employee.py:
一种解决方法,我们标记内容对象用一个marker interface ,即IEmployeeContent接口,然后注册一个适配器到IEployee接口。严格讲,这种方法有某些欺骗性,因为这个适配器假定他的上下文(诸如哪一个方法是有效的,以及使用Archetypes的事实)不是正规的接口定义。但为了节约某些文字输入以及保留接口定义完整性,这不是一个很糟糕的折衷办法。下面是这个适配器定义:
这种模式另外一个作用是分离某些Archetypes依靠的东西从一个员工对象更通常的概义。例如,membrane通常做些关于操纵Archetypes内容对象的假定,
Interfaces intended for utilities and adapters为工具应用提供的接口和适配器
Although interface design should generally not be too concerned with how those interfaces are implemented,
you will often think "this is going to be used a a utility" or "this will most likely be an adapter". In this case, you
may want to make some reference in the doc-string at least. For example, the ILocalWorkflowSelection interface
states:
尽管接口设计通常不太和接口的具体实现相关,但是,你还是要经常考虑"这个接口将用于一个工具应用" 还是“这个接口将用作一个适配器。”在这个意义上讲,你应该至少提供些文档字符串。例如 ILocalWorkflowSelection 接口 :
另外,很多接口是上下文相关的,意思就是他们或者由特定的一个对象提供,或者是可适配该对象的,看下IAddableTypesProvider :
The implication here is that client code will do something like:
下面是应用该接口的代码:
not important. The only time this distinction is really useful is in the case of marker interfaces, such as
IEmployeeContent:
无论IAddableTypesProvider是由上下文直接提供,还是通过一个适配器提供都不重要。重要的是这个区别在marker interface中是相当有用的,例如:
assert IEmployeeContent.providedBy(employeeContentObject)
# we've got an employee, good
Again, the guiding principle here is separation of concerns. The aspect of a component that can provide a list of addable types (IAddableTypesProvider) is logically distinct from (and could be varied independently of) the aspect
of a component that specifies it represents a project (IProject), even though it so happens that at present
projects are the only time we concern ourselves with restricting addable types.
重申,主要原理是松散耦合。一个能提供可添加类型列表的部件的样式是逻辑区分于一个指定它表现一个项目(IProject)的部件的样式。尽管在这里我们关心用受限制的可添加类型。
In the olden days, we would probably have put methods like getAvailableProjectAddableTypes() into the Project
content type. Hopefully, you'll see why this is less optimal than having it in a separate component (hint: what if you in your customisation of b-org wanted to be much more particular about which types were addable?). You will
hopefully start to pick up "fat" interfaces during interface design - if you had a neat IProject interface that
described attributes of a project that were to be saved alongside the project object, and then found a couple of
methods about defining addable types that were related to one another but not so much to the data of a
project in general, you would hopefully reach for a new interface. If so - well done, you're getting there.
在过去,你也许通过放置 getAvailableProjectAddableTypes() 这样的方法在这个项目内容类型中。但在这儿,你将明白为什么如果这样做比分离部件的方式是不够优化的。(提示:……)如果你已有一个整洁的接口描述一个项目对象的属性,你将希望在接口设计中实现一个肥的(复杂的)接口,但是随后将发现关于定义可添加类型的一系列方法,并且他们是相互关联(影响)的,而且所有这些并非项目Project通常的数据。这时,你将希望设计一个新的接口——(也就是上面描述的方法)
Test-driven development 测试驱动开发Testing should come first, not last, when doing development.
当做开发时,首先要做好测试,而不是最后来做
One of the greatest things that Zope 3 has established is a culture of test-driven development. Because Zope 3 components tend to be small and not dependent on a large framework or (typically) a running application server, tests are easier to write and execute faster. Most Zope 3 testing happens in the form of testable documentation - DocTests - which tell the story of how a component should be used along with testable examples.
zope 3最重要的事情之一是建立了一种测试驱动开发文化。因为zope 3部件在大的框架中或在应用服务中通常是较小的,并且互不相关,写部件测试非常容易,运行测试也非常快。大部分的zope 3测试通过可测试文档(DocTests)的形式进行,DocTests通过可测试的例子来描述一个部件将怎样被应用。
The testing tutorial explains the philosophy behind test-driven development and the tools and techniques available in Zope. It is required reading if you are not familiar with testing in Zope, and probably quite useful even if you are.
这个testing tutorial教程解释了 在测试驱动开发和zope3 中的工具和技术背景下的测试哲学。如果你不了解zope 3下的测试,你应该好好读它,应该很有帮助。
Testing strategy 测试策略Tests were (largely) written against interfaces and stub implementations, before the actual functionality was written. One of the first test cases to be created was test_adapters.py, which simply verifies that the various adapter registrations are in effect. This is obviously an integration test (using PloneTestCase), since it is verifying what happens on a "normal" Zope start-up.
在写实际功能模块之前,针对接口和接口的实现写测试。第一个测试应当是验证各种适配器注册是正确有效的,我们命名为test_adapters.py。很明显,这是一个集成性测试(用PloneTestCase),这是验证在zope正常启动时,发生的一些事情。
You will also notice tests named after the three content types, test_department.py, test_employee.py and test_project.py. Each of these contains tests that verify the given type is available and can be instantiated and edited. This catches errors in Archetypes registrations or schemas. There are then further tests for the membrane integration and for the adapters to the canonical interfaces IDepartment, IEmployee and IProject. Lastly, non-trivial methods in content types and relevant adapters are given their own test fixtures.
你也将注意到在三个内容类型当中,还有test_department.py, test_employee.py and test_project.py,这些测试是验证给定的内容类型能正常实例化,并可编辑。捕获的错误在Archetypes注册和schema当中。更深入的测试在membrane集成和规范接口IDepartment, IEmployee and IProject,最后,内容类型当中和适配器相关的一些重要的方法的测试在他们各自的测试当中。
By being systematic and diligent with tests, many, many bugs were caught and dealt with before they ever hit a live system. Of course, this does not replace in-browser acceptance testing, which was also performed regularly.
通过系统化的、亲老的测试,许许多多bug将被发现。当然,这些不能代替实际的浏览器可接受性测试。
At the time of writing, there are no zope.testbrowser based functional tests for the user interface. That is regrettable - and this is an open source project after all, so feel free to contribute some!
在写该教程时,还没有以 zope.testbrowser 为基础功能的用户接口测试。
Test set-up 测试设置You will find b-org's tests in the tests module. Most of these use are DocTest integration tests, using PloneTestCase. Make sure you use a recent version of PloneTestCase (or svn trunk) since there have been some recent changes in how Zope 3 components (or rather, ZCML registrations) are loaded for test runs. The upshot is that with PloneTestCase, things should "just work" for integration testing - components you have defined in ZCML in your products will be loaded as they would when Zope is started.
b-org 的测试在tests模块,大多数用DocTest集成测试,用PloneTestCase。确认正在使用最新的PloneTestCase,因为有些新的变更在zope3 部件为测试运行而装载时。使用PloneTestCase的结果是在你产品中已定义的部件将正常被装载完成集成测试,就好像zope 重启一样。
The file base.py contains an insulating base class for b-org tests, called BorgTestCase and its sister-class BorgFunctionalTesetCase. When imported, this file will trigger the setup of a Plone site with the membrane and borg extension profiles installed, as such:
文件base.py包含了一个为b-org测试的基类BorgTestCase以及他的姊妹类BorgFunctionTestCase。当这个文件被导入时,将触发用membrane和borg扩展配置文件配置一个Plone站点。象下面一样:
Integration and unit tests 集成测试和单元测试Most of the tests are integration test that are set up like so:
大多数集成测试被设置为:
Set roles.
There is also a plain-python (no loading of Zope necessary, which is much faster) unit test for the password digest in test_passwords.py. This is appropriate because the functionality under test does not depend on the Zope application server or database being loaded. Use plain-python (or perhaps rather, plain Zope 3) tests whenever you can to reduce interdependencies and test load times:
这是一个纯粹python代码的单元测试。这很适合我们,因为测试功能不依靠zope 应用服务器,也不依靠数据库。你可以减少相互之间的依赖性,加快测试装载速度。
We expect that the password will be saved as a SHA-1 digest.
Set a password.
The value is stored in an annotation, and there is no direct way to
access it (deliberately). Thus, check the annotation directly.
Ensure it is what we expected:
函数(功能)configurationSetUp() and configurationTearDown() 定义在utils.py中,用来装载配置测试环境所需的ZCML文件。由于没有PloneTestCase集成到测试,当测试运行时,不会发生部件注册,所以这个是必须的。这也许令人讨厌,()但可以控制测试运行时环境,获得更快的测试执行时间。
You will also find a regular unit test in test_setup.py, simply because this was quicker to write:
你将在test_setup.py中发现一个标准的单元测试,这个非常简单:
Finally, there is an docstring DocTest for the ExtensibleSchemaSupport class. This is because this class if largely standalone (it probably shouldn't be b-org at all, but in a more general module, except Archetypes will gain similar functionality of its own for Plone 3.0) and the test provided important documentation in the class' docstring.
最后,还有一个为ExtensibleSchemaSupport 类的docstring DocTest。
The class looks like this:
在test_schema.py中的test runner包含如下:
Setup using GenericSetup 用GenericSetup安装配置b-org uses GenericSetup to impose itself on your Plone instance. Here's how it works.
b-org采用GenericSetup来安装到Plone实例中。下面是其工作原理。
Hands up if you have ever written a workflow definition in Python and tried to figure out how to install it in your Extensions.py and thought, this is the least useful API I have ever had to deal with. Actually, the API is not that bad, it's just not very good for performing set-up. Similarly, it may start to make your separation-of-concerns-brainwashed mind a little uneasy that we tend to define aspects of the type's configuration as class attributes in an Archetypes class (though of course it's better than using a CMF FTI dict or mangling portal_types directly).
如果你用python写了一个工作流定义,想要在Extensions.py中描述怎样安装它。这是我们打过交道的有用的api,对于执行安装配置,这个API还不赖,但也不是很好。我们想要在一个Archetypes类中定义某些类型配置的样式作为类属性,但以API这种方式来理解的话。不容易实现松散耦合。(尽管这种方式明显好于CMF FTI dict或直接修改portal_types)
The fine folks who gave us the CMF came up with another way, called GenericSetup (after a few name changes - you may see the names CMFSetup and ContentSetup as well, which refer to predecessors of what is not GenericSetup). This is based on a declarative XML syntax that can represent site configuration. The configuration of an entire site is called a profile and can be imported and exported to replicate state across multiple Plone (or CMF) sites. There is a smaller version of a profile called an extension profile which can be used to extend a base profile. Both membrane and b-org use extension profiles to install themselves.
聪明的人们给我们提供了另一种方法——GenericSetup(先后经历多次名称变更,CMFSetup ContentSetup等)。这个方法是用基于XML语法来描述站点配置。整个站点的配置叫一个配置文件,能在多个Plone实例间导入导出以实现复制状态信息。一个配置文件较小的版本叫扩展配置文件,能用来扩展一个基本的配置文件。membrane 和b-org都使用扩展配置文件来安装。
GenericSetup is described a tutorial by Rob Miller, cheif GenericSetup protagonist, so we won't repeat too much of the detail here. However, you should be aware that in Plone 2.5, GenericSetup has a slightly awkward user experience and does not have any well-defined way of performing uninstall, which stems from the fact that it was originally designed for the use case of taking a snapshot of the configuration of an entire site, not for installing and uninstalling products and extensions!
GenericSetup详细教程是由Rob Miller写的教程tutorial 。在此,我们不过多重复。然而,我们应该明白在Plone 2.5中,GenericSetup有些难于使用的用户体念,即 没有定义一个好的方式来用于uninstall.事实上,它原来的设计目标就是取得整个站点配置的快照,而不是为了安装或卸载产品或扩展。
The other main shortcoming at the moment is that there is no way to specify interdependencies between profiles. It is important that membrane is installed before b-org, but if you're not careful it will happen the other way around. When you create a Plone site, you will be able to choose a number of extension profiles to apply (including meaningless ones like Archetypes - meaningless because Plone already invokes those when you set up a site). In this list, "Base organisation" comes before "membrane" by virtue of alphabetical sorting. Therefore, you can't just choose both and click "Add". Instead, you should select "membrane" first, and then add "Base organisation" via portal_setup, as described in the b-org README.txt:
另外一个缺点是没有办法指定不同配置文件间的互依赖性。membrane 在b-org之前安装是重要的,但如果你不小心的话,可能出问题。当创建一个PLone站点时,有一个可供选择的配置文件列表(包括无意义的Archetypes)。在这个列表中"Base organisation"按字母顺序出现在"membrane"前面。你不能两个都选择,然后点击"Add",而应该首先选择"membrane",而后再选择安装"Base organisation"。
If you didn't already set up membrane and you created a Plone site without the membrane extension profile, follow the same steps to install membrane before you install b-org.
如果你没有设置membrane,你创建了一个不带membrane扩展配置的plone站点,你应该先安装membrane。
So why did we do all this? Firt of all, both membrane and b-org are really infrastructure that fundamentally influence how you build your site, so the lack of uninstall isn't as bad as it would have been for more user-facing products. Secondly, with Plone 3.0, this will become easier, as the QuckInstaller (and hence the Add/Remove Products control panel page) becomes Extension Profile aware and gives some uninstall support.
首先,membrane和b-org都是基础架构,能根本影响一个站点怎样建立,因此,缺乏uninstall对于多用户使用的产品不一定是坏事。其次,在Plone3.0由于QuickInstaller已变成扩展配置文件明白的部件,因此,能提供uninstall支持。
At the end of this section, you will see how you can use a traditional QuickInstaller Install.py method and still get the nice XML syntax, with a bit of extra work.
在本段的最后,我们将看下怎样用传统的QuickInstaller Install.py method 和XML语法。
Import stepsTo GenericSetup, the installation of a third party product via an extension profile is considered to be the importing of that profile. A file import_steps.xml is used to determine which actual import steps will be executed. First, we need to tell GenericSetup where the import steps are defined, though, by registering the extension profile. This is done in the product's __init__.py:
对于用GenericSetup通过扩展配置文件安装第三方产品,安装过程就是配置文件的导入。一个Import_steps.xml配置文件用于决定哪些导入步骤将被执行。首先,我们需要告诉GenericSetup导入配置文件在哪里。通常通过在产品的__init__.py中注册扩展配置文件来完成。
指向的目录profiles/default包含各种文件:
import_steps.xml Lists the steps to be performed during import (set-up) 列出在导入过程中执行的步骤。 export_steps.xml Lists the steps to be performed during export - that is, if the configuration is changed in the ZODB and the site admin wishes to export the configuration to a file, these steps will be performed. 列出导出过程执行的步骤。如果ZODB中的配置更改,站点管理员可以导出配置到一个文件。
membrane_tool.xml Configuration for membrane tools 配置membrane tools skins.xml Sets up skins in portal_types 在portal_type中设置 皮肤。types.xml Configures FTIs (Factory Type Information settings) for the content types that b-org ships with. Each of the types listed here has a corresponding file in profiles/default/types (the name of the type and the name of the file should match). This file contains all the various FTI settings, such as friendly name, meta type, actions and aliases. 为b-org的内容类型配置FTI信息。列出在这儿的每一个 类型都有一个对应的文件在profiles/default/types下。(类型的名称和文件名称匹配)这样的文件包含所有 FTI设置,比如友好名称、meta type, actions and aliases.等。workflows.xml Configures workflows. This works in the same way as types.xml - the main file configures the names of the workflows and the bindings of workflows to content types. The actual workflow definitions, including states and transitions, are found in profiles/default/workflows.配置工作流。这个类似于types.xml。这个文件配置工作流的名称和工作流到内容类型的绑定。实际的工作流定义包括状态、事务等是定义在profiles/default/workflows The import_steps.xml which orchistrates all this looks like follows:
import_steps.xml文件结构类似如下:
Note that we don't actually specify most of the files - they are referenced by the base profile that was used to set up Plone or the extension profile for membrane. GenericSetup knows all the registered profiles' steps, and looks for the corresponding files.
注意我们实际上并没有指定这些文件。他们被引用通过用于来设置plone和设置membrane扩展配置文件的base profile。GenericSetup 知道所有注册的steps,并能查找相应的文件。
Various setup handlers 各种安装/配置句柄The one setup handler you do see is the "various" handler. This is dependent on the set-up of type info, skins and workflow. Ordinarily, setup handlers will utilise GenericSetup base classes, adapters and utility functions to parse XML files. However, it's not always convenient to invent a generic XML syntax for all types of configuration. The importVarious pattern is used by many products that need to perform some custom set-up in Python. It is invoked as if it were a handler for an XML file, but it just happens to have different side-effects. The main caveat with this type of set-up, of course, is that it cannot symmetrically export (and then re-import) the configuration, and it is more difficult to re-use.
应当明白配置句柄是多种句柄。这取决于类型信息、皮肤和工作流的配置。通常配置句柄将应用GenericSetup基类、适配器和工具应用分析xml文件。然而为所有配置类型转换通常的xml语法并不总是很方便。对于某些需要用python执行某些定制配置信息的产品,通常采用importVarious模式。这个importVarious模式被调用就好像它是一个xml文件的句柄一样。但这种方式有不同的副作用。最主要的是这个配置类型不能恰当地导出配置信息,要重用很困难。
importVarious looks as follows:
importVarious模式样例:
我们这只PAS插件,用portal_factory注册类型,添加工作流策略。实现这些的代码不在此列出,但他们采用和install.py文件同样的技术。注意事实上portal_factory配置在plone 2.5.1以后版本有更友好的XML格式配置。
GenericSetup without portal_setupWhen Plone 3.0 arrives, it will make the Add/Remove Products control panel aware of extension profiles, and thus provide a more user friendly way of performing install using GenericSetup. It will also support uninstall. Until that time, however, it is possible to re-use the GenericSetup XML handlers to parse files like types.xml and workflow.xml from a regular Install.py installation. We do this in the charity example.
当plone3发布后,将使得添加删除产品控制面板能识别扩展配置文件,因此能用GenericSetup提供用户友好的方法来执行安装或卸载。在此,我们应用GenericSetup XML句柄来分析types.xml,workflow.xml,但采用标准的intall.py安装。我们在charity例子中演示。
When importing, GenericSetup requires a setup environment, and usually an object to work on. A simple SetupEnviron is found in charity/Extensions/utils.py, along with a method called updateFTI() which can take an FTI object and update its settings based on a types.xml-like file. This method takes a module and the id of an FTI to update, and finds the corresponding file.
当导入时,GenericSetup要求有一个对象工作的配置环境。在charity/Extensions/utils.py中有个SetupEnviron,还有一个method updateFTI(),能基于一个types.xml式样的文件来更新对象的FTI。这个method 取得一个module和被更新的FTI的id,然后找到对应的文件。
It is used in charity/Extensions/Install.py as follows:
应用示例如下:
其他相关文件在charity/Extensions/setup/types/ 目录中。
Using membrane to provide membership behaviour用membrane提供成员行为
How b-org uses membrane to let employees be users and departments be groups
b-org怎样用membrane来让employee模拟成用户,让部门模拟成组。
Since version 2.5, the user management infrastructure in Plone has been replaced by PAS, the Zope Pluggable Authentication Service, and PlonePAS, a Plone integration layer for this. PAS offers several advantages over plain user folders, mainly in terms of flexibility. Unfortunately, it is also more difficult to work with through-the-web and has a very decentralised API, based on the notion of plugin components, that can be difficult to understand at first.
Plone 2.5以后用户管理体系由PAS取代,即所谓的zope 可插拔认证服务,plone将layer集成进来,叫做PlonePAS。PAS比平面的用户文件夹有优势,主要是灵活性。但是它很难通过TTW方式工作,并且有大量API。基于可插拔部件概义,一开始很难理解。
Membrane (or rather, membrane with a lowercase m) is a component first developed by Plone Solutions and later improved by Rob Miller and others. It is similar to CMFMember in that it can turn content objects into users, although it is less concerned with replicating existing Plone functionality and more concerned with making a thin integration layer to plug into. It therefore fits b-org very well.
membrane是由plone solutions开发的,后来,Rob Miller加以改进。它类似于CMFMember能转换内容对象到用户,它不关心复制plone功能,只关心制作一个可插进的瘦的集成的layer。因此很适合b-org。
Membrane works on Archetypes objects (though theoretically it could be used with other objects as well). It adds a tool called membrane_tool which contains a registry of content types that are member- or group-sources, as well as a special catalog. Using the Archetypes catalog multiplex, it is able to catalog objects (which may also be cataloged in portal_catalog) and find them again based on various interfaces (that is, it catalogs the interfaces provided by an object). membrane provides a number of PAS plug-ins that will search this catalog when looking for users and delegate to the content objects (or rather, adapters on the content object) for obtaining user information, performing authentication and so on.
Membrane和Archetypes对象工作(尽管原理上它也能和其他对象一起工作)。它有一个名为membtane_tool的工具,包含一个member源或group源的内容类型注册以及一个特殊的catalog。用多元的Archetypes catalog,它能分类对象(也可以在portal_catalog中被分类),又可以基于各种接口再发现它们。(那是因为分类了由对象提供的接口)。当查找用户并委派到内容对象时,membrane提供了大量PAS插件用于为包含的用户信息,执行认证等等搜索这个目录。
Registering with membranemembrane_tool contains an API for registering content types as membership providers, but the easiest option is to use a GenericSetup profile (see the section on GenericSetup for the full story). In profiles/default/membrane_tool.xml, you will find:
membrane_tool包含一个API来注册内容类型作为成员提供者。但最简单的方法是用GenericSetup配置文件。
This registers the three content types (by their portal type), and specifies the workflow states in which they are "active" as member and group sources.
这里注册了三个内容类型,并且指定了当它们被激活作为成员源或group 源时的工作流状态。
Applying marker interfacesWhen looking for content objects that provide group and member information, membrane will use a number of marker interfaces that indicate support for various types of behaviour. These are implemented by the three content type classes.
当查找提供group或member信息的内容对象时,membrane用了大量marker interfaces,以指明各种类型的行为。这些在三个内容类型中实现。
In content/department.py, you will find:
All this means is that the Department's schema is capable of providing properties to PAS. Properties (normally related to users, but groups can have properties as well) are just metadata about the user or group. Membrane supports as PAS properties plugin that will look for Archetypes schema fields with member_property=True set and report these back as user properties. Although Department does not use any such properties at the moment, we add this marker so that extension modules that use the schema extension mechanism can benefit from this.
所有这些意味者Department的schema能提供PAS属性。属性是关于用户或组的元数据。Membrane支持PAS属性插件将查找Archetypes schema字段 用一个 member_property=True 的设置并报告作用户属性。尽管Department不用这些属性,我们在此加这个标记是为了用schema扩展机制的扩展模块能取得某些好处。
The equivalent setup for Employees, in content/employee.py, is a little more interesting.
类似Department,Employee设置如下:
Here, we are saying that:
说明一下:
Projects does not require any particular marker interfaces.
Providing membership behaviour 提供成员行为When membrane looks for objects to provide membership-related behaviour, it will not only look for objects directly providing a particular interface, but also for objects that can be adapted to that interface. For example, the presence of the interface IGroup informs membrane that an object can act as a group, and contains methods that describe the members of that group.
当membrane查找对象提供成员相关的行为,它不仅查找该对象直接提供的接口,而且也查看该对象能被适配到的接口。例如接口IGroup告知membrane该对象能扮演一个GROUP。
Of course, we could have declared that Department implemented IGroup and written these methods directly in the Department content object. Hopefully you'll agree now that this would not be optimal, since it mixes the content-object aspect and the group-behaviour aspect of Department into a single monolithic object. Instead, we will use an adapter, which also means that if you require different behaviour in an extension to b-org, you have only to override the adapter, leaving the core content object alone.
当然我们可以申明Department来实现IGroup接口,并且直接在Department内容对象中写这些methods.你现在应当明白由于混合了Department的内容对象样式和组行为样式到单个对象,这不是一种好的方式。代替这种方式,我们将用一个适配器,也即意味着如果你想b-org的一种扩展有不同的行为,仅仅需要覆盖这个适配器即可。
In membership/department.py, you will see:
Mostly, this is about examining the Department content object to find roles (which are listed in an Archetypes field, editable by the Manager role). When calculating roles, we make sure that we don't give roles if the Department group-source is actually disabled (by virtue of its workflow state and the settings in membrane_tool). The group title and id are taken from the object as well.
这是关于部门内容对象找到roles的测试。(roles 被列在Archetype字段,可由管理员角色编辑。)当获得角色时,如果Department被取消group源(取决于工作流状态的设定),则不应该给出角色,但应该能正常取得title和id。
The most interesting method is getGroupMembers(). Here, we perform a search in the membrane_tool catalog for objects adaptable to IMembraneUserAuth. This interface is the basic interface in membrane describing things that can act as users - there is an adapter from IUserAuthProvider to IMembraneUserAuth. We restrict this to objects inside the Department object. The net effect is that all Employee objects inside the Department are returned.
这儿最有趣的方法是getGroupMembers().我们在membrane_tool catalog中搜索能适配到ImembraneUserAuth接口的对象。在membrane中,这个接口是描述能扮作用户的基本接口。有个适配器适配IUserAuthProvider到ImembraneUserAuth。这个getGroupMembers()的最终效果是返回部门内的所有员工。
Now, let's say you had a need for a Department which in addition to acting as a group for all members inside it, also allowed some members from other departments to be in that group. In this case, you could use a schema extender to add a ReferenceField to the schema of Department that allowed the Department owner to reference other Employees. You would then provide an override adapter, perhaps subclassing Products.borg.membership.department.Group but overriding getGroupMembers() to append the ids of the referenced users as well as the contained ones … or instead of, depending on your needs.
我们假设需要为department下面的所有成员将department扮作一个group,并且允许来自其他group成员成为这个department的成员。这种情况下,你能用一个schema extender加一个ReferenceField到Deparment的schema,以允许department的主人引用其他员工。然后提供提供一个覆盖的适配器,子类化Products.borg.membership.department.Group ,但覆盖getGroupMembers() 方法以追加引用的用户id和本来包含在里面的id。或者任何其他功能,取决于你的需求。
As it happens, Projects also act as groups, with members being assigned by reference, using two reference fields - one for project members, and one for project manangers. Here is the equivalent adapter from membership/project.py:
基于上述模式,Project也可扮作group,其成员来自于由引用指定,将用到两个reference fields,一个用于project成员,另一个用于project 管理者。下面是适配器例子:
membrane为Employee的适配器将更加复杂,它们由以下组成:
IUserRelated adapterProvides a user id for employees. Note that user ids and user names are possibly different when PAS is used: the user id must be globally unique; the user name is the named used for logging in. 为为employee提供 user id。当采用PAS时,用户名和id可以不同。IUserAuthentication adapter Used to perform actual authentication by validating a supplied username and password.用于对提供用户名和密码执行认证。 IUserRoles adapterUsed to determine which roles the particular user is given. 用于决定一个用户拥有哪些角色。 IMembraneUserManagement Used by membrane and Plone's UI to deal with changes to the user, such as the adding of a new user (not implemented here, since we All these adapters are found in membership/employee.py.
由membrane使用和Plone UI交互以更改用户对象。比如添加一个新用户(这儿暂未实现)。
The IUserRelated adapter is the simplest, as it simply invokes the user name. Note that by default, membrane will use the Archetypes UID() function as the user id. This is sensible, but unfortunately Plone's UI (and that of third party products) is not always aware of the distinction between user id and user name. Ideally, only the user name would ever be displayed, the user id being an internal concept, but in practice you may end up with things like member folder names that are long, unfriendly UID strings. Sometimes this may even be unavoidable in the general case, because it's possible that two different sources of users could use the same user name for two different user ids! For the purposes of b-org, however, we assume user names are unique and well-defined. The adapter is therefore quite trivial: 这个IUserRelaed适配器最简单,只是简单 调用用户名。注意,缺省情况下,membrane将用Archetypes UID()函数作为用户id。但是Plone UI不一定总能区别user id 和 user name。理想情况下uer name显示,user id 用于内部区分,但实际上由于不友好的UID字符串太长,而不舒服。某些时候这种情况不可避免,因为可能存在两个不同的用户source 使用同一个用户名。但在b-org中,我们假定用户名是唯一的,因此问题变得简单:
在verifyCredentials() 中,适配器被传输由用户输入的用户名和密码和保存在对象上下文中的值比较。(员工内容对象的上下文)这口令作为SHA1 digest 被保存在一个annotation中,以保证在检测该内容对象时,不能再读出明文。也要明白在一个用户登陆成功后,所作出的每一个request ,都将调用这个IUserAuthentication适配器,任何导致返回值是non_True,都将被拒绝访问。这意味着这个method的有效性是非常重要的。而在此如果通过数据库查询来验证,显然不是一个好主意。
The IUserRoles adapter is trivial. Roles are stored on the content object in a field that is editable only by managers. Of course, we could have picked roles from some other rule if necessary:
这个IUserRoles 适配器相当简单。角色被保存在内容对象的一个字段中,只能被管理元编辑。当然,我们也能从其他地方提取角色。
这个getRoles()方法返回一个递归的字符串以表现应用了的角色。注意取决于组成员(由上面的IGroupAwareRolesProvider)和本地角色,当前用户事实上夺得的角色可以比该方法返回的 更多。这个IUserRoles接口仅仅关心该用户的全局角色。
Finally, we have the IMembraneUserManagement adapter. This lets membrane know what to do when it is asked by Plone's UI to add, edit or remove users. In particular, the doChangeUser() method enables the PasswordResetTool to do its magic. Note that we have not implemented doAddUser(), because there is no well-defined global policy for where the actual Employee content object should be added! Recently membrane has gained some functionality whereby a site-local utility providing IUserAdderfrom membrane can be queried for this policy. That may be useful for b-org extension products, but b-org is still not in a position to make a general policy for this, so it is not implemented out of the box.
最后,讲讲IMembraneUserManagement 适配器。它将让membrane知道当Plone UI请求add,edit or remove 用户时,要做什么。特别,其中的doChangeUser() method 调用PasswordResetTool来做更改用户属性的事情。注意,这里没有实现doAddUser(), 因为没有定义好一个全局策略来决定实际的 Employee内容对象应该被加在哪儿。最新的membrane获得了某些功能——从membrane中的一个本地站点的工具应用提供IUserAdder来贯彻这个策略。这对于b-org扩展产品很有用。但b-org本身没有做这个。
总结,通过这些适配器,三个内容类型能够模拟成group源或user源。你将发现将松散耦合注入适配器的巨大灵活性。比如编辑用户属性,判断用户id,获取角色和执行认证等。如果你扩展b-org,你可以提供更多特别的适配器来适配上述接口已定制成员行为。
Writing a custom PAS plug-in写一个定制的PAS插件
Projects require that members are given particular local roles within a project space. This is achieved using a custom PAS plug-in.
Prejects要求在一个Project空间里面给予成员特别的本地角色。这个通过定制的PAS插件来完成。
PAS was introduced in the previous section on membrane. Truth be told, it can be a bit of a jungle of plug-ins and delegation because it is so very generic. Luckily, Plone (and membrane) takes care of most of the complexity for us. Sometimes, however, it is desirable to influence the authentication and role management at a lower level.
PAS已经在前面关于membrane段介绍了。事实告诉我们,PAS插件只是众多插件和委派中的一点点。幸好,Plone完成了这方面大部分的复杂性。然而,PAS插件是希望被用来在更低的层次进行认证和角色管理。
Workspace adapters工作空间适配器
b-org ships with a bit of framework, adapted from some similar code in an unreleased version of teamspace by Wichert Akkerman, which can provide local roles in a "workspace" - in this case a Project. It relies on an adapter to the IWorkspace interface to determine the mapping of users and roles in the particular context. Before showing how this plug-in is written and registered, however, let's look at how it is used by a Project.
b-org从未发布的teamspace项目中取得类似的代码以在一个工作空间(这里指一个Project)取得本地角色。这个技术依靠一个适配到Iworkspace接口的适配器,在特定的上下文中决定用户和角色的映射。在介绍怎样编写和注册该插件前,我们先来看看它怎样在一个Project中调用:
In membership/project.py you will find:
This queries the lists of managers and members assigned (by reference) to the project and specifies that both managers and members should get the role TeamMember and managers should also get the role Manager.
这个查询该Project的管理者列表和已指派的成员列表(通过引用来指派成员),来指定应该得到TeamMember角色的管理者或成员,而且管理者还将得到Manager角色。
As it turns out, this behaviour is also useful in Departments, which can be given one or more department managers by reference. The idea is that department managers should be allowed to add and remove Employees within that Department (recall that Department is a folderish container for Employee objects). The analogous adapter in membership/department.py reads:
这种方式也能用在Departments中,通过引用来给一个或更多的department 管理者。department管理者应该可以在该部门内添加或删除员工(Department是一个包含Employee对象的文件夹式样的容器)。在membership/department.py 类似的适配器如下:
因而,一个容器对象想要用管理本地角色的PAS插件,只需要适配到Iworkspace接口接口。(下文关系不大,不翻译)
The plug-inThe PAS plug-in that uses the IWorkspace interface can be found in pas/localrole.py. It looks like this:
用Iworkspace接口的PAS插件在pas/localrole.py 中,类似如下代码:
On first glance, there is quite a lot going on here, but it is not so hard to understand. First, we define a good old-fashioned Zope 2 factory and ZMI add form. This is good practice, because PAS plug-ins can be managed via acl_users in the ZMI. If you find yourself wandering there, however, remember to bring a torch and keep a trail of breadcrumbs to find your way out. A backup wouldn't hurt either if you try to change things. It is, unfortunately, not the most intuitive of interfaces.
粗略一看,代码很多,复杂,但其实不难理解。首先,定义了一个旧式样的zope 2 factory和ZMI添加表单。……
We will see how the plug-in is registered and activated in a moment, but first notice that the plug-in implements an interface, ILocalRolesPlugin, which is defined by PlonePAS, the PAS-in-Plone integration layer. This defines methods that will be called by the PAS machinery to determine, in this case, local roles. Note that this is not an adapter (perhaps it would have been if PAS had been invented in Zope 3, though Zope 3 has its own authentication machinery that is evolved from PAS and works slightly differently). When created, the ProjectLocalRoleManager is an Zope 2 object that lives in the ZODB in acl_users.
我们接下来将这个插件怎样被注册和激活。首先,我们注意到该插件实现了ILocalRolesPlugin接口,该接口由PlonePAS定义。注意这不是一个适配器。当创建时,这个ProjectLocalRoleManager是一个在acl_users下面保存在ZODB的ZOPE 2对象。
The methods of the ILocalRolesPlugin interface are fairly self-explanatory in purpose. They allow PAS to extract the local roles for a particular user in a particular context (getRolesInContext()), to check whether a user in fact has one of the roles required to access a particular method attribute in a particular context (checkLocalRolesAllowed()), and to get a map of users-to-roles in a particular context.
这个ILocalRolesPlugin接口的方法是清楚的自解析的。为一个特定的用户在一个特定的上下文中,允许PAS抽取本地角色,以检查是否一个特定的用户在特定的上下文中有访问一个特定方法属性 要求的角色,以得到一个用户到角色的映射。
The complex parts are, as often is the case, concerned with acquistion. The helper method _findWorkspace() attempts to walk up the object hierarchy to find the first possible IWorkspace (it will only consider one) to get hold of the appropriate IWorkspace adapter that is then used to determine the actual roles that apply, as above. Without walking up the content hierarchy, it would not be possible to let the local roles of a particular project apply when in the context of a piece of content inside that project (i.e. a sub-object of the folderish Project object). There is some reasonably hairy acqusition-juggling going on in the _chain() method to return this chain as a generator. The hairiness comes from the fact that the thing that is being checked may in fact be a method that is being accessed, and aqusition chains can get themselves in all kinds of knots, especially when Five is in the mix.
复杂部分通常是关于获取。这个_findWorkspace()方法变量对象的层次以找到首个IWorkspace,从而获取恰当的IWorkspace接口的适配器,然后用它决定实际应用的角色。当在一个Project里面内容的上下文中,不遍历对象层次,就不可能应用Project的本地角色。……
Lastly, we need to declare a ClassSecurityInfo and call InitializeClass to get Zope 2 to play ball.
最后,为了让zope 2正常运行,我们需要申明ClassSecurityInfo 并且调用InitizeClass.
Registering the plug-in注册插件
To be able to use this plug-in, we must first register it with PAS. This is done when the product is loaded, in borg/__init__.py:
为了应用该插件,首先必须用PAS注册它。这个在borg/__init__.py中,产品调入时,来做。
这个过程类似于CMF内容类型用ContentInit().initialize() and context.registerClass(). 初始化。
By registering the plug-in, we could now ask our users to instantiate a Workspace Roles Manager within acl_users…. er… somwhere. Like we said - not necessarily obvious. Better to do it once, in the setup code for b-org. Please refer to the section on GenericSetup to learn how b-org is actually installed, but notice that the relevant code is in setuphandlers.py:
通过注册这个插件,我们现在能要求我们的用户在acl_users或任何其他地方实例化一个Workspace Roles Manager 。请参考GenericSetup段学会b-org怎样被安装。但是注意在 setuphandlers.py中的相关代码:
All we do here is get hold of the factory dispatcher for the user folder (from manage_addProduct, which has something to do with that registerClass call for the WorkspaceLocalRoleManager seen in the previous code example, but like we said, it's dont-ask, don't-tell) and if it is not there already, we create an instance of the plugin using the factory. We then need to activate it so that it actually takes effect. out is a StringIO output stream used for logging.
所有在此做的是为用户文件夹获取factory构造器,如果该插件不存在,那么我们就用该factory构造器实例化一个插件。然后我们激活它。这里 out是StringIO输出流为登记日志。
Placeful workflow嵌入工作流
b-org uses CMFPlacefulWorkflow, which ships with Plone 2.5, to manage the workflow of content objects inside a project.
b-org 用捆绑在Plone 2.5当中的CMFPlacefulWorkflow 来管理一个Project里面的内容对象的工作流。
Placeful workflows are based on the concept of policies. You can think of a policy as a mapping of workflows to types, in the same way as you could control from the portal_workflow tool. Policies are created, normally by copying an existing policy (possibly the default, global policy), and then applied to a context. In Plone, this can be done using the policy option in the state menu.
嵌入工作流是基于策略概义。你可以想象一个策略是工作流到类型的一个映射,用同样的方法,你能从portal_workflow控制。创建策略通常是拷贝一个现存的策略(可能是缺省的全局的策略),然后应用它到一个上下文。在Plone中,这个可以在state 菜单中,用policy 来做。
Placeful workflows are used in b-org Projects. Inside a project, project members should have elevated view and modify permissions over content. This is achieved using the following technique:
嵌入工作流被用在b-org Project中。在一个Project里面,项目成员应该有超级试图和修改权限。这是通过下面的技术来实现的:
定制的工作流用GenericSetup定义在profiles/default/workflows/borg_project_default_workflow/definition.xml 中。必要的话,你当然可以安装自己的工作流。工作流策略在setuphandlers.py 的 importVarious setup step 中设置。
另外,如果你需要不同的设置,你可以加不同的策略。
Finally, we apply the policy when a project is created. We will see how this is set up when events are covered in the next section, but the relevant code is in events/project.py:
最后,当一个Project创建时,我们应用策略。在events/project.py相关代码:
这里,本地角色被添加到新建的项目实例中。并且这个策略也配套到该Project对象的内容。
Note that we do not hard-code the name of the workflow policy! Instead, we ask a utility called ILocalWorkflowSelection. This could be overridden using a local utility, but the global one references the policy created above, as defined in DefaultLocalWorkflowSelection. This utility is registered in events/configure.zcml as follows:
注意我们没有硬编码工作流策略的名称。而是请求一个名为ILocalWorkflowSelection工具应用。这个能被一个本地工具应用覆盖。这个工具应用在events/configure.zcml 中被注册:
Sending and handling events发送和操纵事件
Events is undoubtedly one of the most useful things that Zope 3 brings to the Zope 2 world. Here's how b-org uses them.
事件无疑是 ZOPE 3世界最有用的东西。这儿介绍b-org怎样用事件
In the previous section, you saw how an event handler was used to apply a placeful workflow policy to newly created projects. This pattern is quite powerful - instead of needing to subclass Project just to add something to at_post_create_script() or initializeArchetype(), say, you simply register an appropriate event handler. This pattern can of course apply to other situations, such as when objects are modified, deleted, added to a container, or on any other type of event that may occur in your system. Events are synchronous, so when code emits an event, it will block until all event handlers are finished.
在先前的章节中,我们看到了一个事件句柄怎样用于应用一个嵌入工作流策略到一个新建的Project。这种方式是相当强大的,代替子类化Project的方法仅仅需要加某些事务到at_post_create_script()或initializeArchetype(),也就是你简单地注册一个恰当的事件句柄。这种方式当然也能应用到其他情况,比如当一个对象修改、删除、添加到一个容器或者能发生在系统中的任何其他类型。事件是同步的,因此当代码发出一个事件时,它将锁定直到所有的事件句柄完成。
Recall the event handler for adding projects. It can be found in events/project.py and has the following signature:
你能在events/project.py中发现为添加Projecter 而添加事件句柄的代码:
首个参数是发出事件的对象,第二个参数是事件本身的一个实例。事实上,这两部分事件分发是用IobjectEvent 和 它的子接口描述的事件的一种特殊情况。在zope 3内部,捕获所有IobjectEvents并基于被传输到事件实例的对象重新分发这些事件。事件句柄的注册在events/configure.zcml中:
A more general-case event can be found in events/employee.py, which takes care of assigning ownership of an Employee object to the user that is tied to that employee. The code is borrowed and adapted from PloneTool, but notice the signature which only includes the event:
注意有两个接口被注册——对象类型和事件类型。这些必须用空格分开。这和显示定义多适配器是同样的语法(如果你在一个适配器class中不用adapts()语法)。事实上,事件机制用这个适配器内部的注册映射预定者到事件。一个更通常的事件能被发现在events/employee.py中,确保指定一个员工对象的成员到依赖该员工的用户。
The registration in events/onfigure.zcml is similar to the one above, but only uses one for interface:
注册过程类似于上面,但只用一个interface:
Sending custom events发送定制的事件
You will notice that the IEmployeeModifiedEvent is a custom event. In Plone 3.0 (or rather, Archetypes 1.5) this won't be necessary, because Archetypes will take care of sending an event derived from IObjectModifiedEvent, which in turn derives from IObjectEvent and thus is subject to the same registration as the IObjectAddedEvent that includes the object type and the event type. For now, though, we need to send the event ourselves.
IEmployeeModifiedEvent是一个定制事件。在Plone3(或 archetype 1.5)这是不必要的,因为Archetypes将负责发送一个从IObjectModifiedEvent生成的事件。现在我们需要自己发送这个事件。
The event is described by an interface in interfaces/employee.py:
该事件被描述为一个接口,如下:
The implementation is trivial, and can be found in content/employee.py:
实现部分简单,如下:
我们实例化并发送事件类的同时,我们为事件接口注册事件句柄。这意味着我们可以为同样的事件接口提供不同的实现。也意味着为一个父接口预定的事件句柄将被提供子接口的事件调用。
Sending the event is very simple. In the definition of Employee in content/employee.py, we have:
发送事件相当简单:
We construct an event instance and parameterise it with the right object (i.e. self) before sending it with notify(), all on one line.
我们建造一个事件实例,并用正确的对象作为参数(如self).
AnnotationsAnnotations are an elegant solution to the "where do I store this?" problem, and are used in many Zope 3 applications.
Annotations 是一个一流的解决方案为“保存在哪里”的问题,被用在大多数zope 3应用当中。
It is often useful to be able to attach information to an object even if you don't have control over that object's type and schema. For example, a tagging solution may attach a list of tags to an object, or a notification tool may want to add a list of subscribers on a per-object basis. This is known in Zope 3 as "annotations".
能追加信息到一个对象即便没有控制该对象的类型和schema 是非常有用的。例如,一个标签 解决方案能追加一系列的标签到一个对象,或者一个通知工具能加一系列的预定者到一个对象。这个在zope 3中被称为"annotations".
Annotations work like this:
Annotations工作原理:
In b-org, we don't have quite the same need for annotating objects from other parts of Plone, but we use annotations to store users' passwords. This ensures that they cannot be accessed through-the-web (since Zope 2 won't publish the __annotations__ dict, as it begins with an underscore) and keeps passwords out of the way. Strictly speaking, this is probably overkill since the password is also hashed using the SHA1 one-way hasing algorithm, but that never stopped anyone before.
在b-org中,我们用annotations 保存用户密码。这能确保信息不能通过TTW访问,保护密码。严格地讲,这也许过度保密。因为这密码也用SHA1单向hasing算法加密。
First, look at the definition of the Employee class in content/employee.py:
先看下 Employee class的定义:
Here, we explicitly say that Employee is attribute annotatable. Of course, this requires control over the class. If you are trying to annotate another type that isn't already marked as annotatable, you may be able to add the marker interface using classProvides() or directlyProvides() from zope.interface, or use the ZCML <implements /> directive. You need to be a bit careful, though, since the thing you are annotating should probably be persistent. You should also be polite - you're stuffing your own information onto someone else's object. Try not to break it.
这里我们显示申明Employee是annotatable。当然这要求控制这个class。如果你试图annotate另外一个没有标记为annotatable的其他类型,你能加 这个marker interface通过用classProvides() or directlyProvides() ,或者用ZCML <implements />语句。你要小心,因为你正在annotating的东西也许是静态的、持久的。你也应该文雅些,你正在填充自己的信息到其他对象,不要损坏了这对象。
Further down in content/employee.py, you will see the annotation being set:
更进一步,在content/employee.py ,设置如下:
PASSWORD_KEY来自config.py,它是一个简单的字符串。这个报文被验证在IuserAuthentication 适配器中:
我们获得一个IAnnotations适配器,然后查找PASSWORD_KEY 以找到报文。这个Annotation适配器就象一个Python dict,因此能用get()和setdefault()。
Zope 3 ViewsZope 3视图
One of the nicest things that Zope 3 brough us is a way to manage view logic.
zope 3带给我们最美妙的事情是管理试图逻辑的方法。
In Zope 2, a view (be that a view of a content object, or a more standalone template) typically consists of a Zope Page Template that pulls in data from its context. The problem is that non-trivial templates usually require some kind of "view logic" or "display logic". People tend to put these in a few places:
在zope 2中,通常一个视图由页面模版组成,并且页面模版能将从它的上下文中拉入数据。问题时没有一个简单的办法来实现视图逻辑或显示逻辑。人们通常将这些逻辑放置在下面这些地方:
As usual, these problems indicate a lack of separation of concerns. Zope 3's answer is a view - a class (typically) which may be associated with a template.
通常上述这些问题的焦点是缺乏松散耦合。zope 3的解决方案是视图,该视图实际上是一个class,但能配合一个相应的显示模版。
Views are multi-adapters视图是多适配器
You will often hear that views are named multi-adapters of a context and a request. In fact, the concept of a multi-adapter originated in the need for views. For most practical purposes, you can forget about this - it is an implementation detail. However, you may sometimes need to look up views yourself, which can be done using:
视图命名为一个上下文和一个request的多适配器。事实上,原始的多适配器概义是适应视图的需要而提出的。在大多数应用场合,你无须关心这个。然而,可能不时要查看视图,可以通过以下方式完成:
from zope.component import getMultiAdapter
myView = getMultiAdapter((context, request), name='my_view')
More importantly, you need to know that to access the context the view is operating on inside that view, you can use self.context, and to access the request (including form variables submitted as part of that request, if applicable), using self.request.
重要的是,要明白访问视图的上下文是在该视图里面进行的,因此,能用self.context,也能用self.request来访问这个request(包括request中提交的各种变量)。
Explicitly acquiring views显示获取视图
One of the easiest ways of using views with existing code is to make page templates in a skin layer as you normally would, and then acquire a view object that is used for rendering logic. One of the main reasons for using this approach is that it allows page templates to be customised using the normal skin layer mechanism. This is approach is used extensively in Plone 2.5. Here's an example from the "recent" portlet, starting with portlet_recent.pt
使用视图最简单的方法是在皮肤层创建ZPT,然后获取视图对象用于呈现。用这种方式的主要原因是允许ZPT可以用皮肤层管理机制进行定制。这个方式已在Plone2.5中实现,下面是一个针对"recent" portlet,利用该技术做的定制:
…
The important line here is view/@@recent_view. This will look up a view named recent_view relative to the current context (context in page templates is a now-preferred alias for the here variable that was used before - here still works in Zope 2 templates, but is gone in Zope 3).
代码中最重要的一行是view/@@recent_view。这将查找一个相关到当前上下文的一个命名为recent_view的视图。(在ZPT中context是here变量的一个别名,here 在zope 2中有效,但在zope 3中不再有效)
This view is defined by a class and a ZCML directive. The ZCML directive looks like this:
这个视图有一个class定义,并由一个zcml语句来说明,如下:
(较简单,不翻译了……)
The use of aq_inner() on self.context is not strictly necessary always, but is a useful rule of thumb to make acquisition do what you expect it to do (this is because the BrowserView base class extends Acquisition.Explicit, which causes self.context to gain an acquistion wrapper that can mess with its acqusition chain).
在self.context使用aq_inner()不总是需要的。但作为一种经验,能使得获取机制做你希望做的事情。
Views with templatesZope 3 does not use views in this way. Instead, you would bind the template to the browser view explicitly. The main drawback of this technique is that the template is not present in the portal_skins tool, and so cannot be customised through-the-web. This may be possible in future versions of Zope and CMF, but for now the full-blown view technique is best used it is not necessary to customise views through-the-web. Of course, you can still override view registrations using ZCML on more specific interfaces or an overrides.zcml.
显示地绑定模版到browser视图。这种技术的缺点是模版不能被Portlal_skins工具看到,因此无法通过TTW方式定制该模版。如果无需通过TTW方式定制视图,最好采用该技术。当然,你也能用ZCML在更多的接口上覆盖视图注册,或者用一个overrides.zcml。
Here is a view for departments in the charity example product, under charity/browser/configure.zcml. Notice how this entire XML file is in the browser namespace, and thus it is unnecessary to prefix each directive with browser:
在charity 样例产品中有一个为departments做的视图,在charity/browser/configure.zcml下。注意整个XML文件是在browser命名空间,因此不必要在每个语句前加browser,如下:
注意,这儿我们显示指定了视图仅仅为Products.borg.interfaces.IDepartmentContent接口的对象,这意味着如果你在没有提供该接口的其他对象上调用@@charity_department_view,将返回一个查询错误。这个视图也由zope 2的 view权限保护。也请注意,这儿没有定义allowed_attributes 属性,这是因为这个视图不希望被其他模版使用(如果其他模版使用,将得到一个Unauthorized 错误)。
The department.pt template is found in charity/browser, the same directory as the configure.zcml file above. You can use relative paths like ./templates/… if necessary to point to the template file on the filesystem. Here is the class:
这个department.pt 在charity/browser 中,和上面的configure.zcml在同一目录中。下面是DepartmentView视图class定义:
下面是调用视图方法的模版:
现在,你可以通过 /mydept/@@charity_department_view URL查看视图呈现。
Views without templatesIt is also possible to make views without templates. This is useful if you need a URL to submit that does some processing. That processing would normally be done in the __call__() method, as in the hypothetical example below:
也可能使用不用模版的视图。如果你需要一个url提交做某些处理,这将很有用。这个处理通过调用__call__()来做,下面是一个假定的例子:
现在,你能写一个带有action="@@modify_customer" 的表单:
这是一个简单的例子。重要的是认识到视图将能用self.context和self.request来和portal的其他信息交互
extensible, better tested and easier to maintain.
Plone2.5的到来使得我们更加靠近zope3的乐土。zope3带给我们全新的工作方式。本教程将新旧技术结合在一起,使得
plone产品有更好的扩展性,更易于测试和维护。
Introduction 介绍What is b-org, and what will you learn here?
b-org是什么,从这我们能学到什么?
b-org stands for "base-organisation". The name had nothing whatsoever to do with my desire to get an svn URL of http://svn.plone.org/svn/collective/borg. Promise. In fact, it used to be called company, which some people rightly pointed out is a bit too generic and opens up the possibility of conflicts with other people's code.
It just proves that naming generic components is difficult.
b-org顾名思义是指"基本组织"。你能通过svn http://svn.plone.org/svn/collective/borg取得该产品。
Generic is the key word here. Functionally, b-org provides infrastructure to help you manage Departments,Employees and Projects in a natural way. Departments are containers for employees, employees are linked to projects by references. Using membrane, these objects become sources for users and groups, so that a department is a group for all the employees in it, and employees become real users of the system, with usernames and passwords. Projects manage local roles, so that employees that have been associated with the project are able to add and modify content in it. Other users may or may not be able to view content in a project, depending on its workflow state.
However, b-org makes no assumptions about which metadata you want to associate with departments,employees or projects. For that, it expects you to plug in your own content schema. It also delegates almost all its functionality to smaller components, so that if you, for example, want to store authentication details via LDAP or change the way in which users are employees to projects, you can do so by implementing small, isolated components rather than sub-classing and re-implementing large chunks of the three basic content types.
That's all well and good, but you're probably not going to want to read a lengthy tutorial just about how great b-org is. As the title promises, this tutorial is about leveraging new technologies available in Plone 2.5 to write better content types and other software in Plone. Hopefully, you will find the techniques described here useful whether you are writing a member management module using membrane (mmmm), or other code. I for one, want to go and rewrite several of my products (like Poi) to make them more extensible and flexible after having adopted these techniques. Hopefully, you will also learn something about the development process, in particular test-driven development, that I followed, and how the future of Plone is entangled in Zope 3.
This tutorial should be viewed as complementary to, rather than superceding, my earlier tutorial entitled RichDocument - Creating Content Types the Plone 2.1 way. The techniques of RichDocument, in particular relating to extending ATContentTypes, are still valid in Plone 2.5. What Plone 2.5 allows us to do, however, is to achieve better separation of concerns between content storage, business logic and view logic, due to the added spices of Zope 3. For RichDocument, the gain wouldn't be that great since it's relatively simple (and focuses on doing as little as possible by re-using as much as possible from ATContentTypes). Hence, I didn't update the RichDocument tutorial, nor do I feel as compelled to update RichDocument itself (yet). b-org is a more ambitious example which allows us to illustrate the new techniques more fully.
One thing to note is that this tutorial is still centered on Archetypes, and assumes you know the basics of Archetypes development on the filesystem. Archetypes is rooted in a pre-Zope 3 world, and there are times when we have to accommodate it in ways that make our clean patterns a bit messier - luckily, not too often. There are ways of managing content in Zope 3 that can be applied to Plone, for example by way of zope.formlib, but these are generally not quite ready to replace what we can do today with Archetypes. In the future, they may be, but more likely Archetypes will converge a bit more with its Zope 3 equivalents and blur the lines between the two approaches. The upshot is that what you know about Archetypes today continues to be relevant, and is augmented by the Zope 3-inspired techniques you will find here.
A whirlwind tour of Zope 3Zope 3 is still fairly new. After reading this tutorial, it should hopefully start to feel a bit more familiar. In this section, we will give a brief overview of what is different in Zope 3 and how it fits into Plone.
The name Zope 3 is a lie. True - it is brought to you by many of the same clever people who built Zope 2, one of the most advanced open source app servers of its day. True, it is still Python, it still publishes things over the web, and there are still Zope Page Templates. However, Zope 3 is about small, re-usable components orchestrated into a flexible framework. It is this flexibility that allows us to use Zope 3 technologies in Zope 2 applications like Plone.
A piece of wizardry called Five (Zope 2 + Zope 3 = Five, geddit?) makes a number of Zope 3 components directly available in Zope 2, and since Zope 2.8, almost all of Zope 3 has shipped with Zope 2 as a python library. Plone 2.5's primary purpose was to lay the foundations for taking advantage of Zope 3 technologies in Plone.
Zope 3 may seem a bit alien at first, because it uses strange concepts such as adapters and utilities. Luckily, these are not so difficult to understand, and once you do, you will find that they help you focus your development on smaller and more manageable components. You will also find that these basic concepts underpin most of the innovative parts of Zope 3.
Interfaces接口
Everything in Zope 3 starts with interfaces. Unlike Java or C#, say, Python does not have a native type for an interface, so an interface in Zope 3 is basically a class that contains only empty methods and attributes, and inherits from Interface. Here is a basic example:
接口是Zope 3体系结构的开始。python没有一个固有类型来定义接口,在Zope 3中通过定义一个包含空的属性和方法的类,并且该类继承自Interface,下面是接口定义例子:
from zope.interface import Interface, Attribute
class IShoe(Interface):
"""A shoe
"""
color = Attribute("Color of the shoe")
size = Attribute("Shoe size")
class IShoeWearing(Interface):
"""An object that may wear shoes
"""
def wear(left, right):
"""Wear the given pair of shoes
"""Interfaces are primarily documentation - everything has docstrings. Also note that the wear() method lacks a body (there is not even a pass statement - the docstring is enough to keep the syntax valid), and does not take a self parameter. That is because you will never instantiate or use an interface directly, only use it to specify the behaviour of an object.接口提供主要的文档,每一件东西都有文档字符串,你也注意到了这个wear()方法没有实现的语句(即便是空的pass语句也没有,但这个语法有效),也没有self参数。这时因为你从来不需要直接实例化一个接口,仅仅用它来描述一个对象应当有的行为(属性、方法等)。
An object can be associated with an interface in a few different ways. The most common way is via its class. We say that the class implements an interface, and objects of that class provide that interface:
一个对象能有多种方法对接一个接口。最通常的是从他的类定义中获得,这种情况下,我们说这个类实现了该接口,该类所有对象能提高该接口。
from zope.interface import implements
class Shoe(object):
"""A regular shoe
"""
implements(IShoe)
color = u''
size = 0The implements(IShoe) line means that objects of this class will provide IShoe. Further, we fulfill the interface by setting the two attributes (we could have implemented them as properties or used a an __init__() method as well). The IShoeWearing interface will be implemented in the section on adapters below.implements(IShoe)这行 意味这这个类的对象将提供iIshoe接口。说得更明白点,我们通过设置两个属性来实现这个接口(我们能实现它们作为属性或方法)。IShoeWearing 接口将在下文关于适配器段 讨论。
We use interfaces to model components. Interfaces are normally the first stage of design, in that you should define clear interfaces and write actual classes to fulfill those interfaces. This formalism makes for great documentation - interfaces are conventionally found in an interfaces module, and this is typically the first place you look after browsing a package's documentation. It also underpins the adapter and utility system - otherwise known as the Component Architecture - as described below.
我们用接口模型化部件。接口通常是设计的第一步,应该设计清晰的接口,然后定义实际的类来实现这些接口。按照惯例,这样形式上提供了文档化的接口在interface模块中。它也加强了适配和应用系统,另外也称此为部件体系架构。
Note that you can use common OOP techniques in designing interfaces. If one interface describes a component that has an "is-a" or "has-a" relationship to another component, you can let interfaces subclass or reference each other. An object will provide the interfaces of its class, and all its base-classes, and all base-interfaces of those interfaces. Don't worry about untangling that - it works the way you would expect.
注意,你可以用通常的OOP技术设计接口。如果一个接口描述一个部件 “是一个”或“有一个”关联和其他部件,你能子类化该接口后让它们相互引用。一个对象将提供它的类实现的接口,也将提供它的所有基类实现的接口,还将提供那些接口的基 接口。
You can also apply interfaces directly to an object. Of course, if that interface has methods and attributes, they must be provided by the object, and unless you resort to crazy dynamic programming, the object will get those from its class, which means that you may as well have applied the interface to the class. However, some interfaces don't have methods or attributes, but are used as markers to distinguish a particular feature of an object. Such marker interfaces may be used as follows:
你也能直接应用一个接口到一个对象。当然,如果这个接口有方法或属性,它们必须由这个对象提供。然而,某些接口没有方法也没有属性,它们仅仅用于标记以区分一个对象特别的特性。即marker interfaces :
class IDamaged(Interface):
"""A shoe that is damaged
"""
>>> from zope.interface import directlyProvides
>>> boot = Shoe()
>>> IDamaged.providedBy(boot)
False
>>> directlyProvides(boot, IDamaged)
>>> IDamaged.providedBy(boot)
TrueMarker interfaces are very useful for things that change at run-time in response to some event (e.g. some user action), and thus cannot be determined in advance. In a moment, you see that what you will learn about adapters and adapter factories below also applies to marker interfaces - it is possible to alter which adapter factory is invoked by applying a different marker interface.Marker interfaces是很有用的对于在运行时响应的某些事件(例如 某些用户操作),而不能预先估计到。在下面,你将学习关于适配器以及适配器构造器将应用到 marker interface.通过应用不同的marker interfaces可以改变哪个适配器构造器被调用。
It's also possible to apply interfaces directly to classes (that is the class itself provides the interface, as opposed to the more usual case where the class implements the interface so that objects of that class provides it - this is useful because it allows you to group those classes together and describe the type of class they are) and to modules (where you want to describe the public methods and variables of a module). These constructs are less common, so don't worry about them for now. Look at the documentation and interfaces (!) in the zope.interface package for more.
也可能应用接口直接到类(类自己提供接口,暂不明白,不翻译……)
Adapters适配器
The most important thing that Zope 3 promises is separation of concerns. In Zope 2, almost everything has a base class that pulls in a number of mix-in classes, such as SimpleItem (surely, the most ironically named class in Zope 2) and its plethora of base classes that include RoleManager, Acquisition.Implicit and many others. This means that a class written for Zope 2 is nearly impossible to re-use outside of Zope.
Zope 3最重要的概念是承诺松散耦合。在Zope 2中,通过基类拉入很多混合类,例如SimpleItem 就包括RoleManager, Acquisition.Implicit 和很多其他类。这意味着为zope 2写的类不可能在zope 2之外重用。
Furthermore, in Zope 2 we are tightly wedded to the context (aka here) because it is so convenient to use in page templates, workflow scripts etc. For example, people often write an Archetypes class that contains a schema (storage logic), methods for providing various operations (business logic) and methods for preparing things to display in a page template (view logic). Often, people do this simply because they can't think of a better place to put things, but it does mean that re-using any part of the functionality becomes impossible without importing the whole class - and its base classes, which include Archetypes' BaseObject, CMF's DynamicType, and Zope's SimpleItem - to name a few!
此外,在zope 2中紧密绑定context (here) ,因为这个能方便地应用在ZPT WORKFLOW 脚本中。例如,人们开发一个Archetypes类通常包含一个schema (存储逻辑),一些方法为各种操作(商业逻辑),一些方法为显示页面模版(表现逻辑),所有这一切都在一起。但如果要重用其中功能的任何一部分,就必须导入整个类,包括它的基类,也包括Archetype的基对象,CMF的动态类型和ZOPE的simpleitem。所以,重用几乎不可能。
Think about the example above. The Shoe class is well-contained and only concerned with one thing - storing the attributes of shoes. It can be used as an abstraction of shoe anywhere, and is very lightweight. Now let's consider that we may want to wear shoes as well. We can create a pair of shoes easily enough:
考虑上面的例子。这个shoe类仅仅关心一件事情——存储鞋子的属性。它能用作任何抽象的鞋子。我们可以很容易地创建一双鞋子:
>>> left = Shoe()
>>> right = Shoe()
>>> left.size = right.size = 10
>>> left.color = right.color = u"brown"Now we want someone to wear these shoes. Let's say we have a person:现在,我们想某人能穿这些鞋子,让我们假定有一个人:
class IPerson(Interface):
"""A person
"""
name = Attribute("The person's name")
apparel = Attribute("A list of things this person is wearing")
class Person(object):
implements(IPerson)
name = u''
apparel = ()In a Zope 2 world, we may have required Person to mix in some ShoeWearingMixin class that specified exactly how shoes should be worn. That makes for fat interfaces that are difficult to understand. In a Zope 3 world, we would more likely use an adapter.在zope 2中,要实现这个要求这个Person要混合某些shoewearingMixin类以指明鞋子该怎样穿。这不得不制造很肥的接口而难于理解。在zope 3中我们只需用一个适配器。
An adapter is a glue component that can adapt an object providing one interface (or a particular combination of interfaces, in the case of a multi-adapter) to another interface. We already have a specification for something that wears shoes, in the form of IShoeWearing. Here is a snippet of code that may use this interface:
适配器是一个胶合部件,能调节一个对象提供一个接口(或一组接口,多接口适配器环境下)到另一个接口。 在我们的例子中,特殊的事情是wears shoes,有 IShoeWearing 要求的形式。 下面是 用这个接口的代码:
>>> wearing = ...
>>> wearing.wear(left, right)
The question is what to do with the '…' - how do we obtain an object that provides IShoeWearing? Code like this is normally operating on some context, which in this case may be a Person. If that Person implemented IShoeWearing (or at least the wear() method), it would work, but then we are making undue demands on Person. What we need is a way to adapt this IPerson to something that is IShoeWearing. To do that, we need to write an adapter:问题是怎样实现"wearing=…",怎样包含一个提供IshoeWearing接口的对象?这里代码操作的context,应当是Person.如果Person实现了IshoeWearing接口(或者至少实现了wear()方法),问题就能解决。但是,显然,我们对于Person作出了过分的要求。我们需要一个简单的方法以调节Iperson接口,使得它具备IshoeWearing接口的某些东东。为实现这个,我们需要写个适配器:
from zope.interface import implements
from zope.component import adapts
class PersonWearingShoes(object):
"""Adapter allowing a person to wear shoes
"""
implements(IShoeWearing)
adapts(IPerson)
def __init__(self, context):
self.context = context
def wear(self, left, right):
self.context.apparel += (left, right)
Here, we implement the IShoeWearing interface. Note how the wear() method now has a self parameter, since this is a real object. Also note the __init__() method, which takes a parameter conventionally called context. This is the thing that is being adapted, in this case an object providing IPerson. We store this as an instance variable and then reference it later. Note that adapters are almost always transient objects that are created on the fly (we will see how in a second).这里,我们实现了 IShoeWearing接口。注意这个wear()方法有了个self参数,这是因为这儿是个真实的对象。还有__init__()方法,它取得的一个context参数,这个就是被适配的对象,本例中,指提供Iperson接口的对象。我们通过__init__保存这个作为一个实例变量,以便在后面引用。
We could now do something like this:
我们现在能实现:
>>> wearing = PersonWearingShoes(person)
>>> wearing.wear(left, right)However, this still requires that we know exactly which adapter to invoke for the particular object (person in this case), effectively creating a tight coupling between the adapter, the thing being adapted, and the code using the adapter.然而。仍旧有个问题:我们怎样精确地知道为一个特定的对象调用哪一个适配器?怎样有效地将适配器、被适配的对象、使用适配器的代码捆绑在一起?
Luckily, the Zope 3 Component Architecture knows how to find the right adapter if you only tell it about the available adapters. We do that using ZCML, the Zope Configuration Markup Language. This is an XML dialect that is used to configure many aspects of Zope 3 code, such as permissions and component registration. You can do what ZCML does in Python code as well, but typically it's more convenient to use ZCML because it allows you to separate your logic from your configuration.
幸运的是,zope 3部件体系知道怎样找到正确的适配器。通过用ZCML。这是一个XML格式用于配置zope 3代码的样式,诸如权限、部件注册等。python代码能做的配置相关的事情,ZCML都能做。并且ZCML更好,因为可以从应用中分离出逻辑。
ZCML directives are stored in file called configure.zcml, which itself may include other files. A configure.zcml file in your product directory (Products/myproduct/configure.zcml) will be picked up automatically by Five. Here is a snippet that will register the above adapter:
ZCML 语句保持在configure.zcml文件中,该文件位于product目录(Products/myproduct/configure.zcml),能够被Five自动提取,下面是注册adapter的程序片段
<adapter factory=".shoes.PersonWearingShoes" />
注册完整形式如下:
<adapter
factory=".shoes.PersonWearingShoes"
for=".interfaces.IPerson"
provides=".interfaces.IShoeWearing"
/>
Here, we are specifying full dotted names to interfaces in the for or provides attributes. These are equivalent to the adapts() and implements() calls we used when defining the adapter. Note that adapts() did not work prior to Zope 2.9 (so the ZCML for attribute is mandatory), and that if your adapter class for some reason implements more than one interface (e.g. because it's inheriting another adapter that has its own implements() call), you may need to specify provides to let Zope 3 know which interface you're really adapting to.这里我们指定了一个完全.分割的名称到接口。这和我们定义adapter时调用adapts() 和 implements() 的形式完全一样。注意adapts() 在ZOPE 2.9之前不被支持 (因此,在ZCML中应为for这个属性强制指定),如果你的adapter类因为某些原因(如:继承了另外一个adapter,同时又有自己的implements())实现了多个接口,你应该ZCML中需要指定provides ,以便让zope 3知道你适配的是哪一个接口。
Notice here that the dotted names begin with dot. This means "relative to the current package". You can write "..foo.bar" to reference the parent package as well. You could specify an absolute path instead, e.g. Products.Archetypes.interfaces.IBaseObject or zope.app.annotation.interfaces.IAttributeAnnotatable. Typically, you use the full dotted name for things in other packages and the relative name for things in your own package.
注意,这儿“.”开始的命名方式意味路径这“相对于当前包”。也即能用"..foo.bar" 去引用父级包。当然,也能指定绝对路径,例如Products.Archetypes.interfaces.IBaseObject 或 zope.app.annotation.interfaces.IAttributeAnnotatable。通常我们采用"."开始并分割的名称。
The factory attribute normally references a class. In Python, a class is just a callable (taking the parameters specified in its __init__() method) that returns an instance of itself. You can reference another callable as well if you need to, such as a function that takes the same parameters (only context in this case - obviously there is no self for functions), finds or constructs and object (which must provide IShoeWearing) and then returns it. This is rarely used, but can be very powerful (for example, it could find an object providing the given interface in the adapted object's annotations - but don't worry if you don't understand that for now).
这个factory 属性通常引用一个 class,一个可以调用的class(通过他的__init__()方法来取得参数)被调用时将返回该class的一个实例。后文理解不透暂不翻译。
With this wiring in place, we can now find an adapter for an IPerson to IShoeWearing. The Component Architecture will ensure that we find the correct adapter:
用这个贯穿始终,我们我们找到一个adapter为适配Iperson到IShoeWearing。部件体系将确保我们找到正确的adapter。
>>> wearing = IShoeWearing(person)
>>> wearing.wear(left, right)
>>> person.apparel == (left, right,)
TrueWe are "calling" the interface, which is a convenience syntax for an adapter lookup. If an adapter cold not be found, you will get a ComponentLookupError. There are plenty of functions in zope.component to discover adapters and other components - see zope.component.interfaces for the full story.我们调用接口来方便地查找adapter。如果adapter找不到,将有个ComponentLookupError 错误。在zope.component 模块中有大量发现adapter以及其他部件,详情参见zope.component.interfaces 。
It is important to realise that the adapter lookup is essentially a search. The Component Architecture will look at the interfaces provided by person and look for a suitable adapter to IShoeWearing. As mentioned before, it's possible for an object to provide many interfaces, e.g. inherited from its base classes, implemented explicitly by the object (by declaring implements(IFoo, IBar)), via ZCML or because an object directly provides an interface. It is therefore possible that there are multiple adapters that could be applicable. In this case, Zope 3 will use the interface resolution order (IRO) to find the most specific adapter. The IRO is much like you would expect of polymorphism in traditional OOP:
明白adapter查询过程实际上是一个搜索过程。部件体系将查看由person提供的接口,并查找一个为IShoeWearing配套的适配器。正如前面提到的,一个对象可能提供多个接口,因此可能有多个adapter可以被应用。在这种情况下,zope3将用interface resolution order (IRO)来找到最优先的adapter.这个IRO类似于传统OOP设计的多态。
- an interface directly provided by the object is more specific than one provided by its class
- 直接由对象提供的接口优先于由该对象的类实现的接口。
- an object provided by an object's class is more specific than that provided by a base class
- 一个对象提供的来自于该对象的类实现的接口优先于该对象的类的基类实现的接口。
- if an object has multiple base classes, interfaces are inherited in the same order as methods are inherited
- 如果一个对象有多个基类,这个继承顺序和类中methods的继承顺序一样。
- if a class implements multiple interfaces, the first one specified is more specific than the second one, and so on
- 如果一个类实现了多个接口,首个implemnet的接口优先于后面实现的接口,如此等等。
还记得 marker interface吗?marker interface的作用暗示一个特别的adapter。考虑这种情况 当你有特定的adpter为适配某些marker interface IAmputee 到IShoeWearing。如果你标记一个person作为一个IAmputee,这个IShoeWearing adapter不会去修改apparel列表,而会发出一个警告。
All of this may seem a little roundabout and unfamiliar, but you'll get to grips with it soon enough. Let's re-cap how we arrived at this:
所有这些看起来很曲折难以理解。但是你将马上要认真处理这些。现在,让我们来回顾总结下:
- We modelled our application domain with some interfaces - IPerson, IShoe
- 我们通过用接口(如IPerson,IShoe)来模型化我们的应用。
- 我们模拟一个人的外形为 穿鞋子 -IShoeWearing
- 我们写class 来实现接口 IPerson and IShoe
- 我们写并注册一个adapter来适配一个Iperson 到 IShoeWearing
然后,我们来说明怎样应用。假定有某些客户代码,这些客户代码仅仅需要知道IPerson和IShoeWearing,而不必关心一个人穿鞋子是怎样实现的。部件体现确保找到恰当的适配器,无论这个Person是一个vanilla IPerson,还是一个带特殊子接口的子类,或者是一个带marker interface的实例。
Multi-adapters, named adapters and views多适配器,命名适配器和视图
In the example above, we used an adapter with a single context. That is the most common form of adapter, but sometimes there is more than one object that forms the context of an adapter. As a rule of thumb, if you find yourself passing a particular parameter into every method of an adapter, it should probably be a multi-adapter.
在上面的例子中,我们演示了带简单上下文的适配器。这是适配器最通常的用法,但是,在某些情况下,一个适配器的上下文可能有多个对象。单凭经验,你能发现传送一个特定的产生到一个适配器的每个方法,这也许应当称为多适配器。
The most common example of a multi-adapter that you will come across is that of a view, which incidentally is also how Zope 3 solves the "where do I put my view logic" code. We will cover views in detail later, but for now think of them as a python class that is automatically instantiated and bound to a page template when it's rendered. In the template, the variable view refers to the view instance and can be used in TAL expressions to gain things to render or loop on.
多适配器最通常的例子是视图。也附带说明zope 3怎样解决"表现逻辑放在哪儿"。稍后,我们将详细讨论视图,现在仅仅将视图 看作一个python class,当呈现时,这种class 是自动实例化并被绑定到页面模版。在模版中,变量view引用视图view实例,能用在TAL表达式中,获得需要呈现的东东。
When dealing with a view, there are two things that make up its context - the context content object (conventionally called context) and the current request (conventionally called request). Thus, a view class is a multi-adapter from the tuple (context, request) to IBrowserView. As it happens, there are ZCML directives called browser:page and browser:view that make it easier to register a view and bind a page template to it, handle security etc. However, abstractly a view looks like this:
当和视图交换时,有两个事情特别重要,一个是对象的上下文(通常叫context),另一个是当前request(通常叫request)。因而,一个视图类是一个多适配器,适配元组(context, request)到IBrowserView.由于有这个,ZCML语句调用browser:page 和browser:view ,并容易地注册一个视图,并绑定一个页面模版到视图上,还能操作安全关系等等。然而,抽象层面来看,视图就是这么回事。
class PersonView(object):
implements(IBrowserView)
adapts(IPerson, IHttpRequest)
def __init__(self, context, request):
self.context = context
self.request = request
def name(self):
return self.context.name
def requested_shoes(self):
return self.request.get('requested_shoes', [])Notice how this adapts both IPerson and IHttpRequest, and thus takes two parameters in its __init__() method. As you will learn later, views typically inherit the BrowserView base class for convenience, but the principle is the same.注意怎样适配IPerson和 IHttpRequest,因此在__init__()中提供两个参数。稍后,你能学到,视图通常继承BrowserView 基类,但两者的原理是一样的。
To obtain a multi-adapter, you can't use the "calling an interface" syntax that you use for a regular adapter. Instead, you must use the getMultiAdapter() method:
为包括一个多适配器,不能用普通适配器"调用接口"的方法,而应该用 getMultiAdapter() 方法。
>>> from zope.component import getMultiAdapter
...
>>> personView = getMultiAdapter((person, request,), IBrowserView)
You could use queryMultiAdapter() instead if you wanted it to return None instead of raise a ComponentLookupError when it fails to find the adapter.如果你想在找不到adapter的情况下,返回一个none而不报错,应该用queryMultiAdapter() 代替getMultiAdapter()。
The above code has a problem, however (apart from being an incomplete example) - what if you have more than one view on the same object, say for two different tabs? To resolve this ambiguity, views are actually named multi-adapters. The names correspond to the names used as part a URL, and are registered using the name attribute in ZCML. This is used in browser:page and browser:view directives, but can also be used in the standard adapter directive:
然而,上面的例子是不完整的,如果同一个对象有多个视图,比如说对象提供两个不同的tab。为解决这种问题,要采用命名的多适配器,视图名称对应为URL的一部分,通过ZCML用name属性注册。通常是放在browser:page and browser:view语句里面完成注册,但也能用在标准的适配器 配置语句里面。
<adapter factory=".sampleviews.PersonView" name="index.html" />To get this particular view, we can write:
为取得这个特定的视图,我们代码如下:
>>> personView = getMultiAdapter((person, request,), name=u'index.html')conventionally, we leave off the required interface when we used named adapters, although you can supply it if necessary.通常,我们用命名适配器时,停用要求的接口。
Multi-adapters are useful for other things as well. If you have an adapter and find that every method takes at a common parameter, it's a good candidate for a multi-adapter. Also observe that in the case above, we could register a different adapter for a different type of request as well as for a different type of object. Again, the Component Arhictecture will find the most specific one looking at both interfaces.
Named adapters do not have to be multi-adapters, of course. They are typically used in cases where something (e.g. the user) is making a selection from a set of possible choices (such as choosing the particular view among many possible views).
命名适配器不一定是多适配器,当一个用户从多个视图中选择一个时,就用到这种情况。
Utilities工具应用
In the CMF, we have tools, which are essentially singletons. They contain various methods and attributes and may be found using the ubiquitous getToolByName() function. The main problem with tools is that they live in content space, as objects in the ZODB, and require a lot of Zope 2 specific things.
在CMF中,tools本质上是独立的。这些tools包含各种方法和属性,可以用gettoolbyName()取到。这些tools都对象上下文空间中有效,并且要求大量zope 2特性。
Let's say we had a shoe locating service (very useful when you can't find your shoes):
我们设计个查找shoe的服务:
class IShoeLocator(Interface):
"""A service for finding your shoes
"""
def findShoes(owner):
"""Find all shoes for the given owner.
"""
class DefaultShoeLocator(object):
implements(IShoeLocator)
def findShoes(self, owner):
return ... The Component Architecture contains a very flexible utility registry, which lets you look up things by interface and possibly by name. Unlike adapters, utilities do not have context, and they are instantiated only once, when Zope starts up. Global utilities are not persistent (but local utilities are - see below).
部件体系包含灵活的工具应用注册,可以通过接口和名称准确找到工具。工具和适配器不同,不需要context(上下文),他们仅仅当zope启动时,实力化一次。
As with adapters, we register utilities with ZCML:
向用适配器一样,我们用ZCML注册工具应用:
<utility factory=".locator.DefaultShoeLocator" />
Alternatively, you could skip the implements() call on the factory and set it in ZCML. This may also be necessary in order to disambiguate if you have more than one interface being provided by the utility component:另外,可以不在工具应用的factory中调用implements(),而将其放在ZCML中。这种情况在当你的工具应用要支持多个接口时是必要的。
<utility
factory=".locator.DefaultShoeLocator"
provides=".interfaces.IShoeLocator
/>Now you can find the utility using getUtility():现在,通过getUtility() 找到工具应用:
>>> from zope.component import getUtility
>>> locator = getUtility(IShoeLocator)
>>> locator.findShoes(u"optilude")
...The utility registry turns out to be a very useful generic registry, because like the adapter registry, it can manage named utilities. Let's say that you had a few different shoes you wanted to keep around:
工具应用的注册湘适配器注册一样,可以采用命名的工具应用。假定你周围有各式各样的shoe:
>>> left = Shoe()
>>> right = Shoe()
...
>>> from zope.component import provideUtility
>>> provideUtility(left, name=u'left-shoe')
>>> provideUtility(right, name=u'right-shoe')
We can now find these utilities again using the name argument to getUtility().我们通过用name参数,用getUtility() 能找到这些工具应用:
>>> to_put_on = getUtility(IShoe, name=u'left-shoe')
Of course, we are still using the transient global utility registry, so these will diseappear when Zope is restarted. We could use local components instead (see below), or we could register them using ZCML. If we had defined the shoes left and right in a module shoes.py, we could write:当然,上例只是短暂的全局应用注册,当zope重启时,就会消失。我们能用本地部件代替(下文),或者通过ZCML注册。假定我们在shoes.py模块中定义了shoes left 和shoes right,在ZCML中能如下配置:
<utility
component=".shoes.left"
name="left"
/>
<utility
component=".shoes.right"
name="right"
/>
An alternative would have been to define two classes LeftShoe and RightShoe and use the factory attribute of the directive instead of component (which refers to an instance, rather than a class/factory).另外一种方法,可以定义两个classes LeftShoe and RightShoe 用ZCML语句的factory属性取代部件(引用一个实力代替引用一个class/factory)
Local components本地部件
The examples above all use global, transient registries that are reloaded each time Zope is restarted. That is certainly what you want for code and functionality. Sometimes, you would like for utilities to be a bit more like their CMF cousins and also manage persistent state. To achieve that you need to use local components, which are stored in the ZODB.
上文所有例子都是采用全局的、短暂的注册,必须在zope每次重启后,重新调入。你也许想能象CMF一样,能够管理这些工具应用一个可持续的状态。这时,你需要保存在ZODB中的本地部件。
Prior to Zope 3.3, which is included in Zope 2.10, local components were a bit of a black art. Then came the jim-adapter branch and everything was greatly simplified. The theory is still the same, the API is just much more sane. Each time Zope executes a request (or if you implicitly invoke zope.component.setSite(), for example in a test), it discovers which is the nearest site to the context. In Plone, the site is normally the root of the Plone instance, but in theory any folder could be turned into a site.
每次zope执行一个request(或者如果你在测试中调用了zope.component.setSite(), )系统将最近的站点作为context.在Plone中,最近的站点通常是Plone实例的根,但原理上任何文件夹都可视作一个站点。
A site has a local component registry, where local utilities and adapters may be defined. This means that a particular utility or adapter can be specific to a particular Plone site, not affecting other Plone instances in the same Zope instance. You cannot use ZCML to register local components, since ZCML is inherently global (at least for now) - it does not know anything about your particular sites. However, you can register them with Python code, e.g. in an Install.py or a GenericSetup profile, using calls like provideUtility() (and its equivalent, provideAdapter()) called on a local site manager instance:
站点有本地部件注册功能,能注册本地工具应用、适配器。这意味着一个特别的工具应用或适配器能够指定到一个特别的plone站点,而不会影响同一个zope实例中的其他Plone实例。不能用ZCML注册本地部件,因为ZCML继承了global,ZCML不知道任何关于特定站点的东西。然而,可以用python代码来注册他们,例如在install.py或GenericSetup配置文件中,用类似于provideUtility() (或provideAdapter() )来调用在一个本地站点实例中。
>>> from zope.component import getSiteManager
>>> getUtility(IShoe, name=u'left-shoe) is left
True
>>> sm = getSiteManager(context)
>>> sm.provideUtility(myShoe, name=u'left-shoe')
>>> getUtility(IShoe, name=u'left-shoe) is myShoe
True
Unfortunately, Plone 2.5 does not run on Zope 2.10. We won't cover local components here, because, well, I never learnt how to do it the Zope 2.9 way, and what I saw of it scared me. I'm told it's not that bad, and there is documentation in Five and in Zope 3 itself. Local components will become more important in Plone 3.0, where Zope 2.10 or later will be required and more things that use local components will be part of the core.不幸的是,Plone2.5没有运行在zope2.10上,所以,我们在此不深入介绍本地部件。本地部件在Plone3中将变得更加重要。
b-org does not use local components yet, and we will see how the extension mechanism would benefit from local components so that you could have one b-org extension installed in one Plone instance and another extension installed in another Plone instance, without the two interfering. Luckily, to code that uses adapters and utilities, it is completely transparent whether they are global or local.
b-org项目也不用本地部件,但我们将看到来源于本地部件的扩展机制。因此我们可以一个b-org扩展安装在一个plone实例,另一个扩展安装到另一个plone实例,他们之间不会相互影响。幸运的是,工具应用和适配器的代码是完全透明的,无论是global 还是local。
Conclusion结论
That's it! If you can master the concepts of interfaces, adapters and utilities you will go far in a Zope 3 world. They will become much more natural as you use them a few times, and you'll probably wonder how you ever managed without them. Hopefully, that point will come before the end of this tutorial, which is largely focused on showing how the principle of separation of concerns can be imposed upon your Archetypes and Plone code.
这就是 zope 3 部件体系结构的介绍。如果你理解了接口、适配器、工具应用的概义,你就可以去到(扬名)zope 3世界。
这些东西,在你多次使用后,将变得很自然。我们着重在将松散耦合原理强加在Archetypes 和Plone code之上。
Overview of b-org b-org项目概览The big picture
To the user, b-org presents itself as three content types:
b-org定义了三种内容类型:
Department A container for employees, and a source of groups. That is, each department becomes a group, and the employees within that department become group members. 一个包含员工的父对象,用户组的来源,每一个部门可以是一个组,从属于这个部门的员工自动成为组的成员。 Employee Information about employees, and a source of users. That is, each active employee object becomes a user who can log in and interact with the portal. 员工相关信息 ,用户的一个来源。也即每一个员工都将成为一个能登陆该项目的用户。Project A project workspace - a folder where employees can collaborate on content. Content inside the project folder has a custom workflow, and employees who are related to the project (by reference) have elevated permissions over this content. 项目空间,员工能够协作处理里面的内容,并且这些内容都定制了工作流,每个员工对其相关的内容有严格的权限。
Out of the box, these are not terribly interesting, because they have only the minimum of metadata required to function. The task of providing actual schema fields, view templates, content type names (if Department, Employee and Project are not appropriate) and other application-specific facets is left up to simpler third-party products that plug into b-org. One example of such a product is included, which models a hypothetical charity use case and is called charity.
脱离本文,上述这些没有多大意思,因为仅仅为了满足功能而定义了最小的元数据。提供实际schema字段的任务、视图模版,内容
类型类型名称以及面向应用的一些特别的东西都被提到可以插入b-org的第三方产品当中。b-org附带的一个样例产品,该产品定义了
一个charity模型,这也是charity名称的由来。
This seemingly innocuous orchestration of functionality is achieved by a variety of means:
这个项目达到的功能很显然是由很多成员完成的:
Archetypes Used to build the actual content types and their schemata. 用于建立实际的内容类型和他们的schemate(元数据) The Zope 3 Component Architecture Is used to make all this exensibility possible - you will see lots of examples of interfaces, adapters and utilities. 这个部件体系是该项目具备扩展功能的原因。你将看到大量的接口、适配器和工具应用。 Membrane The content types are registered with membrane to be able to act as groups and users 内容类型用membrane 注册能够被视为组和用户。PAS and PlonePAS The Pluggable Authentication Service is used by membrane to actually provide user sources. A custom
PAS plug-in is also used to manage local roles for members and managers within projects and
departments. 由membrane采用的可插拔认真服务 提供用户源。定制的PAS插件用来管理成员的本地角色和项目内的managers和部门。 GenericSetup The next-generation set-up and installation framework is used to install and configure b-org. charity demonstrates how GenericSetup XML profiles can be used directly, without depending on the actual GenericSetup import mechanism. 用来安装配置b-org的下一代安装配置框架。charity证明GenericSetup XML profiles 能够直接使用,而不必依靠实际的GenericSetup import 机制。Zope 3 events
Zope 3's event dispatch mechanism is used to ensure employee users actually own their own Employee
objects, among other things. zope3的事件分发机制用于确保员工用户在众多东西当中拥有他们自己的对象。Zope 3 views The charity demo uses views for its display templates. charity应用将演示视图以及视图的显示模版。 Annotations Employees' passwords are hashed and stored in an annotation 员工口令被hash并存储在一个annotatin. Placeful workflow To let content inside projects have a different workflow to that of the rest of the site, each project uses a CMFPlacefulWorkflow policy. 为让该项目内的内容有不同于站点其他部分的工作流,每个项目采用一个定制的CMFPlacefulWorkflow 策略。On the following pages, you will learn about each of these components and how it fits together. Meanwhile,
you can follow along the code by looking in the subversion repository, or getting b-org from its
product page.
在下面的章节将解释每一个这样的部件怎样集成在一起。
To Archetype or not to ArchetypeArchetypes is still the most complete framework for building content types quickly. With the advent of Zope 3, there is an alternative in Zope 3 schemas. Here's why b-org doesn't use them.
There is a growing consensus that Archetypes has grown a little too organically. On the one hand, Archetypes has given us a lot of flexibility, and made many of us more productive than we would ever have thought possible (for those who remember the heady days of plain Zope 2, and then plain CMF development). On the other hand, Archetypes has become fairly monolithic. The reference engine, for example, is woven tightly into the field type machinery, and the way that views are composed from widgets makes these almost impossible to re-use outside of Archetypes.
In practical terms, the biggest headache that arises from Archetypes' evolution is the very same problem we identified when introducing Zope 3 concepts - it's hard to re-use Archetypes-based components without sub-classing and repeating a large portion of a type's configuration. Take the Poi issue tracker, for example - I frequently get requests from people who want to add a few use-case specific fields to each issue, or add some new functionality such as having private issues or issues submitted on behalf of someone else. The problem is that I don't want to put all this functionality in Poi itself, because this would increase the complexity of the product and thus the maintenance burden and probably impact the intuitiveness of the UI, when in reality not everyone would benefit from such new features.
Ideally, someone would be able to plug in their own schema fields and add some logic in well-defined places without having to re-invent all of Poi. However, this is difficult, because, for example, the "add issue" button assumes you are adding a PoiIssue object, which has a schema defined wholly in Products/Poi/content/PoiIssue.py. There are custom form controller scripts to handle saving of issues, and a lot of methods are found in the various content classes to do things like send mail notifications or perform issue searches for various lists. Again, changing the logic of who gets an email notification or how a particular list of open issues is calculated may involve subclassing one or all of Poi's content types, re-registering view templates and other content type information, and possibly customise a number of templates and scripts to reference the new subclassed types. Of course, when Poi itself changes, keeping these customisations up-to-date becomes difficult.
Zope 3 has, in keeping with its philosophy, approached these problems by promising separation of concerns. In Zope 3, you would typically define an interface that specifies the schema of a content type, and then create a class that is only concerned with holding and persisting the data for this schema:
Zope 3有自己的哲学(设计理念),那就是通过松散耦合来解决这些问题。在Zope 3中,通常通过定义一个接口来描述一个内容类型的schema,然后创建一个class,该class仅仅关心怎样操纵和处理数据为该schema。
from zope.interface import Interface
from zope import schema
class IIssue(Interface):
"""A tracker issue
"""
title = schema.TextLine(title=u"The short title of this issue", required=True)
severity = schema.Int(title=u"The severity of this issue", required=True, default=3)
...
from persistent import Persistent
from zope.interface import implements
class Issue(Persistent):
implementS(IIssue)
title = u""
severity = 0The actual functionality for sending notifications etc would be in various adapters (e.g to INotifying), the view logic in views. Forms can be created from schema interfaces like IIssue above, using zope.formlib. This can handle proper add forms (so the object is not created until the form has been filled in, which is another headache with CMF content types and therefore also Archetypes), validation, edit forms etc. Each form, adapter and menu entry (for the "add" menu, say) is registered separately, meaning that they can also be overridden and customised separately. Rocky Burt has written an excellent tutorial on how to use formlib in a Plone context that may be enlightening.象发送通知等一些具体的功能将由适配器来完成。表单能用zope.formlib从schema接口中提取信息来创建。这能恰当地添加表单()、验证输入、编辑表单等等。每个表单、适配器、菜单条(比如"add"菜单条等)被独立注册,意味着所有这些都能被独立的覆盖和定制。这里有一
个很好的formlib教程:how to use formlib in a Plone context 。
There are voices that say we should dump Archetypes entirely in favour of Zope 3-style content objects. Other voices (including my own) say that this may be a bit premature. Certainly, Zope 3 schemas and content objects are not yet fully integrated into CMF and Plone, so you end up depending on some CMF base classes at the very least. Moreover, the number and richness of widgets available for Zope 3 forms does not yet match that of Archetypes. Fundamentally, Archetypes has been around for a long time and has grown to meet a wide variety of use cases, whereas in the context of Plone at least, Zope 3 schemas are a new kid on the block.
The point is - Archetypes is not going to go away, not for a long time anyway, and are still the right choice for many types of applications. Almost all of Plone's add-on products use Archetypes, and it is well-understood by our developer community. The more likely scenario is that Archetypes will evolve in the same way that Zope 2 is evolving, by seeing its internals refactored piecemeal and pragmatically to take advantage of Zope 3 equivalents and concepts, until theoretically an Archetypes schema and content object is just a different spelling for what Zope 3 is doing, and Zope 3's content type story offers the same richness as Archetypes does (and more).
In the meantime, Archetypes is the right choice for b-org (and for other membrane-based systems). What we will try to do, however, is to alleviate the aforementioned problems by making use of Zope 3 design techniques, in order to make b-org extensible and flexible.
The extension story 扩展样例One of the main drivers behind the componentisation of b-org is that it should be easy to extend and customise for third party developers. We'll take a look at how such customisations may look, before considering how we made it possible.
利用b-org部件体系的一个主要动力是其能被第三方开发人员容易地扩展和定制。下面,我们来看下怎样定制。
b-org ships with an example called charity, found in the examples/charity directory, which demonstrates one use-case specific implementation of b-org. This is quite simple, consisting of the following top-level files and directories:
b-org附带一个charity样例,在examples/charity目录,该例子b-org的一个具体应用实现。他的目录结构如下:
configure.zcmlRegisters the schema extension adapters (see below) and references the browser package 注册扩展schema的适配器,并引用browser包 Extensions/ Contains an Install.py script that configures the Factory Type Information for the Department, Employee and Project content types. It does so by using GenericSetup XML files, but invokes the import handlers explicitly rather than through a GenericSetup profile. 包含一个install.py安装脚本,用来为 Department, Employee and Project内容类型配置FTI。通过调用import handlers 来采用GenericSetup XML 代替GenericSetup profile。 browser/Contains Zope 3 views for the charity department, employee and project content types, and a configure.zcml to register these. More on views in a later section. 包含为Department, Employee and Project内容类型 的视图,以及注册这些视图的ZCML文件。schema/ Contains adapters that extend the schemas for Departments, Employees and Projects with use-case specific fields. 包含为 Department, Employee and Project内容类型 扩展schema的适配器和用户特别的字段。
To use charity you should copy or symlink it from Products/borg/examples/charity to Products/charity. It can be
installed as normal, but you must install b-org first. See borg/README.txt for the full install instructions!A key
aim is to make it possible to meaningfully extend b-org without needing to subclass all its types. Of course,
you can do that, but in most cases it's not necessary. Unfortunately, the mechanisms and techniques
described here will be "global" in nature. That is, you will not be able to have two different modes of
ustomisation for two different Plone instances in the same Zope instance. This is because prior to Zope
2.10 (which Plone 2.5 does not support - it wasn't out until several months after Plone 2.5 was released),
the "local" components story in Zope 3 was not fully developed. There is also a specific problem with the way
the schema extension mechanism works which makes it inherently global.
对于使用charity应用,你应该copy或符号链接 Products/borg/examples/charity 到Plone的产品目录Products/charity,并且
在安装它之前,安装好b-org.charity应用最大的看点是能扩展b-org而不需要subclass所有类型。不幸的是由于这里采用的机制和技
术是"global",所以,对于同一个zope实例下的不同plone实例不能产生两个不同的模型。
When Plone 3.0 rolls around, it will support local components much better, and Archetypes 1.5, in conjunction with a third-party product called ContentFlavors (or possibly another similar tool), will enable the kind of extension story described here to work on almost any type. At that point, the forerunner you see in b-org now will be obsolete.
Plone 3到来后将支持本地部件和Archetypes 1.5再联合象contentFlavors这样的三方产品,理论上讲可以扩展任何类型。
Of course, if you don't need two different b-org customisations for two different Plone sites in the same Zope instance (which I suspect most people can work around - having two separate Zope instances of course isolates you from all of this), you should be fine.
当然,如果你不需要在同一zope下为两个不同的plone实例定制两个不同的b-org,则丝毫不受影响。
The schemas extendersIf you look at charity/configure.zcml you will see the following registrations:
<adapter factory=".schema.department.DepartmentSchemaExtender" />
<adapter factory=".schema.employee.EmployeeSchemaExtender" />
<adapter factory=".schema.project.ProjectSchemaExtender" />These schema extenders are adapters that hook into a specific part of b-org. We will describe this in moredetail later, but here is how they look from the point of view of the extending product:
这些schema扩展是挂钩到b-org特定部分的适配器,我们稍后将详细描述:
from zope.interface import implements
from zope.component import adapts
from Products.Archetypes.atapi import *
from Products.borg.interfaces import IEmployeeContent
from Products.borg.interfaces import ISchemaExtender
CharityEmployeeSchema = Schema((
StringField('title',
accessor='Title',
required=True,
user_property='fullname',
widget=StringWidget(
label=u"Full name",
description=u"Full name of this employee",
),
),
StringField('email',
validators=('isEmail',),
required=True,
searchable=True,
user_property=True,
widget=StringWidget(
label=u"Email address",
description=u"Enter the employee's email address",
),
),
StringField('phone',
required=False,
searchable=True,
user_property=True,
widget=StringWidget(
label=u"Phone number",
description=u"Enter the employee's phone number",
),
),
StringField('mobilePhone',
required=False,
searchable=True,
user_property=True,
widget=StringWidget(
label=u"Mobile phone number",
description=u"Enter the employee's mobile phone number",
),
),
StringField('location',
searchable=True,
user_property=True,
widget=StringWidget(
label=u"Location",
description=u"Your location - either city and country - or in a company setting, whereyour office is located.",
),
),
StringField('language',
user_property=True,
vocabulary="availableLanguages",
widget=SelectionWidget(
label=u"Language",
description=u"Your preferred language.",
),
),
TextField('description',
required=True,
searchable=True,
user_property=True,
default_content_type='text/html',
default_output_type = 'text/x-html-safe',
allowable_content_types = ('text/html', 'text/structured', 'text/x-web-intelligent',),
widget=RichWidget(
label=u"Biography",
description=u"Enter a short biography of the employee",
),
),
))
class EmployeeSchemaExtender(object):
"""Extend the schema of an employee to include additional fields.
"""
implements(ISchemaExtender)
adapts(IEmployeeContent)
def __init__(self, context):
self.context = context
def extend(self, schema):
schema = schema + CharityEmployeeSchema
# Reorder some fields
schema.moveField('description', after='mobilePhone')
schema.moveField('location', before='description')
schema.moveField('language', before='description')
schema.moveField('roles_', after='description')
return schemaThis example is employee.py. The other extensions are simpler, and work on the exact same principle. Whencalculating the schema of a content type, the b-org types (by virtue of Products.borg.content.schema.ExtensibleSchemaSupport, a mix-in class that all the b-org types uses, and which
the aforementioned changes to Archetypes should make obsolete) will look up an adapter from the content
object (which is marked with IEmployeeContent, in this case), to ISchemaExtender. This will be given the chance
to extend (and modify) the schema of the type.
为了计算(组建)一个内容类型的schema,b-org类型将查找一个适配该内容对象到ISchemaExtender的适配器,从而给出机会扩展和修
该该类型的schema。
The returned value is cached (to avoid an expensive re-calculation each time the schema is used). This cache
can be invalidated upon an event, which you will see in charity/Extensions/Install.py:
schema计算返回的值被缓存(避免每次使用该schema时重新计算)。这个缓存值能通过一个事件来声明无效,具体参见
charity/Extensions/Install.py
from zope.event import notify
from Products.borg.content.schema import SchemaInvalidatedEvent
from Products.borg.content.employee import Employee
...
def install(self, reinstall=False):
...
notify(SchemaInvalidatedEvent(Employee))
The event is an instance of a class that implements ISchemaInvalidatedEvent, and takes a class as anargument to know which class the schema is being invalidated for.
该事件是实现ISchemaInvalidatedEvent 接口的类的一个势力,它取得一个类名作为参数,以明确将被通知无效的schema是哪一个类的。
Defining new views and type information定义新的视图和类型信息
We have now managed to add new schema fields to Department, Employee and Project. The auto-generated
edit form will pick these up for editing, but we probably also want some custom views. We may also want to
change other aspects of the Factory Type Information (FTI) which controls how the type is presented within
Plone's UI (an FTI is an object in portal_types).
我们已经添加了新的schema字段到Department, Employee and Project 内容类型,自动生成的表单将提取所有这些字段来实现
编辑。我们也想定制某些视图,改变某些FTI的样式,通过这些来来控制内容类型在Plone UI的表现。
First, we define some views in the browser package. These are described in a later section, but lookin
at charity/configure.zcml, you will see:
首先,我们定义某些视图在browser包中,请看charity/configure.zcml:
<include package=".browser" />This will bring in charity/browser/configure.zcml, which contains several directives like:
这个语句将调用charity/browser/configure.zcml ,里面的主要语句如下:
<page
name="charity_employee_view"
for="Products.borg.interfaces.IEmployeeContent"
class=".employee.EmployeeView"
template="employee.pt"
permission="zope2.View"
/>This, along with the class Products.charity.browser.employee.EmployeeView and the template charity/browser/employee.pt will make a view @@charity_employee_view (the @@ is optional, but servesto disambiguate views from content objects, for example) available on any employee (or rather, any object
providing IEmployeeContent).
用 Products.charity.browser.employee.EmployeeView 和charity/browser/employee.pt 模版将生成一个视图
@@charity_employee_view (@@是可选的,在此使用仅仅为消除来自内容对象视图的歧义)对任何employee有效。
We then need to tell Plone that this view should be invoked when you view an Employee object or click its
'View' tab. This is done by setting the (Default) and view method aliases for the Employee type. See
this page of the RichDocument tutorial for some background.
然后,当我们查看员工对象或点击他的"view" tab时,我们也要告诉plone这个视图将被调用。
To achieve this, we could modify portal_types/Employee in Python during the Install.py script. However, to make
it easier to define the FTI, we use a GenericSetup XML file instead. Take a look at
charity/Extensions/setup/types/Employee.py, for example:
为达到这个目的,我们能在install.py中用python修改 portal_types/Employee ,但是,我们这儿有更简单的办法来定义FTI,
我们用一个GenericSetup XML 文件即可:
<?xml version="1.0"?>
<object name="Employee"
meta_type="Factory-based Type Information"
xmlns:i18n="http://xml.zope.org/namespaces/i18n">
<property name="title">Employee</property>
<property name="description">A charity employee or volunteer.</property>
<property name="content_icon">employee.gif</property>
<property name="content_meta_type">Employee</property>
<property name="product">borg</property>
<property name="factory">addEmployee</property>
<property name="immediate_view">base_edit</property>
<property name="global_allow">False</property>
<property name="filter_content_types">False</property>
<property name="allowed_content_types" />
<property name="allow_discussion">False</property>
<alias from="(Default)" to="@@charity_employee_view"/>
<alias from="view" to="@@charity_employee_view"/>
<alias from="edit" to="base_edit"/>
<alias from="properties" to="base_metadata"/>
<alias from="sharing" to="folder_localrole_form"/>
<action title="View" action_id="view" category="object" condition_expr=""
url_expr="string{object_url}" visible="True">
<permission value="View"/>
</action>
<action title="Edit" action_id="edit" category="object" condition_expr=""
url_expr="string{object_url}/edit" visible="True">
<permission value="Modify portal content"/>
</action>
<action title="Properties" action_id="metadata" category="object" condition_expr=""
url_expr="string{object_url}/properties" visible="True">
<permission value="Modify portal content"/>
</action>
<action title="Sharing" action_id="local_roles" category="object" condition_expr=""
url_expr="string{object_url}/sharing" visible="True">
<permission value="Modify portal content"/>
</action>
</object>
This defines the various aspects of the FTI, and is basically a modified copy of the equivalent file from the b-org extension profile. You'll learn more about these in the section on GenericSetup, but for now observe that we invoke this explicitly in Install.py, via some boilerplate utility code:这样就定义了FTI的各项,下面仔细观察我们在install.py中怎样调用更新这个配置:
from Products.charity.Extensions.utils import updateFTI
def install(self, reinstall=False):
...
if not reinstall:
updateFTI(self, charity, 'Department')
updateFTI(self, charity, 'Employee')
updateFTI(self, charity, 'Project')This will update the FTIs by examing Products/charity/Extensions/setup/types. Each file there is named corresponding to the name of the FTI it modifies.
Adding new functionality扩展新的功能
Extending the schema and modifying the FTI to support different views is probably enough for a large number
of use cases. If you find yourself thinking "I wish I could add a method to the Employee class to support …",
take your left hand, hold it out, raise you right hand and slap your left wrist sternly, then read the section
on adapters again.
扩展schema和修改FTI以支持不同的视图,对于大多数应用情况,这两点就够了。如果你在考虑怎样添加新的的功能,这个
能方便地由适配器来做,请看下文:
For example, let's say you wanted to send an email to administrators when a particular button in the view
was clicked. You could do that in an adapter. For examples, in your interfaces module, you could could have:
举例,假设你想通过点击视图上某个按钮,发送一个邮件到管理员。你能用一个适配器来实现这个。在接口模块,增加一个接口
定义:
from zope.interface import Interface
class IAdministratorNagging(Interface):
"""Someone who will nag the admin
"""
def nag(message):
"""Send nagging email
"""Then, an adapter from IEmployee in module nag.py:
然后,在nag.py中定义一个适配器 适配IEmployee 到IAdministratorNagging
from zope.interface import implements
from zope.component import adapts
from interfaces import IAdministratorNagging
from Products.borg.interfaces import IEmployeeContent
from Products.CMFCore.utils import getToolByName
class NaggingEmployee(object):
implements(IAdministratorNagging)
adapts(IEmployeeContent)
def __init__(self, context):
self.context = context
def nag(self, message):
mailHost = getToolByName(self.context, 'MailHost')
...And finally, in your configure.zcml:
<adapter factory=".nag.NaggingEmployee" />Then, in the form handler that is about to nag the employee, you would do:
然后,在表单界面的动作触发句柄中:
from Products.myproduct.interfaces import IAdministratorNagging
nagger = IAdministratorNagging(employee)
nagger.nag("Give me more disk space!")
Obviously, this is a somewhat contrived example, but hopefully you get the gist.Modifying workflow and other configuration修改工作流和其他配置
The b-org workflows are not special. In your Install.py, you could modify them or change the workflow assignments as you would any other content type. You can also use CMFPlacefulWorkflow to assign different
workflows depending on context, if need be.
b-org工作流没有什么特别,你定制它的工作流就像定制其他内容类型一样。你也能采用CMFPlacefulWorkflow 指派取决于上下文的
不同工作流。
Similarly, if you need to modify the behaviour of the Department, Employee and Project types in other ways,
for example by modifying settings in portal_properties, you are of course free to do so. The intended pattern
is that your b-org customisation product encapsulates the various settings and extensions that describe your
use case.
类似的你也可以用其他方法来修改Department, Employee and Project 类型的行为。如 可以通过ZMI修改Portal_properties设置。理想的模式是由b-org定制产品来封装各种设置和扩展以满足你的应用要求。
Changing fundamental b-org behaviour变更基本的b-org行为
Lastly, as you learn about b-org you will see how it uses adapters to hook into membrane. If you need to
override its behaviour, you can add an overrides.zcml to your product, which is otherwise identical to a
configure.zcml in format, but is able to override earlier registrations (such s those in b-org). For example, you
could override the adapter from IEmployeeContent to IUseRelated to change the way in which user ids is
assigned, or the adapter to IUserAuthentication to change the way in which authentication is performed.
最后,将学习b-org怎样用适配器来插入membrane。如果你要覆盖某些行为,你可以在产品包中加一个overrides.zcml ,这个文件的格式和configure.zcml 一样,但是它能覆盖先前的注册。例如你可以覆盖一个适配 IEmployee到 IuseRelated的适配器 ,以改变当一个用户id被指派时的行为。或者覆盖为 IUserAuthentication 适配的适配器,以改变当一个认证过程被执行时的行为。
Filesystem organisation 文件系统组织b-org attempts to ahere to modern ideal about how code should be laid out on the filesystem.
b-org在此将演示怎样以理想的模式组织在文件系统中。
In the Zope 3 world, the Products pseudo-namespace is frowned upon. In Zope 2, every extension module
lives in the Products/ folder. This raises some obvious namespace clash concerns, but also separates Zope
modules further from plain-Python modules. In Zope 3, you can install a module anywhere in your
PYTHONPATH. For example, in Plone 3.0, there will be a module called plone.portlets, normally installed in lib/python/plone/portlets.
在Zope 2的世界,所有产品扩展模块都在Products文件夹下。这可能导致某些名称空间冲突,但也能隔离zope模块从平面的pyton模块到更深的路径模块。在Zope 3中,你可以将模块安装到PYTHONPath路径的任何地方。例如,在plone3中,有个模块叫Plone.portlets,通常可安装在lib/python/plone/portlets
For module that need to act like Zope products (i.e. they need an initialize() method, they install content types,
they register a GenericSetup profile or CMF skins or use an Extensions/Install.py method, say), this works in
Zope 2.10 and later. It can also be made to work in earlier version of Zope using a product (ironically) called
pythonproducts.
For the purposes of borg, we stick with the traditional Products/ installation. It's nice to have imports like
from borg import …, but fundamentally, b-org is very closely tied to Zope (2) and Plone, so the re-use argument
goes away, and that nice import syntax is not really worth the extra dependency and configuration.
One thing you may notice, though, is that the borg product is named in lowercase, in keeping with Zope 3 and
Python naming conventions. Looking inside it, you will see the following key files and directories:
对于b-org,我们严格地用传统的Products/intallation模式。
__init__.py Initialises the Zope 2 product machinery, registers content types, the skin layer and the GenericSetup
extension profile that is used to install b-org. zope2产品机制初始化,注册部件内容类型、皮肤和需要安装b-org的genericeSetup扩展配置文件。 config.py Holds various constants 定义各种常量。 configure.zcml Starts the Zope 3 snowball going. This references other packages with their own configure.zcml files. zope 3技术,用以引用其他包和这些包自己的configure.zcml配置文件。 content/ Contains the Archetypes content types for Department, Employee and Project. Also contains some utilities,
like EmployeeLocator, an adapter to find employees, two utilities used to provide vocabularies AddableTypesProvider and ValidRolesProvider, and the the schema extension mechanism in schema.py. 包含Department, Employee and Project 的Archetpes内容类型,和某些工具应用。如EmployeeLocator 寻找员工的适配器,AddableTypesProvider and ValidRolesProvider 两个工具应用。还有个schema扩展。events/ Contains event subscribers which modify ownership of an Employee object so that the employee user
owns it (and can thus edit their own profiles, for example), as well as to set up the local workflow when a Project is created. 包含修改员工对象的从属关系以便员工用户拥有它的事件预定,以及当一个项目创建时,为其设置工作流。interfaces/ Contains all the interfaces that b-org defines, in various sub-modules like interfaces/employee.py for the
Employee-related interfaces. All of these are imported into interfaces/__init__.py, so that you can write
from Products.borg.interfaces import …. 包含b-org项目的所有接口,所有这些接口被导入到interfaces/__init__.py中,以便通过from Products.borg.interfaces import …. 导入membership/Contains various adapters for plugging into membrane which enable b-orgs user-and-group functionality. 包含各种为使用用户组功能而嵌入membrane的各种适配器。 pas/ Contains a custom PAS plug-in which is used to manage the local roles for Project members 包含用于管理本地项目成员角色而定制的PAS插件。 permissions.py Contains custom add-content permissions, so that the ability to add Department, Employee and Project
content objects can be controlled by different permissions. 包含定制添加的内容权限,以便能添加部门、员工和项目内容对象。 profiles/ Contains the GenericSetup extension profile that sets up b-org. This is registered in the borg/__init__.py. 包含用于设置b-org的GenericeSetup的扩展配置文件,该文件在 borg/__init__.py注册。setuphandlers.py Defines a custom GenericSetup "import step" which configures aspects of b-org that cannot be expressed
in the existing GenericSetup XML formats. 定制GenericSetup "import step",配置b-org样式,弥补某些不能在已存在的 GenericSetup XML表述的部分。skins/ Contains the borg skin layer, which is registered in borg/__init__.py. This contains only the b-org icons. These could potentially have been defined in a browser package using Zope 3 resources, but are included in a traditional skin layer to make them easier to customise using conventional methods. See the section on Zope 3 views for more details.定义b-org皮肤层,目前仅仅包含b-org图标。 tests/Contains unit and integration tests. 包含单元测试和集成测试。 zmi/ Defines a ZMI page for adding the PAS plug-in, for completeness' sake. 为通过ZMI添加PAS插件而定义的一个zmi页。 You will notice that there are many directories, and many of these directories contain the same set of files -
employee.py, department.py and project.py. This is a side-effect of the finer-grained components and increased
separation of concerns that stem from Zope 3 design concepts. For products that act less as framework, the
degree of separation may be lower, and thus the product may appear smaller. However, as you browse b-org's
source code, it should become obvious why things are placed where they are, and how code is grouped
together by logical functionality rather than a tight coupling to Archetypes content types.
Interfaces 接口In Zope 3, everything is connected to an interface in some way. Sure enough, b-org has a slew of them.
Getting the interface design right is often more than half the battle, so pay attention to this part.
在ZOPE 3中,任何东西都通过某些方法连接到接口。b-org中有很多这样的接口。设计正确的接口是成功的一半,请充分注重接口这部分。
If you were trying to understand b-org without a comprehensive tutorial to hand, you would do well to look at the interfaces package. You will notice that this is subdivided into various files
如果没有一个合适的教程,要理解b-org,最好认真看看interfaces包,你可以发现这里再细分出很多文件。
interfaces/department.py Contains a description of a department (IDepartment) and a marker interface for the content object that stores the department (IDepartmentContent). 包含一个department接口和一个为保存部门数据的对象的marker interface。 interfaces/employee.py Contains the equivalent interfaces, IEmployee and IEmployeeContent, as well as the definition of a specific
event interface, IEmployeeModified. 包含 两个对等的接口,一个IEmployee,一个IEmployeeContent,另外还有一个事件接口IEmployeeModified。interfaces/project.py Again contains IProject and IProjectContent, as well ILocalWorkflowSelection, which is used to denote a utility that defines the placeful workflow policy that projects will use. 包含IProject 和IProjectContent 以及一个用于定义工作流的工具应用的ILocalWorkflowSelection 。interfaces/workspace.py Holds the interface IWorkspace, which is used by the local-role PAS plug-in to extract which users should
have which local roles in a project. interfaces/schema.py Contains interfaces relevant to the custom schema extension mechanism - ISchemaExtender, IExtensibleSchemaProvider and ISchemaInvalidatedEvent. 包含定制扩展shema机制的相关接口,如ISchemaExtender, IExtensibleSchemaProvider and ISchemaInvalidatedEvent 。interfaces/utils.py Defines interfaces that are used as input to various vocabularies - IEmployeeLocator, IAddableTypesProvider and IValidRolesProvider. 定义用于输入各种词汇的接口:IEmployeeLocator, IAddableTypesProvider and IValidRolesProvider 。
In order to understand what each of these interfaces describes in more detail, look at the files above. Recall that interfaces are mainly documentation - these interfaces are accompanied by docstrings and generally self-documenting code.
为了更详细了解各种接口,请查看这些接口源文件。
The various interfaces intended for public consumption are imported to interfaces/__init__.py, so that client code can write, e.g.:
为了让其他模块方便调用这些接口,它们通过interfaces/__init__.py导入,其他模块可以这样引用接口:
from Products.borg.interfaces import IEmployee
This is a common idiom. If you find yourself with too many interfaces to manage in interfaces/__init__.py, you
don't necessarily need to do this, but it's probably a sign that you should be breaking your code into smaller
packages!
Remember that unless you have a particular need to depend on Zope 2, then you don't need to pollute the
Products namespace with such components! (and even if you do, with PythonProducts or Zope 2.10, you can do
without the Products/ namespace too). For example, we could have placed the employee functionality in a
package borg.employee, found in lib/python/borg/employee as a plain-python library, possibly depending on Zope
3 components (i.e. packages in the zope.* namespace).
除非你有特别的要求,否则不要破坏zope2这种产品命名空间。
Conversely, if you have relatively few interfaces, you can simply have an interfaces.py module without a directory.
如果接口很少,我们可以用一个简单的interfaces.py来代替一个接口目录。
Separating Archetypes from real components从部件中分离ArcheTypes
One thing you may notice is that we have split the interface describing the concept of e.g. an employee
(IEmployee) from the interface that describes the employee content object in the ZODB (IEmployeeContent).
Whether this is always the right thing to do is debatable, but the reasoning goes something like this:
有件事情需要注意,我们分离描述员工概义的接口IEmployee 和描述员工内容对象的接口IEmployeeContent。是否这总是正确的有待商讨,但我们这里这样做的原因是:
Archetypes objects contain a very large API. Archetypes schemas and the infamous ClassGen generate
methods on the content objects corresponding to schema fields, so that a field name gets an accessor called
getName() and a mutator called setName(). This is all rather Archetypes-specific, and in Zope 3 schemas, we
typically prefer simple properties (a name attribute) to pairs of methods. To avoid being constrained by the
Archetypes when defining interfaces (Archetypes is just one implementation choice), we created IEmployee as
follows:
Archetypes对象包含一个巨大的API。Archetypes schemas和生成schema 字段的ClassGen 生成方法。以便一个命名为name的字段自动获得一个getName() 和一个setName() 。这些是Archetypes 的独有的特性。而在zope 3 schemas中通常只有简单属性和某些方法,当定义接口时(Archetypes只是其中一种实现方式),为了避免被Archetypes限制,我们创建IEmployee:
class IEmployee(Interface):
"""An employee, which is also a user.
"""
id = schema.TextLine(title=u'Identifier',
description=u'An identifier for the employee',
required=True,
readonly=True)
fullname = schema.TextLine(title=u'Full name',
description=u"The employee's full name for display purposes",
required=True,
readonly=True)To support this, we could put the relevant properties into the Archetypes content object, but this is cumbersome,since the property() declaration normally used to convert methods to properties will only work when those methods actually exist, not when they are created by ClassGen.为配合这个,我们放置相关的属性到Archetypes内容对象,但是,令人遗憾的是用于转换methods 到properties的property()申明仅仅当那些methods确实存在时,才有效,而不能由ClassGen创建。
Instead, we mark the content object with a marker interface, IEmployeeContent and then register an adapter to
IEmployee. Strictly speaking, this is cheating, since the adapter makes assumptions about its context (such as
which methods are available, and the fact that it uses Archetypes) that are not formally defined in the interface.
To save excessive typing and retain some sanity in the interface definitions, it's not a terrible compromise though.
Here's the adapter, from membership/employee.py:
一种解决方法,我们标记内容对象用一个marker interface ,即IEmployeeContent接口,然后注册一个适配器到IEployee接口。严格讲,这种方法有某些欺骗性,因为这个适配器假定他的上下文(诸如哪一个方法是有效的,以及使用Archetypes的事实)不是正规的接口定义。但为了节约某些文字输入以及保留接口定义完整性,这不是一个很糟糕的折衷办法。下面是这个适配器定义:
class Employee(object):
"""Provide department information.
"""
implements(IEmployee)
adapts(IEmployeeContent)
def __init__(self, context):
self.context = context
@property
def id(self):
return self.context.getId()
@property
def fullname(self):
return self.context.Title()Now, you can write:现在可以这样来调用:
emp = IEmployee(some_employee_content_object)
print emp.fullname
Another side-effect of this pattern is that we can separate things that are Archetypes-dependent from things that operate on the more general notion of an employee. For example, membrane generally makes assumptions about operating on Archetypes content objects, so the various membrane adapters adapt IEmployeeContent, whereas the view for charity employees is only concerned with "real" employees and so adapts the context to IEmployee.This pattern is repeated for Departments and Projects as well.这种模式另外一个作用是分离某些Archetypes依靠的东西从一个员工对象更通常的概义。例如,membrane通常做些关于操纵Archetypes内容对象的假定,
Interfaces intended for utilities and adapters为工具应用提供的接口和适配器
Although interface design should generally not be too concerned with how those interfaces are implemented,
you will often think "this is going to be used a a utility" or "this will most likely be an adapter". In this case, you
may want to make some reference in the doc-string at least. For example, the ILocalWorkflowSelection interface
states:
尽管接口设计通常不太和接口的具体实现相关,但是,你还是要经常考虑"这个接口将用于一个工具应用" 还是“这个接口将用作一个适配器。”在这个意义上讲,你应该至少提供些文档字符串。例如 ILocalWorkflowSelection 接口 :
class ILocalWorkflowSelection(Interface):
"""A selection of a local workflow for projects.
This will normally be looked up as a utility.
"""
workflowPolicy = schema.TextLine(title=u'Workflow policy identifier',
description=u'The id of the placeful workflow policy to use',
required=True,
readonly=True)
Conversely, many interfaces are context-dependent, which means that most likely they will either be directly provided by a particular object or adaptable to it. Take the IAddableTypesProvider:另外,很多接口是上下文相关的,意思就是他们或者由特定的一个对象提供,或者是可适配该对象的,看下IAddableTypesProvider :
class IAddableTypesProvider(Interface):
"""A component capable of finding addable types in a given context.
"""
availableTypes = schema.Tuple(title=u'Available types',
description=u'A list of all addable types',
value_type=schema.Object(ITypeInformation))
defaultAddableTypes = schema.Tuple(title=u'Default addable types',
description=u'A list of types to be addable by default',
value_type=schema.Object(ITypeInformation)) The implication here is that client code will do something like:
下面是应用该接口的代码:
from Products.borg.interfaces import IAddableTypesProvider
addableTypes = IAddableTypesProvider(context).availableTypes
Whether IAddableTypesProvider was provided directly by the context or (more likely) provided via an adapter isnot important. The only time this distinction is really useful is in the case of marker interfaces, such as
IEmployeeContent:
无论IAddableTypesProvider是由上下文直接提供,还是通过一个适配器提供都不重要。重要的是这个区别在marker interface中是相当有用的,例如:
class IEmployeeContent(Interface):
"""Marker interface for employee content objects"""
These are often checked with providedBy(): 通常用ProviderBy()来申明:assert IEmployeeContent.providedBy(employeeContentObject)
# we've got an employee, good
Again, the guiding principle here is separation of concerns. The aspect of a component that can provide a list of addable types (IAddableTypesProvider) is logically distinct from (and could be varied independently of) the aspect
of a component that specifies it represents a project (IProject), even though it so happens that at present
projects are the only time we concern ourselves with restricting addable types.
重申,主要原理是松散耦合。一个能提供可添加类型列表的部件的样式是逻辑区分于一个指定它表现一个项目(IProject)的部件的样式。尽管在这里我们关心用受限制的可添加类型。
In the olden days, we would probably have put methods like getAvailableProjectAddableTypes() into the Project
content type. Hopefully, you'll see why this is less optimal than having it in a separate component (hint: what if you in your customisation of b-org wanted to be much more particular about which types were addable?). You will
hopefully start to pick up "fat" interfaces during interface design - if you had a neat IProject interface that
described attributes of a project that were to be saved alongside the project object, and then found a couple of
methods about defining addable types that were related to one another but not so much to the data of a
project in general, you would hopefully reach for a new interface. If so - well done, you're getting there.
在过去,你也许通过放置 getAvailableProjectAddableTypes() 这样的方法在这个项目内容类型中。但在这儿,你将明白为什么如果这样做比分离部件的方式是不够优化的。(提示:……)如果你已有一个整洁的接口描述一个项目对象的属性,你将希望在接口设计中实现一个肥的(复杂的)接口,但是随后将发现关于定义可添加类型的一系列方法,并且他们是相互关联(影响)的,而且所有这些并非项目Project通常的数据。这时,你将希望设计一个新的接口——(也就是上面描述的方法)
Test-driven development 测试驱动开发Testing should come first, not last, when doing development.
当做开发时,首先要做好测试,而不是最后来做
One of the greatest things that Zope 3 has established is a culture of test-driven development. Because Zope 3 components tend to be small and not dependent on a large framework or (typically) a running application server, tests are easier to write and execute faster. Most Zope 3 testing happens in the form of testable documentation - DocTests - which tell the story of how a component should be used along with testable examples.
zope 3最重要的事情之一是建立了一种测试驱动开发文化。因为zope 3部件在大的框架中或在应用服务中通常是较小的,并且互不相关,写部件测试非常容易,运行测试也非常快。大部分的zope 3测试通过可测试文档(DocTests)的形式进行,DocTests通过可测试的例子来描述一个部件将怎样被应用。
The testing tutorial explains the philosophy behind test-driven development and the tools and techniques available in Zope. It is required reading if you are not familiar with testing in Zope, and probably quite useful even if you are.
这个testing tutorial教程解释了 在测试驱动开发和zope3 中的工具和技术背景下的测试哲学。如果你不了解zope 3下的测试,你应该好好读它,应该很有帮助。
Testing strategy 测试策略Tests were (largely) written against interfaces and stub implementations, before the actual functionality was written. One of the first test cases to be created was test_adapters.py, which simply verifies that the various adapter registrations are in effect. This is obviously an integration test (using PloneTestCase), since it is verifying what happens on a "normal" Zope start-up.
在写实际功能模块之前,针对接口和接口的实现写测试。第一个测试应当是验证各种适配器注册是正确有效的,我们命名为test_adapters.py。很明显,这是一个集成性测试(用PloneTestCase),这是验证在zope正常启动时,发生的一些事情。
You will also notice tests named after the three content types, test_department.py, test_employee.py and test_project.py. Each of these contains tests that verify the given type is available and can be instantiated and edited. This catches errors in Archetypes registrations or schemas. There are then further tests for the membrane integration and for the adapters to the canonical interfaces IDepartment, IEmployee and IProject. Lastly, non-trivial methods in content types and relevant adapters are given their own test fixtures.
你也将注意到在三个内容类型当中,还有test_department.py, test_employee.py and test_project.py,这些测试是验证给定的内容类型能正常实例化,并可编辑。捕获的错误在Archetypes注册和schema当中。更深入的测试在membrane集成和规范接口IDepartment, IEmployee and IProject,最后,内容类型当中和适配器相关的一些重要的方法的测试在他们各自的测试当中。
By being systematic and diligent with tests, many, many bugs were caught and dealt with before they ever hit a live system. Of course, this does not replace in-browser acceptance testing, which was also performed regularly.
通过系统化的、亲老的测试,许许多多bug将被发现。当然,这些不能代替实际的浏览器可接受性测试。
At the time of writing, there are no zope.testbrowser based functional tests for the user interface. That is regrettable - and this is an open source project after all, so feel free to contribute some!
在写该教程时,还没有以 zope.testbrowser 为基础功能的用户接口测试。
Test set-up 测试设置You will find b-org's tests in the tests module. Most of these use are DocTest integration tests, using PloneTestCase. Make sure you use a recent version of PloneTestCase (or svn trunk) since there have been some recent changes in how Zope 3 components (or rather, ZCML registrations) are loaded for test runs. The upshot is that with PloneTestCase, things should "just work" for integration testing - components you have defined in ZCML in your products will be loaded as they would when Zope is started.
b-org 的测试在tests模块,大多数用DocTest集成测试,用PloneTestCase。确认正在使用最新的PloneTestCase,因为有些新的变更在zope3 部件为测试运行而装载时。使用PloneTestCase的结果是在你产品中已定义的部件将正常被装载完成集成测试,就好像zope 重启一样。
The file base.py contains an insulating base class for b-org tests, called BorgTestCase and its sister-class BorgFunctionalTesetCase. When imported, this file will trigger the setup of a Plone site with the membrane and borg extension profiles installed, as such:
文件base.py包含了一个为b-org测试的基类BorgTestCase以及他的姊妹类BorgFunctionTestCase。当这个文件被导入时,将触发用membrane和borg扩展配置文件配置一个Plone站点。象下面一样:
from Testing import ZopeTestCaseal
# Let Zope know about the two products we require above-and-beyond a basic
# Plone install (PloneTestCase takes care of these).
ZopeTestCase.installProduct('membrane')
ZopeTestCase.installProduct('borg')
# Import PloneTestCase - this registers more products with Zope as a side effect
from Products.PloneTestCase.PloneTestCase import PloneTestCase
from Products.PloneTestCase.PloneTestCase import FunctionalTestCase
from Products.PloneTestCase.PloneTestCase import setupPloneSite
# Set up a Plone site, and apply the membrane and borg extension profiles
# to make sure they are installed.
setupPloneSite(extension_profiles=('membrane:default', 'borg:default'))Integration and unit tests 集成测试和单元测试Most of the tests are integration test that are set up like so:
大多数集成测试被设置为:
import unittest
from Testing.ZopeTestCase import ZopeDocTestSuite
from base import BorgTestCase
from utils import optionflags
def test_creation():
"""Test that departments can be created an initiated.
>>> self.setRoles(('Manager',))
>>> id = self.portal.invokeFactory('Department', 'dept')
>>> dept = self.portal.dept
Set roles.
>>> dept.setRoles(('Reviewer',))
>>> tuple(dept.getRoles())
('Reviewer',)Add an employee and set it as a manager. >>> id = dept.invokeFactory('Employee', 'emp')
>>> dept.setManagers((dept.emp.UID(),))
>>> tuple(dept.getManagers())
(<Employee at ...>,)
"""...
def test_suite():
return unittest.TestSuite((
ZopeDocTestSuite(test_class=BorgTestCase,
optionflags=optionflags),
))There is also a plain-python (no loading of Zope necessary, which is much faster) unit test for the password digest in test_passwords.py. This is appropriate because the functionality under test does not depend on the Zope application server or database being loaded. Use plain-python (or perhaps rather, plain Zope 3) tests whenever you can to reduce interdependencies and test load times:
这是一个纯粹python代码的单元测试。这很适合我们,因为测试功能不依靠zope 应用服务器,也不依靠数据库。你可以减少相互之间的依赖性,加快测试装载速度。
import unittest
from zope.testing.doctestunit import DocTestSuite
from utils import configurationSetUp, configurationTearDown, optionflags
def test_passwords_hashed():
"""Check that passwords are hashed
We expect that the password will be saved as a SHA-1 digest.
>>> import sha
>>> digest = sha.sha('secret').digest()Set a password.
>>> from Products.borg.content.employee import Employee
>>> e = Employee('emp')
>>> e.setPassword('secret')
The value is stored in an annotation, and there is no direct way to
access it (deliberately). Thus, check the annotation directly.
>>> from zope.app.annotation.interfaces import IAnnotations
>>> from Products.borg.config import PASSWORD_KEY
>>> annotations = IAnnotations(e)
>>> password = annotations[PASSWORD_KEY] Ensure it is what we expected:
>>> password == digest
True
"""
...
def test_suite():
return unittest.TestSuite((
DocTestSuite(setUp=configurationSetUp,
tearDown=configurationTearDown,
optionflags=optionflags),
))
The functions configurationSetUp() and configurationTearDown() are defined in utils.py and are used to load specific ZCML files that enable the test environment to function. This is necessary because without PloneTestCase's integration test layer in effect, there will be no compnent registrations when the tests are run! This may be more cumbersome (though in reality, the same set of components tend to be used), but also allows better control over the environment in which test are run, in addition to (much) faster test execution times.函数(功能)configurationSetUp() and configurationTearDown() 定义在utils.py中,用来装载配置测试环境所需的ZCML文件。由于没有PloneTestCase集成到测试,当测试运行时,不会发生部件注册,所以这个是必须的。这也许令人讨厌,()但可以控制测试运行时环境,获得更快的测试执行时间。
From utils.py:
import doctest
from zope.app.tests import placelesssetup
from zope.configuration.xmlconfig import XMLConfig
# Standard options for DocTests
optionflags = (doctest.ELLIPSIS |
doctest.NORMALIZE_WHITESPACE |
doctest.REPORT_ONLY_FIRST_FAILURE)
def configurationSetUp(self):
"""Set up Zope 3 test environment
"""
placelesssetup.setUp()
# Ensure that the ZCML registrations in membrane and borg are in effect
# Also ensure the Five directives and permissions are available
import Products.Five
import Products.membrane
import Products.borg
XMLConfig('configure.zcml', Products.Five)()
XMLConfig('meta.zcml', Products.Five)()
XMLConfig('configure.zcml', Products.membrane)()
XMLConfig('configure.zcml', Products.borg)()
def configurationTearDown(self):
"""Tear down Zope 3 test environment
"""
placelesssetup.tearDown()You will also find a regular unit test in test_setup.py, simply because this was quicker to write:
你将在test_setup.py中发现一个标准的单元测试,这个非常简单:
from base import BorgTestCase
from Products.membrane.interfaces import ICategoryMapper
from Products.membrane.config import ACTIVE_STATUS_CATEGORY
from Products.membrane.utils import generateCategorySetIdForType
from Products.borg.config import LOCALROLES_PLUGIN_NAME, PLACEFUL_WORKFLOW_POLICY
class TestProductInstall(BorgTestCase):
def afterSetUp(self):
self.types = ('Department', 'Employee', 'Project',)
def testTypesInstalled(self):
for t in self.types:
self.failUnless(t in self.portal.portal_types.objectIds(),
'%s content type not installed' % t)
...
def test_suite():
from unittest import TestSuite, makeSuite
suite = TestSuite()
suite.addTest(makeSuite(TestProductInstall))
return suite
Finally, there is an docstring DocTest for the ExtensibleSchemaSupport class. This is because this class if largely standalone (it probably shouldn't be b-org at all, but in a more general module, except Archetypes will gain similar functionality of its own for Plone 3.0) and the test provided important documentation in the class' docstring.
最后,还有一个为ExtensibleSchemaSupport 类的docstring DocTest。
The class looks like this:
class ExtensibleSchemaSupport(Base):
"""Mixin class to support instance-based schemas.
Note: you must mix this in before BaseFolder or BaseContent, e.g.:
class Foo(ExtensibleSchemaSupport, BaseContent):
...
This is based on Archetype's VariableSchemaSupport.
Define a content type with a marker interface:
>>> from zope.interface import Interface, implements
>>> class IMyType(Interface):
... pass
>>> from Products.Archetypes.atapi import *
>>> from Products.borg.content.schema import ExtensibleSchemaSupport
>>> class MyType(ExtensibleSchemaSupport, BaseObject):
... implements(IMyType)
... schema = BaseSchema.copy() + Schema((StringField('foo'),))
>>> registerType(MyType, 'testing')
Create a schema extender:
...
"""
implements(IExtensibleSchemaProvider)
...And the test runner, in test_schema.py, contains:在test_schema.py中的test runner包含如下:
import unittest
from Testing.ZopeTestCase import ZopeDocTestSuite
from base import BorgTestCase
from utils import optionflags
def test_suite():
return unittest.TestSuite((
ZopeDocTestSuite('Products.borg.content.schema',
test_class=BorgTestCase,
optionflags=optionflags),
))Setup using GenericSetup 用GenericSetup安装配置b-org uses GenericSetup to impose itself on your Plone instance. Here's how it works.
b-org采用GenericSetup来安装到Plone实例中。下面是其工作原理。
Hands up if you have ever written a workflow definition in Python and tried to figure out how to install it in your Extensions.py and thought, this is the least useful API I have ever had to deal with. Actually, the API is not that bad, it's just not very good for performing set-up. Similarly, it may start to make your separation-of-concerns-brainwashed mind a little uneasy that we tend to define aspects of the type's configuration as class attributes in an Archetypes class (though of course it's better than using a CMF FTI dict or mangling portal_types directly).
如果你用python写了一个工作流定义,想要在Extensions.py中描述怎样安装它。这是我们打过交道的有用的api,对于执行安装配置,这个API还不赖,但也不是很好。我们想要在一个Archetypes类中定义某些类型配置的样式作为类属性,但以API这种方式来理解的话。不容易实现松散耦合。(尽管这种方式明显好于CMF FTI dict或直接修改portal_types)
The fine folks who gave us the CMF came up with another way, called GenericSetup (after a few name changes - you may see the names CMFSetup and ContentSetup as well, which refer to predecessors of what is not GenericSetup). This is based on a declarative XML syntax that can represent site configuration. The configuration of an entire site is called a profile and can be imported and exported to replicate state across multiple Plone (or CMF) sites. There is a smaller version of a profile called an extension profile which can be used to extend a base profile. Both membrane and b-org use extension profiles to install themselves.
聪明的人们给我们提供了另一种方法——GenericSetup(先后经历多次名称变更,CMFSetup ContentSetup等)。这个方法是用基于XML语法来描述站点配置。整个站点的配置叫一个配置文件,能在多个Plone实例间导入导出以实现复制状态信息。一个配置文件较小的版本叫扩展配置文件,能用来扩展一个基本的配置文件。membrane 和b-org都使用扩展配置文件来安装。
GenericSetup is described a tutorial by Rob Miller, cheif GenericSetup protagonist, so we won't repeat too much of the detail here. However, you should be aware that in Plone 2.5, GenericSetup has a slightly awkward user experience and does not have any well-defined way of performing uninstall, which stems from the fact that it was originally designed for the use case of taking a snapshot of the configuration of an entire site, not for installing and uninstalling products and extensions!
GenericSetup详细教程是由Rob Miller写的教程tutorial 。在此,我们不过多重复。然而,我们应该明白在Plone 2.5中,GenericSetup有些难于使用的用户体念,即 没有定义一个好的方式来用于uninstall.事实上,它原来的设计目标就是取得整个站点配置的快照,而不是为了安装或卸载产品或扩展。
The other main shortcoming at the moment is that there is no way to specify interdependencies between profiles. It is important that membrane is installed before b-org, but if you're not careful it will happen the other way around. When you create a Plone site, you will be able to choose a number of extension profiles to apply (including meaningless ones like Archetypes - meaningless because Plone already invokes those when you set up a site). In this list, "Base organisation" comes before "membrane" by virtue of alphabetical sorting. Therefore, you can't just choose both and click "Add". Instead, you should select "membrane" first, and then add "Base organisation" via portal_setup, as described in the b-org README.txt:
另外一个缺点是没有办法指定不同配置文件间的互依赖性。membrane 在b-org之前安装是重要的,但如果你不小心的话,可能出问题。当创建一个PLone站点时,有一个可供选择的配置文件列表(包括无意义的Archetypes)。在这个列表中"Base organisation"按字母顺序出现在"membrane"前面。你不能两个都选择,然后点击"Add",而应该首先选择"membrane",而后再选择安装"Base organisation"。
- Go to portal_setup in the ZMI
- Click the Properties tab
- Select "Base organisation" as the active profile (since this is an extension profile, it won't override the base profile that set up your Plone site) and click Update.
- Go to the Import tab and click Import all steps at the bottom. Note that although it seems like this will re-install a whole bunch of stuff, it will only execute those steps that are actually listed in the import_steps.xml for the active profile, which in this case is b-org's.
If you didn't already set up membrane and you created a Plone site without the membrane extension profile, follow the same steps to install membrane before you install b-org.
如果你没有设置membrane,你创建了一个不带membrane扩展配置的plone站点,你应该先安装membrane。
So why did we do all this? Firt of all, both membrane and b-org are really infrastructure that fundamentally influence how you build your site, so the lack of uninstall isn't as bad as it would have been for more user-facing products. Secondly, with Plone 3.0, this will become easier, as the QuckInstaller (and hence the Add/Remove Products control panel page) becomes Extension Profile aware and gives some uninstall support.
首先,membrane和b-org都是基础架构,能根本影响一个站点怎样建立,因此,缺乏uninstall对于多用户使用的产品不一定是坏事。其次,在Plone3.0由于QuickInstaller已变成扩展配置文件明白的部件,因此,能提供uninstall支持。
At the end of this section, you will see how you can use a traditional QuickInstaller Install.py method and still get the nice XML syntax, with a bit of extra work.
在本段的最后,我们将看下怎样用传统的QuickInstaller Install.py method 和XML语法。
Import stepsTo GenericSetup, the installation of a third party product via an extension profile is considered to be the importing of that profile. A file import_steps.xml is used to determine which actual import steps will be executed. First, we need to tell GenericSetup where the import steps are defined, though, by registering the extension profile. This is done in the product's __init__.py:
对于用GenericSetup通过扩展配置文件安装第三方产品,安装过程就是配置文件的导入。一个Import_steps.xml配置文件用于决定哪些导入步骤将被执行。首先,我们需要告诉GenericSetup导入配置文件在哪里。通常通过在产品的__init__.py中注册扩展配置文件来完成。
from Products.CMFPlone.interfaces import IPloneSiteRoot
from Products.GenericSetup import EXTENSION, profile_registry
...
def initialize(context):
...
profile_registry.registerProfile('default',
'Base organisation',
'Organisation and project infrastructure',
'profiles/default',
'borg',
EXTENSION,
for_=IPloneSiteRoot)
This references the directory profiles/default, which contains various files:指向的目录profiles/default包含各种文件:
import_steps.xml Lists the steps to be performed during import (set-up) 列出在导入过程中执行的步骤。 export_steps.xml Lists the steps to be performed during export - that is, if the configuration is changed in the ZODB and the site admin wishes to export the configuration to a file, these steps will be performed. 列出导出过程执行的步骤。如果ZODB中的配置更改,站点管理员可以导出配置到一个文件。
membrane_tool.xml Configuration for membrane tools 配置membrane tools skins.xml Sets up skins in portal_types 在portal_type中设置 皮肤。types.xml Configures FTIs (Factory Type Information settings) for the content types that b-org ships with. Each of the types listed here has a corresponding file in profiles/default/types (the name of the type and the name of the file should match). This file contains all the various FTI settings, such as friendly name, meta type, actions and aliases. 为b-org的内容类型配置FTI信息。列出在这儿的每一个 类型都有一个对应的文件在profiles/default/types下。(类型的名称和文件名称匹配)这样的文件包含所有 FTI设置,比如友好名称、meta type, actions and aliases.等。workflows.xml Configures workflows. This works in the same way as types.xml - the main file configures the names of the workflows and the bindings of workflows to content types. The actual workflow definitions, including states and transitions, are found in profiles/default/workflows.配置工作流。这个类似于types.xml。这个文件配置工作流的名称和工作流到内容类型的绑定。实际的工作流定义包括状态、事务等是定义在profiles/default/workflows The import_steps.xml which orchistrates all this looks like follows:
import_steps.xml文件结构类似如下:
<?xml version="1.0"?>
<import-steps>
<import-step id="borg_various" version="20060803-01"
handler="Products.borg.setuphandlers.importVarious"
title="Various base-org Settings">
<dependency step="typeinfo"/>
<dependency step="skins"/>
<dependency step="workflow"/>
</import-step>
</import-steps>
Note that we don't actually specify most of the files - they are referenced by the base profile that was used to set up Plone or the extension profile for membrane. GenericSetup knows all the registered profiles' steps, and looks for the corresponding files.
注意我们实际上并没有指定这些文件。他们被引用通过用于来设置plone和设置membrane扩展配置文件的base profile。GenericSetup 知道所有注册的steps,并能查找相应的文件。
Various setup handlers 各种安装/配置句柄The one setup handler you do see is the "various" handler. This is dependent on the set-up of type info, skins and workflow. Ordinarily, setup handlers will utilise GenericSetup base classes, adapters and utility functions to parse XML files. However, it's not always convenient to invent a generic XML syntax for all types of configuration. The importVarious pattern is used by many products that need to perform some custom set-up in Python. It is invoked as if it were a handler for an XML file, but it just happens to have different side-effects. The main caveat with this type of set-up, of course, is that it cannot symmetrically export (and then re-import) the configuration, and it is more difficult to re-use.
应当明白配置句柄是多种句柄。这取决于类型信息、皮肤和工作流的配置。通常配置句柄将应用GenericSetup基类、适配器和工具应用分析xml文件。然而为所有配置类型转换通常的xml语法并不总是很方便。对于某些需要用python执行某些定制配置信息的产品,通常采用importVarious模式。这个importVarious模式被调用就好像它是一个xml文件的句柄一样。但这种方式有不同的副作用。最主要的是这个配置类型不能恰当地导出配置信息,要重用很困难。
importVarious looks as follows:
importVarious模式样例:
from StringIO import StringIO
...
def setupPlugins(portal, out):
"""Install and prioritize the project local-role PAS plug-in.
"""
...
def setupPortalFactory(portal, out):
"""Add borg types to portal_factory
"""
...
def addProjectPlacefulWorkflowPolicy(portal, out):
"""Add the placeful workflow policy used by project spaces.
"""
....
def importVarious(context):
"""
Import various settings.
Provisional handler that does initialization that is not yet taken
care of by other handlers.
"""
site = context.getSite()
out = StringIO()
setupPlugins(site, out)
setupPortalFactory(site, out)
addProjectPlacefulWorkflowPolicy(site, out)
logger = context.getLogger("borg")
logger.info(out.getvalue())We set up the PAS plugins, register our types with portal_factory and add a placeful workflow policy. The exact code to perform each of these steps is not listed here to save space, but they use the same techniques you would use in an Install.py file. Note that the portal_factory setup is available in a more friendly XML format in Plone 2.5.1 and later, which was released after b-org.我们这只PAS插件,用portal_factory注册类型,添加工作流策略。实现这些的代码不在此列出,但他们采用和install.py文件同样的技术。注意事实上portal_factory配置在plone 2.5.1以后版本有更友好的XML格式配置。
GenericSetup without portal_setupWhen Plone 3.0 arrives, it will make the Add/Remove Products control panel aware of extension profiles, and thus provide a more user friendly way of performing install using GenericSetup. It will also support uninstall. Until that time, however, it is possible to re-use the GenericSetup XML handlers to parse files like types.xml and workflow.xml from a regular Install.py installation. We do this in the charity example.
当plone3发布后,将使得添加删除产品控制面板能识别扩展配置文件,因此能用GenericSetup提供用户友好的方法来执行安装或卸载。在此,我们应用GenericSetup XML句柄来分析types.xml,workflow.xml,但采用标准的intall.py安装。我们在charity例子中演示。
When importing, GenericSetup requires a setup environment, and usually an object to work on. A simple SetupEnviron is found in charity/Extensions/utils.py, along with a method called updateFTI() which can take an FTI object and update its settings based on a types.xml-like file. This method takes a module and the id of an FTI to update, and finds the corresponding file.
当导入时,GenericSetup要求有一个对象工作的配置环境。在charity/Extensions/utils.py中有个SetupEnviron,还有一个method updateFTI(),能基于一个types.xml式样的文件来更新对象的FTI。这个method 取得一个module和被更新的FTI的id,然后找到对应的文件。
It is used in charity/Extensions/Install.py as follows:
应用示例如下:
from Products import charity
from Products.charity.Extensions.utils import updateFTI
def install(self, reinstall=False):
...
if not reinstall:
updateFTI(self, charity, 'Department')
updateFTI(self, charity, 'Employee')
updateFTI(self, charity, 'Project')The relevant files may be found in charity/Extensions/setup/types/.其他相关文件在charity/Extensions/setup/types/ 目录中。
Using membrane to provide membership behaviour用membrane提供成员行为
How b-org uses membrane to let employees be users and departments be groups
b-org怎样用membrane来让employee模拟成用户,让部门模拟成组。
Since version 2.5, the user management infrastructure in Plone has been replaced by PAS, the Zope Pluggable Authentication Service, and PlonePAS, a Plone integration layer for this. PAS offers several advantages over plain user folders, mainly in terms of flexibility. Unfortunately, it is also more difficult to work with through-the-web and has a very decentralised API, based on the notion of plugin components, that can be difficult to understand at first.
Plone 2.5以后用户管理体系由PAS取代,即所谓的zope 可插拔认证服务,plone将layer集成进来,叫做PlonePAS。PAS比平面的用户文件夹有优势,主要是灵活性。但是它很难通过TTW方式工作,并且有大量API。基于可插拔部件概义,一开始很难理解。
Membrane (or rather, membrane with a lowercase m) is a component first developed by Plone Solutions and later improved by Rob Miller and others. It is similar to CMFMember in that it can turn content objects into users, although it is less concerned with replicating existing Plone functionality and more concerned with making a thin integration layer to plug into. It therefore fits b-org very well.
membrane是由plone solutions开发的,后来,Rob Miller加以改进。它类似于CMFMember能转换内容对象到用户,它不关心复制plone功能,只关心制作一个可插进的瘦的集成的layer。因此很适合b-org。
Membrane works on Archetypes objects (though theoretically it could be used with other objects as well). It adds a tool called membrane_tool which contains a registry of content types that are member- or group-sources, as well as a special catalog. Using the Archetypes catalog multiplex, it is able to catalog objects (which may also be cataloged in portal_catalog) and find them again based on various interfaces (that is, it catalogs the interfaces provided by an object). membrane provides a number of PAS plug-ins that will search this catalog when looking for users and delegate to the content objects (or rather, adapters on the content object) for obtaining user information, performing authentication and so on.
Membrane和Archetypes对象工作(尽管原理上它也能和其他对象一起工作)。它有一个名为membtane_tool的工具,包含一个member源或group源的内容类型注册以及一个特殊的catalog。用多元的Archetypes catalog,它能分类对象(也可以在portal_catalog中被分类),又可以基于各种接口再发现它们。(那是因为分类了由对象提供的接口)。当查找用户并委派到内容对象时,membrane提供了大量PAS插件用于为包含的用户信息,执行认证等等搜索这个目录。
Registering with membranemembrane_tool contains an API for registering content types as membership providers, but the easiest option is to use a GenericSetup profile (see the section on GenericSetup for the full story). In profiles/default/membrane_tool.xml, you will find:
membrane_tool包含一个API来注册内容类型作为成员提供者。但最简单的方法是用GenericSetup配置文件。
<?xml version="1.0"?>
<object name="membrane_tool" meta_type="MembraneTool">
<membrane-type name="Department">
<active-workflow-state name="active" />
</membrane-type>
<membrane-type name="Employee">
<active-workflow-state name="active" />
</membrane-type>
<membrane-type name="Project">
<active-workflow-state name="published" />
<active-workflow-state name="private" />
</membrane-type>
</object>
This registers the three content types (by their portal type), and specifies the workflow states in which they are "active" as member and group sources.
这里注册了三个内容类型,并且指定了当它们被激活作为成员源或group 源时的工作流状态。
Applying marker interfacesWhen looking for content objects that provide group and member information, membrane will use a number of marker interfaces that indicate support for various types of behaviour. These are implemented by the three content type classes.
当查找提供group或member信息的内容对象时,membrane用了大量marker interfaces,以指明各种类型的行为。这些在三个内容类型中实现。
In content/department.py, you will find:
from Products.membrane.interfaces import IPropertiesProvider
...
class Department(ExtensibleSchemaSupport, BaseFolder):
"""A borg department.
Departments can contain other employees.
"""
implements(IDepartmentContent, IPropertiesProvider)All this means is that the Department's schema is capable of providing properties to PAS. Properties (normally related to users, but groups can have properties as well) are just metadata about the user or group. Membrane supports as PAS properties plugin that will look for Archetypes schema fields with member_property=True set and report these back as user properties. Although Department does not use any such properties at the moment, we add this marker so that extension modules that use the schema extension mechanism can benefit from this.
所有这些意味者Department的schema能提供PAS属性。属性是关于用户或组的元数据。Membrane支持PAS属性插件将查找Archetypes schema字段 用一个 member_property=True 的设置并报告作用户属性。尽管Department不用这些属性,我们在此加这个标记是为了用schema扩展机制的扩展模块能取得某些好处。
The equivalent setup for Employees, in content/employee.py, is a little more interesting.
类似Department,Employee设置如下:
from Products.membrane.interfaces import IUserAuthProvider
from Products.membrane.interfaces import IPropertiesProvider
from Products.membrane.interfaces import IGroupsProvider
from Products.membrane.interfaces import IGroupAwareRolesProvider
...
class Employee(ExtensibleSchemaSupport, BaseContent):
"""A borg employee.
Employees are also users.
"""
implements(IEmployeeContent,
IUserAuthProvider,
IPropertiesProvider,
IGroupsProvider,
IGroupAwareRolesProvider,
IAttributeAnnotatable)Here, we are saying that:
说明一下:
- An Employee can be used as a source of authentication (i.e. as a user), since it is marked with IUserAuthProvider. Note that the actual authentication is performed by a different adapter. 一个员工能被当作为一个认证源(例如:一个用户)因此可用IuserAuthProvider来标记。注意实际的认证由一个不同的适配器执行。
- An Employee can provide user properties to PAS via membrane, following IPropertiesProvider. 一个员工能通过membrane提供用户属性。
- An Employee can be part of a group, because of IGroupsProvider. 一个员工对象能作为一个组的部分
- An employee can be given roles. There is an IRolesProvider interface that we cold use for basic role awareness. The IGroupAwareRolesProvider is a sub-interface that will cause membrane to also look at the user's groups.
Projects does not require any particular marker interfaces.
Providing membership behaviour 提供成员行为When membrane looks for objects to provide membership-related behaviour, it will not only look for objects directly providing a particular interface, but also for objects that can be adapted to that interface. For example, the presence of the interface IGroup informs membrane that an object can act as a group, and contains methods that describe the members of that group.
当membrane查找对象提供成员相关的行为,它不仅查找该对象直接提供的接口,而且也查看该对象能被适配到的接口。例如接口IGroup告知membrane该对象能扮演一个GROUP。
Of course, we could have declared that Department implemented IGroup and written these methods directly in the Department content object. Hopefully you'll agree now that this would not be optimal, since it mixes the content-object aspect and the group-behaviour aspect of Department into a single monolithic object. Instead, we will use an adapter, which also means that if you require different behaviour in an extension to b-org, you have only to override the adapter, leaving the core content object alone.
当然我们可以申明Department来实现IGroup接口,并且直接在Department内容对象中写这些methods.你现在应当明白由于混合了Department的内容对象样式和组行为样式到单个对象,这不是一种好的方式。代替这种方式,我们将用一个适配器,也即意味着如果你想b-org的一种扩展有不同的行为,仅仅需要覆盖这个适配器即可。
In membership/department.py, you will see:
class Group(object):
"""Allow departments to act as groups for contained employees
"""
implements(IGroup)
adapts(IDepartmentContent)
def __init__(self, context):
self.context = context
def Title(self):
return self.context.Title()
def getRoles(self):
"""Get roles for this department-group.
Return an empty list of roles if the department is in a workflow state
that is not active in membrane_tool.
"""
mb = getToolByName(self.context, MEMBRANE_TOOL)
wf = getToolByName(self.context, 'portal_workflow')
reviewState = wf.getInfoFor(self.context, 'review_state')
wfmapper = ICategoryMapper(mb)
categories = generateCategorySetIdForType(self.context.portal_type)
if wfmapper.isInCategory(categories, ACTIVE_STATUS_CATEGORY, reviewState):
return self.context.getRoles()
else:
return ()
def getGroupId(self):
return self.context.getId()
def getGroupMembers(self):
mt = getToolByName(self.context, MEMBRANE_TOOL)
usr = mt.unrestrictedSearchResults
members = {}
for m in usr(object_implements=IMembraneUserAuth.__identifier__,
path='/'.join(self.context.getPhysicalPath())):
members[m.getUserId] = 1
return tuple(members.keys())Mostly, this is about examining the Department content object to find roles (which are listed in an Archetypes field, editable by the Manager role). When calculating roles, we make sure that we don't give roles if the Department group-source is actually disabled (by virtue of its workflow state and the settings in membrane_tool). The group title and id are taken from the object as well.
这是关于部门内容对象找到roles的测试。(roles 被列在Archetype字段,可由管理员角色编辑。)当获得角色时,如果Department被取消group源(取决于工作流状态的设定),则不应该给出角色,但应该能正常取得title和id。
The most interesting method is getGroupMembers(). Here, we perform a search in the membrane_tool catalog for objects adaptable to IMembraneUserAuth. This interface is the basic interface in membrane describing things that can act as users - there is an adapter from IUserAuthProvider to IMembraneUserAuth. We restrict this to objects inside the Department object. The net effect is that all Employee objects inside the Department are returned.
这儿最有趣的方法是getGroupMembers().我们在membrane_tool catalog中搜索能适配到ImembraneUserAuth接口的对象。在membrane中,这个接口是描述能扮作用户的基本接口。有个适配器适配IUserAuthProvider到ImembraneUserAuth。这个getGroupMembers()的最终效果是返回部门内的所有员工。
Now, let's say you had a need for a Department which in addition to acting as a group for all members inside it, also allowed some members from other departments to be in that group. In this case, you could use a schema extender to add a ReferenceField to the schema of Department that allowed the Department owner to reference other Employees. You would then provide an override adapter, perhaps subclassing Products.borg.membership.department.Group but overriding getGroupMembers() to append the ids of the referenced users as well as the contained ones … or instead of, depending on your needs.
我们假设需要为department下面的所有成员将department扮作一个group,并且允许来自其他group成员成为这个department的成员。这种情况下,你能用一个schema extender加一个ReferenceField到Deparment的schema,以允许department的主人引用其他员工。然后提供提供一个覆盖的适配器,子类化Products.borg.membership.department.Group ,但覆盖getGroupMembers() 方法以追加引用的用户id和本来包含在里面的id。或者任何其他功能,取决于你的需求。
As it happens, Projects also act as groups, with members being assigned by reference, using two reference fields - one for project members, and one for project manangers. Here is the equivalent adapter from membership/project.py:
基于上述模式,Project也可扮作group,其成员来自于由引用指定,将用到两个reference fields,一个用于project成员,另一个用于project 管理者。下面是适配器例子:
class Group(object):
"""Allow projects to be groups for related members and managers
"""
implements(IGroup)
adapts(IProjectContent)
def __init__(self, context):
self.context = context
def Title(self):
return self.context.Title()
def getRoles(self):
# The project does not imply any special roles *globally*, although
# the IWorkspace adapter above enables some local roles
return ()
def getGroupId(self):
return self.context.getId()
def getGroupMembers(self):
return [IUserRelated(m).getUserId() for m in
self.context.getRefs(PROJECT_RELATIONSHIP) +
self.context.getRefs(PROJECT_MANAGER_RELATIONSHIP)]As may be expected, the membrane adapters for Employee are a bit more involved. They consist of the following:membrane为Employee的适配器将更加复杂,它们由以下组成:
IUserRelated adapterProvides a user id for employees. Note that user ids and user names are possibly different when PAS is used: the user id must be globally unique; the user name is the named used for logging in. 为为employee提供 user id。当采用PAS时,用户名和id可以不同。IUserAuthentication adapter Used to perform actual authentication by validating a supplied username and password.用于对提供用户名和密码执行认证。 IUserRoles adapterUsed to determine which roles the particular user is given. 用于决定一个用户拥有哪些角色。 IMembraneUserManagement Used by membrane and Plone's UI to deal with changes to the user, such as the adding of a new user (not implemented here, since we All these adapters are found in membership/employee.py.
由membrane使用和Plone UI交互以更改用户对象。比如添加一个新用户(这儿暂未实现)。
The IUserRelated adapter is the simplest, as it simply invokes the user name. Note that by default, membrane will use the Archetypes UID() function as the user id. This is sensible, but unfortunately Plone's UI (and that of third party products) is not always aware of the distinction between user id and user name. Ideally, only the user name would ever be displayed, the user id being an internal concept, but in practice you may end up with things like member folder names that are long, unfriendly UID strings. Sometimes this may even be unavoidable in the general case, because it's possible that two different sources of users could use the same user name for two different user ids! For the purposes of b-org, however, we assume user names are unique and well-defined. The adapter is therefore quite trivial: 这个IUserRelaed适配器最简单,只是简单 调用用户名。注意,缺省情况下,membrane将用Archetypes UID()函数作为用户id。但是Plone UI不一定总能区别user id 和 user name。理想情况下uer name显示,user id 用于内部区分,但实际上由于不友好的UID字符串太长,而不舒服。某些时候这种情况不可避免,因为可能存在两个不同的用户source 使用同一个用户名。但在b-org中,我们假定用户名是唯一的,因此问题变得简单:
class UserAuthentication(object):
"""Provide authentication against employees.
"""
implements(IUserAuthentication)
adapts(IEmployeeContent)
def __init__(self, context):
self.context = context
def getUserName(self):
return self.context.getId()
def verifyCredentials(self, credentials):
login = credentials.get('login', None)
password = credentials.get('password', None)
if login is None or password is None:
return False
digest = sha(password).digest()
annotations = IAnnotations(self.context)
passwordDigest = annotations.get(PASSWORD_KEY, None)
return (login == self.getUserName() and digest == passwordDigest)In the verifyCredentials() method, the adapter is passed the login and password as entered by the user in a dict (credentials) and then compares those to the values stored on its context (the Employee content object). The password is stored as a SHA1 digest in an annotation to make sure it cannot be read back by examining the content object - more on this in the section on annotations. Be aware also that the IUserAuthentication adapter is called on every request after a user is logged in and can deny access for whatever reason by returning non-True. This means that it is important that the method is as efficient as possible - expensive database lookups, for example, are probably not a good idea here!在verifyCredentials() 中,适配器被传输由用户输入的用户名和密码和保存在对象上下文中的值比较。(员工内容对象的上下文)这口令作为SHA1 digest 被保存在一个annotation中,以保证在检测该内容对象时,不能再读出明文。也要明白在一个用户登陆成功后,所作出的每一个request ,都将调用这个IUserAuthentication适配器,任何导致返回值是non_True,都将被拒绝访问。这意味着这个method的有效性是非常重要的。而在此如果通过数据库查询来验证,显然不是一个好主意。
The IUserRoles adapter is trivial. Roles are stored on the content object in a field that is editable only by managers. Of course, we could have picked roles from some other rule if necessary:
这个IUserRoles 适配器相当简单。角色被保存在内容对象的一个字段中,只能被管理元编辑。当然,我们也能从其他地方提取角色。
class UserRoles(object):
"""Provide roles for employee users.
Roles may be set (by sufficiently privilged users) on the user object.
"""
implements(IUserRoles)
adapts(IEmployeeContent)
def __init__(self, context):
self.context = context
def getRoles(self):
return self.context.getRoles()The getRoles() method returns an iterable of strings representing applicable roles. Note that depending on group membership (and the IGroupAwareRolesProvider marker as described above) and local roles the user may in fact have more roles than what this method returns! The IUserRoles interface is concerned only with global roles intrinsic to the user.这个getRoles()方法返回一个递归的字符串以表现应用了的角色。注意取决于组成员(由上面的IGroupAwareRolesProvider)和本地角色,当前用户事实上夺得的角色可以比该方法返回的 更多。这个IUserRoles接口仅仅关心该用户的全局角色。
Finally, we have the IMembraneUserManagement adapter. This lets membrane know what to do when it is asked by Plone's UI to add, edit or remove users. In particular, the doChangeUser() method enables the PasswordResetTool to do its magic. Note that we have not implemented doAddUser(), because there is no well-defined global policy for where the actual Employee content object should be added! Recently membrane has gained some functionality whereby a site-local utility providing IUserAdderfrom membrane can be queried for this policy. That may be useful for b-org extension products, but b-org is still not in a position to make a general policy for this, so it is not implemented out of the box.
最后,讲讲IMembraneUserManagement 适配器。它将让membrane知道当Plone UI请求add,edit or remove 用户时,要做什么。特别,其中的doChangeUser() method 调用PasswordResetTool来做更改用户属性的事情。注意,这里没有实现doAddUser(), 因为没有定义好一个全局策略来决定实际的 Employee内容对象应该被加在哪儿。最新的membrane获得了某些功能——从membrane中的一个本地站点的工具应用提供IUserAdder来贯彻这个策略。这对于b-org扩展产品很有用。但b-org本身没有做这个。
class UserManagement(object):
"""Provides methods for adding deleting and changing users
This is an implementation of IUserManagement from PlonePAS
"""
implements(IMembraneUserManagement)
adapts(IEmployeeContent)
def __init__(self, context):
self.context = context
def doAddUser(self, login, password):
"""This can't be done unless we have a canonical place to store users
some implementations may wish to define one and implement this.
"""
raise NotImplementedError
def doChangeUser(self, login, password, **kw):
self.context.setPassword(password)
if kw:
self.context.edit(**kw)
def doDeleteUser(self, login):
parent = aq_parent(aq_inner(self.context))
parent.manage_delObjects([self.context.getId()])
That's it! Through these adapters, the three b-org content types are able to act as sources of groups and users. Hopefully, you will appreciate the flexibility of the separation of concerns into adapters for things like editing user properties, determining user id, calculating roles and performing authentication. If you extend b-org, you can provide a more specific adapter to any of the above interfaces to customise the membership behaviour.总结,通过这些适配器,三个内容类型能够模拟成group源或user源。你将发现将松散耦合注入适配器的巨大灵活性。比如编辑用户属性,判断用户id,获取角色和执行认证等。如果你扩展b-org,你可以提供更多特别的适配器来适配上述接口已定制成员行为。
Writing a custom PAS plug-in写一个定制的PAS插件
Projects require that members are given particular local roles within a project space. This is achieved using a custom PAS plug-in.
Prejects要求在一个Project空间里面给予成员特别的本地角色。这个通过定制的PAS插件来完成。
PAS was introduced in the previous section on membrane. Truth be told, it can be a bit of a jungle of plug-ins and delegation because it is so very generic. Luckily, Plone (and membrane) takes care of most of the complexity for us. Sometimes, however, it is desirable to influence the authentication and role management at a lower level.
PAS已经在前面关于membrane段介绍了。事实告诉我们,PAS插件只是众多插件和委派中的一点点。幸好,Plone完成了这方面大部分的复杂性。然而,PAS插件是希望被用来在更低的层次进行认证和角色管理。
Workspace adapters工作空间适配器
b-org ships with a bit of framework, adapted from some similar code in an unreleased version of teamspace by Wichert Akkerman, which can provide local roles in a "workspace" - in this case a Project. It relies on an adapter to the IWorkspace interface to determine the mapping of users and roles in the particular context. Before showing how this plug-in is written and registered, however, let's look at how it is used by a Project.
b-org从未发布的teamspace项目中取得类似的代码以在一个工作空间(这里指一个Project)取得本地角色。这个技术依靠一个适配到Iworkspace接口的适配器,在特定的上下文中决定用户和角色的映射。在介绍怎样编写和注册该插件前,我们先来看看它怎样在一个Project中调用:
In membership/project.py you will find:
class LocalRoles(object):
"""Provide a local role manager for projects
"""
implements(IWorkspace)
adapts(IProjectContent)
def __init__(self, context):
self.context=context
def getLocalRoles(self):
project = IProject(self.context)
roles = {}
for m in project.getManagers():
roles[m.id] = ('Manager',)
for m in project.getMembers():
if m.id in roles:
roles[m.id] += ('TeamMember',)
else:
roles[m.id] = ('TeamMember',)
return roles
def getLocalRolesForPrincipal(self, principal):
r = self.getLocalRoles()
return r.get(principal, ())This queries the lists of managers and members assigned (by reference) to the project and specifies that both managers and members should get the role TeamMember and managers should also get the role Manager.
这个查询该Project的管理者列表和已指派的成员列表(通过引用来指派成员),来指定应该得到TeamMember角色的管理者或成员,而且管理者还将得到Manager角色。
As it turns out, this behaviour is also useful in Departments, which can be given one or more department managers by reference. The idea is that department managers should be allowed to add and remove Employees within that Department (recall that Department is a folderish container for Employee objects). The analogous adapter in membership/department.py reads:
这种方式也能用在Departments中,通过引用来给一个或更多的department 管理者。department管理者应该可以在该部门内添加或删除员工(Department是一个包含Employee对象的文件夹式样的容器)。在membership/department.py 类似的适配器如下:
class LocalRoles(object):
"""Provide a local role manager for departments
"""
implements(IWorkspace)
adapts(IDepartmentContent)
def __init__(self, context):
self.context = context
def getLocalRoles(self):
project = IDepartment(self.context)
roles = {}
for m in project.getManagers():
roles[m.id] = ('Manager',)
return roles
def getLocalRolesForPrincipal(self, principal):
r = self.getLocalRoles()
return r.get(principal, ())
Thus, a container wanting to use the PAS plug-in we're about to see to manage local roles only need to be adaptable to IWorkspace. In fact, this whole machinery ought to be factored out into a separate component, possibly sharing code to teamspace, another product which provides similar functionality. Mostly, this is down to laziness - creating another product (with all its boilerplate) and managing another dependency in the Products folder seemed too onerous when b-org was being developed. Hopefully, with Zope 2.10/Plone 3.0 and a growing preference for plain-Python packages and "eggs", it will seem a little less of an obstacle to split products up into multiple smaller pieces. So much for making excuses.因而,一个容器对象想要用管理本地角色的PAS插件,只需要适配到Iworkspace接口接口。(下文关系不大,不翻译)
The plug-inThe PAS plug-in that uses the IWorkspace interface can be found in pas/localrole.py. It looks like this:
用Iworkspace接口的PAS插件在pas/localrole.py 中,类似如下代码:
# Borrowed from Project pasification branch - written primarily by
# Wichert Akkerman and Copyright Amaze Internet Services
# This module is releasd under the Zope Public License
from sets import Set
from Globals import InitializeClass
from Acquisition import aq_inner, aq_chain, aq_parent
from AccessControl import ClassSecurityInfo
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from Products.PluggableAuthService.utils import classImplements
from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin
from Products.PlonePAS.interfaces.plugins import ILocalRolesPlugin
from Products.borg.interfaces import IWorkspace
manage_addWorkspaceLocalRoleManagerForm = PageTemplateFile(
"../zmi/WorkspaceLocalRoleManagerForm.pt", globals(),
__name__="manage_addProjectRoleManagerForm")
def manage_addWorkspaceLocalRoleManager(dispatcher, id, title=None, REQUEST=None):
"""Add a WorkspaceLocalRoleManager to a Pluggable Authentication Services."""
plrm = WorkspaceLocalRoleManager(id, title)
dispatcher._setObject(plrm.getId(), plrm)
if REQUEST is not None:
REQUEST.RESPONSE.redirect(
'%s/manage_workspace?manage_tabs_message=WorkspaceLocalRoleManager+added.'
% dispatcher.absolute_url())
class WorkspaceLocalRoleManager(BasePlugin):
meta_type = "Workspace Roles Manager"
security = ClassSecurityInfo()
def __init__(self, id, title=None):
self.id = id
self.title = title
#
# ILocalRolesPlugin implementation
#
security.declarePrivate("getRolesInContext")
def getRolesInContext(self, user, object):
roles = []
uid = user.getId()
obj, workspace = self._findWorkspace(object)
if workspace is not None:
if user._check_context(obj):
roles.extend(workspace.getLocalRolesForPrincipal(uid))
return roles
security.declarePrivate("checkLocalRolesAllowed")
def checkLocalRolesAllowed(self, user, object, object_roles):
roles = []
uid = user.getId()
obj, workspace = self._findWorkspace(object)
if workspace is not None:
if not user._check_context(obj):
return 0
roles = workspace.getLocalRolesForPrincipal(uid)
for role in roles:
if role in object_roles:
return 1
return None
security.declarePrivate("getAllLocalRolesInContext")
def getAllLocalRolesInContext(self, object):
rolemap = {}
obj, workspace = self._findWorkspace(object)
if workspace is not None:
localRoleMap = workspace.getLocalRoles()
for (principal, roles) in localRoleMap.items():
rolemap.setdefault(principal, Set()).update(roles)
return rolemap
# Helper methods
security.declarePrivate("_findWorkspace")
def _findWorkspace(self, object):
"""Find the first workspace, if any, in the acquistion chain of this
object. Returns a tuple obj, workspace where workspace is the adapted
IWorkspace.
"""
for obj in self._chain(object):
workspace = IWorkspace(obj, None)
if workspace is not None:
return obj, workspace
return None, None
security.declarePrivate("_chain")
def _chain(self, object):
"""Generator to walk the acquistion chain of object, considering that it
could be a function.
"""
# Walk up the acquisition chain of the object, to be able to check
# each one for IWorkspace.
# If the thing we are accessing is actually a bound method on an
# instance, then after we've checked the method itself, get the
# instance it's bound to using im_self, so that we can continue to
# walk up the acquistion chain from it (incidentally, this is why we
# can't juse use aq_chain()).
context = aq_inner(object)
while context is not None:
yield context
funcObject = getattr(context, 'im_self', None)
if funcObject is not None:
context = aq_inner(funcObject)
else:
# Don't use aq_inner() since portal_factory (and probably other)
# things, depends on being able to wrap itself in a fake context.
context = aq_parent(context)
classImplements(WorkspaceLocalRoleManager, ILocalRolesPlugin)
InitializeClass(WorkspaceLocalRoleManager)On first glance, there is quite a lot going on here, but it is not so hard to understand. First, we define a good old-fashioned Zope 2 factory and ZMI add form. This is good practice, because PAS plug-ins can be managed via acl_users in the ZMI. If you find yourself wandering there, however, remember to bring a torch and keep a trail of breadcrumbs to find your way out. A backup wouldn't hurt either if you try to change things. It is, unfortunately, not the most intuitive of interfaces.
粗略一看,代码很多,复杂,但其实不难理解。首先,定义了一个旧式样的zope 2 factory和ZMI添加表单。……
We will see how the plug-in is registered and activated in a moment, but first notice that the plug-in implements an interface, ILocalRolesPlugin, which is defined by PlonePAS, the PAS-in-Plone integration layer. This defines methods that will be called by the PAS machinery to determine, in this case, local roles. Note that this is not an adapter (perhaps it would have been if PAS had been invented in Zope 3, though Zope 3 has its own authentication machinery that is evolved from PAS and works slightly differently). When created, the ProjectLocalRoleManager is an Zope 2 object that lives in the ZODB in acl_users.
我们接下来将这个插件怎样被注册和激活。首先,我们注意到该插件实现了ILocalRolesPlugin接口,该接口由PlonePAS定义。注意这不是一个适配器。当创建时,这个ProjectLocalRoleManager是一个在acl_users下面保存在ZODB的ZOPE 2对象。
The methods of the ILocalRolesPlugin interface are fairly self-explanatory in purpose. They allow PAS to extract the local roles for a particular user in a particular context (getRolesInContext()), to check whether a user in fact has one of the roles required to access a particular method attribute in a particular context (checkLocalRolesAllowed()), and to get a map of users-to-roles in a particular context.
这个ILocalRolesPlugin接口的方法是清楚的自解析的。为一个特定的用户在一个特定的上下文中,允许PAS抽取本地角色,以检查是否一个特定的用户在特定的上下文中有访问一个特定方法属性 要求的角色,以得到一个用户到角色的映射。
The complex parts are, as often is the case, concerned with acquistion. The helper method _findWorkspace() attempts to walk up the object hierarchy to find the first possible IWorkspace (it will only consider one) to get hold of the appropriate IWorkspace adapter that is then used to determine the actual roles that apply, as above. Without walking up the content hierarchy, it would not be possible to let the local roles of a particular project apply when in the context of a piece of content inside that project (i.e. a sub-object of the folderish Project object). There is some reasonably hairy acqusition-juggling going on in the _chain() method to return this chain as a generator. The hairiness comes from the fact that the thing that is being checked may in fact be a method that is being accessed, and aqusition chains can get themselves in all kinds of knots, especially when Five is in the mix.
复杂部分通常是关于获取。这个_findWorkspace()方法变量对象的层次以找到首个IWorkspace,从而获取恰当的IWorkspace接口的适配器,然后用它决定实际应用的角色。当在一个Project里面内容的上下文中,不遍历对象层次,就不可能应用Project的本地角色。……
Lastly, we need to declare a ClassSecurityInfo and call InitializeClass to get Zope 2 to play ball.
最后,为了让zope 2正常运行,我们需要申明ClassSecurityInfo 并且调用InitizeClass.
Registering the plug-in注册插件
To be able to use this plug-in, we must first register it with PAS. This is done when the product is loaded, in borg/__init__.py:
为了应用该插件,首先必须用PAS注册它。这个在borg/__init__.py中,产品调入时,来做。
from Products.PluggableAuthService import registerMultiPlugin
...
from pas import localrole
...
registerMultiPlugin(localrole.WorkspaceLocalRoleManager.meta_type)
def initialize(context):
context.registerClass(localrole.WorkspaceLocalRoleManager,
permission = AddUserFolders,
constructors = (localrole.manage_addWorkspaceLocalRoleManagerForm,
localrole.manage_addWorkspaceLocalRoleManager),
visibility = None)
...This is similar to how CMF content types are initialised with ContentInit().initialize() and context.registerClass(). In other words, copy-and-paste and the less you know the happier you will be.这个过程类似于CMF内容类型用ContentInit().initialize() and context.registerClass(). 初始化。
By registering the plug-in, we could now ask our users to instantiate a Workspace Roles Manager within acl_users…. er… somwhere. Like we said - not necessarily obvious. Better to do it once, in the setup code for b-org. Please refer to the section on GenericSetup to learn how b-org is actually installed, but notice that the relevant code is in setuphandlers.py:
通过注册这个插件,我们现在能要求我们的用户在acl_users或任何其他地方实例化一个Workspace Roles Manager 。请参考GenericSetup段学会b-org怎样被安装。但是注意在 setuphandlers.py中的相关代码:
from Products.CMFCore.utils import getToolByName
from Products.PlonePAS.Extensions.Install import activatePluginInterfaces
from config import LOCALROLES_PLUGIN_NAME
...
def setupPlugins(portal, out):
"""Install and prioritize the project local-role PAS plug-in.
"""
uf = getToolByName(portal, 'acl_users')
borg = uf.manage_addProduct['borg']
existing = uf.objectIds()
if LOCALROLES_PLUGIN_NAME not in existing:
borg.manage_addWorkspaceLocalRoleManager(LOCALROLES_PLUGIN_NAME)
print >> out, "Added Local Roles Manager."
activatePluginInterfaces(portal, LOCALROLES_PLUGIN_NAME, out)
All we do here is get hold of the factory dispatcher for the user folder (from manage_addProduct, which has something to do with that registerClass call for the WorkspaceLocalRoleManager seen in the previous code example, but like we said, it's dont-ask, don't-tell) and if it is not there already, we create an instance of the plugin using the factory. We then need to activate it so that it actually takes effect. out is a StringIO output stream used for logging.
所有在此做的是为用户文件夹获取factory构造器,如果该插件不存在,那么我们就用该factory构造器实例化一个插件。然后我们激活它。这里 out是StringIO输出流为登记日志。
Placeful workflow嵌入工作流
b-org uses CMFPlacefulWorkflow, which ships with Plone 2.5, to manage the workflow of content objects inside a project.
b-org 用捆绑在Plone 2.5当中的CMFPlacefulWorkflow 来管理一个Project里面的内容对象的工作流。
Placeful workflows are based on the concept of policies. You can think of a policy as a mapping of workflows to types, in the same way as you could control from the portal_workflow tool. Policies are created, normally by copying an existing policy (possibly the default, global policy), and then applied to a context. In Plone, this can be done using the policy option in the state menu.
嵌入工作流是基于策略概义。你可以想象一个策略是工作流到类型的一个映射,用同样的方法,你能从portal_workflow控制。创建策略通常是拷贝一个现存的策略(可能是缺省的全局的策略),然后应用它到一个上下文。在Plone中,这个可以在state 菜单中,用policy 来做。
Placeful workflows are used in b-org Projects. Inside a project, project members should have elevated view and modify permissions over content. This is achieved using the following technique:
嵌入工作流被用在b-org Project中。在一个Project里面,项目成员应该有超级试图和修改权限。这是通过下面的技术来实现的:
- A new role TeamMember is made available within any Project. 在任何Project里面,一个新的TeamMember有效。
- A custom workflow, borg_project_default_workflow is a customisation of the default Plone workflow that has a simplified set of states and actions, and is aware of the TeamMember role. 一个定制的工作流,borg_project_default_workflow 是缺省Plone工作流的一个定制,有简单的states 和actions集合,能够明白TeamMember角色。
- A placeful workflow policy sets the default workflow, as well as the workflow for folders, to this one. 一个嵌入工作流策略设置缺省的工作流,以及为文件夹的工作流。
- When a Project is created, this placeful workflow policy is enabled for the project.当一个Project创建时,这个嵌入的工作流被启用。
定制的工作流用GenericSetup定义在profiles/default/workflows/borg_project_default_workflow/definition.xml 中。必要的话,你当然可以安装自己的工作流。工作流策略在setuphandlers.py 的 importVarious setup step 中设置。
from Products.CMFCore.utils import getToolByName
from config import LOCALROLES_PLUGIN_NAME, PLACEFUL_WORKFLOW_POLICY
...
def addProjectPlacefulWorkflowPolicy(portal, out):
"""Add the placeful workflow policy used by project spaces.
"""
placeful_workflow = getToolByName(portal, 'portal_placeful_workflow')
if PLACEFUL_WORKFLOW_POLICY not in placeful_workflow.objectIds():
placeful_workflow.manage_addWorkflowPolicy(PLACEFUL_WORKFLOW_POLICY,
duplicate_id='portal_workflow')
policy = placeful_workflow.getWorkflowPolicyById(PLACEFUL_WORKFLOW_POLICY)
policy.setTitle('[borg] Project content workflows')
policy.setDefaultChain(('borg_project_default_workflow',))
policy.setChainForPortalTypes(('Folder', 'Large Plone Folder',),
('borg_project_default_workflow',))Again, you could add a different policy if you needed different settings.另外,如果你需要不同的设置,你可以加不同的策略。
Finally, we apply the policy when a project is created. We will see how this is set up when events are covered in the next section, but the relevant code is in events/project.py:
最后,当一个Project创建时,我们应用策略。在events/project.py相关代码:
from zope.interface import implements
from zope.component import getUtility
from Products.CMFCore.utils import getToolByName
from Products.borg.config import PLACEFUL_WORKFLOW_POLICY
from Products.borg.interfaces import ILocalWorkflowSelection
class DefaultLocalWorkflowSelection(object):
"""Select the default local workflow policy.
Local adapters or overrides may supercede this.
"""
implements(ILocalWorkflowSelection)
workflowPolicy = PLACEFUL_WORKFLOW_POLICY
def addLocalProjectWorkflow(ob, event):
"""Apply the local workflow for project spaces when a project is added.
"""
# Add the TeamMember role if necessary
if 'TeamMember' not in ob.validRoles():
# Note: API sucks :-(
ob.manage_defined_roles(submit='Add Role',
REQUEST={'role': 'TeamMember'})
# Find out which workflow to use - this is looked up as a utility so
# that other components can override it.
workflowSelection = getUtility(ILocalWorkflowSelection, context=ob)
# Set the placeful (local) workflow
placeful_workflow = getToolByName(ob, 'portal_placeful_workflow')
ob.manage_addProduct['CMFPlacefulWorkflow'].manage_addWorkflowPolicyConfig()
config = placeful_workflow.getWorkflowPolicyConfig(ob)
config.setPolicyBelow(policy=workflowSelection.workflowPolicy)Here, the local role is added to the newly created project instance (it is not made global so as not to pollute the global roles list), and the policy is associated with the contents of the (folderish) project object.这里,本地角色被添加到新建的项目实例中。并且这个策略也配套到该Project对象的内容。
Note that we do not hard-code the name of the workflow policy! Instead, we ask a utility called ILocalWorkflowSelection. This could be overridden using a local utility, but the global one references the policy created above, as defined in DefaultLocalWorkflowSelection. This utility is registered in events/configure.zcml as follows:
注意我们没有硬编码工作流策略的名称。而是请求一个名为ILocalWorkflowSelection工具应用。这个能被一个本地工具应用覆盖。这个工具应用在events/configure.zcml 中被注册:
Sending and handling events发送和操纵事件
Events is undoubtedly one of the most useful things that Zope 3 brings to the Zope 2 world. Here's how b-org uses them.
事件无疑是 ZOPE 3世界最有用的东西。这儿介绍b-org怎样用事件
In the previous section, you saw how an event handler was used to apply a placeful workflow policy to newly created projects. This pattern is quite powerful - instead of needing to subclass Project just to add something to at_post_create_script() or initializeArchetype(), say, you simply register an appropriate event handler. This pattern can of course apply to other situations, such as when objects are modified, deleted, added to a container, or on any other type of event that may occur in your system. Events are synchronous, so when code emits an event, it will block until all event handlers are finished.
在先前的章节中,我们看到了一个事件句柄怎样用于应用一个嵌入工作流策略到一个新建的Project。这种方式是相当强大的,代替子类化Project的方法仅仅需要加某些事务到at_post_create_script()或initializeArchetype(),也就是你简单地注册一个恰当的事件句柄。这种方式当然也能应用到其他情况,比如当一个对象修改、删除、添加到一个容器或者能发生在系统中的任何其他类型。事件是同步的,因此当代码发出一个事件时,它将锁定直到所有的事件句柄完成。
Recall the event handler for adding projects. It can be found in events/project.py and has the following signature:
你能在events/project.py中发现为添加Projecter 而添加事件句柄的代码:
def addLocalProjectWorkflow(ob, event):
...
The first argument is the object the event was fired on, the second is an instance of the event itself. In fact, this two-part event dispatcher is a special case of events described with IObjectEvent and its sub-interfaces. Internally, Zope 3 catches all IObjectEvents and re-dispatches the event based on the object that is passed along the event instance. The registration for the event handler in events/configure.zcml looks like this:首个参数是发出事件的对象,第二个参数是事件本身的一个实例。事实上,这两部分事件分发是用IobjectEvent 和 它的子接口描述的事件的一种特殊情况。在zope 3内部,捕获所有IobjectEvents并基于被传输到事件实例的对象重新分发这些事件。事件句柄的注册在events/configure.zcml中:
<subscriber for="..interfaces.IProjectContent
zope.app.container.interfaces.IObjectAddedEvent"
handler=".project.addLocalProjectWorkflow" />
Note that there are two interfaces the subscriber is registered for - the object type and the event type. These must be separated by whitespace, though a newline like above is customary. This is the same syntax that is used to explicitly define multi-adapters (if you are not using the adapts() syntax in an adapter class) - in fact, the events machinery uses the adapter registry internally to map subscribers to events when they are fired.A more general-case event can be found in events/employee.py, which takes care of assigning ownership of an Employee object to the user that is tied to that employee. The code is borrowed and adapted from PloneTool, but notice the signature which only includes the event:
注意有两个接口被注册——对象类型和事件类型。这些必须用空格分开。这和显示定义多适配器是同样的语法(如果你在一个适配器class中不用adapts()语法)。事实上,事件机制用这个适配器内部的注册映射预定者到事件。一个更通常的事件能被发现在events/employee.py中,确保指定一个员工对象的成员到依赖该员工的用户。
def modifyEmployeeOwnership(event):
"""Let employees own their own objects.
Stolen from Plone and CMF core, but made less picky about where users are
found.
"""
The registration in events/onfigure.zcml is similar to the one above, but only uses one for interface:
注册过程类似于上面,但只用一个interface:
<subscriber for="..interfaces.IEmployeeModifiedEvent"
handler=".employee.modifyEmployeeOwnership" />
Sending custom events发送定制的事件
You will notice that the IEmployeeModifiedEvent is a custom event. In Plone 3.0 (or rather, Archetypes 1.5) this won't be necessary, because Archetypes will take care of sending an event derived from IObjectModifiedEvent, which in turn derives from IObjectEvent and thus is subject to the same registration as the IObjectAddedEvent that includes the object type and the event type. For now, though, we need to send the event ourselves.
IEmployeeModifiedEvent是一个定制事件。在Plone3(或 archetype 1.5)这是不必要的,因为Archetypes将负责发送一个从IObjectModifiedEvent生成的事件。现在我们需要自己发送这个事件。
The event is described by an interface in interfaces/employee.py:
该事件被描述为一个接口,如下:
from zope.interface import Interface, Attribute
...
class IEmployeeModifiedEvent(Interface):
"""An event fired when an employee object is saved.
"""
context = Attribute("The content object that was saved.")
The implementation is trivial, and can be found in content/employee.py:
实现部分简单,如下:
from zope.interface import implements
...
from Products.borg.interfaces import IEmployeeModifiedEvent
...
class EmployeeModifiedEvent(object):
"""Event to notify that employees have been saved.
"""
implements(IEmployeeModifiedEvent)
def __init__(self, context):
self.context = context
It is of course the event class that we instantiate and send, whilst we register the event handler for the event interface. This means that we could provide alternative implementations for the same event interface, if need be. It also means that event handlers subscribed for a parent interface will be invoked for events that provide a sub-interface.我们实例化并发送事件类的同时,我们为事件接口注册事件句柄。这意味着我们可以为同样的事件接口提供不同的实现。也意味着为一个父接口预定的事件句柄将被提供子接口的事件调用。
Sending the event is very simple. In the definition of Employee in content/employee.py, we have:
发送事件相当简单:
from zope.event import notify
...
class Employee(ExtensibleSchemaSupport, BaseContent):
...
security.declarePrivate(permissions.View, 'at_post_create_script')
def at_post_create_script(self):
"""Notify that the employee has been saved.
"""
notify(EmployeeModifiedEvent(self))
security.declarePrivate(permissions.View, 'at_post_edit_script')
def at_post_edit_script(self):
"""Notify that the employee has been saved.
"""
notify(EmployeeModifiedEvent(self))
We construct an event instance and parameterise it with the right object (i.e. self) before sending it with notify(), all on one line.
我们建造一个事件实例,并用正确的对象作为参数(如self).
AnnotationsAnnotations are an elegant solution to the "where do I store this?" problem, and are used in many Zope 3 applications.
Annotations 是一个一流的解决方案为“保存在哪里”的问题,被用在大多数zope 3应用当中。
It is often useful to be able to attach information to an object even if you don't have control over that object's type and schema. For example, a tagging solution may attach a list of tags to an object, or a notification tool may want to add a list of subscribers on a per-object basis. This is known in Zope 3 as "annotations".
能追加信息到一个对象即便没有控制该对象的类型和schema 是非常有用的。例如,一个标签 解决方案能追加一系列的标签到一个对象,或者一个通知工具能加一系列的预定者到一个对象。这个在zope 3中被称为"annotations".
Annotations work like this:
Annotations工作原理:
- A marker interface, normally IAttributeAnnotatable is applied to the class or object that is to be annotated. This particular marker means that annotations are stored in a persistent dict called __annotations__ that is added to the object, though this should be considered an implementation detail. 一个marker interface IAttributeAnnotatable被应用到被annotated的class 或对象。这个特别的标记意味着annotations被保存在一个持久的字典中,即一个叫做__annotations__的东东被加到这个对象。
- An adapter exists from IAttributeAnnotable to IAnnotations. If you need a different annotation regime (e.g. one that stores the values keyed by object id in some local utility) you could provide a different adapter to IAnnotations.
- The code that wishes to annotate an object will adapt it to IAnnotations. The annotations adapter acts like a dict. Conventionally, each package that uses annotations will store all its (arbitrary) information under a particular key in that dict. The key name is normally the same as the name of the package. This is mainly to avoid conflicts between different packages annotating a particular object.
In b-org, we don't have quite the same need for annotating objects from other parts of Plone, but we use annotations to store users' passwords. This ensures that they cannot be accessed through-the-web (since Zope 2 won't publish the __annotations__ dict, as it begins with an underscore) and keeps passwords out of the way. Strictly speaking, this is probably overkill since the password is also hashed using the SHA1 one-way hasing algorithm, but that never stopped anyone before.
在b-org中,我们用annotations 保存用户密码。这能确保信息不能通过TTW访问,保护密码。严格地讲,这也许过度保密。因为这密码也用SHA1单向hasing算法加密。
First, look at the definition of the Employee class in content/employee.py:
先看下 Employee class的定义:
from zope.app.annotation.interfaces import IAttributeAnnotatable, IAnnotations
...
class Employee(ExtensibleSchemaSupport, BaseContent):
...
implements(IEmployeeContent,
IUserAuthProvider,
IPropertiesProvider,
IGroupsProvider,
IGroupAwareRolesProvider,
IAttributeAnnotatable)Here, we explicitly say that Employee is attribute annotatable. Of course, this requires control over the class. If you are trying to annotate another type that isn't already marked as annotatable, you may be able to add the marker interface using classProvides() or directlyProvides() from zope.interface, or use the ZCML <implements /> directive. You need to be a bit careful, though, since the thing you are annotating should probably be persistent. You should also be polite - you're stuffing your own information onto someone else's object. Try not to break it.
这里我们显示申明Employee是annotatable。当然这要求控制这个class。如果你试图annotate另外一个没有标记为annotatable的其他类型,你能加 这个marker interface通过用classProvides() or directlyProvides() ,或者用ZCML <implements />语句。你要小心,因为你正在annotating的东西也许是静态的、持久的。你也应该文雅些,你正在填充自己的信息到其他对象,不要损坏了这对象。
Further down in content/employee.py, you will see the annotation being set:
更进一步,在content/employee.py ,设置如下:
security.declareProtected(permissions.SetPassword, 'setPassword')
def setPassword(self, value):
if value:
annotations = IAnnotations(self)
annotations[PASSWORD_KEY] = sha(value).digest()
PASSWORD_KEY comes from config.py, and is simply a string. The digest is verified in membership/employee.py, in the IUserAuthentication adapter:PASSWORD_KEY来自config.py,它是一个简单的字符串。这个报文被验证在IuserAuthentication 适配器中:
class UserAuthentication(object):
"""Provide authentication against employees.
"""
implements(IUserAuthentication)
adapts(IEmployeeContent)
def __init__(self, context):
self.context = context
def getUserName(self):
return self.context.getId()
def verifyCredentials(self, credentials):
login = credentials.get('login', None)
password = credentials.get('password', None)
if login is None or password is None:
return False
digest = sha(password).digest()
annotations = IAnnotations(self.context)
passwordDigest = annotations.get(PASSWORD_KEY, None)
return (login == self.getUserName() and digest == passwordDigest)
That's all there is to it. We get an IAnnotations adapter, and then look up the PASSWORD_KEY to find the digest. The annotations adapter has the same contract as a Python dict, so we can use functions like get() and setdefault().我们获得一个IAnnotations适配器,然后查找PASSWORD_KEY 以找到报文。这个Annotation适配器就象一个Python dict,因此能用get()和setdefault()。
Zope 3 ViewsZope 3视图
One of the nicest things that Zope 3 brough us is a way to manage view logic.
zope 3带给我们最美妙的事情是管理试图逻辑的方法。
In Zope 2, a view (be that a view of a content object, or a more standalone template) typically consists of a Zope Page Template that pulls in data from its context. The problem is that non-trivial templates usually require some kind of "view logic" or "display logic". People tend to put these in a few places:
在zope 2中,通常一个视图由页面模版组成,并且页面模版能将从它的上下文中拉入数据。问题时没有一个简单的办法来实现视图逻辑或显示逻辑。人们通常将这些逻辑放置在下面这些地方:
- Complex python: expressions in the ZPT. This is bad because it makes your templates hard to understand, and because there is a limit to what you can do with one-line Python expressions. 放在ZPT中复杂的python表达式。这种方法将使模版难于理解,并且在ZPT中只能用 one-line的python表达式。
- External Python Scripts in a skin layer that get acquired in the page template, e.g. here/calculateDate. This is bad because it is cumbersome to create a new file for something which may be quite trivial, because all such scripts are part of a global namespace (and thus there may be conflicts between two different scripts with the same name), and also because Python scripts in the skin layers (and python: expressions) are slower than filesystem Python code and more restricted. 在ZPT中获取在皮肤层的外部python scripts,如here/calculateDate。这种方式缺点在于要做某些简单的事情也必须创建新的文件,很麻烦。而且所有这些scripts是全局命名空间的一部分(很容易由于两个不同的脚本使用相同的名称而导致冲突),另外,在皮肤层的python脚本比文件系统python代码要慢,而且受到更多的限制。
- A custom tool that provides some necessary functionality. This is bad because a tool is a singleton, so you will probably need to explicitly pass around a context. Tools are also part of that same global namespace (by way of acquisition from the portal root), and are a hassle to create and install. 由一个定制的工具提供所需的功能。这种方式的缺点是由于工具本身是独立的,因此在使用时,必须显示地传输上下文。另外,工具应用也同样是全局命名空间的一部分。
- Methods on the context content object (where applicable). This is bad because it mixes presentation logic and the model (the schema) and storage logic. This often leads to an explosion of methods on each content type that are highly specific to a particular template. This pattern also requiers that you have the ability to add new methods to the content type class, even if you are just adding a new view template for it.
As usual, these problems indicate a lack of separation of concerns. Zope 3's answer is a view - a class (typically) which may be associated with a template.
通常上述这些问题的焦点是缺乏松散耦合。zope 3的解决方案是视图,该视图实际上是一个class,但能配合一个相应的显示模版。
Views are multi-adapters视图是多适配器
You will often hear that views are named multi-adapters of a context and a request. In fact, the concept of a multi-adapter originated in the need for views. For most practical purposes, you can forget about this - it is an implementation detail. However, you may sometimes need to look up views yourself, which can be done using:
视图命名为一个上下文和一个request的多适配器。事实上,原始的多适配器概义是适应视图的需要而提出的。在大多数应用场合,你无须关心这个。然而,可能不时要查看视图,可以通过以下方式完成:
from zope.component import getMultiAdapter
myView = getMultiAdapter((context, request), name='my_view')
More importantly, you need to know that to access the context the view is operating on inside that view, you can use self.context, and to access the request (including form variables submitted as part of that request, if applicable), using self.request.
重要的是,要明白访问视图的上下文是在该视图里面进行的,因此,能用self.context,也能用self.request来访问这个request(包括request中提交的各种变量)。
Explicitly acquiring views显示获取视图
One of the easiest ways of using views with existing code is to make page templates in a skin layer as you normally would, and then acquire a view object that is used for rendering logic. One of the main reasons for using this approach is that it allows page templates to be customised using the normal skin layer mechanism. This is approach is used extensively in Plone 2.5. Here's an example from the "recent" portlet, starting with portlet_recent.pt
使用视图最简单的方法是在皮肤层创建ZPT,然后获取视图对象用于呈现。用这种方式的主要原因是允许ZPT可以用皮肤层管理机制进行定制。这个方式已在Plone2.5中实现,下面是一个针对"recent" portlet,利用该技术做的定制:
…
<tal:recentlist tal:define="view context/@@recent_view;
results view/results;">
...
<tal:items tal:repeat="obj results">
...
</tal:items>
...
</tal:recentlist>
The important line here is view/@@recent_view. This will look up a view named recent_view relative to the current context (context in page templates is a now-preferred alias for the here variable that was used before - here still works in Zope 2 templates, but is gone in Zope 3).
代码中最重要的一行是view/@@recent_view。这将查找一个相关到当前上下文的一个命名为recent_view的视图。(在ZPT中context是here变量的一个别名,here 在zope 2中有效,但在zope 3中不再有效)
This view is defined by a class and a ZCML directive. The ZCML directive looks like this:
这个视图有一个class定义,并由一个zcml语句来说明,如下:
<browser:view
for="*"
name="recent_view"
class=".portlets.recent.RecentPortlet"
permission="zope.Public"
allowed_attributes="results"
/>
Actually, this is not exactly what's in the file in Plone, since Plone is working around a few Zope 2.8 issues, but basically, this says that the view is available on all types of contexts (for="*" - this could specify a dotted name to an interface if needed, more on that below), has the name recent_view, is public (because of the magic permission zope.Public) and that when acquired, the attribute (method) results is allowed - more attributes could be specified separated by whitespace. The class that is referenced contains the view implementation. Here it is, again slightly modernised:(较简单,不翻译了……)
from Products.Five.browser import BrowserView
from Products.CMFCore.utils import getToolByName
from Acquisition import aq_inner
class RecentPortlet(BrowserView):
"""The recent portlet
"""
def results(self):
"""Get the search results
"""
context = aq_inner(self.context)
putils = getToolByName(context, 'plone_utils')
portal_catalog = getToolByName(context, 'portal_catalog')
typesToShow = putils.getUserFriendlyTypes()
return self.request.get(
'items',
portal_catalog.searchResults(sort_on='modified',
portal_type=typesToShow,
sort_order='reverse',
sort_limit=5)[:5])The use of aq_inner() on self.context is not strictly necessary always, but is a useful rule of thumb to make acquisition do what you expect it to do (this is because the BrowserView base class extends Acquisition.Explicit, which causes self.context to gain an acquistion wrapper that can mess with its acqusition chain).
在self.context使用aq_inner()不总是需要的。但作为一种经验,能使得获取机制做你希望做的事情。
Views with templatesZope 3 does not use views in this way. Instead, you would bind the template to the browser view explicitly. The main drawback of this technique is that the template is not present in the portal_skins tool, and so cannot be customised through-the-web. This may be possible in future versions of Zope and CMF, but for now the full-blown view technique is best used it is not necessary to customise views through-the-web. Of course, you can still override view registrations using ZCML on more specific interfaces or an overrides.zcml.
显示地绑定模版到browser视图。这种技术的缺点是模版不能被Portlal_skins工具看到,因此无法通过TTW方式定制该模版。如果无需通过TTW方式定制视图,最好采用该技术。当然,你也能用ZCML在更多的接口上覆盖视图注册,或者用一个overrides.zcml。
Here is a view for departments in the charity example product, under charity/browser/configure.zcml. Notice how this entire XML file is in the browser namespace, and thus it is unnecessary to prefix each directive with browser:
在charity 样例产品中有一个为departments做的视图,在charity/browser/configure.zcml下。注意整个XML文件是在browser命名空间,因此不必要在每个语句前加browser,如下:
<configure xmlns="http://namespaces.zope.org/browser"
i18n_domain="charity">
<page
name="charity_department_view"
for="Products.borg.interfaces.IDepartmentContent"
class=".department.DepartmentView"
template="department.pt"
permission="zope2.View"
/>
...
</configure>
Here, we explicitly state that this view is only available for IDepartmentContent objects. This means that if you try to invoke @@charity_department_view on anything that does not provide this interface, you will get a lookup error. The view is protected by the Zope 2 View permission. Also note that there is no allowed_attributes (or allowed_interface) attribute here. This is because the view is not intended to be used by other templates (if they tried, they would get an Unauthorized error when trying to access any attribute of the view) - all the logic is in the department.pt template.注意,这儿我们显示指定了视图仅仅为Products.borg.interfaces.IDepartmentContent接口的对象,这意味着如果你在没有提供该接口的其他对象上调用@@charity_department_view,将返回一个查询错误。这个视图也由zope 2的 view权限保护。也请注意,这儿没有定义allowed_attributes 属性,这是因为这个视图不希望被其他模版使用(如果其他模版使用,将得到一个Unauthorized 错误)。
The department.pt template is found in charity/browser, the same directory as the configure.zcml file above. You can use relative paths like ./templates/… if necessary to point to the template file on the filesystem. Here is the class:
这个department.pt 在charity/browser 中,和上面的configure.zcml在同一目录中。下面是DepartmentView视图class定义:
from Products.Five.browser import BrowserView
from Products.borg.interfaces import IDepartment
class DepartmentView(BrowserView):
"""A view of a charity department"""
def __init__(self, context, request):
self.context = context
self.request = request
def name(self):
return self.context.Title()
def managers(self):
return self.context.getManagers()
def details(self):
return self.context.Description()
And here is the template that uses these methods:下面是调用视图方法的模版:
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
lang="en"
metal:use-macro="here/main_template/macros/master"
i18n:domain="charity">
<body>
<metal:main fill-slot="main">
<div metal:use-macro="here/document_actions/macros/document_actions">
Document actions (print, sendto etc)
</div>
<h1 class="documentFirstHeading" tal:content="view/name" />
<table class="listing vertical" style="float:right" tal:condition="view/managers">
<tr>
<th>Manager(s)</th>
<td>
<div tal:repeat="obj view/managers">
<a href="#" tal:attributes="href obj/absolute_url" tal:content="obj/Title" />
</div>
</td>
</tr>
</table>
<div tal:content="structure view/details" />
<metal:listing use-macro="here/folder_listing/macros/listing" />
<div class="visualClear"><!----></div>
</metal:main>
</body>
</html>
Now, you can go to a hypothetical URL /mydept/@@charity_department_view to see this view rendered. In fact, this is set as the view and (Default) aliases for the Department content type when charity is installed, so the user will never see this URL.现在,你可以通过 /mydept/@@charity_department_view URL查看视图呈现。
Views without templatesIt is also possible to make views without templates. This is useful if you need a URL to submit that does some processing. That processing would normally be done in the __call__() method, as in the hypothetical example below:
也可能使用不用模版的视图。如果你需要一个url提交做某些处理,这将很有用。这个处理通过调用__call__()来做,下面是一个假定的例子:
<browser:view
name="modify_customer"
for=".interfaces.ICustomer"
class=".customer.ModifyCustomerView"
permission="cmf.ModifyPortalContent"
/>Now, we could write a form that has action="@@modify_customer", which would result in this being called:现在,你能写一个带有action="@@modify_customer" 的表单:
class ModifyCustomerView(BrowserView):
"""Modify a customer from a form
"""
def __call__(self):
name = self.request.form.get('name', None)
dog = self.request.form.get('dog', None)
self.context.name = name
self.context.dog = dog
self.request.response.redirect('@@customer_view')
This is obviously a simplified example, but the important thing to realise is that the view will tend to use self.context and self.request to interact with the rest of the portal.这是一个简单的例子。重要的是认识到视图将能用self.context和self.request来和portal的其他信息交互