papakiteliatziar

A Beta Boring Product

Creating a Python Product for Zope 2 isn't so hard. A very simple example can be done in around 80 lines of code.

Here is an image of the files we'll create and their structure:

images/boring_filestructure.png

All these files will be placed inside a folder in our Zope instance's "Products" folder. I'll use the folder name BetaBoring for now. People usually choose capitalized names for these product folders.

I'll walk you through the files and their contents now. I will try to keep the lines of code in order and not do repetitions or back references. That way you can go through the descriptions as if they were the actual code with very heavy comments.

When you are using the code snippets from this file, make sure you don't copy and paste the ## snippet X lines -- they are only there to get the indentation right and so we can reference them in discussing things. Copying them could mess up your python code at some points, when their position and indentation runs contrary to the python code structure. When in doubt download the sample code (link to the .tgz at the end).


betaboring.py

This file is where the main pile of python code ends up. There can be several .py files (e.g. one for each class), but in a simple product we'll have just one. What's in here? Let's see...

One of Zope's strengths is the security and access control. It's not just about being able to define logins and passwords, but about fine grained permission settings. We import these two items we need for a start.:

## snippet 1
from AccessControl.Role import RoleManager
from AccessControl import ClassSecurityInfo

Something else people like in Zope is "Zope Page Templates", a clean templating system for XML and especially HTML pages. Let's get us something so we can put our Page Templates along with our python code on the file system:

## snippet 2
from Products.PageTemplates.PageTemplateFile import PageTemplateFile

In Python Products all imports tend to be at the start of the file, grouped by generic python stuff, generic Zope stuff, and your own products stuff. So we import this now, will use it later, promised!

Building your own Zope Product means building your own Python class. By basing your class on an existing and well understood Zope object class, you get a pile of functionality with almost no work.

I suggest using one of these four to base your object class on. Three of them are some kind of "Folder", a thing that can hold other things.

  • Plain Folder for those very, very simple products that sometimes need to hold an image, a template, or a few other product instances.
  • A bit more fancy is OrderedFolder, which allows to re-order the contents through the ZMI - and also through python code.
  • If you need to store lots of objects, go with a BTreeFolder2. Once you go beyond a couple of hundred objects, the others will get sluggish, while the BTreeFolder2 keeps on rocking. This thing can handle lots of contained objects - we're talking about tens and hundreds of thousands.
  • Sometimes you need something that isn't meant to contain other objects, a classical "object structure leaf node". In that case you'd go with a SimpleItem. For most stuff I'd suggest using one of the others, they are way more flexible and simpler to use.

In our code I've left in all the choices and only commented out what we won't use. (For this example we'll go with fancy OrderedFolder, which would be good for example for a product that holds a blog's sideboxes, since you can then reorder them very easy).

## snippet 3
# choose one of these:
# from OFS.Folder import Folder
from OFS.OrderedFolder import OrderedFolder
# from Products.BTreeFolder2.BTreeFolder2 import BTreeFolder2
# from OFS.SimpleItem import SimpleItem # this needs a bit extra work

One more import for Python's 'os' module, and we use it to find our 'www' folder from our code:

## snippet 4
import os
_www= os.path.join(os.path.dirname(__file__), 'www')

Now we're really getting started with our class. Whatever you chose in the imports, you'll use here to base your class upon. Conventions for class name's vary. Some people use lowercase, some CamelCase. Don't forget the docstring, as Zope uses "having a docstring" as a low level security check - no docstring, no access from the web:

## snippet 5
class betaboring(OrderedFolder):
    """
    What your product is doing (e.g. being boring).
    """

Inside the ZMI, and in object-to-object relations in Zope code, product instances are identified by the meta_type attributes. We will set our's here:

## snippet 6
    meta_type='Beta Boring'

Back to the security machinery. We will now initialize the security setup for our new object class. Then we set the default access. The default access is the policy that is taken for everything that we do not give a dedicated security declaration on. In our example I use "allow", since opening security up is always easier than keeping it all tight :-) If I would set it to "deny", then I would have to make sure that every attribute (even something as simple as the title_or_id() method) has a security declaration or else it would raise Unauthorized errors:

## snippet 7
    security=ClassSecurityInfo()
    security.setDefaultAccess("allow")

We're diving right in with a security declaration. Here we're declaring index_zpt to be private, i.e. not accessible through the web or from a ZMI "Script (Python)" method. It can only be used from inside "product code". We're using the PageTemplateFile method to link index_zpt to a file in our www folder on the filesystem (more on that file later).

## snippet 8
    security.declarePrivate('index_zpt')
    index_zpt = PageTemplateFile(_www+'/index.zpt', globals())

More security stuff, this time for a real python method. index_html is the main view method Zope calls when you feed it the URL of any object. Normally a folder-like object doesn't have an index_html method, since you probably want to place one in there. But in your product index_html could actually do something useful, like presenting some information about the folders contents.

Since this method is meant to be viewed, we'll give it the "View" permission. Who actually has the permission to "View" things is defined in the ZMI's "security" tab. Our security declaration links the access restrictions on our method to the "View" permission with its checkboxes in that tab:

## snippet 9
    security.declareProtected('View','index_html')

The method definition is plain and simple. Right now we have only two parameters: "self" (for python's class access) and the REQUEST object that Zope uses when methods are called from a browser and that holds a shockfull of information and is useful in multiple ways to interact with the user, the browser, and almost everything else.

## snippet 10
    def index_html(self, REQUEST):
        """
        Display boring main view.
        """

Again, we gave a docstring, to make sure access is granted and ... for documenting what our class is doing.

In our method itself, we just collect some meaningless information:

## snippet 11
        id = self.getId() # a boring example for the "options" space

But now it's getting interesting: We "return" the page template we had declared earlier. The page template was "security declared" to be not accessible through the web, but we access it from python code here, so we're allowed to use it.

We also feed it with our collected data as arguments. Those arguments will then be available from inside the ZPT, using the "options" namespace. When we "return" the ZPT, this means that it will be evaluated, and the resulting html output is fed back to the users browser:

## snippet 12
        return self.index_zpt(id=id)

We are done with the actual code from our class. Now we need two more methods to make it possible to instantiante objects from our class in the ZMI. The naming convention for these is that they start with manage_add -- in a references to the /manage URL access to the ZMI.

Since these two methods are "outside" of the class, it would be useless to have security declarations on them. Instead access to them is handled by Zope's auto-generated "Add PRODUCT NAMEs?" security permission. (So for this product, Zope will invent a "Add Beta Borings" permission that can be used to restrict who is allowed to add objects of this type in the ZMI.)

First we have the ZPT that users will see when they choose "Beta Boring" from the ZMI's "Add" menu:

## snippet 13
manage_addBoring_form = PageTemplateFile(_www+'/manage_addform.zpt', globals())

Second we have the actual "add" method. This one uses the form input from the last method, and it does the work of actually instantiating our object, and of adding it to a folder in the ZMI. In this case "self" is not our class, but the "container" in the ZMI where we're in:

## snippet 14
def manage_addBoring(self,id='boring',REQUEST=None):
    """
    Add a boring object to a folder
    """

Calling our class like betaboring(id) is creating the new object. _setObject is the internal, very rough method to add the created object to a folder:

## snippet 15
    self._setObject(id,betaboring(id))

The resulting "manage_add..." method can actually be called in two ways:

  • Through the ZMI, as the "action=" parameter of the "add-form".
  • Through python code, in order to be able to programmatically add a "Beta Boring" object somewhere. This is done with the special syntax self.manage_addProduct['ProductsFolderName'].manage_add..() in our case it could be some product adding one of our objects using something.manage_addProduct['BetaBoring'].manage_addBoring('boooring'). Note how 'BetaBoring' has to be the exact spelling and capitalization of the folder name.

The difference for our code here is that when being called through the ZMI, we get a REQUEST object in our parameters, otherwise we get REQUEST set to None. We test for this, and accordingly redirect the browser or just plain return our new object's id string:

## snippet 16
    if REQUEST is not None:
        try:    url=self.DestinationURL()
        except AttributeError: url=REQUEST['URL1']
        REQUEST.RESPONSE.redirect('%s/manage_main' % url)
    return id

__init__.py

This is the file that makes a python module into a Zope Product. As such it contains a few mystic things that you'll rarely need to dig up in detail, at least not for writing something as simple as this boring product.

The name __init__.py is special: Where you could (and would!) change the betaboring.py file's name to something that makes sense for your project, this one has to be exactly like this.

Let's start out with importing what we have from the betaboring.py file:

## snippet 17
from betaboring import betaboring, manage_addBoring_form, manage_addBoring

See how the import is from betaboring? It's the filename, but the .py gets lost. We import the class and our two methods we made for "manage_add..."-ing an object of that class.

Next we "initialize" our class. This method gets called by Zope upon starting up to get all Products rolling. We feed it with the information we have from our .py file:

## snippet 18
def initialize(context):
    # comment describing the boring product
    context.registerClass(

First our class name:

## snippet 19
        betaboring,

Then the names of our two "manage_add..." methods, these go into a tuple. The form comes first, in a gross violation of the "form follows function" design principle:

## snippet 20
        constructors = (manage_addBoring_form, manage_addBoring),

Next step is optional, we can provide the path to an image file, which will be used as the icon in the ZMI:

## snippet 21
        icon = 'www/boring_icon.png'
        )

README.txt

A StructuredText? file describing your product. Use this exact name and the StructuredText? format and it will be included in the "readme" tab in the /Control_Panel Products listing in the ZMI:

The Beta Boring Product

    Just an example product with an example README.txt.

version.txt

Make a one-line text file with this name, put your products version in here. It will be used for the Products listing in the ZMI's Control_Panel:

BetaBoring-Demo-0.1

www/boring_icon.png

The small icon for the ZMI. I usually copy one of the existing one's to get the size right. Here I've just added a cross, so you can spot in the ZMI which object we're talking about:

images/boring_icon.png

www/manage_addform.zpt

This is a simple html file with a simple html form. The form should submit to the "manage_add..." method we defined earlier -- and of course it should feed the proper arguments into that method. In our case all we want is the "id" parameter:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Add BetaBoring Instance</title>
</head>
<body>
Please enter the id of the new BetaBoring instance:<br/>
<form name="form" action="manage_addBoring"><br/>
<input type="text" name="id" value="boooooring"><br/>
<input type="submit" value="add">
</form>
</body>
</html>

www/index.zpt

This one is even more simple. We're displaying some html. Only thing special in this very boring ZPT example: We're referencing the "id" variable that was passed to us from the python method "index_html" (we've talked about this earlier). This can be accessed through the "options" namespace in TAL:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>This is Beta Boring</title>
</head>
<body>
<p>
    So, we are in <span tal:replace="options/id" />.
</p>
</body>
</html>

That's it.

Yes, it really is. Making your own filesystem based "Python Product" for Zope is that easy.

Go ahead, download the source (to get indentation right), untar it, put the resulting folder into your Products directory, restart your Zope, add a Beta Boring object in the ZMI. Open it and click on its "View" tab.

And it has lot's of features too. In the ZMI it:

  • can show you its contents
  • let's you add stuff to it
  • let's you view, add, change properties (it already comes with a "title" property - we haven't even seen anything about that).
  • let's you change security settings, who can see and do what
  • has Undo support, make changes and have them "undone" if you want to
  • let's you find objects contained in it.

In short: It's a complete Zope folder, but with whatever abilities you added to it!

What's missing?

You knew there was something missing. This is a simple, boring example product. It doesn't include a lot of fancy stuff.

You could add lots of ZPT's and methods to the class. You could use more than one class, either in the same file or each in its own modulename.py file. (You would then "initialize" those classes in __init__.py too.) You could add special methods that get called on occasions like object creation and deletion, or that let you mess with the URL the user entered into the browser.

Also missing is some stuff that you don't see on the surface, but that's important for a well maintained project: We haven't added any tests to our boring product yet, be they unit tests, functional tests, or whatever testing system is in the buzz at them moment.

About the Author

This "boring product" example was written by Sascha Welter, aka betabug. See also http://betabug.ch.