--- myst: html_meta: "description": "" "property=og:description": "" "property=og:title": "" "keywords": "" --- (plone5-viewlets1-label)= # Writing Viewlets ````{sidebar} Get the code! Code for the beginning of this chapter: ```shell git checkout behaviors_1 ``` Code for the end of this chapter: ```shell git checkout viewlets_1 ``` {doc}`code` ```` In this part you will: - Display data from a behavior in a viewlet Topics covered: - Viewlets (plone5-viewlets1-featured-label)= ## A viewlet for the featured behavior ```{only} not presentation 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 ({file}`colophon.pt`). Now we add a new one. - Viewlets don't save data (portlets do) - Viewlets have no user interface (portlets do) (plone5-viewlets1-featured2-label)= ## Featured viewlet ```{only} not presentation Let's add a link to the site that uses the information that we collected using the featured behavior. ``` We register the viewlet in {file}`browser/configure.zcml`. ```{code-block} xml :linenos: ``` `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. ```{only} not presentation This registers a viewlet called `featured`. It is visible on all content that implements the interface {py:class}`IFeatured` from our behavior. It is also good practice to bind it to a specific `layer`, so it only shows up if our add-on is actually installed. We will return to this in a later chapter. ``` The viewlet class {py:class}`FeaturedViewlet` is expected in a file {file}`browser/viewlets.py`. ```{code-block} python :linenos: from plone.app.layout.viewlets import ViewletBase class FeaturedViewlet(ViewletBase): pass ``` ```{only} not presentation This class does nothing except rendering the associated template (That we have yet to write) ``` Let's add the missing template {file}`templates/featured_viewlet.pt`. ```{code-block} html :linenos: ``` ```{only} not presentation As you can see this is not a valid HTML document. That is not needed, because we don't want a complete view here, a HTML snippet is enough. There is a {samp}`tal:define` statement, querying for {samp}`view/is_featured`. Same as for views, viewlets have access to their class in page templates, as well. ``` We have to extend the Featured Viewlet now to add the missing attribute: ```{code-block} python :emphasize-lines: 2, 6-8 :linenos: from plone.app.layout.viewlets import ViewletBase from ploneconf.site.behaviors.featured import IFeatured class FeaturedViewlet(ViewletBase): def is_featured(self): adapted = IFeatured(self.context) return adapted.featured ``` So far, we > - register the viewlet to content that has the IFeatured Interface. > - adapt the object to its behavior to be able to access the fields of the behavior > - return the link ````{only} not presentation ```{note} **Why not to access context directly** In this example, {samp}`IFeatured(self.context)` does return the context directly. It is still good to use this idiom for two reasons: > 1. It makes it clear that we only want to use the IFeatured aspect of the object > 2. If we decide to use a factory, for example to store our attributes in an annotation, we would `not` get back our context, but the adapter. Therefore in this example you could simply write {samp}`return self.context.featured`. ``` ```` (plone5-viewlets1-excercises-label)= ## Exercise 1 Register a viewlet 'number_of_talks' in the footer that is only visible to admins (the permission you are looking for is {py:class}`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...) ````{dropdown} Solution :animate: fade-in-slide-down :icon: question Register the viewlet in {file}`browser/configure.zcml` ```xml ``` For the `for` and `layer`-parameters `*` is shorthand for {py:class}`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 {file}`browser/templates/number_of_talks.pt`: ```html
There are talks.
``` {samp}`python:context.portal_catalog` will return the catalog through Acquisition. Be careful if you want to use path expressions: {samp}`context/portal_catalog` calls the catalog (and returns all brains). You need to prevent this by using {samp}`nocall:context/portal_catalog`. Relying on Acquisition is a bad idea. It would be much better to use the helper view `plone_tools` from {file}`plone/app/layout/globals/tools.py` to get the catalog. ```html
There are talks.
``` {samp}`context/@@plone_tools/catalog` traverses to the view `plone_tools` and calls its method {py:meth}`catalog`. In python it would look like this: ```html
There are talks.
``` 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: ```html
There are ${python:number_of_talks} talks.
``` ```` ## 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. ````{dropdown} Solution :animate: fade-in-slide-down :icon: question In {file}`configure.zcml`: ```xml ``` In {file}`viewlets.py`: ```python 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 {ref}`plone5-registry-label` you learn how store global configuration and easily create control panels. And in {file}`templates/days_to_conference.pt`: ```html
${python: view.human()}
``` Or using the moment pattern in Plone 5: ```html
${python: view.date()}
``` ```` [plone5_browserlayer]: https://5.docs.plone.org/develop/plone/views/layers.html?highlight=browserlayer#introduction