元数据和标注(annotations)
元数据和标注(annotations)
http://www.315ok.org/blogfolder/261
http://www.315ok.org/logo.png
元数据和标注(annotations)
元数据和标注(annotations)
zope3 部件体系架构第14章——metadata主要数据的存储是web应用尤其是内容管理系统的重要概义。然而,对象相关联的次要数据(metadata)也扮演了重要角色。这些元数据 主要有:
为了不要修改recipe内容部件而保存我们的元数据,ZOPE采用一个叫annotation的系统。
As this is again functionality that the original component does not provide, you can bet that adapters are involved.
In annotations, we must distinguish the following two concepts:
IAnnotations Despite the confusing plural form, an IAnnotations adapter is a singular object with a dictionary-like interface that can store metadata. This adapter persistently associates metadata with content objects such as our recipes.
Often a simple IAnnotations adapter is used that stores the metadata container on a special attribute of the object itself (see below), but this should not be assumed. Simply adapt the object to Annotations, and trust that you have a persistent store of metadata associated with the object.
IAnnotatable Zope needs to determine if an object has an IAnnotations adapter very frequently, which means it needs to be done very quickly.
Rather than trying to adapt each object to IAnnotations and reacting to success or failure, Zope uses an optimization. It requests that objects “promise” to be adaptable to IAnnotations.They do this by declaring to be annotatable, by providing IAnnotatable from the zope.annotation package. This interface does not promise any additional functionality expressed in methods or attributes. It is an
implied contract, a marker interface. The implied contract is that it is possible to get an annotations adapter for any annotatable object.
Now the only question remaining is where is metadata stored? Obviously, the annotations adapter has to take care of that. It would be no other component’s responsibility. Because there are different ways of storing metadata,some of them appropriate for persistent objects, some of them not, there can be no general annotations adapter.
However, Zope would not be Zope if it did not already provide a builtin solution. Most objects in Zope are persisted in the ZODB. Its persistency machinery automatically stores objects and their attributes in the database.The attribute annotations adapter makes use of this by storing an object’s annotations in an annotations attribute on the object.
Objects can decide whether they want to allow that by providing IAttributeAnnotatable, a subinterface of IAnnotatable. The adapter is registered for IAttributeAnnotatable.
Trying out annotations
For a demonstration, consider the following interactive interpreter session.After initializing Zope, we create a bare recipe object and try to adapt it. It obviously fails because it is not marked annotatable.
In any case, it should not matter to the components storing metadata.All they care about is getting an annotations adapter to store the metadata.
Where it is stored does not matter to them.
Annotations per class
Obviously we would like to allow annotations on all recipe objects, so having the class implement IAttributeAnnotatable sounds like a reasonable thing to do. However, it is not common to add such a statement to the Python code directly. The Recipe class as a content object only cares to implement those interfaces that require implementation, such as IRecipe.IAttributeAnnotatable, however, is a marker interface promising an abstract contract. Whether or not this promise should be given is more of a configuration issue than an implementation issue. Therefore, the implementation of IAttributeAnnotatable is generally expressed in a class’s configuration, as shown by Example 14.1.1.
Example 14.1.1 Making recipes annotatable through attribute annotations (configure.zcml)
Summary
• As it is not part of an object’s primary data, metadata should be managed and possibly stored separately from the content objects it is associated with.
• Zope 3 uses annotation adapters as metadata storage; an annotation adapter manages all metadata associated with a particular object.
• For persistent objects, attribute annotations is the preferred way to handle annotations. On objects marked as attribute-annotatable the annotations adapter stores metadata in a hidden attribute on the object, thus letting it to be persisted with the object.
• Different software packages should use different annotation keys to distinguish their metadata and avoid conflicts and ambiguities.
Rocky says…
Java developers should not confuse Zope 3’s implementation and use of metadata with java.jmi. Zope’s metadata and annotations are more about enhancing user generated content, not about defining extra attributes of Python code.
14.2 The Dublin CoreA de facto industry standard in the field of metadata is the Dublin Core [5],[16]. It defines a set of information that is generally found useful in documentoriented systems, high-level Internet protocols such as WebDAV, and data exchange formats such as RSS. Here are the metadata categories for resources as defined by the Dublin Core:
Title provides a human-readable and meaningful name for the resource. Most of the time, even in Zope, documents are referred to by their filename or name within their container which is not always meaningful to a person.
Creator states a person or organization responsible for the content of the resource.
Subject contains a list of keywords thematically describing the contents of the resource.
Description is usually a short abstract of what the resource depicts.
Publisher states a person or organization responsible for making the resource available.
Contributor lists possible contributors to the contents of the resource.
Date can be one or more dates representing an important event in the resource’s life-cycle. Most of the time, a creation date and modification dates are recorded.
Type gives information about what kind of information the resource contains.This may include general categories or even genres.
Format informs readers and editors about the data format that the information is stored and presented in. This would most typically be a MIME type identifier.
Identifier gives a unique and unambiguous reference to the resource. This can be anything within a system of unique identifiers, such as an ISBN number, a URI, or even an IP address or telephone number.
Source is a list of resources from which the current resource was derived from.
Language states the language the resource’s text is written and presented in.
Relation can contain a list of identifiers with which the resource stands in relation to. The type of relation is arbitrary and up to the application to fill with a meaning.
Coverage defines the scope of the resource.
Rights gives information about intellectual rights, copyrights, etc. regarding the resource.
All Dublin Core elements support multiple values. This obviously only makes sense for a few of them, such as Subject, Contributor, and Rights. Other properties, such as Date and Relation, only make sense when treated with qualifiers that tell the application which date and what kind of a relation are meant. Zope supports the Dublin Core standard to its full extent, but the general interfaces are designed for every-day use-cases, thus simplifying the unnecessary complexity.
Zope’s support for Dublin Core resides in the zope.dublincore package.At the heart of this package is the ZopeDublinCore adapter, an adapter for annotatable objects that allows you to work with Dublin Core properties without having to go through annotations, even though the properties are stored in annotations, of course. Key interfaces are IZopeDublinCore for property read access and IWriteZopeDublinCore for write access, respectively.
Trying it out in the interpreter shell
Again we want to demonstrate how the Dublin Core adapter works with an example from the interactive interpreter shell:
Metadata automation
Zope does a lot more for us when our objects are annotatable than just providing the Dublin Core adapter. As you may have noticed, recipes now have a new management tab, Metadata (see Figure 14.1). It allows content editors to edit the most basic metadata, namely title and description.
The Metadata ZMI view also reveals automatically computed metadata:
• creation date,
• date of last modification,
• and the user name of the creator.
[img]http://315ok.org/blogs/yuanshujuheannotation/images/c14_1/image_preview[/img]
Fig. 14.1. Zope provides a form for all annotatable objects in which basic Dublin Core metadata can be edited.
If you add a new recipe to a folder, you will see that these values has automatically been computed and annotated to the object. We will learn in Chapter 16 how this works, for now it is just important to know that Zope does this.
Dublin Core is not only used within a document or resource management system, it also comes into action when data is interchanged. As mentioned above, the WebDAV protocol allows a client program to query object metadata using the PROPFIND method. Zope’s central view implementation for PROPFIND builds upon annotations and annotation-related adapters such as Dublin Core, thus allowing client programs to query Dublin Core and other metadata directly though WebDAV.
Permissions
Viewing and changing Dublin Core properties is protected by special permissions:
zope.app.dublincore.view is required from all principals when accessing Dublin Core metadata. Like zope.View, it is granted to anonymous by default.
zope.app.dublincore.change is required for changing Dublin Core properties.
An example in Page Templates
Since Zope’s Dublin Core solution is integrated, most of your work with the Dublin Core is in presentation components, such as Page Templates. Here is a simple example to demonstrate how to access Dublin Core metadata in ZPT.
After this we will look at how to use the Dublin Core adapter in Python. In order not to over-complicate things, we will simply write a small viewlet that shows when a particular object was created and when it was last modified.
Example 14.2.1 shows its source code. Its registration in ZCML shall be omitted here as it looks like any other viewlet registration we have done so far. Just note that we will register this viewlet for objects providing IAnnotatable, as the Dublin Core metadata will likely be available for such objects.
An example in Python
As an example for working with the Dublin Core adapter from Python, let us extend the XML-RPC view class from the previous chapter to provide another method for Dublin Core metadata retrieval. Example 14.2.2 shows
the modified XML-RPC view class whereas Example 14.2.3 displays the little change necessary in order to configure the additional view method.
Since we already wrote an XML-RPC client in Chapter 13, consider it an optional exercise to write one for the new view method. The functional doctest in xmlrpc/README.txt was updated, so the functionality of the view is ensured either way.
Summary
• The Dublin Core specification describes a set of metadata commonly associated with resources in document management systems.
Example 14.2.1 Accessing Dublin Core metadata from Page Templates (skin/metadata.pt)
latter two are accessed here.
4, 11, and 17. Note that we have a textbook example of localization here. The created and modified dates are localization-sensitive values. As discussed in Chapter 9, we acquire a date formatter from the request’s locale and format the dates so they will be displayed according to local conventions.
• Zope’s Dublin Core support lies mainly in an adapter provided by the zope.dublincore package.Since it is metadata, the adapter stores Dublin Core information inannotations.
• Zope also provides automated metadata updating facilities that are triggered when objects are added and/or modified.
• In Page Templates, the zope TALES namespace adapter provides access to basic Dublin Core fields.
Example 14.2.2 Enhanced XML-RPC view class providing a view method for metadata retrieval (xmlrpc/recipe.py)
23–38. As specified by the IZopeDublinCore interface, the date values are stored as datetime objects, a data type that xmlrpclib cannot serialize1. To ensure proper data exchange between Zope and the XML-RPC client, we have to convert datetime objects into xmlrpclib.DateTime objects. Values that are None are converted to empty strings.
[img]http://315ok.org/blogs/yuanshujuheannotation/images/c14_2/image_preview[/img]
Fig. 14.2. Showing created and last modified dates in a sidebar viewlet.
Flashback
In Zope 2, the Content Management Framework (CMF) first introduced Dublin Core support, mainly through a set of interfaces. CMF content classes can choose to implement these to signal to the rest of application that they supported Dublin Core methods. This is by far the most prominent usage of interfaces in a Zope application prior to Zope 3.
The CMF also provides an implementation of these interfaces, DefaultDublinCoreImpl, that content classes can inherit from to gain Dublin Core functionality. The obvious difference to Zope 3 here
is, again, that extra functionality like handling metadata, especially a certain kind like Dublin Core, is constrained to external components like Example 14.2.3 Adding another XML-RPC view method to the configuration (xmlrpc/configure.zcml)
Finally, Zope 3 absolutely surpasses the CMF’s metadata model by offering a totally generic solution: annotations. They not only allow us to associate information useful to humans but also application-relevant data, such as workflow states, revision control status, etc.
14.3 Custom metadataIt is often necessary to store custom metadata. Experience shows that advanced and complex applications always require at least one or two fields more than the Dublin Core standard provides. This is far from being a tragedy, since we can simply write adapters to store the custom metadata in annotations.
As a simple example for such a component, consider an online rating system through which visitors of the World Cookery website can rate recipes according to how good they found the dish or the recipe description. Again,the rating information is not part of the actual recipe data schema. Since it is clearly metadata, it belongs in an annotation.Interfaces
First, we will have to define two interfaces (see Example 14.3.1). IRatable is a marker interface identifying objects that can be rated. It extends the IAnnotatable marker interface to express the dependency on annotations.
The second interface, IRating, describes the actual rating API, gathering rating information and performing ratings. An adapter for ratable objects will provide this interface.
Example 14.3.1 Interfaces for the simple rating system (interfaces.py)
An adapter
In analogy to the IZopeDublinCore adapter, we now provide the IRating adapter for ratable objects. It will store all rating information in the ratable object’s annotations, thus providing a frontend to this particular type of metadata through the IRating interface. Example 14.3.2 shows the source code.
Example 14.3.2 Adapter providing rating functionality based on annotations (rating.py)
15. Since this is a trusted adapter, it will be security-proxied. In that case it is a good idea to fill the parent attribute with the context object so that the security machinery can look up local grants (in case they exist) by walking up the parent hierarchy.
16–17. Like the Dublin Core adapter, we read and write to and from annotations.Therefore we get yet another adapter, the annotations adapter. From it we acquire a mapping object for storing rating data in. The IAnnotations interface requires the annotations adapter to behave like a mapping object itself.
19–20. In case the mapping object cannot be acquired, it means that this adapter is invoked on this particular object instance for the first time. We thus provide a default mapping with a zero average and an empty list. Since the annotations are likely to be persisted, we have to be careful that we comply with the rules of persistency. To be safe, we use PersistentDict and PersistentList instead of their non-persistent flavours.
For configuration, we will not only have to configure the adapter, but we will also have to make the Recipe class ratable so that the adapter works on it. Example 14.3.3 shows how configure.zcml needs to be enhanced.As usual, we can now test the adapter on the command line. Consider a recipe:
21. Like all other components invoked through user-interaction, adapters have to respect security-protected methods and attributes. That is usually not a problem because the object’s attributes that the adapter works with should have security declarations that requires a permission from the user. The problem is that this mechanism does not work with attribute annotations because the annotations attribute is hardly ever configured with security declarations. After all, the classes are not supposed to know about it.
The solution here is to mark this adapter as trusted. This will prevent any security checks when the adapter accesses attributes on the object, it will have free access. In return, the adapter itself will be security proxied and needs security declarations (see below).
24–29. Since the IRatable adapter is a trusted adapter, it itself rather than the adapted object will be security proxied. That means the adapter implementation (the Rating class) now needs security declarations as well, otherwise other components could not use it.
Example 14.3.4 Docfile test for the rating adapter (rating.txt)
Example 14.3.4 shows the listing of a docfile test, serving as a test and documentation for the rating system at the same time. Example 14.3.5 contains the mandatory test suite and initialization routines as shown in Chapter 12.
Example 14.3.5 Test suite and initialization routines for the rating docfile test (tests/test rating.py)
As you may have noticed from the pattern of Zope’s Dublin Core adapter and our rating adapter, annotations effectively allow you to implement “persistent” adapters. This doesn’t mean that the adapter objects themselves are persisted. Rather, the adapters draw all their values from the object’s persistent annotations. Adapting the same object over and over will always yield the same values. To the user of the adapter it seems as if the adapter itself is persistent.
Browser views
As with the Dublin Core, an adapter by itself is not helpful for the user. A visitor of the World Cookery website wants to rate objects through a web browser. That means we have to provide browser access to the rating system.
Once again we can make this functionality available through a viewlet and have it appear every time we view a ratable object. Such a viewlet,however, would have to be slightly more complicated than the ones we have implemented so far. It would not only render itself, it would also have to take input and do something with this input. Such functionality cannot be handled by a Page Template alone. We will have to implement this viewlet in Python. Example 14.3.6 shows the viewlet class that makes the rating functionality available as part of the skin. Example 14.3.7 shows the Page Template that goes with the viewlet, Example 14.3.8 demonstrates how a viewlet that implemented in Python is registered.
Example 14.3.6 Viewlet for rating functionality implemented in Python (skin/rating.py)
7–12. Viewlets need to have two methods, update and render. In the former,viewlets can prepare (e.g. compute) some data that they will later need for rendering themselves. The update methods of all viewlets in a viewlet manager will be called first. Here we use this method to inspect the request for a rating that may have been submitted from the form that appears when the viewlet is rendered. A viewlet is rendered by invoking its render method. Here we again implement this method by pointing to a Page Template, whose source code is shown in Example 14.3.7.
Example 14.3.7 Page Template rendering the rating viewlet (skin/rating.pt)
8 and 10. Here we have an another example of localization. The average rating is obviously a floating point number, which is localization-sensitive too. Again, we acquire a number formatter from the request’s locale to format the number according to a pattern. Here, it makes sense to round to the first digit after the dot.
8. Since the viewlet itself will take care of processing the rating input in its update method, we set the form action to whatever the current URL is. That way we can ensure that the same page will be shown again and that the rating viewlet will be invoked.
Example 14.3.8 Configuration for rating viewlet (skin/configure.zcml)
Fig. 14.3. Viewlet allowing the user to rate a recipe.
Summary
• Sometimes, custom metadata outside of the already supported Dublin Core needs to be associated with objects.
• It is recommended to abstract custom metadata—including the functionality to change it—in an interface, such as IRating in the example.
• Instead of directly reading and writing annotated data, an adapter for the abstract metadata interface takes care of storing the annotated data, like the IRating adapter does in the example.
- 关于文档的作者、编辑者等信息
- 对象创建和修改的日期时间等
- 在工作流中的状态
为了不要修改recipe内容部件而保存我们的元数据,ZOPE采用一个叫annotation的系统。
As this is again functionality that the original component does not provide, you can bet that adapters are involved.
In annotations, we must distinguish the following two concepts:
IAnnotations Despite the confusing plural form, an IAnnotations adapter is a singular object with a dictionary-like interface that can store metadata. This adapter persistently associates metadata with content objects such as our recipes.
Often a simple IAnnotations adapter is used that stores the metadata container on a special attribute of the object itself (see below), but this should not be assumed. Simply adapt the object to Annotations, and trust that you have a persistent store of metadata associated with the object.
IAnnotatable Zope needs to determine if an object has an IAnnotations adapter very frequently, which means it needs to be done very quickly.
Rather than trying to adapt each object to IAnnotations and reacting to success or failure, Zope uses an optimization. It requests that objects “promise” to be adaptable to IAnnotations.They do this by declaring to be annotatable, by providing IAnnotatable from the zope.annotation package. This interface does not promise any additional functionality expressed in methods or attributes. It is an
implied contract, a marker interface. The implied contract is that it is possible to get an annotations adapter for any annotatable object.
Now the only question remaining is where is metadata stored? Obviously, the annotations adapter has to take care of that. It would be no other component’s responsibility. Because there are different ways of storing metadata,some of them appropriate for persistent objects, some of them not, there can be no general annotations adapter.
However, Zope would not be Zope if it did not already provide a builtin solution. Most objects in Zope are persisted in the ZODB. Its persistency machinery automatically stores objects and their attributes in the database.The attribute annotations adapter makes use of this by storing an object’s annotations in an annotations attribute on the object.
Objects can decide whether they want to allow that by providing IAttributeAnnotatable, a subinterface of IAnnotatable. The adapter is registered for IAttributeAnnotatable.
Trying out annotations
For a demonstration, consider the following interactive interpreter session.After initializing Zope, we create a bare recipe object and try to adapt it. It obviously fails because it is not marked annotatable.
$ bin/debugzope
>>> class Recipe(object):
... pass
...
>>> meatloaf = Recipe()
>>> from zope.annotation.interfaces import IAnnotations
>>> annotations = IAnnotations(meatloaf)
Traceback (most recent call last):
...
TypeError: (’Could not adapt’, <__main__.Recipe object at 0x35c4f0>,
<InterfaceClass zope.annotation.interfaces.IAnnotations>)However, we can instantly make it annotatable by directly providing IAttributeAnnotatable on it. This marks it as annotatable and allows us to adapt it to IAnnotations:>>> from zope.interface import alsoProvides
>>> from zope.annotation.interfaces import IAttributeAnnotatable
>>> alsoProvides(meatloaf, IAttributeAnnotatable)
>>> annotations = IAnnotations(meatloaf)The annotations adapter behaves like a standard mapping object, such as a dictionary. Since there are different types of metadata, storing data directly in this mapping is strongly discouraged. The convention is to store an additional mapping under a key, usually the name of the software’s Python package,and to store that metadata in that second mapping. World Cookery-specific metadata, for example, would be the cook who first invented the dish:>>> annotations[’worldcookery’] = {}
>>> annotations[’worldcookery’][’cook’] = \
... u"Philipp von Weitershausen"Similarly, Dublin Core metadata is stored using the zope.app.dublincore.ZopeDublinCore key, rating meta-data with the worldcookery.rating key (more on those later). This way different metadata will not conflict. When we provided IAttributeAnnotatable on the recipe object, we not only made it annotatable; we also explicitly allowed annotations to be stored on the object as an attribute. The following lines reveal that this has indeed happened:>>> meatloaf.__annotations__
<BTrees._OOBTree.OOBTree object at 0x28a3a50>
>>> dict(meatloaf.__annotations__)
{’worldcookery’: {’cook’: u’Philipp von Weitershausen’}}For objects persisted in the ZODB, attribute annotations are a care-free and easy solution, because they are persisted with the object automatically. There are circumstances, however, when it is not good to use attribute annotations,for example when content objects are generated from data coming from the file-system or an SQL database. Then, attribute annotation data would not be stored automatically which is why you would have to handle annotations differently in those cases.In any case, it should not matter to the components storing metadata.All they care about is getting an annotations adapter to store the metadata.
Where it is stored does not matter to them.
Annotations per class
Obviously we would like to allow annotations on all recipe objects, so having the class implement IAttributeAnnotatable sounds like a reasonable thing to do. However, it is not common to add such a statement to the Python code directly. The Recipe class as a content object only cares to implement those interfaces that require implementation, such as IRecipe.IAttributeAnnotatable, however, is a marker interface promising an abstract contract. Whether or not this promise should be given is more of a configuration issue than an implementation issue. Therefore, the implementation of IAttributeAnnotatable is generally expressed in a class’s configuration, as shown by Example 14.1.1.
Example 14.1.1 Making recipes annotatable through attribute annotations (configure.zcml)
1 ...
2 <class class=".recipe.Recipe">
3 <implements
4 interface="zope.annotation.interfaces.IAttributeAnnotatable"
5 />
6 <require
7 permission="zope.View"
8 interface=".interfaces.IRecipe"
9 />
10 <require
11 permission="zope.ManageContent"
12 set_schema=".interfaces.IRecipe"
13 />
14 </class>
15 ...3–5. Classes can be made to implement interfaces in addition to the ones they already implement from their source code. This is done with the implements subdirective of the class directive. This directive only makes sense for marker interfaces. Otherwise classes could promise to implement certain interfaces when in fact they do not. IAttributeAnnotatable is the most common interface this directive is used for.Summary
• As it is not part of an object’s primary data, metadata should be managed and possibly stored separately from the content objects it is associated with.
• Zope 3 uses annotation adapters as metadata storage; an annotation adapter manages all metadata associated with a particular object.
• For persistent objects, attribute annotations is the preferred way to handle annotations. On objects marked as attribute-annotatable the annotations adapter stores metadata in a hidden attribute on the object, thus letting it to be persisted with the object.
• Different software packages should use different annotation keys to distinguish their metadata and avoid conflicts and ambiguities.
Rocky says…
Java developers should not confuse Zope 3’s implementation and use of metadata with java.jmi. Zope’s metadata and annotations are more about enhancing user generated content, not about defining extra attributes of Python code.
14.2 The Dublin CoreA de facto industry standard in the field of metadata is the Dublin Core [5],[16]. It defines a set of information that is generally found useful in documentoriented systems, high-level Internet protocols such as WebDAV, and data exchange formats such as RSS. Here are the metadata categories for resources as defined by the Dublin Core:
Title provides a human-readable and meaningful name for the resource. Most of the time, even in Zope, documents are referred to by their filename or name within their container which is not always meaningful to a person.
Creator states a person or organization responsible for the content of the resource.
Subject contains a list of keywords thematically describing the contents of the resource.
Description is usually a short abstract of what the resource depicts.
Publisher states a person or organization responsible for making the resource available.
Contributor lists possible contributors to the contents of the resource.
Date can be one or more dates representing an important event in the resource’s life-cycle. Most of the time, a creation date and modification dates are recorded.
Type gives information about what kind of information the resource contains.This may include general categories or even genres.
Format informs readers and editors about the data format that the information is stored and presented in. This would most typically be a MIME type identifier.
Identifier gives a unique and unambiguous reference to the resource. This can be anything within a system of unique identifiers, such as an ISBN number, a URI, or even an IP address or telephone number.
Source is a list of resources from which the current resource was derived from.
Language states the language the resource’s text is written and presented in.
Relation can contain a list of identifiers with which the resource stands in relation to. The type of relation is arbitrary and up to the application to fill with a meaning.
Coverage defines the scope of the resource.
Rights gives information about intellectual rights, copyrights, etc. regarding the resource.
All Dublin Core elements support multiple values. This obviously only makes sense for a few of them, such as Subject, Contributor, and Rights. Other properties, such as Date and Relation, only make sense when treated with qualifiers that tell the application which date and what kind of a relation are meant. Zope supports the Dublin Core standard to its full extent, but the general interfaces are designed for every-day use-cases, thus simplifying the unnecessary complexity.
Zope’s support for Dublin Core resides in the zope.dublincore package.At the heart of this package is the ZopeDublinCore adapter, an adapter for annotatable objects that allows you to work with Dublin Core properties without having to go through annotations, even though the properties are stored in annotations, of course. Key interfaces are IZopeDublinCore for property read access and IWriteZopeDublinCore for write access, respectively.
Trying it out in the interpreter shell
Again we want to demonstrate how the Dublin Core adapter works with an example from the interactive interpreter shell:
$ bin/debugzope
>>> class Recipe(object):
... pass
...
>>> fried_chicken = Recipe()Since the object is not annotatable, we cannot adapt it to the Dublin Core interface.
>>> from zope.dublincore.interfaces import IWriteZopeDublinCore
>>> dc = IWriteZopeDublinCore(fried_chicken)
Traceback (most recent call last):
...
TypeError: (’Could not adapt’, <__main__.Recipe object at 0x227fc30>,
<InterfaceClass zope.dublincore.interfaces.IWriteZopeDublinCore>)However, after marking it attribute-annotatable, the adaption works.We can successfully assign Dublin Core metadata, more importantly without going through annotations:>>> from zope.interface import alsoProvides
>>> from zope.annotation.interfaces import IAttributeAnnotatable
>>> alsoProvides(fried_chicken, IAttributeAnnotatable)
>>> dc = IWriteZopeDublinCore(fried_chicken)
>>> dc.title = u’Fried chicken’Of course, the metadata still ends up in the annotations as the following look behinds the scenes reveals:>>> dict(fried_chicken.__annotations__)
{’zope.app.dublincore.ZopeDublinCore’: <zope.dublincore.
annotatableadapter.ZDCAnnotationData object at 0x29036b0>}We see that the Dublin Core adapter in fact stores its data under the zope.dublincore.ZopeDublinCore annotation key. How and in what format is secondary. We will not have to worry about ever having to work with this manually.Metadata automation
Zope does a lot more for us when our objects are annotatable than just providing the Dublin Core adapter. As you may have noticed, recipes now have a new management tab, Metadata (see Figure 14.1). It allows content editors to edit the most basic metadata, namely title and description.
The Metadata ZMI view also reveals automatically computed metadata:
• creation date,
• date of last modification,
• and the user name of the creator.
[img]http://315ok.org/blogs/yuanshujuheannotation/images/c14_1/image_preview[/img]
Fig. 14.1. Zope provides a form for all annotatable objects in which basic Dublin Core metadata can be edited.
If you add a new recipe to a folder, you will see that these values has automatically been computed and annotated to the object. We will learn in Chapter 16 how this works, for now it is just important to know that Zope does this.
Dublin Core is not only used within a document or resource management system, it also comes into action when data is interchanged. As mentioned above, the WebDAV protocol allows a client program to query object metadata using the PROPFIND method. Zope’s central view implementation for PROPFIND builds upon annotations and annotation-related adapters such as Dublin Core, thus allowing client programs to query Dublin Core and other metadata directly though WebDAV.
Permissions
Viewing and changing Dublin Core properties is protected by special permissions:
zope.app.dublincore.view is required from all principals when accessing Dublin Core metadata. Like zope.View, it is granted to anonymous by default.
zope.app.dublincore.change is required for changing Dublin Core properties.
An example in Page Templates
Since Zope’s Dublin Core solution is integrated, most of your work with the Dublin Core is in presentation components, such as Page Templates. Here is a simple example to demonstrate how to access Dublin Core metadata in ZPT.
After this we will look at how to use the Dublin Core adapter in Python. In order not to over-complicate things, we will simply write a small viewlet that shows when a particular object was created and when it was last modified.
Example 14.2.1 shows its source code. Its registration in ZCML shall be omitted here as it looks like any other viewlet registration we have done so far. Just note that we will register this viewlet for objects providing IAnnotatable, as the Dublin Core metadata will likely be available for such objects.
An example in Python
As an example for working with the Dublin Core adapter from Python, let us extend the XML-RPC view class from the previous chapter to provide another method for Dublin Core metadata retrieval. Example 14.2.2 shows
the modified XML-RPC view class whereas Example 14.2.3 displays the little change necessary in order to configure the additional view method.
Since we already wrote an XML-RPC client in Chapter 13, consider it an optional exercise to write one for the new view method. The functional doctest in xmlrpc/README.txt was updated, so the functionality of the view is ensured either way.
Summary
• The Dublin Core specification describes a set of metadata commonly associated with resources in document management systems.
Example 14.2.1 Accessing Dublin Core metadata from Page Templates (skin/metadata.pt)
1 <div id="metadata" class="box" i18n:domain="worldcookery"
2 tal:define="created context/zope:created;
3 modified context/zope:modified;
4 formatter python:request.locale.dates.getFormatter(’
dateTime’)"
5 tal:condition="python:created or modified">
6 <h1 i18n:translate="heading-about">About this item</h1>
7
8 <p i18n:translate="" tal:condition="created">
9 Created on
10 <span i18n:name="created_date"
11 tal:replace="python:formatter.format(created)" />
12 </p>
13
14 <p i18n:translate="" tal:condition="modified">
15 Last modified on
16 <span i18n:name="modified_date"
17 tal:replace="python:formatter.format(modified)" />
18 </p>
19 </div>2–3. Apart from traversal to attributes or dictionary keys, ZPT’s path expressions can make use of namespace adapters. These are special traversing adapters for the TALES expressions that allow accessing additional information, such as metadata. The zope namespace adapter provides access to the most basic set of Dublin Core metadata, such as Title, Created Date, and Modified Date. Thelatter two are accessed here.
4, 11, and 17. Note that we have a textbook example of localization here. The created and modified dates are localization-sensitive values. As discussed in Chapter 9, we acquire a date formatter from the request’s locale and format the dates so they will be displayed according to local conventions.
• Zope’s Dublin Core support lies mainly in an adapter provided by the zope.dublincore package.Since it is metadata, the adapter stores Dublin Core information inannotations.
• Zope also provides automated metadata updating facilities that are triggered when objects are added and/or modified.
• In Page Templates, the zope TALES namespace adapter provides access to basic Dublin Core fields.
Example 14.2.2 Enhanced XML-RPC view class providing a view method for metadata retrieval (xmlrpc/recipe.py)
1 import time
2 import xmlrpclib
3 from zope.schema import getFields
4 from zope.dublincore.interfaces import IZopeDublinCore
5 from zope.app.publisher.xmlrpc import XMLRPCView
6 from worldcookery.interfaces import IRecipe
7
8 def to_unicode(string):
9 if isinstance(string, unicode):
10 return string
11 return string.decode(’utf-8’)
12
13 class RecipeView(XMLRPCView):
14
15 def info(self):
16 return dict((field, getattr(self.context, field))
17 for field in getFields(IRecipe))
18
19 def dublincore_info(self):
20 dc = IZopeDublinCore(self.context)
21 info = dict((field, getattr(dc, field))
22 for field in getFields(IZopeDublinCore))
23 for name in (’effective’, ’created’, ’expires’, ’modified’):
24 if info[name]:
25 epochtime = time.mktime(info[name].timetuple())
26 info[name] = xmlrpclib.DateTime(epochtime)
27 else:
28 info[name] = ’’
29 return info
30
31 def edit(self, info):
32 context = self.context
33 context.name = to_unicode(info[’name’])
34 context.ingredients = \
35 [to_unicode(ingr) for ingr in info[’ingredients’]]
36 context.tools = [to_unicode(tool) for tool in info[’tools’]]
37 context.time_to_cook = info[’time_to_cook’]
38 context.description = to_unicode(info[’description’])
39
40 return "Object updated successfully"20–22. After adapting the recipe object to IZopeDublinCore, we use the same trick as in the info method to populate a dictionary with values from the instance attributes, this time from the adapter.23–38. As specified by the IZopeDublinCore interface, the date values are stored as datetime objects, a data type that xmlrpclib cannot serialize1. To ensure proper data exchange between Zope and the XML-RPC client, we have to convert datetime objects into xmlrpclib.DateTime objects. Values that are None are converted to empty strings.
[img]http://315ok.org/blogs/yuanshujuheannotation/images/c14_2/image_preview[/img]
Fig. 14.2. Showing created and last modified dates in a sidebar viewlet.
Flashback
In Zope 2, the Content Management Framework (CMF) first introduced Dublin Core support, mainly through a set of interfaces. CMF content classes can choose to implement these to signal to the rest of application that they supported Dublin Core methods. This is by far the most prominent usage of interfaces in a Zope application prior to Zope 3.
The CMF also provides an implementation of these interfaces, DefaultDublinCoreImpl, that content classes can inherit from to gain Dublin Core functionality. The obvious difference to Zope 3 here
is, again, that extra functionality like handling metadata, especially a certain kind like Dublin Core, is constrained to external components like Example 14.2.3 Adding another XML-RPC view method to the configuration (xmlrpc/configure.zcml)
1 <configure
2 xmlns="http://namespaces.zope.org/zope"
3 xmlns:xmlrpc="http://namespaces.zope.org/xmlrpc"
4 >
5
6 <xmlrpc:view
7 for="worldcookery.interfaces.IRecipe"
8 class=".recipe.RecipeView"
9 methods="info dublincore_info"
10 permission="zope.View"
11 />
12
13 <xmlrpc:view
14 for="worldcookery.interfaces.IRecipe"
15 class=".recipe.RecipeView"
16 methods="edit"
17 permission="zope.ManageContent"
18 />
19
20 </configure>9. By adding the dublincore info method to this list it will be registered as an XML-RPC view, protected with the zope.View permission like the info method.adapters. This makes Zope 3 much more flexible, since the an adapter can easily be exchanged—a base class cannot.Finally, Zope 3 absolutely surpasses the CMF’s metadata model by offering a totally generic solution: annotations. They not only allow us to associate information useful to humans but also application-relevant data, such as workflow states, revision control status, etc.
14.3 Custom metadataIt is often necessary to store custom metadata. Experience shows that advanced and complex applications always require at least one or two fields more than the Dublin Core standard provides. This is far from being a tragedy, since we can simply write adapters to store the custom metadata in annotations.
As a simple example for such a component, consider an online rating system through which visitors of the World Cookery website can rate recipes according to how good they found the dish or the recipe description. Again,the rating information is not part of the actual recipe data schema. Since it is clearly metadata, it belongs in an annotation.Interfaces
First, we will have to define two interfaces (see Example 14.3.1). IRatable is a marker interface identifying objects that can be rated. It extends the IAnnotatable marker interface to express the dependency on annotations.
The second interface, IRating, describes the actual rating API, gathering rating information and performing ratings. An adapter for ratable objects will provide this interface.
Example 14.3.1 Interfaces for the simple rating system (interfaces.py)
1 ...
2 from zope.schema import Float
3 from zope.annotation.interfaces import IAnnotatable
4
5 class IRatable(IAnnotatable):
6 """Marker interface that promises that an implementing object maybe
7 rated using ‘‘IRating‘‘ annotations.
8 """
9
10 class IRating(Interface):
11 """Give and query rating about objects, such as recipes.
12 """
13
14 def rate(rating):
15 """Rate the current object with ‘rating‘.
16 """
17
18 averageRating = Float(
19 title=_(u"Average rating"),
20 description=_(u"The average rating of the current object"),
21 required=True
22 )
23
24 numberOfRatings = Int(
25 title=_(u"Number of ratings"),
26 description=_(u"The number of times the current has been rated"),
27 required=True
28 )An adapter
In analogy to the IZopeDublinCore adapter, we now provide the IRating adapter for ratable objects. It will store all rating information in the ratable object’s annotations, thus providing a frontend to this particular type of metadata through the IRating interface. Example 14.3.2 shows the source code.
Example 14.3.2 Adapter providing rating functionality based on annotations (rating.py)
1 from persistent.dict import PersistentDict
2 from persistent.list import PersistentList
3 from zope.interface import implements
4 from zope.component import adapts
5 from zope.annotation.interfaces import IAnnotations
6 from worldcookery.interfaces import IRating, IRatable
7
8 KEY = "worldcookery.rating"
9
10 class Rating(object):
11 implements(IRating)
12 adapts(IRatable)
13
14 def __init__(self, context):
15 self.context = self.__parent__ = context
16 annotations = IAnnotations(context)
17 mapping = annotations.get(KEY)
18 if mapping is None:
19 blank = {’average’: 0.0, ’ratings’: PersistentList()}
20 mapping = annotations[KEY] = PersistentDict(blank)
21 self.mapping = mapping
22
23 def rate(self, rating):
24 ratings = self.mapping[’ratings’]
25 ratings.append(float(rating))
26 self.mapping[’average’] = sum(ratings)/len(ratings)
27
28 @property
29 def averageRating(self):
30 return self.mapping[’average’]
31
32 @property
33 def numberOfRatings(self):
34 return len(self.mapping[’ratings’])8. As mentioned earlier in this chapter, annotations are stored under a certain key so that different sets of metadata do not conflict. The string here defines the key used for storing rating information. By making this a de facto string constant, we avoid potential typos when using the key later.15. Since this is a trusted adapter, it will be security-proxied. In that case it is a good idea to fill the parent attribute with the context object so that the security machinery can look up local grants (in case they exist) by walking up the parent hierarchy.
16–17. Like the Dublin Core adapter, we read and write to and from annotations.Therefore we get yet another adapter, the annotations adapter. From it we acquire a mapping object for storing rating data in. The IAnnotations interface requires the annotations adapter to behave like a mapping object itself.
19–20. In case the mapping object cannot be acquired, it means that this adapter is invoked on this particular object instance for the first time. We thus provide a default mapping with a zero average and an empty list. Since the annotations are likely to be persisted, we have to be careful that we comply with the rules of persistency. To be safe, we use PersistentDict and PersistentList instead of their non-persistent flavours.
For configuration, we will not only have to configure the adapter, but we will also have to make the Recipe class ratable so that the adapter works on it. Example 14.3.3 shows how configure.zcml needs to be enhanced.As usual, we can now test the adapter on the command line. Consider a recipe:
$ bin/debugzope
>>> class Recipe(object):
... pass
...
>>> hamburgers = Recipe()In order to mark it ratable, it also needs to be annotatable, for example attribute-annotatable:>>> from zope.annotation.interfaces import IAttributeAnnotatable
>>> from worldcookery.interfaces import IRatable
>>> from zope.interface import alsoProvides
>>> alsoProvides(hamburgers, IAttributeAnnotatable, IRatable)▼
Now we can rate the object using the IRating adapter:>>> from worldcookery.interfaces import IRating
>>> rating = IRating(hamburgers)
>>> rating.rate(1) # I don’t like hamburgers
>>> rating.rate(9) # I like hamburgersExample 14.3.3 Configuring the rating adapter and making recipes ratable (configure.zcml)1 ...
2 <class class=".recipe.Recipe">
3 <implements
4 interface="zope.annotation.interfaces.IAttributeAnnotatable
5 .interfaces.IRatable"
6 />
7 <require
8 permission="zope.View"
9 interface=".interfaces.IRecipe"
10 />
11 <require
12 permission="zope.ManageContent"
13 set_schema=".interfaces.IRecipe"
14 />
15 </class>
16
17 ...
18
19 <adapter
20 factory=".rating.Rating"
21 trusted="true"
22 />
23
24 <class class=".rating.Rating">
25 <require
26 permission="zope.View"
27 interface=".interfaces.IRating"
28 />
29 </class>
30 ...3–6. We want recipes to be ratable, we therefore add IRatable to the list of additional interfaces the Recipe class should implement.21. Like all other components invoked through user-interaction, adapters have to respect security-protected methods and attributes. That is usually not a problem because the object’s attributes that the adapter works with should have security declarations that requires a permission from the user. The problem is that this mechanism does not work with attribute annotations because the annotations attribute is hardly ever configured with security declarations. After all, the classes are not supposed to know about it.
The solution here is to mark this adapter as trusted. This will prevent any security checks when the adapter accesses attributes on the object, it will have free access. In return, the adapter itself will be security proxied and needs security declarations (see below).
24–29. Since the IRatable adapter is a trusted adapter, it itself rather than the adapted object will be security proxied. That means the adapter implementation (the Rating class) now needs security declarations as well, otherwise other components could not use it.
Example 14.3.4 Docfile test for the rating adapter (rating.txt)
1 ==============
2 Rating objects
3 ==============
4
5 The rating system allows users to rate objects on a continuous scale
6 (a discrete scale can be enforced by a view). We distinguish
7 *ratable* objects which have to annotatable and implement ‘‘IRatable‘
8 on one hand and the ‘‘IRating‘‘ adapter for ratable objects which the
9 rating of the latter on the other hand.
10
11 Consider a simple object, e.g. a recipe:
12
13 >>> from worldcookery.recipe import Recipe
14 >>> hamburgers = Recipe()
15
16 In order to mark it ratable, it also needs to annotatable, for example
17 attribute-annotatable:
18
19 >>> from zope.annotation.interfaces import IAttributeAnnotatable
20 >>> from worldcookery.interfaces import IRatable
21 >>> from zope.interface import alsoProvides
22 >>> alsoProvides(hamburgers, IAttributeAnnotatable, IRatable)
23
24 Now we can rate the object using the ‘‘IRating‘‘ adapter:
25
26 >>> from worldcookery.interfaces import IRating
27 >>> rating = IRating(hamburgers)
28 >>> rating.rate(1) # I don’t like hamburgers
29 >>> rating.rate(9) # I like hamburgers
30
31 Of course, the adapter also tells about the average rating and number
32 of ratings that have been issued yet:
33
34 >>> rating.averageRating
35 5.0
36 >>> rating.numberOfRatings
37 2
Of course, the adapter also tells about the average rating and number of ratings that have been issued yet:
>>> rating.averageRating
5.0
>>> rating.numberOfRatings
2 ■
This small interpreter session makes a nice doctest if slightly modified.Example 14.3.4 shows the listing of a docfile test, serving as a test and documentation for the rating system at the same time. Example 14.3.5 contains the mandatory test suite and initialization routines as shown in Chapter 12.
Example 14.3.5 Test suite and initialization routines for the rating docfile test (tests/test rating.py)
1 import unittest
2 from doctest import DocFileSuite
3
4 import zope.component.testing
5 from zope.annotation.attribute import AttributeAnnotations
6 from worldcookery.rating import Rating
7
8 def setUp(test):
9 zope.component.testing.setUp(test)
10 zope.component.provideAdapter(AttributeAnnotations)
11 zope.component.provideAdapter(Rating)
12
13 def test_suite():
14 return unittest.TestSuite((
15 DocFileSuite(’rating.txt’,
16 package=’worldcookery’,
17 setUp=setUp,
18 tearDown=zope.component.testing.tearDown),
19 ))
20
21 if __name__ == ’__main__’:
22 unittest.main(defaultTest=’test_suite’)“Persistent” adaptersAs you may have noticed from the pattern of Zope’s Dublin Core adapter and our rating adapter, annotations effectively allow you to implement “persistent” adapters. This doesn’t mean that the adapter objects themselves are persisted. Rather, the adapters draw all their values from the object’s persistent annotations. Adapting the same object over and over will always yield the same values. To the user of the adapter it seems as if the adapter itself is persistent.
Browser views
As with the Dublin Core, an adapter by itself is not helpful for the user. A visitor of the World Cookery website wants to rate objects through a web browser. That means we have to provide browser access to the rating system.
Once again we can make this functionality available through a viewlet and have it appear every time we view a ratable object. Such a viewlet,however, would have to be slightly more complicated than the ones we have implemented so far. It would not only render itself, it would also have to take input and do something with this input. Such functionality cannot be handled by a Page Template alone. We will have to implement this viewlet in Python. Example 14.3.6 shows the viewlet class that makes the rating functionality available as part of the skin. Example 14.3.7 shows the Page Template that goes with the viewlet, Example 14.3.8 demonstrates how a viewlet that implemented in Python is registered.
Example 14.3.6 Viewlet for rating functionality implemented in Python (skin/rating.py)
1 from zope.viewlet.viewlet import ViewletBase
2 from zope.app.pagetemplate import ViewPageTemplateFile
3 from worldcookery.interfaces import IRating
4
5 class RatingViewlet(ViewletBase):
6
7 def update(self):
8 rating = self.request.form.get(’worldcookery.rating’)9 if rating is not None:
10 IRating(self.context).rate(rating)
11
12 render = ViewPageTemplateFile(’rating.pt’)
13
14 def rating(self):
15 return IRating(self.context)
16
17 ratingChoices = (1, 2, 3, 4, 5)1 and 5. Viewlets can choose to subclass the optional base class ViewletBase.Much like base classes such as BrowserPage, this base class merely provides an object constructor ( init ).7–12. Viewlets need to have two methods, update and render. In the former,viewlets can prepare (e.g. compute) some data that they will later need for rendering themselves. The update methods of all viewlets in a viewlet manager will be called first. Here we use this method to inspect the request for a rating that may have been submitted from the form that appears when the viewlet is rendered. A viewlet is rendered by invoking its render method. Here we again implement this method by pointing to a Page Template, whose source code is shown in Example 14.3.7.
Example 14.3.7 Page Template rendering the rating viewlet (skin/rating.pt)
1 <div id="ratings" class="box" i18n:domain="worldcookery">
2 <h1 i18n:translate="heading-ratings">Ratings</h1>
3
4 <p i18n:translate=""
5 tal:define="rating view/rating;
6 average rating/averageRating;
7 votes rating/numberOfRatings;
8 formatter python:request.locale.numbers.getFormatter(’
decimal’)">
9 This recipe has received an average rating of
10 <strong tal:content="python:formatter.format(average, ’###0.0’)"
11 i18n:name="rating">0.0</strong>
12 (<strong i18n:name="votes" tal:content="votes">12</strong> votes).
13 </p>
14
15 <form action="" tal:attributes="action request/getURL" method="post">
16 <p i18n:translate="">Rate this recipe:</p>
17 <select name="worldcookery.rating:float">
18 <option tal:repeat="rating view/ratingChoices"
19 tal:attributes="value rating"
20 tal:content="rating">rating</option>
21 </select>
22 <input type="submit" value="Rate" i18n:attributes="value rate-button"
/>
23 </form>
24 </div>5 and 18. From the viewlet instance we acquire the rating adapter instance and a list of possible ratings.8 and 10. Here we have an another example of localization. The average rating is obviously a floating point number, which is localization-sensitive too. Again, we acquire a number formatter from the request’s locale to format the number according to a pattern. Here, it makes sense to round to the first digit after the dot.
8. Since the viewlet itself will take care of processing the rating input in its update method, we set the form action to whatever the current URL is. That way we can ensure that the same page will be shown again and that the rating viewlet will be invoked.
Example 14.3.8 Configuration for rating viewlet (skin/configure.zcml)
1 ...
2 <browser:viewlet
3 name="rating"
4 for="worldcookery.interfaces.IRatable"
5 manager=".interfaces.ISidebar"
6 class=".rating.RatingViewlet"
7 layer=".interfaces.IWorldCookerySkin"
8 permission="zope.View"
9 />
10 ...6. As you now can see we refer to our viewlet class via the class parameter. With the previous viewlets we simply used the template parameter to refer to a Page Template.Fig. 14.3. Viewlet allowing the user to rate a recipe.
Summary
• Sometimes, custom metadata outside of the already supported Dublin Core needs to be associated with objects.
• It is recommended to abstract custom metadata—including the functionality to change it—in an interface, such as IRating in the example.
• Instead of directly reading and writing annotated data, an adapter for the abstract metadata interface takes care of storing the annotated data, like the IRating adapter does in the example.