A Viewlet for the Votable Behavior – Mastering Plone 5 development

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:

../_images/voting_flowchart.png