Advanced Viewlets

In the previous chapter Dexterity Types III: Sponsors you created the sponsor content type. Now let's learn how to display them at the bottom of every page.

To be solved task in this part:

  • Display sponsors on all pages sorted by level

In this part you will:

  • Display data from collected content

The topics we cover are:

  • Viewlets

  • Image scales

  • Caching

The view

For sponsors we will stay with the default view provided by Dexterity since we will only display the sponsors in a viewlet and not in their own page.

Note

If we really want a custom view for sponsors it could look like this.

 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    <h3 tal:content="structure view/w/level/render">
 7      Level
 8    </h3>
 9
10    <div tal:content="structure view/w/text/render">
11      Text
12    </div>
13
14    <div class="newsImageContainer">
15      <a tal:attributes="href context/url">
16        <img tal:condition="python:getattr(context, 'logo', None)"
17             tal:attributes="src string:${context/absolute_url}/@@images/logo/preview" />
18      </a>
19    </div>
20
21    <div>
22      <a tal:attributes="href context/url">
23        Website
24      </a>
25
26      <img tal:condition="python:getattr(context, 'advertisement', None)"
27           tal:attributes="src string:${context/absolute_url}/@@images/advertisement/preview" />
28
29      <div tal:condition="python: 'notes' in view.w"
30           tal:content="structure view/w/notes/render">
31        Notes
32      </div>
33
34    </div>
35  </metal:content-core>
36</body>
37</html>

Note how we handle the field with special permissions: tal:condition="python: 'notes' in view.w" checks if the convenience-dictionary w (provided by the base class DefaultView) holds the widget for the field notes. If the current user does not have the permission cmf.ManagePortal it will be omitted from the dictionary and get an error since notes would not be a key in w. By first checking if it's missing we work around that.

The viewlet

Instead of writing a view you will have to display the sponsors at the bottom of the website in a viewlet. In the chapter Writing Viewlets you already wrote a viewlet.

Remember:

  • A viewlet produces in a snippet of HTML that can be put in various places in the page. These places are called viewletmanager.

  • They can but don't have to have a association to the current context.

  • The logo and searchbox are viewlets for example and they are always the same.

  • Viewlets don't save data (portlets do).

  • Viewlets have no user interface except the one to sort and hide/unhide viewlets.

Register the viewlet in browser/configure.zcml

1<browser:viewlet
2    name="sponsorsviewlet"
3    manager="plone.app.layout.viewlets.interfaces.IPortalFooter"
4    for="*"
5    layer="..interfaces.IPloneconfSiteLayer"
6    class=".viewlets.SponsorsViewlet"
7    template="templates/sponsors_viewlet.pt"
8    permission="zope2.View"
9    />

Add the viewlet class in browser/viewlets.py

 1# -*- coding: utf-8 -*-
 2from collections import OrderedDict
 3from plone import api
 4from plone.app.layout.viewlets.common import ViewletBase
 5from plone.memoize import ram
 6from ploneconf.site.behaviors.featured import IFeatured
 7from ploneconf.site.content.sponsor import LevelVocabulary
 8from random import shuffle
 9from time import time
10
11
12class FeaturedViewlet(ViewletBase):
13
14    def is_featured(self):
15        adapted = IFeatured(self.context)
16        return adapted.featured
17
18
19class SponsorsViewlet(ViewletBase):
20
21    @ram.cache(lambda *args: time() // (60 * 60))
22    def _sponsors(self):
23        results = []
24        for brain in api.content.find(portal_type='sponsor'):
25            obj = brain.getObject()
26            scales = api.content.get_view(
27                name='images',
28                context=obj,
29                request=self.request)
30            scale = scales.scale(
31                'logo',
32                width=200,
33                height=80,
34                direction='down')
35            tag = scale.tag() if scale else None
36            if not tag:
37                # only display sponsors with a logo
38                continue
39            results.append({
40                'title': obj.title,
41                'description': obj.description,
42                'tag': tag,
43                'url': obj.url or obj.absolute_url(),
44                'level': obj.level
45            })
46        return results
47
48    def sponsors(self):
49        sponsors = self._sponsors()
50        if not sponsors:
51            return
52        results = OrderedDict()
53        levels = [i.value for i in LevelVocabulary]
54        for level in levels:
55            level_sponsors = []
56            for sponsor in sponsors:
57                if level == sponsor['level']:
58                    level_sponsors.append(sponsor)
59            if not level_sponsors:
60                continue
61            shuffle(level_sponsors)
62            results[level] = level_sponsors
63        return results
  • _sponsors() returns a list of dictionaries containing all necessary info about sponsors.

  • We create the complete img tag using a custom scale (200x80) using the view images from plone.namedfile. This actually scales the logos and saves them as new blobs.

  • In sponsors() we return an ordered dictionary of randomized lists of dicts (containing the information on sponsors). The order is by sponsor-level since we want the platinum sponsors on top and the bronze sponsors at the bottom. The randomization is for fairness among equal sponsors.

_sponsors() is cached for an hour using plone.memoize. This way we don't need to keep all sponsor objects in memory all the time. But we'd have to wait for up to an hour until changes will be visible.

Instead we should cache until one of the sponsors is modified by using a callable _sponsors_cachekey() that returns a number that changes when a sponsor is modified.

...
def _sponsors_cachekey(method, self):
    brains = api.content.find(portal_type='sponsor')
    cachekey = sum([int(i.modified) for i in brains])
    return cachekey

@ram.cache(_sponsors_cachekey)
def _sponsors(self):
    catalog = api.portal.get_tool('portal_catalog')
...

The template for the viewlet

Add the template browser/templates/sponsors_viewlet.pt

 1<div metal:define-macro="portal_sponsorbox"
 2     i18n:domain="ploneconf.site">
 3    <div id="portal-sponsorbox" class="container"
 4         tal:define="sponsors view/sponsors;"
 5         tal:condition="sponsors">
 6        <div class="row">
 7            <h2>We ❤ our sponsors</h2>
 8        </div>
 9        <div tal:repeat="level sponsors"
10             tal:attributes="id python:'level-' + level"
11             class="row">
12            <h3 tal:content="python: level.capitalize()">
13                Gold
14            </h3>
15            <tal:images tal:define="items python:sponsors[level];"
16                        tal:repeat="item items">
17                <div class="sponsor">
18                    <a href=""
19                       tal:attributes="href python:item['url'];
20                                       title python:item['title'];">
21                        <img tal:replace="structure python:item['tag']" />
22                    </a>
23                </div>
24            </tal:images>
25        </div>
26    </div>
27</div>

You can now add some CSS in browser/static/ploneconf.css to make it look OK.

.sponsor {
  display: inline-block;
  margin: 0 1em 1em 0;
}

.sponsor:hover {
  box-shadow: 0 0 8px #000;
  -moz-box-shadow: 0 0 8px #000;
  -webkit-box-shadow: 0 0 8px #000;
}

Result:

The result of the newly created content type.

The result of the newly created content type.

Exercise 2

This is more of a Python exercise. The gold and bronze sponsors should also have a bigger logo than the others. Scale the sponsors' logos to the following sizes without using CSS.

  • Platinum: 500x200

  • Gold: 350x150

  • Silver: 200x80

  • Bronze: 150x60

Solution

 1# -*- coding: utf-8 -*-
 2from collections import OrderedDict
 3from plone import api
 4from plone.app.layout.viewlets.common import ViewletBase
 5from plone.memoize import ram
 6from ploneconf.site.behaviors.social import ISocial
 7from ploneconf.site.content.sponsor import LevelVocabulary
 8from random import shuffle
 9
10LEVEL_SIZE_MAPPING = {
11    'platinum': (500, 200),
12    'gold': (350, 150),
13    'silver': (200, 80),
14    'bronze': (150, 60),
15}
16
17
18class SocialViewlet(ViewletBase):
19
20    def lanyrd_link(self):
21        adapted = ISocial(self.context)
22        return adapted.lanyrd
23
24
25class SponsorsViewlet(ViewletBase):
26
27    def _sponsors_cachekey(method, self):
28        brains = api.content.find(portal_type='sponsor')
29        cachekey = sum([int(i.modified) for i in brains])
30        return cachekey
31
32    @ram.cache(_sponsors_cachekey)
33    def _sponsors(self):
34        results = []
35        for brain in api.content.find(portal_type='sponsor'):
36            obj = brain.getObject()
37            scales = api.content.get_view(
38                name='images',
39                context=obj,
40                request=self.request)
41            width, height = LEVEL_SIZE_MAPPING[obj.level]
42            scale = scales.scale(
43                'logo',
44                width=width,
45                height=height,
46                direction='down')
47            tag = scale.tag() if scale else None
48            if not tag:
49                # only display sponsors with a logo
50                continue
51            results.append({
52                'title': obj.title,
53                'description': obj.description,
54                'tag': tag,
55                'url': obj.url or obj.absolute_url(),
56                'level': obj.level
57            })
58        return results
59
60    def sponsors(self):
61        sponsors = self._sponsors()
62        if not sponsors:
63            return
64        results = OrderedDict()
65        levels = [i.value for i in LevelVocabulary]
66        for level in levels:
67            level_sponsors = []
68            for sponsor in sponsors:
69                if level == sponsor['level']:
70                    level_sponsors.append(sponsor)
71            if not level_sponsors:
72                continue
73            shuffle(level_sponsors)
74            results[level] = level_sponsors
75        return results