A Viewlet for the Votable Behavior#
Voting Viewlet#
In this part you will:
Write the viewlet template
Add jQuery include statements
Saving the vote on the object using annotations
Topics covered:
Viewlets
JavaScript inclusion
Earlier we added the logic that saves votes on the objects. We now create the user interface for it.
Since we want to use the UI on more than one page (not only the talk view but also the talk listing) we need to put it somewhere.
To handle the user input we don't use a form but links and ajax.
The voting itself is a fact handled by another view
We are working in the add-on starzel.votable_behavior
we created using mr.bob.
We register the viewlet in browser/configure.zcml
.
1 <configure xmlns="http://namespaces.zope.org/zope"
2 xmlns:browser="http://namespaces.zope.org/browser">
3
4 ...
5
6 <browser:viewlet
7 name="voting"
8 for="starzel.votable_behavior.interfaces.IVoting"
9 manager="plone.app.layout.viewlets.interfaces.IBelowContentTitle"
10 layer="..interfaces.IVotableLayer"
11 class=".viewlets.Vote"
12 template="templates/voting_viewlet.pt"
13 permission="zope2.View"
14 />
15
16 ....
17
18 </configure>
We extend the file browser/viewlets.py
1from plone.app.layout.viewlets import common as base
2
3
4class Vote(base.ViewletBase):
5 pass
This will add a viewlet to a slot below the title and expect a template voting_viewlet.pt
in a folder browser/templates
.
Let's create the file browser/templates/voting_viewlet.pt
without any logic
1 <div class="voting">
2 Wanna vote? Write code!
3 </div>
4
5 <script type="text/javascript">
6 jq(document).ready(function(){
7 // please add some jQuery-magic
8 });
9 </script>
restart Plone
show the viewlet
Writing the Viewlet code#
Update the viewlet to contain the necessary logic in browser/viewlets.py
1from plone.app.layout.viewlets import common as base
2from Products.CMFCore.permissions import ViewManagementScreens
3from Products.CMFCore.utils import getToolByName
4
5from starzel.votable_behavior.interfaces import IVoting
6
7
8class Vote(base.ViewletBase):
9
10 vote = None
11 is_manager = None
12
13 def update(self):
14 super(Vote, self).update()
15
16 if self.vote is None:
17 self.vote = IVoting(self.context)
18 if self.is_manager is None:
19 membership_tool = getToolByName(self.context, 'portal_membership')
20 self.is_manager = membership_tool.checkPermission(
21 ViewManagementScreens, self.context)
22
23 def voted(self):
24 return self.vote.already_voted(self.request)
25
26 def average(self):
27 return self.vote.average_vote()
28
29 def has_votes(self):
30 return self.vote.has_votes()
The template#
And extend the template in browser/templates/voting_viewlet.pt
1<tal:snippet omit-tag="">
2 <div class="voting">
3 <div id="current_rating" tal:condition="viewlet/has_votes">
4 The average vote for this talk is <span tal:content="viewlet/average">200</span>
5 </div>
6 <div id="alreadyvoted" class="voting_option">
7 You already voted this talk. Thank you!
8 </div>
9 <div id="notyetvoted" class="voting_option">
10 What do you think of this talk?
11 <div class="votes"><span id="voting_plus">+1</span> <span id="voting_neutral">0</span> <span id="voting_negative">-1</span>
12 </div>
13 </div>
14 <div id="no_ratings" tal:condition="not: viewlet/has_votes">
15 This talk has not been voted yet. Be the first!
16 </div>
17 <div id="delete_votings" tal:condition="viewlet/is_manager">
18 Delete all votes
19 </div>
20 <div id="delete_votings2" class="areyousure warning"
21 tal:condition="viewlet/is_manager"
22 >
23 Are you sure?
24 </div>
25 <a href="#" class="hiddenStructure" id="context_url"
26 tal:attributes="href context/absolute_url"></a>
27 <span id="voted" tal:condition="viewlet/voted"></span>
28 </div>
29 <script type="text/javascript">
30 $(document).ready(function(){
31 starzel_votablebehavior.init_voting_viewlet($(".voting"));
32 });
33 </script>
34</tal:snippet>
We have many small parts, most of which will be hidden by JavaScript unless needed. By providing all this status information in HTML, we can use standard translation tools to translate.
Translating strings in JavaScript requires extra work.
We need some css that we store in static/starzel_votablebehavior.css
1.voting {
2 float: right;
3 border: 1px solid #ddd;
4 background-color: #DDDDDD;
5 padding: 0.5em 1em;
6}
7
8.voting .voting_option {
9 display: none;
10}
11
12.areyousure {
13 display: none;
14}
15
16.voting div.votes span {
17 border: 0 solid #DDDDDD;
18 cursor: pointer;
19 float: left;
20 margin: 0 0.2em;
21 padding: 0 0.5em;
22}
23
24.votes {
25 display: inline;
26 float: right;
27}
28
29.voting #voting_plus {
30 background-color: LimeGreen;
31}
32
33.voting #voting_neutral {
34 background-color: yellow;
35}
36
37.voting #voting_negative {
38 background-color: red;
39}
JavaScript code#
To make it work in the browser, some JavaScript static/starzel_votablebehavior.js
1/*global location: false, window: false, jQuery: false */
2(function ($, starzel_votablebehavior) {
3 "use strict";
4 starzel_votablebehavior.init_voting_viewlet = function (context) {
5 var notyetvoted = context.find("#notyetvoted"),
6 alreadyvoted = context.find("#alreadyvoted"),
7 delete_votings = context.find("#delete_votings"),
8 delete_votings2 = context.find("#delete_votings2");
9
10 if (context.find("#voted").length !== 0) {
11 alreadyvoted.show();
12 } else {
13 notyetvoted.show();
14 }
15
16 function vote(rating) {
17 return function inner_vote() {
18 $.post(context.find("#context_url").attr('href') + '/vote', {
19 rating: rating
20 }, function () {
21 location.reload();
22 });
23 };
24 }
25
26 context.find("#voting_plus").click(vote(1));
27 context.find("#voting_neutral").click(vote(0));
28 context.find("#voting_negative").click(vote(-1));
29
30 delete_votings.click(function () {
31 delete_votings2.toggle();
32 });
33 delete_votings2.click(function () {
34 $.post(context.find("#context_url").attr("href") + "/clearvotes", function () {
35 location.reload();
36 });
37 });
38 };
39}(jQuery, window.starzel_votablebehavior = window.starzel_votablebehavior || {}));
This js code adheres to crockfort jshint rules, so all variables are declared at the beginning of the method. We show and hide quite a few small HTML elements here.
Writing 2 simple view helpers#
Our JavaScript code communicates with our site by calling views that don't exist yet. These Views do not need to render HTML, but should return a valid status. Exceptions set the right status and aren't being shown by JavaScript, so this will suit us fine.
As you might remember, the vote
method might return an exception, if somebody votes twice.
We do not catch this exception. The user will never see this exception.
See also
Catching exceptions contain a gotcha for new developers.
1try:
2 something()
3except:
4 fix_something()
Zope claims some exceptions for itself. It needs them to work correctly.
For example, if two requests try to modify something at the same time, one request will throw an exception, a ConflictError
.
Zope catches the exception, waits for a random amount of time, and tries to process the request again, up to three times. If you catch that exception, you are in trouble, so don't do that. Ever.
As so often, we must extend browser/configure.zcml
:
1...
2
3<browser:page
4 name="vote"
5 for="starzel.votable_behavior.interfaces.IVotable"
6 layer="..interfaces.IVotableLayer"
7 class=".vote.Vote"
8 permission="zope2.View"
9 />
10
11<browser:page
12 name="clearvotes"
13 for="starzel.votable_behavior.interfaces.IVotable"
14 layer="..interfaces.IVotableLayer"
15 class=".vote.ClearVotes"
16 permission="zope2.ViewManagementScreens"
17 />
18
19...
Then we add our simple views into the file browser/vote.py
1from zope.publisher.browser import BrowserPage
2
3from starzel.votable_behavior.interfaces import IVoting
4
5
6class Vote(BrowserPage):
7
8 def __call__(self, rating):
9 voting = IVoting(self.context)
10 voting.vote(rating, self.request)
11 return "success"
12
13
14class ClearVotes(BrowserPage):
15
16 def __call__(self):
17 voting = IVoting(self.context)
18 voting.clear()
19 return "success"
A lot of moving parts have been created. Here is a small overview: