Zope 3 Quick Start Guide

Introduction

This Quick Start assumes you are familiar with Python, Subversion, and the generalities of the web (HTML, servers, etc.).

Download Zope 3

This quick start was written using a pre-3.2 version of Zope. Later versions may also work. Also note that you need Python 2.4 and Subversion in order to check out the Zope 3 trunk.

To get a version of Zope you can use for this tutorial follow these steps (substitute back slashes for forward slashes in file names for Windows):

mkdir zope3_quick_start
cd zope3_quick_start
svn checkout svn://svn.zope.org/repos/main/Zope3/trunk zope3

Install

Linux

Run these commands (note the trailing dot is part of the command):

cd zope3
python setup.py build_ext -i install_data --install-dir .
cd ..

Windows

If you have an appropriate compiler installed, you can run the same commands as listed above for Linux.

Otherise run these commands (note the trailing dot is part of the command):

cd zope3
python setup.py install_data --install-dir .
cd ..

And then download Tim Peter's Windows binaries for the Zope 3 C code and install according to the instructions on the page (make sure you get the version for Python 2.4): http://www.zope.org/Members/tim_one/

Make an Instance

Zope 3 uses "instances" to contain the information relevant to a server (or set of servers). Let's make an instance we can use to develop our app.

python zope3/bin/mkzopeinstance

You'll go through this dialog with the utility:

Please choose a directory in which you'd like to install Zope
'instance home' files such as database files, configuration files,
etc.

Directory: instance

Please choose a username for the initial administrator account.
This is required to allow Zope's management interface to be used.

Username: admin

Please provide a password for the initial administrator account.

Password:
Verify password:

After you've made your instance, change to the instance directory and run:

bin/runzope

You should see something like this:

------
2005-09-28T20:40:11 INFO PublisherHTTPServer zope.server.http (HTTP) started.
        Hostname: my-computer
        Port: 8080
------
2005-09-28T20:40:11 INFO PublisherFTPServer zope.server.ftp started.
        Hostname: my-computer
        Port: 8021
------
2005-09-28T20:40:11 INFO root Startup time: 5.538 sec real, 3.120 sec CPU

The ZMI

If you open a web browser and go to http://localhost:8080 you'll see the ZMI (Zope Management Interface).

Go ahead and click the "Login" link at the upper right. Enter the user name and password you gave when creating the instance. Now click on [top] under "Navigation" on the right. Play around with adding some content objects (the Zope 3 name for instances that are visible in the ZMI). Note how content objects can be arranged in a hierarchy by adding "folders" which are special content objects that can hold other content objects.

There is nothing special about the ZMI, it is just the default skin for Zope 3. You can modify it to your liking, or replace it entirely.

When you're done exploring with the ZMI, go back to the window where you typed "runzope" and see that each request from your browser was displayed there as it happened. Press Control-C to stop Zope.

Hello World

We'll write a simple "Hello World" program.

Our First Content Object

We need to go to the lib/python directory of our instance and create a directory called "hello" mkdir instance/lib/python/hello and create a directory called and in that directory a file called "hello.py".

Inside "hello.py", type this text:

import persistent

class HelloWorld(persistent.Persistent):
    greeting = 'Hello'
    subject = 'world'

This differs from a "normal" Python class in that it derives from persistent.Persistent. The "Persistent" base class will note any changes made to our instances and make sure they are written to the object database (ZODB). There are a couple other things you should know about persistence, but we'll look at those later.

We'll also need an empty file named "__init__.py" for the "hello" directory to be treated as a package by Python.

Registering the Content Type

We need to let Zope 3 know that we want to see our content type in the "add menu" in the ZMI. We need to create a "configure.zcml" file in our "hello" directory and put a "browser:addMenuItem" directive there.

ZCML (Zope Configuration Markup Language) is an XML language for configuring Zope components. It is always possible substitute the appropriate Python code for a particular piece of ZCML, but ZCML is usually much more succinct, composable, and maintainable.

The first thing in the file should be a "configure" tag. All ZCML files begin with "configure" tags which contain the other tags. Zope makes use of XML namespaces (the "xmlns" attribute) to identify the context for each tag. The most fundamental tags are in the http://namespaces.zope.org/zope namespace. Another namespace we'll use here is for browser-specific directives. The "configure" tag also includes the (optional, but highly recommended) "i18n_domain" attribute which describes the internationalization domain which applies to the human-readable text in this ZCML file (like "title" below).

So our "configure.zcml" file looks like this:

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

  <browser:addMenuItem
      class=".hello.HelloWorld"
      title="QS Hello World"
      permission="zope.ManageContent"
  />

</configure>

The "class" attribute specifies the module path for the class, a leading dot means to make the import relative to the package containing the ZCML file. Therefore in this case Zope will import the hello module, then import "HelloWorld" from that module.

The "title" attribute provides the title to display in the add menu (we prefix the title with "QS" because Zope comes with another "hello world" object out of the box, so be sure to use the right one).

The "permission" attribute is used to describe what permission is required for a person to be able to add one of these objects. The "zope.ManageContent" permission means that the user can add, remove, and modify content (the "admin" user you created while making the instance is one such user).

We need to tell Zope to read our ZCML file, and the easiest way to do that is to put a "slug" in the instance/etc/package-includes directory. A "slug" is a ZCML file that just includes another file. Here's what our slug should look like (save it as "hello-configure.zcml"):

<include package="hello" />

Now if we start Zope back up, we can go to the ZMI and add our content type by clicking on "QS Hello World" and entering a name for our object; name it "hello". If we click on our object after adding it, we'll just see a generic view that tells us what class the object is ("hello.hello.HelloWorld").

A New View

We want to provide a more interesting view. Our view will use the greeting and subject from the content object and construct a message from it.

A "view" is simply a way of rendering a particular content object. We'll create a simple view that renders our content object to HTML. Views are informed about their content object by being assigned a "context" attribute. In other words, if you're writing code for your view class you can access the content object you're providing a view of as "self.context".

Here's a view class to add to "hello.py":

class MessageView(object):

    def message(self):
        return '%s %s!' % (self.context.greeting, self.context.subject)

    def __call__(self):
        return '<html><body><big>%s</big></body></html>' % self.message()

So, why do we have views? There are a few of reasons:

  • views are a way to separate the data from the presentation. Content objects represent the problem domain objects in our application; the views represent implementation objects.
  • you might want more than one view of a single content object (HTTP, FTP, JSON, XML, etc.)
  • you might also want to apply the same view to different content objects (like the built-in "Introspector")

Configuring the View

We have to configure the view so it is available for our content object. This is done in ZCML so we (or people using our code) can add, remove, or substitute views later without changing the code.

Add the following to the end of hello/configure.zcml (but before "</configure>"):

<browser:page
  for=".hello.HelloWorld"
  name="index.html"
  class=".hello.MessageView"
  permission="zope.Public"
  />

Now restart zope (Control-C, bin/runzope) and visit the view in your browser at this URL: http://localhost:8080/hello.

Uh oh! What does "A system error occurred." mean? Whenever an exception is raised but not caught the "system error" message will be generated.

You can go to the console window you have Zope running in and look at the line that starts "ForbiddenAttribute". The rest of the line should look like this:

('greeting', <hello.hello.HelloWorld object at 0xb687182c>)

That means that we tried to access an attribute (named "greeting") that we don't have permission to see. That's because Zope 3 comes with an extensive security system which assumes that any attribute is not accessible unless declared otherwise.

Security

So, to say that anyone can see the "greeting" (and just so we don't get another ForbiddenAttribute exception, "subject" too) we'll add this to hello/configure.zcml:

<content class=".hello.HelloWorld">
  <require
      permission="zope.Public"
      attributes="greeting subject"
      />
</content>

In English, this says: Require the zope.Public permission for a user to be able to read the "greeting" and "subject" attributes of HelloWorld objects.

Any user (even anonymous ones) have the zope.Public permission, so because our view only uses those two attributes, anyone should be able to see it. Restart Zope and go back to http://localhost:8080/hello and you will see the results of the view.

Sidebar: So why doesn't Zope 3 just assume every attribute is public until told otherwise? History has shown that writing secure software can be difficult. Therefore it is best to make everything secure by default and make conscious decisions to open up security when necessary. The alternative of making everything public by default and having to "add in" security is not practical.

Zope Page Templates

You'll note that our view doesn't "cooperate" with the ZMI, the HTML we generated was returned untouched to the browser. Sometimes that is exactly what you want, but sometimes you want to include standard headings, menus, etc. like the ZMI does.

The way you do that in Zope 3 is by using page templates. The main ZMI page template provides a way for us to generate content for it to display using ZPT macros. Create a file called "hello.pt" in our "hello" package. Make the file look like this:

<html metal:use-macro="context/@@standard_macros/view">
<body>
<div metal:fill-slot="body">
  <big tal:content="python: view.message()"></big>
</div>
</body>
</html>

Note how the template calls the view's "message" method to get the contents of the "big" tag and then fills the ZMI's "body" slot with the "big" tag (or anything else we put in the slot).

Now we need to wire up the template so it will be used. Add this import to the top of "hello.py":

from zope.app import pagetemplate

And add this new __call__ to the "MessageView" class (deleting the old "__call__" method):

template = pagetemplate.ViewPageTemplateFile('hello.pt')

def __call__(self):
  return self.template()

You can now restart Zope and refresh http://localhost:8080/hello and you'll see the new template in action.

ZCML and Templates

Because associating templates with views is so common and to make it easier to substitute one template with another without having to change the view code, there is a ZCML shortcut for doing it.

So, we can remove the three lines we added above so our view class just looks like this (and remove the "from zope.zpp import pagetemplate" line):

class MessageView(object):

    def message(self):
        return '%s %s!' % (self.context.greeting, self.context.subject)

Then add the line 'template="hello.pt"' to the "browser:page" tag in hello's "configure.zcml" so that it looks like this:

<browser:page
    for=".hello.HelloWorld"
    name="index.html"
    class=".hello.MessageView"
    permission="zope.Public"
    template="hello.pt"
    />

Now, if you restart the server you can refresh http://localhost:8080/hello and see that we get the same results.

An Edit View

Now we want to be able to edit the subject and greeting attributes of our objects through the web. We could go through the effort of creating HTML "form" tags and processing the responses, making sure to validate the user input, but Zope makes it easier.

Interfaces

First, we need to know what data the form will collect. Zope uses "interfaces" with "schema fields" to do that. Add this simple interface to the begining of "hello.py" (after the imports):

from zope import interface, schema

class IHelloWorld(interface.Interface):
    greeting = schema.TextLine()
    subject = schema.TextLine()

Now that we have an interface describing our HelloWorld class, we can annotate the class with the interface it implements. Add an "implements" call to the HelloWorld class definition so that it looks like this:

class HelloWorld(persistent.Persistent):
    interface.implements(IHelloWorld)
    greeting = 'Hello'
    subject = 'world'

That line lets other parts of the system know what interface(s) our instances promise to provide.

Forms

Now we can add an edit view to "hello.py". Instead of building an HTML form "by hand", we'll use a standard zope package called "formlib". We have to import "formlib" at the top of "hello.py" like so:

from zope.formlib import form

And then add our edit view to the end of the file:

class EditView(form.EditForm):
    form_fields = form.Fields(IHelloWorld)

We want the form to render inside the ZMI, so we'll create a template named "edit.pt" with these contents:

<html metal:use-macro="context/@@standard_macros/view">
<body>
<div metal:fill-slot="body" tal:content="view">
</div>
</body>
</html>

Now all we have to do is to tell Zope about this new view, and its template. So we add this to the "hello" package's "configure.zcml" file (this is the same data we provided for our first view):

<browser:page
    for=".hello.HelloWorld"
    name="edit.html"
    class=".hello.EditView"
    permission="zope.ManageContent"
    template="edit.pt"
    />

Now, if we restart Zope and go to http://localhost:8080/hello/edit.html we'll see an edit form.

Schemas

When we made the interface IHelloWorld we set the attributes to "schema.TextLine". That is a way of describing what that attribute holds. There are other schema fields, and they can also get more descriptive. Let's flesh out our IHelloWorld schema a bit. First we'll add titles and descriptions:

class IHelloWorld(interface.Interface):
    greeting = schema.TextLine(
        title=u'Greeting',
        description=u'The salutation used to greet the subject.')

    subject = schema.TextLine(
        title=u'Subject',
        description=u'Who or what is being greeted.')

If we restart Zope and refresh the edit form we'll see the results of our changes: now the text fields have titles, and if we click (and hold) the mouse button on the field title, we'll see the description.

There are several other settings for fields including: required/optional, constraints (max/min, match regular expression, etc.), complex data types (sequences, mappings, etc.).

More Security

Change the value of one of the fields and press "Apply". Another exception! Click the back button and follow the "Errors" link. See the "ForbiddenAttribute" error?

What happened was this: you tried to modify an attribute that you don't have permission to modify. Remember the ZCML for the "greeting" and "subject" attributes we added before (in "configure.zcml"):

<content class=".hello.HelloWorld">
  <require
      permission="zope.Public"
      attributes="greeting subject"
      />
</content>

That only let us read those attributes, not write them. Now we'll let anyone with "zope.ManageContent" permission (like the "admin" user) change the attributes. Instead of naming the attributes one-by-one like we did before, we can leverage the fact that the interface already knows about them, so we'll make the "content" element look like this:

<content class=".hello.HelloWorld">
  <require
      permission="zope.Public"
      interface=".hello.IHelloWorld"
      />
  <require
      permission="zope.ManageContent"
      set_schema=".hello.IHelloWorld"
      />
</content>

We replaced the "attributes" attribute with "interface", and added a "require" directive indicating that only users with "zope.ManageContent" (like "admin") can actually change the values. Now you can restart Zope and change the values.

Adding Menu Entries

Now click on "[top]" and then click on "hello" and you'll see that our views aren't displayed; Zope doesn't assume that we want them to be listed in the ZMI view menu (along with "Introspector") just because they exist.

In this case we do want them displayed, so we'll register them as part of the "zmi_views" menu. We could register them using only Python, but it's easier to do with ZCML (and also means they'll be overridable by someone else if they don't like our menu entries).

The ZMI view menu works by associating an interface with the views that should be listed for it, therefore instead of the "for" attribute pointing directly to our content class, we need to use the IHelloWorld interface instead. This has the added benefit that any content object that implements the IHelloWorld interface will also get our "Message" and "Edit" views! This also means that it would be easy for a third party to add views to our content objects.

We'll add "menu" and "title" attributes to the "browser:page" directive in "configure.zcml". The two "browser:page" elements should look like this now:

<browser:page
    for=".hello.IHelloWorld"
    name="index.html"
    class=".hello.MessageView"
    permission="zope.Public"
    template="hello.pt"
    menu="zmi_views"
    title="Message"
    />

<browser:page
    for=".hello.IHelloWorld"
    name="edit.html"
    class=".hello.EditView"
    permission="zope.ManageContent"
    template="edit.pt"
    menu="zmi_views"
    title="Edit"
    />

Restart zope and go to http://localhost:8080/ and click on our "hello" object. You'll see that both of our views are listed and can be clicked on. The views are listed in order of "specificity" and then order of definition in the ZCML. Our views are more specific because they are registered for only IHelloWorld, where as the Introspector view is registered for the most basic interface "Interface".

More Persistence

So far we haven't worried much about persistence, everything has just magically worked. That's because we've been using attribute assignment to mutate our instances, so it has been easy for Zope (actually ZODB) to know when our objects change, but for more complex object models that isn't always the case.

Fortunately we only have to do a couple of simple things and Zope takes care of the rest. If you want a persistent list or dict, use persistent.list.PersistentList and persistent.dict.PersistentDict, (respectively) instead of the built-in objects.

If you want to use a built-in or other instance for which there are no persistent versions available (and you can't make your own), all you have to do is make sure the persistent object to which it is attached knows that its value has changed. There are two ways to do that. First, after mutating the object re-assign it to it's attribute:

def doSomething(self):
    self.some_mutable.mutate()
    self.some_mutable = self.some_mutable

Alternatively you can directly set the _p_changed attribute:

def doSomething(self):
    self.some_mutable.mutate()
    self._p_changed = True

Note that you only have to think about persistence for objects that are actually persistent, not for things like views. In practice even very large Zope 3 applications won't have to do much of this.

Version

This is version 0.5 of the Zope 3 Quick Start Guide.

License

CreativeCommons This work is licensed under a Creative Commons Attribution-ShareAlike 2.5 License.

Attribution in derivative works should be given as "Benji York (http://benjiyork.com)".