Step 5: Adapters

In the last step we were able to move all the Plone-specific things out of the Page Template and into a supplementary view class. The view as a whole (consisting of the template and the class) still is Plone or CMF-specific, even though creating an Atom feed in itself isn't CMF specific at all. In this step, we will see how to make the view independent of the CMF by moving all the CMF-specific logic out of the view class. This is done by using another component type known from the Zope 3 Component Architecture called adapters.

Adapters

Imagine you're making a listing of all objects in a folder and want to display their size along with their name, etc. Now, in Zope 2, we just expect every object to have a get_size method that returns an integer counting the bytes, so that's very easy. You can expect every object in Zope 2 to carry that method. That is not only a bit inflexible, but it's also quite a burden on objects that want to work inside a Zope 2 environment.

Of course, displaying the size is just an example. For every bit of an API that Zope 2 and frameworks like the CMF have come up with, objects are expected to implement it, be it the WebDAV interface or the Dublin Core API. As a result, huge mix-in classes have to be used which makes the whole thing even more inflexible.

Zope 3 takes a different approach. In Zope 3, objects are typically not expected to provide anything, ever. Of course, you still want to be able to list the size of an object in a folder listing. So, what does Zope 3 do? It tries to close the gap between the "size API" (in Zope 3 that's the zope.app.size.interfaces.ISized interface) and the object that it doesn't make any assumptions about with a special component called adapter. Thus, instead that the object itself calculates its size, the adapter does it and complies to the ISized API for the object. In summary we can say that adapters mediate between abstract APIs and concrete objects.

Syndication API

The API in our example is the logic we've implemented in the supplementary view class which is about syndication and syndication policy. The concrete objects that we are dealing with are CMF content objects. Right now we just expect to be able to use the portal syndication tool, but if we want to abstract the view to be usable in other systems, we need to fold that logic out of the view and into an adapter. That way the view will become independent of the particular syndication policy implementation and just work with an abstract syndication API. The specific policy implementation such as querying the CMF syndication tool for CMF content will be left up to the adapter.

In order to write and use the adapter, we first need to formalize the syndication and syndication policy API. We know from Step 1 that API formalization in Zope 3 happens through interfaces. Let us thus create another interface called ISyndicationPolicy that expresses the information we need for the Atom feed view. Since that is just about retrieving information and doesn't actually involve any actions that would require method calls, we can just make those attributes. Therefore, we create ISyndicationPolicy as a schema in interfaces.py (download source):

from zope.interface import Interface
from zope.schema import Bool, Iterable, TextLine

class IPortalObject(Interface):
    pass

class ISyndicationPolicy(Interface):

    syndicationAllowed = Bool(
        title=u"Is syndication allowed",
        description=u"Flags whether syndication is allowed",
        required=True,
        readonly=True
        )

    siteSyndicationAllowed = Bool(
        title=u"Is site syndication allowed",
        description=u"Flags whether site syndication is allowed",
        required=True,
        readonly=True
        )

    syndicatableContent = Iterable(
        title=u"Syndicatable Content",
        description=u"An iterable containing syndicatable content objects",
        required=True,
        readonly=True
        )

    updateBase = TextLine(
        title=u"Update base",
        description=u"Date of the last update, in HTML4 format",
        required=True,
        readonly=True
        )

As promised in Step 1, schemas are defined just like interfaces. Attributes are specified in their type using schema fields from the zope.schema package. The rest should be self explanatory, especially with the background of Archetypes schemas.

An adapter for CMF objects

Now, to make the browser page independent of the CMF, we have to take out all the CMF-specific things (i.e. the calls to the portal syndication tool) and move them to a new class that will comply with the ISyndicationPolicy interface. Since this particular implementation is about CMF objects, we will call it CMFPolicy and put it into its own Python module called cmfpolicy.py (download source):

from zope.interface import implements
from Products.CMFCore.utils import getToolByName
from Products.FiveFeeds.interfaces import ISyndicationPolicy

class CMFPolicy(object):
    implements(ISyndicationPolicy)
    #adapts(IPortalContent)

    def __init__(self, context):
        self.context = context
        self.ps = getToolByName(self.context, "portal_syndication")

    def syndicationAllowed(self, site=False):
        return self.ps.isSyndicationAllowed(self.context)
    syndicationAllowed = property(syndicationAllowed)

    def siteSyndicationAllowed(self):
        return self.ps.isSiteSyndicationAllowed()
    siteSyndicationAllowed = property(siteSyndicationAllowed)

    def syndicatableContent(self):
        return self.ps.getSyndicatableContent(self.context)
    syndicatableContent = property(syndicatableContent)

    def updateBase(self):
        return self.ps.getHTML4UpdateBase(self.context)
    updateBase = property(updateBase)

You see in the class definition that the class declares to implement the ISyndicationPolicy interface. This is the API the adapter will provide. The type of object it adapts is IPortalContent which cannot be expressed explicitly in the Python code in Zope X3 3.0 and Zope 2.8. In upcoming Zope versions, this can be declared with the adapts() function.

Adapters are initialized with exactly one argument: the object they're adapting. It is common practice to call this object context and it's a convention to store it on the adapter object as an attribute of the same name, just like in views.

Now we just need to hook up the adapter, which is again done using ZCML. The adapter directive is quite simple and usually takes three arguments: the interface of the objects that are to be adapted, the interface that the adapter will provide and the adapter factory, which is usually the class itself. So, we end up with adding to our configure.zcml file the following directive:

<adapter
    for=".interfaces.IPortalObject"
    provides=".interfaces.ISyndicationPolicy"
    factory=".cmfpolicy.CMFPolicy"
    />

Adapter look-up

Now what remains is only to take the CMF-specific code out of the view class. Basically, wherever we've made calls to the portal syndication tool, we replace that with a call to the ISyndicationPolicy adapter. Adapter look-up is quite easy, you simply call the interface with the object you want to adapt as an argument. So, we end up with this adjusted implementation of SyndicationView in browser.py (download source):

from Products.Five import BrowserView
from Products.FiveFeeds.interfaces import ISyndicationPolicy

class SyndicationView(BrowserView):

    def syndicationAllowed(self, site=False):
        """Check whether syndication is allows on the context

        Based on Plone's rssAllows.py PythonScript
        """
        policy = ISyndicationPolicy(self.context)
        allowed = True
        if not site and not policy.syndicationAllowed:
            allowed = False
        if site and not policy.siteSyndicationAllowed:
            allowed = False
        # really we should be raising an HTTP error, something that
        # rss news readers would understand
        if not allowed:
            raise ValueError, "Site syndication via RSS feeds is not " \
                  "allowed. Ask the sites system administrator to go to " \
                  "portal_syndication > Policies and enable syndication. "\
                  "Each folder then needs to have syndication enabled."
        return allowed

    def syndicatableContent(self):
        policy = ISyndicationPolicy(self.context)
        return policy.syndicatableContent

    def updateBase(self):
        """Return the date of the last update in HTML4 form as a string"""
        policy = ISyndicationPolicy(self.context)
        return policy.updateBase

Now, as you can see there's not a single CMF-specific line in this code anymore, all the CMF-specific stuff is hidden behind those ISyndicationPolicy(self.context) calls. Note that we didn't have to change the template, because the template was already CMF-free [1] and we didn't change the method signature of the view class. That also means that there will be nothing new to see when you open your browser once again to view the Atom feed. The benefit lies in the abstraction we've made to the view to make it reusable in other frameworks.

Summary

Where to go from here

If you wonder how you can further explore the usage of Five now that this tutorial has ended, consider these suggestions:


[1](1, 2) That is actually a big lie because the template relies on the objects' implementing the CMF DublinCore API. Zope 3 obviously doesn't do that, it puts DublinCore in the responsibility of an adapter. For simplicity's sake we don't do this here, but it could be done quite easily by writing an adapter that does the mechanical translation of the CMF DublinCore API to the Zope 3 one.