24. Writing Viewlets¶
In this part you will:
- Display data from a behavior in a viewlet
Topics covered:
- Viewlets
A viewlet for the social behavior¶
A viewlet is not a view but a snippet of HTML and logic that can be put in various places in the site. These places are called viewletmanager.
- Inspect existing viewlets and their managers by going to http://localhost:8080/Plone/@@manage-viewlets.
- We already customized a viewlet (
colophon.pt). Now we add a new one. - Viewlets don’t save data (portlets do)
- Viewlets have no user interface (portlets do)
Exercise 1¶
Register a viewlet ‘number_of_talks’ in the footer that is only visible to admins (the permission you are looking for is cmf.ManagePortal). Use only a template (no class) to display the number of talks already submitted. Hint: Use Acquisition to get the catalog (You know, you should not do this but there is plenty of code out there that does it...)
Solution
Register the viewlet in browser/configure.zcml
<browser:viewlet
name="number_of_talks"
for="*"
manager="plone.app.layout.viewlets.interfaces.IPortalFooter"
layer="zope.interface.Interface"
template="templates/number_of_talks.pt"
permission="cmf.ManagePortal"
/>
For the for and layer-parameters * is shorthand for zope.interface.Interface and the same effect as omitting them: The viewlet will be shown for all types of pages and for all Plone sites within your Zope instance.
Add the template browser/templates/number_of_talks.pt:
<div class="number_of_talks"
tal:define="catalog python:context.portal_catalog;
talks python:len(catalog(portal_type='talk'));">
There are <span tal:replace="talks" /> talks.
</div>
python:context.portal_catalog will return the catalog through Acquisition. Be careful if you want to use path expressions: content/portal_catalog calls the catalog (and returns all brains). You need to prevent this by using nocall:content/portal_catalog.
Relying on Acquisition is a bad idea. It would be much better to use the helper view plone_tools from plone/app/layout/globals/tools.py to get the catalog.
<div class="number_of_talks"
tal:define="catalog context/@@plone_tools/catalog;
talks python:len(catalog(portal_type='talk'));">
There are <span tal:replace="talks" /> talks.
</div>
context/@@plone_tools/catalog traverses to the view plone_tools and calls its method catalog(). In python it would look like this:
<div class="number_of_talks"
tal:define="catalog python:context.restrictedTraverse('plone_tools').catalog();
talks python:len(catalog(portal_type='talk'));">
There are <span tal:replace="talks" /> talks.
</div>
It is not a good practice to query the catalog within a template since even simple logic like this should live in Python. But it is very powerful if you are debugging or need a quick and dirty solution.
In Plone 5 you could even write it like this:
<?python
from plone import api
catalog = api.portal.get_tool('portal_catalog')
talks_amount = len(catalog(portal_type='talk'))
?>
<div class="number_of_talks">
There are ${talks_amount} talks.
</div>
Exercise 2¶
Register a viewlet ‘days_to_conference’ in the header. Use a class and a template to display the number of days until the conference. You get bonus points if you display it in a nice format (think “In 2 days” and “Last Month”) by using either javascript or a python library.
Solution
In configure.zcml:
<browser:viewlet
name="days_to_conference"
for="*"
manager="plone.app.layout.viewlets.interfaces.IPortalHeader"
layer="*"
class=".viewlets.DaysToConferenceViewlet"
template="templates/days_to_conference.pt"
permission="zope2.View"
/>
In viewlets.py:
from plone.app.layout.viewlets import ViewletBase
from datetime import datetime
import arrow
CONFERENCE_START_DATE = datetime(2015, 10, 12)
class DaysToConferenceViewlet(ViewletBase):
def date(self):
return CONFERENCE_START_DATE
def human(self):
return arrow.get(CONFERENCE_START_DATE).humanize()
Setting the date in python is not very user-friendly. In the chapter Manage Settings with Registry, Controlpanels and Vocabularies you learn how store global configuration and easily create controlpanels.
And in templates/days_to_conference.pt:
<div class="days_to_conf">
${python: view.human()}
</div>
Or using the moment pattern in Plone 5:
<div class="pat-moment"
data-pat-moment="format: relative">
${python: view.date()}
</div>
Social viewlet¶
Let’s add a link to the site that uses the information that we collected using the social behavior.
We register the viewlet in
browser/configure.zcml.for,manager,layerandpermissionare constraints that limit the contexts in which the viewlet is loaded and rendered, by filtering out all the contexts that do not match those constraints.This registers a viewlet called
social. It is visible on all content that implements the interfaceISocialfrom our behavior. It is also good practice to bind it to a specificlayer, so it only shows up if our add-on is actually installed. We will return to this in a later chapter.The viewlet class
SocialViewletis expected in a filebrowser/viewlets.py.This class does nothing except rendering the associated template (That we have yet to write)
Let’s add the missing template
templates/social_viewlet.pt.As you can see this is not a valid HTML document. That is not needed, because we don’t want a complete view here, just a html snippet.
There is a
tal:definestatement, querying forview/lanyrd_link. Same as for views, viewlets have access to their class in page templates, as well.We have to extend the Social Viewlet now to add the missing attribute:
Why not to access context directly
In this example,
ISocial(self.context)does return the context directly. It is still good to use this idiom for two reasons:Therefore in this example you could simply write
return self.context.lanyrd.So far, we