Extending Silva HowTo


Author: Benno Luthiger (benno.luthiger(+)id.ethz.ch), 2004/06/09

You can download the source code of this example extension.

(Silva Version 0.9.3)

Goal: We want to extend the basic Silva content object, the Silva Document, with some specific fields so that our new content object contains all relevant information needed for a 'EuroPython Talk' object. Further we want to manage and display this information in a consistent manner. We will package all this functionality into a product named 'SilvaExtEPT'. To do this, we extend Silva by implementing a Silva Extension.

In the first place, a Silva Extension is a Zope product. It is one or more modules containing functionality and an __init__ file to hook into the Zope framework.

Because a Silva Extension is a Zope product, it can be deployed as tar file and installed into the Products directory of a Zope instance.

But to work as a Silva Extension, it has to meet some conditions to hook into the Silva framework as well.

In Silva, content is stored as XML. For our purpose the XML should look like the following:

    <eptalk>
      <eptspeaker>
        Benno Luthiger
      </eptspeaker>
      <epttitle>
        Extending Silva
      </epttitle>
      <eptabstract>
         Silva has plugin capability built in. This feature ...
      </eptabstract>
      <eptaudience>
         technical, advanced
      </eptaudience>
      <epttype>
         normal talk
      </epttype>
      <eptduration>
         60 Minutes
      </eptduration>
      <eptbio>
        The speaker has been ...
      </eptbio>

      <p> etc.

    </eptalk>

Step 1: The Model

The Silva architecture is inspired by the Model-View-Controller (MVC) pattern. As first step, therefore, we implement the model for our EuroPython Talk extension.

In the directory /Products/SilvaExtEPT we create the module EPTalk.py and therein we create two classes class EPTalk(Document) and class EPTalkVersion(DocumentVersion). The first class inherits form Silva Document. It's the container of the different versions an author creates of a specific instance of an EPTalk class. The second class implements the versions of an EPTalk class. Basically it contains the parsed XML depicted above with varying content. This class inherits from 'Silva DocumentVersion':

      class EPTalk(Document):
          """Silva Extension for EuroPython Talk.
          """
          security = ClassSecurityInfo()

          meta_type = "Silva EuroPython Talk"

          __implements__ = IVersionedContent

          inherited_manage_options = CatalogedVersionedContent.manage_options
          manage_options=(
              (inherited_manage_options[0],)+
              ({'label':'Silva /edit...', 'action':'edit'},)+
              inherited_manage_options[1:]
              )

      InitializeClass(EPTalk)

      class EPTalkVersion(DocumentVersion):
          """Silva version of EuroPython Talk.
          """
          meta_type = "Silva EuroPython Talk Version"
          __implements__ = IVersion

          security = ClassSecurityInfo()

      manage_options = (
          {'label':'Edit',       'action':'manage_main'},
          ) + CatalogedVersion.manage_options

      def __init__(self, id, title):
          EPTalkVersion.inheritedAttribute('__init__')(self, id, title)
          self.content = ParsedXML('content', '<eptalk></eptalk>')

      def addNode(self, node_name, node_value, attribute=None):
          docroot = self.content.documentElement
          node = docroot.createElement(node_name)
          node.appendChild(docroot.createTextNode(node_value))
          docroot.appendChild(node)
          if attribute:
              node.setAttribute('type', attribute)

      InitializeClass(EPTalkVersion)

The constructor of EPTalkVersion creates a ParsedXML with the document root &lt;eptalk&gt;. The class EPTalkVersion provides a method addNode() to add further nodes to this parsed XML.

Next we have to implement the handlers to add new instances of EPTalk and EPTalkVersion to the ZODB:

      manage_addEPTalkForm = PageTemplateFile("www/epTalkAdd", globals(),
                                              __name__='manage_addEPTalkForm')

      def manage_addEPTalk(self, id, title, result, REQUEST=None):
          """Add a EPTalk."""

          if not Id(self, id).isValid():
              return
          object = EPTalk(id)
          self._setObject(id, object)
          object = getattr(self, id)
          object.manage_addProduct['SilvaExtEPT'].manage_addEPTalkVersion('0', title)
          version = getattr(object, '0')

          version.addNode('epttitle', title)
          version.addNode('eptspeaker', result['eptspeaker'])
          version.addNode('eptabstract', result['eptabstract'])
          version.addNode('eptaudience', ', '.join(result['eptaudience']))
          version.addNode('epttype', result['epttype'])
          version.addNode('eptduration', result['eptduration'])
          version.addNode('eptbio', result['eptbio'])

          object.create_version('0', None, None)
          add_and_edit(self, id, REQUEST)
          return ''

      manage_addEPTalkVersionForm = PageTemplateFile("www/epTalkVersionAdd", globals(),
                                                     __name__='manage_addEPTalkVersionForm')

      def manage_addEPTalkVersion(self, id, title, REQUEST=None):
          """Add a EPTalk version to the Silva-instance."""
          version = EPTalkVersion(id, title)
          self._setObject(id, version)

          version = self._getOb(id)
          version.set_title(title)

          add_and_edit(self, id, REQUEST)
          return ''

The method manage_addEPTalk passes as parameters the object's id, title and a dictionary containing the data for the nodes in the EPTalk XML. It creates an instance of EPTalk and adds an instance of EPTalkVersion to this newly created object. At this moment we have an instance of EPTalk with the specified id and title, which contains the first version of EPTalkVersion identified by the id 0. This version instance contains a parsed XML with the document element <eptalk>. We then fill this XML using the version's addNode() method and the data passed by the result dictionary.

Step 2: The __init__.py script

In the __init__.py script we have to implement the method initialize(), which is called when the Zope instance is started:

      from Products.Silva.ExtensionRegistry import extensionRegistry

      from Products.Silva.fssite import registerDirectory
      from Products.SilvaMetadata.Compatibility import registerTypeForMetadata

      from Products.SilvaExtEPT import EPTalk
      import install

      from Products.PythonScripts.Utility import allow_module
      allow_module('Products.SilvaExtEPT.helpers')

      def initialize(context):

          extensionRegistry.register(
              'EPTalk', 'Silva EuroPython Talk', 
              context, [EPTalk],
              install, depends_on='Silva')

          registerDirectory('views', globals())
          registerDirectory('widgets', globals())

          registerTypeForMetadata(EPTalk.EPTalkVersion.meta_type)

This is the place we can tell Silva that it has to take this product for a Silva extension. This is done using the service extensionRegistry imported from Silva.ExtensionRegistry. In the register method of this service we have to define the extension's name, it's displayed name, a list of classes (i.e. models), the module to install this extension and the prerequisites of this extension.

Then we have to register the fss directories for the extension's views and widgets and, at last, we have to register the extension version's metatype for the Silva Metdata.

Step 3: The install script

The extension's install script is called when the site administrator wants to install or uninstall the extension. Therefore, this module has to provide the two methods install() and uninstall(). Both methods get the Silva root as parameter:

      def install(root):
          """The view infrastructure for Silva.
          """
          # create the extension views from filesystem
          add_fss_directory_view(root.service_views, 'EPTalk', __file__, 'views')

          # register views
          registerViews(root.service_view_registry)    

          # create the widget editor views from filesystem
          add_fss_directory_view(root, _custom_widgets_name, __file__, _widgets_dir)

          # add the services for XMLWidgets
          configureXMLWidgets(root)

          # now add permissions 
          root.manage_permission('Add Silva EuroPython Talks', _permissions)
          root.manage_permission('Add Silva EuroPython Talk Versions', _permissions)

          # add meta_type to silva_addables_allowed
          set_to_addables(root)

          # include this tpye into metadata system: 
          mapping = root.service_metadata.getTypeMapping()
          chain = mapping.getTypeMappingFor('Silva Document Version').getMetadataChain()
          mapping.editMappings('', [
              {'type': EPTalk.EPTalkVersion.meta_type,
              'chain': chain},])    

The install() method first adds the fss view for the extension's views system. Then these views are registered to Silva's 'service_view_registry':

      def registerViews(reg):
          """Register eptalk views on registry.
          """

          meta_type = EPTalk.EPTalk.meta_type
          type = 'EPTalk'

          # edit
          reg.register('edit', meta_type, ['edit', 'VersionedContent', type])
          # public
          reg.register('public', meta_type, ['public', type])
          # add
          reg.register('add', meta_type, ['add', type])

This is done by calling the service's register() method and passing the model's meta_type and the path for all modes edit, public and add. For example if Silva is in add mode it looks in views/add/EPTalk for the views to add an instance of EPTalk.

As next step in the install script, the fss view for the extension's widgets system is added. After that, these widgets are configured and registered. Configuration consists of adding XML widget registries (i.e. instances of XMLWidgets) for editing, previewing and viewing to the Silva root:

      def configureXMLWidgets(root):
          """Configure XMLWidgets registries, editor, etc.
          """    
          # create the services for XMLWidgets
          for name in ['service_eptalk_editor', 
                       'service_eptalk_previewer', 
                       'service_eptalk_viewer']:
              root.manage_addProduct['XMLWidgets'].manage_addWidgetRegistry(name)

Having created these registries, we have to register every single widget of our extension for all modes edit, public and add. In this step we tell Silva where to look for the functionality to render a specific widget if it bumps into this widget while parsing the extension's XML:

      def registerEPTalkWidgets(root, custom_widgets):
          """ register the widgets at the corresponding registries.
          this function assumes the registries already exist.
          """
          registerEPTalkEditor(root, custom_widgets)
          registerEPTalkPreviewer(root, custom_widgets)
          registerEPTalkViewer(root, custom_widgets)

      def registerEPTalkEditor(root, custom_widgets):
          wr = root.service_eptalk_editor
          wr.clearWidgets()

          wr.addWidget('eptalk', (custom_widgets, 'top', 'eptalk', 'mode_normal'))

          for name in tag_names:
              wr.addWidget(name, (custom_widgets, 'element', 'eptalk_elements'))

          for nodeName in ['p', 'heading', 'list', 'toc', 'image', 'table', 'nlist']:
              wr.addWidget(nodeName, ('service_widgets', 'element', 
                                      'doc_elements', nodeName, 'mode_normal'))

          wr.setDisplayName('doc', 'Title')
          wr.setDisplayName('p', 'Paragraph')
          wr.setDisplayName('heading', 'Heading')
          wr.setDisplayName('list', 'List')
          wr.setDisplayName('pre', 'Preformatted')
          wr.setDisplayName('toc', 'Table of contents')
          wr.setDisplayName('image', 'Image')
          wr.setDisplayName('table', 'Table')
          wr.setDisplayName('nlist', 'Complex list')
          wr.setDisplayName('dlist', 'Definition list')
          wr.setDisplayName('source', 'External Source')

          wr.setAllowed('eptalk', ['p', 'heading', 'list', 'dlist', 
                                   'pre', 'image', 'table', 'nlist', 'toc', 'source'])

At this time I restrict myself to discuss the case for the edit mode. The implementations of the view and preview mode are analogous. To prepare the edit mode, we register the extension's widgets by calling the widget registry's addWidget() method and passing the widget's name and the path. For example functionality for the document element eptalk is located in widgets/top/eptalk/mode_normal and the special nodes for out EPTalk document are registered under widgets/element/eptalk_elements. In the rest of the method registerEPTalkEditor() we define which further elements from Silva Document are allowed to be placed on an EPTalk document.

The uninstall method simply removes the objects added to the Silva root during installation:

      _custom_widgets_name = 'service_custom_widgets_ept'

      def uninstall(root):
          unregisterWidgets(root)
          unregisterViews(root.service_view_registry)
          root.service_views.manage_delObjects(['EPTalk'])
          root.manage_delObjects([_custom_widgets_name])

Having set up the init and installation procedure, we have to create a views and widgets directory in our extension product before we can deploy our extension for testing purpose and restart the Zope instance. When we open the ZMI and look at the list of extensions in Silva/service_extensions we shall find there a new entry for our EPTalk extension. We can now install, uninstall and refresh our extension by clicking on the buttons displayed.

We install the EPTalk extension and switch to the SMI. We expect now to see the new entry Silva EuroPython Talk added in the list of Silva addables.

However, if we try to add a Silva EuroPython Talk, we will get an error at this moment. We haven't implemented the view needed to add a new instance of EPTalk. This will be done in the next step.

Step 4: The views and controllers

For that a page can be displayed to enter the information for a EuroPython Talk document, we need a Formualtor form named form.form. in the directory SilvaExtEPT/views/add/EPTalk. I usually create such a Formualtor form somewhere in the ZODB, switch to the Formualtor's XML tab and copy and paste the form's XML to the file form.form in the file system.

Note that you have to name the form field to enter the object's id with object_id and the entry field for the object's title with object_title. With this convention, the form fits nicely into the Silva framework. For the remaining fields, I choose the names according to the XML's node name.

We can check now in the SMI whether the form is displayed. This will be the case. However, we still have something to do to add a new EPTalk document. We need the controller functionality called after the user has entered the data and clicked on the save or save + exit button. A click on one of these buttons calls the Silva's add_submit script which in turn expects a script add_submit_helper we have to provide:

      model.manage_addProduct['SilvaExtEPT'].manage_addEPTalk(id, title, result)
      ept = getattr(model, id)
      return ept

In this script we call the manage_addEPTalk() method of out SilvaExtEPT module. As parameters we pass the object's id and title and an additional dictionary containing the values entered in the form.

Having done this we are able to create our first instance of an EPTalk document. We can check the document after creating in the ZMI by opening the document's version and looking at the Raw tab of the parsed XML. Indeed, we'll find the XML as depicted at the beginning.

Being able to create EPTalk documents, what remains is to implement the functionality to edit and display the entered data.

For that the document ca be edited, we have to provide a script named edit.py in 'SilvaExtEPT/views/edit/VersionedContent/EPTalk':

      session = context.REQUEST.SESSION
      model = context.REQUEST.model
      editable = model.get_editable()

      request = context.REQUEST
      xml_url = (model.absolute_url() + '/' + model.get_unapproved_version() + '/content')
      request.set('xml_url', xml_url)
      request.set('xml_rel_url', '/%s/%s/content' % (model.absolute_url(1), 
                                                     model.get_unapproved_version()))

      service_editor = context.service_editor
      service_editor.setDocumentEditor(editable.content.documentElement, 
                                       'service_eptalk_editor')

      return service_editor.render(service_editor.getRoot(editable.content.documentElement))

The important thing in this script is to set the correct XML Widget Registry to the document editor in charge. In our case this is service_eptalk_editor. After that the service editor's render() method is called, which parses the editable version of the document we want to edit.

Before we implement the functionality in the widgets system, we copy the basic scripts and page templates from the Silva Document product. In the actual version of Silva, the widgets system is not yet as nicely elaborated as the views system, therefore, we have to work around this by copy and paste.

For that the document's data can by displayed in the SMI, the path SilvaExtEPT/widgets/element/eptalk_elements/mode_normal must exist and a page template render.pt must be acquirable from there. In our case, the mode_normal directory is empty and the render.pt in the parent directory is an empty file. We can proceed this way because I've defined in the installation script that all rendering for out special EuroPython Talk nodes is done when the document's root node is parsed.

As next step, therefore, we have to provide this functionality by filling the SilvaExtEPT/widgets/top directory in the widgets system. When I open an EPTalk document in the SMI, the extension is in normal mode and expects a page template render.pt somewhere. We therefore provide the directory SilvaExtEPT/widgets/top/eptalk/mode_normal and place the following render.pt in it's parent directory:

      <init tal:omit-tag=""
        tal:define="global disable_delete python:1;
                    global disable_move_up python:1;
                    global disable_move_down python:1" />
      <br />
      <div metal:use-macro="container/macro_normal_view/macros/normal_view">
      <div metal:fill-slot="node_content" tal:omit-tag=""
           tal:define="helpers python:modules['Products.SilvaExtEPT.helpers']">
        <div tal:omit-tag="" 
             tal:content="structure python:helpers.get_ept_content(
                          request.node, here.service_editorsupport)">EPTalk</div>
      </div>
      </div>
      <content tal:replace="structure python:
                            here.service_editor.renderElementsCache(request.node)" />

In this page template I call the method get_ept_content() in my helpers module. This method retrieves the data from the model and returns it as a simple html table.

To handle the view and preview mode, we have to provide the two scripts render_view.py and render_preview.py in the directory SilvaExtEPT/views/public/EPTalk in the views system. These two scripts are similar to the script edit.py discussed above. We have to set the viewer or previewer for out EPTalk document and then let the widgets service do the rendering:

      model = context.REQUEST.model
      version = model.get_viewable()
      if version is None:
         return "There is no public version of this EuroPython Talk."
      node = version.content.documentElement
      context.service_editor.setViewer('service_eptalk_viewer')
      return context.service_editor.getViewer().getWidget(node).render()

For that the widgets service can accomplish this task, we have to provide a render.pt in the directory 'SilvaExtEPT/widgets/top/eptalk/mode_view'::

OSI Certification Mark Public domain: no rights reserved
Public Domain

Scroll to top of page To table of contents for the site: acc-m Search the site: acc-f To site index: acc-i Find content in the site: acc-f No link