Chapter 13
Writing a new Content Object
Difficulty
Newcomer
Skills
- The developer should be familiar with Python and some object-oriented
concepts. Component-based programming concepts are a plus.
- Some understanding of the schema and interface packages. Optional.
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
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)
- Line 1: Import the base Interface class. Any object that has this meta-class
in its inheritance path is an interface and not a regular class.
- Line 2: The attributes and properties of an object are described by fields.
Fields hold the meta-data about an attribute and are used, among other
things, to validate values and create auto-generated input forms. Most
fields are defined in the zope.schema package. For more details and a
complete list of fields see “Zope Schemas and Widgets (Forms)”.
- Line 4: ContainerTypesConstraint conditions allow us to tell the system
to which type of containers an object can be added. For example, a message
only wants to be contained by the message board and another message
(when it is a reply to the parent message). See below how it is used.
- Line 5: The ItemTypePrecondition is the opposite of the container types
constraint in that it specifies the object types that can be contained by a
container. See below for its usage.
- Line 6: Objects providing the IContained interface can be included in
the Zope object tree. We also import IContainer here, which is used
as a base interface in Line 29 and 52. IContainer defines all necessary
methods for this object to be recognized as a container by Zope 3.
Note that we do not need to inherit Interface directly, since IContainer
already inherits it, which automatically makes IMessageBoard also an
interface.
- Line 10: You might have already noticed the “I” in front of all the
interfaces, which simply stands for “Interface” as you might have guessed
already. It is a convention in Zope, so that we do not confuse interfaces
and classes, since these two different object types cannot be used in the
same way.
In general messages simply are objects that have a title and a body.
Nothing more. We later declare more semantics through additional
interfaces and meta-data.
- Line 16-20: A simple title/subject headline for the message. Note that
we made this a TextLine instead of a Text field, so that no newline
characters can be inserted. This way the title will be relatively short and
will be perfect for the title display where desired.
- Line 22-26: The body is the actual message content. Note that we made
no restriction to its size, which you might want to do, in case you are
afraid of spam filling your message board.
- Line 33-36: We do not want to allow any content type to be added to a
message board. In fact, we just want to be able to add IMessage objects.
Therefore we declare a precondition on the __setitem__() method of the
message board interface. Simply list all allowed interfaces as arguments of
the ItemTypePrecondition constructor.
Note: Even though IContainer already defined __setitem__(), we have
to declare it here again, so that it is in the scope of the interface and
specific to the IMessageBoard; otherwise all IContainer objects will
have this precondition.
- Line 38-42: Declare a property on the interface that will contain the
description of the message board. It is a typical Text field with the usual
options (see “Zope Schemas and Widgets (Forms)” for details). One note
though: Notice that we always use unicode strings for human-readable
text - this is a required convention throughout Zope 3. One of the major
focus points of Zope 3 is internationalization and unicode strings are the
first requirement to support multi-lingual applications.
- Line 45-49: This interface describes via field constraint which other
content types can contain a message. Clearly message boards can contain
messages, but also messages can contain other messages - known as
responses. We usually specify this constraint on the parent on the main
content type interface (i.e. IMessage) directly, but since this constraint
refers explicitely to IMessage we have to wait till the interface is defined.
- Line 52-59: We also want the message to be container, so it can contain
responses and attachments. However, we do not want any object to be
able to be added to messages, so we add a precondition as we did for the
IMessageBoard interface. Again, we have to do this in a separate interface
here, since we reference IMessage in the condition.
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''
- Line 1: The implements() method is used to declare that a class
implements one or more interfaces. See “An Introduction to Interfaces”
for more details.
- Line 2: Everything that has to do with containers is located in zope.
app.container. BTreeContainers are a very efficient implementation of
the IContainer interface and are commonly used as base classes for other
containerish objects, such as the message board.
- Line 7-24: The class docstring’s purpose is to document the class. To
follow Python documentation standards, all docstrings should be using the
re-structured text format. And doc tests are considered documentation,
so it should be written in a narrative style.
On line 12-14 we verify that the MessageBoard component really
implements IMessageBoard. The verifyClass function actually checks
the object for the existence of the specified attributes and methods.
Lines 18 through 23 just give a demonstration about the default
description value and how it can be set. The test seems trivial, but at
some point you might change the implementation of the description
attribute to using properties and the test should still pass.
- Line 25: Here we tell the class that it implements IMessage. This function
call might seem like magic, since one might wonder how the function knows
as to which class to assign the interface. For the ones interested, it uses
sys.getframe().
- Line 27-28: Make the description a simple attribute.
Note: Python is very unique in this way. In almost any other
object-oriented language (for example Java) one would have written an
accessor ( getDescription()) and a mutator ( setDescription(desc))
method. However, Python’s attribute and property support makes this
unnecessary, which in turn makes the code cleaner.
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>
- Line 1-2, 65: As the file extension promises, configuration is done using
XML. All configuration in a ZCML file must be surrounded by the
configure element. At the beginning of the configure element, we list
all the ZCML namespaces that we are going use and define the default
one. In this case we only need the generic zope namespace. You will get
to know many more namespaces as we develop new functionality in the
following chapters.
- Line 4-7: It is sometimes necessary to categorize interfaces. One type of
category is to specify which interface provides a content type for Zope 3.
The zope:interface directive is used to assign these types on interfaces.
Another way to think about it is that interfaces are just components, and
components can provide other interfaces.
- Line 9-28: The zope:content directive registers the MessageBoard
class as a content component. The element always has only one attribute,
class, that points to the component’s class using a dotted Python path.
- Line 10-12: In order for the object to have a creation and modification
date as well as other meta-data (for example the Dublin Core),
we need to tell the system that this object can have annotations
associated with itself. This is not necessarily required, but is a good
habit. See the chapter on “Using Annotations to Store Meta-Data”
for details.
Annotations store add-on data which is also commonly known as
meta-data, since it is data that is not necessary for the correct
functioning of the object itself. However, meta-data allows an object
to be better integrated in the system. Annotations are heavily used
in Zope 3.
In general, the zope:implements sub-directive allows you to assert
new implemented interfaces on a class. It is totally equivalent
to classImplements(Class,ISomeInterface) in Python. So why
would we want to declare interfaces in ZCML instead of Python?
For one, it clutters the Python code and distracts from the actual
functionality of the component. Also, when dealing with 3rd party
Python packages, we do not want to touch this code, but still be able
to make assertions about objects, so that they can be used inside
Zope 3 without modification.
Note that usually only “marker interfaces”, interfaces that have
no methods and/or properties, are declared via ZCML, since no
additional Python code for the implementation of the interface is
required.
- Line 13-15: The IContentContainer interface is another example of
a marker interface. All that it declares is that this container contains
ordinary content in content space, which is clearly the case for our
message board.
- Line 16-19: The zope:factory sub-directive allows us to register
a factory named book.messageboard.MessageBoard for the
MessageBoard component.
Every factory needs an id (first directive argument) through which
the factory can be accessed and executed. However, you are not
required to specify an id; if you don’t, the literal string value
of the zope:content’s class attribute is used, which would be
.messageboard.MessageBoard in this case.
The zope:factory directive also supports two human-readable
information strings, title and description, that can be used for
user interfaces.
- Line 20-27: In Zope 3 we differentiate between trusted and untrusted
environments. Trusted environments have no security or can easily
circumvent security. And example is file-based Python code, which
is always trusted. The opposite is true for untrusted environments;
here security should apply everywhere and should not be avoidable.
All Web and FTP transactions are considered untrusted.
Of course, we want to use our message board via the Web, since
it is the default user interface of Zope 3. To make it usable, we
have to declare the minimal security assertions for the properties and
methods of our component. Security assertions are done using the
zope:require and zope:allow directive.
The require directive usually starts out with specifying a
permission. Then we have to decide what we want to protect with
this declaration. Here are your choices:
- The attributes attribute allows us to specify attributes and
methods (note that methods are just callable attributes) that
can be accessed if the user has the specified permission.
- set_attributes allows you to specify individual attributes that
can be modified or mutated. Note that you should not list any
methods here, since otherwise someone could override a method
inserting malicious code.
- If you specify one or more interfaces using the interface
attribute, the directive will automatically extract all declared
methods and properties of the interfaces and grant access rights
to them.
- When you specify a set of schemas using the set_schema
attribute, then all the defined properties in it are granted
modification rights. Methods listed in the schema will be ignored.
Note: In ZCML tokens of a list are separated by simple whitespace and
not by comma, as you might have expected.
A somewhat different option to the above choices is the like_class
attribute, which must be specified without any permission. If used, it
simply transfers all the security assertions from the specified class to the
class specified in the zope:content directive that encloses the security
assertions. In our case this is our MessageBoard component. The usage of
the directive looks like this:
1 <require like_class=".message.Message" />
Here the MessageBoard would simply “inherit” the security assertions
made for the Message component.
The second security directive, zope:allow, either takes a set of
attributes or interfaces. All attributes specified will be publicly
available for everyone to access. This is equivalent to requiring someone to
have the zope.Public permission, which every principal accessing the
system automatically possesses.
So now it is easy to decipher the meaning of our two security assertions.
We basically gave read and write access to the IMessageBoard interface
(which includes all IContainer methods and the description
attribute), if the user has the zope.ManageContent permission.
- Line 30-54: This is the same as above for the Message content component.
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>
- Line 2: In this configuration file we do not use the zope, but the browser
namespace, since we want to configure browser-specific functionality. Also
note that browser is the default namespace, so that our directives do not
need the namespace prefix.
Namespaces for ZCML start commonly
with http://namespaces.zope.org/ followed by the short name of the
namespace, which is commonly used in this book to refer to namespaces.
- Line 4-11: Register an auto-generated “Add” form for the Message Board.
- Line 5: The label is a small text that is shown on top of the screen.
- Line 6: The name of the view. The name is the string that is actually
part of the URL.
- Line 7: This defines the schema that will be used to generate the form.
The fields of the schema will be used to provide all the necessary
meta data to create meaningful form elements.
- Line 8: The content factory is the class/factory used to create the
new content component.
- Line 9: The fields are a list of field names that are displayed in the
form. This allows you to create forms for a subset of fields in the
schema and to change the order of the fields in the form.
- Line 10: Specifies the permission required to be able to create and
add the new content component.
- Line 13-19: After creating a view for adding the message board, we now have to
register it with the add menu, which is done with the browser:addMenuItem
directive. The title is used to display the item in the menu. The
important attribute is the view, which must match the name of the add
form.
- Line 21-28: Declaring an edit form is very similar to defining an add form and
several of the options/attributes are the same. The main difference is that we
do not need to specify a content factory, since the content component already
exists.
The for attribute specifies the interface for which type of component the
edit form is for. All view directives (except the browser:addform)
require the for attribute. If you would like to register a view for a
specific implementation, you can also specify the class in the for
attribute.
We also commonly specify the menu for edit views directly in the directive
using the menu and title attribute as seen on line 27. The zmi_views menu
is the menu that creates the tabs on the default Web UI. It contains all views
that are specific to the object.
- Line 30-35: The message board is a container and a quick way to register all
necessary container-specific views is to use the browser:containerViews
directive. Note though that this directive is not very flexible and you should
later replace it by writing regular views.
- Line 37-69: These are exactly the same directives over again, this time just for
the IMessage interface.
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.