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.
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.
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.
OK, that's it for now. Now you should be ready to build your own Products and distribute them.
Go for it!