34. Dexterity Types III: Python#
Without sponsors, a conference would be hard to finance! Plus it is a good opportunity for Plone companies to advertise their services. But sponsors want to be displayed in a nice way according to the size of their sponsorship.
In this part we will:
create the content type sponsor that has a Python schema,
create a viewlet that shows the sponsor logos sorted by sponsoring level.
The topics we cover are:
Python schema for Dexterity
schema hint and directives
field permissions
image scales
caching
34.1. The Python schema#
First we create the schema for the new type. Instead of XML, we use Python this time.
In chapter Return to Dexterity: Moving contenttypes into Code you already created a folder content
with an empty __init__.py
in it.
We don't need to register that folder in configure.zcml
since we don't need a content/configure.zcml
(at least not yet).
Now add a new file content/sponsor.py
.
1# -*- coding: utf-8 -*-
2from plone.app.textfield import RichText
3from plone.autoform import directives
4from plone.namedfile import field as namedfile
5from plone.supermodel import model
6from plone.supermodel.directives import fieldset
7from ploneconf.site import _
8from z3c.form.browser.radio import RadioFieldWidget
9from zope import schema
10from zope.schema.vocabulary import SimpleTerm
11from zope.schema.vocabulary import SimpleVocabulary
12
13
14LevelVocabulary = SimpleVocabulary(
15 [SimpleTerm(value=u'platinum', title=_(u'Platinum Sponsor')),
16 SimpleTerm(value=u'gold', title=_(u'Gold Sponsor')),
17 SimpleTerm(value=u'silver', title=_(u'Silver Sponsor')),
18 SimpleTerm(value=u'bronze', title=_(u'Bronze Sponsor'))]
19 )
20
21
22class ISponsor(model.Schema):
23 """Dexterity Schema for Sponsors
24 """
25
26 directives.widget(level=RadioFieldWidget)
27 level = schema.Choice(
28 title=_(u'Sponsoring Level'),
29 vocabulary=LevelVocabulary,
30 required=True
31 )
32
33 text = RichText(
34 title=_(u'Text'),
35 required=False
36 )
37
38 url = schema.URI(
39 title=_(u'Link'),
40 required=False
41 )
42
43 fieldset('Images', fields=['logo', 'advertisement'])
44 logo = namedfile.NamedBlobImage(
45 title=_(u'Logo'),
46 required=False,
47 )
48
49 advertisement = namedfile.NamedBlobImage(
50 title=_(u'Advertisement (Gold-sponsors and above)'),
51 required=False,
52 )
53
54 directives.read_permission(notes='cmf.ManagePortal')
55 directives.write_permission(notes='cmf.ManagePortal')
56 notes = RichText(
57 title=_(u'Secret Notes (only for site-admins)'),
58 required=False
59 )
Some things are notable here:
The fields in the schema are mostly from
zope.schema
. A reference of available fields is at https://5.docs.plone.org/external/plone.app.dexterity/docs/reference/fields.htmlIn
directives.widget(level=RadioFieldWidget)
we change the default widget for a Choice field from a dropdown to radio-boxes. An incomplete reference of available widgets is at https://5.docs.plone.org/external/plone.app.dexterity/docs/reference/widgets.htmlLevelVocabulary
is used to create the options used in the fieldlevel
. This way we could easily translate the displayed value.fieldset('Images', fields=['logo', 'advertisement'])
moves the two image fields to another tab.directives.read_permission(...)
sets the read and write permission for the fieldnotes
to users who can add new members. Usually this permission is only granted to Site Administrators and Managers. We use it to store information that should not be publicly visible. Please note thatobj.notes
is still accessible in templates and Python. Only using the widget (like we do in the view later) checks for the permission.
See also
See the chapter Dexterity: Reference for a reference of all field-types and directives you can use in dexterity.
34.2. The Factory Type Information, or FTI#
Next, we create the factory type information ("FTI") for the new type in profiles/default/types/sponsor.xml
1<?xml version="1.0"?>
2<object name="sponsor" meta_type="Dexterity FTI" i18n:domain="plone"
3 xmlns:i18n="http://xml.zope.org/namespaces/i18n">
4 <property name="title" i18n:translate="">Sponsor</property>
5 <property name="description" i18n:translate=""></property>
6 <property name="icon_expr">string:${portal_url}/document_icon.png</property>
7 <property name="factory">sponsor</property>
8 <property name="add_view_expr">string:${folder_url}/++add++sponsor</property>
9 <property name="link_target"></property>
10 <property name="immediate_view">view</property>
11 <property name="global_allow">True</property>
12 <property name="filter_content_types">True</property>
13 <property name="allowed_content_types"/>
14 <property name="allow_discussion">False</property>
15 <property name="default_view">view</property>
16 <property name="view_methods">
17 <element value="view"/>
18 </property>
19 <property name="default_view_fallback">False</property>
20 <property name="add_permission">cmf.AddPortalContent</property>
21 <property name="klass">plone.dexterity.content.Container</property>
22 <property name="behaviors">
23 <element value="plone.dublincore"/>
24 <element value="plone.namefromtitle"/>
25 </property>
26 <property name="schema">ploneconf.site.content.sponsor.ISponsor</property>
27 <property name="model_source"></property>
28 <property name="model_file"></property>
29 <property name="schema_policy">dexterity</property>
30 <alias from="(Default)" to="(dynamic view)"/>
31 <alias from="edit" to="@@edit"/>
32 <alias from="sharing" to="@@sharing"/>
33 <alias from="view" to="(selected layout)"/>
34 <action title="View" action_id="view" category="object" condition_expr=""
35 description="" icon_expr="" link_target="" url_expr="string:${object_url}"
36 visible="True">
37 <permission value="View"/>
38 </action>
39 <action title="Edit" action_id="edit" category="object" condition_expr=""
40 description="" icon_expr="" link_target=""
41 url_expr="string:${object_url}/edit" visible="True">
42 <permission value="Modify portal content"/>
43 </action>
44</object>
Then we register the FTI in profiles/default/types.xml
1<?xml version="1.0"?>
2<object name="portal_types" meta_type="Plone Types Tool">
3 <property name="title">Controls the available contenttypes in your portal</property>
4 <object name="talk" meta_type="Dexterity FTI"/>
5 <object name="sponsor" meta_type="Dexterity FTI"/>
6 <!-- -*- more types can be added here -*- -->
7</object>
After reinstalling our package we can create the new type.
Exercise 1#
Sponsors are containers but they don't need to be. Turn them into items by changing their class to plone.dexterity.content.Item
.
34.3. The view#
We use the default view provided by Dexterity for testing 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.
34.4. The viewlet#
Instead of writing a view you will have to display the sponsors at the bottom of the website in a viewlet.
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') ...
34.5. 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#
Turn the content type Speaker from Exercise 2 of the first chapter on Dexterity into a Python-based type.
When we're done, it should have the following fields:
title
email
homepage
biography
company
twitter_name
irc_name
image
Do not use the IBasic
or IDublinCore
behavior to add title and description. Instead add your own field title
and give it the title Name.
Exercise 3#
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