Writing Zope Products
Last month, we continued our discussion of the Zope web development environment by installing and examining Zope products. As we saw, each product is actually a Python object module that can be instantiated one or more times on a Zope server. Hundreds of products are available for Zope, ranging from a small and lightweight fortune-teller to the large and impressive content management framework (CMF).
Many Zope administrators are content to install and use the products that they can download from the Web. And indeed, there are enough products to satisfy most basic sites; whatever you cannot do with an existing product is often simple enough to create in DTML, Zope's Dynamic Template Markup Language (described in the February 2002 issue of LJ).
But as easy and straightforward as it may be to do many tasks in DTML, it is neither as complete nor as flexible as Python. And while the addition of PythonScripts (and PerlScripts!) in Zope has certainly reduced the need to write products for many medium-sized tasks, most Zope hackers eventually find themselves writing a product of some sort.
This month, we look at how to build a simple Zope product that we can then integrate into our site. As you will see, it is quite easy to create a Zope product that integrates well into the rest of this environment.
At its core, a Zope product is a Python module. The product (as we saw last month) is installed into the lib/python/Products directory under the main Zope directory. Zope looks for products when it is started (or restarted), which means that you must start or restart Zope after installing a new product.
We can instantiate an installed product as many times as we like, placing each instance somewhere in the web hierarchy. Each instance has a unique “id” attribute that both uniquely identifies it within a folder and allows us to invoke methods on the object.
If this already sounds confusing, remember that the URL /foo/bar normally means that a web server should retrieve the file “bar” within the directory “foo”. But in Zope, the URL foo/bar means that the system should invoke the method “bar” on the object “foo”. In other words, foo/bar gets turned into foo.bar. And when we say “the object foo”, we really mean to say, “the object whose ID is foo”. Setting the ID is essential if we are going to invoke methods on our object successfully.
We also will need to define a meta_type for our product. The meta_type name is the text string that will appear in the Add pull-down list of Zope products in the upper-right corner of the /manage screen. In general, you can name the meta_type the same as your class or perhaps something a bit easier to understand. Remember that items in the Add menu are displayed in ASCII order, which means that capital “Z” comes before lowercase “a”.
To create our own product, we need to do the following:
Define a class in its own module and install it in lib/python/Products.
Define an __init__ method in which we assign the “id” instance variable a value.
Define one or more methods whose return value is a text string containing HTML.
Define a meta_type class variable, which sets the meta_type for all instances of our product.
This turns out to be surprisingly simple to do, as you can see in Listing 1, which defines helloworld.py, a simple Zope product that you can install and almost instantiate in your site. (We'll soon see how to get around these limitations.)
There are several important items to notice in our helloworld class. For starters, the class and its methods contain documentation strings. It's always a good idea to write docstrings, and Python's inclusion of such a feature is a rare and refreshing reminder that programmers can and should include documentation along with the source code. Zope enforces this restriction by mandating that classes and methods must include docstrings if you want them to be used in the system.
Our class also defines two methods, __init__ and index_html. The __init__ method is invoked automatically by Zope when it creates an instance of our object and typically is used to initialize instance variables and define other behaviors that will be needed later on. In this particular case, __init__ defines a single instance variable (self.id) that allows our object to keep track of its identity. As you might expect, __init__ is not meant to be invoked from the outside world, but rather from within Zope itself.
The index_html method, by contrast, is designed to be invoked via a URL. If we place an instance of “helloworld” in the root (/) directory of our Zope server, we can invoke the index_html method on it with a URL of /helloworld/index_html. But index_html is special; like the index.html file on many Apache servers, it is invoked by default if no other method is named explicitly.
Finally, notice that index_html returns HTML to its caller. It does not return a status code or anything other than the HTML.
helloworld.py is a perfectly legal Zope product; we can install it in lib/python/Products, and Zope will not mind. Unfortunately, Zope also will fail to notice that helloworld.py is there at all, will not add it to the Add selection list and generally will ignore the work we did in writing our product. It's clear that we will need to beef up our skeletal product if we want it to interact with Zope. We will call this enhanced version the “smallhello” product.
For starters, we must change the structure of our product from a single standalone module file (helloworld.py) into a full-fledged Python package. A package is a directory (smallhello) within the Python search path (defined by the variable sys.path) containing one or more Python source files. In our case, the smallhello directory will contain two files: a smallhello.py file that is quite similar to helloworld.py (see Listing 2) and __init__.py (see Listing 3), which initializes and helps to register our object.
The __init__.py file first imports smallhello.smallhello, defining the module's methods and attributes. But the crucial part of __init__.py, at least as far as Zope is concerned, is the initialize method. After Zope discovers and imports smallhello, it invokes smallhello.initialize, passing it a ProductContext object (called “context”). In other words, initializing an object results in that object registering itself with the server.
The initialize routine itself is pretty straightforward, although our version does some rudimentary error trapping (using try/except) to ensure that things work correctly. Our smallhello product only passes two arguments to context.registerClass: the finalhello.finalhello object that we want to add and then a tuple of constructors that should be invoked when we want to create a new instance of our product. Remember to include a trailing comma if you pass a single item to constructors; otherwise, Zope will fail to load the product.
The constructors parameter is just one of a number of named parameters that we can pass to context.registerClass to customize the way in which our object is registered with Zope. For example, we also can pass an icon parameter that tells Zope which graphic (a filename inside of our package directory) should be placed next to instances of our package within Zope.
Turning helloworld.py into smallhello.py (see Listing 2) requires some small changes. We start by adding a method that allows Zope to create new instances of our product. By convention, such management-related methods begin with manage_, so our method is called manage_smallhello. This is the same method that was named in the constructors tuple passed to context.registerClass.
The most significant change to our smallhello class is also one of the least obvious: we have made it a subclass of OFS.SimpleItem.SimpleItem, one of the Zope product base classes provided as part of the Zope package (in the OFS.SimpleItem package). Without inheriting from SimpleItem, many things—from cutting and pasting of objects to acquisition—would fail to work as we expect, if at all. There are several possible base classes from which your products can inherit; SimpleItem, as its name implies, is designed to be the simplest and most straightforward of the bunch.
While modifying smallhello.py, I decided to add two other methods that produce content. One of them, other_html, produces output that is similar to index_html—except, of course, index_html is displayed by default when no other method is specified, while other_html only is called when explicitly named in the URL.
I also added a foo_file method that demonstrates how to return the contents of an HTML (or DTML) file from disk. It can be annoying and frustrating to put all of your HTML inside a Python module file; this way, you can keep the DTML files inside of the package directory but modify them independently of the program itself. Note that we had to import the HTMLFile method from the Globals package in order for this to work.
I modified the __init__ function in smallhello.py to take three arguments: self, id and title. (Previously, it only accepted self and id.) The __init__ function is invoked each time a new instance of smallhello.py is created, which is done through a call to manage_smallhello. Inside of manage_smallhello, our call to self._setObject sets the object ID to a generic smallhello_id, with a title of smallhello_title. Because we hard code the ID in our example, and because IDs must be unique within a folder, this means that we only can have one instance of our smallhello product in a given folder. There isn't enough space here to describe how to read and write parameters, but a quick look at the examples mentioned in the Resources section should make it obvious how to do this.
After the call to self._setObject, manage_smallhello then redirects the user's browser to the main (index_html) method. We could display some output instead, redirecting the user's browser to another method in our object, but I took the easy way out and decided to send users to /index.html on our site.
After you have installed the smallhello product, you can stop Zope and restart it again. You should see “smallhello” near the bottom of the Add menu; selecting it will send you to the index.html page on your Zope site. Because we haven't made our product as user-friendly as it could be, you will have to enter the URL manually (index_html, other_html or foo_file) in your browser. But there isn't any reason why these pages cannot contain links to each other or why other pages on the site cannot link to them.
What do you know? We've created a Zope product!
If we were to release our simplehello project as it currently stands, no one would really want to use it. In addition to the problems mentioned above (e.g., the lack of unique ids for individual instances), our product lacks the management tabs that make Zope so user-friendly for administrators. It also fails to handle security permissions in a standard or easy way.
It is almost as easy to install these features as the others we have seen so far. For example, each tab is represented by a dictionary containing two name-value pairs, label and action. The value associated with label is what the user sees on the screen, while the value associated with action tells Zope which method should be invoked when someone clicks on the appropriate tab. To install your tabs in Zope, define a manage_options tuple in your object, the members of which are the dictionaries describing the tabs.
One of the most important items that we haven't addressed so far is user input. This is actually a pretty easy issue to address because Zope treats HTML form inputs as if they were standard parameters to a method. For example, consider the following HTML form:
<form action="manage_edit" method="POST"> <p>id: <input type="text" name="id"></p> <p>Title: <input type="text" name="title"></p> <p><input type="submit"></p> </form>
Clicking on the “submit” button will submit the name-value pairs for id and title to our product's manage_edit method. We can define that method with a signature like the following:
def manage_edit(self, id, title):Within this method, we can retrieve the values of the id and title HTML form elements using the variables of the same names.
Zope products are a more advanced and sophisticated way to build Zope applications than DTML files, giving greater flexibility but also requiring greater discipline and understanding of the underlying mechanisms. Knowing how to write Zope products is something like knowing how to write mod_perl modules for Apache; it means that the underlying system is completely at your disposal.
Unfortunately, while programmers can take advantage of a rich API for creating their own Zope products, the lack of good introductory documentation has scared many people from trying. Our simplehello product demonstrates that with just a little code, you can get impressive and useful applications working in a short period of time.
Reuven M. Lerner is a consultant specializing in web/database applications and open-source software. His book, Core Perl, was published in January 2002 by Prentice-Hall. Reuven lives in Modi'in, Israel, with his wife and daughter.