Home Company Services Portfolio Contact us nav spacer

Chapter 4. Developing Zope Products in Python

Chapter 4. Developing Zope Products in Python

Zope Product

Roché Compaan

Edited by

Jean Jordaan

In this section, we construct some very simple Python classes to model an organisation with divisions and employees. We then make the minimum changes necessary to make this Python module into a Zope Product, and also look at what the Zope framework offers us in order to simplify and extend our code.

What is a Zope Product?

Zope Products are simply Python packages in Zope's Products directory that use Zope's registration API to register classes in the product registry. There are however a few important base classes that one should subclass to make your class usable inside Zope.

Before we explore these base classes and the registration API, let's begin by making a very simple Python application without any Zope dependency. Once this is done we can gradually modify our Python application to work inside Zope.

Familiar Python

Our Python application will implement a very simple organisational model: an organisation contains the people affiliated with it, as well as other organisations reporting to it. We want our application to implement the following scenario:

ACME Corporation is a global software development company that has resellers on all continents: ACME USA, ACME UK, ACME Europe, etc. Pete Smith is the president of ACME and focuses on the global strategy of the company. John Smith is vice-president and oversees global operations. Mary, Jim, Sarah, Richard and Anna head the ACME reseller companies and manage local operations.

For now, we need only two classes to model the above: an Organisation and an Employee. First, we need to create a Python package for our application. We'll name this Organisations. We do this by creating a directory named Organisations inside our Zope instance's Products directory, and creating the file __init__.py (the package constructor) inside the directory. Add a documentation string to __init__.py describing our application:


""" This application implements a hierarchy of organisations and
    the people affiliated with each organisation.
"""

Let start with the implementation of Organisation. First, create a new module named Organisation.py inside the package you just created, and create the following class inside it:


class Organisation:
    """ An organisation that knows its people """

    def __init__(self, id, name, description):
        self.id = id
        self.name = name
        self.description = description
        self._employees = {}
        self._organisations = {}

    def addEmployee(self, employee):
        """ Add an employee to the organisation """
        self._employees[employee.id] = employee

    def delEmployee(self, id):
        """ Delete employee given its id """
        del self._employees[id]

    def getEmployee(self, id):
        """ Retrieve an employee given its id """
        return self._employees[id]

    def addOrganisation(self, organisation):
        """ Add an organisation """
        self._organisations[organisation.id] = organisation

    def delOrganisation(self, id):
        """ Delete organisation given its id """
        del self._organisations[id]

    def getOrganisation(self, id):
        """ Retrieve organisation given its id """
        return self._organisations[id]

Next, create the Employee.py module:


class Employee:
    """ An employee of an organisation """

    def __init__(self, id, name, surname, age, date_appointed):
        self.id = id
        self.name = name
        self.surname = surname
        self.age = age
        self.date_appointed = date_appointed

    def getFullname(self):
        """ Compute the fullname of an employee """
        return "%s %s" % (self.name, self.surname)

We test our code by writing a unittest with Python's unit testing framework. Create a directory named tests inside the package. In tests, create a file named test_organisation.py to hold tests for our Organisation class:


import sys
import unittest
from datetime import date

if __name__ == '__main__':
    sys.path.insert(0, '..')

from Employee import Employee
from Organisation import Organisation

# some test data
orgX = Organisation('test', 'Test Organisation', 'A test organisation')
orgY = Organisation('testY', 'Test Organisation', 'A test organisation')
pete = Employee('pete', 'Pete', 'Smith', 40, date.today())

class OrganisationTests(unittest.TestCase):

    def test_init(self):
        self.assertEquals(orgX.id, 'test')
        self.assertEquals(orgX.name, 'Test Organisation')
        self.assertEquals(orgX.description, 'A test organisation')

    def test_addEmployee(self):
        orgX.addEmployee(pete)
        self.assertEquals(orgX._employees[pete.id], pete)

    def test_getEmployee(self):
        self.test_addEmployee()
        self.assertEquals(orgX.getEmployee(pete.id), pete)

    def test_delEmployee(self):
        self.test_addEmployee()
        orgX.delEmployee('pete')
        self.assertEquals(orgX._employees, {})

    def test_addOrganisation(self):
        orgX.addOrganisation(orgY)
        self.assertEquals(orgX._organisations[orgY.id], orgY)

    def test_getOrganisation(self):
        self.test_addOrganisation()
        self.assertEquals(orgX.getOrganisation(orgY.id), orgY)

    def test_delOrganisation(self):
        self.test_addOrganisation()
        orgX.delOrganisation(orgY.id)
        self.assertEquals(orgX._organisations, {})

if __name__ == '__main__':
    unittest.main()

Create test_employee.py to hold tests for our Employee class:


import sys
import unittest
from datetime import date

if __name__ == '__main__':
    sys.path.insert(0, '..')

from Employee import Employee

# some test data
pete = Employee('pete', 'Pete', 'Smith', 40, date.today())

class EmployeeTests(unittest.TestCase):

    def test_init(self):
        self.assertEquals(pete.id, 'pete')
        self.assertEquals(pete.name, 'Pete')
        self.assertEquals(pete.surname, 'Smith')
        self.assertEquals(pete.age, 40)
        self.assertEquals(pete.date_appointed, date.today())

    def test_getFullname(self):
        self.assertEquals(pete.getFullname(), 'Pete Smith')

if __name__ == '__main__':
    unittest.main()

We haven't setup our testing environment properly yet, and therefore can't run the tests from any location. But we're eager to see if the tests pass, so change to the tests directory, and run the tests:


$ python test_employee.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.002s

OK
$ python test_organisation.py
.......
----------------------------------------------------------------------
Ran 7 tests in 0.002s

OK
Q:

Implement an Address class with id, address_type, address, city, state, postal_code and country as attributes. Update Employee to allow the addition of multiple addresses to it.

A:

See source/zopeproduct/step1/Organisations/Address.py

Interfaces

Reviewing the code we have written thus far reveals some areas of concern. When adding an employee or organisation to an organisation, we are not asserting the type of object we are adding. Developers can potentially call addEmployee with an instance that is not an Employee and corrupt instance data on our organisation.

Although this can easily be solved by Python's built-in isinstance check (e.g. isinstance(employee, Employee)), we can also solve this problem using interfaces. Interfaces are playing an increasingly important role in Zope itself and the products developed for it.

The Interfaces package is distributed as part of Zope, but it has no dependency on any other Zope packages and can be used in standalone Python applications.

In Python, interfaces serve primarily as documentation for a class. It saves developers the trouble of browsing the source of your application to discover its API. One might argue that tools such as pydoc in the Python standard library, which generates API documentation from source code, already serves this purpose, and makes this use somewhat redundant.

Interfaces have other uses, though. They simplify code inspection by providing an API for listing the methods and attributes of a class, and for checking whether a class or instance implements a given interface.

Interfaces are conventionally named by prepending the letter I to the class name. In the Organisations directory, create a file named interfaces.py and add the following interface definitions:


from Interface import Interface
from Interface.Attribute import Attribute

class IOrganisation(Base):
    """ An organisation that knows its people """

    id = Attribute("organisation id")
    name = Attribute("name of the organisation")
    description = Attribute("description of the organisation")

    def addEmployee(self, employee):
        """ Add an employee to the organisation """

    def delEmployee(self, id):
        """ Delete employee given its id """

    def getEmployee(self, id):
        """ Retrieve an employee given its id """

    def addOrganisation(self, organisation):
        """ Add organisation """

    def delOrganisation(self, id):
        """ Delete organisation given its id """

    def getOrganisation(self, id):
        """ Retrieve organisation given its id """

class IEmployee(Base):
    """ An employee of an organisation """

    id = Attribute("employee id")
    name = Attribute("firstname of the employee")
    surname = Attribute("surname of the employee")
    age = Attribute("age of the employee")
    date_appointed = Attribute("date_appointed")

    def getFullname(self):
        """ Compute the fullname of an employee """

Now we can let our classes know what interfaces they implement by setting their __implements__ attribute.

Organisation.py:


from interfaces import IOrganisation

class Organisation:
    """ An organisation that knows its people """

    __implements__ = IOrganisation

...

Employee.py:


from interfaces import IEmployee

class Employee:
    """ An employee of an organisation """

    __implements__ = IEmployee

...

Now we are ready to use interface checks when adding Employees and Organisations. Modify Organisation.py to include the interface checks listed below:


def addEmployee(self, employee):
    """ Add an employee to the organisation """
    assert IEmployee.isImplementedBy(employee)
    self._employees[employee.id] = employee

...

def addOrganisation(self, organisation):
    """ Add organisation """
    assert IOrganisation.isImplementedBy(organisation)
    self._organisations[organisation.id] = organisation

Let's modify test_organisation.py to make doubly sure that our interface checks are doing what they should. We check that our code raises an AssertionError when we try to add string objects instead of Employee and Organisation instances:


def test_addEmployee(self):
    orgX.addEmployee(pete)
    self.assertEquals(orgX._employees[pete.id], pete)
    self.assertRaises(AssertionError, orgX.addEmployee, 'pete')

...

def test_addOrganisation(self):
    orgX.addOrganisation(orgY)
    self.assertEquals(orgX._organisations[orgY.id], orgY)
    self.assertRaises(AssertionError, orgX.addOrganisation, 'test')

And the tests should pass:


$ python test_organisation.py
.......
----------------------------------------------------------------------
Ran 7 tests in 0.004s

OK

Zope is still Python

Before we prepare our classes for usage within Zope, we need to understand the Zope base classes we'll be subclassing.

Written as Python imports, they are:


from Acquisition import Implicit
from Globals import Persistent
from AccessControl.Role import RoleManager
from OFS.SimpleItem import Item
from OFS.ObjectManager import ObjectManager
from OFS.PropertyManager import PropertyManager

Zope classes to subclass

Acquisition.Implicit

This enables instances of your class to acquire methods and attributes from their context. All Zope Products should inherit from this class given the overwhelming amount of services and products that depend on it.

Globals.Persistent

As the name suggests, this is the class that persists instances of your class in the ZODB (Z Object Database). By subclassing from Persistent, any changes to attributes are automatically persisted, with a couple of exceptions: if your class has any non-persistent mutable attributes such as a dictionary or list, you will have to let the persistence machinery know that these attributes changed. This is done by assigning to a special attribute named _p_changed, e.g.:


org = Organisation('test', 'Test Organisation', 'A test organisation')
pete = Employee('pete', 'Pete', 'Smith', 40, date.today())
org._employees[pete.id] = pete
org._p_changed = 1
OFS.SimpleItem.Item

Its docstring says:


"""A common base class for simple, non-container objects."""

This is modest, considering the services it provides! They include (as listed by the Zope Developers Guide) “the ability to be cut and pasted, capability with management views, WebDAV support, basic FTP support, undo support, ownership support, and traversal controls. [...] Item is really an everything-but-the-kitchen-sink kind of base class.” If your class implements Item, make sure to set the meta_type of the class. meta_type is used to distinguish classes in a Zope application. The most obvious place where the value of meta_type is used, is in the ZMI's product add list. All instances of Item must set an id attribute upon creation. This is to make sure that objects are unique in a specific container. All ids in Zope must be strings.

AccessControl.Role.RoleManager

RoleManager allows the configuration of permissions and roles on the objects that implement it. Its API provides methods to modify permission settings, assign permissions to roles, define local roles, and it comes in handy with methods for querying permissions and roles.

OFS.PropertyManager.PropertyManager

As its name suggests, PropertyManager provides an API to manage properties on objects. It also makes adding, deleting and changing properties possible through the web, by means of a management view in the ZMI.

OFS.ObjectManager.ObjectManager

ObjectManager is the Zope container type. Subclass from ObjectManager if you want your object to have an API for adding objects inside it, deleting contained objects and retrieving objects based on their id or metatype.

Zopifying our Python classes

To make our Python classes usable in Zope they should all subclass Persistent, Acquisition.Implicit, Item and RoleManager. Create two new Python modules in the Organisations package named ZOrganisation.py and ZEmployee.py for the definition of the Zope incarnations of Employee and Organisation.

ZEmployee.py:


import Globals
from Acquisition import Implicit
from AccessControl.Role import RoleManager
from OFS.SimpleItem import Item
from Employee import Employee

class ZEmployee(Employee, Item, Globals.Persistent, Implicit, RoleManager):
    """ Zope wrapper for Employee """

    meta_type = 'Employee'

    def __init__(self, id, name, surname, age, date_appointed):
        Employee.__init__(self, id, name, surname, age, date_appointed)
        self.title = self.getFullname()

Globals.InitializeClass(ZEmployee)

You can't do much with this class inside Zope yet --- it simply introduces the Zope base classes into our application. Also note the call to InitializeClass. It is important to initialise all Zope classes in this way, since it checks the security settings on the class, and sets up permissions for it.

Before we continue to look at Organisation, let's pause at a questionable line of ZEmployee.__init__ --- it should bother you that we assign the result of getFullname to the title attribute. What happens if we change the employee's name or surname? Obviously title won't reflect such a change. It would be preferable if title were computed dynamically when accessed. Thankfully, Zope provides computed attributes. Here's how to use it them:


import Globals
from Acquisition import Implicit
from AccessControl.RoleManager import RoleManager
from OFS.SimpleItem import Item
from ComputedAttribute import ComputedAttribute
from Employee import Employee

class ZEmployee(Employee, Item, Globals.Persistent, Implicit, RoleManager):
    """ Zope wrapper for Employee """

    meta_type = 'Employee'

    title = ComputedAttribute(Employee.getFullname, 1)

    def __init__(self, id, name, surname, age, date_appointed):
        Employee.__init__(self, id, name, surname, age, date_appointed)

Globals.InitializeClass(ZEmployee)

ComputedAttribute takes the name of the method that should be called as first parameter, and an optional boolean as second parameter that indicates whether the method should be called with or without an acquisition context.

ZOrganisation.py:


import Globals
from Acquisition import Implicit
from AccessControl.Role import RoleManager
from OFS.SimpleItem import Item
from ComputedAttribute import ComputedAttribute
from Organisation import Organisation

class ZOrganisation(Organisation, Item, Globals.Persistent, Implicit,
                    RoleManager):
    """ Zope wrapper for Organisation """

    meta_type = 'Organisation'

    def __init__(self, id, name, description):
        Organisation.__init__(self, id, name, description)

    def addEmployee(self, employee):
        """ add an employee to the organisation """
        Organisation.addEmployee(self, employee)
        self._p_changed = 1

    def delEmployee(self, id):
        """ delete employee given its id """
        Organisation.delEmployee(self, id)
        self._p_changed = 1

    def addOrganisation(self, organisation):
        """ add organisation """
        Organisation.addOrganisation(self, organisation)
        self._p_changed = 1

    def delOrganisation(self, id):
        """ delete organisation given its id """
        Organisation.delOrganisation(self, id)
        self._p_changed = 1

    def _getTitle(self):
        return self.name
    title = ComputedAttribute(_getTitle, 1)

Globals.InitializeClass(ZOrganisation)

Feel safe with Security

In addition to subclassing from Zope bases you must declare security information for each class. Zope gives us ClassSecurityInfo that removes developers' concern with the underlying security implementation and provides them with declarative statements for protecting objects and their methods.

ZEmployee.py:


from AccessControl import ClassSecurityInfo

class ZEmployee(...): 
    ... 
    security = ClassSecurityInfo() 
    security.declarePublic('getFullname')

ZOrganisation.py:

 
from AccessControl import ClassSecurityInfo

class ZOrganisation(...):
    ... 
    security = ClassSecurityInfo() 
    security.declareProtected('addEmployee', 'Add Employee') 
    security.declareProtected('delEmployee', 'Delete Employee') 
    security.declarePublic('getEmployee') 
    security.declareProtected('addOrganisation',
                               'Add Organisation') 
    security.declareProtected('delOrganisation',   
                               'Delete Organisation') 
    security.declarePublic('getOrganisation')

In the above code listing we use two methods of ClassSecurityInfo, namely declareProtected and declarePublic. declareProtected takes the method name and the name of the permission as parameters, and declarePublic requires only a method name.

These security declarations are set up when you call InitializeClass mentioned above.

Moving to PersistentMapping

You can't miss all the _p_changed assignments. They notify the persistence machinery that the _employees or _organisations dictionaries have changed. We have to do this in order to make our changes stick. Luckily there is an easier way, which doesn't require us to override almost all of the methods in our Organisation base class. Instead of using built-in dictionaries, we can use their persistent counterpart, named PersistentMapping. This significantly reduces the coding needed to wrap Organisation for Zope: all the add... and del... methods and their security declarations are gone, and the code is reduced to the following:


import Globals
from Acquisition import Implicit
from AccessControl.Role import RoleManager
from OFS.SimpleItem import Item
from ComputedAttribute import ComputedAttribute
from ZODB.PersistentMapping import PersistentMapping
from Organisation import Organisation

class ZOrganisation(Organisation, Item, Globals.Persistent, Implicit,
                    RoleManager):
    """ Zope wrapper for Organisation """

    meta_type = 'Organisation'

    def __init__(self, id, name, description):
        Organisation.__init__(self, id, name, description)
        self._employees = PersistentMapping()
        self._organisations = PersistentMapping()

    def _getTitle(self):
        return self.name
    title = ComputedAttribute(_getTitle, 1)

Globals.InitializeClass(ZOrganisation)

If you don't want to put up with the hassle of multiple _p_changed assignments required by the built-in list type, the ZODB also provides a PersistentList type.

Management wants to see you

When building a user interface for a Zope Product, consider that there are administrators who have to manage the product from within the ZMI as well as end-users using the application.

Merely by subclassing from the Zope bases we've covered so far, we already inherit a couple of management views:

  • Item gives us a management interface for undoing changes and changing ownership on an object.

  • RoleManager gives us an interface for changing security settings. The management interfaces provided by an object is determined by the manage_options definition on a class. Below is the manage_options of the Undo class in App.Undo.UndoSupport:

    
    manage_options=(
        {'label':'Undo', 'action':'manage_UndoForm',
        'help':('OFSP','Undo.stx')},
        )
    

manage_options is a tuple containing a sequence of dictionaries that have label, action and optionally help as keys.

The keys of the dictionaries in the 'manage_options' structure

label

The name of the management view in the ZMI.

action

The name of a method that displays a management view.

help

The product id and topic id of context sensitive help for your class. We discuss this later on.

Since both Item and RoleManager define manage_options, Item's manage_options will override that defined by RoleManager due to the order of the bases ( Item precedes RoleManager). We can solve this by redefining manage_options and including the security management view provided by RoleManager:


class ZEmployee(Employee, Item, Globals.Persistent, Implicit, RoleManager):
    """ Zope wrapper for Employee """

    meta_type = 'Employee'

    title = ComputedAttribute(Employee.getFullname, 1)

    manage_options=(
        Item.manage_options + RoleManager.manage_options
        )
...

Management interfaces are built using Zope's templating languages, DTML and ZPT. So far, we've only used DTML and Zope Page Templates inside the ZMI, but they can easily be used from the file system.

For both ZEmployee and ZOrganisation we need to create forms to add instances, and we'll use ZPTs on the filesystem for this purpose. Create a new directory called www inside the Organisations product, and create two files named addZEmployee.zpt and addZOrganisation.zpt inside it. (The "zpt" extension is not required, but it is useful to distinguish ZPTs from other file types.)

This is how addZEmployee.zpt should look:


<tal:zmi_header replace="structure here/manage_page_header"/>

<p class="form-help">
Create a new ZEmployee object using the form below. 
</p>

<form action="manage_addZEmployee" method="post">
<table cellspacing="0" cellpadding="2" border="0">
<tr>
    <td align="left" valign="top">
    <div class="form-label">
    Id
    </div>
    </td>
    <td align="left" valign="top">
    <input type="text" name="id" size="40" />
    </td>
</tr>
<tr>
    <td align="left" valign="top">
    <div class="form-label">
    Name
    </div>
    </td>
    <td align="left" valign="top">
    <input type="text" name="name" size="40" />
    </td>
</tr>
<tr>
    <td align="left" valign="top">
    <div class="form-label">
    Surname
    </div>
    </td>
    <td align="left" valign="top">
    <input type="text" name="surname" size="40" />
    </td>
</tr>
<tr>
    <td align="left" valign="top">
    <div class="form-label">
    Age
    </div>
    </td>
    <td align="left" valign="top">
    <input type="text" name="age:int" size="10" />
    </td>
</tr>
<tr>
    <td align="left" valign="top">
    <div class="form-label">
    Date appointed
    </div>
    </td>
    <td align="left" valign="top">
    <input type="text" name="date_appointed:date" size="40" />
    </td>
</tr>

<tr>
    <td align="left" valign="top">
    </td>
    <td align="left" valign="top">
    <div class="form-element">
    <input class="form-element" type="submit" name="submit" 
    value=" Add " /> 
    </div>
    </td>
</tr>
</table>
</form>

<tal:zmi_footer replace="structure here/manage_page_footer"/>

Traditionally, Zope management interfaces were written in DTML (mainly because DTML predates ZPT). Even though we created a Page Template, we can re-use Zope's DTML to give our templates a look and feel consistent with the ZMI. Note how we re-use Zope's DTML management header and footer with TAL replace statements:


<tal:zmi_header replace="structure here/manage_page_header"/>
...
<tal:zmi_footer replace="structure here/manage_page_footer"/>

Notice that the form action is set to 'manage_addZEmployee':


<form action="manage_addZEmployee" method="post">

No such method exists yet. This is the constructor method which we will discuss in-depth later on. Just keep it in mind for now.

To make use of this template we must create a PageTemplateFile instance inside the appropriate module:


import Globals
from Acquisition import Implicit
from AccessControl.Role import RoleManager
from OFS.SimpleItem import Item
from ComputedAttribute import ComputedAttribute
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from Employee import Employee

class ZEmployee(Employee, Item, Globals.Persistent, Implicit,
ger):
    """ Zope wrapper for Employee """

    meta_type = 'Employee'

    title = ComputedAttribute(Employee.getFullname, 1)

    def __init__(self, id, name, surname, age, date_appointed):
        Employee.__init__(self, id, name, surname, age, date_appointed)

Globals.InitializeClass(ZEmployee)

manage_addZEmployeeForm = PageTemplateFile(
    'www/addZEmployee.zpt', globals(),
    __name__='manage_addZEmployeeForm')

We import PageTemplateFile from the PageTemplates Product:


from Products.PageTemplates.PageTemplateFile import PageTemplateFile

And create a PageTemplateFile instance named manage_addZEmployeeForm :


manage_addZEmployeeForm = PageTemplateFile(
    'www/addZEmployee.zpt', globals(),
    __name__='manage_addZEmployeeForm')

www/addZEmployee.zpt is the path of the PageTemplate relative to your product directory.

The builtin Python method globals returns the current scope's global variables. This is used by PageTemplateFile's constructor to determine the path to your product, so that it can resolve the path to the filename you passed as first parameter.

The __name__ property can be used to set the name of the PageTemplate, if it is different than the filename. By convention, this is the same as the identifier that you're assigning to, i.e. manage_addZEmployeeForm.

The add form we created above is also called the constructor form, since it is the form used to create new instances of our class. Each constructor form has an accompanying constructor method. Below we define a constructor named manage_addZEmployee for ZEmployee:


...

manage_addZEmployeeForm = PageTemplateFile(
    'www/addZEmployee.zpt', globals(),
    __name__='manage_addZEmployeeForm')

def manage_addZEmployee(dispatcher, id, name, surname, age,
                      date_appointed, REQUEST=None):
    """ Add a ZEmployee instance """
    ob = ZEmployee(id, name, surname, age, date_appointed)
    dispatcher._setObject(id, ob)

    if REQUEST is not None:
        return dispatcher.manage_main(dispatcher, REQUEST, update_menu=1)

A constructor method will always be called with the FactoryDispatcher as first argument, named dispatcher above. The FactoryDispatcher stands in for the location (it proxies the location) where the instance is added. We use _setObject to persist the newly created instance inside the container. The dispatcher acquires _setObject from the proxied container.

Zope's REQUEST instance will automatically be passed in as the REQUEST argument if the constructor is the target of a web request --- otherwise (e.g. when calling the constructor programmatically) REQUEST is None. We know that REQUEST will not be None if the constructor is called from our add form, and consequently the method will return to the manage_main page of the container. manage_main is available on subclasses of ObjectManager ( Zope's container type) and lists the contents of a container.

Note that form variables present on the REQUEST (id, name, surname, etc.) that match argument names can be conveniently referenced using their literal argument name. This saves us the trouble of picking them off the REQUEST with REQUEST.get("name") calls.

Q:

As an exercise, create the constructor form and method for ZOrganisation, using ZEmployee as guide.

A:

See source/zopeproduct/step2/Organisations/ZOrganisation.py and source/zopeproduct/step2/Organisations/www/addZOrganisation.zpt

All classes must register

Just before we start Zope and play around with our product, we need to register each class with Zope, so that they become visible in the ZMI. This is usually done in an initialize method inside your Product's __init__.py:


import ZEmployee
import ZOrganisation

def initialize(context):
    context.registerClass(
        instance_class=ZEmployee.ZEmployee,
        constructors=(ZEmployee.manage_addZEmployeeForm,
                      ZEmployee.manage_addZEmployee),
        )

context.registerClass(
    instance_class=ZOrganisation.ZOrganisation,
    constructors=(ZOrganisation.manage_addZOrganisationForm,
                  ZOrganisation.manage_addZOrganisation),
    )

Your product must define initialize if it wants to register classes with Zope. initialize is called by Zope with a ProductContext as parameter. This provides an API for registering classes and help. As the name implies, registerClass is used to register classes. Although we only specify instance_class and constructors, registerClass can take an number of other keyword arguments. They are as follows (adapted from the ProductContext docstring):

registerClass parameters

instance_class

The class we want to register.

meta_type

The kind of object being created. This appears in add lists. If not specified, then the class's meta_type will be used.

permission

The permission name for the constructors. If not specified, then a permission name based on the meta type will be used.

constructors

A list of constructor methods in the form of a tuple, with the constructor form as first item, and the constructor method as second item.

icon

The name of an image file in the package to be used for instances in the ZMI. Note that the class icon attribute will be set automagically if an icon is provided.

permissions

Additional permissions to be registered. If not provided, then permissions defined in the class will be registered.

legacy

A list of legacy methods to be added to ObjectManager for backward compatibility.

visibility

Global if the object is globally visible, otherwise None.

interfaces

a list of the interfaces supported the object.

container_filter

function that is called with an ObjectManager object as the only parameter, which should return a true result if the object may be created in that container. The filter is called before showing ObjectManager's Add list, and before pasting (after the object has previously been copied or cut), but not before calling an object's constructor.

We call registerClass with just enough arguments for the registration to make sense. By passing in instance_class, we can be sure that the meta_type can be retrieved from the class, and that permissions we defined inside our classes will be registered.

Organisations in the ZMI

Installing our product in Zope is a simple matter of copying the Organisations directory to the Products directory of a Zope instance.

Once this is done, start your Zope instance, open your browser, and browse to the Products page in the ZMI Control Panel. Your product should be listed as Organisations in the list of installed products. If you browse to the root of your Zope instance you should also see two new types in the dropdown: Employee and Organisation.

If you add a couple of Employee and Organisation instances and visit them in the ZMI, you will notice management tabs for Undo and Ownership. This doesn't give a site manager much to work with, but we'll fix that shortly.

More Zope for our classes

The Organisation class is a container type: it contains Employee and Organisation instances. In the Python version of our class, we implemented containment using dictionaries. Although there is nothing wrong with this, it would require us to build a custom management view for adding Employee instances. This duplicates functionality which Zope already provides. Additionally, we will have to define custom traversal methods, if the contained objects are to be published on a subpath of the container (i.e. if the employee of an organisation should be reachable via an URL such as /acme/em12345678).

We don't have to do to much to bring our implementation closer to Zope's way of doing --- we simply have to subclass the container type, ObjectManager, and include its manage_options:


import Globals
from Acquisition import Implicit
from AccessControl.Role import RoleManager
from OFS.SimpleItem import Item
from ComputedAttribute import ComputedAttribute
from ZODB.PersistentMapping import PersistentMapping
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from OFS.ObjectManager import ObjectManager
from Organisation import Organisation

class ZOrganisation(Organisation, ObjectManager, Item,
                    Globals.Persistent, Implicit, RoleManager):

    """ Zope wrapper for Organisation """

    meta_type = 'Organisation'

    manage_options=(
        ObjectManager.manage_options +
        Item.manage_options + RoleManager.manage_options
        )
...

At this point, the _employees and _organisations PersistentMappings become obsolete, because ObjectManager persists objects in its _objects attribute. We are no longer compatible with the API of our Python application, and we may as well drop Organisation as base class. The purpose of the latest modification is not to stay compatible, but to illustrate the functionality Zope offers out of the box.

After more digging you will discover that Zope has a Folder class. We can use it to further simplify our implementation, since it imports all the classes needed for a container implementation. Over and above the base classes we import, it subclasses FindSupport and PropertyManager. Here is the final implementation of our Organisation class in Zope, short and sweet:


import Globals
from OFS.Folder import Folder
from ComputedAttribute import ComputedAttribute
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from interfaces import IOrganisation

class ZOrganisation(Folder):
    """ Organisation """

    __implements__ = IOrganisation

    meta_type = 'Organisation'

    _properties=Folder._properties + (
        {'id':'name', 'type': 'string','mode':'wd'},
        {'id':'description', 'type': 'text','mode':'wd'},
    )

    def _getTitle(self):
        return getattr(self, 'name', '')
    title = ComputedAttribute(_getTitle, 1)

Globals.InitializeClass(ZOrganisation)

manage_addZOrganisationForm = PageTemplateFile(
    'www/addZOrganisation.zpt', globals(),
    __name__='manage_addZOrganisationForm')

def manage_addZOrganisation(dispatcher, id, REQUEST=None):
    """Add a ZOrganisation instance
    """
    ob = ZOrganisation(id)
    dispatcher._setObject(id, ob)
    ob = dispatcher._getOb(id)
    ob.manage_changeProperties(REQUEST)

    if REQUEST is not None:
        return dispatcher.manage_main(dispatcher, REQUEST, update_menu=1)

First, notice that we are defining _properties, a sequence of dictionaries that predefines properties for subclasses of PropertyManager. Each dictionary requires id and type keys, where id is the name of the property, and type can be one of the following values: float, int, long, string, lines, text, date, tokens, selection, or multiple selection.

If you specify selection or multiple selection as type, an additional key, select_variable, is required. The value must be the name of a variable or a method that returns a list of strings from which a selection can be made, e.g.:


genders = ('female', 'male')

_properties = (
    {'id':'gender', 'type': 'selection', 'mode':'wd',
        'select_variable': 'genders'}

mode is an optional key, determining whether a property can modified or deleted:


'mode': ''   # property is listed but readonly
'mode': 'w'  # property can be modified, but can't be deleted
'mode': 'd'  # property can be deleted, but can't be modified
'mode': 'wd' # property can be modified and deleted

Secondly, we modified the constructor method, manage_addZOrganisation, to only take id and REQUEST as parameters. We don't need to enumerate the properties of our class, since PropertyManager's manage_changeProperties method will automatically find them in the REQUEST.

Since we deleted the ZOrganisation's constructor method, __init__, we inherit Folder's __init__ which only takes id as argument. So we instantiate ZOrganisation with a single id value:


ob = ZOrganisation(id)

After persisting the new instance with _setObject, you will notice that we assign the output of dispatcher._getOb(id) to ob:


ob = dispatcher._getOb(id)

The _getOb call automatically wraps the newly created object in an acquisition context. Although in this case the subsequent call to manage_changeProperties doesn't require a wrapped object, it is wise to first wrap a newly created instance before doing anything else with it.

Besides manage_changeProperties, the PropertyManager API includes a number of methods for property management (see PropertyManager.py for a full list):


def hasProperty(self, id):
    """Return true if object has a property 'id'"""

def getProperty(self, id, d=None):
    """Get the property 'id', returning the optional second
    argument or None if no such property is found."""

def propertyIds(self):
    """Return a list of property ids """

def propertyValues(self):
    """Return a list of actual property objects """

def propertyItems(self):
    """Return a list of (id,property) tuples """

def manage_addProperty(self, id, value, type, REQUEST=None):
    """Add a new property via the web. Sets a new property with
    the given id, type, and value."""

def manage_delProperties(self, ids=None, REQUEST=None):
    """Delete one or more properties specified by 'ids'."""

Just like we can use Folder as class for common container types, we can use SimpleItem instead of Item for simple non-container types. SimpleItem already subclasses Item, Globals.Persistent, Acquisition.Implicit, and AccessControl.Role.RoleManager, which gives us another saving on imports (it doesn't subclass PropertyManager though, so we still need to import this if we want property management):


import Globals
from ComputedAttribute import ComputedAttribute
from OFS.SimpleItem import SimpleItem
from OFS.PropertyManager import PropertyManager
from Products.PageTemplates.PageTemplateFile import PageTemplateFile

class ZEmployee(SimpleItem, PropertyManager):
    """ Zope wrapper for Employee """
...
Q:

Modify the rest of the ZEmployee module based on the changes we made to ZOrganisation. Here are a few things to keep in mind:

  • If you remove Employee as base, ZEmployee won't have an __init__ method, since neither SimpleItem nor PropertyManager has one.

  • You will have to recreate the getFullName method.

A:

See source/zopeproduct/step3/Organisations/ZEmployee.py

Once you've modified ZEmployee, re-start Zope, create an instance of both classes, and notice that both instances now have a few more familiar tabs.

Note

The existing instances of ZEmployee and ZOrganisation are broken since they are missing state introduced by the new classes we subclassed. If you click on any of these instances, you will see a traceback informing you exactly how they are broken.

Public faces for our classes

Creating a public interface for a class is very similar to creating management views. The only difference lies in the security settings. In addition to security declarations, Zope restricts access to views starting with manage_ to users with the Manager role. If we want to provide our ZEmployee instance with a default public interface, we can create another PageTemplateFile instance using the the canonical index_html as name:


...

class ZEmployee(SimpleItem, PropertyManager):
    """ Employee """

...

manage_options=(
    PropertyManager.manage_options + SimpleItem.manage_options +
    ( {'label':'View', 'action':''},)
    )

...

security.declarePublic('getFullname')
index_html = PageTemplateFile('www/index', globals())

...

Notice how we modified manage_options to add a convenient View tab for testing the public interface. Finally, create a PageTemplate named index.zpt on the filesystem, in the www directory:


<html>
<body>
    <table>

    <tr>
    <th>id</th><td tal:content="here/id">pete</td>
    </tr>

    <tr>
    <th>Name</th><td tal:content="here/name">Pete</td>
    </tr>

    <tr>
    <th>Surname</th><td tal:content="here/surname">Smith</td>
    </tr>

    <tr>
    <th>Age</th><td tal:content="here/age">22</td>
    </tr>

    <tr>
    <th>Date appointed</th><td tal:content="here/date_appointed">2003/10/10</td>
    </tr>
    </table>
</body>
</html>
Document Actions