Developing Plone Products Using Zope 3 Technologies: An Introduction

Rocky Burt's presentation from Plone Conference 2006.

text/plain Developing Plone Products Using Zope 3 Technologies.txt — 13.4 KB

File contents

Developing Plone Products Using Zope 3 Technologies: An Introduction
********************************************************************

About this Talk
===============

Plone developers are already productive and innovative. But the new Zope 3
component architecture, which is part of Plone 2.5 and above, makes it
even easier and faster to build powerful add-on Products that are both
manageable and reusable.

This in-depth tutorial, suitable for folks who have some experience
building add-on Products for Plone but are not yet comfortable with 
Zope 3, will demonstrate how you can take an existing Plone Product and
refactor it to use Zope 3 technology and techniques.

About the Speaker
=================

- Founder of ServerZen Software, focusing on Python, Zope, and Plone
  based technologies (http://www.serverzen.com)
- Experienced Java/J2EE developer
- Plone framework team member
- Major contributor to Plone4Artists (http://www.plone4artists.org)

Getting Started
===============

The Zope 3 Component Architecture
---------------------------------

The Zope 3 Component Architecture enables building small components to
construct larger applications using Python.

ATAudio
-------

A great way to demonstrate the ways the component architecture can be used
to augment existing Plone development styles is to use a real life example.

Using rocky-ploneconf2006-tutorial branch of ATAudio.

Testing
=======

ATAudio is missing any sort of tests at all.  It's bad to write code
without tests so we're going to have to build tests as we go.

- if a product is untested, it's unproven
- if a product is unproven, it's not going to get deployed

Doctests
  Documenting and testing simultaneously.

Testing: tests.py
=================

::

  import unittest
  from zope.testing import doctest

  def test_suite():
      return unittest.TestSuite((
          doctest.DocFileSuite('audio.txt',
                               package='Products.ATAudio',
                               optionflags=doctest.ELLIPSIS),
          ))

  if __name__ == "__main__":
      unittest.main(defaultTest='test_suite')

Testing: audio.txt
==================

::

  Audio
  =====

  We start of by ensuring we can actually instantiate our 
  content classes.

    >>> from Products.ATAudio.ATAudio import ATAudio
    >>> foo = ATAudio('foo')
    >>> foo
    <ATAudio ...>
    
    >>> from Products.ATAudio.ATAudioFolder import ATAudioFolder
    >>> ATAudioFolder('bar')
    <ATAudioFolder ...>

Interfaces
==========

The first step to defining what functionality should be provided by
the ATAudio content class is to define the interface.  Those familiar
with Zope 2 interfaces should understand the basics here.  The conventions
used by Zope 3 interfaces are quite similar.

It is standard Zope 3 convention to define all interfaces for a package
project or product in a toplevel ``interfaces`` module or package.  In this
case we define the ``interfaces`` module...

Interfaces: interfaces.py
=========================

::

  from zope import interface

  class IATAudio(interface.Interface):
      """An interface for handling an ATAudio content type.
      """
    
      def getAudioURL(media_server=None):
          """Get the URL for the audio file. Optionally we 
          can pass in the url for a media server which is 
          assumed to be a regular web-server with a similar 
          directory structure to the zope instance.
          """

      # snip snip ...

Views
=====

Zope 3 views (commonly, but erroneously, referred to as Five views)
provide a nice way of separating presentation from business logic.

But Why?
--------

Separation of concerns is a good thing.  

- keeping business logic out of the page templates and in python code 
  helps keep page designers sane

- python code can be tested at a unit level to ensure a level of 
  quality whereas page templates cannot unit tested

Views: A Comparison
===================

Similaries between standard Plone skin templates/scripts and Zope 3 view
components:

============= ============ ======== ==========
Tech          Presentation Logic    Security
============= ============ ======== ==========
Plone         zpt          py class .metadata
Zope 3 Views  zpt          py class ZCML
============= ============ ======== ==========

But Zope 3 view code is regular python code, not "py scripts".

Views: Usage
============

There are three common configurations:
  1. just page template
  2. just python class
  3. python class and page template combo

The simplest configuration is to just start with the page template.  We
begin by copying the audio_view.pt file located in the skins directory
to the root of the product (after making a small adjustment) and hooking 
it up with some ZCML...

Views: ZCML
===========

Kind of an enhanced .metadata file ...

::

    <configure
        xmlns="http://namespaces.zope.org/zope"
        xmlns:browser="http://namespaces.zope.org/browser">

      <browser:page
          name="view-with-z3.html"
          for=".interfaces.IATAudio"
          permission="zope2.View"
          template="audio.pt"
          />

    </configure>

Views: Adding Logic
===================

Next we write some python code for the view class.  First step is the
define the view class and then wire it up with ZCML by adding a class attribute::

  <browser:page
      name="view-with-z3.html"
      for=".interfaces.IATAudio"
      permission="zope2.View"
      template="audio.pt"
      class=".browser.AudioView"
      />

**Don't forget that view class docstring!**

Views: Adding Logic
===================

We begin by weeding out one use of a ``python:`` (ugh) TAL expression,
where we retrieve the object size.

::

    class AudioView(object):
        """A view for our audio.
        """

        def __init__(self, context, request):
            self.context = context
            self.request = request

        def pretty_size(self, size=None, obj=None):
            # snip snip...

Views: Testing Logic
====================

We defined a view component to display the view information for an audio
item, lets make sure it works.  The ``pretty_size`` method seems like a prime target, lets start with it.

::

    >>> from Products.ATAudio.browser import AudioView
    >>> view = AudioView(None, None)
    >>> view.pretty_size(size=12345)
    '12.1 zkB'

    >>> view.pretty_size(1)
    '1 zB'

Interfaces: Schema
========================

A schema is a Zope 3 interface that defines all of the fields an object
has.  It also defines metadata about those fields like whether they should
be read-only, have a default value, etc.

Comparing to Archetypes notion of a schema, a Zope 3 schema will only
define the type and other related metadata of a field.  It will not
define what storage should be used nor will it define the widget to use
for presentation.

Let's define our first schema...

Interfaces: interfaces.py
=========================

::

  from zope import schema
  class IAudio(interface.Interface):
    """A pythonic representation of an object that 
       contains audio information.
    """
    title = schema.TextLine(title=u'Title')
    description = schema.Text(title=u'Description', 
                              required=False)
    year = schema.Int(title=u'Year', required=False)
    frequency = schema.Int(title=u'Frequency', 
                           readonly=True)
    length = schema.Int(title=u'Length in seconds', 
                        readonly=True)
    url = schema.TextLine(title=u'URL', readonly=True)

Interfaces: Schema
==================

Why Attributes?
---------------

The preferred manner to access the information that makes up an object
in Python is to use attributes.  Common getter/setter and accessor/mutator
patterns that other languages use are not normally publically exposed
in Python.  And yes, Archetypes made that mistake.

Formlib
=======

Form Generation From A Schema
-----------------------------

A Zope 3 schema provides most of the information required to setup
a very basic form.  Formlib is able to take a schema and by using useful
defaults can generate a form automatically.

Let's define an audio *edit* form that is based directly on our IAudio
schema.  This will be similar to what ``base_edit`` does for us based
on the Archetypes schema of a content type.

Formlib: Audio Edit Form
========================

::

    from zope.formlib import form

    class AudioEditForm(form.EditForm):
        """A form for editing IAudio content.
        """

        form_fields = form.FormFields(interfaces.IAudio)

Of course trying to display this in our browser yields::

  TypeError: ('Could not adapt', 
              <ATAudio at /plone1/audio-items/05_-_Cmon_Everyone.mp3>,
              <InterfaceClass Products.ATAudio.interfaces.IAudio>)

Adapters
========

What the previous error was telling us was that formlib was trying
to get ``IAudio`` information out of an object that had no way of 
providing it.  Afterall, the only interface that our ``ATAudio`` content
class knows about is ``IATAudio`` which is simply not compatible 
with ``IAudio``.

So now we need to retrieve the information from an object which provides
``IATAudio`` in an ``IAudio`` manner.  Lets get adapting.

Adapters: IAudio
================

::

    from zope import component, interface
    from Products.ATAudio import interfaces

    class ATAudioAudio(object):
        """An IAudio adapter for IATAudio.
        """

        interface.implements(interfaces.IAudio)
        component.adapts(interfaces.IATAudio)

        # snip snip ...

    # and the ZCML: <adapter factory=".audio.ATAudioAudio" />

Adapters: IAudio
================

One quick observation will note that modifying the 'title' field on the
new formlib-based edit form shows that the navtree portlet on the left-side
isn't updated.  A sure sign that the catalog entry for this content item
isn't being updated.

Also when update the year information with the form, it's not being
saved back into the embedded ID3 tags of the MP3.

How does Zope 3 handle these use cases?

Events
======

Zope 3 events is a very basic framework for hooking actions up to be
performed whenever something of interest happens.  There are a certain 
set of base event types that Zope 3 has defined for common use.  
Some of these are:

- ``zope.app.event.interfaces.IObjectCreatedEvent``
- ``zope.app.event.interfaces.IObjectModifiedEvent``
- ``zope.app.container.interfaces.IObjectAddedEvent``

All of these besides the first one are available with Zope 2.9 and 
Archetypes 1.4.1 (Plone 2.5.1) today.

Events: Subscribing
===================

Both Archetypes 1.4.1 (and higher) and formlib are smart enough that
everytime someone clicks on the "save" button of an edit form it fires
an ``IObjectModifiedEvent`` that can be subscribed to.  So this means
all we have to do is listen for that event.

**audio.py**::

    def update_catalog(obj, evt):
        obj.reindexObject()

    def update_id3(obj, evt):
        obj.save_tags()

Events: Subscribing
===================

**configure.zcml**::

  <subscriber
      for=".interfaces.IATAudio
           zope.app.event.interfaces.IObjectModifiedEvent"
      handler=".audio.update_catalog"
      />

  <subscriber
      for=".interfaces.IATAudio
           zope.app.event.interfaces.IObjectModifiedEvent"
      handler=".audio.update_id3"
      />

Utilities
=========

Utilities allow reusable business logic to be wrapped up as a component
and used as necessary.  Similar to the tool concept in CMF, they can
provide whatever functionality deemed appropriate.

``ATAudio`` provides the logic for doing some migration directly into
the content class.  This isn't the appropriate place to be so we'll 
instead move that out into a utility.

Utilities: migration
====================

**migration.py**::

    from zope import interface
    from Products.ATAudio import interfaces
    class ATAudioMigrator(object):
        interface.implements(interfaces.IATAudioMigrator)

        def migrate(self, audio_file):

**configure.zcml**::
    <utility
        provides=".interfaces.IATAudioMigrator"
        factory=".migration.ATAudioMigrator" />

Advanced Concepts
=================

Local Components
  Zope 2.10 provides the ability to have *local* utilities and adapters.
  This essentially means that registered components can be defined locally
  (within an ISite ... which for us means a Plone site).  A local utility
  in this sense is a near identical concept as a CMF tool.

Plone4ArtistsAudio
==================

http://www.plone4artists.org/products/plone4artistsaudio

- Took many of the concepts demonstrated here to create an entirely
  new audio product that doesn't use it's own content type

- Uses different *named* adapters to provide functionality specific to
  the mime type (supports mp3 and ogg currently)

Further Reading
===============

- ServerZen blog @ http://www.serverzen.net
- Zope 3 wiki @ http://zope3.zwiki.org/FrontPage
- Appetizers @ http://worldcookery.com/Appetizers
- Paperback: Web Component Development with Zope 3 by 
  Philipp von Weitershausen
- Paperback: Zope 3 Developer's Handbook by Stephan Richter

**Don't forget to pre-order your second edition of Web Component
Development with Zope 3 !**