Log in |
ZPatterns HowtoContents
IntroductionThis 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 ZPatternsThe 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 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 ConceptsThere 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: DataSkinAn 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 SpecialistSpecialists 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?
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 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 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 WHEN OBJECT ADDED CALL sqlInsert( oid = self.id, Country = self.Country, etc.) A simple ZClass-based exampleOK, 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
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
Do exactly the same for Employees and Organisations, setting
TODO: Incorporate RemoteRackBTree as option for "Store 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
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, You have already seen 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
Methods for retrieval Go to the "Methods" tab of the return container.defaultRack.getItems() Go to the "Methods" tab of the defaultRack in items = [] for id in container.getPersistentItemIDs(): items.append(container.getItem(id)) return items You'll notice that the Specialist's 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:
In the Basic view for Organisations Go to the "Methods" tab of the <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 <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 Still under the "Methods" tab of <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 Add a Page Template called <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 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 Create a directory in your Zope Products folder called
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
|