Author: | Paul M. Winkler, Revolution Health Group |
---|---|
Date: | 2006-02-25 |
email: | stuff@slinkp.com |
Latest version is at http://slinkp.com/~paul/pycon_2006/z2/notes.html
s5 slides version at http://slinkp.com/~paul/pycon_2006/z2/slides.html
source code for the demo blog at http://slinkp.com/~paul/pycon_2006/z2/slinkblog.tgz
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.
Familiarity with object-oriented programming in Python
Familiarity with HTML
Basic familiarity with XML
Familiarity with "what Zope is"; see e.g. first four chapters of the Zope Book: http://www.plope.com/Books/2_7Edition
... don't worry, those are quick reads.
This talk will be much easier if you've at least downloaded and played with Zope 2
Show of hands:
(many of these are covered in The Zope Book)
Through-the-Web (TTW) development ... scripts, ZClasses, etc.
DTML
Advanced ZPT (METAL)
external data sources (RDBMs, LDAP...)
i18n / l10n
optimization
security, mostly :-(
Users, roles, and permissions are covered pretty well in the Zope Book. Custom Zope 2 user folders and zope 3 pluggable auth are interesting but out of scope.
testing :-(
administration (installation, deployment, scaling...)
indexing (ZCatalog)
acquisition
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.
All of these apply to both Zope 2 and Zope 3.
never mind.
models request data as a mapping
(with some attribute namespaces for disambiguation)
often (in zope 2) spelled "REQUEST", but sometimes "request" :(
in zope 3, it's always spelled "request".
requests are derived from ZPublisher.BaseRequest.BaseRequest, specialized for the protocol, e.g. HTTPRequest.
request has a RESPONSE attribute, derived from ZPublisher.BaseRequest.BaseResponse.
Traversal and publishing
Zope 2 publishing rules are simple but a bit odd: if object is callable (i.e. __call__()), call it; or if it has an index_html() method, call that. First argument to either must be REQUEST. Also, the object must have a docstring or zope 2's publisher refuses to publish it.
There's a bit more to it; see frighteningly long method in BaseRequest.traverse() (in lib/python/ZPublisher/BaseRequest.py)
a running zope process provides a single Zope.App instance.
is a ZODB application:
Extensible via Products
A "Product" is Zope 2 jargon for a python package which typically contains some code to register it with the Zope app.
Typically you drop these into the "Products" subdirectory of your Zope instance. Zope then automatically loads them on startup.
Usually, a Product provides at least one persistent class whose instances can be stored in the ZODB.
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...
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...
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)
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.
That Z2Post class demonstrates a lot of what's difficult about Zope 2.
Ad-hoc configuration
lots of boilerplate python code ("dead chickens").
Features provided by mixing in many base classes
Huge inheritance hierarchy
Bloated, inflexible classes
Design is made up of many implicit / vague interfaces
Unclear boundary between requirements and convention. This relates to previous point: "I'm inheriting from some class, but I'm not sure why I need to." These things are not spelled out unless you spend a lot of time reading the source.
Only pluggable by subclassing
A classic example is adding tabs to the Zope Management Interface. You can only add tabs for classes that you write; to add tabs for third-party classes, you have to monkeypatch or maintain a fork.
(skip if short on time) In Zope 3, the ZMI gives you something to play with out of the box but it's totally reconfigurable and apparently you can get rid of it completely and build all UI from scratch (by removing some things from a config file).
Existing (persistent) instances hard to extend
that's somewhat overstated, but the three usual techniques are:
We will try to avoid these antipatterns, but today (Zope 2.9.0) we still have to make some sacrifices.
"Design to an interface"
literally, we implement Interfaces
Features provided by small classes: "Components"
Components are pluggable by:
Existing instances can get new features, via adapters
Don't use python code for configuration
There is currently much discussion on the zope 3 developers' list about what does and doesn't belong in ZCML and in python. There is general agreement to simplify ZCML; details being hashed out now.
The concensus seems to be that policy belongs in ZCML but machinery belongs in Python, which leaves us arguing about what counts as policy and what counts as machinery...
come to the sprint!
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.
Persistent
Acquisition.Implicit
AccessControl.Role.RoleManager
SimpleItem (inherits from all the above and lots more)
almost all third-party Products provide classes that inherit from SimpleItem.
Today we still have to do this.
ObjectManager
For containing persistent sub-objects. e.g. "Folder".
PropertyManager
For storing arbitrary metadata on objects.
You get a simple ZMI edit form for free. Sometimes too flexible: if users can edit properties, they can also add and delete properties.
The Zope 3 idiom to use instead is Annotations, but I'm not sure how well supported they are in Five yet.
CatalogAware
For automatically updating ZCatalog index data when the object is modified.
in Zope 3, we instead use an events framework; much more general and flexible. I won't get into this topic.
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.
interfaces
adapters
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.
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.
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.
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'])
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. """ ...
<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.
<!-- 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.
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" />
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.
<!-- 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" />
Now we can add and edit Post instances. But we can't really view them!
Informally, there are two kinds of views:
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'))
<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>
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" />
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.)
here I'll show this minimal blog running.
Very flexible!
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" />
zope.formlib and zc.form, nicer ways to do views and forms
I haven't used these, but there's an interesting post from Jeff Shell on the z3-dev mailing list. http://mail.zope.org/pipermail/zope3-dev/2006-February/018273.html
some miscellaneous advice follows.
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
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
See also: ZODB persistence rules: http://www.zope.org/Wikis/ZODB/FrontPage/guide/node3.html#SECTION000360000000000000000
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
Zope 2 (not 3, not Five):
Five and Zope 3:
General:
options to enable in zope.conf:
debug-mode on verbose-security on security-policy-implementation python
ExternalEditor: http://plope.com/software/ExternalEditor/ - use your favorite editor to edit Zope objects
DocFinderTab: http://www.zope.org/Members/shh/DocFinderTab - browse an object's inheritance hierarchy and docstrings through the ZMI
Zope Profiler: http://www.dieter.handshake.de/pyprojects/zope/#bct_sec_3.8
ZopeTestCase: http://www.zope.org/Members/shh/ZopeTestCaseWiki/FrontPage very useful for setting up zope 2 application "init" tests and integration tests.