You are not logged in Log in Join
You are here: Home » Members » hathawsh » mywiki » SandBox » wikipage_view

Log in
Name

Password

 
 
FrontPage » HelpPage »

SandBox

Zope 2.2 Product Developer's Guide - Chapter III - Python Products

Approaching the Zope Product API

Extending Zope is quite simple compared to many other environments. Since Zope is based on the clear, elegant Python scripting language, Zope extensions are quick to develop and clear to maintain.

However, since they can leverage the full power of the Zope framework, writing a Zope Product is more complicated than the extremely simple extensibility of External Methods. Fortunately the step up in complexity is much less than the dramatic increase in functionality.

In this tutorial we are going to build a poll Product.

The included Python and DTML files show some of the steps in the development of a sample poll Product. The Poll.tar.gz file is the completed Product. To install the completed Product ungzip and untar it inside your Zope directory. Then shutdown and restart Zope to start using the sample poll Product.

What you need to know before you begin

Before you read this tutorial you should have a basic understanding of how Zope publishes Python objects. You should, of course, know Python reasonably well. If you've used object publishing before, perfect. If not, you might want to read an introduction to object publishing before you begin, because the Zope Product API is just a special case of object publishing programming. Finally you should have a reasonable familiarity with Zope itself. You should understand how to do things like create Folders and Documents, and use DTML to call methods, and set properties. The Zope Manager's Guide is a good place to learn how to use Zope.

Defining and analyzing your task

Writing a Zope Product, creating a website, building a database, all these activities are tasks which are done to solve specific problems. If you want to do a good job building a solution, you must understand your problem first. In the next section we'll talk about different approaches to solving problems in Zope. However, its a good idea to start with a description of the problem.

Here's a short problem description and analysis for our poll project.

Requirements

Customers manage their areas of a Zope site and want to provide information services. In this case, a customer wants to collect feedback from users regarding a certain topic. That is, they want to conduct a poll.

A poll contains ballot with question with one or more answers. The ballot is the HTML presentation of the question, answers, and control elements. Users can select only one answer for a poll. Users can either vote in the poll or view the current results or both. The results at any time are displayed by default as text using an attractive out-of-the-box format. The appearance of the ballot should be customizable, and the ballot should be able to be embedded in other documents.

Finally, a poll should allow voting by only a select group of users that have the correct credentials. That is, should be able to be private.

Analysis

The Poll appears to have actors of Customer, User, Privileged User. The Customer actor is someone able to add and manage Zope content. The User actor is someone with the Anonymous role that interacts with Zope. A Privileged User appears to be a non-Anonymous Zope User. This means that the Poll should expose some permissions for operations such as voting and presenting results, allowing the permissions to include various roles.

The Poll has a number of crisp abstractions. A Poll represents all the state and behavior needed to accomplish the task. A Ballot appears to be Document with some DTML scripting in it. A Question might not be a separate object. Answers might be separate objects that leverage the normal Zope Folder interface to manage them.

Is the Product API right for the project?

After you have described and thought about your pojects, the next step is to evaluate whether your idea would work well as a Product, or if it could be better implemented some other way. In our case we would like to build a poll product like you see on Slashdot and many other sites.

We could probably build it with SQL Methods and DTML without too much difficulty. Though this would require mucking around with the RDBMS whenever you wanted to create a new poll or modify an existing poll.

We could probably build a simple polling facility just using DTML, Documents and maybe properties. Again, this approach would probably involve more administration hassles than we would like.

Using a Zope Product we could provide easy manageabiliy to Zope users who want to create their own polls. Using a Product also gives us fine control over access controls, and gives us room to refine and develop our poll over a period of time.

So we have a marginal case for turning our idea into a Product. OK, good enough let's get going.

Getting started in plain old Python

Now that we have decided to harness the full power of the Zope Framework to create a reusable Zope Product, the first thing to do is to write a prototype in Python. This prototype will try to capture the essential algorithms of our product. It will also allow us to test the guts of the product and make sure they are sound before we start confusing ourselves with exotic Zopisms.

Here's a first cut at what a poll class might look like:

      class Poll:
          "A multiple choice poll."

          def __init__(self,question,choices):
              self.question=question
              self.choices=choices
              self.votes={}
              for choice in range(len(choices)):
                  self.votes[choice]=0

          def vote(self,choice):
              "vote for a choice"
              self.votes[choice]=self.votes[choice]+1

          def total_votes(self):
              "total number of votes cast"
              total=0
              for v in self.votes.values():
                  total=total+v
              return total

          def votes_for(self,choice):
              "number of votes cast for a given choice"
              return self.votes[choice]   

Basically our Poll object keeps track of what is going on in a dictionary that maps choices to numbers of votes. The vote method actually casts a vote. votes is a dictionary keyed the the choice index which keeps track of the number of votes for each choice. The total_votes and votes_for methods just report statistics about the poll.

Of note is the fact that we haven't made any effort to keep people from voting more than once. There are a number of ways we could handle this, for example we could somehow record the identity of everyone who voted. Then when someone tries to vote we could look them up in our list of people who have voted. This approach has some problems since it is often difficult to reasonably identify someone. Let's leave this problem unresolved for now.

Testing our initial implementation in Python

An original purpose of Zope was to hide the publishing details and allow Python developers to think in terms of Python. Thus, at this stage let's go into Python and test what we have.

OK, so does our first attempt at a poll work? Let's exercise it a little:

        def test():
            p=Poll("What's for breakfast?",["spam","eggs","toast"])
            p.vote(0) # vote for spam
            p.vote(2) # vote for toast
            p.vote(0) # vote for spam
            print p.total_votes() #returns 3
            print p.votes_for(0)  #returns 2

You can find this code in the included Poll1.py example file. Run it and see for yourself that it works.

At first glance it seems pretty reasonable. Though if you try to vote for a non-existent choice you get an KeyError. Also there's nothing forcing you to initialize your poll class with reasonable arguments. Well that's OK for now. But we may want to add some error handling later. Zope provides a number of error handling features, like automated user notification of errors, and transaction handling to abort transactions when uncaught exceptions are raised.

Adding a user interface

OK, now we have a semi-functional poll, let's add a user interface so that people can actually use it through the web. The normal Zopish way to do this is to use DocumentTemplate. A product can choose to use a full fledged Document object, or a plain old DocumentTemplate object. Let's start simple by defining a DocumentTemplate.HTMLFile to display the poll. Let's create a file called 'poll2.dtml':

        <p>&dtml-question;</p>
        <form action="vote">
        <dtml-in choices>
        <input type="radio" name="choice:int"
        value="&dtml-sequence-index;">
        &dtml-sequence-item;<br>
        </dtml-in>
        <input type="submit" value="Vote">
        </form>

Then create a class attribute for template:

        index_html=DocumentTemplate.HTMLFile("poll2.dtml")

OK, looking petty good. Now let's test it:

        def test2():
            p=Poll("What's for breakfast?",["spam","eggs","toast"])
            print p.index_html(p)

You can see the complete program in the Poll2.py example file. Run it and see for yourself that it works.

What this test function does is create a Poll instance and display it using its template. When Zope publishes an object it will publish an object's index_html method if no other method is specified. So by naming our template index_html we ensure that it is the default view of our object.

Additionally, when Zope publishes a template it will pass the template's object and the REQUEST as arguments to the template. So by calling our poll's template with itself as an argument we are emulating to a certain extent what Zope will do when it publishes our object.

If these methods seem strange to you, go back read up on Zope's object publishing system. You may also want to read some introductions to the Zope ORB and its conventions for object publishing. The basic idea of publishing an object is that a template is used to provide a skeleton of how an object should be displayed. Then when an object is published, its data is used to fill in the template. The Document Template User's Guide gives more details about templates.

Here's what we get when our Poll object is displayed:

        <p>What's for breakfast?</p>
        <form action="vote">
        <input type="radio" name="choice:int" value="0"> spam<br>
        <input type="radio" name="choice:int" value="1"> eggs<br>
        <input type="radio" name="choice:int" value="2"> toast<br>
        <input type="submit" value="Vote">
        </form>

This seems more or less right. We have the question followed by a form which allows you to select one of the choices. The form calls the poll's vote method, with the choice argument set. So far so good.

You've got a publishable object

At this point you've developed a pretty simple publishable Python object. If you are not interested in the Zope framework, you could at this point start publishing your poll objects with the Zope ORB and be done with it. Of course our poll still doesn't do a lot of things like return a response when you vote, or handle persistence. However, we do in fact have a publishable Zope object--pretty easy isn't it.

Now we can start publishing our poll object. If you're an experienced Zope user, this is old news to you. You may wish to skip ahead to the next section. If not welcome to publishable objects.

The core element in Zope is the object publishing ORB. The publisher lives in the ZPublisher.Publisher package. Zope runs the publisher for you over the web converting URLs? into object calls. After our poll object is plugged into Zope it will be published normally, by being referenced in a Zope URL. This is exactly how other Zope objects like Folders are published.

Testing the poll with the debugger

You can run the Zope publisher from the commandline to test objects before they are integrated into Zope.

The Zope debugger (formerly bobo.py) is located in the ZPublisher.Test module. It allows you to activate the publishing ORB from the command line. To test our Poll class we need to create an instance of that class and publish it. Create a new file 'poll_test.py':

        import Poll2
        p=Poll2.Poll("What's for breakfast?",["spam","eggs","toast"])

You now should have three example files, Poll1.py our original class, Poll2.py which contains the latest Poll class definition, and pole_test.py which imports Poll and creates a Poll instance.

Now you can publish the poll object by issuing this command from within the directory where your two poll files are located:

        <Zope directory>/bin/python <Zope
        directory>/lib/python/Zpublisher/Test.py poll_test p

Note, this command should be one complete line.

' refers to your Zope directory, for example, /usr/local/Zope'. This command will fire up Zope's copy of Python and run the ZPublisher.Test module with poll_test as the published module and p as the published object. (If you are using the source rather than the binary distribution of Zope you should use your own copy of Python, rather than Zope's to run the debugger.)

Here's what you should get back when you run the debugger:

        Status: 200 OK
        Content-Length: 332
        Content-Type: text/html

        <html><head>    <base href="http://127.0.0.1/poll_test/p/">
        </head>
        <p>What's for breakfast?</p>
        <form action="vote">
        <input type="radio" name="choice:int" value="0"> spam<br>
        <input type="radio" name="choice:int" value="1"> eggs<br>
        <input type="radio" name="choice:int" value="2"> toast<br>
        <input type="submit" value="Vote">
        </form>

This is letting you know that ZPublisher? will successfully publish your poll object. In fact, this is exactly what Zope will send back to your browser if you were able to publish your poll object through the web right now.

We can see that we're getting a status of 200 which is right for most conditions. Also the HTML looks pretty much like what we defined in our template. The main difference is that Zope has inserted a base href for us.

So, now let's test what will happen when you vote by submitting the form. We can do this by telling the debugger to publish the voting method like so:

        <Zope directory>/bin/python <Zope
        directory>/lib/python/Zpublisher/Test.py poll_test
        p/vote?choice:int=0

Note, this command should be one complete line.

This tells the debugger to publish the poll's vote method with an argument of choice=0. So we are simulating what would happen if the poll was published on the web and we clicked the spam radio button on the poll's form and clicked the submit button.

This is the result we get:

        Status: 204 No Content

Which is understandable considering that the vote method doesn't return anything.

So far so good. Our work with the debugger is done for now, but we may have good reason to come back to it later. The debugger can do some pretty powerful things including allow you to run the publishing process through the standard Python debugger which can come in pretty handy. Also, right now we are debugging an object which is not installed in Zope. We can however, debug objects inside Zope. The only difference is to tell the debugger that we want to operate on object inside Zope's Main module, rather our own module.

How are you doing?

If you are lost at this point you should read up on the Zope ORB and come back to this tutorial later when you have a better feeling for how Zope object publishing works.

If you're doing fine, get yourself a snack and then let's continue.

Going from publishable object to Zope Product

There is a long way between having a publishable object and having a Zope product. You have to attend to a number of things in order to turn your publishable object into a functioning Product:

Create a Product package--All Zope products need to be structured as Python packages. There are a number of details you will need to attend to in order to make your package fulfill all the Product requirements. Most of these requirements are met by including various attributes in your package's __init__.py file.

Adhere to the Product API--The Zope Framework gives a managed environment providing a number of facilities for Products. The more Zope conventions you adhere to, the more manageable your Product will be in Zope. For example Zope requires specific handling of object creation, object access controls, object meta data, etcetera. You will fulfill these requirements by inheriting from Zope Product classes, implementing appropriate methods and setting appropriate class attributes.

Product packaging

A Zope product has many attributes, but one of the most essential, is that it lives as a Python package inside the /lib/python/Products directory. Zope interprets all packages in this directory as Products, and tries to install them at startup.

To create a package you need to create a directory inside the lib/python/Products directory to hold your files. So we create a directory called Poll. Inside our package directory we need at least two files: an __init__.py file which lets Python know that our directory is a package, and additional Python files. In our case we will probably only have two Python files, __init__.py, and Poll.py.

Our package directory will hold other files too, for example, DTML files, and an icon GIF file. You may want to include other files in your package, too such as README files.

You also may choose to locate some support Python files in the shared package. The shared package is a central repository of utility modules located in lib/python/Shared. To use the shared package create a sub-package inside it with the name of your organization. This measure is to help minimize the risk of package name collisions. So for example you could create a package KingSuperInc inside the shared package. Then you could import that package like so:

        import Shared.KingSuperInc

This way you can access to your shared package from within your product Python files. You may even want to create sub-packages inside your shared package.

Beefing up the Poll class with inheritance

Now that we've got a basic publishable poll class, we're ready to start dressing up our poll in Zope finery. Zope can provide our classes with many services but let's start with the basics. One of the most common ways to give your Zope product abilities is to use standard Zope mix-in classes. Here's how we'll define our class for starters:

        class Poll(
            Acquisition.Implicit,
            Persistent,
            AccessControl.Role.RoleManager
            OFS.SimpleItem.Item,
            ):
            "A Poll product"
            ...

This is a lot of inheritance. Let's look at what we're buying.

Acquisiton.Implicit provides our poll with the ability to acquire attributes from it container objects. This probably won't be vitally important to our poll object, but it might come in handy. More importantly, acquisition is central to the Zope way. Even if we don't plan on using acquisition too much, we shouldn't assume that attributes of the poll object won't need to use it. This is less important for non-Folderish objects, but take my word for it--if you're writing a product, inherit from Acquisition.Implicit.

Persistent gives your Product the ability to have its state transparently stored and retrieved in Zope's object database. By inheriting from the class your Product will also participate in transactions, and all the other benefits of Zope's storage system. Products must inherit from this class. Using persistence requires a small amount of care and discipline. Read up on Bobo POS to find out more.

AccessControl.Role.RoleManager allows your class to use Zope's permissions machinery. By inheriting from RoleManager we allow our poll to be managed normally. For example, we will be able to have a Security tab in our poll management screen to set and modify permissions.

OFS.SimpleItem.Item is a basic Zope base class for non-Folderish Products. This class gives your Product the ability to be managed normally via the Zope management interface. By inheriting from the class your Product gains the ability to be cut and pasted, and to work with management tabs. It also gives you some standard methods including manage which is the standard management URL for Zope products. You also get the title_or_id, title_and_id, and this DTML utility methods. Finally this class gives your Product basic tree tag support.

This collection of base classes provides any simple Zope Product with a good place to start. You may want to add to or change from this list of base classes later when you have a better understanding of Zope's internals. You may also want to override some of the methods of these base classes as you gain more knowledge.

How Product creation works

Now that our poll class is starting to get closer to a working Product, we need to add some methods in order to allow Zope to manage it as a Product. By inheriting from OFS.SimpleItem.Item we gain a couple management methods, notably manage, however, will still need to provide other methods.

One of the defining characteristics of Zope Products are that they can be added to Zope Folders. To allow your Product to be created in this way you need to provide some management methods.

The normal way to accomplish Product creation is to provide two methods for each product. The first method displays a Product creation form to collect pertinent data such as the Product id, title, and various parameters. The second method is called by the creation form and it actually creates the Product object and installs it in its parent Folder.

If you are paying close attention, you may notice that these methods can't be methods of the Product's class, since they are needed before the Product object is created. In fact, these two creation methods need to be methods of the Zope Folder class.

To facilitate the Product creation process, Zope installs special Product creation methods in the Zope Folder class when it starts up. So to allow your Product to be created in Zope, you will have to write Folder methods.

Product creation form

OK, so how will we handle actually creating poll objects and installing them in Folders? It's relatively simple. Let's start with the poll creation form defined in 'pollAdd.dtml':

        <html>
          <head><title>Add Poll</title></head>
          <body bgcolor="#FFFFFF">

            <h2>Add Poll</h2>

            <form action="manage_addPoll" method="POST">
              <table cellspacing="2">

            <tr>
              <th align="LEFT" valign="TOP">Id</th>
              <td align="LEFT" valign="TOP">
                    <input type="TEXT" name="id" size="50">
                  </td>
            </tr>

            <tr>
              <th align="LEFT" valign="TOP"><em>Title</em></th>
              <td align="LEFT" valign="TOP">
                    <input type="TEXT" name="title" size="50">
                  </td>
            </tr>

            <tr>
              <th align="LEFT" valign="TOP">Poll Question</th>
              <td align="LEFT" valign="TOP">
                    <input type="TEXT" name="question" size="50">
                  </td>
            </tr>

            <tr>
              <th align="LEFT" valign="TOP">Poll Answers<br>(one per
              line)</th>
              <td align="LEFT" valign="TOP">
                    <textarea name="choices:lines" cols="50"
                    rows="10"></textarea>
                  </td>
            </tr>

            <tr>
              <td></td>
              <td><br><input type="SUBMIT" value="Add"></td>
            </tr>

            </table>
            </form>
          </body>
        </html>

What does this form do? It collects information needed to create a poll. We recognize the question and answers parameters, but what are the id and title for?

Zope Products are connected to each other in an object hierarchy. Each Folder has a number of sub-objects which are bound to its attributes. So for example a Folder might contain a Document with an id index_html. This means that the Folder's index_html attribute is bound to the folder, and that the Document's id attribute is index_html. It's pretty simple actually.

Another important thing about an object's id is that its id is its web address. For example, if our example Folder has a URL of /myFolder then, the URL of the index_html Document inside it would be /myFolder/index_html. In fact all Zope URLs? consist of concatenations of Zope object ids.

An object's title is an optional string property which is used to give it more description than is possible with an id.

There are a few more interesting things about our poll creation form. If you look closely you will notice that the name of the answers textarea is choices:lines this is a specially encoded name which tells the Zope ORB to convert the contents of to a list of strings rather than one large string with line breaks. To find out more about form data marshalling and coercion, see the Zope ORB documentation.

Finally we note that the action of the poll creation form is manage_addPoll. This is the name of the poll installation method which we will describe next.

Product installation method

The job of the installation method is to create an instance of the Product and install it in the parent Folder. As we mentioned before, this method will be installed in the Folder class by Zope.

Here's how we define the product installation method for our poll:

        def addPoll(self,id,title,question,answers,REQUEST=None):
            """Create a poll and install it in its parent Folder.

            The argument 'self' will be bound to the parent Folder.
            """
            poll=Poll(id, title, question, answers)
            self._setObject(id, poll)
            if REQUEST is not None:
                return self.manage_main(self,REQUEST)

This method creates a poll and then installs it with the Folder's _setObject method. The reason it tests for the REQUEST is allow the method to forgo returning a management screen if it is not called from the web.

Next we will see how we need to specially indicate the object creation form and the object installation method in our Product's package so that they are recognized by Zope.

Making creation methods available to Zope

Now that we have a creation form and an installation method defined in Poll.py it's time to add them to __init__.py so that Zope can easily find them when it inspects our package.

You must define these methods in a special methods dictionary in your Product's __init__.py file. Here's how we do this for our poll Product:

        methods={
            'manage_addPollForm': Poll.addPollForm,
            'manage_addPoll': Poll.addPoll,
        }

So what we've done is let Zope know about two special methods of our Poll module, addPollForm which is the creation form, and addPoll which is the installation method. By listing them in the methods dictionary, we are telling Zope to install them as methods of normal Zope Folders at startup time. These methods will be by bound to Folders with the names we give them in the methods dictionary. So, for example, Folder.manage_addPollForm is Poll.addPollForm. You may wonder why we rename these methods. By convention creation methods (and other management related methods) begin with the prefix manage_.

By now we have done much of the work of making our poll class available to Zope as a Product.

Are you still with us?

Right now the process of turning your simple publishable object into a full fledged Zope Product is probably starting to seem a bit tedious. It's true, it is a bit tedious. Perhaps in the future there will be a stream lined process.

Take heart. You're most of the way there. Most of the rest of the work is fairly straightforward. Soon you'll bask in the glory of seeing your Product appear the the Zope add list.

Get another snack and when you're ready let's continue Productifying our poll.

Product editing

Next on our list of features to add to our poll is the ability to edit its attributes through the web. This process closely mirrors the process of creating and installing the poll. It will be accomplished with two methods: an editing form which displays the current information and a method which processes the form input and changes the poll object. These two components will closely mirror the poll adding and installation methods, with the exception that they will be methods of the Poll class itself, rather than the Zope Folder class.

Product editing form

Our poll editing form is quite similar to the poll creation form. In fact, it's easiest just to make a copy of the adding form and go through and make small changes to create the editing form. Here's what it looks like:

        <html>
          <head><title>Edit Poll</title></head>
          <body bgcolor="#FFFFFF">

          <!--#var manage_tabs-->

            <h2>Edit Poll</h2>

            <form action="manage_edit" method="POST">
              <table cellspacing="2">

            <tr>
              <th align="LEFT" valign="TOP"><em>Title</em></th>
              <td align="LEFT" valign="TOP">
                    <input type="TEXT" name="title" size="50"
                    value="<!--#var title-->">
                  </td>
            </tr>

            <tr>
              <th align="LEFT" valign="TOP">Poll Question</th>
              <td align="LEFT" valign="TOP">
                    <input type="TEXT" name="question" size="50"
                    value="<!--#var question-->">
                  </td>
            </tr>

            <tr>
              <th align="LEFT" valign="TOP">Poll Answers<br>(one per
              line)</th>
              <td align="LEFT" valign="TOP">
                    <textarea name="choices:lines">
                    <!--#in answers-->
                    <!--#var sequence-item fmt="html-quote"-->
                    <!--#/in-->
                    </textarea>
                  </td>
            </tr>

            <tr>
              <td></td>
              <td><br><input type="SUBMIT" value="Edit"></td>
            </tr>

            </table>
            </form>
          </body>
        </html>

There are a couple things to notice here. For one we've included a manage_tabs variable at the top to create the characteristic Zope tabbed management interface. We've also changed the form's action to point to manage_edit which is the name of editing method that will we discuss next. Also we do not allow editing the the poll's id. This should be handled by the standard Zope rename facility. Finally, we've included value attributes for the form inputs to supply the current values. In the case of the answers textarea, we've dumped all the answers into the textarea with an in loop.

Product editing method

To allow editing of the poll's attributes, we need to have a method to accept the editing form's input. By convention such a method is called manage_edit though you need not always name your editing method this. In fact complex products will have a number of editing methods with different names.

The editing method isn't very complex. It simply updates the poll's attributes and returns a confirmation message to the user indicating that their update was successful. it is a good idea to provide the user with a confirmation message when they perform a management task, unless the results of their action is readily visible in some other way.

Here's what our poll's editing method looks like:

        def manage_edit(self,title,question,choices,REQUEST=None):
            "edit the poll's characteristics"
            self.title=title
            self.question=question
            self.choices=[]
            for choice in choices:
                if choice:
                    self.choices.append(choice)
            if len(self.choices) < 2:
                raise ValueError, "You must supply at least two valid
                choices."
            if REQUEST is not None:
                return MessageDialog(
                    title='Edited',
                    message='<strong>%s</strong> has been edited.' %
                    self.id,
                    action ='./manage_main',
                    )       

Since this is a published method it needs to have a doc string. The method replaces the title and question attributes. For the choices it makes sure to only add non blank choices to the poll.

The method returns a confirmation message using the MessageDialog facility, which is a standard Zope document for notifying users. The title and message of the MessageDialog inform the user of what has happened. The action of the message dialog is the URL that the user will be taken to when they click OK after reading the confirmation. Since manage_main is the default management screen for Zope Products we have chosen that as the URL to display following a confirmation.

The method tests to see if the REQUEST exists before sending a confirmation message since, this will indicate whether the method has been called through the web or not. In cases where the method is called in DTML or External Methods, etcetera, it is not necessary to return an HTML confirmation.

OK, so now our poll is looking pretty complete. Congratulations! At this point there is very little logic that we need to add to it. The majority of what's left to do are niggling Zope details which we need to nail down.

Defining Zope permissions

Permissions are at the heart of Zope's access control system. All Products need to define their use in terms of permissions. Permissions are what allows users to define who gets to access objects through the web and through DTML. Permissions describe activities, and they are bound to roles through the management interface. Roles in turn are bound to users.

A permission consists of a number of methods along with default roles settings. To define permissions the Product class should have an __ac_permissions__ class attribute which is a tuple of tuples. The individual permissions are defined by a tuple of three items: the permission name, a tuple of methods bound to the permission, and an optional tuple of roles.

Here's how we initially define permissions for our poll Product:

        __ac_permissions__=(
            ('View management screens',('manage_tabs','manage_main')),
            ('Change permissions',('manage_access',)),
            ('Change Polls',('manage_edit',),('Manager',)),
            ('Vote',('vote',),('Anonymous','Manager')),
            ('View',('','total_votes','votes_for')),
        )

You should not assign the same method to more than one permission. Also you should probably only count on Manager and Anonymous roles being present when you set your default roles. Finally you should try to define as much of your Product's permissions with the same names as those of existing Products. This way your Product can profitably inherit permission settings. Categorizing methods into permissions is a tricky business.

In our case we have chosen to use three existing permissions: View management screens, View and Change permissions. We have added only two new permissions, Vote and Change Polls.

In order for your Product's permissions to work well with acquisition, you should export your Product's unique permissions to the Folder class, so that they can be set on Folders and acquired easily. To do this you need to define a second set of __ac_permissions__ in your Product's __init__.py file. So in our Poll package's __init__.py file we define the following:

        __ac_permissions__=(
            ('Add Polls',('manage_addPollForm','manage_addPoll')),
            ('Change Polls', ()),
            ('Vote', ()),
        )

As the example shows, you needn't fully define permission that are already defined in your Product's class. In other words, since Change Polls is already defined in our poll class, we only need to add its name here.

Also, you may wish to added permissions in the __init__.py file to cover the creation methods that you define there. We have done this by defining Add Polls here. Why didn't we define Add Polls in our Poll class? Because the Add Polls permission doesn't actually cover methods of the Poll class, it covers methods of the Folder class. Remember that Zope installs the product creation methods in its Folder class.

Whew, we've got permissions mostly nailed. Now on the the next detail.

Defining management tabs

All Products are manageable through the web. This is the Zope way. The standard convention for Product management is to provide a series of tabbed management screens. These tabs are visible on the top of the right frame in the Zope management interface. As we have seen you create these tabs with the manage_tabs method in the DTML of the management screens. You can inherit the manage_tabs method, but you need to define the tabs in order for the method to do its work.

To provide tabs for your Product you need to define them with the manage_options class attribute. Here are the tabs that we decide to define for our poll Product:

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

This tuple of dictionaries defines a couple management tabs. The label item defines the tab's name. The action item defines the method that the tab calls. Note that a blank method is the same as calling index_html. You can optionally provide a target item to indicate the frame target.

Defining management tabs is not always straightforward. In general you should group like management functions together onto one management screen. You can also choose to include more standard management tabs than we have chosen to include like Undo and Find. You're best bet is to study how existing Products work and try to steal good ideas from them.

If you're paying very close attention you will notice that Zope automatically adds a Help tab for you.

Specifying Product meta-type

An object's meta-type is a string which describes what sort of Product it is. A meta-type is just a name; it has nothing to do with Python meta-classes. All Products need to have different meta-types. The meta-type of Folder objects is Folder, the meta-type of Documents is Document. You get the idea. A meta-type is the name of a Zope Product as displayed in the management interface, and it is needed to create a Zope Product.

To define a meta-type for your object you need to include a meta_types tuple in your Product's __init__.py file. Here's how we will do this for our poll Product:

        meta_types=(
            {'name': 'Poll',
            'action': 'manage_addPollForm'
            },
        )

So basically what we have is a tuple of addable objects with their names and the object creation form method name in dictionaries. Note that your Product can define more than one meta-type, if it defines more than one type of addable object.

You will also need to define you Product's meta-type in its class as a class attribute. For example this is how we indicate our poll's meta-type:

        class Poll:
            ...
            meta_type='Poll'
            ...

The Product's meta-type appears in the management interface in the list of objects which can be added. The meta-type is also used in a number of Zope methods (such as objectValues) when querying an object about its sub-objects.

OK, that was pretty easy. Only a few more details and we're done.

Product icons

Products are identified in the management interface by their icons. Icons are 16 pixel by 16 pixel GIFs? with transparent backgrounds. By convention Folderish products have icons that looks somewhat like a folder.

Product icons are defined in the Product's __init__.py file with the misc_ mapping.

Zope defines a special top-level object, misc_, which has a sub-object for each installed Product. Each Product sub-object in misc_ has sub-objects for each of the keys defined in the misc_ dictionary in the Product's __init__.py. This provides a mechanism for creating Product-dependent, instance-independent objects that are web accessible. By convention, misc_ only holds Product icons.

Here's our poll's misc_ definition:

        misc_={
            'poll': ImageFile('poll.gif',globals()),
        }

It only defines one thing--the poll's icon. For your Product's icons you should use 'ImageFile?'s.

Objects defined in a Product's misc_ mapping are made available through the web at '/misc_//'.

To allow Zope to find this icon you need to specify its URL in your Product's class. So here's how we indicate our poll's icon:

        class Poll:
            ...
            icon='misc_/Poll/poll'
            ...

We're done for now

Hooray! We've now wrapped up our poll Product. It was a fair amount of work, but hopefully it helped you learn a little more about how Zope functions under the hood.

Now that we have our Product fleshed out let's install it and test it out. You should not break for a snack now, the anticipation is too great at this point.

Installing the poll

To install you Product simply create a directory inside lib/python/Products and place your files in it. So for our poll we will create a directory lib/python/Products/Poll and inside we will put these files:

'__init__.py'--misc poll product data.

'Poll.py'--the poll class definition.

'poll.dtml'--the display method.

'pollAdd.dtml'--the creation form.

'pollEdit.dtml'--the editing form.

'poll.gif'--the icon file.

Later we may want to add additional files to the distribution like a README file and a version file.

To make the Product accessible to Zope you now need to shutdown and restart Zope. The best way to do this is via the Zope Control Panel.

Testing the poll

Now let's see if our Product works. If the Product took, you should see Poll listed in the pop-up menu of items to add on the management screen. You can also check things out under the Zope Control Panel. In the Control Panel there is a sub-object called Products. If your Product failed to load you should be able to see a traceback there which will give you information about what went wrong loading your Product.

If a Product fails to load, existing instances of the Product will be unusable and will appear broken.

Edit the Product. Shutdown and restart Zope. Repeat as necessary until the Product loads.

If you really mess things up you can cause Zope to fail to start. In these cases you'll need to read the traceback and figure out what you did wrong.

Once the poll actually loads correctly into Zope we can test it further by creating a poll object, editing it and using it.

Refining the poll

As soon as you have a semi-functional Product it's time to start refining. In the case of our Poll product the main things we need to work on are user interface issues. We have a simple method for displaying a Poll's form, but no feedback mechanism. Another thing we would like to be able to do is to embed a poll in another document. Here's a plan to deal with these issues.

  • Factor the user interface into a couple different components
  • Provide a default view and operation for the Poll

By factoring the UI, we allow access to different poll component to be embedded in Documents. By providing a default view and operation we allow simple use of a poll without any other Documents.

Let's factor the voting form and results display into two class attributes, form and results. Next we provide a default poll view, index_html which calls these components. We also need to make sure that we define the appropriate permissions for these different user interface components.

Here's the poll form:

        <p>&dtml-question;</p>
        <form action="&dtml.url_quote-id;/vote" method="post">
        <dtml-in choices>
        <input type="radio" name="choice:int"
        value="&dtml-sequence-index;"> &dtml-sequence-item;<br>
        </dtml-in>
        <dtml-if action>
        <input type="hidden" name="action" value="&dtml-action;">
        </dtml-if>
        <input type="submit" value="Vote">
        </form>

It's basically our original poll user interface. It is not a complete HTML document.

Here's the poll results template:

        <dtml-if total_votes>
        <p>
        <dtml-in choices>
        &dtml-sequence-item;: <dtml-var
        expr="percent_for(_['sequence-index'])" fmt="%2d"> %<br>
        </dtml-in>
        </p>
        <p>Total number of votes cast: &dtml-total_votes;</p>
        <dtml-else>
        <em>No votes have been cast yet.</em>
        </dtml-if>

Again this is an HTML fragment. It uses a new method we've added called percent_for to display the percentage of votes for each choice.

Finally we can redo the main poll interface template now:

        <html>
        <head><title>Poll: &dtml-title_or_id;</title></head>
        <body>
        <h1>Poll: &dtml-title_or_id;</h1>
        <h2>Vote</h2>
        <dtml-var form>
        <h2>Results</h2>
        <dtml-var results>
        </body>
        </html> 

This template presents a complete way to use the poll. It allows you to vote, and it displays the results.

To finish up the user interface changes we need to modify the vote method a little:

        def vote(self,choice,action=None,REQUEST=None):
            """Vote for a choice

            'choice' is an integer which represents a choice.
            'action' is an optional URL which the user will
            be taken to after they vote."""

            # this is done like this in order to allow Zope's
            # persistence system notice that we're updating 
            # a mutable attribute.
            votes=self.votes
            votes[choice]=votes[choice]+1
            self.votes=votes
            if action is None:
                action=''
            if REQUEST is not None:
                return MessageDialog(
                    title='Vote Accepted',
                    message='Your vote has been accepted.',
                    action =action,
                    )

What we have here is a moderately flexible way of responding to a user when they vote. We display a standard Zope message indicating that their vote was accepted, and then we send them to a URL. If you call the vote method with an action URL you can set the return page, if not, you are sent to the poll's default view after you vote.

We can always do better

Well, this is a reasonable solution, but we could do much better. For example, it would be nice to make the Poll Folderish, and provide form, results, and index_html not as fixed class attributes, but as default Documents inside the Poll. Then users could modify the look and feel of a Poll and even add more methods.

There are many other things that might be nice to do as well. Like we might want to provide more hooks for different voting controls. For example, we could keep track of users and make sure they only vote once. We could limit anonymous voters by IP address.

We could also expand our concept of poll to include more than just multiple choice polls. For example, we could allow write in votes, voting for multiple choices at once, and weighted votes--i.e. allowing you to vote for or against choices.

We might want to have some concept of poll closure. Managers should be able to close a poll to further voting. Alos, polls could have some time limit, so they close automatically after a week for example.

We could add graphs showing voting trends and results. In fact we could make simple ones by stretching GIFs? in proportion to the numbers of votes.

OK, we're not going to do these things right now, but you might want to try some of these ideas on your in order to learn more.

Since we chose to create a Zope Product, all these goals are easily within our reach, and if we plan it right, we should be able to roll out our poll Product now, and add more features to it later.

Congratulations

If you made it this far, congratulations! You've slogged through a long discussion. At this point you might want to re-read the example poll code and maybe the final poll Product code, now that you understand some of the ideas behind it better.

The rest of our discussion will cover issues that arise as you get more comfortable with writing Zope products and prepare to share them with other people.

Now might be a good time for another snack, or maybe a actual meal--you've earned it.

Control Panel Product features

There are a couple interesting tricks you can do with the Zope Control Panel. Besides showing your Product's traceback if it's broken, the Control Panel will show your Product's version information and a README file.

To include version information with your Product create a version.txt file at the top level of your Product's package directory. Include one line of text in the file to name your Product's version. For example we might want to include something like this:

        Poll 0.1.0

To indicate this is an early version of our poll Product.

Additionally if you include a README file with your Product, it will be made available through the Control Panel. You need to make sure to name your README file README.txt and to include it at the top level of your Product's package directory. Zope will interpret your README file as structured text. To find out more about structured text visit the Zope site's documentation area. Suffice it to say that your README file should look OK if you use normal text and include blank lines in between paragraphs.

Distributing Products

The standard way to distribute a Zope product is in a gzipped tar archive. You should arrange you tarball so that it can be untarred from within the Zope directory. For example assuming we have developed our poll Product in Zope and that it is currently in lib/python/Products/Poll, we can create an archive for our poll product by first cding to the Zope directory, and then issuing this command:

        tar cvfz Poll.tar.gz lib/python/Products/Poll

If your Product relies on resources in the Shared package you will also need to include those packages in your tarball.

If you reasonably document your Product with doc strings, Zope gives you reasonable online help via the help tab. It's really worth it to make the effort to document your Product.

In order for your Product's methods to show up in the online help, you need to have both a doc string for the method and assign it to a permission.

Also don't forget the version.txt and the README.txt files that we mentioned earlier. Providing these files helps others who want to use your Product quite a bit.

Upgrading and evolving Products

Issues can occur when you change your Product class and then reload objects that were created with an earlier version of the class. The simplest way to handle these sorts of situations is to provide class attributes as defaults for added attributes. For example if the latest version of your Product expects a improved_spam attribute while earlier versions only sported spam attributes, you may wish to define improved_spam class attribute in your new class so your old object won't break when they run under your new class. Another solution is to use the standard Python pickling hook __setstate__, however, this is in general more error prone and complex.

While you are developing a Product you won't have to worry too much about these details, since you can always delete old instances that break with new class definitions. However, once you release your Product and other people start using it, then you need to start planning for the eventuality of upgrading.

Another nasty problem that can occur when unpickling objects is what to do when you change the name of your Product's class. This should be avoided by not renaming your Products class, unless you wish to lose all stored instances.

We're done

OK, that's it for now. Now you should be ready to build your own Products and distribute them.

Go for it!