The Zope 3 Book

Author: Baiju M
Date: 0.1.1 (12-October-2008)

Copyright 2008 Baiju M <baiju.m.mail AT gmail.com>

This work is licensed under the Creative Commons Attribution-ShareAlike License 3.0. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative Commons, 559 Nathan Abbott Way, Stanford, California 94305, USA. A summary of the license is given below.

You are free:

Under the following conditions:

Contents

1   Introduction

1.1   Overview

This book is about Zope 3, a Python framework for web application development. Zope 3 was developed by the Zope community with the leadership of Jim Fulton, the creator of original Zope. Zope 3 consists of a number of small frameworks and libraries written in Python programming language and it is usable in pieces or in whole. These frameworks and libraries can be put together to build any kind of web application. Most of of the Zope 3 packages are built on top of a component architecture which helps to separate presentation code from the problem domain code and to create reusable components (zope.component).

The goal for this book project is to create a complete, free, open-content, well-organized book for Zope 3. The target audience of this book are Python programmers looking for developing web applications. However, the book doesn't assume you are familiar with any other web framework. This book will require prior knowledge Python programming language and at least some exposure to the basics of HTML, CSS and JavaScript.

Zope 3 has an object publisher (zope.publisher), web server (zope.server), transactional object database (ZODB), XML based configuration language for registering components (zope.configuration), flexible security architecture with pluggable security policies (zope.security), unit and functional testing frameworks (zope.testing , zope.testbrowser), XHTML-compliant templating language (zope.pagetemplate), schema engine and automatic form generation machinery (zope.schema , z3c.form) and many more core and third-party packages.

Originally, the term ZOPE was used as an acronym for Z Object Publishing Environment (the Z doesn't really mean anything in particular). However, now-a-days ZOPE is simply written as Zope .

Zope 3 is a ZPL (BSD like, GPL compatible license) licensed free/open source software. It was developed by the Zope community with the leadership of Jim Fulton. A brief history is given in the next section.

1.2   Scope of the book

The intension of this book is not to cover how to use Zope 3 packages independently or with other Python applications/frameworks. Rather, this book focus on developing web applications using Zope 3 packages. More specifically, this book is not going to cover using Zope 3 technology in Zope 2, Plone, Grok or any other Python application/framework. WSGI is also not a current focus of this book. This book is not going to cover using zopeproject to bootstrap application (it's very easy, look at the PyPI page for zopeproject). This book use Buildout for setting up an isolated development environment for building applications. Setuptools and vitualenv are also covered.

1.3   Audience

The target audience of this book are Python programmers looking for developing web applications. However, the book doesn't assume you are familiar with any other web framework.

1.4   Prerequisites

This book will require prior knowledge Python programming language and at least some exposure to the basics of HTML, CSS and JavaScript.

1.5   Brief history

The beginning of Zope's story goes something like this, in 1996, Jim Fulton (CTO of Zope Corporation) was drafted to teach a class on common gateway interface (CGI) programming, despite not knowing very much about the subject. CGI programming is a commonly-used web development model that allows developers to construct dynamic websites. On his way to the class, Jim studied all the existing documentation on CGI. On the way back, Jim considered what he didn't like about traditional, CGI-based programming environments. From these initial musings, the core of Zope was written while flying back from the CGI class.

Zope Corporation (then known as Digital Creations) went on to release three open-source software packages to support web publishing: Bobo, Document Template, and BoboPOS. These packages were written in a language called Python, and provided a web publishing facility, text templating, and an object database, respectively. Digital Creations developed a commercial application server based on their three opensource components. This product was called Principia. In November of 1998, investor Hadar Pedhazur convinced Digital Creations to open source Principia. These packages evolved into what are now the core components of Zope 2.

In 2001, the Zope community began working on a component architecture for Zope, but after several years they ended up with something much more: Zope 3. While Zope 2 was powerful and popular, Zope 3 was designed to bring web application development to the next level. This book is about this Zope 3, which is not really a new version of Zope 2.

Most recently, in 2007 the Zope community created yet another framework based on Zope 3 called Grok. The original Zope which is now known as Zope 2 is also widely used.

1.6   Organization of the book

This book has divided into multiple chapters. Summary of each chapter is given below.

1.6.1   Introduction

This chapter introduce Zope 3 with an overview and scope of the book, then briefly go through the history of Zope 3. Later discuss organization of the book. And finish with a thanks section.

1.6.2   Getting Started

This chapter begins with installation details of Python and Zope 3. Then introduce Buildout, the build system we use to setup an isolated Python working environment and its configurations. Later, it explore setting up development sandbox using Buildout. A simple application is developed further and it ends with a hello world page. During the application development we see how to use ZMI (Zope Management Interface) briefly. This chapter also provides a brief overview of important packages and installation of additional packages.

1.6.3   Development Tools

This chapter is going to the details about how to develop a web application using Python and Zope components. You should familiarize some essential tools like Python eggs, setuptools and buildouts. If you are already familiar with these you may skip this chapter.

1.6.4   Interfaces

This chapter introduce the concept of interfaces. If you are already familiar with this you may skip this chapter.

1.6.5   Component Architecture

This chapter introduce Zope component architecture. If you are already familiar with this you may skip this chapter.

1.7   Thanks

This book would not be possible if Zope 3 did not exist. For that, the I would like to thank all developers of Zope 3. I am grateful to Stephan Richter for allowing me to use his book and training material for this work.

2   Getting Started

2.1   Python installation

The Zope community has always recommended using a custom built Python for development and deployment. Both Python 2.4 and 2.5 will work for Zope 3 packages. As of now, the author reccomends using Python 2.5 for any new projects.

2.1.1   GNU/Linux

To install Python, you will be required to install gcc, g++ and other development tools in your system. A typical installation of Python can be done like this:

$ wget -c http://www.python.org/ftp/python/2.4.5/Python-2.4.5.tar.bz2
$ tar jxvf Python-2.4.5.tar.bz2
$ cd Python-2.4.5
$ ./configure --prefix=/home/guest/usr
$ make
$ make install

As given above, you can provide an option, --prefix to install Python in a particular location. The above steps install Python inside /home/guest/usr directory.

After installation, you can invoke the Python interpreter like this:

$ ~/usr/bin/python2.4
>>> print "Hello, world!"
Hello, world!

If you are not getting old statements in Python interactive prompt when using up-arrow key, try installing libreadline development libraries (Hint: apt-cache search libreadline). After installing this library, you should install Python again. You also will be required to install zlib (Hint: apt-cache search zlib compression library) to properly install Zope 3.

2.1.2   MS Windows

FIXME: Write about installation of Python in MS Windows with few screenshots.

2.2   Buildout

2.2.1   Introduction

We are going to use a build tool called Buildout for developing Zope 3 applications from multiple parts. Buildout will give you an isolated working environment for developing applications. The Buildout package, named zc.buildout is available for download from PyPI. This section briefly goes through the usage of Buildout for developing applications.

Buildout has a boostrap.py script for initializing a buildout based project for development or deployment. It will download and install zc.buildout, setuptools and other dependency modules in a specified directory. Once bootstrapped it will create a buildout executable script inside bin directory at the top of your project source. The default configuration for each project is buildout.cfg file at the top of your project source. Whenever you run the buildout command it will look into the default configuration file and will do actions based on it. Normally, the configuration file and boostrap script will be bundled with the project source itself. Other than the default configuration file along with the project source, you may also create a system wide default configuration file at ~/.buildout/default.cfg .

Buildout creator Jim Fulton recommend a custom built clean Python installation, i.e., there should not be any Python modules installed in your site-packages (ideally, a fresh Python installation). When you boostrap your project using Buildout's boostrap.py script, it will download and install all necessary packages in a specified directory. So, for an ideal project you only required a custom built clean Python and the project source with proper Buildout configuration and bootstrap script along with the source package.

2.2.2   Buildout configuration

These days, most of the Python packages are available in egg format. Buildout will download and install the eggs in a specified directory and the location can be changed from the configuration file. It is better to give a system-wide location for eggs directory. And this configuration can be added to your system-wide configuration file. The default configuration file for Buildout is ~/.buildout/default.cfg . We are going to use eggs directory inside your home directory to keep all eggs, so first create those directories and global configuration file:

$ cd $HOME
$ mkdir .buildout
$ mkdir eggs
$ touch .buildout/default.cfg

You can add the following to your global configuration file (~/.buildout/default.cfg):

[buildout]
newest = false
eggs-directory = /home/guest/eggs
find-links = http://download.zope.org/ppix

The eggs-directory is where Buildout stores the eggs that are downloaded. The last option, find-links points to a reliable mirror of the Python Package Index (PyPI). The default configurations given above will be available to all buildouts in your system.

2.3   Setting up development sandbox

To demonstrate the concepts, tools and techniques, we are going to develop a simple ticket/issue tracking application named Ticket Collector. To begin the work, first create a directory for the project. After creating the directory, create a configuration file, buildout.cfg as given below. To bootstrap this application checkout bootstrap.py and run it using a clean Python.

$ mkdir ticketcollector
$ cd ticketcollector
$ echo "#Buildout configuration" > buildout.cfg
$ svn co svn://svn.zope.org/repos/main/zc.buildout/trunk/bootstrap
$ ~/usr/bin/python2.4 bootstrap/bootstrap.py

You can see a buildout script created inside bin directory. Now onwards, you can run this script when changing Buildout configuration.

Note

You can save bootstrap.py in a local repository. If you are using svn for managing repository, create an svn:external to the svn URL given above.

Our application is basically a Python package. First, we will create an src directory to place our package. Inside the src directory, you can create ticketcollector Python package. You can create the src and the ticketcollector package like this:

$ mkdir src
$ mkdir src/ticketcollector
$ echo "#Python package" > src/ticketcollector/__init__.py

To start building our package you have to create a setup.py file. The setup.py should have the minimum details as given below:

from setuptools import setup, find_packages

setup(
    name='ticketcollector',
    version='0.1',

    packages=find_packages('src'),
    package_dir={'': 'src'},

    install_requires=['setuptools',
                      'zope.app.zcmlfiles',
                      'zope.app.twisted',
                      'zope.app.securitypolicy',
                      ],
    include_package_data=True,
    zip_safe=False,
    )

We have included the bare minimum packages required for installation in install_requires argument: zope.app.zcmlfiles, zope.app.twisted , zope.app.securitypolicy and setuptools.

To make this package buildout aware, we have to modify the buildout.cfg as given below:

[buildout]
develop = .
parts = py

[py]
recipe = zc.recipe.egg
eggs = ticketcollector
interpreter = python

Now run the buildout script inside bin directory. This will download all necessary eggs and install it.

$ ./bin/buildout

As you can see above, installing Zope is nothing but just setting up a buildout with setup.py with proper packages given as install_requires in it.

Note

Unless you specify a parts section which use ticketcollector in some way, Buildout will not download dependency packages. In the above example, we created a [py] section with zc.recipe.egg recipe.

2.4   A simple application

2.4.1   Configuring application

We are going to continue the Ticket Collector application in this section. In the last section when you run ./bin/buildout command all necessary Zope 3 packages required for running our application is downloaded inside ~/eggs directory. Now to run the bare minimum Zope 3, we have to create Zope Configuration Markup Language (ZCML) file and extend the buildout.cfg with appropriate Buildout recipes. We are going to use zc.zope3recipes:app, zc.zope3recipes:instance and zc.recipe.filestorage recipes for setting up our application. Here is our modified buildout.cfg (inside the ticketcollector project directory):

[buildout]
develop = .
parts = ticketcollectorapp instance

[zope3]
location =

[ticketcollectorapp]
recipe = zc.zope3recipes:app
site.zcml =
  <include
    package="ticketcollector"
    file="application.zcml"
    />
eggs = ticketcollector

[instance]
recipe = zc.zope3recipes:instance
application = ticketcollectorapp
zope.conf = ${database:zconfig}

[database]
recipe = zc.recipe.filestorage

Then we will create application.zcml inside src/ticketcollector directory with the following text. Consider it as boiler plate code now, we will explain this in detail later:

<configure
  xmlns="http://namespaces.zope.org/zope"
  xmlns:browser="http://namespaces.zope.org/browser"
  >

  <include package="zope.securitypolicy"
    file="meta.zcml"
    />

  <include package="zope.app.zcmlfiles" />
  <include package="zope.app.authentication" />
  <include package="zope.app.securitypolicy" />
  <include package="zope.app.twisted" />

  <securityPolicy
    component="zope.securitypolicy.zopepolicy.ZopeSecurityPolicy"
    />

  <role id="zope.Anonymous" title="Everybody"
    description="All users have this role implicitly"
    />

  <role id="zope.Manager" title="Site Manager" />

  <role id="zope.Member" title="Site Member" />

  <grant permission="zope.View"
    role="zope.Anonymous"
    />

  <grant permission="zope.app.dublincore.view"
    role="zope.Anonymous"
    />

  <grantAll role="zope.Manager" />

  <unauthenticatedPrincipal
    id="zope.anybody"
    title="Unauthenticated User"
    />

  <unauthenticatedGroup
    id="zope.Anybody"
    title="Unauthenticated Users"
    />

  <authenticatedGroup
    id="zope.Authenticated"
    title="Authenticated Users"
    />

  <everybodyGroup
    id="zope.Everybody"
    title="All Users"
    />

  <principal
    id="zope.manager"
    title="Manager"
    login="admin"
    password_manager="Plain Text"
    password="admin"
    />

  <grant
    role="zope.Manager"
    principal="zope.manager"
    />

</configure>

2.4.2   Running application

Now you can run the application by executing ./bin/buildout command followed by ./bin/instance command.

$ ./bin/buildout
$ ./bin/instance fg

2.4.3   Using ZMI

After running your instance, If you open a web browser and go to http://localhost:8080 you'll see the ZMI (Zope Management Interface ).

Go ahead and click the Login link at the upper right corner. Enter the user name and password as admin, which is given in applications.zcml. Now click on [top] under Navigation on the right. Play around with adding some content objects (the Zope 3 name for instances that are visible in the ZMI). Note how content objects can be arranged in a hierarchy by adding folders which are special content objects that can hold other content objects.

There is nothing special about the ZMI, it is just the default skin for Zope 3. You can modify it to your liking, or replace it entirely.

When you're done exploring with the ZMI, go back to the window where you typed ./bin/instance fg and press Control-C to stop Zope 3.

2.4.4   Hello world

Now you can begin your development inside src/ticketcollector directory. Create a browser.py with following content:

from zope.publisher.browser import BrowserView

class HelloView(BrowserView):

    def __call__(self):
        return """
        <html>
        <head>
          <title>Hello World</title>
        </head>
        <body>
          Hello World
        </body>
        </html>
        """

Now append the following text just above the last line of application.zcml:

<browser:page
  for="*"
  name="hello"
  permission="zope.Public"
  class="ticketcollector.browser.HelloView"
  />

After restarting Zope, open http://localhost:8080/hello, you can see that it displays Hello World!.

3   Development Tools

Before going to the details about how to develop a web application using Python and Zope components, we should familiarize some essential tools like Python eggs, setuptools and buildouts. If you are already familiar with these you may skip this chapter.

3.1   Eggs

Eggs are Python's new distribution format managed using setuptool package http://peak.telecommunity.com/DevCenter/PythonEggs .

To install an egg, you can use easy_install program http://peak.telecommunity.com/DevCenter/EasyInstall .

An easy way to automate the installtion would be to use easy_setup.py program.

$ wget -c http://peak.telecommunity.com/dist/ez_setup.py
$ python easy_setup.py

3.2   Buildout

The Buildout project provides support for creating applications, especially Python applications. It provides tools for assembling applications from multiple parts, Python or otherwise. An application may actually contain multiple programs, processes, and configuration settings.

The word buildout refers to a description of a set of parts and the software to create and assemble them. It is often used informally to refer to an installed system based on a buildout definition. For example, if we are creating an application named Foo`, then the Foo buildout is the collection of configuration and application-specific software that allows an instance of the application to be created. We may refer to such an instance of the application informally as a Foo buildout.

Buildout provides support for creating, assembling and deploying applications, especially Python applications. You can build applications using Buildout recipes. Recipes are Python programs which follows a pattern to build various parts of an application. For example, a recipe will install Python eggs and another one will install test runner etc. Applications can be assembled from multiple parts with different configurations. A part can be a Python egg or any other program. We have already seen how to use buildout to setup a Zope~3 application in the getting started chapter.

3.3   Buildout recipes

Buildout recipes are distributed in egg formats. Some examples of recipes are:

  • zc.recipe.egg -- The egg recipe installes one or more eggs, with their dependencies. It installs their console-script entry points with the needed eggs included in their paths.
  • zc.recipe.testrunner -- The testrunner recipe creates a test runner script for one or more eggs.
  • zc.recipe.zope3recipes -- Recipes for creating Zope 3 instances with distinguishing features:
    • Don't use a skeleton
    • Separates application and instance definition
    • Don't support package-includes
  • zc.recipe.filestorage -- The filestorage recipe sets up a ZODB file storage for use in a Zope 3 instance creayed by the zope3recipes recipe.

3.3.1   Using a recipe

The procedure for using a recipe is common for almost all recipes. You can create buildouts with parts controlled recipes. Suppose you want to experiment with one package, say, zope.component, you can use zc.recipe.egg for installing it in buildout. The zc.recipe.egg will also provide an interpreter with the egg installed in the path. First you can create a directory and initialize Buildout:

$ mkdir explore-zope.component
$ cd explore-zope.component
$ echo "#Buildout configuration" > buildout.cfg
$ svn co svn://svn.zope.org/repos/main/zc.buildout/trunk/bootstrap
$ ~/usr/bin/python2.4 bootstrap/bootstrap.py

Now modify the buildout.cfg like this:

[buildout]
parts = py

[py]
recipe = zc.recipe.egg
interpreter = mypython
eggs = zope.component

Now run buildout script inside bin directory. This will download zope.component and its dependency eggs and install it. Now you can access the interpreter created by the Buildout recipe like this:

$ ./bin/buildout
$ ./bin/mypython
>>> import zope.component

3.4   Developing a package

The initial steps are not different from the above exmaple:

$ mkdir hello
$ cd hello
$ echo "#Buildout configuration" > buildout.cfg
$ svn co svn://svn.zope.org/repos/main/zc.buildout/trunk/bootstrap
$ ~/usr/bin/python2.4 bootstrap/bootstrap.py

Our application is a simple hello world package. First we will create an src directory to place our package. Inside the src directory, you can create the hello Python package. You can create the src and the hello package like this:

$ mkdir src
$ mkdir src/hello
$ echo "#Python package" > src/hello/__init__.py

Now create a file named say.py inside the hello package with this code:

def say_hello():
    print "Hello"

To start building our package you have to create a setup.py file. The setup.py should have the minimum details as given below:

from setuptools import setup, find_packages

setup(
  name='hello',
  version='0.1',

  packages=find_packages('src'),
  package_dir={'': 'src'},

  install_requires=['setuptools',
                    ],
  entry_points = {'console_scripts':
                  ['print_hello = hello.say:say_hello']},
  include_package_data=True,
  zip_safe=False,
  )

Modify buildout.cfg as given below:

[buildout]
develop = .
parts = py

[py]
recipe = zc.recipe.egg
scripts = print_hello
eggs = hello

Now run buildout script inside bin directory. Now you can run the print_hello script.

$ ./bin/buildout
$ ./bin/print_hello
Hello

3.5   Summary

This chapter provided a brief introduction to eggs. Later we found how to use buildout tool for developing application.

4   Interfaces

4.1   Introduction

Interfaces are objects that specify (document) the external behavior of objects that "provide" them. An interface specifies behavior through:

  • Informal documentation in a doc string
  • Attribute definitions
  • Invariants, which are conditions that must hold for objects that provide the interface

Some of the motivations for using interfaces are:

  • Avoid monolithic design by developing small, exchangeable pieces
  • Model external responsibility, functionality, and behavior
  • Establish contracts between pieces of functionality
  • Document the API

The classic software engineering book Design Patterns by the Gang of Four recommends: "Program to an interface, not an implementation". Defining a formal interface is helpful in understanding a system. Moreover, interfaces bring you all the benefits of Zope Component Architecture which we are going to learn in next chapter.

In some modern programming languages: Java, C#, VB.NET etc, interfaces are an explicit aspect of the language. Since Python lacks interfaces, Zope implements them as a meta-class to inherit from.

4.2   Types of contract

"I can do X":Describing the ability to do something is the classical definition of an API. Those abilities are defined and implemented as methods.
"I have X":This statement declares the availability of data, which is classically associated with schemas. The data is stored in attributes and properties.
"You can do X with me":
 Here we describe the behavior of an object. Classically there is no analog. However, MIME-types are great example of behavior declaration. This is implemented using empty "marker interfaces" as they describe implicit behavior.

The distinction between those three types of contracts was first pointed out in this form by Philipp von Weitershausen.

Understanding those distinctions is very important, since other programming languages do not necessarily use all three of these notions. In fact, often only the first one is used.

4.3   Defining interfaces

  • Python has no concept of interfaces
  • Not a problem
  • Interfaces are just objects
  • "Abuse" the class statement to create an interface
  • Syntax proposed in PEP 245

Jim Fulton does not see this as a problem, since it makes interfaces first class citizens. In Java, for example, interfaces are special types of objects that can only serve as interfaces in their intended, limited scope.

An interface from the zope.interface package, on the other hand, defines the interface by implementing a meta-class, a core concept of Python. Thus, interfaces are merely using an existing Python pattern.

4.4   An example

Here is a classic hello world style example:

>>> class Host(object):
...
...     def goodmorning(self, name):
...         """Say good morning to guests"""
...
...         return "Good morning, %s!" % name

In the above class, you defined a goodmorning method. If you call the goodmorning method from an object created using this class, it will return Good morning, ...!

>>> host = Host()
>>> host.goodmorning('Jack')
'Good morning, Jack!'

Here host is the actual object your code uses. If you want to examine implementation details you need to access the class Host, either via the source code or an API documentation tool.

Now we will begin to use the Zope interfaces. For the class given above you can specify the interface like this:

>>> from zope.interface import Interface

>>> class IHost(Interface):
...
...     def goodmorning(guest):
...         """Say good morning to guest"""

As you can see, the interface inherits from zope.interface.Interface. This use (abuse?) of Python's class statement is how Zope defines an interface. The I prefix for the interface name is a useful convention.

4.5   Declaring interfaces

You have already seen how to declare an interface using zope.interface in previous section. This section will explain the concepts in detail.

Consider this example interface:

>>> from zope.interface import Interface
>>> from zope.interface import Attribute

>>> class IHost(Interface):
...     """A host object"""
...
...     name = Attribute("""Name of host""")
...
...     def goodmorning(guest):
...         """Say good morning to guest"""

The interface, IHost has two attributes, name and goodmorning. Recall that, at least in Python, methods are also attributes of classes. The name attribute is defined using zope.interface.Attribute class. When you add the attribute name to the IHost interface, you don't set an initial value. The purpose of defining the attribute name here is merely to indicate that any implementation of this interface will feature an attribute named name. In this case, you don't even say what type of attribute it has to be!. You can pass a documentation string as a first argument to Attribute.

The other attribute, goodmorning is a method defined using a function definition. Note that self is not required in interfaces, because self is an implementation detail of class. For example, a module can implement this interface. If a module implement this interface, there will be a name attribute and goodmorning function defined. And the goodmorning function will accept one argument.

Now you will see how to connect interface-class-object. So object is the real living thing, objects are instances of classes. And interface is the actual definition of the object, so classes are just the implementation details. This is why you should program to an interface and not to an implementation.

Now you should familiarize two more terms to understand other concepts. First one is provide and the other one is implement. Object provides interfaces and classes implement interfaces. In other words, objects provide interfaces that their classes implement. In the above example host (object) provides IHost (interface) and Host (class) implement IHost (interface). One object can provide more than one interface also one class can implement more than one interface. Objects can also provide interfaces directly, in addition to what their classes implement.

Note

Classes are the implementation details of objects. In Python, classes are callable objects, so why other callable objects can't implement an interface. Yes, it is possible. For any callable object you can declare that it produces objects that provide some interfaces by saying that the callable object implements the interfaces. The callable objects are generally called as factories. Since functions are callable objects, a function can be an implementer of an interface.

4.6   Implementing interfaces

To declare a class implements a particular interface, use the function zope.interface.implements in the class statement.

Consider this example, here Host implements IHost:

>>> from zope.interface import implements

>>> class Host(object):
...
...     implements(IHost)
...
...     name = u''
...
...     def goodmorning(self, guest):
...         """Say good morning to guest"""
...
...         return "Good morning, %s!" % guest

Note

If you wonder how implements function works, refer the blog post by James Henstridge (http://blogs.gnome.org/jamesh/2005/09/08 /python-class-advisors/) . In the adapter section, you will see an adapts function, it is also working similarly.

Since Host implements IHost, instances of Host provides IHost. There are some utility methods to introspect the declarations. The declaration can write outside the class also. If you don't write interface.implements(IHost) in the above example, then after defining the class statement, you can write like this:

>>> from zope.interface import classImplements
>>> classImplements(Host, IHost)

4.7   Marker interfaces

An interface can be used to declare that a particular object belongs to a special type. An interface without any attribute or method is called marker interface.

Here is a marker interface:

>>> from zope.interface import Interface

>>> class ISpecialGuest(Interface):
...     """A special guest"""

This interface can be used to declare an object is a special guest.

4.8   Invariants

Sometimes you will be required to use some rule for your component which involve one or more normal attributes. These kind of rule is called invariants. You can use zope.interface.invariant for setting invariants for your objects in their interface.

Consider a simple example, there is a person object. A person object has name, email and phone attributes. How do you implement a validation rule that says either email or phone have to exist, but not necessarily both.

First you have to make a callable object, either a simple function or callable instance of a class like this:

>>> def contacts_invariant(obj):
...
...     if not (obj.email or obj.phone):
...         raise Exception(
...             "At least one contact info is required")

Then define the person object's interface like this. Use the zope.interface.invariant function to set the invariant:

>>> from zope.interface import Interface
>>> from zope.interface import Attribute
>>> from zope.interface import invariant

>>> class IPerson(Interface):
...
...     name = Attribute("Name")
...     email = Attribute("Email Address")
...     phone = Attribute("Phone Number")
...
...     invariant(contacts_invariant)

Now use validateInvariants method of the interface to validate:

>>> from zope.interface import implements

>>> class Person(object):
...     implements(IPerson)
...
...     name = None
...     email = None
...     phone = None

>>> jack = Person()
>>> jack.email = u"jack@some.address.com"
>>> IPerson.validateInvariants(jack)
>>> jill = Person()
>>> IPerson.validateInvariants(jill)
Traceback (most recent call last):
...
Exception: At least one contact info is required

As you can see jack object validated without raising any exception. But jill object didn't validated the invariant constraint, so it raised exception.

5   Component Architecture

Zope Component Architecture (ZCA) is a framework for supporting component based design and programming. It is very well suited to developing large Python software systems. The ZCA is not specific to the Zope web application server: it can be used for developing any Python application.

The ZCA is all about using Python objects effectively. Components are reusable objects with introspectable interfaces. A component provides an interface implemented in a class, or any other callable object. It doesn't matter how the component is implemented, the important part is that it comply with its interface contracts. Using ZCA, you can spread the complexity of systems over multiple cooperating components. It helps you to create two basic kinds of components: adapter and utility.

There are two core packages related to the ZCA:

Remember, the ZCA is not about the components themselves, rather it is about creating, registering, and retrieving components. Remember also, an adapter is a normal Python class (or a factory in general) and utility is a normal Python callable object.

The ZCA framework is developed as part of the Zope 3 project. As noted earlier, it is a pure Python framework, so it can be used in any kind of Python application. Currently both Zope 3 and Zope 2 projects use this framework extensively. There are many other projects including non-web applications using it.

5.1   Installation

Using zc.buildout with zc.recipe.egg recipe you can create Python interpreter with specified Python eggs. First you can create a directory and initialize Buildout:

$ mkdir explore-zope.component
$ cd explore-zope.component
$ echo "#Buildout configuration" > buildout.cfg
$ svn co svn://svn.zope.org/repos/main/zc.buildout/trunk/bootstrap
$ ~/usr/bin/python2.4 bootstrap/bootstrap.py

Now modify the buildout.cfg like this:

[buildout]
parts = py

[py]
recipe = zc.recipe.egg
eggs = zope.component
interpreter = mypython

Now run buildout script inside bin directory. This will download zope.component and its dependency eggs and install it. Now you can access the interpreter created by the Buildout recipe like this:

$ ./bin/buildout
$ ./bin/mypython
>>> import zope.component

5.2   Adapters

5.2.1   Implementation

This section will describe adapters in detail. Zope component architecture, as you noted, helps to effectively use Python objects. Adapter components are one of the basic components used by Zope component architecture for effectively using Python objects. Adapter components are Python objects, but with well defined interface.

To declare a class is an adapter use adapts function defined in zope.component package. Here is a new FrontDeskNG adapter with explicit interface declaration:

>>> from zope.interface import implements
>>> from zope.component import adapts

>>> class FrontDeskNG(object):
...
...     implements(IDesk)
...     adapts(IGuest)
...
...     def __init__(self, guest):
...         self.guest = guest
...
...     def register(self):
...         guest = self.guest
...         next_id = get_next_id()
...         bookings_db[next_id] = {
...         'name': guest.name,
...         'place': guest.place,
...         'phone': guest.phone
...         }

What you defined here is an adapter for IDesk, which adapts IGuest object. The IDesk interface is implemented by FrontDeskNG class. So, an instance of this class will provide IDesk interface.

>>> class Guest(object):
...
...     implements(IGuest)
...
...     def __init__(self, name, place):
...         self.name = name
...         self.place = place

>>> jack = Guest("Jack", "Bangalore")
>>> jack_frontdesk = FrontDeskNG(jack)

>>> IDesk.providedBy(jack_frontdesk)
True

The FrontDeskNG is just one adapter you created, you can also create other adapters which handles guest registration differently.

5.2.2   Registration

To use this adapter component, you have to register this in a component registry also known as site manager. A site manager normally resides in a site. A site and site manager will be more important when developing a Zope 3 application. For now you only required to bother about global site and global site manager ( or component registry). A global site manager will be in memory, but a local site manager is persistent.

To register your component, first get the global site manager:

>>> from zope.component import getGlobalSiteManager
>>> gsm = getGlobalSiteManager()
>>> gsm.registerAdapter(FrontDeskNG,
...                     (IGuest,), IDesk, 'ng')

To get the global site manager, you have to call getGlobalSiteManager function available in zope.component package. In fact, the global site manager is available as an attribute (globalSiteManager) of zope.component package. So, you can directly use zope.component.globalSiteManager attribute. To register the adapter in component, as you can see above, use registerAdapter method of component registry. The first argument should be your adapter class/factory. The second argument is a tuple of adaptee objects, i.e, the object which you are adapting. In this example, you are adapting only IGuest object. The third argument is the interface implemented by the adapter component. The fourth argument is optional, that is the name of the particular adapter. Since you gave a name for this adapter, this is a named adapter. If name is not given, it will default to an empty string ('').

In the above registration, you have given the adaptee interface and interface to be provided by the adapter. Since you have already given these details in adapter implementation, it is not required to specify again. In fact, you could have done the registration like this:

>>> gsm.registerAdapter(FrontDeskNG, name='ng')

There are some old API to do the registration, which you should avoid. The old API functions starts with provide, eg: provideAdapter, provideUtility etc. While developing a Zope 3 application you can use Zope configuration markup language (ZCML) for registration of components. In Zope 3, local components (persistent components) can be registered from Zope Management Interface (ZMI) or you can do it programmatically also.

You registered FrontDeskNG with a name ng. Similarly you can register other adapters with different names. If a component is registered without name, it will default to an empty string.

5.2.3   Querying adapter

Retrieving registered components from component registry is achieved through two functions available in zope.component package. One of them is getAdapter and the other is queryAdapter. Both functions accepts same arguments. The getAdapter will raise ComponentLookupError if component lookup fails on the other hand queryAdapter will return None.

You can import the methods like this:

>>> from zope.component import getAdapter
>>> from zope.component import queryAdapter

In the previous section you have registered a component for guest object (adaptee) which provides IDesk interface with name as ng. In the first section of this chapter, you have created a guest object named jack.

This is how you can retrieve a component which adapts the interface of jack object (IGuest) and provides IDesk interface also with name as ng. Here both getAdapter and queryAdapter works similarly:

>>> getAdapter(jack, IDesk, 'ng') #doctest: +ELLIPSIS
<FrontDeskNG object at ...>
>>> queryAdapter(jack, IDesk, 'ng') #doctest: +ELLIPSIS
<FrontDeskNG object at ...>

As you can see, the first argument should be adaptee then, the interface which should be provided by component and last the name of adapter component.

If you try to lookup the component with an name not used for registration but for same adaptee and interface, the lookup will fail. Here is how the two methods works in such a case:

>>> getAdapter(jack, IDesk, 'not-exists') #doctest: +ELLIPSIS
Traceback (most recent call last):
...
ComponentLookupError: ...
>>> reg = queryAdapter(jack,
...           IDesk, 'not-exists') #doctest: +ELLIPSIS
>>> reg is None
True

As you can see above, getAdapter raised a ComponentLookupError exception, but queryAdapter returned None when lookup failed.

The third argument, the name of registration, is optional. If the third argument is not given it will default to empty string (''). Since there is no component registered with an empty string, getAdapter will raise ComponentLookupError. Similarly queryAdapter will return None, see yourself how it works:

>>> getAdapter(jack, IDesk) #doctest: +ELLIPSIS
Traceback (most recent call last):
...
ComponentLookupError: ...
>>> reg = queryAdapter(jack, IDesk) #doctest: +ELLIPSIS
>>> reg is None
True

In this section you have learned how to register a simple adapter and how to retrieve it from component registry. These kind of adapters is called single adapter, because it adapts only one adaptee. If an adapter adapts more that one adaptee, then it is called multi adapter.

5.2.4   Retrieving adapter using interface

Adapters can be directly retrieved using interfaces, but it will only work for non-named single adapters. The first argument is the adaptee and the second argument is a keyword argument. If adapter lookup fails, second argument will be returned.

>>> IDesk(jack, alternate='default-output')
'default-output'

Keyword name can be omitted:

>>> IDesk(jack, 'default-output')
'default-output'

If second argument is not given, it will raise `TypeError`:

>>> IDesk(jack) #doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
Traceback (most recent call last):
...
TypeError: ('Could not adapt',
  <Guest object at ...>,
  <InterfaceClass __builtin__.IDesk>)

Here `FrontDeskNG` is registered without name:

>>> gsm.registerAdapter(FrontDeskNG)

Now the adapter lookup should succeed:

>>> IDesk(jack, 'default-output') #doctest: +ELLIPSIS
<FrontDeskNG object at ...>

For simple cases, you may use interface to get adapter components.

5.3   Utility

Now you know the concept of interface, adapter and component registry. Sometimes it would be useful to register an object which is not adapting anything. Database connection, XML parser, object returning unique Ids etc. are examples of these kinds of objects. These kind of components provided by the ZCA are called utility components.

Utilities are just objects that provide an interface and that are looked up by an interface and a name. This approach creates a global registry by which instances can be registered and accessed by different parts of your application, with no need to pass the instances around as parameters.

You need not to register all component instances like this. Only register components which you want to make replaceable.

5.3.1   Simple utility

A utility can be registered with a name or without a name. A utility registered with a name is called named utility, which you will see in the next section. Before implementing the utility, as usual, define its interface. Here is a greeter interface:

>>> from zope.interface import Interface
>>> from zope.interface import implements

>>> class IGreeter(Interface):
...
...     def greet(name):
...         """Say hello"""

Like an adapter a utility may have more than one implementation. Here is a possible implementation of the above interface:

>>> class Greeter(object):
...
...     implements(IGreeter)
...
...     def greet(self, name):
...         return "Hello " + name

The actual utility will be an instance of this class. To use this utility, you have to register it, later you can query it using the ZCA API. You can register an instance of this class (utility) using registerUtility:

>>> from zope.component import getGlobalSiteManager
>>> gsm = getGlobalSiteManager()

>>> greet = Greeter()
>>> gsm.registerUtility(greet, IGreeter)

In this example you registered the utility as providing the IGreeter interface. You can look the interface up with either queryUtility or getUtility:

>>> from zope.component import queryUtility
>>> from zope.component import getUtility

>>> queryUtility(IGreeter).greet('Jack')
'Hello Jack'

>>> getUtility(IGreeter).greet('Jack')
'Hello Jack'

As you can see, adapters are normally classes, but utilities are normally instances of classes. Only once you are creating the instance of a utility class, but adapter instances are dynamically created whenever you query for it.

5.3.2   Named utility

When registering a utility component, like adapter, you can use a name. As mentioned in the previous section, a utility registered with a particular name is called named utility.

This is how you can register the greeter utility with a name:

>>> greet = Greeter()
>>> gsm.registerUtility(greet, IGreeter, 'new')

In this example you registered the utility with a name as providing the IGreeter interface. You can look up the interface with either queryUtility or getUtility:

>>> from zope.component import queryUtility
>>> from zope.component import getUtility

>>> queryUtility(IGreeter, 'new').greet('Jill')
'Hello Jill'

>>> getUtility(IGreeter, 'new').greet('Jill')
'Hello Jill'

As you can see here, while querying you have to use the name as second argument.

Calling getUtility function without a name (second argument) is equivalent to calling with an empty string as the name. Because, the default value for second (keyword) argument is an empty string. Then, component lookup mechanism will try to find the component with name as empty string, and it will fail. When component lookup fails it will raise ComponentLookupError exception. Remember, it will not return some random component registered with some other name. The adapter look up functions, getAdapter and queryAdapter also works similarly.

6   Testing

6.1   Unit testing

This chapter will discuss about unit testing and integration testing. Doctest-based testing is heavily used in Zope 3. And test driven development (TDD) is prefered in Zope 3. To explain the idea, consider a use case. A module is required with a function which returns "Good morning, name!". The name will be given as an argument. Before writing the real code write the unit test for this. In fact you will be writing the real code and it's test cases almost in parallel. So create a file named example1.py with the function definition:

def goodmorning(name):
    "This returns a good morning message"

See, you have not yet written the logic. But this is necessary to run tests successfully with failures!. Ok, now create a file named example1.txt with test cases, use reStructuredText format:

These are tests for example1 module.

First import the module:

  >>> import example1

Now call the function goodmorning without any arguments:

>>> example1.goodmorning()
Traceback (most recent call last):
...
TypeError: goodmorning() takes exactly 1 argument (0 given)

Now call the function goodmorning with one argument:

>>> example1.goodmorning('Jack')
'Good morning, Jack!'

See the examples are written like executed from prompt. You can use your python prompt and copy paste from there. Now create another file test_example1.py with this content:

import unittest
import doctest

def test_suite():
    return unittest.TestSuite((
        doctest.DocFileSuite('example1.txt'),
        ))

if __name__ == '__main__':
    unittest.main(defaultTest='test_suite')

This is just boilerplate code for running the test. Now run the test using python2.4 test_example1.py command. You will get output with following text:

File "example1.txt", line 16, in example1.txt
Failed example:
    example1.goodmorning('Jack')
Expected:
    'Good morning, Jack!'
Got nothing

Now one test failed, so implement the function now:

def goodmorning(name):
    "This returns a good morning message"
    return "Good morning, %s!" % name

Now run the test again, it will run without failures.

Now start thinking about other functionalities required for the module. Before start coding write about it in text file. Decide API, write test, write code, than continue this cycle until you finish your requirements.

6.2   Running tests

By conventions your test modules are put in tests module under each package. But the doctest files can be placed in the package itself. For example if the package is ticketcollector. Then the main doctest file can be placed in ticketcollector/README.txt. And create a sub-package zopetic.tests, under this package create test modules like test_main.py, test_extra.py etc. To run the unit tests, change to instance home:

$ cd ticketcollector
$ ./bin/test

7   Browser Resources

7.1   File Resource

Certain presentation, like images and style sheets are not associated with any other component, so that one cannot create a view. To solve this problem, resources were developed, which are presentation components that do not require any context. This mini-chapter will demonstrate how resources are created and registered with Zope 3.

The first goal is to register a simple plain-text file called resource.txt as a browser resource. The first step is to create this file anywhere you wish on the filesystem, and adding the following content:

Hello, I am a Zope 3 Resource Component!

Now just register the resource in a ZCML configuration file using the browser resource directive:

<browser:resource
  name="resource.txt"
  file="resource.txt"
  layer="default"
  />

Line 2: This is the name under which the resource will be known in Zope.

Line 3: The file attribute specifies the path to the resource on the filessytem. The current working directory ('.') is always the directory the configuration file is located. So in the example above, the file resource.txt is located in the same folder as the configuration file is.

Line 4: The optional layer attribute specifies the layer the resource is added to. By default, the default layer is selected.

Once you hook up the configuration file to the main configuration path and restart Zope 3, you should be able to access the resource now via a Browser using http://localhost:8080/@@/resource.txt. The @@/ in the URL tells the traversal mechanism that the following object is a resource.

7.2   Image resource

If you have an image resource, you might want to use different configuration. Create a simple image called img.png and register it as follows:

<browser:resource
  name="img.png"
  image="img.png"
  permission="zope.ManageContent"
  />

Line 3: As you can see, instead of the file attribute we use the image one. Internally this will create an Image object, which is able to detect the content type and returns it correctly. There is a third possible attribute named template. If specified, a Page Template that is executed when the resource is called. Note that only one of file, image, or template attributes can be specified inside a resource directive.

Line 4: A final optional attribute is the ''permission'' one must have to view the resource. To demonstrate the security, I set the permission required for viewing the image to zope.ManageContent, so that you must log in as an administrator/- manager to be able to view it. The default of the attribute is zope.Public so that everyone can see the resource.

7.3   Directory resource

If you have many resource files to register, it can be very tedious to write a single directive for every resource. For this purpose the resourceDirectory is provided, with which you can simply declare an entire directory, including its content as resources. Thereby the filenames of the files are reused as the names for the resource available. Assuming you put your two previous resources in a directory called resource, then you can use the following:

<browser:resourceDirectory
  name="resources"
  directory="../resource"
  />

The image will then be publically available under the URL: http://localhost:8080/@@/resources/img.png

The DirectoryResource object uses a simple resource type recognition. It looks at the filename extensions to discover the type. For page templates, currently the extensions ''pt'', ''zpt'' and ''html'' are registered and for an image ''gif'', ''png'' and ''jpg''. All other extensions are converted to file resources. Note that it is not necessary to have a list of all image types, since only Browser-displayable images must be recognized.

7.4   ZRT resource

When working locally, you may be storing your image resources in a directory. If you have a subfolder called images with an image logo.png. And you have a template, so here is the HTML to insert the logo:

<img src="./images/logo.png" />

Now you can see that the template locally works.

If you view the HTML via Zope, you can see that it is broken.

Now, let's try to register the logo with the system like this:

<resource
  name="logo.png"
  file="images/logo.png"
  />

Now try again, after restarting Zope 3, you can see that it is still broken!. So, relative path is not correct.

Zope Resource Templates (ZRT) allows for locally working resources to work with Zope 3 as well. It will rewrite text segments in a resource. It is a 3rd party package developed by Stephan Richter for Lovely Systems. The package is available from here: http://pypi.python.org/pypi/z3c.zrtresource

To use the zrt-resource add the following lines to the page template:

<!--
  /* zrt-replace: "./images/logo.png" \
                  tal"string:${context/++resource++logo.png}" */
-->

Then convert HTML resource registration to:

<zrt-resource
  name="helloworld.html"
  file="helloworld.html"
  />

8   Browser Pages

In the last chapter we have seen how to use resources HTML. The resource HTML will be only available on site-level with the @@ prefix.

Browser page (or more generically views) are representations for particular objects/components.

If you have a template like this (helloworld.pt):

Hello, World !

Here is how to register a page for IFolder interface:

<browser:page
  name="helloworld.html"
  for="zope.app.folder.interfaces.IFolder"
  template="helloworld.pt"
  permission="zope.Public"
  />

8.1   View components

While templates display data view components are preparing data. View components convert data to output formats also prepare related data (meta-data). Then, create TAL-friendly object structures (dicts and lists). View components know about: component for which the representation is created (context) and request object holding all output media information (request)

8.1.1   Implementation

Normally view components are added inside browser package inside your main package. The organization of the browser code is really up to you and the above examples are just the most basic rules of thumb.

Here is simple view defined:

from zope.publisher.browser import BrowserPage
from zope.app.folder import interfaces

class HelloWorld(BrowserPage):

    def subFolderIds(self):
        for name, subobj in self.context.items():
            if interfaces.IFolder.providedBy(subobj):
                yield name

Since methods and attributes of the view component are directly used by the template, they should return simple iterable objects (e.g. lists, tuples, generators) or mappings (e.g. dicts).

9   Content Components

9.1   Introduction

See this example:

>>> from zope import interface

>>> class IPerson(interface.Interface):
...     name = interface.Attribute("Name")
>>> class Person(object):
...     interface.implements(IPerson)
...     name = None
>>> jack = Person()
>>> jack.name = "Jack"

Here jack is a content component. So a content component is nothing but an object which provides a particular interface. As said in the previous chapter, use zope.schema to define fields of interface. The above interface can be declared like this:

>>> from zope import interface
>>> from zope import schema

>>> class IPerson(interface.Interface):
...     name = schema.TextLine(
...         title=u"Name",
...         description=u"Name of person",
...         default=u"",
...         required=True)

If you are developing an enterprise application content will be the most important thing you have to organize first. To learn Zope 3 application development with content components, this chapter introduce a simple ticket/issue collector application.

First look at the user stories, this book will implement these stories in coming chapters.

  1. Individual small ticket collector for each project. Many collectors can be added to one running zope.
  2. Any number of tickets can be added to one collector.
  3. Each ticket will be added with a description and one initial comment.
  4. Additional comments can be added to tickets.

This chapter starts a simple implementation of ticket collector.

As stated above, our goal is to develop a fully functional, though not great-looking, web-based ticket collector application. The root object will be the Collector, which can contain Ticket objects from various users. Since you want to allow people to respond to various tickets, you have to allow tickets to contain replies, which are Comment objects.

That means you have two container-based components: The Collector contains only tickets and can be added to any Folder or container that wishes to be able to contain it. To make the ticket collector more interesting, it also has a description, which briefly introduces the subject/theme of the discussions hosted. Tickets, on the other hand should be only contained by ticket collector. They will each have a summary and a description. And last Comment should be only contained by tickets.

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

The most convenient place to put your package is $HOME/myzope/lib/python. To create that package, add a directory using:

$ cd $HOME/myzope/lib/python/
$ mkdir collector

on GNU/Linux.

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

$ echo "# Make it a Python package" >> collector/__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.

From now on you are only going to work inside this collector package, which should be located at $HOME/myzope/lib/python/collector.

9.2   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, you 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, you are only interested in defining one interface for the ticket collector itself and one for a single ticket, which are listed below (add these to the file interfaces.py):

from zope.interface import Interface
from zope.schema import Text, TextLine, Field

from zope.app.container.constraints import containers, contains
from zope.app.container.interfaces import IContained, IContainer

class IComment(Interface):
    """Comment for Ticket"""

    body = Text(
        title=u"Additional Comment",
        description=u"Body of the Comment.",
        default=u"",
        required=True)

class ITicket(IContainer):
    """A ticket object."""

    summary = TextLine(
        title=u"Summary",
        description=u"Short summary",
        default=u"",
        required=True)

    description = Text(
        title=u"Description",
        description=u"Full description",
        default=u"",
        required=False)

    contains('.IComment')

class ICollector(IContainer):
    """Collector the base object. It can only
    contains ITicket objects."""

    contains('.ITicket')

    description = Text(
        title=u"Description",
        description=u"A description of the collector.",
        default=u"",
        required=False)


class ITicketContained(IContained):
    """Interface that specifies the type of objects that can contain
    tickets.  So a ticket can only contain in a collector."""

    containers(ICollector)

class ICommentContained(IContained):
    """Interface that specifies the type of objects that can contain
    comments.  So a comment can only contain in a ticket."""

    containers(ITicket)

If you want a hierarchy of comments, the IComment and ICommentContained can be changed like this:

class IComment(Interface):
    """Comment for Ticket"""

    body = Text(
        title=u"Additional Comment",
        description=u"Body of the Comment.",
        default=u"",
        required=True)

    contains('.IComment')

class ICommentContained(IContained):
    """Interface that specifies the type of objects that can contain
    comments.  So a comment can contain in a ticket or a comment itself."""

    containers(ITicket, IComment)

See the IComment interface calls contains function with .IComment as argument. And in ICommentContained interface, IComment is also added. But for simplicity these interfaces are not used in this chapter.

9.3   Unit tests

Unit testing is explained in another chapter . Here you can see some boiler-plate code which helps to run the doctest based unittests which you will write later. Since Collector and Ticket objects are containers, this code also run common tests for containers. By convention write all unit test files under tests directory. But doctest files are placed in the package directory itself.

First create tests/test_collector.py:

import unittest
from zope.testing.doctestunit import DocTestSuite

from zope.app.container.tests.test_icontainer import TestSampleContainer

from collector.ticketcollector import Collector


class Test(TestSampleContainer):

    def makeTestObject(self):
        return Collector()

def test_suite():
    return unittest.TestSuite((
        DocTestSuite('collector.ticketcollector'),
        unittest.makeSuite(Test),
        ))

if __name__ == '__main__':
    unittest.main(defaultTest='test_suite')

Then tests/test_ticket.py:

import unittest
from zope.testing.doctestunit import DocTestSuite

from zope.app.container.tests.test_icontainer import TestSampleContainer

from collector.ticket import Ticket


class Test(TestSampleContainer):

    def makeTestObject(self):
        return Ticket()

def test_suite():
    return unittest.TestSuite((
        DocTestSuite('collector.ticket'),
        unittest.makeSuite(Test),
        ))

if __name__ == '__main__':
    unittest.main(defaultTest='test_suite')

tests/test_comment.py:

import unittest
from zope.testing.doctestunit import DocTestSuite

def test_suite():
    return unittest.TestSuite((
        DocTestSuite('collector.comment'),
        ))

if __name__ == '__main__':
    unittest.main(defaultTest='test_suite')

To run the unit test:

$ cd $HOME/myzope/etc
$ ../bin/test -vpu --dir collector

Of course now all tests should fail. In next section you will write doctests along with implemetation.

9.4   Implementation

As you can see in the unit test module, collector is going to be implemented in ticketcollector.py. A base class, BTreeContainer is used to implement the container. This will make the implementation easier.

Here is the ticketcollector.py:

from zope.interface import implements
from zope.app.container.btree import BTreeContainer

from interfaces import ICollector

class Collector(BTreeContainer):
    """A simple implementation of a collector using B-Tree Containers.

    Make sure that the ``Collector`` implements the ``ICollector``
    interface::

      >>> from zope.interface.verify import verifyClass
      >>> verifyClass(ICollector, Collector)
      True

    Here is an example of changing the description of the collector::

      >>> collector = Collector()
      >>> collector.description
      u''
      >>> collector.description = u'Ticket Collector Description'
      >>> collector.description
      u'Ticket Collector Description'
    """

    implements(ICollector)

    description = u''

Similarly ticket.py:

from zope.interface import implements
from zope.interface import classProvides
from zope.app.container.btree import BTreeContainer
from zope.app.container.contained import Contained

from interfaces import ITicket, ITicketContained

class Ticket(BTreeContainer, Contained):
    """A simple implementation of a ticket using B-Tree Containers.

    Make sure that the ``Ticket`` implements the ``ITicket`` interface::

      >>> from zope.interface.verify import verifyClass
      >>> verifyClass(ITicket, Ticket)
      True

    Here is an example of changing the summary and description of the ticket::

      >>> ticket = Ticket()
      >>> ticket.summary
      u''
      >>> ticket.description
      u''
      >>> ticket.summary = u'Ticket Summary'
      >>> ticket.description = u'Ticket Description'
      >>> ticket.summary
      u'Ticket Summary'
      >>> ticket.description
      u'Ticket Description'
    """

    implements(ITicket, ITicketContained)

    summary = u''
    description = u''

Then comment.py:

from zope.interface import implements

from interfaces import IComment
from interfaces import ICommentContained
from zope.app.container.contained import Contained

class Comment(Contained):
    """A simple implementation of a comment.

    Make sure that the ``Comment`` implements the ``IComment`` interface::

      >>> from zope.interface.verify import verifyClass
      >>> verifyClass(IComment, Comment)
      True

    Here is an example of changing the body of the comment::

      >>> comment = Comment()
      >>> comment.body
      u''
      >>> comment.body = u'Comment Body'
      >>> comment.body
      u'Comment Body'
    """

    implements(IComment, ICommentContained)

    body = u""

9.5   Registration

You have written interfaces and its implementations, now how to bind this with Zope 3 framework. You can use use Zope Configuration Markup Language (ZCML) based configuration file for this.

This is our configure.zcml:

<configure
    xmlns="http://namespaces.zope.org/zope"
    i18n_domain="collector">

  <interface
      interface=".interfaces.ICollector"
      type="zope.app.content.interfaces.IContentType"
      />

  <class class=".ticketcollector.Collector">
    <implements
        interface="zope.annotation.interfaces.IAttributeAnnotatable"
        />
    <implements
        interface="zope.app.container.interfaces.IContentContainer"
        />
    <require
        permission="zope.ManageContent"
        set_schema=".interfaces.ICollector"
        />
    <require
        permission="zope.ManageContent"
        interface=".interfaces.ICollector"
        />
  </class>

  <interface
      interface=".interfaces.ITicket"
      type="zope.app.content.interfaces.IContentType"
      />

  <class class=".ticket.Ticket">
    <implements
        interface="zope.annotation.interfaces.IAttributeAnnotatable"
        />
    <implements
        interface="zope.app.container.interfaces.IContentContainer"
        />
    <require
        permission="zope.ManageContent"
        set_schema=".interfaces.ITicket"
        />
    <require
        permission="zope.ManageContent"
        interface=".interfaces.ITicket"
        />
  </class>

  <interface
      interface=".interfaces.IComment"
      type="zope.app.content.interfaces.IContentType"
      />

  <class class=".comment.Comment">
    <implements
        interface="zope.annotation.interfaces.IAttributeAnnotatable"
        />
    <require
        permission="zope.ManageContent"
        set_schema=".interfaces.IComment"
        />
    <require
        permission="zope.ManageContent"
        interface=".interfaces.IComment"
        />
  </class>

  <include package=".browser" />

</configure>

9.6   Running application

Before running the applcation create one view for Collector.

Create a browser directory and under that, a new configure.zcml file:

<configure
    xmlns="http://namespaces.zope.org/browser">

  <addMenuItem
      class="collector.ticketcollector.Collector"
      title="Collector"
      description="A Collector"
      permission="zope.ManageContent"
      />

</configure>

The class attribute specifies the module path for the class, a leading dot means to make the import relative to the package containing the ZCML file. Therefore in this case Zope will import the collector.ticketcollector module, then import "Collector" from that module.

The title attribute provides the title to display in the add menu.

The permission attribute is used to describe what permission is required for a person to be able to add one of these objects. The zope.ManageContent permission means that the user can add, remove, and modify content (the "admin" user you created while making the instance is one such user).

You have to tell Zope to read our ZCML file, and the easiest way to do that is to put a "slug" in the $HOME/myzope/etc/package-includes/ directory. A slug is a ZCML file that just includes another file. Here's what our slug should look like (save it as "collector-configure.zcml"):

<include package="collector" />

Now if you start Zope back up, you can go to the ZMI and add our content type by clicking on "Add Collector" and entering a name for our object; name it "MyCollector".

Now restart Zope and visit http://localhost:8080 . You can add collector from menu.

10   Skinnig

Motivation

The level of customization of Zope (2 and 3) has been a strong point for years. Initially the UI was customized relying on implicit acquisition, where it was simply a matter of adding an object higher in the object path to customize the UI. Since implicit acquisition is often hard to debug, the CMF introduced the concept of skins, where a skin describes the look and feel of a site. Skins could acquire from other skins explicitly.

In Zope 3 the concept of skins was reconsidered and re-implemented to use the component architecture.

Layers

Skins

Layers versus Skins

This is realized through a combination of interface inheritance and component lookup techniques. We will discuss this in more detail later.

Skins are directly provided by a request

10.1   Core Skins

  • Access skin using ++skin++Name after the server root
  • Core Skins that are part of the repository
    • Rotterdam -- the default skin shown
    • Boston -- a newer skin featuring viewlets
    • Basic -- a skin with no layout styles
    • Debug -- based on Rotterdam, shows debug information upon failures
  • Try http://localhost:8080/++skin++Boston

Unfortunately, it is hard to reuse the UI components developed for these skins, since they still rely on the not so flexible macro pattern. Thus, it is better if you start from scratch. This will also avoid a lot of the overhead that comes with the over-generalized core skins.

10.2   A New Skin

  • Views registered for default layer by default zope.publisher.interfaces.browser.IDefaultBrowserLayer
  • Default layer contains a lot of things we do not need (security concerns)
  • Since pretty much everything in zope.app is registered into the default layer, it has an uncontrollable amount of junk in it. It is very hard to verify that all those registrations fulfill your security needs. Another problem is that views might be available that you do not want to be available.
  • Always want to develop skins from scratch
  • Some registrations in the default layer are very useful
  • Examples of those useful registrations include error views, traversal registrations, and widgets.
  • Useful set of registrations collected in the minimal layer z3c.layer.minimal
  • Add the z3c.layer.minimal package to your project dependencies

10.3   Setting up a Layer

Write an interface for the layer that inherits the minimal layer:

from z3c.layer import minimal

class IHelloWorldLayer(minimal.IMinimalBrowserLayer):
    """Hello World Application Layer"""

Change all page, viewletmanager, and viewlet directives to specify this layer:

layer=".interfaces.IHelloWorldLayer"

Once you changed those registrations, the helloworld.html page is not available anymore in the core skins. The templates by themselves do not matter.

10.3.1   Setting up a Skin

Write an interface for each new skin that inherits the Hello World application layer:

class IBasicSkin(IHelloWorldLayer):
    """Basic Skin for Hello World App."""

Register the interface as a skin interface:

<zope:interface
    interface=".interfaces.IBasicSkin"
    type="zope.publisher.interfaces.browser.IBrowserSkinType"
    name="HWBasic"
    />

Register all templates for this skin by adding the layer attribute:

layer=".interfaces.IBasicSkin"

10.3.2   Using the Skin

Access it via: http://localhost:8080/++skin++HWBasic

Hide skin traversal step by using Apache's Virtual Hosting feature

To change the default skin to something else use:

<browser:defaultSkin name="HWBasic" />

Simply specifying the browser:defaultSkin directive in your configuration file will not work, since it has been specified in zope/app/zcmlfiles/browser.zcml already. You can either change the skin at this location or use the zope:includeOverrides directive, which will override the any included directives.

10.4   Exercise

  • Develop the Hello World application layer.
  • Develop two skins based on this layer.
  • Write some tests that specifically test the difference between the skins.