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 >>> from Products.ATAudio.ATAudioFolder import ATAudioFolder >>> ATAudioFolder('bar') 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 ... :: 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:: **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', , ) 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: 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**:: 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**:: 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 !**