Developer's Guide

  • Docs Home
  • Community Home

5. Zope 3 Views Explained

In an effort to decouple the model layer from the UI layer, we've taken to implementing Zope 3 views in Zenoss. So far, we've just done the JSON-providing methods that feed the portlets, event console, etc., but ideally we would like to move the entire application to this style.

Let's say you're adding a new screen to Zenoss. This screen shows a list of components under a Device and their event pills (the actual worth of this screen is both nonexistent and irrelevant). Here's how you'd do it, the old way and the new way.

5.1. The Zope 2 Way

  1. Add a method to the relevant class that assembles and delivers your data. In this case, you'd probably add a method to the Products.ZenModel.Device.Device class that walks components under self and generates an event pill for each. We'll call it getComponentList. If your method should logically be broken up into several methods, for organization or otherwise, you'll add those to the class as well, or find a way to use nested functions.

  2. Create a page template that calls the method and renders the data. Your template would be, say, ZenModel/skins/zenmodel/viewDeviceComponents.pt. Surrounding the content block, you'd have something like:

    <tal:block tal:define="componentdata here/getComponentList">...</tal:block>
    
  3. Link to your template. Either by adding a tab to the Device class, or by dropping a link in another template, you're going to point to a URL that describes a Device instance and your template:

    <a tal:attributes="href string:${here/absolute_url_path}/viewDeviceComponents">
          Component List</a>
    

And you're done! Now, here are the problems with this approach:

  • You've added a method used only for the UI layer to a class in the model layer, which leads to bloated classes and a terrible time reading dir().

  • Another developer will have a difficult time figuring out why the method is there, unless they grep templates for a call.

  • There's nothing identifying the template as being applicable to a particular class or group of classes.

  • If your method is applicable to another class, or if you want your template to apply to different kinds of objects, you either need to define the same method on the other classes, or create a mixin and modify your classes to inherit from it. In the first case, you've got to (remember to) update methods in two places if changes are ever desired. In the second case, you add to the already terrible Zope class inheritance tree (plus, where do you draw the line? Should we really have forty-seven mixins for a class if only the UI demands it?).

  • Calling your template on another object will get you a traceback. Not a 404, a traceback.

5.2. The Zope 3 Way

  1. Create a BrowserView class to contain logic and load the template. Instead of inflating model classes with view methods, make yourself a BrowserView, which will adapt the context to add logic you need to render the template. That is, when a view is the result of traversal, the view class will be instantiated, passing the context into the constructor (it will be available on the view instance as self.context; the request object will be self.request).

    You'll put something like this in ZenModel/browser/DeviceViews.py (browser is a convention):

    from Products.Five.browser import BrowserView
    from Products.Five.pagetemplatefile import ViewPageTemplateFile
    
    class ComponentListView(BrowserView):
    
        __call__ = ViewPageTemplateFile('viewDeviceComponents.pt')
    
        def getComponentList(self):
            ... do things with self.context and self.request ...
    

    BrowserViews are called when they're the result of a traversal, so that's your hook. ViewPageTemplateFile() is a callable, so the assignment is fine. If, instead of rendering a template, you just wanted to return some text (for example, JSON), you could do:

    from Products.Five.browser import BrowserView
    from Products.Five.pagetemplatefile import ViewPageTemplateFile
    
    class ComponentListView(BrowserView):
    
        def __call__(self):
            ... do things with self.context and self.request ...
            return results
    
  2. Create a page template that calls the method and renders the data. This is the same as the Zope 2 way, except for one key difference: view is now a global, and that's how you can access your custom method (here is still available and still refers to the context, just as before).

        <tal:block tal:define="componentdata view/getComponentList">
          ...
        </tal:block>
    

    Another difference is that you don't render the template by traversing to a template against a context; instead, you traverse to a BrowserView, which knows which template to use. This is great, especially when you want to use the same template for radically different contexts; as long as you have two BrowserViews that know how to provide the methods the template wants, you're good.

  3. Wire everything up with ZCML. This is where most people start scoffing. It's okay. It actually makes sense.

    So you have a view, but you don't have a way to call that view; there isn't a URL that will resolve to an instance of your BrowserView. To fix that, you register the view.

    When Zope starts up, it looks inside every Product for a file called configure.zcml. In Zenoss, most Products don't have one (though some do now). You can do a bunch of stuff with these, but we're going to ignore everything except registration of views.

    You would, in this case, modify Products/ZenModel/browser/configure.zcml (because Device is in ZenModel; it doesn't actually matter where you register the view, but you should try to keep Products pluggable), adding the registration of your view:

              <browser:page
                  for="Products.ZenModel.Device.Device"
                  name="componentlist"
                  class=".DeviceViews.ComponentListView"
                  permission="zope2.View"
                  />
    

    Notice that your view is defined as being applicable only to instances of the Device class. Were you to attempt to call componentlist against an IpInterface instance, for example, you'd get a 404 -- not so if componentlist were a mere template. Also notice the relative import in the class attribute; .DeviceViews will look for the DeviceViews module in the current package, that is, ZenModel.browser.

So, the whole request workflow progresses thusly:

  1. Someone asks for /zport/dmd/Devices/devices/mydevice/componentlist

  2. Zope resolves mydevice; that's the context in which it'll attempt to resolve componentlist

  3. Zope attempts to resolve componentlist as an attribute of mydevice, then a method of mydevice, then a dictionary key of mydevice, then starts looking up registered views.

  4. We find a view in the ZCML. Does it match?

    name="componentlist": Check.

    Context class="Products.ZenModel.Device.Device": Check.

    We want the view DeviceViews.ComponentListView.

  5. Zope makes sure the user has zope2.View in this context. We'll assume they do; if not, kicked out to login screen.

  6. Zope instantiates ComponentListView(mydevice), then calls it, which renders the template file.

  7. The template is rendered, using view and here, and returned as the response.

So much better! No bloated classes; no ridiculous class inheritance; great code organization. Define a method in one place, then adapt objects to provide it, instead of modifying many classes with the same method. If you want to see the screens available for a Device, just go look in the ZCML -- no need to remember which page templates are applicable to which objects. Also, you can adapt many different objects for the same template with different views.

There are a few other things that could be mentioned, but they all require a discussion of interfaces, which will deferred to a later section. Briefly, the Zope Component Architecture, and its aspect-oriented approach, saves a lot of hackery. Also it's the rules now.