Writing Viewlets

In this part you will:

  • Display data from a behavior in a viewlet

Topics covered:

  • Viewlets

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;
                 number_of_talks python:len(catalog(portal_type='talk'));">
    There are <span tal:replace="number_of_talks" /> talks.
</div>

python:context.portal_catalog will return the catalog through Acquisition. Be careful if you want to use path expressions: context/portal_catalog calls the catalog (and returns all brains). You need to prevent this by using nocall:context/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;
                 number_of_talks python:len(catalog(portal_type='talk', review_state='pending'));">
    There are <span tal:replace="number_of_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();
                 number_of_talks python:len(catalog(portal_type='talk'));">
    There are <span tal:replace="number_of_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')
number_of_talks = len(catalog(portal_type='talk'))

?>

<div class="number_of_talks">
    There are ${python:number_of_talks} 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 Vocabularies, Registry-Settings and Control Panels 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>