Views II: A Default View for "Talk"#
In this part you will:
Register a view with a Python class
Write a template used in the default view for talks
Topics covered:
View classes
BrowserView and DefaultView
displaying data from fields
View Classes#
Earlier we wrote a demo view which we also used to experiment with page templates. Now we are going to enhance that view so that it will have some Python code, in addition to a template. Let us have a look at the ZCML and the code.
browser/configure.zcml
1<configure xmlns="http://namespaces.zope.org/zope"
2 xmlns:browser="http://namespaces.zope.org/browser"
3 i18n_domain="ploneconf.site">
4
5 <browser:page
6 name="training"
7 for="*"
8 class=".views.DemoView"
9 template="templates/training.pt"
10 permission="zope2.View"
11 />
12
13</configure>
We are adding a file called views.py
in the browser
folder.
browser/views.py
1from Products.Five.browser import BrowserView
2
3class DemoView(BrowserView):
4
5 def the_title(self):
6 return u'A list of great trainings:'
In the template training.pt
we can now use this view as view
and access all its methods and properties:
<h2 tal:content="python: view.the_title()" />
The logic contained in the template can now be moved to the class:
1# -*- coding: utf-8 -*-
2from Products.Five.browser import BrowserView
3from operator import itemgetter
4
5
6class DemoView(BrowserView):
7 """A demo listing"""
8
9 def the_title(self):
10 return u'A list of talks:'
11
12 def talks(self):
13 results = []
14 data = [
15 {'title': 'Dexterity is the new default!',
16 'subjects': ('content-types', 'dexterity')},
17 {'title': 'Mosaic will be the next big thing.',
18 'subjects': ('layout', 'deco', 'views'),
19 'url': 'https://www.youtube.com/watch?v=QSNufxaYb1M'},
20 {'title': 'The State of Plone',
21 'subjects': ('keynote',)},
22 {'title': 'Diazo is a powerful tool for theming!',
23 'subjects': ('design', 'diazo', 'xslt')},
24 {'title': 'Magic templates in Plone 5',
25 'subjects': ('templates', 'TAL'),
26 'url': 'http://www.starzel.de/blog/magic-templates-in-plone-5'},
27 ]
28 for item in data:
29 url = item.get('url', 'https://www.google.com/search?q={}'.format(item['title']))
30 talk = {
31 'title': item['title'],
32 'subjects': ', '.join(item['subjects']),
33 'url': url
34 }
35 results.append(talk)
36 return sorted(results, key=itemgetter('title'))
And the template will now be much simpler.
1<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
2 lang="en"
3 metal:use-macro="context/main_template/macros/master"
4 i18n:domain="ploneconf.site">
5<body>
6
7<metal:content-core fill-slot="content-core">
8
9<h2 tal:content="python: view.the_title()" />
10
11<table class="listing">
12 <tr>
13 <th>Title</th>
14 <th>Topics</th>
15 </tr>
16
17 <tr tal:repeat="talk python:view.talks()">
18 <td>
19 <a href="${python:talk['url']}">
20 ${python:talk['title']}
21 </a>
22 </td>
23 <td>
24 ${python:talk['subjects']}
25 </td>
26 </tr>
27</table>
28
29</metal:content-core>
30
31</body>
32</html>
Note
It is a very common pattern that you prepare the data you want to display in Python.
Browser Views#
In the next example you will access the context
in which the view is called.
Edit browser/views.py
and add a method context_info
to the view DemoView
that returns information on the context.
In a method of a Browser View the content object which was context
in the template is now accessed as self.context
.
1def context_info(self):
2 context = self.context
3 title = context.title
4 portal_type = context.portal_type
5 url = context.absolute_url()
6 return u"This is the {0} '{1}' at {2}".format(portal_type, title, url)
Note
The result is the same as in python expressions where you wrote `<p tal:content="python: "This is the {0} '{1}' at {2}".format(context.portal_type, context.title, context.absolute_url()">
` in the template.The template training.pt
still needs to display that:
1<p tal:content="python: view.context_info()">
2 Info on the context
3</p>
Open the view on a talk and it will show you information on that talk.
Note
Changes in Python files are picked up by restarting Plone or using the add-on plone.reload
: http://localhost:8080/@@reload
Reusing Browser Views#
Browser Views can be called by accessing their name in the browser. Append
/training
to any URL and the view will be called.Browser Views can be associated with a template (like
training.pt
) to return some HTML.Browser Views can be reused in your code using
plone.api.content.get_view('<name of the view>', context, request)
. This allows you to reuse code and methods.
The method context_info
that returned information on the current object can be reused any time like this:
1from Products.Five.browser import BrowserView
2from plone import api
3
4class SomeOtherView(BrowserView):
5
6 def __call__(self):
7 training_view = api.content.get_view('training', self.context, self.request)
8 return training_view.context_info()
You would still need to register the view in configure.zcml:
1<browser:page
2 name="some_view"
3 for="*"
4 class=".views.SomeOtherView"
5 permission="zope2.View"
6 />
Using /some_view
would now return infomation of the current object in the browser without a template.
You can define which context
-object should be used:
1from Products.Five.browser import BrowserView
2from plone import api
3
4class SomeOtherView(BrowserView):
5
6 def __call__(self):
7 portal = api.portal.get()
8 some_talk = portal['dexterity-for-the-win']
9 training_view = api.content.get_view('training', some_talk, self.request)
10 return training_view.context_info()
typeinfo
will now be "This is the talk 'Dexterity for the win' at http://localhost:8080/Plone/dexterity-for-the-win"
Note
Browser Views
are the Swiss Army knife of every Plone developer
can be called by appending their name to a URL in the browser. Append
/training
to any URL and the view will be called.can be associated with a template (like
training.pt
) to return some HTML.can be reused in your code using
plone.api.content.get_view('<name of the view>', context, request)
.can be protected with permissions
can be constrained to certain content types by using
for="plonconf.site.content.sponsor.ISponsor"
can be constrained to certain add-ons by using
layer="plonconf.site.interfaces.IPloneconfSiteLayer"
The default view#
Now you know everything to create a nice view for talks in views.py
.
First we will not write any methods for view
but access the fields from the talk schema as context.<fieldname>
.
Register a view talkview
in browser/configure.zcml
:
1<browser:page
2 name="talkview"
3 for="*"
4 layer="zope.interface.Interface"
5 class=".views.TalkView"
6 template="templates/talkview.pt"
7 permission="zope2.View"
8 />
browser/views.py
1class TalkView(BrowserView):
2 """ The default view for talks"""
Add the template templates/talkview.pt
:
1<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
2 lang="en"
3 metal:use-macro="context/main_template/macros/master"
4 i18n:domain="ploneconf.site">
5<body>
6 <metal:content-core fill-slot="content-core">
7 <p>Suitable for <em tal:content="python: ', '.join(context.audience)"></em>
8 </p>
9
10 <div tal:condition="python: context.details"
11 tal:content="structure python: context.details.output" />
12
13 <div tal:content="python: context.speaker">
14 User
15 </div>
16 </metal:content-core>
17</body>
18</html>
After a restart, we can test our view by going to a talk and adding /talkview to the URL.
Using helper methods from DefaultView
#
In the previous section we used BrowserView
as the base class for TalkView
.
Dexterity comes with a nice helper class suited for views of content types: the DefaultView
base class in plone.dexterity
.
It has some very useful properties available to use in the template:
view.w
is a dictionary of all the display widgets, keyed by field names. This includes widgets from alternative fieldsets.view.widgets
contains a list of widgets in schema order for the default fieldset.view.groups
contains a list of fieldsets in fieldset order.view.fieldsets
contains a dict mapping fieldset name to fieldsetOn a fieldset (group), you can access a widget list to get widgets in that fieldset
You can now change the TalkView
to use it
1from plone.dexterity.browser.view import DefaultView
2
3...
4
5class TalkView(DefaultView):
6 """ The default view for talks
7 """
The template templates/talkview.pt
still works but now you can modify it
to use the pattern view/w/<fieldname>/render
to render the widgets:
1<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
2 lang="en"
3 metal:use-macro="context/main_template/macros/master"
4 i18n:domain="ploneconf.site">
5<body>
6 <metal:content-core fill-slot="content-core">
7 <p>Suitable for <em tal:replace="structure view/w/audience/render"></em>
8 </p>
9
10 <div tal:content="structure view/w/details/render" />
11
12 <div tal:content="python: context.speaker">
13 User
14 </div>
15 </metal:content-core>
16</body>
17</html>
After a restart, we can test the modified view by going to a talk and adding /talkview
to the URL.
We should tell Plone that the talkview should be used as the default view for talks instead of the built-in view.
This is a configuration that you can change during runtime and is stored in the database, as such it is also managed by GenericSetup profiles.
open profiles/default/types/talk.xml
:
1...
2<property name="default_view">talkview</property>
3<property name="view_methods">
4 <element value="talkview"/>
5 <element value="view"/>
6</property>
7...
We will have to either reinstall our add-on or run the GenericSetup import step typeinfo
so Plone learns about the change.
Note
To change it TTW go to the ZMI (http://localhost:8080/Plone/manage), go to portal_types
and select the type for which the new view should be selectable (talk).
Now add talkview
to the list Available view methods.
Now the new view is available in the menu Display.
To make it the default view enter it in Default view method
.
The complete template for talks#
Now you can improve the talkview to show data for all fields in the talk schema:
type_of_talk
details
audience
room
speaker
email
image
speaker_biography
Since we will use the macro content-core
the values for title
and description
of the talk will be rendered for us and we do not have to deal with them.
templates/talkview.pt
:
1<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
2 metal:use-macro="context/main_template/macros/master"
3 i18n:domain="ploneconf.site">
4<body>
5 <metal:content-core fill-slot="content-core">
6
7 <p>
8 <span tal:content="python:context.type_of_talk">
9 Talk
10 </span>
11 suitable for
12 <span tal:replace="structure view/w/audience/render">
13 Audience
14 </span>
15 </p>
16
17 <p tal:content="structure view/w/room/render">
18 Room
19 </p>
20
21 <div tal:content="structure view/w/details/render">
22 Details
23 </div>
24
25 <div class="newsImageContainer">
26 <img tal:condition="python:getattr(context, 'image', None)"
27 tal:attributes="src python:context.absolute_url() + '/@@images/image/thumb'" />
28 </div>
29
30 <div>
31 <a class="email-link" tal:attributes="href python:'mailto:' + context.email">
32 <strong tal:content="python: context.speaker">
33 Jane Doe
34 </strong>
35 </a>
36 <div tal:content="structure view/w/speaker_biography/render">
37 Biography
38 </div>
39 </div>
40
41 </metal:content-core>
42</body>
43</html>
Note
If you want to customize the rendering of title
and description
simply use the macro main
and add your own version to your template.
The default rendering is defined in Products.CMFPlone
in /Products/CMFPlone/browser/templates/main_template.pt
.
<header>
<div id="viewlet-above-content-title" tal:content="structure provider:plone.abovecontenttitle" />
<metal:title define-slot="content-title">
<h1 class="documentFirstHeading"
tal:define="title context/Title"
tal:condition="title"
tal:content="title">Title or id</h1>
</metal:title>
<div id="viewlet-below-content-title" tal:content="structure provider:plone.belowcontenttitle" />
<metal:description define-slot="content-description">
<div class="documentDescription description"
tal:define="description context/Description"
tal:content="description"
tal:condition="description">
Description
</div>
</metal:description>
</header>
Note that both title
and description
are wrapped in slots
and can be overwritten like this example:
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
lang="en"
metal:use-macro="context/main_template/macros/master"
i18n:domain="ploneconf.site">
<body>
<metal:foo fill-slot="content-title">
<h1 class="documentFirstHeading">
<span tal:replace="python:context.title" />
(<span class="pat-moment"
data-pat-moment="format:relative"
tal:content="python:context.Date()">
</span>)
</h1>
</metal:foo>
<metal:content-core fill-slot="content-core">
[...]
</metal:content-core>
</body>
</html>
Since in DefaultView
you have access to the widget you can also use other information, like label
which is the title of the field: <label tal:content="view/w/room/label"></label>
.
One benefit of this approach is that you automatically get the translated title.
This is used in the default-view for dexterity content plone/dexterity/browser/item.pt
.
Behind the scenes#
1from Products.Five.browser import BrowserView
2
3class DemoView(BrowserView):
4
5 def __init__(self, context, request):
6 self.context = context
7 self.request = request
8
9 def __call__(self):
10 # Implement your own actions
11
12 # This renders the template that was registered in zcml like this:
13 # template="templates/training.pt"
14 return super(DemoView, self).__call__()
15 # If you don't register a template in zcml the Superclass of
16 # DemoView will have no __call__-method!
17 # In that case you have to call the template like this:
18 # from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
19 # class DemoView(BrowserView):
20 # template = ViewPageTemplateFile('templates/training.pt')
21 # def __call__(self):
22 # return self.template()
Do you remember the term MultiAdapter
?
The BrowserView is just a MultiAdapter.
The ZCML statement browser:page
registers a MultiAdapter
and adds additional things needed for a browser view.
An adapter adapts things, a MultiAdapter
adapts multiple things.
When you enter a URL, Zope tries to find an object for it. At the end, when Zope does not find any more objects but there is still a path item left, or there are no more path items, Zope looks for an adapter that will reply to the request.
The adapter adapts the request and the object that Zope found with the URL. The adapter class gets instantiated with the objects to be adapted, then it gets called.
The code above does the same thing that the standard implementation would do.
It makes context
and request
available as variables on the object.
I have written down these methods because it is important to understand some important concepts.
The __init__()
method gets called while Zope is still trying to find a view. At that phase, the security has not been resolved.
Your code is not security checked.
For historical reasons, many errors that happen in the __init__()
method can result
in a page not found error instead of an exception.
Use the __init__()
method to do as little as possible, if at all.
Instead, you have the guarantee that the __call__()
method is called before anything else (but after the __init__()
method).
It has the security checks in place and so on.
From a practical standpoint, consider the __call__()
method your __init__()
method,
the biggest difference is that this method is supposed to return the HTML already.
Let your base class handle the HTML generation.