Views are the basic elements of modern Python web frameworks. A view runs code to set up Python variables for a rendering template. The 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:
a Python class, which performs the user interface logic setup, and
corresponding Templates, or direct Python string output.
Templates should be kept simple. Logic should be kept in a separate Python file. This enhances readability and makes components more reusable. You can override the Python logic, the template file, or both.
When you work with Plone, the most common view type is
BrowserView from the package
Other view types include
BrowserView class is a Python callable.
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.
Creating and registering a view#
This section shows how to create and register a view in Plone.
Creating a view#
Create your add-on package using plonecli:
plonecli create addon collective.awesomeaddon
Then change the working directory into the created package, and add a view:
cd collective.awesomeaddon plonecli add view
Python logic code#
Depending on how you answered the questions when invoking
plonecli, it generated a Python file at the location
src/collective/awesomeaddon/views/my_view.py with the following content.
# from p6.docs import _ from Products.Five.browser import BrowserView from zope.interface import Interface # from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile class IMyView(Interface): """Marker Interface for IMyView""" class MyView(BrowserView): # If you want to define a template here, please remove the template attribute from # the configure.zcml registration of this view. # template = ViewPageTemplateFile('my_view.pt') def __call__(self): # your code here # render the template return self.index()
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.
Additionally, a view class may be instantiated in other places than where you intended to render the view.
plone.app.contentmenu does this when creating the menu to select a view layout.
This will result in the
__init__() being called on unexpected contexts, probably wasting a lot of time.
Instead, use a pattern where you have a
setup() or similar method which
__call__() or view users can explicitly call.
Registering a view#
Zope 3 views are registered in ZCML, an XML-based configuration language.
plonecli did the following registration in
src/collective/awesomeaddon/views/configure.zcml for you.
The following example registers a new view. See below for comments about what the code does.
<configure xmlns="http://namespaces.zope.org/zope" xmlns:browser="http://namespaces.zope.org/browser" > <browser:page for="*" name="myview" permission="zope2.Public" class=".views.MyView" /> <browser:page name="my-view" for="*" class=".my_view.MyView" template="my_view.pt" permission="zope2.View" layer="collective.awesomeaddon.interfaces.ICollectiveAwesomeaddonLayer" /> </configure>
Specifies which content types receive this view.
for="*"means that this view can be used for any content type. This is the same as registering views to the
The name by which the view is exposed to traversal and
getMultiAdapter()look-ups. If your view's name is
myview, then you can render it in the browser by calling
This is the permission needed to access the view. When an HTTP request comes in, the currently authenticated user's access rights in the current context are checked against this permission. See Permissions for Plone's out-of-the-box permissions. Usually you want to use
This is a Python dotted name for a class based on
BrowserView, which is responsible for managing the view. The Class's
__call__()method is the entry point for view processing and rendering.
You need to declare the
browser namespace in your
configure.zcml to use
browser configuration directives.
Depending on how you answered the questions when you invoked
plonecli, it created a template at
src/collective/awesomeaddon/views/my_view.pt with the following content.
<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="p6.docs" metal:use-macro="context/main_template/macros/master"> <body> <metal:content-core fill-slot="content-core"> <metal:block define-macro="content-core"> <h2 i18n:translate="">Sample View</h2> <!--<div tal:replace="view/my_custom_view_method" />--> <!--<div tal:replace="context/my_custom_field" />--> </metal:block> </metal:content-core> </body> </html>
When you restart Plone, and activate your add-on, the view should be available through your browser.
Accessing your newly created view#
Now you can access your view within the news folder by visiting the following URL.
Or on a site root:
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:
In the generated template above, we have a
fill-slot attribute, which will fill the slot with the name
content-core, which is defined in Plone's
The following list shows the available options for
<metal fill-slot=""> in your template.
Please note that our template above inherits from
Used to set parameters on the request, for example to deactivate the left and right columns or caching.
<metal:block fill-slot="top_slot" tal:define="dummy python:request.set('disable_border',1); disable_column_one python:request.set('disable_plone.leftcolumn',1); disable_column_two python:request.set('disable_plone.rightcolumn',1);" />
Used to define HTML
headelements, such as
linktags for RSS and CSS.
Used to define
styletags to load CSS files.
Used to define
Global status message#
Used to fill in global status messages.
The content area, including the title, description, content-core and viewlets around them.
A slot inside the content macro.
mainslot in the main template. You must render
Content description for your view.
Content body specific to your view for Plone version 4.x or greater.
Asides and portlets#
Relationship between views and templates#
<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
For example, the following ZCML configuration:
<browser:page for="*" name="myview" permission="zope2.Public" class=".views.MyView" />
together with the following Python code:
from Products.Five.browser import BrowserView from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile class MyView(BrowserView): index = ViewPageTemplateFile("my-template.pt")
is equal to the following ZCML configuration:
<browser:page for="*" name="myview" permission="zope2.Public" class=".views.MyView" template="my-template.pt" />
together with 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()
Several templates per view#
You can bind several templates to one view and render them individually. This is useful for reusable templating, or when you subclass your functional views.
from Products.Five.browser import BrowserView from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile class CourseTimetables(BrowserView): template1 = ViewPageTemplateFile('template1.pt') template2 = ViewPageTemplateFile('template2.pt') def render_template1(self): return self.template1.render(self) def render_template2(self): return self.template2.render(self)
And then in the template call:
<div tal:replace="structure view/render_template1"> <div tal:replace="structure view/render_template2">
__init__() method special cases#
The Python constructor method of the view,
__init__(), is special.
You should almost never try to put your code there. Instead, use the
_call__() method or further helper methods called from it.
__init__() method of the view might not have an acquisition chain available, meaning that it does not know where is its parent or hierarchy.
This also means that you don't have user information and permissions for the view.
This information is set after the constructor has been run.
All Plone code relies on the acquisition chain, which means almost all Plone helper code does not work in
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 either:
a theme which defines that layer is active, or
if a specific add-on product which defines that layer is installed.
You should register your views against a certain layer in your own code.
For more information, read the Layers chapter.
Register and unregister a view directly using
The following is an example of how to register a view directly using the
import zope.component import zope.publisher.interfaces.browser zope.component.provideAdapter( # Our class factory=TestingRedirectHandler, # (context, request) layers for multiadapter lookup # We provide None as layers are not used adapts=(None, None), # All views are registered as IBrowserView interface provides=zope.publisher.interfaces.browser.IBrowserView, # View name name='redirect_handler')
The following is an example of how to unregister the same view:
# Dynamically unregister a view gsm = zope.component.getGlobalSiteManager() gsm.unregisterAdapter(factory=TestingRedirectHandler, required=(None, None), provided=zope.publisher.interfaces.browser.IBrowserView, name="redirect_handler")
To customize existing Plone core or add-on views, you have different options.
Usually you can override the related page template file (
Sometimes you also need to change the related Python view class code. In this case, you override the Python class by using your own add-on which installs a view class replacement using an add-on specific browser layer.
Overriding view template#
The recommended approach to customize
.pt files for Plone is to use a little helper called
If you need to override templates in core Plone or in an existing add-on, you can do the following:
Create your own add-on with plonecli, which you can use to contain your page templates on the file system.
The created package already contains an overrides folder for
browser/overridesHere you can place your template overrides.
z3c.jbotcan override page templates (
.ptfiles) for views, viewlets, old style page templates, and portlets. In fact, it can override any
.ptfile in the Plone source tree, except the
Overriding a template using
First of all, make sure that your customization add-on supports
z3c.jbot. Add-on packages created by
overridesfolder in the
browserfolder where you can drop in your new
Locate the template you need to override in the Plone source tree. You can do this by searching in all the installed packages for
.ptfiles. If you use buildout with the
collective.recipe.omeletterecipe, a good folder to search in is
The following is an example UNIX
findcommand to find
.ptfiles. You can also use Windows Explorer file search or similar tools:
find -L ./parts/omelette -name "*.pt"
./parts/omelette/plone/app/contenttypes/browser/templates/listing_album.pt ./parts/omelette/plone/app/contenttypes/browser/templates/listing.pt ./parts/omelette/plone/app/contenttypes/browser/templates/newsitem.pt ./parts/omelette/plone/app/contenttypes/browser/templates/full_view.pt ./parts/omelette/plone/app/contenttypes/browser/templates/full_view_item.pt ./parts/omelette/plone/app/contenttypes/browser/templates/document.pt ./parts/omelette/plone/app/contenttypes/browser/templates/image_view_fullscreen.pt ./parts/omelette/plone/app/contenttypes/browser/templates/link.pt ./parts/omelette/plone/app/contenttypes/browser/templates/file.pt ./parts/omelette/plone/app/contenttypes/browser/templates/listing_summary.pt ./parts/omelette/plone/app/contenttypes/browser/templates/listing_tabular.pt ./parts/omelette/plone/app/contenttypes/browser/templates/image.pt ./parts/omelette/plone/app/contenttypes/behaviors/richtext_gettext.pt ./parts/omelette/plone/app/contenttypes/behaviors/leadimage.pt ./parts/omelette/plone/app/contentrules/browser/templates/manage-elements.pt ./parts/omelette/plone/app/contentrules/browser/templates/controlpanel.pt ./parts/omelette/plone/app/contentrules/browser/templates/contentrules-pageform.pt ./parts/omelette/plone/app/contentrules/browser/templates/manage-assignments.pt ./parts/omelette/plone/app/contentrules/actions/templates/mail.pt ./parts/omelette/plone/app/viewletmanager/manage-viewlets.pt ./parts/omelette/plone/app/viewletmanager/manage-viewletmanager.pt
Make a copy of the
.ptfile you want to override. To override a particular file, first determine its canonical filename. It is defined as the path relative to the package within which the file is located. Directory separators are replaced with dots.
Suppose you want to override
You would use the filename
Place the file in the registered
overridesfolder in your add-on.
Make your changes in the new
After overriding the template for the first time (adding the file to the
overridesfolder), you need to restart Plone.
z3c.jbotscans new overrides only during the restart.
After the file is in place, changes to the file are instantly picked up. The template code is re-read on every HTTP request.
If you want to override an already overridden template, read How can I override an already overridden template by jbot?.
Overriding a view class#
In this example, we will override the
@@register form from the
plone.app.users package, creating a custom form which subclasses the original.
We assume you already have created a Plone add-on package with plonecli.
As such, you will have the following browser layer interface and its registration in
Create an interface in
from plone.theme.interfaces import IDefaultPloneLayer class ICollectiveAwesomeaddon(IDefaultPloneLayer): """ A marker interface for the theme layer """
<layers> <layer name="collective.awesomeaddon" interface="collective.awesomeaddon.interfaces.ICollectiveAwesomeaddon" /> </layers>
<configure xmlns="http://namespaces.zope.org/zope" xmlns:browser="http://namespaces.zope.org/browser" i18n_domain="collective.awesomeaddon"> <browser:page name="register" class=".customregistration.CustomRegistrationForm" permission="cmf.AddPortalMember" for="plone.app.layout.navigation.interfaces.INavigationRoot" layer="collective.awesomeaddon.interfaces.ICollectiveAwesomeaddon" /> </configure>
We have retained the permissions and marker interface of the original view. You may provide a specific permission or marker interface instead of these as your product requires.
from plone.app.users.browser.register import RegistrationForm class CustomRegistrationForm(RegistrationForm): """ Subclass the standard registration form """
You can also add a view to your package via plonecli and place the Python code and ZCML registration in the generated view files as shown above. This way you also have a test for the generated view.
The Mastering Plone Training has several chapters on views.
Anatomy of a view#
Views are Zope Component Architecture (ZCA) multi-adapter registrations.
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 with three inputs:
Any class or interface for which the view applies. If not given,
zope.interface.Interfaceis used (corresponds to a registration
for="*"). Usually this is a content item instance.
The current HTTP request. The interface
Theme layer and add-on layer interface. If not given,
Views return an HTTP request payload as the output. Returned strings are turned into HTML page responses.
Views can be any Python class taking in
(context, request) construction parameters.
A minimal view would be the following.
class MyView(object): def __init__(self, context, request): self.context = context self.request = request def __call__(self): return "Hello world. You are rendering this view at the context of %s" % self.context
However, in most cases:
Replace links to Plone 5.2 docs with links to Plone 6 docs. Specifically:
Full Plone page views are a subclass of
Products.Five.browser.BrowserViewwhich is a wrapper class. It wraps
zope.publisher.browser.BrowserViewand adds an acquisition (parent traversal) support for it.
Views have an attribute
indexwhich points to a TAL page template responsible for rendering the HTML code. You get the HTML output by doing
self.index(). The page template gets a context argument
viewpointing to the view class instance. The
indexvalue is usually an instance of
Products.Five.browser.pagetemplate.ViewPageTemplateFilefor full Plone pages or
zope.pagetemplate.pagetemplatefile.PageTemplateFilefor HTML snippets without using acquisition.
Views that render page snippets and parts can be direct subclasses of
zope.publisher.browser.BrowserView, as snippets might not need acquisition support which adds some overhead to the rendering process.
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. As such, you can call view methods directly from a page template, not only from Python code.
Replace links to Plone 5.2 docs with links to Plone 6 docs. Specifically:
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:
Limiting Python expression to one line.
Not being able to import Python modules.
RestrictedPython scripts (creating Python through the Management Interface) and Zope 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 separate
BrowserViewfor it and then call this view (see Accessing a view instance in code), or
ViewPageTemplateinstance between views and using it several times.
The old 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.
If you ever need to change the template language, or mix in other template languages, you can do better when templates are a feature of a pure Python based view, and not vice versa.
The following code snippet 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.
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 method
renderSummary() 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') != 'None'" tal:replace="structure context/Description" /> </div>
summarybox.pt itself is a piece of HTML code without the Plone decoration frame (
main_template/master, and other 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>
Accessing a view instance in code#
You need to get access to the view in your code if you are:
calling a view from inside another view, or
calling a view from your unit test code.
Below are two different approaches for that.
This is the most efficient way in Python.
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
By using traversal#
Traversal is slower than directly calling
However, traversal is readily available in templates and
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 look-ups 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 the `lang` attribute by using `BrowserView` whose name is `plone_portal_state`. </div>
Use a skin-based template in a
For 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#
The following example 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 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) ]
Default view of a content item#
Objects have views for
edit, and so on.
The distinction between the
view views are that, for files, the default can be
defaultview is configured in Content Types.
defaultview is rendered when a content item is called. Even though they are objects, they have the
__call__()Python method defined.
If you need to explicitly get a content item's view for page rendering, you can do it as follows:
def viewURLFor(item): cstate = getMultiAdapter((item, item.REQUEST), name='plone_context_state') return cstate.view_url()
Replace links to Plone 5.2 docs to Plone 6 docs. Specifically:
There are two different classes that share the same name
Compare the differences in code.
from zope.app.pagetemplate import ViewPageTemplateFile
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
The difference is that the
Five version supports:
Other Plone-specific TAL expression functions, such as
Usually, Plone code needs the
Some subsystems, notably the
z3c.formpackage, expect the Zope 3 (not
Five) version of
Views and automatic member variable acquisition wrapping#
View class instances will automatically assign themselves as a parent for all member variables.
This is because
Five package based views inherit from
Acquisition.Implicit base class.
For example, say you have a content item
Basket with an
Then if you use this object in a view code's member variable assignment, such as in the method
self.basket = my_basket
…it will mess up the Basket content item's acquisition chain:
<Basket at /isleofback/sisalto/yritykset/katajamaan_taksi/d59ca034c50995d6a77cacbe03e718de>
This concerns views, viewlets, and portlet renderers. It will, for example, make the following code fail:
self.obj = self.context.reference_catalog.lookupObject(value) return self.obj.absolute_url() # Acquistion chain messed up, getPhysicalPath() fails
One workaround to avoid this mess is to use
aq_inner when accessing
self.obj values, as described in Dealing with view implicit acquisition problems in Plone.