Introduction to Zope 2 Application Development

Author: Paul M. Winkler, Revolution Health Group
Date: 2006-02-25
email:stuff@slinkp.com
images/rhg_logo.png

This talk distills into tutorial form the fundamentals of application development for the Zope 2 platform, with some best practices and practical advice along the way. It aims for the oft-noted gap between the online Zope Book (which covers only through-the-web development) and the Zope Developers' Guide (which is a reference, not a tutorial).

In an attempt to be forward-looking, the talk will use Zope 3 / Five features in preference to Zope 2 features whenever possible and appropriate for the novice Zope developer.

The first half of the talk will cover fundamentals, the second half will present a simple example application.

Prerequisites

Show of hands:

  • who has done some actual development with zope 2?
  • who has used zope, e.g. through-the-web templating, scripting, etc?
  • who has never even downloaded or run zope?

Scope

Following Jim's point about audiences: I am not speaking to the "non-interested" people. I'm part of the zope 2 crowd that's addressing those people less and less. I'm addressing people who want to know how to develop zope 2 applications that can scale to large complexity and developer teams larger than one. This is not really for the "quick website in an hour" crowd.

Out of scope

(many of these are covered in The Zope Book)

Also out of scope: Zope 3

The Zope 3 features described in this talk are only those that are available in Zope 2.9.

Zope 3 itself is a quite different beast.

What Zope Is

Features

All of these apply to both Zope 2 and Zope 3.

Z2 architecture

images/zope2_diagram.png

Arch: ZServer

images/zserver.png

never mind.

Yep, it serves HTTP and a few other protocols. Not relevant to this talk, let's just move along.

Arch: ZPublisher

images/zpublisher.png

Arch: ZPublisher, continued

images/zpublisher.png

Arch: Zope.App

images/app.png

Arch: Zope.App continued...

images/app.png

Arch: ZODB

images/zodb.png

Example "Traditional" Zope 2 class: A blog post

class Z2Post(SimpleItem):
   """A simple Post."""

   meta_type = 'Zope 2 Blog Post'
   title = ''
   body = ''

   manage_options=(
       {'label':'Edit',
        'action':'manage_main',
        },
       {'label':'View',
        'action':'index_html',
        },
       ) + SimpleItem.manage_options

   security = ClassSecurityInfo()  # Needed for declarative security.

continued...

Example class continued...

def __init__(self, id, title, body):
    self.id = id
    self.edit(title, body)

security.declareProtected(EDIT_PERMISSION, 'edit')
def edit(self, title, body, REQUEST=None):
    """ Edit a post. """
    self.title = title
    self.body = body
    if REQUEST is not None:
        msg = '%s updated' % self.getId()
        url = '%s/%s?manage_tabs_message=%s' % (self.absolute_url(),
                                                'manage_main', msg)
        REQUEST.RESPONSE.redirect(url)

security.declareProtected(Permissions.view, 'getTitle')
def getTitle(self, title):
    return self.title

continued...

Example class, the end (finally)

   security.declareProtected(Permissions.view, 'getRenderedBody')
   def getRenderedBody(self):
       return structured_text(self.body)

   security.declareProtected(EDIT_PERMISSION, 'getRawBody')
   def getRawBody(self, body):
       return self.body

   security.declareProtected(Permissions.view, 'index_html')
   index_html = PageTemplateFile('z2_forms/post_view', globals())

   security.declareProtected(Permissions.view_management_screens,
                             'manage_main')
   manage_main = PageTemplateFile('z2_forms/post_edit', globals())

InitializeClass(Z2Post)

Pretty hefty for such a simple post!

Lots of zope-specific stuff sprinkled into the class body.

SimpleItem is the most ironic name ever for a base class.

SimpleItem derives from Item, Base, object, Resource, LockableItem, EtagSupport, CopySource, Tabs, Traversable, Element, Node, Node Interface, Owned, UndoSupport, Persistent, Acquirer, RoleManager

"Item" alone has 18 methods.

Zope 2 Antipatterns

That Z2Post class demonstrates a lot of what's difficult about Zope 2.

We will try to avoid these antipatterns, but today (Zope 2.9.0) we still have to make some sacrifices.

Zope 3 Patterns

Zope 2 + Zope 3 = Five

come to the sprint!

Common Zope 2 base classes

Although it's good to not inherit from too much at once, you still need to know these when you see them, and for some of them there's no currently easy way to use a zope 3 idiom instead.

Content and Utilities

Informally, many persistent objects in Zope can be categorized as providing either content or functionality (utilities).

Best practice: implement either but not both in one class.

So e.g. utilities should not have state except perhaps local configuration, and content should not have behavior that could be generalized to other kinds of content.

A Moment of Z3 Zen

interfaces

images/uparrow.png

adapters

images/downarrow.png

interfaces

That's about it. The one big part of zope 3 not covered by this diagram is Utilities. Utilities are what we call components that provide functionality rather than content. They are stateless. Probably not going to get into utilities any further in this talk.

Interfaces

from zope.interface import Interface

class IPost(Interface):

    """A minimal blog post.

    """

    title = ''

    body = ''

    def edit(title, body):
        """Save changes to the title and body.
        """

Interfaces look like classes with nothing but docstrings.

notice there's no "self" in method signatures.

Purposes of Interfaces

A Simpler Blog Post

In the rest of this talk, we'll develop a minimal proto-blog.

We'll use some zope 3 technologies that are available in zope 2.9 today.

An Example Interface

A nice way to use an interface is as a schema. This allows us to specify constraints on attribute values:

from zope.interface import Interface
from zope.schema import Text, TextLine

class IPost(Interface):
    """A minimal blog post.

    """
    title = TextLine(
       title=u"Title",
       description=u"Displayed title of the post.",
       required=True,
       )
    body = Text(
       title=u"Body",
       description=u"Main body of the post.",
       required=True,
       )

You may notice that the edit() method is gone. We will instead use schema features to give us a nice way to modify the title and body.

A Simple Implementation

We need a class that explicitly implements our interface:

from zope.interface import implements
from zope.app.container.interfaces import IOrderedContainer
from zope.schema.fieldproperty import FieldProperty
from OFS.SimpleItem import Item, SimpleItem
from interfaces import IPost, IComment

class Post(object):
    """
    A base implementation of a blog post.
    """
    implements(IPost)

    # Using FieldProperty() for attributes allows
    # auto-validation whenever they are set.
    title = FieldProperty(IPost['title'])
    body  = FieldProperty(IPost['body'])

Making it persistent

To store data in the ZODB, a class must inherit from Persistent:

from persistent import Persistent

class Post(Persistent):
    """
    A persistent implementation of a blog post.
    """
    ...

register the class as addable in zope 2

<configure
    xmlns="http://namespaces.zope.org/zope"
    xmlns:five="http://namespaces.zope.org/five"
    xmlns:browser="http://namespaces.zope.org/browser"
    >
    <!-- First, tell the system about our IPost interface. -->
    <interface
        interface=".interfaces.IPost"
        type="zope.app.content.interfaces.IContentType"
      />
    <!-- Create a new permission for editing and managing posts. -->
    <permission
        id="slinkblog.ManagePosts"
        title="slinkblog: Manage posts"
      />
    <!-- Register blog.Post as a zope 2 addable "product".
         This does the equivalent of zope 2's registerClass().
     -->
    <five:registerClass
        class=".blog.Post"
        meta_type="Slinkblog Post"
        permission="slinkblog.ManagePosts"
        addview="post_addform"
     />
</configure>

First we have to declare the namespaces that are used in typical Five configuration.

Next, we register the Interface that was defined in our code.

"addview" is the name of the view that users will browse to in order to create a new one of these. We'll actually create this view in a later slide.

"meta_type" is a special attribute that Zope 2 uses to distinguish between different addable content types. This will show up in the "Add" menu in the ZMI.

In traditional Zope 2 development, information like views and meta_type and security declarations had to be written in python in the body of your class. In Zope 3 and Five, we try to keep such information out of the class and put it only in configuration. This makes classes leaner and reconfiguration much easier.

Content Types

<!-- Register blog.Post as a viewable zope 3 content type,
     with security protections. -->
<content
    class=".blog.Post">
  <require
      permission="zope2.View"
      interface=".interfaces.IPost"
      />
  <require
      permission="slinkblog.ManagePosts"
      set_schema=".interfaces.IPost"
      />
</content>

In zope 3 and Five, a content type is simply a component that can be added and displayed by the system. Here we declare that the zope2.View permission applies to viewing all attributes declared by the IPost interface, and with the "set_schema" attribute we declare that the slinkblog.ManagePosts permission applies to modifying all attributes and calling all methods defined by IPost.

This replaces all the security.declareProtected() calls we used to have to do. It's a lot shorter when you want to protect multiple attributes with the same permission.

Creating a Simple UI: Add Form

One nice thing about schemas is that they allow you to automatically generate simple forms.

<!-- Auto-generate an add view, i.e. an html form for
     adding Posts.  -->
<browser:addform
    schema=".interfaces.IPost"
    content_factory=".blog.Post"
    label="Add Slinkblog Post"
    name="add_blog_post"
    permission="slinkblog.ManagePosts"
    />

Unfortunately, in order for this to work, we have to sacrifice a few chickens to the spirits of Zope 2. First some ZCML.

<!-- in order to get to those add forms, we need the z3
     add view "+" to be traversable in zope 2 ObjectManagers,
     e.g. Folders. -->
<five:traversable class="OFS.Folder.Folder" />

Creating an Add View, continued

If we want to add Posts to standard Zope 2 folders, the class must inherit from SimpleItem :-(

class Post(Persistent, SimpleItem):
    ...

The reason for inheriting SimpleItem is that Zope 2 folders call a number of post-add methods on objects that you add to them; if those methods don't exist, you get errors.

This is not necessary in Zope 3. Hopefully, we can eliminate the SimpleItem requirement some day, and have the Five ZCML directives take care of this behind the scenes.

Making an Edit Form

 <!-- Auto-generate an edit view.
      Note that the "for" attribute is NOT optional
      in zope 2.9.0. -->
 <browser:editform
     schema=".interfaces.IPost"
     for=".interfaces.IPost"
     name="post_editform"
     label="Edit Slinkblog Post"
     permission="slinkblog.ManagePosts"
     />
<!-- Edit form won't be available in z2
     unless we declare the class traversable. -->
<five:traversable class=".blog.Post" />

Creating a View

Now we can add and edit Post instances. But we can't really view them!

Informally, there are two kinds of views:

A View Class

This example only concerns itself with the body:

class PostView(BrowserView):

   """Adapt an IPost into a view.
   """
   def render_body(self):
       """
       Render the body of an IPost as structured text.
       """
       # This stx implementation can only handle ascii.
       return structured_text(self.context.body.encode('ascii'))

The Template

<html
 xmlns:tal="http://xml.zope.org/namespaces/tal"
 xmlns:metal="http://xml.zope.org/namespaces/metal"
 >
<head>
 <title tal:content="context/title">The Post Title</title>
</head>
<body>

 <img src="++resource++post.png" style="float:left" />
 <h2 style="text-transform: capitalize;"
     tal:content="context/title">The Post Title</h2>

 <div tal:content="structure view/render_body">
   Body Text Goes Here
 </div>
 <div>
   <a href="@@post_editform"
   tal:attributes="href string:${context/@@absolute_url}/@@post_editform">
   Edit this post</a>
 </div>

</body>
</html>

The Configuration

First, create the view:

 <browser:page
  for=".interfaces.IPost"
  permission="zope2.Public"
  name="view_post.html"
  template="view_post.zpt"
  class=".blog.PostView"

/>

Next make it the default view for this class:

<browser:defaultView
  for=".interfaces.IPost"
  name="view_post.html"
/>
<five:defaultViewable class=".blog.Post" />

Summary

We have now reimplemented the complete old-style content class with much less gunk in the class body.

This means you can pretty easily take existing classes and hook them up to Zope 2 / Five just by writing configuration. If you need persistent instances, write a tiny subclass that mixes in Persistent. (And SimpleItem, for now, ugh.)

Demo

here I'll show this minimal blog running.

Adapters

Very flexible!

Adapter Example

You've already seen one! Views are adapters.

views are "multi-adapters", meaning they take more than one object and adapt them to yet another.

They adapt a context object (your class instance) and a request, and return something that can be published, i.e. something that implements the IView interface.

Views are set up by special ZCML. Other kinds of adapters can be applied manually by calling the interface that you want to adapt to, with the object to adapt as argument. In Python:

foo = IFoo(bar)

A bit of ZCML is needed to set this up:

<adapter provides="IFoo"
  for="IBar"
  factory="some_callable"
/>

Other Five features I didn't get around to

Wrapping up

some miscellaneous advice follows.

THE PERSISTENCE GOTCHA

Everyone hits this sooner or later.

This is why I call ZODB persistence "translucent" rather than totally transparent.

Watch out for mutable sub-objects! ZODB doesn't automatically know when these have changed.

Just don't store non-persistent mutable sub-objects (e.g. lists or dictionaries) as attributes of persistent objects.

If you do, you must sprinkle your code with calls to "self._p_changed = 1" to notify ZODB that those attributes have changed.

class Pets(Persistent):

   def __init__(self, id='my pets', names=[], kinds={}):
       self.id = id
       self.names = names
       self.kinds = kinds

   def setId(self, id):
       self.id = id  # No problem here.

   def addPet(self, name='', kind='cat'):
       self.names.append(name)
       self.kinds[kind].setdefault(0)
       self.kinds[kind] += 1
       # Must notify ZODB that self.kinds changed!
       self._p_changed = 1

Persistence Gotcha, continued

Solution

Better is to use PersistentMapping and PersistentList:

from persistent.list import PersistentList
from persistent.mapping import PersistentMapping

class Pets(SimpleItem):

   def __init__(self, names=[], kinds={'cats': 0, 'dogs': 0}):
       self.names = PersistentList(names)
   self.kinds = PersistentMapping(kinds)

   def addPet(self, name='', kind='cat'):
       self.names.append(name)
       self.kinds[kind].setdefault(0)
       self.kinds[kind] += 1
  • No more tedium
  • Less storage bloat: only the attributes are copied
  • Changing one attribute --> less chance of a conflict error.

See also: ZODB persistence rules: http://www.zope.org/Wikis/ZODB/FrontPage/guide/node3.html#SECTION000360000000000000000

Better solution: use BTrees.

BTrees handle much larger data structures; provide conflict resolution when multiple threads modify one mapping.

But not a 100% drop-in replacement. Very similar usage to dicts, but need to write a wrapper if you want list-like behavior.

from BTrees.IOBTree import IOBTree

   def __init__(self, names=[], kinds={'cats': 0, 'dogs': 0}):
       self.names = IOBTree([(i,n) for (i,n) in enumerate(names)])
       self.kinds = OIBTree(kinds)

See: http://www.zope.org/Wikis/ZODB/FrontPage/guide/node6.html#SECTION000630000000000000000

Random Things You Will Want

Q&A (5+ minutes)