(Views)

Description

Rendering HTML pages in Plone using Zope 3 view pattern.

はじめに

Plone/Zope uses a view pattern to output dynamically generated HTML pages.

Views are the basic elements of modern Python web frameworks. A view runs code to set-up Python variables for a template rendering. Output may not limited to HTML pages and snippets, but may contain JSON, file download payloads and other.

Views are usually combination of

  • Python class which performs the user interface logic settup
  • Corresponding page template (in TAL language) or direct Python string output

By keeping as much of the logic code in a separate Python class and making page template as dumb as possible better component readability and reuse is achieved. You can override either Python logic or the template file separately.

When you are working with Plone, the most usual view type is BrowserView from Products.Five package, but there are others.

Each BrowserView class is a Python callable. BrowserView.__call__() method acts as an entry point of executing the view code. From Zope point of view, even a function would be enough as it is callable.

Plain Zope 3 vs. Grok

Views were introduced in Zope 3 and made available in Plone by Products.Five package (which provides some Plone/Zope 2 specific adaption hooks). However, Zope 3’s way of XML based configuration languae ZCML and separating things to three different files (Python module, ZCML configuration, TAL template) was later seen as cumbersome to maintain.

Later a project called Grok was started to introduce easy API to Zope, including how to set up and maintain views. For more information how to use Grok (five.grok package) with Plone, please read Plone and Grok tutorial.

ノート

When writing this (Q1/2010), all project templates in Paster still use old-style Zope views.

View components

Views are Zope component architecture multi-adapter registrations. If you are doing manual view look-ups then this information concerns you.

Views are looked up by name. Zope publisher does forced view look-up, instead of traversing, if the traversing name is prefixed with @@.

Views are resolved against different interfaces

  • context: Any class/interface. If not given zope.interface.Interface is used (corresponds for=”*”)
  • request: Always HTTP request. Interface zope.publisher.interfaces.browser.IBrowserRequest is used.
  • layer: Theme layer interface. If not given zope.publisher.interfaces.browser.IDefaultBrowserLayer is used.

See also related source code.

Creating and registering a view

This shows how to create and register view in Zope 3 manner.

Creating a view

Example:

# We must use BrowserView from view, not from zope.browser
# Zope version does not
from Products.Five.browser import BrowserView

class MyView(BrowserView):

    def __init__(self, context, request):
        """
        This will initialize context and request object as they are given as view multiadaption parameters.

        Note that BrowserView constructor does this for you and this step here is just to show
        how view receives its context and request parameter. You do not need to write
        __init__() for your views.
        """
        self.context = context
        self.request = request

    # by default call will call self.index() method which is mapped
    # to ViewPageTemplateFile specified in ZCML
    #def __call__():
    #

警告

Do not attempt to run any code in __init__() method of a view. If this code fails and exception is caused, zope.component machinery remaps this as “View not found” exception or traversing error. Instead, use a pattern where you have setup() or similar method which __call__() or view users explicitly call.

Registering a view

Zope 3 views are registered in ZCML, XML-based configuration language. Usually the configuration file, where the registration done is yourapp.package/yourapp/package/browser/configure.zcml.

The following example registers a new view

  • for tells which content receive this view. for=”*” means that this view can be used for any content. This equals for registering views to zope.interface.Interface base class.
  • name is view’s name which exposes it to traversing and getMultiAdapter() look-ups. If your view’s name is “test” then you can render it in browser by calling http://yourhost/site/page/@@test
  • permission is permission needed to access the view. When HTTP request comes in, the currently logged in user’s access rights in the current context are checked against this permission. See :doc:<Security chapter /security/permission_lists.txt> for available Plone out-of-the-box permissions. Usually you want have zope2.View, cmf.ModifyPortalContent, cmf.ManagePortal or zope2.Public here.
  • class is Python dotted name for BrowserView based class which is responsible for managing the view. Class __call__() method is the entry point for view processing and rendering.
  • Note that you need to declare browser namespace in your configure.zcml to use browser configuration directives
<configure
      xmlns="http://namespaces.zope.org/zope"
      xmlns:browser="http://namespaces.zope.org/browser"
      >

        <browser:page
              for="*"
              name="test"
              permission="zope2.Public"
              class=".views.MyView"
              />

</configure>

Relationship between views and templates

ZCML <browser:view template=”“> will set index class attribute.

The default view __call__() method will return the value returned by self.index() call.

Example:

<browser:page
    for="*"
    name="test"
    permission="zope2.Public"
    class=".views.MyView"
    />

from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile

class MyView(BrowserView):

    index = ViewPageTemplateFile("my-template.pt")

is equal to:

<browser:page
    for="*"
    name="test"
    permission="zope2.Public"
    class=".views.MyView"
    template="my-template.pt"
    />

class MyView(BrowserView):
    pass

Rendering of the view is done by following:

from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile

class MyView(BrowserView):

    # This may be overridden in ZCML
    index = ViewPageTemplateFile("my-template.pt")

    def render(self):
        return self.index()

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

Layers

Views can be registered against a specific layer interface. This means that views are only looked up if the specific layer is effective. Since one Zope application server can contain multiple Plone sites, layers are used to determine which Python code is effective for which Plone site.

Layer can be be effective when

  • certain theme is active
  • if a specific add-on product is installed

You should generally always register your views against a certain layer in your own code.

For more information, see

Zope ViewPageTemplateFile vs. Five ViewPageTemplateFile

Difference in code:

from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile

vs.

from zope.app.pagetemplate import ViewPageTemplateFile

Difference is that Five version supports

  • Acquisition
  • provider: TAL expression
  • Other Plone specific TAL expression functions like test()
  • Usually Plone code needs Five version of ViewPageTemplateFile
  • Some subsystems, notably z3c.form package, expect Zope 3 version of ViewPageTemplateFile instances

Overriding a view class in a product

Most of the code in this section is copied from a tutorial by Martin Aspeli (on slideshare.net). The main change is that, at least for Plone 4, the interface should subclass plone.theme.interfaces.IDefaultPloneLayer instead of zope.interface.Interface.

In this example we override the “@@register” form from the plone.app.users package, creating a custom form which subclasses the original.

  • Create an interface in interfaces.py:
from plone.theme.interfaces import IDefaultPloneLayer

class IExamplePolicy(IDefaultPloneLayer):
    """ A marker interface for the theme layer
    """
  • Then create profiles/default/browserlayer.xml:
<layers>
  <layer
    name="example.policy.layer"
    interface="example.policy.interfaces.IExamplePolicy"
  />
</layers>
  • Create browser/configure.zcml:
<configure
    xmlns="http://namespaces.zope.org/zope"
    xmlns:browser="http://namespaces.zope.org/browser"
    i18n_domain="example.policy">
  <browser:page
      name="register"
      class=".customregistration.CustomRegistrationForm"
      permission="zope2.View"
      layer="..interfaces.IExamplePolicy"
      />
</configure>
  • Create browser/customregistration.py:
from plone.app.users.browser.register import RegistrationForm

class CustomRegistrationForm(RegistrationForm):
    """ Subclass the standard registration form
    """

Helper views

Not all views need to return HTML output, or output at all. Views can be used as a helpers around in the code to provide APIs to objects. Since views can be overridden using layers, view is a natural plug-in point which an add-on product can customize or override in a conflict-free manner.

View methods are exposed to page templates and such, so you can also call view methods directly from a page template, besides Python code.

Historical perspective

Often the point of using helper views is that you can have reusable functionality which can be plugged-in as one-line code around the system. Helper views also get around the following limitations:

  • TAL security
  • Limiting Python expression to one line
  • Not being able to import Python modules

ノート

Using RestrictedPython scripts (creating Python through Zope Management Interface) and Zope 2 Extension modules is discouraged. The same functionality can be achieved with helper views, with less potential pitfalls.

Reusing view template snippets or embedding another view

To use the same template code several times you can either

  • Create a own BrowserView for it call this view (see Getting a view below)
  • Share a ViewPageTemplate instance between views and using it several times

ノート

Plone 2.x way, TAL template language macros, are discouraged to provide reusable functionality in your add-on product. This is because macros are hardwired to TAL template language and referring them outside templates is difficult. Also, if you ever need to mix or change the template language, you can do it easily when templates are a feature of a pure Python based view and not vice versa.

Here is an example how to have a view snippet which can be used subclasses of a base view class. Subclasses can refer to this template in any point of the view rendering, making it possible for subclasses to have fine tuned control over how the template snippet is represented.

Related Python code:

from Products.Five import BrowserView
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile

class ProductCardView(BrowserView):
    """
    End user visible product card presentation.
    """
    implements(IProductCardView)

    # Nested template which renders address box + buy button
    summary_template = ViewPageTemplateFile("summarybox.pt")


    def renderSummary(self):
        """ Render summary box

        @return: Resulting HTML code as Python string
        """
        return self.summary_template()

Then you can render summary template in the main template associated with ProductCardView, by calling renderSummary() method and TAL non-escaping HTML embedding.

<h1 tal:content="context/Title" />

<div tal:replace="structure view/renderSummary" />

<div class="description">
    <div tal:condition="python:context.Description().decode('utf-8') != u'None'" tal:replace="structure context/Description" />
</div>

And the summarybox.pt itself is just HTML code without Plone decoration frame (main_template/master etc. macros). Make sure that you declare i18n:domain again or the strings in this template are not translated.

<div class="summary-box" i18n:domain="your.package">
        ...
</div>

Getting a view

You need to get access to view in your code if you are

  • Calling a view from inside another view
  • Calling a view from unit test code

Below are two different approach.

By using getMultiAdapter()

This is the most efficient way in Python.

Example

from Acquisition import aq_inner
from zope.component import getMultiAdapter

def getView(context, request, name):
    # Remove acquisition wrapper which may cause false context assumptions
    context = aq_inner(context)
    # Will raise ComponentLookUpError
    view = getMultiAdapter((context, request), name=name)
    # Put view to acquisition chain
    view = view.__of__(context)
    return view

By using traversing

Traversing is slower than direct getMultiAdapter() call. However, traversing is available in templates and RestrictedPython modules easily.

Example

def getView(context, name):
    """ Return a view which is associated with context object and current HTTP request.

    @param context: Any Plone content object
    @param name: Attribute name holding the view name
    """


    try:
        view = context.unrestrictedTraverse("@@" + name)
    except AttributeError:
        raise RuntimeError("Instance %s did not have view %s" % (str(context), name))

    view = view.__of__(context)

    return view

You can also do direct view looks up and method calls in template by using @@ notation in traversing.

<div tal:attributes="lang context/@@plone_portal_state/current_language">
    We look up lang attribute by using BrowserView which name is "plone_portal_state"
</div>

Use a skin based template in a Five view

Use aq_acquire(object, template_name)

Example: Get an object by its path and render it using its default template in the current context.

from Acquisition import aq_base, aq_acquire
from Products.Five.browser import BrowserView

class TelescopeView(BrowserView):
    """
    Renders an object in a different location of the site when passed the
    path to it in the querystring.
    """
    def __call__(self):
        path = self.request["path"]
        target_obj = self.context.restrictedTraverse(path)
        # Strip the target_obj of context with aq_base.
        # Put the target in the context of self.context.
        # getDefaultLayout returns the name of the default
        # view method from the factory type information
        return aq_acquire(aq_base(target_obj).__of__(self.context),
                          target_obj.getDefaultLayout())()

Listing available views

This is useful for debugging purposes:

from plone.app.customerize import registration
from zope.publisher.interfaces.browser import IBrowserRequest

# views is generator of zope.component.registry.AdapterRegistration objects
views = registration.getViews(IBrowserRequest)

Listing all views of certain type

How to filter out views which provide certain interface.

from plone.app.customerize import registration from zope.publisher.interfaces.browser import IBrowserRequest

# views is generator of zope.component.registry.AdapterRegistration objects views = registration.getViews(IBrowserRequest)

# Filter out all classes which do not filter a certain interface views = [ view.factory for view in views if IBlocksView.implementedBy(view.factory) ]