Description
Rendering HTML pages in Plone using the 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 setup Python variables for a rendering template. Output is not limited to HTML pages and snippets, but may contain JSON, file download payloads, or other data formats.
Views are usually a combination of:
By keeping as much of the view logic in a separate Python class as we can and making the page template as simple as possible, better component readability and reuse is achieved. You can override the Python logic or the template file, or both.
When you are working with Plone, the most usual view type is BrowserView from the Products.Five package, but there are others.
Each BrowserView class is a Python callable. The BrowserView.__call__() method acts as an entry point to executing the view code. From Zope's point of view, even a function would be sufficient, as it is a callable.
Views were introduced in Zope 3 and made available in Plone by way of the Products.Five package, which provides some Plone/Zope 2 specific adaptation hooks to the modern Zope 3 code base. However, Zope 3's way of XML-based configuration using ZCML and separating things to three different files (Python module, ZCML configuration, TAL template) was later seen as cumbersome.
Later, a project called Grok was started to introduce an easy API to Zope 3, including a way to set up and maintain views. For more information about how to use Grok (found in the five.grok package) with Plone, please read the Plone and Grok tutorial.
Note
At the time of writing (Q1/2010), all project templates in Paster still use old-style Zope views.
Views are Zope Component Architecture (ZCA) multi-adapter registrations. If you are doing manual view look-ups, then this information is relevant to you.
Views are looked up by name. The Zope publisher always does a view lookup, instead of traversing, if the name to be traversed is prefixed with @@.
Views are resolved against different interfaces:
See also related source code.
To customize existing Plone core or add-on views you have different options.
Follow instructions how to use z3c.jbot to override templates.
Here is a short introduction on finding how existing views are defined.
First, you go to portal_types to see what views have been registered for a particular content type.
For example, if you want to override the Tabular view of a Folder, you find out that it is registered as the handler for /folder_tabular_view.
So you look for both folder_tabular_view old style page templates and @@folder_tabular_view BrowserView ZCML registrations in the Plone source tree — it can be either.
Example of how to search for this using UNIX tools (assuming that collective.recipe.omelette is in use, to keep included code together):
# find old style .pt files:
find parts/omelette -follow -name "folder_tabular_view*"
# find new style view registrations in ZCML files:
grep -ri --include="\*.zcml" folder_tabular_view parts/omelette
The folder_tabular_view is found in the skin layer called plone_content in the CMFPlone product.
More info:
This shows how to create and register view in a Zope 3 manner.
This is the simplest method and recommended for Plone 4.1+ onwards.
First, create your add-on product using Dexterity project template.
Add the file yourcompany.app/yourcompany/app/browser/views.py:
""" Viewlets related to application logic.
"""
# Zope imports
from zope.interface import Interface
from five import grok
# Search for templates in the 'templates' directory
grok.templatedir('templates')
class MyView(grok.View):
""" Render the title and description of item only (example)
"""
# The view is available on every content item type
grok.context(Interface)
...
The view in question is not registered against any layer, so it is always available. The view becomes available upon Zope start-up, and is available even if you don't run an add-on installer. This is the suggested approach for logic views which are not theme-related.
The grok.context(Interface) statement makes the view available for every content item: you can use it in URLs like http://yoursite/news/newsitem/@@yourviewname or http://yoursite/news/@@yourviewname. In the first case, the incoming self.context parameter received by the view would be the newsitem object, and in the second case, it would be the news container.
Alternatively, you could use the content interface docs to make the view available only for certain content types.
Then create yourcompany.app/yourcompany/app/browser/templates and add the related template:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:metal="http://xml.zope.org/namespaces/metal"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
i18n:domain="example.dexterityforms"
metal:use-macro="context/main_template/macros/master">
<metal:block fill-slot="main">
<h1 class="documentFirstHeading" tal:content="context/Title | string:'No title'" />
<p>This is an example view.</p>
<div id="content-core">
XXX - render content using content widgets
</div>
</metal:block>
</html>
Another example (empty.pt), which renders only the title and description fields in the Plone 3 way:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:metal="http://xml.zope.org/namespaces/metal"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
i18n:domain="example.dexterityforms"
metal:use-macro="context/main_template/macros/master">
<metal:block fill-slot="main">
<h1 class="documentFirstHeading" tal:content="context/pretty_title_or_id" />
<p class="documentDescription" tal:content="context/Description|nothing" />
</metal:block>
</html>
Available slot options you can use in the template:
Now you can access your view within the news folder:
http://localhost:8080/Plone/news/myview
... or on a site root:
http://localhost:8080/Plone/myview
... or on any other content item.
You can also use the @@ notation at the front of the view name to make sure that you are looking up a view, and not a content item that happens to have the same id as a view:
http://localhost:8080/Plone/news/@@myview
More info
Use grok.require
Example:
from five import grok
class MyView(grok.View):
# Require admin to access this view
grok.require("cmf.ManagePortal")
Use available permissions in Zope 3 style strings.
More info:
Example:
# We must use BrowserView from view, not from zope.browser
from Products.Five.browser import BrowserView
class MyView(BrowserView):
def __init__(self, context, request):
""" Initialize context and request as view multiadaption parameters.
Note that the BrowserView constructor does this for you.
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__():
#
Warning
Do not attempt to run any code in the __init__() method of a view. If this code fails and an exception is raised, the zope.component machinery remaps this to a "View not found" exception or traversal error.
Instead, use a pattern where you have a setup() or similar method which __call__() or view users can explicitly call.
Zope 3 views are registered in ZCML, an XML-based configuration language. Usually, the configuration file, where the registration done, is called yourapp.package/yourapp/package/browser/configure.zcml.
The following example registers a new view (see below for comments):
<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>
Note
You need to declare the browser namespace in your configure.zcml to use browser configuration directives.
The ZCML <browser:view template=""> directive will set the index class attribute.
The default view's __call__() method will return the value returned by a call to self.index().
Example: this ZCML configuration:
<browser:page
for="*"
name="test"
permission="zope2.Public"
class=".views.MyView"
/>
and this Python code:
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
class MyView(BrowserView):
index = ViewPageTemplateFile("my-template.pt")
is equal to this ZCML configuration:
<browser:page
for="*"
name="test"
permission="zope2.Public"
class=".views.MyView"
template="my-template.pt"
/>
and this Python code:
class MyView(BrowserView):
pass
Rendering of the view is done as follows:
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()
Below is a sample code snippet which allows you to override an already constructed ViewPageTemplateFile with a chosen file at run-time:
import plone.z3cform
from zope.app.pagetemplate import ViewPageTemplateFile as Zope3PageTemplateFile
from zope.app.pagetemplate.viewpagetemplatefile import BoundPageTemplate
# Construct template from a file which lies in a certain package
template = Zope3PageTemplateFile(
'subform.pt',
os.path.join(
os.path.dirname(plone.z3cform.__file__),
"templates"))
# Bind template to context:
# make the template callable with template() syntax and context
form_instance.template = BoundPageTemplate(template, form_instance)
You can bind several templates to one view and render them individually. This is very useful for reusable templating, or when you subclass your functional views.
Example using five.grok:
class CourseTimetables(grok.View):
# For communicating state variables from Python code to Javascript
jsHeaderTemplate = grok.PageTemplateFile("templates/course-timetables-fees-js-snippet.pt")
def renderJavascript(self):
return self.jsHeaderTemplate.render(self)
And then call in the template:
<metal:javascriptslot fill-slot="javascript_head_slot">
<script tal:replace="structure view/renderJavascript" />
</metal:javascriptslot>
The Python constructor method of the view, __init__(), is special. You should never try to put your code there. Instead, use helper method or lazy construction design pattern if you need to set-up view variables.
The __init__() method of the view might not have an acquisition chain available, meaning that it does not know the parent or hierarchy where the view is. This information is set after the constructor have been run. All Plone code which relies on acquistion chain, which means almost all Plone helper code, does not work in __init__(). Thus, the called Plone API methods return None or tend to throw exceptions.
Views can be registered against a specific layer interface. This means that views are only looked up if the specified layer is in use. Since one Zope application server can contain multiple Plone sites, layers are used to determine which Python code is in effect for a given Plone site.
A layer is in use when:
You should normally register your views against a certain layer in your own code.
For more information, see
If you need to produce non-(X)HTML output, here are some resources:
Warning
There are two different classes that share the same ViewPageTemplateFile name.
Difference in code:
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
vs.:
from zope.app.pagetemplate import ViewPageTemplateFile
The difference is that the Five version supports:
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>
<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
"""
Not all views need to return HTML output, or output at all. Views can be used as helpers in the code to provide APIs to objects. Since views can be overridden using layers, a 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, not only from Python code.
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:
Note
Using RestrictedPython scripts (creating Python through the ZMI) and Zope 2 Extension modules is discouraged. The same functionality can be achieved with helper views, with less potential pitfalls.
To use the same template code several times you can either:
Note
The Plone 2.x way of doing this with TAL template language macros is discouraged as a way to provide reusable functionality in your add-on product. This is because macros are hardwired to the TAL template language, and referring to them outside templates is difficult.
Also, if you ever need to change the template language, or mix in other template languages, you can do it much more easily when templates are a feature of a pure Python based view, and not vice versa.
Here is an example of how to have a view snippet which can be used by subclasses of a base view class. Subclasses can refer to this template at 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 the summary template in the main template associated with ProductCardView by calling the 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>
The summarybox.pt itself is just a piece of HTML code without the Plone decoration frame (main_template/master etc. macros). Make sure that you declare the i18n:domain again, or the strings in this template will not be translated.
<div class="summary-box" i18n:domain="your.package">
...
</div>
You need to get access to the view in your code if you are:
Below are two different approaches for that.
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 the acquisition wrapper (prevent false context assumptions)
context = aq_inner(context)
# May raise ComponentLookUpError
view = getMultiAdapter((context, request), name=name)
# Add the view to the acquisition chain
view = view.__of__(context)
return view
Traversal is slower than directly calling getMultiAdapter(). However, traversal is readily available in templates and RestrictedPython modules.
Example:
def getView(context, name):
""" Return a view associated with the context 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 lookups and method calls in your template by using the @@-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 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())()
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)
How to filter out views which provide a 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 implement a certain interface
views = [ view.factory for view in views if IBlocksView.implementedBy(view.factory) ]
Objects have views for default, view, edit, and so on.
The distinction between the default and view views are that for files, the default can be download.
The default view ...
If you need to get a content item's view for page rendering explicitly, you can do it as follows:
def viewURLFor(item):
cstate = getMultiAdapter((item, item.REQUEST),
name='plone_context_state')
return cstate.view_url()
More info:
Warning
This is really nasty stuff. If this were not be a public document I'd use more harsh words here.
In Plone 3, the following will lead to errors which are very hard to debug.
Views will automatically assign themselves as a parent for all member variables.
E.g. you have a Basket content item with absolute_url() of:
http://localhost:9666/isleofback/sisalto/matkasuunnitelmat/d59ca034c50995d6a77cacbe03e718de
Then if you use this object in a view code's member variable assignment:
self.basket = my_basket
... this will mess up the Basket content item's acquisition chain:
<Basket at /isleofback/sisalto/yritykset/katajamaan_taksi/d59ca034c50995d6a77cacbe03e718de>
One workaround to avoid this mess is to put a member variable inside a Python array and create an accessor method to read it when needed:
def initSomeVariables():
basket = collector.get_collector(basket_folder, self.request, create)
if basket is not None:
# Work around acquisition wrapping thing
# which forces the parent
# Assign a variable inside an array which prevents automatic
# acquisition wrapping for doing its broken magic or something
# along the lines
self.basket_holder = [basket]
else:
self.basket_holder = [None]
def getCollector(self):
""" @return: User's collector object where pages are stored
"""
return self.basket_holder[0]
The source code of this file is hosted on GitHub. Everyone can update and fix errors in this document with few clicks - no downloads needed.
For basic information about updating this manual and Sphinx format please see Writing and updating the manual guide.