32. Un viewlet para el comportamiento de votaciones

Viewlet de Votaciones

  • El viewlet para la interfaz IVoteable.

  • La plantilla del viewlet.

  • Agregar jQuery incluyen declaraciones.

  • Guardar el voto en el objeto usando anotaciones.

Anteriormente hemos agregado la lógica que salva los votos en los objetos. Nosotros ahora podemos crear la interfaz de usuario para esto.

Puesto que queremos utilizar la interfaz de usuario en más de una página (no sólo la vista talk-view sino también la vista talk-listing) debemos ponerla en alguna parte.

  • Para manejar el ingreso de datos del usuario nosotros no usamos un formulario pero si links y ajax.

  • La votación en si, es en su efecto manejada por otra vista.

Registramos el viewlet en el archivo browser/configure.zcml, agregando el siguiente código ZCML:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
 <configure xmlns="http://namespaces.zope.org/zope"
     xmlns:browser="http://namespaces.zope.org/browser">

     ...

   <browser:viewlet
     name="voting"
     for="starzel.votable_behavior.interfaces.IVoting"
     manager="plone.app.layout.viewlets.interfaces.IBelowContentTitle"
     layer="..interfaces.IVotableLayer"
     class=".viewlets.Vote"
     template="templates/voting_viewlet.pt"
     permission="zope2.View"
     />

     ....

 </configure>

Nosotros extendemos en el archivo browser/viewlets.py, agregando el siguiente código Python:

1
2
3
4
5
from plone.app.layout.viewlets import common as base


class Vote(base.ViewletBase):
    pass

Esto agregará un viewlet a una ranura debajo del título y esperará una plantilla voting_viewlet.pt en una carpeta browser/templates.

Vamos a agregar el archivo de la plantilla templates/social_viewlet.pt sin ninguna lógica.

1
2
3
4
5
6
7
8
9
 <div class="voting">
     Wanna vote? Write code!
 </div>

 <script type="text/javascript">
   jq(document).ready(function(){
     // please add some jQuery-magic
   });
 </script>
  • Reinicie Plone.

  • Muestre el viewlet.

Escribiendo el código del Viewlet

Actualizar en la clase viewlet que contiene la lógica necesaria en el archivo browser/viewlets.py, con el siguiente código Python:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from plone.app.layout.viewlets import common as base
from Products.CMFCore.permissions import ViewManagementScreens
from Products.CMFCore.utils import getToolByName

from starzel.votable_behavior.interfaces import IVoting


class Vote(base.ViewletBase):

    vote = None
    is_manager = None

    def update(self):
        super(Vote, self).update()

        if self.vote is None:
            self.vote = IVoting(self.context)
        if self.is_manager is None:
            membership_tool = getToolByName(self.context, 'portal_membership')
            self.is_manager = membership_tool.checkPermission(
                ViewManagementScreens, self.context)

    def voted(self):
        return self.vote.already_voted(self.request)

    def average(self):
        return self.vote.average_vote()

    def has_votes(self):
        return self.vote.has_votes()

La plantilla

Y extiende la plantilla en el archivo browser/templates/voting_viewlet.pt, con el siguiente código ZPT:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<tal:snippet omit-tag="">
  <div class="voting">
    <div id="current_rating" tal:condition="viewlet/has_votes">
      The average vote for this talk is <span tal:content="viewlet/average">200</span>
    </div>
    <div id="alreadyvoted" class="voting_option">
      You already voted this talk. Thank you!
    </div>
    <div id="notyetvoted" class="voting_option">
      What do you think of this talk?
      <div class="votes"><span id="voting_plus">+1</span> <span id="voting_neutral">0</span> <span id="voting_negative">-1</span>
      </div>
    </div>
    <div id="no_ratings" tal:condition="not: viewlet/has_votes">
      This talk has not been voted yet. Be the first!
    </div>
    <div id="delete_votings" tal:condition="viewlet/is_manager">
      Delete all votings
    </div>
    <div id="delete_votings2" class="areyousure warning"
         tal:condition="viewlet/is_manager"
         >
      Are you sure?
    </div>
    <a href="#" class="hiddenStructure" id="context_url"
       tal:attributes="href context/absolute_url"></a>
    <span id="voted" tal:condition="viewlet/voted"></span>
  </div>
  <script type="text/javascript">
    $(document).ready(function(){
      starzel_votablebehavior.init_voting_viewlet($(".voting"));
    });
  </script>
</tal:snippet>

Tenemos muchas piezas pequeñas, la mayoría de las cuales serán ocultadas por javascript a menos que sea necesario. Al proporcionar toda esta información de estado en HTML, podemos utilizar herramientas de traducción estándar para traducir. Traducir cadenas en javascript requiere trabajo adicional.

Nosotros necesitamos algunos sentencias CSS que almacenaremos en el archivo static/starzel_votablebehavior.css, con el siguiente código CSS:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
.voting {
    float: right;
    border: 1px solid #ddd;
    background-color: #DDDDDD;
    padding: 0.5em 1em;
}

.voting .voting_option {
    display: None;
}

.areyousure {
    display: None;
}

.voting div.votes span {
    border: 0 solid #DDDDDD;
    cursor: pointer;
    float: left;
    margin: 0 0.2em;
    padding: 0 0.5em;
}

.votes {
    display: inline;
    float: right;
}

.voting #voting_plus {
    background-color: LimeGreen;
}

.voting #voting_neutral {
    background-color: yellow;
}

.voting #voting_negative {
    background-color: red;
}

El código javascript

Para que funcione en el navegador, algunas sentencias Javascript en el archivo static/starzel_votablebehavior.js, con el siguiente código Javascript:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/*global location: false, window: false, jQuery: false */
(function ($, starzel_votablebehavior) {
    "use strict";
    starzel_votablebehavior.init_voting_viewlet = function (context) {
        var notyetvoted = context.find("#notyetvoted"),
            alreadyvoted = context.find("#alreadyvoted"),
            delete_votings = context.find("#delete_votings"),
            delete_votings2 = context.find("#delete_votings2");

        if (context.find("#voted").length !== 0) {
            alreadyvoted.show();
        } else {
            notyetvoted.show();
        }

        function vote(rating) {
            return function inner_vote() {
                $.post(context.find("#context_url").attr('href') + '/vote', {
                    rating: rating
                }, function () {
                    location.reload();
                });
            };
        }

        context.find("#voting_plus").click(vote(1));
        context.find("#voting_neutral").click(vote(0));
        context.find("#voting_negative").click(vote(-1));

        delete_votings.click(function () {
            delete_votings2.toggle();
        });
        delete_votings2.click(function () {
            $.post(context.find("#context_url").attr("href") + "/clearvotes", function () {
                location.reload();
            });
        });
    };
}(jQuery, window.starzel_votablebehavior = window.starzel_votablebehavior || {}));

Este código Javascript se adhiere a las reglas jshint de crockfort, por lo que todas las variables se declaran al principio del método. Mostramos y ocultamos bastantes pequeños elementos html aquí.

Escribiendo 2 simples helpers view

Nuestro código Javascript se comunica con nuestro sitio llamando a las vistas que aún no existen. Estas vistas no necesitan mostrar código html, pero deben devolver un estatus válido. Las excepciones establecen el estado correcto y no están siendo mostradas por Javascript, por lo que nos conviene bien.

Como puede recordar, el método vote puede devolver una excepción, si alguien vota dos veces. No capturamos esta excepción. El usuario nunca verá esta excepción.

Ver también

La captura de excepciones contiene un comportamiento contra-intuitivo para los nuevos desarrolladores.

1
2
3
4
try:
    something()
except:
    fix_something()

Zope reclama algunas excepciones para sí mismos. Eso necesita que funcionen correctamente.

Por ejemplo, si dos solicitudes intentan modificar algo al mismo tiempo, una solicitud lanzará una excepción, a ConflictError.

Zope captura la excepción, espera una cantidad de tiempo aleatoria e intenta procesar la solicitud nuevamente, hasta tres veces. Si capturas esa excepción, estás en problemas, así que no lo hagas. Nunca.

Como tantas veces, debemos extender en el archivo browser/configure.zcml, agregando el siguiente código ZCML:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
...

<browser:page
  name="vote"
  for="starzel.votable_behavior.interfaces.IVotable"
  layer="..interfaces.IVotableLayer"
  class=".vote.Vote"
  permission="zope2.View"
  />

<browser:page
  name="clearvotes"
  for="starzel.votable_behavior.interfaces.IVotable"
  layer="..interfaces.IVotableLayer"
  class=".vote.ClearVotes"
  permission="zope2.ViewManagementScreens"
  />

...

A continuación, agregamos nuestras vistas simples en el archivo browser/vote.py, con el siguiente código Python:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from zope.publisher.browser import BrowserPage

from starzel.votable_behavior.interfaces import IVoting


class Vote(BrowserPage):

    def __call__(self, rating):
        voting = IVoting(self.context)
        voting.vote(rating, self.request)
        return "success"


class ClearVotes(BrowserPage):

    def __call__(self):
        voting = IVoting(self.context)
        voting.clear()
        return "success"

Se han creado muchas partes móviles. Aquí hay un pequeño resumen:

digraph composition {
rankdir=LR;
layout=fdp;
context[label="IVotable object" shape="box" pos="0,0!"];
viewlet[label="Voting Viewlet" pos="3,-1!"];
helperview1[label="Helper View for Voting" pos="3,0!"];
helperview2[label="Helper View for deleting all votes" pos="3,1!"];
js[label="JS Code" shape="box" pos="6,0!"];
viewlet -> context [headlabel="reads" labeldistance="3"]
helperview1 -> context [label="modifies"]
helperview2 -> context [label="modifies"]
js -> helperview1 [label="calls"]
js -> helperview2 [taillabel="calls" labelangle="-10" labeldistance="6"]
viewlet -> js [label="loads"]
js -> viewlet [headlabel="manipulates" labeldistance="8" labelangle="-10"]
}