Escribiendo Viewlets

In this part you will:

  • Display data from a behavior in a viewlet

Topics covered:

  • Viewlets

Un viewlet para el comportamiento social

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.

  • Inspeccione los viewlets existentes y gestionelos yendo a http://localhost:8080/Plone/@@manage-viewlets.

  • We already customized a viewlet (colophon.pt). Now we add a new one.
  • Los viewlets no guardan los datos (los portlets lo hacen)

  • Viewlets have no user interface (portlets do)

Social viewlet

Let’s add a link to the site that uses the information that we collected using the social behavior.

Registramos el viewlet en el archivo browser/configure.zcml.

1
2
3
4
5
6
7
8
9
<browser:viewlet
  name="social"
  for="ploneconf.site.behaviors.social.ISocial"
  manager="plone.app.layout.viewlets.interfaces.IBelowContentTitle"
  class=".viewlets.SocialViewlet"
  layer="zope.interface.Interface"
  template="templates/social_viewlet.pt"
  permission="zope2.View"
  />

for, manager, layer and permission are 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 interface ISocial from our behavior. It is also good practice to bind it to a specific layer, so it only shows up if our addon is actually installed. We will return to this in a later chapter.

The viewlet class SocialViewlet is expected in a file browser/viewlets.py.

1
2
3
4
from plone.app.layout.viewlets import ViewletBase

class SocialViewlet(ViewletBase):
    pass

This class does nothing except rendering the associated template (That we have yet to write)

Vamos a añadir la plantilla faltante templates/social_viewlet.pt.

1
2
3
4
5
6
7
8
9
<div id="social-links">
    <a href="#"
       class="lanyrd-link"
       tal:define="link view/lanyrd_link"
       tal:condition="link"
       tal:attributes="href link">
         See this talk on Lanyrd!
    </a>
</div>

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 define statement, querying for view/lanyrd_link. Same as for views, viewlets have access to their class in page templates, as well.

Tenemos que ampliar el Viewlet Social ahora para añadir el atributo que faltan:

1
2
3
4
5
6
7
8
from plone.app.layout.viewlets import ViewletBase
from ploneconf.site.behaviors.social import ISocial

class SocialViewlet(ViewletBase):

    def lanyrd_link(self):
        adapted = ISocial(self.context)
        return adapted.lanyrd

Hasta ahora, hemos

  • registrar el viewlet al contenido que tiene la interfaz iSocial.

  • adapt the object to its behavior to be able to access the fields of the behavior
  • devolver el enlace

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()

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>