Difference between revisions of "Extending ccPublisher"

From Creative Commons
Jump to: navigation, search
(Registration)
 
(7 intermediate revisions by one other user not shown)
Line 1: Line 1:
 
[[Category:CcPublisher]]
 
[[Category:CcPublisher]]
 
[[Category:Developer]]
 
[[Category:Developer]]
{{incomplete}}
 
  
 
ccPublisher 2 has been designed for extensibility and customization by 3rd party developers. This document covers extending ccPublisher 2 and other P6-based applications through the use of extensions.
 
ccPublisher 2 has been designed for extensibility and customization by 3rd party developers. This document covers extending ccPublisher 2 and other P6-based applications through the use of extensions.
 
'''Much of this page is currently theoretical; most of the infrastructure is in place, but until ccPublisher 2 ships, things may be in flux.'''
 
  
 
== Overview ==
 
== Overview ==
Line 26: Line 23:
 
             xmlns:i18n="http://namespaces.zope.org/i18n"
 
             xmlns:i18n="http://namespaces.zope.org/i18n"
 
             i18n_domain="org_cc_p6">
 
             i18n_domain="org_cc_p6">
 
+
 
 
   <!-- P6 Sample Extension Configuration           
 
   <!-- P6 Sample Extension Configuration           
 
       (c) 2005-2006, Nathan R. Yergler, Creative Commons   
 
       (c) 2005-2006, Nathan R. Yergler, Creative Commons   
 
       licensed under the GNU GPL 2                  -->
 
       licensed under the GNU GPL 2                  -->
 
+
 
 
   <extension
 
   <extension
 
     id="org.cc.p6.sample.blog_ping"
 
     id="org.cc.p6.sample.blog_ping"
 
     name="Blog Ping Example"
 
     name="Blog Ping Example"
     config_factory="blogping.config"
+
     description="A sample extension which pings the user's blog"
   >
+
   />
 +
 
 
     <subscriber
 
     <subscriber
 
           for="p6.storage.events.IStored"
 
           for="p6.storage.events.IStored"
 
           handler="blogping.handlers.afterStored"
 
           handler="blogping.handlers.afterStored"
 
           />
 
           />
   </extension>
+
    
 
  </configure>
 
  </configure>
  
In this example we first declare a <code><configure></code> object, which all of our configuration will take place within.  The i18n_domain attribute will be used for transparent internationalization.
+
In this example we first declare a <code><configure></code> object, which all of our configuration will take place within.  The i18n_domain attribute will be used for transparent internationalization.  
  
The remainder of the configuration takes place within an <code><extension></code> 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 <code>blogping</code> module.  Note that this implies that the code must be on the application's Python path.  P6 places all subdirectories of the <code>extensions</code> directory into the Python before loading extensions.  See [[Installing Extensions]] for details on platform-specific extension locations.
+
The <code>extension</code> directive registers the extension with the P6 framework and intializes the translation mechanism.  See [[Translating Extensions]] for more information on translating your extension.
  
Finally, we define a <code><subscriber></code> 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 <code>p6.storage.events.IStored</code>.
+
Finally, we define a <code><subscriber></code> which registers a callable (blogping.handlers.afterStored) which will be called after all storage providers have processed the submission (the Stored event).  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 <code>p6.storage.events.IStored</code>.  Subscribers should always be for an interface and not a specific class that implements the interface.  This allows the application to be extended in a way that will be consistent to the user.
 +
 
 +
Note that Python must be able to import the package path for the subscriber handler(the value, less the final element); in this case, Python will attempt to import the <code>blogping.handlers</code> module.  This implies that the code must be on the application's Python path.  P6 places all subdirectories of the <code>extensions</code> directory into the Python before loading extensions.  See [[Distributing Extensions]] for details on platform-specific extension locations.
  
 
=== Responding to Events ===
 
=== Responding to Events ===
 +
 +
In the ZCML slug above an event subscriber was declared for the <code>IStored</code> event.  The handler is implemented as a Python callable which takes one object, the event.  For example, the <code>blogping.handlers</code> module (specified in the <code>handler</code> attribute) might contain the following to print out the metadata collected during the publishing process:
 +
 +
  def afterStored(event):
 +
      """Simple event handler."""
 +
 
 +
      print event.metadata
 +
 +
We know that event has a <code>metadata</code> attribute by examining the interface declaration in <code>p6/storage/events.py</code>. 
  
 
=== Contributing to the User Interface ===
 
=== Contributing to the User Interface ===
 +
 +
Applications can allow extensions to contribute to the user interface through the use of [[Extension Point|Extension Points]].  An extension point declares a place in the application where extensions have the opportunity to insert one or more pages to collect specific information.  For example, the blog ping extension may need to ask the user for the URL to use for the XML-RPC interface for their blog.  To accomplish this for ccPublisher the blog ping extension would want to implement the <code>IStorageMetaCollection</code> extension point.
 +
 +
Extension Points are declared by the application in <code>[[App_zcml|app.zcml]]</code> as a wizard page.  An application may conceivably declare extension points which are not implemented by the default feature set, as a service to future extensions.  Each extension point is declared to be ''for'' a particular interface.  A sample declaration, taken from ccPublisher's app.zcml, declares an extension point that is intended to allow storage providers to collect provider-specific metadata before the upload occurs.
 +
 +
    <extensionPoint
 +
      for="p6.extension.interfaces.IStorageMetaCollection" />
 +
 +
As with extensions, the interface an extension point is declared for must be importable from Python.
 +
 +
Extensions can implement extension points by registering an event subscription handler which adapts the extension point and the event published to an <code>IPageList</code>.  This registration can either be done in Python code or ZCML.  For example, the Internet Archive storage provider registers a handler to collect the username and password for the Internet Archive in its constructor.  The constructor is called by the application framework when the storage provider's registration is processed.
 +
 +
  def __init__(self):
 +
 
 +
        ...
 +
 
 +
        zope.component.provideSubscriptionAdapter(
 +
            archiveStorageUi(self),
 +
            (p6.extension.interfaces.IStorageMetaCollection,
 +
            p6.extension.events.IExtensionPageEvent,
 +
            ),
 +
            p6.ui.interfaces.IPageList)
 +
 +
The extension-specific part of the call is the first parameter:
 +
 +
            ...
 +
            archiveStorageUi(self),
 +
            ...
 +
 +
The value provided as the subscription handler needs to be a callable which takes two parameters: the IStorageMetaCollection instance and the IExtensionPageEvent.  The archiveStorageUi function simply wraps and returns the actual handler so that a reference to the storage provider is maintained.  The handler returns an instance of IPageList.  In the case of the Internet Archive storage provider, the handler is actually a constructor for a class which implements IPageList.
 +
 +
IPageList declares a single method, <code>list()</code>, which must be implemented.  The list() method returns a list of pages to be inserted in the user interface.  The returned list may be empty.  Each element in the list must be a callable which, when called with a parent window, returns an object which subclasses ccwx.xrcwiz.XrcWizPage.
  
 
== Extension Patterns ==
 
== Extension Patterns ==
Line 102: Line 143:
  
 
=== Post-Upload activities ===
 
=== Post-Upload activities ===
 +
 +
A natural model for an extension is to perform some activity after the file upload is completed.  There are two pieces of code for implementing such an extension: registering for the appropriate extension point and actually performing the task.  For ccPublisher, the extension point to implement is <code>p6.extension.interfaces.IPostStoreExtension</code>.
 +
 +
When the blog ping extension is complete, we'll have a more concrete and fleshed out example here.
  
 
=== Storage Providers ===
 
=== Storage Providers ===
 +
 +
Storage providers are special case extensions.  They are registered using a specific ZCML directive (<code>storage</code>), but utilize the same principles as extensions for interacting with the user interface.  An important difference is the use of interface decoration.  The storage selector page allows users to select one or more repositories to upload their work to.  When a repository is chosen, the storage provider has the <code>p6.extension.interfaces.IActivated</code> added to the list of interfaces it implements.  When implementing the storage metadata extension point, providers are responsible for checking for the presence of this interface before returning the page list (in <code>list()</code>).  Note that the <code>CommonStorageMixin</code> provides a convenience method, <code>activated()</code> which returns True if the storage provider's interface list has been decorated.  See [[Writing a Storage Provider]] for more details on Storage Providers.
  
 
== Future Directions ==
 
== Future Directions ==
  
 
The Extension API is relatively new and so still under development.  See [[P6 Extension Ideas]] for possible extension ideas and API suggestions.'
 
The Extension API is relatively new and so still under development.  See [[P6 Extension Ideas]] for possible extension ideas and API suggestions.'

Latest revision as of 14:59, 5 January 2007


ccPublisher 2 has been designed for extensibility and customization by 3rd party developers. This document covers extending ccPublisher 2 and other P6-based applications through the use of extensions.

Overview

ccPublisher 2 is built on a framework dubbed P6. P6 is a loosely coupled system written in Python where software components communicate with one another via message passing. The P6 extension model allows applications to declare ways in which they may be extended, and allows 3rd party developers to provide additional functionality to the applications. Extensions have the following lifecycle: registration, event response, clean up. This document provides instructions on creating basic extensions, as well as details about common extension patterns. All instructions assume a Linux-based development environment.

Creating An Extension

Extensions are developed as Python module or packages, and a piece of ZCML (a "slug") which the P6 framework uses to complete the registration process. We are currently working on finalizing the installation location for extensions. When that is finalized, we will provide a script or template for creating a distribution package for extensions. The final distribution model will likely involve packaging extensions as Python Eggs.

To begin creating an extension, first check out a ccPublisher sandbox. Create a subdirectory in the checkout for your extension.

Registration

Extension registration and configuration is done using a ZCML slug.

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-2006, Nathan R. Yergler, Creative Commons  
      licensed under the GNU GPL 2                   -->
 
 <extension
    id="org.cc.p6.sample.blog_ping"
    name="Blog Ping Example"
    description="A sample extension which pings the user's blog"
 />
 
   <subscriber
         for="p6.storage.events.IStored"
         handler="blogping.handlers.afterStored"
         />
 
</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 extension directive registers the extension with the P6 framework and intializes the translation mechanism. See Translating Extensions for more information on translating your extension.

Finally, we define a <subscriber> which registers a callable (blogping.handlers.afterStored) which will be called after all storage providers have processed the submission (the Stored event). 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. Subscribers should always be for an interface and not a specific class that implements the interface. This allows the application to be extended in a way that will be consistent to the user.

Note that Python must be able to import the package path for the subscriber handler(the value, less the final element); in this case, Python will attempt to import the blogping.handlers module. This implies that the code must be on the application's Python path. P6 places all subdirectories of the extensions directory into the Python before loading extensions. See Distributing Extensions for details on platform-specific extension locations.

Responding to Events

In the ZCML slug above an event subscriber was declared for the IStored event. The handler is implemented as a Python callable which takes one object, the event. For example, the blogping.handlers module (specified in the handler attribute) might contain the following to print out the metadata collected during the publishing process:

 def afterStored(event):
     """Simple event handler."""
 
     print event.metadata

We know that event has a metadata attribute by examining the interface declaration in p6/storage/events.py.

Contributing to the User Interface

Applications can allow extensions to contribute to the user interface through the use of Extension Points. An extension point declares a place in the application where extensions have the opportunity to insert one or more pages to collect specific information. For example, the blog ping extension may need to ask the user for the URL to use for the XML-RPC interface for their blog. To accomplish this for ccPublisher the blog ping extension would want to implement the IStorageMetaCollection extension point.

Extension Points are declared by the application in app.zcml as a wizard page. An application may conceivably declare extension points which are not implemented by the default feature set, as a service to future extensions. Each extension point is declared to be for a particular interface. A sample declaration, taken from ccPublisher's app.zcml, declares an extension point that is intended to allow storage providers to collect provider-specific metadata before the upload occurs.

   <extensionPoint
      for="p6.extension.interfaces.IStorageMetaCollection" />

As with extensions, the interface an extension point is declared for must be importable from Python.

Extensions can implement extension points by registering an event subscription handler which adapts the extension point and the event published to an IPageList. This registration can either be done in Python code or ZCML. For example, the Internet Archive storage provider registers a handler to collect the username and password for the Internet Archive in its constructor. The constructor is called by the application framework when the storage provider's registration is processed.

  def __init__(self):
 
       ...
 
       zope.component.provideSubscriptionAdapter(
           archiveStorageUi(self),
           (p6.extension.interfaces.IStorageMetaCollection,
            p6.extension.events.IExtensionPageEvent,
            ),
           p6.ui.interfaces.IPageList)

The extension-specific part of the call is the first parameter:

           ...
           archiveStorageUi(self),
           ...

The value provided as the subscription handler needs to be a callable which takes two parameters: the IStorageMetaCollection instance and the IExtensionPageEvent. The archiveStorageUi function simply wraps and returns the actual handler so that a reference to the storage provider is maintained. The handler returns an instance of IPageList. In the case of the Internet Archive storage provider, the handler is actually a constructor for a class which implements IPageList.

IPageList declares a single method, list(), which must be implemented. The list() method returns a list of pages to be inserted in the user interface. The returned list may be empty. Each element in the list must be a callable which, when called with a parent window, returns an object which subclasses ccwx.xrcwiz.XrcWizPage.

Extension Patterns

Metadata Providers

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.

Post-Upload activities

A natural model for an extension is to perform some activity after the file upload is completed. There are two pieces of code for implementing such an extension: registering for the appropriate extension point and actually performing the task. For ccPublisher, the extension point to implement is p6.extension.interfaces.IPostStoreExtension.

When the blog ping extension is complete, we'll have a more concrete and fleshed out example here.

Storage Providers

Storage providers are special case extensions. They are registered using a specific ZCML directive (storage), but utilize the same principles as extensions for interacting with the user interface. An important difference is the use of interface decoration. The storage selector page allows users to select one or more repositories to upload their work to. When a repository is chosen, the storage provider has the p6.extension.interfaces.IActivated added to the list of interfaces it implements. When implementing the storage metadata extension point, providers are responsible for checking for the presence of this interface before returning the page list (in list()). Note that the CommonStorageMixin provides a convenience method, activated() which returns True if the storage provider's interface list has been decorated. See Writing a Storage Provider for more details on Storage Providers.

Future Directions

The Extension API is relatively new and so still under development. See P6 Extension Ideas for possible extension ideas and API suggestions.'