You are not logged in Log in Join
You are here: Home » Members » upfront » ZPatterns Howto

Log in
Name

Password

 

ZPatterns Howto

Contents

  • Introduction
  • The current state of ZPatterns
  • When should you use ZPatterns?
  • New concepts
  • A simple ZClass-based example
  • A simple filesystem-based example
  • Making life easier with an SQLRack
  • ZPatterns and CMF Skins
  • Conclusion

Introduction

This HOWTO is long overdue. I feel compelled to write it, because a lot of people still show interest in ZPatterns, and many more are still using it. A lot has been said about all the wonderful concepts employed in the ZPatterns framework, but there is almost no documentation showing how ZPatterns make complex things simple. I will try avoid heavy computer science jargon and focus on practical use cases.

This HOWTO assumes knowledge of ZClasses, Zope Python Products, ZSQLMethods and Python Scripts. If you have written a Zope product with ZClasses or with Python classes, I promise that you will know how to cook up a ZPatterns app after reading this HOWTO.

The current state of ZPatterns

The last version of ZPatterns available from the product page is 0.4.3b2. This version is no longer maintained and most people are using Steve Alexander's repackaging, under the name ZedPatterns, which includes some patches to ZPatterns and contains his TransactionAgents. They were mainly announced and distributed via the ZPatterns mailinglist. Steve has a download page at: http://www.cat-box.net/steve/TransactionAgents This works happily with Zope 2.4.x and 2.5.x.

To make things easier I have bundled Steve's ZedPatterns/TransactionAgents and LoginManager with some Linux and Windows binaries of the DynPersist.c file that you can download here. You also need to install my ZPatternsExtensions.

There is also a ZPatterns mailing list" where you can ask questions and report problems or bugs that you find.

Before you continue, make sure that you have successfully installed the ZPatternsBundle and ZPatternsExtensions by following the installation instructions provided with it. Feel free to ask for help on the ZPatterns mailing list if you have trouble installing the software.

When should you use ZPatterns?

The obvious reason for using ZPatterns is to separate business logic from data management. If you want to ensure that developers or users of your product will be able to choose whether they want to store their data in a relational database, in the ZODB, or in any other datastore, then ZPatterns is a good choice. (In my opinion we might have had more collaboration on Zope products and fewer people feeling that they need to make their own product simply because they need to use a different datastore.)

If you want to ensure that services offered by your product integrate well with each other, and also integrate with other ZPatterns applications, then ZPatterns will give you great joy.

For example: suppose you have organisations, contact persons and employees. They all have addresses. With ZPatterns, you can provide a Specialist to manage addresses, and use SkinScript to give all organisations, contact persons and employees an address. In this way, all the methods and attributes of addresses are available on the objects that have addresses. Further, the address Specialist is now also available for reuse by third-party applications that need to manage addresses.

New Concepts

There are a couple of new concepts that you should become familiar with before you start. DataSkins, Specialists and Racks and a small glue language called SkinScript play the most important part in a ZPatterns application. Here are some definitions to get started:

DataSkin

An object that inherits from DataSkin delegates the manipulation of its attributes and propertysheets to its data manager. The data manager can fetch attributes from other objects, from RDBs, or can compute them dynamically.

All classes in your problem domain must inherit from DataSkin. TODO: elaborate

Specialist

Specialists are object managers that provide services belonging to the class of object they manage. As such, they might be called "service objects", and take center stage in ZPatterns. It's easier to give you an idea of what a Specialist is by an example:

In an application that manages addresses, we will create an Address class as well as a Specialist called Addresses or AddressManager. So what will the Addresses Specialist do?

  • First, the specialist will be responsible for adding and deleting addresses and will provide us with the user interface to do so. There will be absolutely no storage-specific code in the Specialist, so that adding/deleting will be operations on pure Zope objects. The Specialist delegates interaction with the datastore to Racks.
  • To be true to its name, it will also have "specialist" methods such as getAddressesForOrganisation, getAddressByStreet, etc.

Rack

Racks are data managers, and talk directly to the datastore. A Rack manages a single class, but one can have multiple Racks for a single class. For example, if you wanted, you could have a Rack (let's call it zRack) to manage only those attributes of your Address instances that are stored in the ZODB, and another rack (call it ldapRack) managing those Address attributes that are stored on an LDAP server.

Although this won't happen often, it is thinkable to store properties of a class in the ZODB and duplicate them in an SQL store, or to store some properties in the ZODB and others in an SQL store -- in both cases you will need a Rack per datastore.

Back to our Address example: when the Addresses Specialist adds an address instance, ZPatterns generates an ADDED event. Such ZPatterns events are handled by SkinScript, a "little language" for data managers.

Through SkinScript, the Rack will call the approriate method to dispatch data to the backend. If we are using a SQL backend this will obviously be a ZSQLMethod, but if we are using the ZODB then ZPatterns takes care of it.

SkinScript

"Not another language!", I hear some people moan, "why not just use Python?" I can assure you that SkinScript takes 20 minutes to learn and will give you tremendous joy. It's a little language that does a few particular things well. (The term, little language, comes from Jon Bentley's column in Communications of the ACM 29:8 (August 1986), pp. 711-721. He points out that much of what we do as programmers can be thought of as creating "little languages" that are suited to particular small tasks, so it's not an unusual practice. Another "little language" that more people will be familiar with is regular expressions. Who'd like to do them in Python?)

As mentioned above, SkinScript is a glue language. It glues the object to its datastore, and it glues objects to other objects. For the ADDED event mentioned above, we will have a single line of SkinScript that maps the properties of our object to the arguments of a SQLMethod, eg.:

            WHEN OBJECT ADDED CALL sqlInsert(
                oid = self.id,
                Country = self.Country, etc.)

A simple ZClass-based example

OK, now the fun starts!

We'll have a through-the-web example and a filesystem-based one. Both examples will have the classes Employee, Organisation and Address. The Employee and Organisation classes will have Addresses associated with them.

Create the DataSkin ZClasses

  1. Go to the Products folder in the Zope Management Interface (ZMI) and create a Product called ContactDirectory.
  2. Create ZClasses with ids Employee, Organisation and Address inside your new Product. Make the meta_type the same as the id for each class. Be sure to deselect "Create constructor objects" because we get basic constructors for free with ZPatterns. Select ZPatterns: Dataskin as a base class for all the classes. As mentioned above, the DataSkin base class makes it possible for the class to delegate manipulation of attributes to a Rack, its data manager. Later on, we will tell each Rack what class it should manage.
  3. Go to the Property Sheets tab of each ZClass and add a DataSkin Attribute Property Sheet called Basic for each ZClass. Common Instance Property Sheet is of no use to ZPatterns and can be ignored.
  4. Add the properties PayNumber, Name, Surname and AddressID, all of type string, to the Basic propertysheet of the Employee ZClass.
  5. Add the properties Name and AddressID, both of type string, to the Basic propertysheet of the Organisation ZClass.
  6. Add the properties Street, City, Country, ZipCode, all of type string, to the Basic propertysheet of the Address ZClass.

Create the Specialists

Specialists are folderish objects. I often use them to organise the application space even if the top level or master Specialists don't do anything special. For our example, we will nest the Specialists for Employees, Organisations and Addresses under a master Specialist called ContactDirectory.

  1. Through the ZMI, create a Specialist in the root of your Zope instance with the id ContactDirectory. This Specialist doesn't need a Rack, so select "(none)" for the option "Create Default Rack of type" when you create it.
  2. Inside ContactDirectory, add Specialists with ids Employees, Organisations and Addresses. As a naming convention I use the plural of the metatype as the name of a Specialist -- it's discoverable, and looks nice in urls: eg. Organisations/<organisation_id>. Select Rack for the option "Create Default Rack of type" when you create the Specialists.
  3. Browse to the Addresses Specialist, go to its Racks tab, browse to the defaultRack and go its Storage tab. The time has come to marry the DataSkin to its Rack. Select ContactDirectory: Address for the option "Class for stored items". This tells the DataSkin what its Rack is. Since we are using the ZODB as storage this time round, you should leave "Objects are:" set to "stored persistently". Later on, when we switch to a SQL database I will explain what the "loaded by accessing attribute" option is all about (but you can probably guess already :).

    Commit your changes by clicking on "Change Storage Settings".

Do exactly the same for Employees and Organisations, setting Employee as "Class for stored items" for the Employees defaultRack, and Organisation as class for the Organisations defaultRack.

TODO: Incorporate RemoteRackBTree as option for "Store persistent data".

Create SkinScripts

We don't need SkinScript to store and retrieve attributes for our DataSkin if we use the ZODB as storage -- ZPatterns has special PlugIns called PersistentSheets and PersistentAttributes that take care of that. For this example, we only need SkinScript to compute an Address attribute for Organisation and Employee instances.

  1. Go to the DataPlugIns tab of the defaultRack of the Organisations Specialist. Add a SkinScript Method with the id OrganisationSkinScript. Edit OrganisationSkinScript and type in the following:
                WITH Addresses.getItem(self.AddressID)
                    COMPUTE Address=RESULT or NOT_FOUND
    
  2. Go to the DataPlugIns tab of the defaultRack of the Employees Specialist. Add a SkinScript Method with the id EmployeeSkinScript. Edit EmployeeSkinScript and type in the following:
                WITH Addresses.getItem(self.AddressID)
                    COMPUTE Address=RESULT or NOT_FOUND
    

At this point all the components of our app are wired together: DataSkins know their Racks, Racks have SkinScript and Persistence PlugIns to handle attribute manipulation and Specialists know to which Racks they should delegate object creation and retrieval.

Before we start building a user interface and services for our application, you need to become familiar with two methods that are central to ZPatterns development namely, newItem and getItem. Both are methods of a Specialist.

You have already seen getItem used in the SkinScript above. As can be deduced from its use, getItem retrieves an instance for a given key. getItem takes the id of the desired object as only parameter.

newItem is ZPatterns's builtin constructor. Whenever you want to create a new instance of a class, you call newItem with the id of the new instance as parameter. I find esthetically very pleasing to say MySpecialist.newItem(new_id) instead of context.manage_addProduct['MyProduct'].addSomething.

Create the user interface and Specialist services

First, create (1) a basic form to display a list of instances, (2) forms for adding instances, and (3) methods to add and delete instances. It is enough to do this for the Organisations and Employees Specialists.

Methods for retrieval

Go to the "Methods" tab of the Organisations Specialist and create a PythonScript with the id getItems and the following source:

            return container.defaultRack.getItems()

Go to the "Methods" tab of the defaultRack in Organisations and add a PythonScript with the same id, 'getItems':

             items = []

             for id in container.getPersistentItemIDs():
                 items.append(container.getItem(id))

             return items

You'll notice that the Specialist's getItems method just passes on the call to the defaultRack. This style of coding is an example of Demeter's law. From the ZPatterns Wiki:

The Law of Demeter says that if I need to obtain a service from an object's sub-object, I should instead make the request of the parent object and let it propagate the request (by method delegation) to any relevant sub-objects. Thus, objects avoid meddling with other objects' innards.

Some of the benefits of this style of coding are:

  • You know that the location of all code that talks to the backend is inside the Rack. So if you want to change the backend from the ZODB to a SQL RDBMS you only have to go the Racks of your application to modify storage specific code.
  • Specialists are the focal point of your application. You don't "meddle" with the datastore -- instead, you ask the Specialist for whatever service you require.

In the getItems method on the Rack you will notice that we make a call to getPersistentItemIDs. This methods returns all the ids of the instances stored in the Rack. When we store objects in the ZODB, they are stored in BTree objects and just as the name implies, the Rack provides this method to retrieve the ids of all persistent objects.

Basic view for Organisations

Go to the "Methods" tab of the Organisations Specialist and add a PageTemplate called index_html with the following source:

            <html>
            <head><title tal:content="template/title">The title</title>
            </head>
            <body>

            <h2>Organisations</h2>

            <form action="." method="post">
            <table tal:condition="container/getItems"
                border="1" cellspacing="0" cellpadding="3">

            <tr><th>Id </th><th>Name </th><th>Address </th>
            </tr>

            <tr tal:repeat="organisation container/getItems">
            <td><input type="checkbox" name="ids:list"
                tal:attributes="value organisation/id">
            </td>
            <td><a tal:content="organisation/Name"
                tal:attributes="href organisation/id">Name</a>
            </td>
            <td tal:content="organisation/Address/Country">Country
            </td>
            </tr>

            <tr><td colspan="3">
            <input type="submit" name="addInstanceForm:method" value="Add">
            <input type="submit" name="deleteInstances:method" value="Delete">
            </td></tr>
            </table>

            <input tal:condition="not:container/getItems"
            type="submit" name="addInstanceForm:method" value="Add">
            </form>

            </body>
            </html>

If you "Test" the PageTemplate at this stage, you will see only the "Add" button, since no organisations have been added.

The only special thing to notice in the above source is that we reference Address as an attribute of Organisation:

            <td tal:content="organisation/Address/Country">Country

This is made possible by the SkinScript we defined for the Organisation class:

            WITH Addresses.getItem(self.AddressID)
                COMPUTE Address=RESULT or NOT_FOUND

We say that Address is a computed attribute for Organisation. The magic behind this will become visible as soon as we add the first Organisation.

Basic adding form for Organisations

Still under the "Methods" tab of Organisations, create a PageTemplate called 'addInstanceForm':

            <html>
            <head><title tal:content="template/title">The title</title>
            </head>
            <body>

            <h2>Add Organisation</h2>

            <form action="addInstance" method="post">
            <table> 
            <tr><th>Id</th>
                <td><input name="id" value=""></td>
            </tr>

            <tr><th>Name</th>
                <td><input name="Name" size="50" value=""></td>
            </tr>

            <tr><th>Street</th>
                <td><input name="Street" size="50" value=""></td>
            </tr>

            <tr><th>City</th>
                <td><input name="City" size="50" value=""></td>
            </tr>

            <tr><th>Country</th>
                <td><input name="Country" size="50" value=""></td>
            </tr>

            <tr><th>ZipCode</th>
                <td><input name="ZipCode" size="50" value=""></td>
            </tr>

            <tr><td colspan="2">
                <input type="submit" value="  Add  ">
            </td>
            </tr>

            </table>
            </form>

            </body>
            </html>

      Methods to add and delete instances

        Create a PythonScript called 'addInstance' with the parameters
        'REQUEST=None, **kw', and with content::

            properties = {}

            if REQUEST:
                property_items = REQUEST.items()
                for key, value in property_items:
                    properties[key] = value

            if kw:
                for key, value in kw.items():
                    properties[key] = value

            properties['AddressID'] = (
                'organisation_address_' + properties['id'])

            new_address = context.Addresses.newItem(properties['AddressID'])
            new_address.propertysheets.Basic.manage_changeProperties(properties)

            new_organisation = container.newItem(properties['id'])
            new_organisation.propertysheets.Basic.manage_changeProperties(properties)

            if REQUEST:
                return container.index_html()
            else:
                return ni

        We compute the 'AddressID' as the prefix 'organisation_address_'
        plus the id of the 'Organisation' specified in the
        addInstanceForm.  I don't recommend creating ids in this manner,
        we only do this to ensure that Address ids for 'Organisations'
        and 'Employees' don't collide.  I normally use an
        'ObjectCounter' Specialist that generates unique ids per
        metatype, but introducing this approach would complicate the
        HOWTO too much.

        We call the constructor 'newItem' on the 'Addresses' and
        'Organisations' Specialists to obtain new instances of the class
        that it manages. We modify the properties on 'new_address' and
        'new_organisation' like you would for any other ZClass instance:
        by calling 'manage_changeProperties' on its propertysheets.

      Method to delete instances

        Create a PythonScript called 'deleteInstances' with the
        parameters 'REQUEST=None, ids=[]' and this content::

            if ids:
                for id in ids:
                    item = container.getItem(id)
                    address = context.Addresses.getItem(item.AddressID)
                    address.manage_delete()
                    item.manage_delete()

            if REQUEST:
                return container.index_html()

        I hope the code is pretty clear so far :)  We ask the Specialist
        for an instance by calling 'getItem', and we delete the instance
        by calling its 'manage_delete' method.

       Take two: decoupling the 'Organisations' and 'Addresses' Specialists

        While the above code is perfectly correct, it misses a piece of
        ZPatterns zen. The 'deleteInstances' method in step 5 above can
        be rewritten as follows::

             if ids:
                 for id in ids:
                     item = container.getItem(id)
                     item.manage_delete()

             if REQUEST:
                 return container.index_html()

        Simply delete the item! Won't this leave an orphaned 'Address'
        instance lying around? No, that's what SkinScript is for::

            WHEN OBJECT DELETED CALL
                Addresses.getItem(self.AddressID).manage_delete()

        Add those two lines to 'OrganisationSkinScript', and the Address
        of an Organisation will be deleted along with the Organisation
        itself.

      Testing

        Go to 'index_html', click on "Test", and add and delete some
        Organisations to see how it works.

Create the public view and interface for editing an Organisation

Go to the Organisation ZClass of the ContactDirectory Product in the Control Panel that you created earlier.

Add a Page Template called index_html with the following content:

            <html>
            <head><title tal:content="template/title">The title</title>
            </head>
            <body>

            <h2 tal:content="here/Name">Organisation Name</h2> 
            <a href="..">Return to list</a>

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

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

            <tr><th>Street</th>
                <td tal:content="here/Address/Street">Street</td>
            </tr>

            <tr><th>City</th>
                <td tal:content="here/Address/City">City</td>
            </tr>

            <tr><th>Country</th>
                <td tal:content="here/Address/Country">Country</td>
            </tr>

            <tr><th>ZipCode</th>
                <td tal:content="here/Address/ZipCode">ZipCode</td>
            </tr>

            <tr><td colspan="2">
                <form action="editForm" method="post">
                <input type="submit" value="  Edit  ">
                </form></td>
            </tr> 
            </table> 
            </body>
            </html>

Add another Page Templage called 'editForm':

            <html>
            <head><title tal:content="template/title">The title</title>
            </head>
            <body>

            <h2 tal:content="here/Name">Organisation Name</h2>

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

            <table> 
            <tr><th>Name</th>
                <td><input name="Name" size="50"
                       tal:attributes="value here/Name"></td>
            </tr>

            <tr><th>Street</th>
                <td><input name="Street" size="50"
                       tal:attributes="value here/Address/Street"></td>
            </tr>

            <tr><th>City</th>
                <td><input name="City" size="50"
                       tal:attributes="value here/Address/City"></td>
            </tr>

            <tr><th>Country</th>
                <td><input name="Country" size="50"
                       tal:attributes="value here/Address/Country"></td>
            </tr>

            <tr><th>ZipCode</th>
                <td><input name="ZipCode" size="50"
                    tal:attributes="value here/Address/ZipCode"></td>
            </tr>

            <tr><td colspan="2">
                <input type="submit" value="Save Changes"></td>
            </tr> 
            </table>
            </form> 
            </body>
            </html>

Add a Python Script called edit with the parameters REQUEST=None, **kw, and with content:

            properties = {}

            if REQUEST:
                property_items = REQUEST.items()
                for key, value in property_items:
                    properties[key] = value

            if kw:
                for key, value in kw.items():
                    properties[key] = value

            address = context.Addresses.getItem(container.AddressID)
            address.propertysheets.Basic.manage_changeProperties(properties)

            container.propertysheets.Basic.manage_changeProperties(properties)

            if REQUEST:
                return container.index_html()
            else:
                return container

This concludes the ZClass based example.

A simple filesystem-based example

In this step we will create DataSkins on the filesystem and use them with the Specialists we created in the previous chapter. I will only show you how the Organisation DataSkin looks -- creating the Address and Employee DataSkin should be obvious after this. We'll suffix the filesystem stuff with FS so that it doesn't conflict with the product and classes we created in the ZODB

Create a directory in your Zope Products folder called ContactDirectoryFS.

Create a new module called OrganisationFS.py:

        from OFS.SimpleItem import Item
        from Acquisition import Implicit
        from Globals import InitializeClass
        from Products.PageTemplates.PageTemplateFile import PageTemplateFile
        from Products.ZPatterns.DataSkins import DataSkin, _ZClass_for_DataSkin
        from AccessControl.Role import RoleManager
        from AccessControl import ClassSecurityInfo
        from OFS.PropertyManager import PropertyManager

        class OrganisationFS(DataSkin, Implicit, RoleManager, PropertyManager, Item):
            """ Creme Customer Implementation """

            meta_type = 'OrganisationFS'
            icon = 'www/organisation.gif'

            security = ClassSecurityInfo()

            _properties = (
                {'id':'Name', 'type':'string', 'mode':'w'},
                {'id':'AddressID', 'type':'string', 'mode':'w'},
                )

            security.declareProtected('View management screens',
                                    'manage_propertiesForm')
            manage_options = PropertyManager.manage_options + \
                            RoleManager.manage_options + \
                            Item.manage_options

            index_html = PageTemplateFile('www/index', globals(),
                                            __name__='index_html')

            editForm = PageTemplateFile('www/editForm', globals(),
                                        __name__='editForm')

            def edit(self, REQUEST=None, **kw):
                """ Edit OrganisationFS """

                properties = {}

                if REQUEST:
                    property_items = REQUEST.items()
                    for key, value in property_items:
                        properties[key] = value

                if kw:
                    for key, value in kw.items():
                        properties[key] = value

                address = self.aq_parent.Addresses.getItem(container.AddressID)
                address.manage_changeProperties(properties)

                self.manage_changeProperties(properties)

                if REQUEST:
                    return self.index_html()
                else:
                    return self

        InitializeClass(OrganisationFS)

        class _ZClass_for_SimpleDataskin(_ZClass_for_DataSkin):
            _zclass_ = OrganisationFS

        def initialize(context): 
            context.registerZClass(_ZClass_for_SimpleDataskin)

Create the Product's __init__.py

::

import OrganisationFS

def initialize(context): OrganisationFS.initialize(context)

Create the directory www and create the PageTemplates index.zpt and editForm.zpt with the same content as used for the Organisation ZClass that we created in the previous chapter.

Restart Zope and browse to the Organisation Specialist's defaultRack. If you go the Storage tab you will notice that OrganisationFS is now available in the dropdown opposite Class used for stored items.

Making life easier with an SQLRack

I haven't really explained how to use ZPatterns with an external data source like a SQL backend yet. This will become apparent through the binding code that is automatically generated by a SQLRack.

In this step we will set up a connection to a MySQL database and create a SQLRack that will generate all the SQL, SkinScript and Python Scripts needed to store an object's data in a MySQL database. It should be fairly easy to subclass the SQLRack so that it generates SQL for other SQL databases.

  1. Go to the ContactDirectory specialist and add a new ZMySQL Database connection.
  2. Go to the Organisation Specialist's Racks tab and delete the defaultRack.
  3. Add a SQLRack under the Racks tab and use sqlRack as id so that we can refer to it without confusion.
  4. Browse to the sqlRack's Storage tab and select the ZClass "ContactDirectory: Organisation", as "Class for stored items". The SQLRack can generate binding code for ZClass Dataskins or for Dataskins that reside on the filesystem.
  5. The next option on the form ("Objects are loaded by accessing attribute") used to confuse me a lot when I first saw it, so I think it needs a good explanation. In very simple terms this field refers to the key field of the table or class in your external datasource. This field cannot be named "id" since this is the attribute that Zope uses to uniquely identify objects in a Zope context. You'll notice that the SQLRack set this to oid by default. We essentially map id in the Zope context to oid in our database.

    A Specialist retrieves an object by id when you call its getItem method, or access the url "/MySpecialist/id" (which also results in a call to getItem). If you are using an external datasource, then the Rack will first see what query (like a SQL select) is used to retrieve data from that datasource, call that query and then inspect the value of the attribute you specify in this step to check if a match was found.

    If you don't let the SQLRack create the table for you, then you have to make sure that you have "oid" as primary key in your Organisation table.

  6. Select the ZMySQL Connection you created as the Connection ID and click on Change Storage Settings. The SQLRack will now create all the necessary ZSQL Methods and SkinScript so let's see what it did.
  7. Go to the Methods tab of sqlRack. Here you'll notice that it created the ZSQL Methods sqlCreate, sqlDelete, sqlInsert, sqlSelect and sqlDelete. These methods shouldn't need any explanation so let's see what the SkinScript looks like.

SkinScript for sqlRack

Go to OrganisationSkinScript on the Rack's Data Plug-ins tab. You should see the following:

        WHEN OBJECT ADDED CALL sqlInsert(
            oid = self.id,
            Name = self.Name,
            AddressID = self.AddressID
            )

        WHEN OBJECT DELETED CALL sqlDelete(oid=self.id)

        WHEN OBJECT CHANGED STORE
            Name,
            AddressID
        USING sqlUpdate(
            oid = self.id,
            Name = self.Name,
            AddressID = self.AddressID
            )

        WITH QUERY sqlSelect(oid=self.id) COMPUTE
            oid,
            Name,
            AddressID

        WITH Addresss.getItem(self.AddressID)
            COMPUTE Address=RESULT or NOT_FOUND

You will notice that SkinScript was generated for object adding, deleting, changing and retrieval:

           # Adding
           WHEN OBJECT ADDED CALL *expression* 

           # Deleting
           WHEN OBJECT DELETE CALL *expression* 

           # Changing 
           WHEN OBJECT CHANGED STORE *attributelist* USING *expression* 

           # Object retrieval (from external datasource) 
           WITH QUERY *expression* COMPUTE *attributelist*

           # Object retrieval (from other Specialist) 
           WITH *expression* COMPUTE *attributelist*

Note that the SkinScript generated for retrieving the Organisation record with the sqlSelect statement computes the oid attribute. It is very important that the attribute that we specified on the storage tab for "Objects are loaded by accessing attribute" is computed here, otherwise our object will not be retrieved.

For extensive help on the SkinScript syntax you can click on the help link when looking at any SkinScript method.

The SQLRack was also ambitious enough to generate the SkinScript to compute the Address attribute for an 'Organisation':

        WITH Addresss.getItem(self.AddressID)
            COMPUTE Address=RESULT or NOT_FOUND

The SQLRack generates SQL and SkinScript based on a simple naming convention:

  • If a property ends in ID, SQLRack assumes this is a reference to an instance of another class.
  • If a property ends in IDs, SQLRack assumes this is a reference to a list of instances of another class and it will generate SkinScript, SQL and exta Python Scripts needed to bind with multiple SQL tables.
  • Any text before ID or IDs is assumed to be the name of the class. It is further assumed that the plural of the class name is the Specialist. The plural is simply calculated by adding an s to the class name, which is obviously wrong in the case of Address. You should manually fix this up. I think this is excusable if it reduces the amount of coding to the fixing of a spelling error rather than typing all of the SkinScript yourself.

The most important assumption that the SQLRack makes at this stage of its development is that you will review the SkinScript and SQL that it generates ;-)

Adding the database tables

Finally you can run the sqlCreate ZSQL Method to create the table in the database and test adding, editing and deleting of Organisations through the user interface we created earlier.

ZPatterns and CMF Skins

Every product developer should strive to allow users of their product the maximum amount of customisability. Whereas ZPatterns solves this partially with its object collaboration and data delegation services, CMF Skins solves direct customisation of object services, especially the look and feel of the product.

I created SkinnableSpecialist, SkinnableRack and FSSkinScript to realise the power of both ZPatterns and CMF Skins in one solution. This allows you to create standard Zope objects such as Python Scripts, Page Templates and DTML Methods on the filesystem and make Product distribution much easier. I added FSSkinScript to this list and Chris Withers of NIP has created FSSQLMethod which makes it possible to create SkinScript and ZSQL Methods on the filesytem.

In this chapter we will copy methods created in earlier chapters to the filesystem, showing how they continue to work in a different context.

  1. Inside our ContactDirectoryFS filesystem product, create a directory to hold all our skins, predictably called skins. Inside this skins directory, create a directory Organisations for all the skins of the Organisations Specialist, and Organisations_defaultRack as its sibling, for all the rack skins inside the skins directory.
  2. Copy the Page Templates index_html and addInstanceForm from the Organisations Specialist in the ZODB and save them as index_html.pt and addInstanceForm.pt in the Organisations skins directory.
  3. Copy the Python Scripts addInstance, deleteInstance and getItems from the Organisations Specialist in the ZODB and save them as addInstance.py and deleteInstance.py in the Organisations skins directory. Also copy getItems in the defaultRack to the Organisations_defaultRack directory on the filesystem. I suggest you use FTP to get the Python Scripts on the filesystem so that the script headers will be downloaded with the rest of the content.
  4. Copy OrganisationSkinScript to the Organisations_defaultRack skins directory as OrganisationSkinScript.ss.
  5. Go to the ContactDirectory Specialist in the ZMI. Add a SkinnableSpecialist with the id SkinnableOrganisations and SkinnableRack as the rack type.
  6. Go to SkinnableOrganisations. You will notice that the CMF portal tool portal_skins is listed under the Contents tab. portal_skins has one directory view named default, and a folder custom where customisations will live. Modify the Filesystem path of the default Directory View to Products/ContactDirectoryFS/skins/Organisations. If you now look at the contents of default you will see the methods on the filesystem.
  7. Go to the defaultRack of SkinnableOrganisations and, as in the previous step, modify the Filesystem path of the default directory view to Products/ContactDirectoryFS/skins/Organisations_defaultRack
  8. Go back to the default Directory View of the defaultRack and click on OrganisationSkinScript. As soon as you do this, the SkinScript for the rack gets compiled.

    This is a bit of a kludge since you have to view the FSSkinScript before it gets compiled to attribute providers for the rack but I think one can solve this with a script in the root of your application that compiles all SkinScript before deployment. You can't really compile the SkinScript at runtime since that will introduce a huge performance burden into your application. I'm sure there are more graceful solutions to this problem and I invite ideas and comments you might have.

  9. Finally you can test adding, editing and deleting of Organisations from its user interface.

Conclusion

I really hope that this HOWTO enables you to get up and running with ZPatterns without banging your head to much. I have a whole lot of best practices and advanced tips that I plan to document as soon as I have more time. At this stage, I would like to invite comments on this HOWTO from old time zopers and newbies. If you find that I make a mental jump somewhere that you can't follow, please let me know so that I can bolt on some more steps on the ladder.