Chapter 13
Writing a new Content Object

Difficulty

Newcomer

Skills

Problem/Task

Of course it is essential for any serious Zope 3 developer to know how to implement new content objects. Using the example of a message board, this chapter will outline the main steps required to implement and register a new content component in Zope 3.

Solution

This chapter is the beginning of the development of MessageBoard content type and everything that is to it. It serves very well as the first hands-on task, since it will not assume anything other than that you have Zope 3 installed, know some Python and are willing to invest some time in learning the framework.

13.1 Step I: Preparation

Before you start, you should have installed Zope 3, created a principal.zcml file and have successfully started Zope. You have already done that? Okay, then let’s start.

Other than in Zope 2, Zope 3 does not require you to place add-on packages in a special directory and you are free to choose where to put it. The most convenient place is inside ZOPE3/src ( ZOPE3 is your Zope 3 directory root), since it does not require us to mess with the PYTHONPATH. To clearly signalize that this application is demo from the book, we place all of the code in a package called book. To create that package, add a directory using


1  mkdir ZOPE3/src/book

on Unix.

To make this directory a package, place an empty __init__.py file in the new directory. In Unix you can do something like


1  echo "# Make it a Python package" >> ZOPE3/src/book/__init__.py

but you can of course also just use a text editor and save a file of this name. Just make sure that there is valid Python code in the file. The file should at least contain some whitespace, since empty files confuse some archive programs.

Now we create another package inside book called messageboard, in a similar manner (do not forget to create the __init__.py file). From now on we are only going to work inside this messageboard package, which should be located at ZOPE3/src/book/messageboard.

Note: While the source code, that you can download for every step at http://svn.zope.org/book/trunk/messageboard, contains a license header, we omit these throughout the book to save typing and space. However, the copyright as stated in the source files still applies.

13.2 Step II: The Initial Design

As stated above, our goal is to develop a fully functional, though not great-looking, Web-based message board application. The root object will be the MessageBoard, which can contain postings or Message objects from various users. Since we want to allow people to respond to various messages, we need to allow messages to contain replies, which are in turn just other Message objects.

That means we have two container-based components: The MessageBoard contains only messages and can be added to any Folder or container that wishes to be able to contain it. To make the message board more interesting, it also has a description, which briefly introduces the subject/theme of the discussions hosted. Messages, on the other hand should be only contained by message boards and other messages. They will each have a title and a body.

This setup should contain all the essential things that we need to make the object usable. Later on we will associate a lot of other meta-data with these components to integrate them even better into Zope 3 and add additional functionality.

13.3 Step III: Writing the interfaces

The very first step of the coding process is always to define your interfaces, which represent your external API. You should be aware that software that is built on top of your packages expect the interfaces to behave exactly the way you specify them. This is often less of an issue for attributes and arguments of a method, but often enough developers forget to specify what the expected return value of a method or function is or which exceptions it can raise or catch.

Interfaces are commonly stored in an interfaces module or package. Since our package is not that big, we are going to use a file-based module; therefore start editing a file called interfaces.py in your favorite editor.

In this initial step of our application, we are only interested in defining one interface for the message board itself and one for a single message, which are listed below (add these to the file interfaces.py):


1  from zope.interface import Interface
2  from zope.schema import Text, TextLine, Field
3  
4  from zope.app.container.constraints import ContainerTypesConstraint
5  from zope.app.container.constraints import ItemTypePrecondition
6  from zope.app.container.interfaces import IContained, IContainer
7  from zope.app.file.interfaces import IFile
8  
9  
10  class IMessage(Interface):
11      """A message object. It can contain its own responses."""
12  
13      def __setitem__(name, object):
14          """Add a IMessage object."""
15  
16      title = TextLine(
17          title=u"Title/Subject",
18          description=u"Title and/or subject of the message.",
19          default=u"",
20          required=True)
21  
22      body = Text(
23          title=u"Message Body",
24          description=u"This is the actual message. Type whatever you wish.",
25          default=u"",
26          required=False)
27  
28  
29  class IMessageBoard(IContainer):
30      """The message board is the base object for our package. It can only
31      contain IMessage objects."""
32  
33      def __setitem__(name, object):
34          """Add a IMessage object."""
35  
36      __setitem__.precondition = ItemTypePrecondition(IMessage)
37  
38      description = Text(
39          title=u"Description",
40          description=u"A detailed description of the content of the board.",
41          default=u"",
42          required=False)
43  
44  
45  class IMessageContained(IContained):
46      """Interface that specifies the type of objects that can contain
47      messages."""
48      __parent__ = Field(
49          constraint = ContainerTypesConstraint(IMessageBoard, IMessage))
50  
51  
52  class IMessageContainer(IContainer):
53      """We also want to make the message object a container that can contain
54      responses (other messages) and attachments (files and images)."""
55  
56      def __setitem__(name, object):
57          """Add a IMessage object."""
58  
59      __setitem__.precondition = ItemTypePrecondition(IMessage, IFile)

13.4 Step IV: Writing Unit tests

There are two ways unit tests can be written in Zope 3. The first one is through special TestCase classes using the unittest package, which was modeled after JUnit. The second technique is writing tests inside doc strings, which are commonly known as doc tests.

Late in the development of Zope 3 doc tests became the standard way of writing tests. For philosophical and technical differences between the two approaches, see the section on “Writing Tests”, especially the “Writing Basic Unit Tests” and “Doctests: Example-driven Unit Tests” chapters.

Common unit tests, however, are of advantage when it is desirable to reuse abstract tests, as it is the case for various container tests. Therefore, we will use unit tests for the container tests and doc tests for everything else.

First, create a package called tests inside the messageboard package. Note that calling the test module tests (file-based test modules would be called tests.py) is a convention throughout Zope 3 and will allow the automated test runner to pick up the tests.

Next, start to edit a file called test_messageboard.py and insert:


1  import unittest
2  from zope.testing.doctestunit import DocTestSuite
3  
4  from zope.app.container.tests.test_icontainer import TestSampleContainer
5  
6  from book.messageboard.messageboard import MessageBoard
7  
8  
9  class Test(TestSampleContainer):
10  
11      def makeTestObject(self):
12          return MessageBoard()
13  
14  def test_suite():
15      return unittest.TestSuite((
16          DocTestSuite('book.messageboard.messageboard'),
17          unittest.makeSuite(Test),
18          ))
19  
20  if __name__ == '__main__':
21      unittest.main(defaultTest='test_suite')

A lot of cool stuff just happened here. You just got your first 12 unit tests. Let’s have a closer look:

Now it is time to do the second test module for the IMessage component. To start, we simply copied the test_messageboard.py to test_message.py and modified the new file to become:


1  import unittest
2  from zope.testing.doctestunit import DocTestSuite
3  
4  from zope.app.container.tests.test_icontainer import TestSampleContainer
5  
6  from book.messageboard.message import Message
7  
8  
9  class Test(TestSampleContainer):
10  
11      def makeTestObject(self):
12          return Message()
13  
14  def test_suite():
15      return unittest.TestSuite((
16          DocTestSuite('book.messageboard.message'),
17          unittest.makeSuite(Test),
18          ))
19  
20  if __name__ == '__main__':
21      unittest.main(defaultTest='test_suite')

There is not really any difference between the two testing modules, so that I am not going to point out the same facts again.

Note that none of the tests deal with implementation details yet, simply because we do not know what the implementation details will be. These test could be used by other packages, just as we used the SampleContainer base tests, since these tests only depend on the API. In general, however, tests should cover implementation-specific behavior.

13.5 Step V: Implementing Content Components

Now we are finally ready to implement the content components of the package. This is the heart of this chapter. But how do we know which methods and properties we have to implement? There is a neat tool called pyskel.py in ZOPE3/utiltities that generates a skeleton. Go to ZOPE3/src and type:


1  python2.3 ../utilities/pyskel.py \
2            book.messageboard.interfaces.IMessageBoard

The expected result is shown below. The tool inspects the given interface and creates the skeleton of an implementing class. It also recurses into all base interfaces to get their methods. Here the generated code:


1  from zope.interface import implements
2  from book.messageboard.interfaces import IMessageBoard
3  
4  class MessageBoard:
5      __doc__ = IMessageBoard.__doc__
6  
7      implements(IMessageBoard)
8  
9  
10      def __setitem__(self, name, object):
11          "See book.messageboard.interfaces.IMessageBoard"
12  
13      # See book.messageboard.interfaces.IMessageBoard
14      description = None
15  
16      def __getitem__(self, key):
17          "See zope.interface.common.mapping.IItemMapping"
18  
19      def get(self, key, default=None):
20          "See zope.interface.common.mapping.IReadMapping"
21  
22      def __contains__(self, key):
23          "See zope.interface.common.mapping.IReadMapping"
24  
25      def __getitem__(self, key):
26          "See zope.interface.common.mapping.IItemMapping"
27  
28      def keys(self):
29          "See zope.interface.common.mapping.IEnumerableMapping"
30  
31      def __iter__(self):
32          "See zope.interface.common.mapping.IEnumerableMapping"
33  
34      def values(self):
35          "See zope.interface.common.mapping.IEnumerableMapping"
36  
37      def items(self):
38          "See zope.interface.common.mapping.IEnumerableMapping"
39  
40      def __len__(self):
41          "See zope.interface.common.mapping.IEnumerableMapping"
42  
43      def get(self, key, default=None):
44          "See zope.interface.common.mapping.IReadMapping"
45  
46      def __contains__(self, key):
47          "See zope.interface.common.mapping.IReadMapping"
48  
49      def __getitem__(self, key):
50          "See zope.interface.common.mapping.IItemMapping"
51  
52      def __setitem__(self, name, object):
53          "See zope.app.container.interfaces.IWriteContainer"
54  
55      def __delitem__(self, name):
56          "See zope.app.container.interfaces.IWriteContainer"

This result is good but some parts are unnecessary; we will for example simply inherit the BTreeContainer base component, so that we do not have to implement the methods from the IReadMapping, IEnumerableMapping, IReadMapping, IItemMapping and IWriteContainer interfaces.

Open a new file called messageboard.py for editing. The implementation of the message board including doc tests looks like this:


1  from zope.interface import implements
2  from zope.app.container.btree import BTreeContainer
3  
4  from book.messageboard.interfaces import IMessageBoard
5  
6  class MessageBoard(BTreeContainer):
7      """A very simple implementation of a message board using B-Tree Containers
8  
9      Make sure that the ``MessageBoard`` implements the ``IMessageBoard``
10      interface:
11  
12      >>> from zope.interface.verify import verifyClass
13      >>> verifyClass(IMessageBoard, MessageBoard)
14      True
15  
16      Here is an example of changing the description of the board:
17  
18      >>> board = MessageBoard()
19      >>> board.description
20      u''
21      >>> board.description = u'Message Board Description'
22      >>> board.description
23      u'Message Board Description'
24      """
25      implements(IMessageBoard)
26  
27      # See book.messageboard.interfaces.IMessageBoard
28      description = u''

The next task is to write the Message object, which is pretty much the same code. Therefore we will not list it here and refer you to the code at http://svn.zope.org/book/trunk/messageboard/step01/message.py. The only difference is that in this case the Message component must implement IMessage, IMessageContained, and IMessageContainer.

13.6 Step VI: Running Unit Tests against Implementation

After having finished the implementation, we need to make sure that all the tests pass. There is a script called test.py that will run all or only specified tests for you. To run the test against your implementation, execute the following line from the Zope 3 root directory:


1  python2.3 test.py -vpu --dir src/book/messageboard

The -v option cases the currently running test to be displayed, the -p allows us to see the progress of the tests being run and -u tells the test runner to just run the unit tests. For a list of all available options run the script with the -h (help) option.

You should see 26 tests pass. The output at the of the test run should look like this:


1  Configuration file found.
2  Running UNIT tests at level 1
3  Running UNIT tests from /opt/zope/Zope3/Zope3-Cookbook
4    26/26 (100.0%): test_values (....messageboard.tests.test_messageboard.Test)
5  ----------------------------------------------------------------------
6  Ran 26 tests in 0.346s
7  
8  OK

It is very likely that some tests are failing or the test suite does not even run due to syntax errors. This is totally normal and exactly the reason we write tests in the first place. In these cases keep fixing the problems until all tests are passing.

13.7 Step VII: Registering the Content Components

Now that we have developed our components, it is necessary to tell Zope 3 how to interact with them. This is commonly done using Zope’s own configuration language called ZCML. The configuration is stored in a file called configure.zcml by convention. Start to edit this file and add the following ZCML code:


1  <configure
2      xmlns="http://namespaces.zope.org/zope">
3  
4    <interface
5        interface=".interfaces.IMessageBoard"
6        type="zope.app.content.interfaces.IContentType"
7        />
8  
9    <content class=".messageboard.MessageBoard">
10      <implements
11          interface="zope.app.annotation.interfaces.IAttributeAnnotatable"
12          />
13      <implements
14          interface="zope.app.container.interfaces.IContentContainer"
15          />
16      <factory
17          id="book.messageboard.MessageBoard"
18          description="Message Board"
19          />
20      <require
21          permission="zope.ManageContent"
22          interface=".interfaces.IMessageBoard"
23          />
24      <require
25          permission="zope.ManageContent"
26          set_schema=".interfaces.IMessageBoard"
27          />
28    </content>
29  
30    <interface
31        interface=".interfaces.IMessage"
32        type="zope.app.content.interfaces.IContentType"
33        />
34  
35    <content class=".message.Message">
36      <implements
37          interface="zope.app.annotation.interfaces.IAttributeAnnotatable"
38          />
39      <implements
40          interface="zope.app.container.interfaces.IContentContainer"
41          />
42      <require
43          permission="zope.ManageContent"
44          interface=".interfaces.IMessage"
45          />
46      <require
47          permission="zope.ManageContent"
48          interface=".interfaces.IMessageContainer"
49          />
50      <require
51          permission="zope.ManageContent"
52          set_schema=".interfaces.IMessage"
53          />
54    </content>
55  
56  </configure>

13.8 Step VIII: Configure some Basic Views

Even though the content components are registered now, nothing interesting will happen, because there exists only a programmatic way of adding and editing the new components. Thus we are going to define some very basic browser views to make the content components accessible via the browser-based user interface.

First create a package called browser (do not forget the __init__.py file) inside the messageboard package. Add a new configuration file, configure.zcml, inside browser and insert the following content:


1  <configure
2      xmlns="http://namespaces.zope.org/browser">
3  
4    <addform
5        label="Add Message Board"
6        name="AddMessageBoard.html"
7        schema="book.messageboard.interfaces.IMessageBoard"
8        content_factory="book.messageboard.messageboard.MessageBoard"
9        fields="description"
10        permission="zope.ManageContent"
11        />
12  
13    <addMenuItem
14        class="book.messageboard.messageboard.MessageBoard"
15        title="Message Board"
16        description="A Message Board"
17        permission="zope.ManageContent"
18        view="AddMessageBoard.html"
19        />
20  
21    <editform
22        schema="book.messageboard.interfaces.IMessageBoard"
23        for="book.messageboard.interfaces.IMessageBoard"
24        label="Change Message Board"
25        name="edit.html"
26        permission="zope.ManageContent"
27        menu="zmi_views" title="Edit"
28        />
29  
30    <containerViews
31        for="book.messageboard.interfaces.IMessageBoard"
32        index="zope.View"
33        contents="zope.View"
34        add="zope.ManageContent"
35        />
36  
37    <addform
38        label="Add Message"
39        name="AddMessage.html"
40        schema="book.messageboard.interfaces.IMessage"
41        content_factory="book.messageboard.message.Message"
42        fields="title body"
43        permission="zope.ManageContent"
44        />
45  
46    <addMenuItem
47        class="book.messageboard.message.Message"
48        title="Message"
49        description="A Message"
50        permission="zope.ManageContent"
51        view="AddMessage.html"
52        />
53  
54    <editform
55        schema="book.messageboard.interfaces.IMessage"
56        for="book.messageboard.interfaces.IMessage"
57        label="Change Message"
58        fields="title body"
59        name="edit.html"
60        permission="zope.ManageContent"
61        menu="zmi_views" title="Edit"
62        />
63  
64    <containerViews
65        for="book.messageboard.interfaces.IMessage"
66        index="zope.View"
67        contents="zope.View"
68        add="zope.ManageContent"
69        />
70  
71  </configure>

In order for the system to know about the view configuration, we need to reference the configuration file in messageboard/configure.zcml. To include the view configuration, add the following line:


1  <include package=".browser" />

13.9 Step IX: Registering the Message Board with Zope

At this stage we have a complete package. However, other than in Zope 2, you have to register a new package explicitly. That means you have to hook up the components to Zope 3. This is done by creating a new file in ZOPE3/package-includes called messageboard-configure.zcml. The name of the file is not arbitrary and must be of the form *-configure.zcml. The file should just contain one directive:


1  <include package="book.messageboard" />

When Zope 3 boots, it will walk through each file of this directory and execute the ZCML directives inside each file. Usually the files just point to the configuration of a package.

13.10 Step X: Testing the Content Component

Congratulations! You have just finished your first little Zope 3 application, which is quiet a bit more than what would minimally be required as you will see in a moment. It is time now to harvest the fruits of your hard work. Start your Zope 3 server now, best using makerun from the Zope 3 root. If you get complains about the Python version being used, edit the Makefile and enter the correct path to Python’s executable. Other errors that might occur are due to typos or mis-configurations. The ZCML interpreter will give you the line and column number of the failing directive in Emacs-friendly format. Try to start Zope 3 again and again until you have fixed all the errors and Zope 3 starts up ending with this output:


1  ------
2  2003-12-12T23:14:58 INFO PublisherHTTPServer zope.server.http (HTTP) started.
3          Hostname: localhost
4          Port: 8080
5  ------
6  2003-12-12T23:14:58 INFO PublisherFTPServer zope.server.ftp started.
7          Hostname: localhost
8          Port: 8021
9  ------
10  2003-12-12T23:14:58 INFO root Startup time: 11.259 sec real, 5.150 sec CPU

Note that you also get some internationalization warnings, which you can safely ignore for now.

Once the server is up and running, go to your favorite browser and display the following URL:
http://localhost:8080/@@contents.html

At this point an authentication box should pop up and ask you for your username and password - users are listed in the principals.zcml. If you have not added any special user, use “gandalf” as the login name and “123” as password. After the authentication is complete you should be taken to the Zope 3 Web user interface. Under Add: you can now see a new entry “Message Board”.

Feel free to add and edit a message board object.

Once you created a message board, you can click on it and enter it. You will now notice that you are only allowed to add “Message” objects here. The choice is limited due to the conditions we specified in the interfaces. The default view will be the “Edit” form that allows you to change the description of the board. The second view is the “Contents” with which you can manage the messages of the message board.

Add a “Message” now. Once you added a message, it will appear in the “Contents” view. You can now click on the message. This will allow you to modify the data about the message and add new messages (replies) to it. With the code we wrote so far, you are now able to create a complete message board tree and access it via the Web UI.

Note that you still might get errors, in which case you need to fix them. Most often you have security problems, which narrows the range of possible issues tremendously. Unfortunately, NotFoundError is usually converted to ForbiddenAttributeError, so be careful, if you see this problem.

Another common trap is that standard error screens do not show the traceback. However, for these situations the Debug skin comes in handy - instead of http://localhost:8080/@@contents.html use http://localhost:8080/++skin++Debug/@@contents.html and the traceback will be shown.

Note: If you make data-structural changes in your package, it might become necessary to delete old instances of the objects/components. Sometimes even this is not enough, so that you have to either delete the parent Folder or best delete the Data.fs (ZODB) file. There are ways to upgrade gracefully to new versions of objects, but during development the listed methods are simpler and faster.

The code is available in the Zope SVN under http://svn.zope.org/book/trunk/messageboard/step01.