SilvaMetadata Access Handlers
Intro
SilvaMetadata
has a little known or used feature which provides hooks for filtering
out content types based on whatever criteria you want. They are
called metadata access handlers. They allow you to extend the
metadata sets defined on a content type, but only for objects of that
type meeting a certain criteria. At Bethel, I've used this for
two purposes so far. Both of them involve inspecting the patch of
the content to see if it is within a certain container:
- The ITS knowledgebase (http://kb.its.bethel.edu) and the BU Library (http://kb.library.bethel.edu)
knowledgebase both use silva as a backend document repository. KB
articles have associated metadata including: the groups article is
geared towards, the groups that can publically view the article, who
created, last edited, and the editors are for the article, and the
article number.
Using Silva Documents for kb articles is a really user-friendly way to edit them. We don't want this metadata seen everywhere, only within kb repositories. Metadata Access Handlers allow us to remove this metadata set for Silva Documents that aren't in repositories. This is a VERY nice feature, because the alternative is to create a Silva KB Article that extends Silva Document. Access Handlers are much more lightweight! - The Career Services e-Resume listings allows Bethel alumni and students to post their resumes to a website only visible by approved employers. For this app, we use Silva Files, and extend their metadata to include things like the categories for this resume, the duration to display, and personal information (for Career Services to use). Again the alternative, extended SilvaFile, is much more work.
That's great and all, but how do you do it?
I
learned about this feature by asking silva-dev@lists.infrae.com.
Jan-Wijbrand Kolman, one of the very talented Zope/Silva hackers at
infrae, alerted me to this feature. Here's a summary, with an
example of a closure that creates access handlers for use in the above
examples.
Installing Metadata Sets from the filesystem
First, you need to have your custom
metadata sets. When developing these, it's best to create an xml
file (using emacs of course!), and keep reinstalling it. To
install the metadata set from within Zope, you need to call an external
method that looks like this:
def install_metadata(context):
root = context.get_root()
from os import path
from Products.SilvaMetadata.Extensions.SilvaInstall import install as install_metadata
from Products.SilvaMetadata.Access import registerAccessHandler,_typeAccessHandlers
registerAccessHandler('Silva Document Version', kb_metadata_access_handler)
collection = root.service_metadata.getCollection()
if 'its-kb' in collection.objectIds():
collection.manage_delObjects(['its-kb'])
xml_file = '/export1/Zope-2.7.2-0/Products/SilvaECT/kb_metadata.xml'
fh = open(xml_file, 'r')
collection.importSet(fh)
# initialize the default set if not already initialized
for set in collection.getMetadataSets():
if not set.isInitialized():
set.initialize()
return 'Finished installing custom metadata type.'
I like to have a Zope Product or two laying around as containers for special extensions like extra metadata sets. You can alter the content type -> metadata set mappings when this product is installed via the service_extensions tab in Silva. I won't go into the structure of the install.py script, but suffice it to say you can have something like this (pattern taken from Silva's install.py):
mapping = root.service_metadata.getTypeMapping()
mapping.editMappings('', [
{'type':'Silva Document Version', 'chain':'its-kb,silva-content,silva-extra'},
])
Unfortunately, there usually isn't a complete uninstaller, so I don't have example of removing the 'its-kb' metadata set (though it would be a trivial modification of the above code). If you're having many of these within separate products that alter the mappings of a single content type, it may be a good idea to append the new sets onto the current list instead of assuming the default list.
The Access Handlers (finally)
Now the groundwork is laid to talk about access handlers (hereafter AH). The AH is a runtime thing (it's not stored in the zodb so it is not persistent). It needs to be registered whenever the zope instance starts up. Of course we all know that these initialization things are placed in the products __init__.py. So, that is where the rest of the code samples go. Here's the process:
Import the necessary functions
from Products.SilvaMetadata.Binding import MetadataBindAdapter from Products.SilvaMetadata.Compatibility import getContentType from Products.SilvaMetadata.Exceptions import BindingError from Products.SilvaMetadata.Access import registerAccessHandler
In initialize(), register the AHs
The function to call is registerAccessHandler(meta_type, ah_hook). For example:
#add a metadata register access handler. This function (defined above) is called
#whenever the metadata sets for SDV are retrieved. This one filters would the its-kb
#set if the version isn't within the knowledgebase container.
registerAccessHandler('Silva Document Version', kb_metadata_access_handler)
Create the AH hooks
The AH hooks have the following signature:
def ah_hook(tool, content_type, content): ...do stuff to get list of metadata sets for object return MetadataBindAdapter(content, actual_md_sets).__of__(content)
Where the parameters are: tool is a SilvaMetadata.MetadataTool, content_type is the meta_type of the object (string), content is the object (e.g. a SilvaDocumentVersion).
So, the AH I used to filter out the its-kb metadata sets for documents outside of kb repositories looks like this:
def kb_metadata_access_handler(tool, content_type, content):
type_mapping = tool.getTypeMapping()
metadata_sets = type_mapping.getMetadataSetsFor(content_type)
if 'kb' not in content.getPhysicalPath():
metadata_sets = [ m for m in metadata_sets if m.id != 'its-kb' ]
if not metadata_sets:
raise BindingError("no metadata sets defined for %s" % content_type)
return MetadataBindAdapter(content, metadata_sets).__of__(content)
But nearly the exact same code is used for the Career Services e-Resume app. This is a perfect time to use a lexical closure which 'generates' the appropriate AH function:
def md_access_handler_filter_by_path(metadata_sets, path_token):
def ah(tool, content_type, content):
type_mapping = tool.getTypeMapping()
actual_md_sets = type_mapping.getMetadataSetsFor(content_type)
path = content.getPhysicalPath()
if path_token not in path:
#filter out metadata_sets from actual_sets if content isn't
#in right path
actual_md_sets = [ m for m in actual_md_sets if m.id not in metadata_sets ]
if not actual_md_sets:
raise BindingError("no metadata sets defined for %s" % content_type)
return MetadataBindAdapter(content, actual_md_sets).__of__(content)
return ah
student_resumes_metadata_access_handler = md_access_handler_filter_by_path(['student-resumes'],'resumes')
kb_metadata_access_handler = md_access_handler_filter_by_path(['its-kb'],'kb')
I've heard about this type of code for a long time now, especially since it is one of the strengths of python. I haven't seen much document on it or really understood it well. That is, until I saw an explanation of lexical closures in Paul Grahams book On Lisp (http://paulgraham.com/onlisptext.html). That when it "just clicked". My eyes were opened and I understood. Now I really want to get into lisp and learn it's secrets ;-). But, that's not really a story for this article.
Summary
I hope someone will find this article to be of some use. Metadata Access Handlers allow you to create very interesting extensions and custom applications within Silva, without the need to create new content types. They are called whenever metadata sets are retrieved for an object, so it's probably a good idea to make them as fast as possible. I can see caching being used, though an slick and interesting caching solution is a bit beyond the scope of this document and this programmers abilities ;-)