Walking through Five to Zope 3

« Return to page index

In this tutorial we start you on your journey to the powerful Zope 3 Component Architecture (CA) by way of Five. You will learn about defining and configuring interfaces and adapters, and the CA programming model. You will also learn how to use our new TreeWalker, that helps you easily define recursive operations on object trees.

Introduction

How we came to write this tutorial. An overview of our assumptions, our goals, and the pattern we are going to implement.

sprinting

Authors

This is a joint document between Jean Francois (Jeff) Roche and Russ Ferriday. It stems from collaboration that started at the Plone Multimedia Sprint in September 2005, immediately after the Vienna Plone Conference. Jeff was porting his famous ZPhotoSlides Product to create ATPhoto and ATPhotoAlbum for Plone. Russ gave a little help but Jeff did the bulk of the work, with the help of Gawel and others, and made a great job of the ATPhoto package. Now we are working together again to move the features into ATContentTypes (ATCT) using Five, ready for the eventual move to Zope 3. We can offer a useful perspective because Jeff had some experience of the Five interfaces, and Russ was a newbie to Five at the start of this. We hope to share our learning with you.

Assumptions

  • You have some background in Plone Product development.
  • You know about object oriented programming.
  • You have a basic knowledge of UML.
  • You are interested and have some basic knowledge in the Zope 3 Component Architecture.

Our Goals

More and more people are speaking about the Zope 3 Component Architecture (CA). We are for the moment in a transition period where people have to mix the Zope 2 and the Zope 3 concepts. We understand that it's not that easy to change your way of thinking to the component architecture, as it comes with many (really beautiful, interesting and useful) new concepts. There isn't much documentation and usage of the Zope 3 CA in Zope 2, so we will try here to make our contribution in the hope of helping your progress towards Zope 3.

If you consider that Zope 3 CA is a nice way to code/think when you have read this document then we will have reached our goal.

The pattern we are going to implement

We have been working on moving over to Five ATPhoto's Zip feature, which can archive a tree of objects to a Zip file. We had completed and tested a generic Archiver. And had even merged it into the ATContentTypes tree. But while documenting what we had done, we realized that we had missed an opportunity to decompose our design into more useful and reusable components. So we took a close look and decided to split out a general purpose iteration mechanism that can be used for traversing and operating on a wide variety of objects or object trees. This has delayed our original task a bit, but we think the learning value and reuse aspect were worth it. This tutorial describes what we have made. Beyond the tutorial value, you might find this a useful tool for your own projects. It will become part of ATContentTypes in Plone 2.5

Separation of concerns, testability, and easy maintenance are important factors in design. But we also wanted people to use our work, and that means being able to understand it, too! So we came up with three easy to define classes that implement our concept.

  • An Operator that knows how to operate on nodes in a tree
  • A Filter that knows how to filter items in a tree so that we only look at items that are interesting for our purpose
  • A TreeWalker, that knows how to visit nodes in a tree, given a starting point, an Operator, and optionally a Filter.

The behaviour of the TreeWalker might never need to be changed, but the whole idea of the Operator and Filter is that they will be overridden by new classes, to achieve new functionality. So we introduce the concept of Interfaces. Interfaces are part of python. But in "plain" python they do not have the same power that they do in Java, C++, Delphi and other languages. Five and Zope 3 change all that with the interfaces package, which adds rigor to the definition and use of interfaces, and also enables the adapter concept. We needed to define interfaces for Filter and Operator, as you will see later. Though we did not need it, we also made an interface for TreeWalker to provide rigor now and more flexibility later.

In the UML diagram below, you can see the following interfaces:

  • ITreeWalker
  • IOperator
  • IFilter

They define the interface contracts for the classes that implement them.

ITreeWalker is implemented in TreeWalker. And this is the same TreeWalker you will instantiate to do your recursive task.

IOperator is provided only as an interface. You will make an implementation of it to do your task. Just for example, we have shown the ArchivingOperator created for the Archiver for which we made the TreeWalker. You can provide any Operator you like to replace ArchivingOperator.

IFilterFolder is an Interface which you can re-implement as you wish, if you want to process only certain types while traversing the tree. We provide a default implementation called FolderFilter that might be all you ever need. FolderFilter, looks at all folders and items in those folders. We'll talk more about Filtering actions later.

UML diagram of our TreeWalker class collaboration

The UML gives a very general overview of what we have done. The "Using Treewalker" section shows you how to use it, and later we get deeper to the implementation.

Test, tests, and more tests

Describes usage of doctests, and why we use them here.

Unit Testing

Test driven development has proved to be a really great practice for productive developers. And that's why writing tests for every new functionality, for every change and bug fix, is now done by most of the Zope/Plone developer community. (So much so that if you submit code and want your code released, don't imagine it will see the light of day without tests!).

Writing tests takes time, and you must take the time to write them. Once written that time will be payed back many times. Well planned tests will show you that your changes, bug fixes, and refactoring didn't create other bugs in your code, and will prevent you losing time looking for them.

Testing is magic, it transforms a developer into a user. When you are writing your tests you place yourself in the user's skin and begin to see how he might use your code. This can show you how nice your code is: where are the obscure parts, the hard to understand methods, the wrong class decompositions, etc.

You know that one of the biggest assets of Plone is its community. Tests are even more important in collective code for two main reasons:

  1. You want to share with others your functionalities, your way of coding, your way of thinking and show the right way how to use these functionalities.
  2. Many different people can/could write inside your code, if many tests are already written they can assure themselves that the code they write doesn't break yours.
  3. It covers you when the maintainer, or release manager come to scream at you that you broke something with your last changes.

So all this is about verifying your code and explaining it to others.

Tests should:

  • Be repeatable
  • Run without human intervention needed
  • Be concise
  • Tell a story
  • Not test obvious things
  • Be deterministic

PloneTestCase

When you create tests for Plone, you don't want to lose time installing a Plone portal, or other basic Zope/Plone products. So to get quicker test results, we use a unit test framework that create automated unit tests suites. Plone has its own: PloneTestCase. As Plone is based on Zope, PloneTestCase is a layer on the top of the ZopeTestCase (the zope unit test framework - which is based on Python's unittest package [and if you want to know the whole story, python's unittest package is based on Java's JUnit and the Smalltalk testing framework]). This framework is a huge help for running your test quickly, often and with clear results.

Vocabulary

By test we mean a test method.

By unittest we mean a class which contains all the test methods (if you want to use the Plone test framework, this class should inherit from PloneTestCase).

By unittest suite we mean a collection of unittest.

We will describe here a bunch of basic things available in PloneTestCase and that we use a lot in the next sections.

Products Installed:

Here is the list of default installed products. If you need additional products you will have to install them explicitly with the method we describe later.

Zope

  • ZCTextIndex
  • MailHost
  • PageTemplates
  • PythonScripts
  • ExternalMethod
  • GroupUserFolder
  • Five

CMF

  • CMFCore
  • CMFDefault
  • CMFCalendar
  • CMFTopic
  • DCWorkflow
  • CMFUid
  • CMFActionIcons
  • CMFQuickInstallerTool
  • CMFFormController

Plone - Archetypes

  • Archetypes
  • MimetypesRegistry
  • PortalTransfroms
  • ATContentTypes
  • ATReferenceBrowserWidget
  • CMFDynamicViewFTI
  • ExternalEditor
  • ExtendedPathIndex
  • ResourceRegistries
  • SecureMailHost
  • kupu

and, last but not least, CMFPlone.

In Plone 2.5 other important products are also installed: CMFPlacefulWorkflow, PlonePAS...

Objects installed:

Here are the objects you can use when you instantiate a PloneTestCase:

  • self.portal : a fresh Plone Portal install with all the portal tools you need inside.
  • self.folder : when running a PloneTestCase you are logged as a default user. This (empty) folder is the home folder of the default user. As it will be important for you to be able to do everything you need in this folder, default user is the owner of this folder.

Useful Methods you can use:

Here are the methods you can use on the self object (inside the PloneTestCase instance):

  • addProduct(name) : Uses the quickinstaller to install a products inside the Plone portal (self.portal). So if you defined your Product and your content type, don't forget to install it in the Plone Portal before trying to invoke it.
  • setRoles(roles, name=default_user) : Change the current user's roles (roles can be a string, a tuple or a list). Really important if you want to check security issues. You can also change the roles of other users by setting the name parameter.
  • setGroup(groups, name=default_user) : Change the current user's groups (groups can be a string, a tuple or a list). You can also change the groups for other users by setting the name parameter.
  • setPermissions(permissions, role) : Change the permissions on the portal object for the role. Permissions can be a string, a tuple or a list. Role must be a string.
  • login(name) : It's sometimes clearer to create new users with different roles or groups and after that login in as these users.
  • logout() : You want to be relegated to Anonymous inside the Plone instance? Use this method.

Unit Test Setup

The testing framework will run all the methods inside any class where the method name starts with test. So testMethod1(self) will be automatically run by the framework and you won't have to bother anymore about explicitly calling it somewhere.

You might often want to repeat the same initialization before calling your test method. The framework gives you an powerful method for that:

  • afterSetUp(self) : You should put in this method all the code you want to do before running each of your test methods.

Unit Test Setup

Assertion Testing Methods (python unittest based)

With test there is a known input and an expected output. This input-output correctness is checked by assertion. Python unittest package give us a range of methods to test assertions:

  • failIf(expression) : Fail the test if the expression is true.
  • failUnless(expression) : Fail the test unless the expression is true.
  • failUnlessEqual(first, second) : Fail if the two objects are unequal as determined by the == operator.
  • failIfEqual(first, second) : Fail if the two objects are equal as determined by the == operator.
  • fail(msg) : Fail immediately, with the given message.
  • failUnlessRaises(excClass, callableObj, args, *kwargs) : Fail unless an exception of class excClass is thrown by callableObj when invoked with arguments args and keyword arguments kwargs.

Failure and Errors

Failures and Errors are two different things!

Failures occur when an assertion has failed (you were expecting the opposite result from the test assertion).

Errors occur when something you didn't expect occurs (exceptions, errors in your code...).

Let's create a test! You learn better with practice. You will see it's easy.

Common practice is to create a test class for each class you want to test and one (and sometimes more) test method for each important method in your class (getter and setter are often left untested due to their obviousness).

First download the PloneTestCase (http://plone.org/products/plonetestcase or from svn https://svn.plone.org/svn/collective/PloneTestCase/trunk/) and extract it in your favourite Zope Products folder (let's assume that you installed Plone in there ;)).

Let's take the PloneTestCase class with all the things we need inside:

    >>> from Products.PloneTestCase import PloneTestCase

Let's say we want to test some of the Plone Document (ATDocument) behaviour. Let's create a class which will use this great PloneTestCase we have just imported:

    class TestATDocument(PloneTestCase):
         """
           A basic test case for Plone Document
         """
         pass

Here it is we have done our first Plone test case. Not hard ? Yes i agree this doesn't test much :). Let's test two things:

1) when I edit the title of my document, I want it to be edited correctly (I agree that we are basically testing obvious thing here, let's keep things simple).

2) when I add a document, I want to it to be inside the Plone Catalog.

As you see in these two tests we will need a basic document created, so let's do it once in the afterSetUp method so that our document will be created before each test.

Remember that each testing method must begin with test. Let's create testDocument.py:

     class TestATDocument(PloneTestCase):
          """
            A less basic test case for Plone Document
          """
          def afterSetUp(self):
             """
               Let's create in our home folder the document we need
             """
             self.folder.invokeFactory('Document', id='doc')
             # We now have a document with id "doc" inside our home folder

          def testEditTitle(self):
             """
              Let's see if a title change on the document goes well
             """
             self.folder.doc.setTitle('A wonderful document title')
             self.assertEqual(self.folder.doc.Title, 'A wonderful document title')
             # this will fail if the setTitle didn't  correctly do its job!

          def testDocumentInCatalog(self):
             """
               Let's see if the document is in the catalog
             """
             # the catalog is in the Plone portal
             self.failUnless(self.portal.portal_catalog(getId='doc'))

And there it is. If this passes we can be sure that we can change the title of a document and that once created a document is in the plone catalog.

Now comes the time to include our fresh testcase inside a testsuite and to run our tests.

To be able to run this you will need two files :

  • framework.py : To be able to run test from python you will need to setup a few PATHs, this file will do most of the job for you.
  • runalltests.py : This small python code will just run all the files in the current directory which begin the the word test.

Copy these files from the PloneTestCase folder to the folder where all your test cases are (often the "tests" folder).

So, to run the test suite, we will need to decorate our PloneTestCase. To add a bit of difficulty I want also to install a product inside my portal which isn't provided in the above list. Let's say I want to use the Plone Language Tool (I agree, we won't need it for executing our test):

       # First, above all, execute the framework.py

       import os, sys 
       if __name__ == '__main__':
          execfile(os.path.join(sys.path[0], 'framework.py'))

       # Install the PloneLanguageTool Product in Zope

       from Testing import ZopeTestCase
       ZopeTestCase.installProduct('PloneLanguageTool')

       # Initialize our Plone and default install PloneLanguageTool in it

       from Products.PloneTestCase import PloneTestCase
       PloneTestCase.setupPloneSite(products=['PloneLanguageTool'])

       # Here it is, everything installed. We can put here our testcase...

       class TestATDocument(PloneTestCase.PloneTestCase):
             """
               A less basic test case for Plone Document
             """
             def afterSetUp(self):
                """
                  Let's create in our home folder the document we need
                """
                self.folder.invokeFactory('Document', id='doc')
                # We now have a document with id "doc" inside our home folder

             def testEditTitle(self):
                """
                  Let's see if a title change on the document goes well
                """
                self.folder.doc.setTitle('A wonderful document title')
                self.assertEqual(self.folder.doc.Title(), 'A wonderful document title')
                # this will fail if the setTitle didn't correctly do its job!

             def testDocumentInCatalog(self):
                """
                  Let's see if the document is in the catalog
                """
                # the catalog is in the Plone portal
                self.failUnless(self.portal.portal_catalog(getId='doc'))

       # Now we need our testcase inside a test suite.

       def test_suite():
            from unittest import TestSuite, makeSuite
            suite = TestSuite()
            suite.addTest(makeSuite(TestATDocument))
            return suite

       # and if you want to be able to run your suite directly (python testDocument.py)

       if __name__ == '__main__':
            framework()

Everything is set up now. Last thing to do is to say where your zope is in your system. On Unix based system you can do this like so :

      export SOFTWARE_HOME=/usr/lib/zope2.9/lib/python

Now you have two ways to run your test suite, either

  • "python runalltests.py" : Which will look in every file with the name beginning with "test" and run all defined test suites.
  • "python testDocument.py" : Which will run the specified test suite.

While running you will see

1) Installation of the Zope - Products

2) Once executed, a single test (method) will be represented by:

"." : which means that your test ran correctly.

"F" : which means that your test failed (you will get more information at the end).

"E" : which means that your test has error (you will get more information at the end).

Each time a test fails or has an error you will get a traceback and more verbose information about the failure/error.

DocTest

What do you see in the word "DocTest"? Doc and Test. So a doctest is documentation and, at the same time, a test that proves that your code is working.

Many argue that people should read unit tests and they should be clear enough so that no more verbose comments should be added. It's correct that tests should be clear but I wouldn't be that strict. I think the more people I can explain my code to, the more feedback I will get .

Although we consider test cases as developer documentation, doctest is considered as a middle technique between documentation and test case. No more stale and useless documentation! Doctest enables living documentation, always in step with the current implementation.

A doctest is a text, or structured text file (which should be written inside the docs folder of your package/products). So inside this file you will explain your code and at the same time you will be able to call python code. To call python code just do:

    >>>

This represents a call to the python interpreter. Around it you can place your explanation. If your python code returns something, you have to do exactly the same as if you would call this code from a python interpreter session. For example:

    >>> print 'hello world'
    hello world

The return value must be written at the same indentation level as the >>>

One problem is that one doctest represents in itself more than one test. You want to show multiple things inside your doctest, but afterSetUp is only run once before execution of the whole doctest. One doctest represents one python session. So if i do:

    >>> a = 'hello'

My variable a will be set to hello until the end of the document. Never forget that, it could lead to some big problems!

By the way, all this document is a doctest for the ATContentTypes products. It can be executed there.

Vocabulary:

  • a doctest "file" will represent the txt file which include our doctest.
  • a doctest "class" will represent the unittest class that defines a doctest.

Once written, the doctest file should be linked to a testcase class and a testsuite. So let's see how do we setup a doctest in the test part (this should go inside a python file in the tests folder - with a file name which begins with test):

      # Like before we use the framework.py

      import os, sys
      if __name__ == '__main__':
           execfile(os.path.join(sys.path[0], 'framework.py'))

      # We install plone as usual. We want to test plone related stuff in
      # our doctest

      from Products.PloneTestCase import PloneTestCase
      PloneTestCase.setupPloneSite()

      # then we need the zope doctestsuite and link our doctest text file
      # with a functional test case

      from Testing.ZopeTestCase import FunctionalDocFileSuite
      from Products.PloneTestCase.PloneTestCase import FunctionalTestCase

      # we have a doctest file named archive.txt which is located in
      # ATContentTypes inside the docs folder (I say it again, the doctest file
      # should always be inside the docs folder, not in tests folder).

      def test_suite():
            import unittest
            suite = unittest.TestSuite()
            suite.addTest(FunctionalDocFileSuite('archive.txt',
                                                 package="Products.ATContentTTypes.docs",
                                                 test_class=FunctionalTestCase
                                                 )
                            )
      if __name__ == '__main__':
            framework()

As usual you can run this file directly, or just run python runalltests.py.

Now imagine that you want to prepare some things inside your testcase class before running your doctest file (archive.txt). It's easy, just create your test class which inherits from FunctionalTestCase, define the afterSetUp method and change the test suite to use your class. Let's do it...

Be careful with this, it could confuse the people reading your doctest file if you don't explain clearly that you have already created tests in the doctest class.

We keep it basic. We imagine that we really need to create a document inside the home folder but don't need to show that in the doctest file:

      import os, sys
      if __name__ == '__main__':
           execfile(os.path.join(sys.path[0], 'framework.py'))

      from Products.PloneTestCase import PloneTestCase
      PloneTestCase.setupPloneSite()

      # we will now subclass FunctionalTestCase and define our afterSetUp method
      from Products.PloneTestCase.PloneTestCase import FunctionalTestCase

      class TestArchiveWithDocument(FunctionalTestCase):
            """
               Our Functional test class with a document inside
            """

            def afterSetUp(self):
                """
                   Creating a document in the home directory that the archive doc test can use
                """
                self.folder.invokeFactory('Document', id='doc')

      # And that's all! No test method, the only test method will be our
      # doctest file.  

      # now we need to link our functional test class to our doctest file
      # inside a test suite:
      from Testing.ZopeTestCase import FunctionalDocFileSuite
      def test_suite():
            import unittest
            suite = unittest.TestSuite()
            suite.addTest(FunctionalDocFileSuite('archive.txt',
                                                 package="Products.ATContentTTypes.docs",
                                                 test_class=TestArchiveWithDocument
                                                 )
                            )
      if __name__ == '__main__':
            framework()

Now you know everything about tests, you have no excuse anymore for not writing them!

Using TreeWalker

A look at the TreeWalker from the point of view of a developer who wants to understand what we did, or even download and use our code.

Getting started

To use the TreeWalker in your code, you don't need to do much! That's the whole point. Yes, there are imports to deal with, as usual, but apart from that, you need only to define an Operator, and you are in business!

First let's look at the simplest thing you can do, and work from there. We'll simply walk a tree and perform no operation. For our own testing we defined a NullOperator that does exactly nothing, except satisfy the IOperator interface. We can use this class to get started.

Incidentally, the snippets I'm going to use for the examples are taken from a "doctest" file called treeWalker.txt in the docs folder of the ATContentTypes Product. That's why you'll see the >>> at the left of each line (if you aren't familiar with doctests you shouldn't worry about this syntax - just imagine the >>> is a prompt for an interactive python session that you could use in your favorite python shell or you can go back to the previous section and get the whole story). If you want to run this doctest you should use the photoimagemerge branch with Archetypes 1.4 branch and CMFDynamicFTI trunk You are also welcome to look at treeWalker.txt itself for reference. The source of this Plone.org tutorial document is available as a single Structured Text file called treeOperations.txt in the docs folder, and is also tested in our test suite.

OK, so on with the tutorial...

First let's import TreeWalker and NullOperator:

      >>> from Products.ATContentTypes.adapters.treeWalker import TreeWalker
      >>> from Products.ATContentTypes.adapters.operator import NullOperator

Then let's instantiate the NullOperator:

      >>> nulloperator = NullOperator()

Now we instantiate a TreeWalker using the nulloperator - we always need to tell the TreeWalker what we want it to do at each node it visits. In this case, nothing:

      >>> walker = TreeWalker(nulloperator)

Let it walk the tree starting at some folder:

      >>> walker.walk(folder)

That's it. The nodes are visited. The operate method on the NullOperator class is called, and we expect nothing to happen.

Doing something real

eyes on the job

Imagine you want to make a text outline or tree (like the unix tree command) showing the path of each object in a folder hierarchy. And assume you don't want to deal with the iteration details.

We can do that quite quickly. All we need to do is define an operator conforming to the IOperator interface, and pass it to a new tree walker. Here's the code from our doctest:

        >>> from zope.interface import implements
        >>> from Products.ATContentTypes.interface.operator import IOperator
        >>> class TracingOperator (object):
        ...     implements(IOperator)
        ...     def __init__(self):
        ...        self.trace = []
        ...     def getTrace(self):
        ...        return self.trace
        ...     def operate(self, context, path='', **kwargs):
        ...        self.trace.append(path)

We called it TracingOperator, because it returns a trace of the traversal of the hierarchy. We needed to add two new imports: implements which gives us the new interface rigor mentioned in the introduction, and IOperator, which is the interface we need to implement.

The constructor initializes a list that will hold the places we visit. getTrace() returns that list, and operate() is the method, defined in the IOperator interface, that is called for every node in the tree. In this case, operate() simply appends to the trace the path from the node on which the walk started.

Quoting directly from the doctest:

     Add some folders::

        >>> folder = self.folder
        >>> folder.invokeFactory('Folder', 'f1')
        'f1'
        >>> folder.f1.invokeFactory('Folder', 'subf1_1')
        'subf1_1'
        >>> folder.f1.invokeFactory('Folder', 'subf1_2')
        'subf1_2'
        >>> folder.f1.subf1_2.invokeFactory('Folder', 'subsubf1_2_1')
        'subsubf1_2_1'

Add a selection of documents:

        >>> folder.f1.invokeFactory('Document', 'd1')
        'd1'
        >>> folder.f1.d1.setText("A nice text")
        >>> folder.f1.subf1_1.invokeFactory('Document', 'd2')
        'd2'
        >>> folder.f1.subf1_1.d2.setText("A nice text")
        >>> folder.f1.subf1_1.invokeFactory('Document', 'd3')
        'd3'
        >>> folder.f1.subf1_1.d3.setText("A nice text")
        >>> folder.f1.subf1_2.subsubf1_2_1.invokeFactory('Document', 'd4')
        'd4'

      And perform the operation::

        >>> tracingoperator = TracingOperator()
        >>> tracingwalker = TreeWalker(tracingoperator)
        >>> tracingwalker.walk(folder.f1) 
        >>> trace = tracingoperator.getTrace()

We instantiate the TracingOperator, and pass it to the instantiation of TreeWalker. Then we walk, starting from the example folder we created above. By the way, folder is the root folder that we are provided by the test infrastructure.

Finally we see the list of items on the path that we visited:

        >>> print trace
          ['f1', 'f1/subf1_1', 'f1/subf1_1/d2', 'f1/subf1_1/d3', 'f1/subf1_2', 
          'f1/subf1_2/subsubf1_2_1', 'f1/subf1_2/subsubf1_2_1/d4', 'f1/d1']

This is a trace of the original folder structure. There are a couple of things to note:

  • the origin of the walk, folder.f1 in this case, is seen in the result. Meaning that operate was called on it.
  • intermediate folders were also visited. For example, f1/subf1_2 is a folder that contains another folder.
  • folders are visited before their contents. This is a type of recursion called "pre-order" meaning that folders are visited before their contents. This seems like the most common use-case.

More on Filtering

darthspanky and witt

Filtering is an important part of tree walking. It improves performance and can simplify the code in your operator. We wanted power and flexibility without complexity. Initially, filtering was simple and homogenous - the filter provided by default, or the filter optionally passed on creation, was applied across all folders in the tree. We improved this by offering a third option that we might call Heterogenous filtering, or perhaps adaptive filtering - automagically applying different filters at different nodes of a tree.

The filtering options are:

  1. If a filterClass is passed as a parameter to TreeWalker(), that class is applied to filter the start node, and all sub nodes, and it is responsible for security of access to content of the tree (see later).
  2. Otherwise, if a folder implements IFilterFolder, it will be adapted during recursion through that interface, and the filter configured for that type will be applied. A level of security is provided by the Filter superclass.
  3. Otherwise, the default filter - FolderFilter - will be applied. FolderFilter returns all items in a folder for operation, in accordance with the security comments below.

This gives us the option, by configuration of special adapters to a type, to control the number and type of items returned on filtering. A simple example would be a PhotoAlbum, where if a custom adapter were defined, only photos and sub-albums would be returned. Any other documents in a PhotoAlbum hierarchy would be ignored during traversal of the tree. This is developed in more detail in the Implementation section, below.

Using filterClass=FolderFilter as parameter to TreeWalker(), we also have the option to easily force the operation on all allowed content (see Security, below), by forcing application of the broadest filter to all permitted folderish nodes, without exception.

Security

There is a security issue that's important to address in tree walking. In Zope a method may be invoked on a node if the user has the necessary permission for that node. But that permission does not prevent us accessing or manipulating content during the walk that the user should not normally be able to access. In the case of our Archive method, we don't want the zip archive to contain items that the user would normally not be able to see.

And from the programmer's perspective, we don't want the creator of a filter to wrestle with security concerns that we can take care of on his behalf.

To deal with both of these issues, we've created a Filter hierarchy that breaks apart the security and selection aspects of filtering into two methods. The user will normally implement the selection aspect in a new subclass of Filter, leaving our wrapper method, listPermittedObjects(), provided in the superclass, to deal with security aspect. We do not go into this in great detail here, just suffice it to say that implementing a filter adapter is as easy as this:

         class PhotoAlbumFilter(Filter):
             """
             """
             implements(IFilterFolder)

             def filter(self):
                 return self.context.ObjectValues(['Image','PhotoAlbum','Folder'])

And if you want to bypass our security completely, you always have the option to provide your own filterClass that will give you all the control and all the responsibility that goes with it.

Implementation

Overview of the components we will look at in following sections

"Generalization, generalization but no over generalization" is the main point. We want to make an implementation that can be used by others trying to do something close to what we want to do, but not exactly the same. But be aware that too much generalization defeats the object. The trick is to find the balance. Let us know if you think we have done that with TreeWalker

Previously we used inheritance, mixin and multiple inheritance mechanism to generalize/specialize the behaviour of a class. This meant to organize the class into a hierarchy, generalize in a bottom-up approach and and specialize in a top-down approach.

In the Zope 3 CA, implementation will be different and will use different components:

  • The interfaces
  • The adapter
  • The event
  • The ZCML configuration
  • The browser view

This will help to separate the different functionality into different components.

Five

Homepage: http://codespeak.net/z3/five/

Five is a Zope Product which has been created to help developers move step by step from Zope 2 to the Zope 3 CA. Without it, Zope 2 and Zope 3 would be two totally separate worlds, and it would be impossible to migrate between them.

Five is included in the recent Zope 2.8 (Five version 1.2) and Zope 2.9 (Five version 1.3). Five is really close to the Zope 3 style and allow us to use Zope 3 interfaces, adapters and events inside Zope 2 without having to install Zope 3.

Five isn't widely documented, unfortunately, because Zope 2 developers aren't aware of the power that Zope 3 could provide them and therefore only few projects use it. We are trying to break through this and push things forward but we can't be the only ones doing it, that's why we hope this tutorial might help you to get into Five and Zope 3.

Interfaces

This section makes explicit the difference between Zope 2 and Zope 3 interfaces, and demonstrates the difference between interfaces implemented in the configure.zcml file and within the class. It's our first look at the ZCML configuration file, so goes into some depth.

Interfaces

Description

This section makes explicit the difference between Zope 2 and Zope 3 interfaces, and demonstrates the difference between interfaces implemented in the configure.zcml file and within the class. It's our first look at the ZCML configuration file, so we go into some depth.

Interfaces are really useful to describe what a class will do. Zope 2 and Zope 3 interfaces both document the "what" without speaking about the "how" but Zope 3 goes further than this and uses the interfaces as components for adapting a class and thus specialize its behaviour.

Zope 2

In the Zope 2 world, interfaces were just meant to document the class and give an overview of the methods and attributes. So you could have a quick look at the different methods of a class and their doc strings without bothering about the implementation.

Zope 2 interfaces inherit from these packages:

    >>> from Interface import Interface
    >>> from Interface import Attribute

So an interface is just a class with methods without code and attributes which inherit from Interface.

By convention, an interface always begin with letter I. Here is a simple interface:

      >>> class ITreeWalker (Interface):
      ...     """
      ...       Walk over a tree
      ...     """
      ...
      ...     my_attribute = Attribute("""A dummy attribute just for example purposes""")
      ...
      ...     def walk(context, path=''):
      ...         """
      ...             walk the whole tree.
      ...         """

Notice that we don't speak about self in the walk method in an interface, as we just describe methods of the class. See also how we define the Attribute in an interface.

We say that a class implements an interface as soon as the class has the same methods and attributes as its interface. A class can have more methods than its interface but not less! There is no problem for a class to implement more than one interface.

So let's create the class which implements the previous interface in the Zope 2 style:

    >>> class SimpleTreeWalker:
    ...     """
    ...       A really simple class which will walk a hierarchy
    ...     """
    ...
    ...     __implements__ = (ITreeWalker)
    ...
    ...     my_attribute = None
    ...
    ...     def walk(self, context, path=''):
    ...         """
    ...             walk the whole tree
    ...         """
    ...         pass

See the __implements__ , that's the Zope 2 way to say that this class implements its interfaces.

Note that Zope/Python won't complain if you didn't correctly implement the interfaces (necessary methods implemented, correct signatures, ...)

So creating a test for this is really useful:

    >>> ITreeWalker.isImplementedByInstancesOf(SimpleTreeWalker)
    1
    >>> from Interface.Verify import verifyClass
    >>> verifyClass(ITreeWalker, SimpleTreeWalker)
    1

We can also test on the instance of the SimpleTreeWalker class:

    >>> simpleTreeWalkerObject = SimpleTreeWalker()
    >>> ITreeWalker.isImplementedBy(simpleTreeWalkerObject)
    1
    >>> from Interface.Verify import verifyObject
    >>> verifyObject(ITreeWalker, simpleTreeWalkerObject)
    1

So we are happy we have an interface and a class which implements the interface correctly.

Zope 3

Creating a Zope 3 interface isn't different from Zope 2, just the inherited classes change.

Zope 3 interfaces inherit from a different package:

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

Now bear in mind that all the things coming from zope.XXX are Zope 3 ready.

So we still can use the exact same interface as before:

    >>> class ITreeWalker (Interface):
    ...     """
    ...       Walk over a tree
    ...     """
    ...
    ...     my_attribute = Attribute("""A dummy attribute just for example purposes""")
    ...
    ...     def walk(context, path=''):
    ...         """
    ...             walk the whole tree.
    ...         """

In the Zope 3 style we have two ways to define that a class implements an interface:

  1. The first way can be done from the class itself in the Z2 style
  2. The second way will be very different. We don't do it from the class itself as we did in Z2 but we do it from the ZCML file.

The second one might be a bit more difficult than the first one. Though it's a good way to explain the link between the what and the how outside the how itself.

From the class

sprinting

Here is the simple way to define that a class implements a zope 3 interface:

      >>> from zope.interface import implements
      >>> class SimpleTreeWalker:
      ...     """
      ...       A really simple class which will walk a hierarchy
      ...     """
      ...
      ...     implements(ITreeWalker)
      ...
      ...     my_attribute = None
      ...
      ...     def walk(self, context, path=''):
      ...         """
      ...             walk the whole tree
      ...         """
      ...         pass

And that's all, you don't have to go back to your configure.zcml anymore. Your class implements the Zope 3 interface.

From the ZCML

ZCML digression

We must speak about the Zope 3 ZCML. Zope 3 comes with a new way to configure/wire/glue main Zope 3 components, using XML. This is considered as a good thing for some and as a really painful new thing for others, anyway we are not here to complain, let's just explain.

Here is a simple example zcml configuration file:

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

xmlns defines the different namespaces. (W3C definition: "An XML namespace is a collection of names, identified by a URI reference..., which are used in XML documents as element types and attribute names"). We declare here the zope 3 namespace for basic zcml configuration and the five namespace for being able to plug our Zope 2 code with the Zope 3 CA.

This zcml resides in configure.zcml and is parsed upon zope start by Five (in zope 2).

So now that we can play with ZCML let's get back to our interface implementation. Let's create the class which implements the interface:

        >>> class SimpleTreeWalker:
        ...     """
        ...       A really simple class which will walk a hierarchy
        ...     """
        ...
        ...     my_attribute = None
        ...
        ...     def walk(self, context, path=''):
        ...         """
        ...             walk the whole tree
        ...         """
        ...         pass

See that this class is like the Z2 class except that we don't speak about __implements__ anymore! The new way to say that this class implements the ITreeWalker interface is:

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

         <five:implements
             class="Products.ATContentTypes.adapters.treeWalker.TreeWalker"
             interface="Products.ATContentTypes.interface.treeWalker.ITreeWalker"
             />

      </configure>

Now TreeWalker implements the interface ITreeWalker. Again Zope/Python will not complain if you don't correctly implement your interface so a test is really welcome:

Testing from the SimpleTreeWalker class

  • Zope 2 Interface implementation test was: ITreeWalker.isImplementedByInstanceOf(SimpleTreeWalker)
  • Zope 3 Interface implementation test is: ITreeWalker.implementedBy(SimpleTreeWalker)

Testing from the SimpleTreeWalker object (simpleTreeWalkerObject):

  • Zope 2 Interface implementation test was: ITreeWalker.isImplementedBy()
  • Zope 3 Interface implementation test is: ITreeWalker.providedBy(simpleTreeWalkerObject)

usage:

      >> ITreeWalker.implementedBy(SimpleTreeWalker)
      True
      >> from zope.interface.verify import verifyClass
      >> verifyClass(ITreeWalker, SimpleTreeWalker)
      True

Let's do the test from an instance of the SimpleTreeWalker class:

      >> simpleTreeWalkerObject = SimpleTreeWalker()
      >> ITreeWalker.providedBy(simpleTreeWalkerObject)
      True
      >> from zope.interface.verify import verifyObject
      >> verifyObject(ITreeWalker, simpleTreeWalkerObject)
      True

Again we are so happy, our class implements a Zope 3 interface!

ZCML include directive

sprinting

If you need to define many zcml directives inside your configure.zcml, a good practice is to split up the file using the <include file="myfile.zcml"/> directive.

Example:

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

             <include file="implements.zcml"/>

         </configure>

Zope 2 and Zope 3 Interfaces file location

In ATCT, Zope 2 interfaces are defined in the interfaces.py file. All Zope 3 interfaces are in the folder interface. There isn't yet a definitive common practice convention on where to put the interfaces (even in Zope 3).

Zope 3 will force you to write interfaces (which is a good practice) as interfaces won't be just for documentation purposes, they are also the starting point for getting the adapter. Let's see that closer now.

Adapters

This section covers the rationale for and use of Adapters with a real example, including details of configuration, and a demonstration of adapter lookups in action.

The main problem in Zope 2 classes is that they become really big over time (even though inheritance techniques help somewhat to reduce the number of functionalities in a class). The new Zope 3 way of thinking says: keep your base class really small and add functionality to your base class using adapters. So adding functionality means adding an adapter, an adapter adds a functionality for a base class. All the specific implementations of a functionality are to be hidden in the Zope 3 CA by the interfaces.

So let's keep in mind that there are 3 important parts here:

  • The Interfaces that define the functionality
  • The adapter which implements the functionality
  • The object that can be adapted

You can have different adapters which implement an interface. The adapter interface describe only the functionality and must be kept as general as possible. So let's take a look at the Interfaces that define a functionality.

Let's take the Filter functionality. In a folderish object you want to list only specific content (eg: in a PhotoAlbum you just want to list PhotoAlbums and Images). This will have a really simple interface:

    >>> from zope.interface import Interface
    >>> class IFilterFolder(Interface):
    ...     """
    ...         Filter the content of a folderish object
    ...     """
    ...     def listObjects():
    ...         """
    ...             return the list of filtered objects contained in the folder
    ...         """

Let's create a dummy filter adapter which implements this interface and that will just return all the objects in the folder:

    >>> from zope.interface import implements
    >>> class FolderFilter(object):
    ...     """
    ...         Dummy Filter which doesn't filter anything
    ...     """
    ...     implements(IFilterFolder)
    ...
    ...     def __init__(self, context):
    ...         """
    ...            Initialize our adapter
    ...         """
    ...         self.context = context
    ...
    ...     def listObjects(self):
    ...         """
    ...             return the list of objects contained in the folder
    ...         """
    ...         return self.context.objectValues()

As you see an adapter inherits from the object class, the most simple type you can imagine. Note that simple adapter takes the object to adapt (context) at initialization. The convention is to name it context.

Let's create another adapter for a PhotoAlbum. This adapter will filter the Image and contained PhotoAlbums:

    >>> class PhotoAlbumFilter(object):
    ...     """
    ...         Filter which returns only Image and PhotoAlbum in the folder
    ...     """
    ...     implements(IFilterFolder)
    ...
    ...     def __init__(self, context):
    ...         """
    ...            Initialize our adapter
    ...         """
    ...         self.context = context
    ...
    ...     def listObjects(self):
    ...         """
    ...             return the list of Image and PhotoAlbum in the folder
    ...         """
    ...         return [item for item in self.context.ObjectValues(['Image','Folder'])]

So we have two adapters for one Interface. We should now put some glue to make a FolderFilter adapt a Folder and a PhotoAlbumFilter to adapt a PhotoAlbum. Again this will be done thanks to ZCML. We will define an adapter implementing an Interface (and so a functionality) for a certain type of object implementing another interface (this will become clearer with the example,... we hope :)):

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

    <adapter
      for="Products.ATContentTypes.interface.IATFolder"
      provides="Products.ATContentTypes.interface.IFilterFolder"
      factory="Products.ATContentTypes.adapters.folder.FolderFilter"
      />

    <adapter
      for="Products.ATContentTypes.interface.IPhotoAlbum"
      provides="Products.ATContentTypes.interface.IFilterFolder"
      factory="Products.ATContentTypes.adapters.image.PhotoAlbumFilter"
      />

  </configure>

So now it's easy to understand, we define two adapters which will adapt two different content types.

First IATFolder

We want an adapter which will adapt each object which implements the Products.ATContentTypes.interface.IATFolder interface. So an ATFolder object implements it and it can be adapted by our adapter:

      for="Products.ATContentTypes.interface.IATFolder"

We say here that the functionality that the adapter implements is described in this Products.ATContentTypes.interface.IFilterFolder interface:

      provides="Products.ATContentTypes.interface.IFilterFolder"

We say now that the adapter which implement the IFilterFolder is in 'Products.ATContentTypes.adapters.folder.FolderFilter':

      factory="Products.ATContentTypes.adapters.folder.FolderFilter"

So, summarizing, this directive means that we will register our FolderFilter adapter in the adapter registry for any object which implements IATFolder.

Second IPhotoAlbum

We want an adapter which will adapt each object which implement the Products.ATContentTypes.interface.IPhotoAlbum interface. So basically we adapt a 'PhotoAlbum':

    for="Products.ATContentTypes.interface.IPhotoAlbum"

We say here that the functionality that the adapter implements is described in this Products.ATContentTypes.interface.IFilterFolder interface:

    provides="Products.ATContentTypes.interface.IFilterFolder"

We say now that the adapter which implement the IFilterFolder is in Products.ATContentTypes.adapters.image.PhotoAlbumFilter:

      factory="Products.ATContentTypes.adapters.image.PhotoAlbumFilter"

So summarized this directive means that this will register our PhotoAlbumFilter adapter in the adapter registry for any object which implements IPhotoAlbum.

Remember the general pattern for the adapter directive:

  <adapter
     for="A"
     provides="B"
     implements="C"
     />

  *for* any object implementing the A interface
    we *provide* a new functionality defined in the B interface
      the implementation of the functionality is defined by its *factory* in the C class.

We hope this is clearer now ?

And now let's see the magic of adapter lookup in action. As we said before, the specific implementation of a functionality is to be hidden in the Zope 3 CA by the interfaces. So if I want the filtered list of object of any folderish content without having to bother if it's a PhotoAlbum, a simple Folder, an Audio Folder... I just adapt the object to the interface - we just call the interface:

If we have a folder 'folder1':

    >>> self.folder.invokeFactory('Folder', 'folder1')
    'folder1'
    >>> folder1 = self.folder.folder1

We just adapt the folder to the IFilterFolder interface:

    >>> from Products.ATContentTypes.interface import IFilterFolder
    >>> adapter = IFilterFolder(folder1)

And we get the FolderFilter:

    >>> adapter.__class__
    <class 'Products.ATContentTypes.adapters.folder.FolderFilter'>

This adapter uses our folder1 folder as context:

    >>> adapter.context
    <ATFolder at ...>

Now if we have a PhotoAlbum folder1:

    >>> from Products.ATContentTypes.interface import IPhotoAlbum
    >>> from zope.interface import directlyProvides
    >>> directlyProvides(folder1, IPhotoAlbum)

We now have a folder which is in fact a PhotoAlbum.

We adapt the PhotoAlbum by calling the IFilterFolder interface:

    >>> adapter = IFilterFolder(folder1)

And we get the PhotoAlbumFilter

Here you may ask: "Why do we get a PhotoAlbumFilter and not a FolderFilter, since folder1 implements both IATFolder and IPhotoAlbum?". The answer is simple, the adapter lookup finds the first interface that matches an adapter.

So be aware that there is priority in Interface declaration and when we did directlyProvides(folder1, IPhotoAlbum), we gave the first priority to the IPhotoAlbum interface above all others. So the adapter lookup first takes IPhotoAlbum, looks if there is an adapter that matches this interface (looking for the "for" attribute in the zcml directive), if there is, just use it, if there is no adapter for this interface take the next provided Interface (which should be IATFolder) and do the same lookup for the adapter matching the interface:

    >>> adapter.__class__
    <class 'Products.ATContentTypes.adapters.image.PhotoAlbumFilter'>

Don't worry about the implementation of the adapter for your object, the adapter lookup will do it for you.

Now you can define other Filter adapters for your own content type just by implementing the IFilterFolder interface and by registering your new adapter as we did in the previous ZCML example. An upcoming section goes into more detail of this.

Browser View

Here we look at the benefits, use, and configuration of, browser views, and touch on Marker Interfaces.

Browser View

Description

Here we look at the benefits, use, and configuration of, browser views, and touch on Marker Interfaces.

Zope 2 views are defined in the class, and methods called by the view are also inside the same class, this becomes really ugly and the class grows quickly. Moreover as people don't want to make their class bigger they begin to program inside their ZPT and they completely mix interface and code. Others prefer to write python scripts or external methods (which is definitely better than the python: directive in ZPT). But all this proves that there is a big gap between core and presentation. This has been solved by Zope 3 views.

And what would you think if we say now that a view on an object is just an adapter? Uhm... nearly in fact. The goal is that we want to hide away all the view related stuff somewhere else. And that's what we will do. We move all those things into a Browser view class and into the zcml configuration. So that the base class doesn't have to care anymore about how consumers see its content and doesn't have to provide the consumer methods for the content.

A browser view class is a multi-adapter, we mean by this that it adapts an object and the request. Let's define a simple Browser View for our Archiver. We want to be able to call http://myhostname/plone/Afolder/zip and get a zip containing all the objects in the folder. Also we want to be able to call http://myhostname/plone/Adocument/zip and get the zipped content of the document.

As we can't really directly use Zope 3 views, we still have to use Five views:

  >>> from Products.Five import BrowserView
  >>> from Products.ATContentTypes.interface.archive import IArchiver

 Let's define our view, we just inherit from BrowserView::

  >>> class ArchiveView(BrowserView):
  ...    """
  ...        View on an object to get its content zipped
  ...    """
  ...    def getZipFile(self,**kwargs):
  ...       """
  ...       """
  ...       adapted = IArchiver(self.context)
  ...       self.request.RESPONSE.setHeader('Content-Type','application/zip')
  ...       self.request.RESPONSE.addHeader("Content-Disposition","filename=%s.zip" % self.context.getId())
  ...       self.request.RESPONSE.write(adapted.getRawArchive(**kwargs))

Notice that we have a multiadapter here, we can use self.context as the adapted object and self.request as the adapted request (understand by this that init is something like __init__(self, context, request))

Have a look at adapted = IArchiver(self.context) ; Here again we use the adapter lookup magic to get an archiver of the View's adapted object. So this allows us to archive a Document or a folder calling the same methods (defined in the IArchiver Interface) without having to bother about how this is done! It's just simple magic!

Ok, now we have our BrowserView class, we should put some glue around it and stick it to some objects... Guess what, again we will use ZCML directives for that.

So let's say we want to be able to Zip Folders only, this is done by:

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

    <browser:page
       for=".interface.IATFolder"
       name="zip"
       class=".browser.archive.ArchiveView"
       attribute="getZipFile"
       permission="zope2.View"
       />

    <traversable class=".content.folder.ATFolder"/>

  </configure>

So we define a browser view coming from Products.ATContentTypes.browser.archive.ArchiveView for any objects implementing the IATFolder interface for the user who has the permission View on the object. This view can be called as a zip method to the object. Don't forget the traversable directive on the class implementing the interface in the for option of the browser directive. We are obliged to do this as Zope 2 publisher isn't aware of Zope 3 views (that will allow us to call http://myhostname/plone/Afolder/zip).

Now let's say we want to be able to Zip Documents and folders and this in the same zcml directive... We have a problem here because the for="A" (as for the adapter) option in the browser directive just accepts one Interface... We will see a useful concept (that I should have spoken about in the interface section ...): Marker Interfaces.

Marker Interfaces

sprinting

As we don't want to define a browser for each object that implements a certain interface, we will create a general marker interface: IArchivable and mark the Document and Folder as IArchivable. This will create a kind of hierarchy. Marker means: Document and Folder are Archivable.

So first thing is to define the Marker Interface. A marker Interface is nothing more than a dummy interface:

    >>> from zope.interface import Interface
    >>> class IArchivable(Interface):
    ...     """
    ...         marker interface for possible archivable object
    ...     """

We have our marker interface, now let's put again some glue around it with our friendly ZCML directives:

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

        <implements
            class=".content.document.ATDocument"
            interface=".interface.archive.IArchivable"
        />

This way ATDocument and ATFolder are known now as IArchivable. And now we can use a single browser view directive pointing to our brand new marker interface:

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

        <browser:page
            for=".interface.IArchivable"
            name="zip"
            class=".browser.archive.ArchiveView"
            attribute="getZipFile"
            permission="zope2.View"
        />

Now both Document and Folder can "be viewed" as a zip archive. And as we didn't forget the traversable directive we can call: http://myhostname/plone/Adocument/zip and http://myhostname/plone/Afolder/zip. All this thanks to our Browser View, our Marker Interface, our adapters, our interfaces and our base classes.

Events

Describes the new way to think about events in Zope 3.

Zope 3 introduces a clean way to define event. Zope 2 didn't give a good way to play with events (implementing a specific method name in a class isn't the best way to handle events). Just let's forget Zope 2 and dive directly into Zope 3.

First let's get clearer what we mean by event. A definition first:

An event is something that takes place; an occurrence and arbitrary point in time.

In computer science, an event indicates something has happened. Zope 3 definition of event isn't far from the definition you might know if you have programmed a GUI. A user clicks on a button, an event is sent to the program, the program catches the event and does what it has to.

Zope 3 wants to keep events registration and event processing outside the base application. Which (as for adapters and views) helps a lot to define your application without having to directly define inside your base class how to play with events (as Zope 2 did with the manageAfterAdd method...).

So what kind of things could we handle in a content management system ?

  • an object has been created (zope.app.container.interfaces.IObjectAddedEvent)
  • an object has been modified (zope.app.event.interfaces.IObjectModifiedEvent)
  • an object has been removed (zope.app.container.interfaces.IObjectRemovedEvent)
  • an object has been copied (zope.app.event.interfaces.IObjectCopiedEvent)
  • an object has been moved (zope.app.container.interfaces.IObjectMovedEvent)
  • an object container has been modified (zope.app.container.interfaces.IContainerModifiedEvent)
  • an object's metadata has been modified (zope.app.event.interfaces.IObjectAnnotationsModifiedEvent)
  • ... (others but really specific events (mails, traversing, zope startup)

We have to register an event on a certain type of object, just like we registered an adapter for a certain type of object. In Zope 3, an object which registers for an event is called an "event subscriber", most of the time a subscriber will have a handler which is a callback function that will do some stuff on the object.

All these events are of course defined by their interface (as you see above) and these interfaces we will be used to register the event and create our subscriber.

Let's take a simple event, once a document is created we want to get an email which says that says so. We don't care here about telling the container folder that our document has been created.

First thing we need to do is to define the code of the object event subscriber. You see here that an event subscriber handler takes two parameter,

first is the object for which the event has been sent, second is the event (we might try to look from where the event has been sent):

   >>> from Products.CMFCore.utils import getToolByName
   >>> def sendMailUpponDocumentCreation(document, event):
   ...     mailhost = getToolByName(document, 'MailHost')
   ...     msg = "A Document %s has been created in %s " % (document.getId(), document.absolute_url())
   ...     mto = 'somebody@somewhere.org'
   ...     mfrom = 'plone@plone.org'
   ...     msubject = 'Document Created'
   ...     mailhost.send(msg, mto, mfrom, msubject=subject)

And that's all for the event subscriber. Now we need to put some glue around the Document content type (using its interface as always - this allows us to use marker interfaces there too!) and the created event will use our new function. Five 1.2 and Five 1.3 describe event in a different way, let's say we use Five 1.2:

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

          <subscriber
              for="Products.ATContentTypes.interface.IATDocument
                   zope.app.container.interfaces.IObjectAddedEvent"
              factory="Products.ATContentTypes.content.document.sendMailUpponDocumentCreation"
              />

     </configure>

(in Five 1.3 just replace the factory attribute by the handler attribute - that's all).

So as you see an event subscriber is defined by the subscriber directive (this directive is in the zope namespaces). Watch out for the two interfaces in the for attribute. The first one has to be the interface which describe the object. The second one describe the event (and is one of the interface we gave in the event list above). Factory describe the factory (often a function) that we use for the event for the object we want.

Again the directive is clear:

    for="A
         B"

    factory="C"

    which means

    for all the object that provide the interface A
      I want to do C when event B occur

Note that we can have more than one subscriber for a certain content interface. This will allow for example to email once I create my document but also send a message to the container that contains the new document.

More about Zope 2 backward compatibility

As we said earlier in Zope 2 style, we were using methods in the class like manage_afterAdd, manage_beforeDelete, manage_afterClone ... to manage the event. In this method we needed to explicitly call the event on the class(es) we inherited from to propagate the event in the class we inherit from (being careful to propagate the event to the parent class only if the parent class can handle the event...).

As we can now use Zope 3 events, manage_before manage_after are deprecated! Let's see how we can migrate them.

Let's imagine we are in ATDocument, we have a manage_afterAdd method in this old Zope 2 style:

      class ATDocument(ATCTContent, HistoryAwareMixin):

         ...

         implements(ATCTContent, IATDocument, HistoryAwareMixin)

         ...

         security.declarePrivate('manage_afterAdd')
         def manage_afterAdd(self, item, container):
             """Fix text when created througt webdav
                 Guess the right mimetype from the id/data
              """
              ATCTContent.manage_afterAdd(self, item, container)
              field = self.getField('text')
              # hook for mxTidy / isTidyHtmlWithCleanup validator
              tidyOutput = self.getTidyOutput(field)
              if tidyOutput:
                  if hasattr(self, '_v_renamed'):
                      mimetype = field.getContentType(self)
                      del self._v_renamed
                  else:
                      mimetype = self.guessMimetypeOfText()
                  if mimetype:
                      field.set(self, tidyOutput, mimetype=mimetype) # set is ok
                  elif tidyOutput:
                      field.set(self, tidyOutput) # set is ok

          ...

Note that ATDocument implements the IATDocument interface. So we will use it in the ZCML later.

The first thing to do is to create the factory function. Let's call it afterDocumentCreation. We replace self by document:

      def afterDocumentCreation(document, event):
          """Fix text when created through webdav
             Guess the right mimetype from the id/data
          """
          field = document.getField('text')
          # hook for mxTidy / isTidyHtmlWithCleanup validator
          tidyOutput = document.getTidyOutput(field)
          if tidyOutput:
              if hasattr(document, '_v_renamed'):
                  mimetype = field.getContentType(document)
                  del document._v_renamed
              else:
                  mimetype = document.guessMimetypeOfText()
              if mimetype:
                  field.set(document, tidyOutput, mimetype=mimetype) # set is ok
              elif tidyOutput:
                  field.set(document, tidyOutput) # set is ok

Note that we can't propagate the event to ATCTContent from this function as we aren't in the class anymore... This is a problem because we know that ATContent needs to get information about the event and isn't Zope 3 event aware. Two solution there, make it aware or call the event in the Zope 3 style and in the Zope 2 style.

Let's implement the second solution. So we will have to leave the call to manage_afterAdd in our class with one line, the call to ATContent.manage_afterAdd. So manage_afterAdd becomes:

      class ATDocument(ATCTContent, HistoryAwareMixin):

         ...

         implements(ATCTContent, IATDocument, HistoryAwareMixin)

         ...

         security.declarePrivate('manage_afterAdd')
         def manage_afterAdd(self, item, container):
             """Fix text when created through webdav
                 Guess the right mimetype from the id/data
              """
              ATCTContent.manage_afterAdd(self, item, container)
          ...

Now we need to configure ATDocument to be at the same time Zope 3 and Zope 2 event aware. Five gives us the possibility to do that with one directive: deprecatedManageAddDelete:

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

          <subscriber
              for="Products.ATContentTypes.interface.IATDocument
                   zope.app.container.interfaces.IObjectAddedEvent"
              factory="Products.ATContentTypes.content.document.afterDocumentCreation"
              />

          <five:deprecatedManageAddDelete
              class="Products.ATContentTypes.content.document.ATDocument"
              />
       </configure>

And that's all. The first directive is like the above example, we link the event to the objects which provide the IATDocument interface and specify the factory we have just implemented by taking manage_afterAdd code. The second directive says that we know that ATDocument uses a zope 2 event method in its class (manage_afterAdd) but we still need to call it.

Using Filtering

Describe how to define new filters for your own content.

Using Filtering

Description:

Describes how to define new filters for your own content.

As you now know all about Z3 adapters, you might want to use our interfaces to define new filters. And walk through your hierarchy.

To begin with, take a quick look at the interesting interface IFilterFolder which is in Products.ATContentTypes.interface.folder.IFilterFolder. This interface says what you have to define to create a correct filter:

    class IFilterFolder(Interface):

        def listObjects():
            """
              return the list of filtered object without looking at security
            """

        def listPermittedObjects():
            """
              return the list of filtered object and looking at security
            """

        def filter(context):
            """
              filter the objects in the context to be walked by TreeWalker.
              This method is private and shouldnt be called from outside.
              Use listObjects() instead
            """

I won't give more comment about this interface, it describes itself.

Now let's have a look at Products.ATContentTypes.adapters.folder, there you find the Filter class :

        class Filter(object):
            def __init__(self, context):
                """
                  context should be a folderish object
                """
                self.context = context

            def listObjects(self):

                """
                    return the list of filtered object without looking at security                    
                """
                return self.filter()

            def listPermittedObjects(self):
                """
                   return the list of filtered object taking care of security
                """
                return LazyFilter(self.filter(), skip='')

            def filter(self):
                """
                    filter the objects in the context to be walked by TreeWalker.
                """
                raise NotImplementedError, 'This method should be overriden in a subclass'

            def __call__(self):
                return self.listPermittedObjects()

As you see this class nearly implements IFilterFolder. By nearly I mean that there is just one method that isn't clearly implemented, the filter method. So the only thing you need to do to create a new filter is to implement this method and inherit from the Filter class. Let's do it!

Let's say that we want all documents in the folder which are in the workflow state "published":

        >>> from Products.ATContentTypes.adapters.folder import Filter
        >>> from Products.ATContentTypes.interface.folder import IFilterFolder
        >>> from Products.CMFCore.utils import getToolByName
        >>> class FilterPublishedDocument(Filter):
        ...     """
        ...         Just takes the published documents in folder
        ...     """
        ...     implements(IFilterFolder)
        ...
        ...     def filter(self):
        ...         workflow = getToolByName(self.context, 'portal_workflow')
        ...         publishedDocuments = []
        ...         for document in self.context.objectValues('Document'):
        ...             if(workflow.getInfoFor(document,'review_state') == 'published'):
        ...                 publishedDocument.append(document)
        ...
        ...         return publishedDocuments

And that's all for filtering. If you want to use it inside the treeWalker you should link this filter to the folderish object you want to walk on (should be ATFolder), or just give the FilterPublishedDocument class as parameter to TreeWalker initialization.

To use FilterPublishedDocument for ATFolder the zcml would look like this like this:

        <adapter
            for="Products.ATContentTypes.interface.IATFolder"
            factory="Products.ATContentTypes.adapters.folder.FilterPublishedDocument"
            provides="Products.ATContentTypes.interface.IFilterFolder"
            />

Note that we could have used the catalog in the filter to filter the objects in a defined state (this might have been a bit quicker - but less simple).

Final

Our thanks to reviewers and contributors, and references.

Reviewers

  • Raphael Ritz - thanks for your detailed comments and for spotting the security issue with Filter, which prompted our subclassing from Filter and the addition of a base class wrapper method to apply LazyFilter().
  • Martin Aspelli - thanks for your quick review and encouragement
  • Joel Burton - We expected nothing less than a thoughtful and incisive review, Joel. We were not disappointed. Thank you!

Advice

  • Alec Mitchell - thanks for helping with the merge of our branches into ATCT trunk, for your encouragement, and for taking on the awesome task of Plone release manager.

Other

  • The great Plone Community who make all this possible. - Thanks!

References

Please consult the following for more information:

    Web Component Development with Zope 3
    by Philipp von Weitershausen
    http://worldcookery.com/

    Zope 3 Developer's Handbook
    by Stephan Richter
    http://www.amazon.com/gp/product/0672326175/103-9651091-6407816

    Literate Testing: Automated Testing with doctest 
    Jim Fulton, jim@zope.com 
    Tim Peters, tim@zope.com 
    PyCon 2004
    http://www.python.org/pycon/dc2004/papers/4/