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 viewimages
fromplone.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:
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