You are not logged in Log in Join
You are here: Home » Members » gtk » Boring Product » How to Create a Boring Product in Python (v0.1)

Log in
Name

Password

 

How to Create a Boring Product in Python (v0.1)

This document is a companion to the Boring Product, which I hacked together so that budding Zope programmers out there could get into coding products in Python quickly. If you want to see all of the code in one place at once, grab Boring and have a look. If you want to know what the code does, read on.

Why code a Product in Python?

Any markup language pretending to be a programming language is going to run out of gas pretty quickly. On the other hand, coding an entire content management system from scratch is lots of hard work, even in a language as fiendishly easy as Python. Zope provides you with the best of both worlds by letting you code in either DTML or Python as appropriate for the task.

External methods provide a quick way for your DTML documents to call functions coded in Python. They're most useful in the case where you have some Python which does something already, and just need to call it. You could code entire web interfaces to back-end systems using nothing but external methods.

If you want to be able to use the Zope management interface to create and manage your objects, though, you need to implement a Product. Whilst any Product can use external methods, sometimes it's just simpler to code the object from scratch in Python itself and be done with it.

What do you need?

__init.py__

Zope product initialization is handled through an initialization module named __init__ in your product's directory. The initialization module's job is to tell Zope about the product's classes and perform any run-time initialization required for the product as a whole. It should not contain any of your product's working code.

__doc__ = """Boring initialization module."""
__version__ = '0.1'

I'm not entirely sure that Zope itself does anything with the doc and version strings, but anything that could potentially be used later by automatic code documentation tools sounds good to me. :)

import Boring

Don't forget to import the modules containing your product's classes! Otherwise you'll have a really hard time referring Zope to them later.

initialize(): what a relief!

Zope 1 needed to be told about your product's capabilities through lots of dictionaries defined in the __init__ module. That was a little unwieldy, to say the least. Zope 2 now supports the initialize function, which has lots of default arguments and does most of the hard work for you.

All of the heavy lifting of product initialization is now handled through initialize, which is passed an instance of App.ProductContext to customize through its registerClass method. Your initialize function needs to call registerClass once for each of your product's classes that you need to be handled through the Zope management interface. It looks like this:

def initialize(context): 
    try: # to register the product.
        context.registerClass(
            Boring.Boring,                   # Which is the addable bit? 
            constructors = (                 # Constructors; 
                Boring.manage_addBoringForm, # Add goes to the first one. 
                Boring.manage_addBoring),    # Both get permissions. 
            icon = 'item.gif'                # Icon for the product. 
            )
    except: # if you can't register the product, tell someone.
        import sys, traceback, string
        type, val, tb = sys.exc_info()
        sys.stderr.write(string.join(traceback.format_exception(type, val, tb), ''))
        del type, val, tb

The except code is from Jonothan Farr's LocalFS product. If there's a problem with your product, Zope will sometimes provide you with access to "broken product" and a backtrace of what went wrong. I suspect that's only for problems in your class modules themselves, however. Jonothan's code provides traceback on the console for anything that happened in registerClass(), assuming you're running Zope in debug mode.

registerClass(): what's going on?

__init__ is just a life-support system for that one call to registerClass, so it's worth taking a closer look at what it does. Here's the definition of registerClass:

def registerClass(self, instance_class=None, meta_type='',
                  permission=None, constructors=(), 
                  icon=None, permissions=None, legacy=(), 
    ):

Look at all those default arguments! Unlike the old Zope 1 method, where you had to go to all of the hard work even if you weren't particularly interested in customizing your object's behaviour, registerClass will do as much of the work for you.

Here are the arguments:

instance_class

The class you're registering with Zope. Don't trust the source, which as of Zope 2.0.1 still claims this argument is "not currently used". If you don't specify instance_class, some of the other defaults won't work. I found it much easier to supply it.

meta_type
The Zope meta_type of the object, used in many places including the drop-down list for adding an object to a folder. If you don't specify this, Zope will use the meta_type attribute of your class -- if you specified instance_class, that is.
permission
The name to be used for the permissions associated with your class' constructor methods (none of which are in the class itself; see the coverage for Boring itself). If you don't supply a name, Zope will sensibly use the meta_type.
constructors
A list of constructor methods for your class. The first of these will be called by the Zope management interface when someone selects your class from the Add drop-down list. You should list any other methods in your class module involved in construction so that the "Add Whatevers" permissions are applied to them.
icon
The name of the image file used to label any instances of this class in Zope's management interface.
permissions
Any additional permissions you want to associate with your class. If you define them in the class itself, you don't need to duplicate effort and put them here, so for most purposes the default will do just fine.

Since I've defined meta_type in my Boring module, I don't need to specify meta_type when calling registerClass(). I also don't need permission because the meta_type name will do just fine, or permissions because I define everything else in the class. All that's left is the class itself, its constructors, and the icon.

Boring.py

Now that __init__ more or less takes care of itself in Zope 2, the only heavy lifting to do is in your product's main module. First up, the documentation strings:

__doc__ = """Boring product module."""
__version__ = '0.1'  

Now, some more comprehensive imports:

from Globals import HTMLFile 
from Globals import MessageDialog 
from Globals import Persistent

import OFS.SimpleItem 
import Acquisition                # Various acquisition types. 
import AccessControl.Role         # Zope Security management. 

What do they all do?

Globals.HTMLFile
Provides a callable object wrapped around a DTML file. You don't want to litter your Python source with stray fragments of DTML. Put it in a file in your product directory where it belongs.
Globals.MessageDialog
Provides a response designed for use in the Zope management interface. I think.
Globals.Persistent
Why we're getting this from Globals rather than Persistence, I'm not particularly sure, but here 'tis anyway. Persistent makes our object stick in the ZODB.
OFS.SimpleItem
SimpleItem.Item is a minimally interesting Zope class.
Acquisition
Provides Zope acquisition services to this class so that it can inherit methods and properties from its containers in Zope.
AccessControl.Role
Provides Zope security services.

Of these, the first we'll use is HTMLFile, in the constructor:

manage_addBoringForm = HTMLFile('boringAdd', globals()) 

def manage_addBoring(self, id, title='', REQUEST=None):
    self._setObject(id, Boring(id, title))
    if REQUEST is not None:
        return self.manage_main(self, REQUEST)

manage_addBoringForm and manage_addBoring fulfil the promise we made in __init__ to provide Zope constructor methods for this class. The former (excuse the pun) is called by Zope when someone chooses to add our class, and should return a form the user can fill in to specify the instance's configuration. The easiest way to do this is code the form in DTML (or pure HTML) and use HTMLFile to make it callable.

manage_addBoring just creates a new Boring, calls the _setObject method obtained from somewhere to register it with Zope, and returns the main management form for the class.

Having provided the functions Zope requires as a wrapper around our class, we can get around to the business of the class itself:

class Boring(
    OFS.SimpleItem.Item,   # A simple Principia object. Not Folderish. 
    Persistent,            # Make us persistent. Yaah! 
    Acquisition.Implicit,  # Let us use our parent's variables and methods.  
    AccessControl.Role.RoleManager # Security manager. 
    ): 

Boring inherits a few Zope classes. I'd go into them, but I'm not yet entirely sure what they all do. :)

Since we didn't specify the meta_type Zope needs to be able to identify us in __init__, registerClass() looks it up in our class:

    meta_type = 'Boring'

We define the tabs available in the management pane as follows:

    manage_options = (
	{'label': 'Edit',       'action': 'manage_main'},
	{'label': 'View',       'action': ''},
	{'label': 'Security',   'action': 'manage_access'},
        ) 

Each tab needs a label and an action. Action can be either a method in the class or an attribute that happens to be a callable object, like an HTMLFile object. Empty actions default to index_html. The manage_access method is provided by the AccessControl.Role.RoleManager object we inherit.

Permissions are set with a list that associates permissions with methods:

    __ac_permissions__=(
	('View management screens', ('manage_tabs', 'manage_main')),
	('Change permissions',      ('manage_access',)           ),
	('Change Borings',          ('manage_edit',)             ),
	('View Borings',            ('',)                        ),
	)

Again, '' defaults to index_html.

Our class initialization method just has to do the basic work:

    def __init__(self, id, title=''): 
        self.id = id
        self.title = title

A couple more HTMLFile objects provide our index_html and manage_main "methods":

    index_html = HTMLFile('index', globals())
    
    manage_main = HTMLFile('boringEdit', globals())

Finally, a genuine manage_edit method handles the FORM results from manage_main:

    def manage_edit(self, title, REQUEST=None): 
        self.title = title
	if REQUEST is not None:
	    return MessageDialog(
		title = 'Edited',
		message = "Properties for %s changed." % self.id,
		action = './manage_main',
		)

MessageDialog handles, I suspect, that nice interface kludge whereby when you submit some changes you're directed back to the edit form but with some text at the top describing what you just did.

That's all there is!

What if I don't want to be Boring?

Once I've done some more research, I'll post an Interesting class which handles advanced stuff like acting as a container and being able to be used as a parent of a ZClass. If you couldn't be bothered waiting, there's plenty of source to dissect.

Enjoy!