========================
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" and in that directory a file called "hello.py".
Inside, 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::
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)::
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 '
%s' % 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
"").::
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
uncaught that 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', )
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::
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::
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 at
the top of "hello.py"::
from zope.app import pagetemplate
Add the template to the MessageView class and change its "__call__" to use it::
template = pagetemplate.ViewPageTemplateFile('hello.pt')
def __call__(self):
return self.template()
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.
If we 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)
and add the line 'template="hello.pt"' to the "browser:page" tag in hello's
"configure.zcml" so that it looks like this::
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 your "hello.py" file::
class IHelloWorld(interface.Interface):
greeting = schema.TextLine()
subject = schema.TextLine()
And don't forget to import "interface" and "schema" at the top of "hello.py"::
from zope import interface, schema
Now that we have an interface describing our HelloWorld class, we can annotate
the class with the interface it implements. Add this line after "class
HelloWorld(object):"::
interface.implements(interfaces.IHelloWorld)
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(interfaces.IHelloWorld)
We want the form to render inside the ZMI, so we'll create a template named
"edit.pt" with these contents::
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).::
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")::
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::
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. 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.
One way to add a view to the ZMI's view menu is by adding "menu" and "title"
attributes to the "browser:page" directive.
The two "browser:page" elements should look like this now::
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.4 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)".
..
.. |CreativeCommons| image:: http://creativecommons.org/images/public/somerights20.png