#105: Using Five/Zope 3 Views

Contents
  1. Motivation
  2. Assumptions
  3. Proposal
  4. Implementation
  5. Deliverables
  6. Risks
by Sidnei da Silva last modified Jan 21, 2010 07:25 AM

This proposal aims to describe some of the goals and guidelines for bootstrapping the move of Plone to using Five/Zope 3 Views.

Proposed by
Sidnei da Silva
Seconded by
Alan Runyan
Proposal type
Architecture
Assigned to release
State
completed

Motivation

  • The Plone UI is built from several different bits and pieces. It is common to find lots of complex 'python:' TAL expressions on the different templates that together compose what is called 'The Plone UI'.
  • A recent message from Kapil Thangavelu to one of Plone's mailing lists points out that more than 100 resources are loaded from the filesystem for rendering a Plone page.
  • Customization of UI aspects is spread through different places and has several 'features' that require touching content-space.
  • Separation of logic from presentation is very vague, with Page Templates often accessing implementation-specific methods.

Introducing the concept of Views should help separating presentation from data by moving the logic out of Python Scripts and Page Templates into View Classes.

Proper use of View Classes should result in code that can be tested in isolation without having to get into the complexity of creating a full-blown Plone site and configuring skins. Views should be able to perform equally with both real and stub objects as long as they provide the same 'interface', so Views testing can be done by the use of lightweight objects that don't require any special machinery.

While View testing could in theory be done with real objects, we recommend against that. Content objects interfaces should be tested in isolation. Proper integration tests should be handled by some other means like functional testing, functional doc tests or Selenium-based tests.

Assumptions

  • Migration should be in baby steps. Evolution, not Revolution.
  • We don't plan to cover every single template used by Plone.
  • We assume people interested in this PLIP will be familiar with the concept of what Views and Adapters are (in terms of Five/Zope 3).

But... for those not familiar with Zope 3 concepts, such as Views and Adapters, we will provide some examples.

What is an Adapter?

An Adapter is a very simple concept. An Adapter is something that sits between whatever interface you require for your component and the component you are trying to use and allowing you to use the target component even if it doesn't provide the interface you need directly.

In Five/Zope 3 land, you get an Adapter when you call the interface you need providing the object(s) you want to be adapted as arguments.

Jumping from talk to code, let's have a look at an example.

Re-using the same example above, let's say you have a US-style outlet, and you want to use it with some component that requires a European-style outlet. In Five/Zope 3 terms it would be expressed like the following. Note we named the methods on the interfaces differently, even though they do the exact same thing, just like in real life:

from zope.interface import Interface, implements

class IUSOutlet(Interface):
   """The flat-looking one
   """

   def connect(plug):
       """Connect plug to outlet
       """

class IEuropeanOutlet(Interface):

   def attach(device):
       """Attach device to outlet
       """

class IUSPlug(Interface):
   """The flat-looking one
   """

   def plug_into(outlet):
       """Plug me into the outlet
       """

class IEuropeanPlug(Interface):
   """The round-looking one
   """

   def plug_into(outlet):
       """Plug me into the outlet
       """

class Outlet:
   implements(IUSOutlet)

   def __init__(self):
       self.connected = None

   def connect(self, plug):
       self.connected = plug

class Plug:
   implements(IEuropeanPlug)

   def plug_into(self, outlet):
       # We assume it's a European outlet.
       outlet.attach(self)

if __name__ == '__main__':
    plug = Plug()
    outlet = Outlet()
    plug.plug_into(outlet)
    print outlet.connected

If you run this example you will get an error like this:

Traceback (most recent call last):
  File "/home/sidnei/tmp/plug.py", line 52, in ?
    plug.plug_into(outlet)
  File "/home/sidnei/tmp/plug.py", line 47, in plug_into
    outlet.attach(self)
AttributeError: Outlet instance has no attribute 'attach'

What happened? Well, we tried to attach a IEuropeanPlug to a IUSOutlet, but IUSOutlet doesn't know anything about attach.

The solution here is to use an Adapter that knows how to perform the attach operation for a IUSOutlet:

class USOutletToEuropeanOutlet:
   implements(IEuropeanOutlet)

   def __init__(self, context):
       self.context = context

   def attach(self, plug):
       self.context.connect(plug)

This adapter only works for adapting a IUSOutlet to IEuropeanOutlet, but will do for now. Let's assume we only have those two kinds of outlets and use the adapter directly. The plug now looks like this:

class Plug:
   implements(IEuropeanPlug)

   def plug_into(self, outlet):
       # Check if it's an US outlet and adapt
       if IUSOutlet.providedBy(outlet):
          outlet = USOutletToEuropeanOutlet(outlet)
       # Now we can use it as if it's a European outlet.
       outlet.attach(self)

Now, with those changes, if we execute the code again it will work just fine. However, we have a problem. As soon as a new outlet type is created we will need to change the European plug to be aware of the new kind of outlet.

Zope 3, and Five by exposing Zope 3 features to Zope 2 solves this problem by doing an Adapter lookup in a special registry when you call an Interface with some arguments. However, we must inform Zope 3 of the existence of the new Adapter. This is done with a bit of ZCML (don't be afraid, it's just a very simple XML-like configuration language). We assume you put the code above into a module named plug.py. See the next example:

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

  <adapter for=".plug.IUSOutlet"
           provides=".plug.IEuropeanOutlet"
           factory=".plug.USOutletToEuropeanOutlet"
           />

</configure>

If you put this snippet into a file named configure.zcml alongside your plug.py module and both are into a installed Zope 2 product, Five will automagically load it for you when you start Zope 2.

Now that we informed Five/Zope 3 about our adapter, we can simplify the Plug code:

class Plug:
   implements(IEuropeanPlug)

   def plug_into(self, outlet):
       IEuropeanOutlet(outlet).attach(self)

Assuming there is an IEuropeanOutlet registered for whatever interface the outlet being used provides, this code will work just like before, but is much more generic and extensible as it moves the responsability for finding the correct adapter out of the Plug class into Zope 3 specialized adapter lookup machinery.

What is a View?

A View is not much different from an Adapter. In fact in Zope 3 a View is implemented as something called multiadapter, which happens to be an Adapter just like the one we've seen above, except it's for for more than one object. In the case of Views those objects are the a) object being accessed and b) the request.

Zope 3 is mainly based on Python's Zen bit:

Explicit is better than Implicit

In order to separate Views from content-space, a prefix of @@ was set as a convention for accessing Views. We will be walking through examples of setting up and using Five Views.

Instead of creating new content types, we will create just a interface and bind it to some existing class:

from zope.interface import Interface

class ISample(Interface): pass

Now for binding this to a existing class, say, OFS.Folder.Folder (configure.zcml):

<five:implements
    class="OFS.Folder.Folder"
    interface=".interfaces.ISample"
    />

We also need to hook traversal on this class so that Five can kick in and find the views later on (configure.zcml):

<five:traversable
    class="OFS.Folder.Folder"
    />

So far, so good. Now for some concrete View examples.

There are basically four ways of using a View:

  • Direct access by view name, bound to Page Template
  • Direct access by view name, not bound to Page Template
  • From Python, using view name
  • From Python, using provided interface

Now for example purposes, let's define an interface for a view that just returns the list of object ids in reverse order (interfaces.py):

class IReverseIds(Interface):

  def reverseIds():
      """Reverse the order of objectIds
      """

A View class (view.py) that implements this:

from interfaces import IReverseIds
from zope.interface import Implements
from Products.Five import BrowserView

class ReverseIds(BrowserView):
   implements(IReverseIds)

   def __init__(self, context, request):
      self.context = context
      self.request = request

   def reverseIds(self):
      ids = context.objectIds()
      ids.reverse()
      return ids

And a simple Page Template (reverse.pt) that will be registered for the View:

<html>
 <body>
  <ul>
   <li tal:repeat="id view/reverseIds"
       tal:content="id">
   </li>
  </ul>
 </body>
</html>

Now, we will register the same view in a few different ways. The example below registers the view with a Page Template bound to it:

<browser:page
    for=".interfaces.ISample"
    name="reverse.html"
    class=".views.ReverseIds"
    permission="zope.Public"
    allowed_interface=".interfaces.IReverseIds"
    template="reverse.pt"
    />

If you access this page through http://<zope>/some_folder/@@reverse.html it will render the template we defined above. Note on the template we used view/reverseIds. When a view has a Page Template bound to itself, the Page Template is rendered and has a view variable defined that is bound to an instance of the view class, in this case the class defined in .views.ReverseIds.

Now for a very similar example:

<browser:page
    for=".interfaces.ISample"
    name="reverse_ids"
    class=".views.ReverseIds"
    permission="zope.Public"
    allowed_interface=".interfaces.IReverseIds"
    />

In this example, we've suppressed the template argument when registering the view. That means the view will not have a Page Template bound to it.

So, how you use this view? Simple: You can access it by name from any Page Template, even from a Page Template in portal_skins, as long as the context being accessed has the view registered for one of the interfaces it provides.

With a Page Template very similar to the one above, we can get the very same output:

<html>
 <body>
  <ul>
   <li tal:repeat="id context/@@reverse_ids/reverseIds"
       tal:content="id">
   </li>
  </ul>
 </body>
</html>

Note we've used context/@@reverse_ids to get a reference to the view here and everything else stays the same. There's really not much difference from the previous example.

In the case you wanted to access this very same view from some Python code, perhaps in another view, you would do the following:

from zope.component import getView

...
   view = getView(context, 'reverse_ids', request)
   return view.reverseIds()
...

However, using a name might not be the safest thing to do, because you are not being explicit enough about what interface you expect the view to provide.

A more explicit way to do this follows:

<view
    for=".interfaces.ISample"
    factory=".view.ReverseIds"
    type="zope.publisher.interfaces.http.IHTTPRequest"
    permission="zope.Public"
    provides=".interfaces.IReverseIds"
    />

Now in this case you can do the same thing very similarly to the example above, but now being more explicit about what interface you want to access:

from interfaces import IReverseIds
from zope.component import getViewProviding

...
   view = getViewProviding(context, IReverseIds, request)
   return view.reverseIds()
...

Proposal

This proposal aims mainly to set some guidelines and identify places where using views can be useful while still keeping backwards compatibility. Later proposals will be created to expand subtopics defined in this proposal with more specific tasks.

Here we will list some of the key areas where the use of Views can provide immediate gain and flexibility while being minimally intrusive.

  • General Navigation Elements
    • Navigation Tree
    • Breadcrumbs
    • Document 'by line'
    • Left and Right Slots
    • Document Actions
    • Personal Bar
  • Form Views
    • Decouple Archetypes Widgets and Fields from Content
    • Reuse Archetypes Widgets for forms in general
    • Content Views
    • Configuration Views
    • Wizards

General Guidelines for Views

  • A View should only used documented, public APIs for accessing content data
  • If the above is not possible, use an Adapter to provide a API you can depend on.
  • A View should always return simple, efficient and lightweight data structures where possible but should not make any assumption about how this information will be consumed.
  • In general, any place where a Python Script would be used should be served equally if not better by using a View or adding extra methods to a View interface.
  • A View should not make policy decisions that affect Presentation, like deciding which CSS selector a particular element should use. If this is unavoidable, abstract away the code that does this by using an Adapter.
  • A View should contain just enough functionality to extract information from the context and nothing more. If you find yourself writing lots of branching code to deal with special cases you are doing something wrong. Move as much policy as possible to adapters or separate Views.
  • If you find yourself subclassing a View class to overwrite or extend functionality you are doing something wrong. Take some time to think about how you can split the job between cooperating Views or by using Adapters instead.
  • While writing a View stop for a second and think about the reusability of your View. Can it be reused to present data coming from a RDBMS? Can it be reused to present data coming from an external system using RDF? If you catch yourself depending on Acquisition or Persistence or some other ZODB-only feature try to refactor this piece of code to use an Adapter.

Views vs. Adapters

When should you use Views and when should you use Adapters?

Views and Adapters serve a very similar purpose. In fact, a View can be seen as something called 'MultiAdapter'. While simple Adapters adapt a single object, Views adapt two objects usually named 'context' and 'request'.

In general, a View should be used when:

  • You are doing something that is specific to the presentational aspect of an object
  • You are doing something that depends on the object's containment (eg: using acquisition to fetch a tool or property)
  • You are doing something that depends on some request variable, or that is specific to the request type coming in (WebDAV, XML-RPC, FTP, Browser)

Adapters should avoid as much as possible depending on aspects 'external' to the object like Acquisition and Persistence. Though that rule is not enforced anywere, it will provide a smoother migration to an eventual Zope 3-based Plone and also better integration with non-Plone or non-ZODB-based systems.

Implementation

  • Extra PLIPs will be written for specific functionality.
  • Every View will have an Interface. Tests will be written to make sure the View provides and keeps providing the expected interface.
  • As much as possible, Views should avoid using direct object APIs but instead use Adapters. Stub objects should be used to provide data/APIs for the Views to be tested, that will make sure the View works solely on the proper functioning of the accessed API, without depending on the 'real' object too much.
  • Avoid duplication. ZCML tends to be repetitive sometimes. Identify the cases where it's being repetitive and replace those by a custom, specialized ZCML directive.

Deliverables

At minimum we will provide a guide for how and when to use Views and Adapters.

Moving the majority of the logic to Views will also increase the testing coverage, as long as tests are written for the new Views. Currently test coverage for scripts is near zero.

Risks

Minimal, tending to zero incompatibility is expected.

Requiring Five/Z3 might be a burden for people still tied to Zope 2.7, however Zope 2.8 already comes with all the required libraries.

Comments (1)

Peter Simmons Sep 08, 2005 09:42 PM
I read the proposal sounds like a great idea in general. Towards the end the 'General Guidelines for Views' and 'Views vs. Adapters' sections are great to have. One bit I found confusing though in General Guidelines for Views you say " * While writing a View stop for a second and think about the reusability of your View. Can it be reused to present data coming from a RDBMS? Can it be reused to present data coming from an external system using RDF? If you catch yourself depending on Acquisition or Persistence or some other ZODB-only feature try to refactor this piece of code to use an Adapter." but then in Views vs. Adapters in the "In general, a Veiw should be used when:" you have the point " * You are doing something that depends on the object's containment (eg: using acquisition to fetch a tool or property)" maybe I am mis-intepretting but that seems contradictory.

The general guidelines says you should avoid ZODB-only features sue as Acquisition, Persistence but then if something relies on the containment you should use a view? Isn't containment a ZODB feature?