Writing an Extension

From Creative Commons
Revision as of 14:14, 4 January 2006 by Nathan Yergler (talk | contribs)
Jump to: navigation, search


P6 based applications (including ccPublisher 2) can be extended by third-party developers using a loosely-coupled component architecture. At the time ccPublisher 2.0 is released CC will provide a sample extension which pings a user's blog when they successfully publish media, along with an installer which can be used as a reference.

THIS PAGE IS LARGELY THEORETICAL -- THIS IS HOW WE THINK THINGS WILL WORK, BUT THERE'S NOT MUCH CODE BEHIND IT -- YET

Extension Anatomy

Extensions are bits of code and configuration that can add or modify the functionality of a P6 Application. By default extensions are application-agnostic: they are only aware that they are operating in a P6 application, but have no direct knowledge of the specific application. This implies that when installed the extension will add it's functionality to all installed P6 Applications on a user's system.

An extension consists of (at least) two parts:

  • code to perform your new functionality
  • a ZCML file which registers your events, adapters and handlers

An extension may also contain:

  • additional wizard pages to be presented in the application
  • a configuration UI which allows the user to configure your extension

Registering Your Extension

Extension registration and configuration is done using ZCML. An extension's installation program should add the ZCML to P6's site-extensions directory. The location of the site-extensions directory can be found using the p6.api module.

A sample registration ZCML would look something like this:

<configure xmlns="http://namespaces.zope.org/zope"
           xmlns:i18n="http://namespaces.zope.org/i18n"
           i18n_domain="org_cc_p6">
 <!-- P6 Sample Extension Configuration          
      (c) 2005, Nathan R. Yergler, Creative Commons  
      licensed under the GNU GPL 2                   -->
 <!-- If the extensions Python files are not installed on the 
      regular P6 PythonPath, specify new path element(s) here -->
 <path>${site-extensions}/blog_ping</path>
 <extension
    id="org.cc.p6.sample.blog_ping"
    name="Blog Ping Example"
    config_factory="blogping.config"
 >
   <subscriber
         for="p6.storage.events.IStored"
         handler="blogping.handlers.afterStored"
         />
 </extension>
</configure>

In this example we first declare a <configure> object, which all of our configuration will take place within. The i18n_domain attribute will be used for transparent internationalization.

The first non-comment inside configuration is our <path> element. You can specify any number of path elements; the contents are interpolated and appended to the PYTHON_PATH of the application. This must be the first element if you are referencing code which is not in the standard Python path.

The remainder of the configuration takes place within an <extension> element. The extension element defines an id and name (both required), and an optional config_factory. The value of config_factory is a Python callable which will display the extensions configuration UI when called. Note that Python must be able to import the package path (the value, less the final element); in this case, Python will attempt to import the blogping module.

Finally, we define a <subscriber> which registers a callable (blogping.handlers.afterStored) which will be called after all storage providers have processed the submission. This callable should be a standard Zope3 event-handler; that is, it will be passed a single parameter. That parameter will be an event object which implements p6.storage.events.IStored.

Writing Handler Code

XXX: Need some sample code; working on fleshing out customization docs first.

Extension Patterns

Metadata Provider: Provide Metadata for a Specific File Type

The Metadata Provider pattern is used to extract additional metadata from files or retrieve it from outside sources. For example, ccPublisher uses this pattern to extract the title and artist from an MP3 using ID3 tags. There are two steps to implementing this pattern: listening for ItemSelected events and, if appropriate, publishing UpdateMetadata events.

Because a particular P6 application will have a specific metadata grouping, we use canonical URIs to specify fields outside of the group-field structure. For example, ccPublisher uses the following declaration in app.zcml to declare the Work Title field:

    <field id="title"
           label="Title of Work"
           type="p6.metadata.types.ITextField"
           validator="ccpublisher.validators.validateTitle"
           canonical="http://purl.org/dc/elements/1.1/title"
           />

The canonical property specifies the URI to use, regardless of grouping. In general, metadata providers included with P6 will map attributes to Dublin Core elements whenever possible. See Canonical IDs for more information.

To examine the details of a metadata provider we'll examine some relevant bits of the ID3 provider included with P6. The source for the provider is located at p6/storage/providers/id3.py. The provider function is registered in configure.zcml as a subscriber:

 <!-- Metadata provider registrations -->
 <subscriber
       for="p6.storage.events.IItemSelected"
       handler=".providers.id3.itemSelected"
       />

Listening for the IItemSelected events will notify the provider function, id3.itemSelected in this case, when the user selects a file. The actual provider function has the following signature:

 def itemSelected(event):
   """IItemSelected event subscriber which provides ID3 metadata for
   MP3 files."""
   

The value passed in for event will be an object implementing IItemSelected. The ID3 provider then checks to make sure we're dealing with an actual file (as opposed to a stream, URI, etc) since that's what it knows how to handle:

   # make sure a FileItem was selected
   if (p6.storage.interfaces.IFileItem in
       zope.interface.implementedBy(event.item.__class__)):

The IFileItem interface states that the .getIdentifier() method will return the filename. We use this to retrieve the ID3 information. Once we have the metadata extracted, we publish an UpdateMetadataEvent for each field we found:

           updateEvent = p6.metadata.events.UpdateMetadataEvent(
               event.item,
               'http://purl.org/dc/elements/1.1/title',
               id3.getTitle()
               )
           zope.component.handle(updateEvent)

Note that we specify event.item as the item we're updating metadata for, and that we use the canonical URI for the field.

Extension Ideas and Plans

We hope to ship sample extensions with ccPublisher 2.0. Currently planning on:

  • Blog ping extension: post to your blog when you upload new media

Other ideas we'd like to see implemented:

  • Upload to my website (via FTP, etc) for self-hosting
  • Upload to .Mac; this might also integrate with the DotMac SDK to allow easy signup for new users

Future Directions

Ideas which will probably not be implemented for ccPublisher 2 / P6 1.0 are:

  • user interface for selectively enabling/disabling extensions in an application
  • application-specific extensions