Tutorial

Como construir un sistema de calificaciones de contenidos en Plone en tan solo unos minutos.

Objectivo

Queremos ofrecer a nuestros visitantes la posibilidad de hacer clic en un botón «Me gusta» en cualquier contenido de Plone, y el total de votos debe mostrarse junto al botón.

Nota

Hay un screencast que cubre los primeros pasos del tutorial de Rapido.

Requisitos previos

Ejecute buildout para implementar Rapido y sus dependencias (consulte Instalación).

Instale el complemento rapido.plone desde la Configuración del sitio de Plone.

Inicializando la aplicación Rapido

Vamos a la Configuración del sitio de Plone, y luego Temas.

Si nuestro tema activo actual no es editable en línea a través de la interfaz web de Plone (es decir, no hay botón «Modificar tema»), primero deberemos crear una copia editable del mismo:

  • haga clic en «Copiar»,
  • introduzca un nombre, por ejemplo «tutorial».
  • marque la casilla «Inmediatamente habilitar nuevo tema».

De lo contrario, simplemente haga clic en el botón «Modificar tema».

Podemos ver nuestra estructura de tema, que contiene archivos CSS, imágenes, HTML y reglas de Diazo.

Para inicializar nuestra aplicación Rapido llamada «rating», necesitamos:

  • crear una carpeta llamada rapido en la raíz del tema,
  • en esta carpeta rapido, cree una carpeta llamada rating.
_images/screen-1.png

La aplicación ya está lista.

Creación del botón «Me gusta»

Las aplicaciones de Rapido están compuestas de bloques. Vamos a crear un bloque que hará que nuestro botón:

  • vaya a la carpeta rating y cree una nueva carpeta denominada blocks,
  • en esta carpeta de blocks, vamos a crear un nuevo bloque llamado rate. Para ello, necesitamos crear 3 archivos:
_images/screen-2.png

El archivo rate.html:

<i>If you like what you read, say it! {like}</i>

Esto nos permite implementar el diseño del bloque. Es un archivo HTML normal, pero puede contener elementos Rapido, entre paréntesis. En nuestro caso, tenemos un elemento, a saber {like}, encargado de representar el botón «Like».

El archivo rate.py

def like(context):
    # nothing for now
    pass

Proporciona la implementación del elemento. Cada elemento del bloque tiene una función Python correspondiente que tiene el mismo id. En nuestro caso, ese es el código que se ejecutará cuando un usuario haga clic en «Like». En este momento, no hace nada, pero lo cambiaremos más tarde.

El archivo rate.yaml:

elements:
    like:
        type: ACTION
        label: Like

Este archivo contiene todos los ajustes necesarios para nuestro bloque. Aquí declaramos que nuestro bloque contiene un elemento denominado like, que es una acción (es decir, se renderizará como un botón), y su etiqueta mostrada es «Like».

Ahora que nuestro bloque está listo, podemos verlo usando la siguiente dirección URL:

_images/screen-3.png

El siguiente paso es incrustar nuestro bloque en nuestras páginas de Plone.

Insertar el bloque en páginas Plone

Para incluir nuestro bloque en algún lugar de Plone, usaremos una regla de Diazo. Abramos nuestro archivo rules.xml en la raíz de nuestro tema y agregue las siguientes líneas:

<after css:content=".documentFirstHeading">
    <include css:content="form" href="/@@rapido/rating/blocks/rate" />
</after>

La directiva include nos permite recuperar una parte del contenido; En nuestro caso, la forma HTML producida por nuestro bloque. Y la directiva after inserta después del título principal en nuestra página.

Por lo tanto, ahora si visitamos cualquier página de nuestro sitio de Plone, vemos nuestro bloque mostrado justo debajo del título.

_images/screen-4.png

Eso es bueno, pero hay un pequeño problema: cuando hacemos clic en el botón «Like», estamos redirigidos al contenido bruto del bloque, y perdemos nuestra página actual de Plone.

Vamos a arreglar eso.

Estando en nuestra página Plone

Si queremos permanecer en nuestra página actual después de enviar nuestro bloque, necesitamos habilitar el modo AJAX.

Para hacer esto, debe cambiar nuestro archivo rate.yaml así:

target: ajax
elements:
    like:
        type: ACTION
        label: Like

Ahora, si hacemos clic en el botón «Like», el bloque se vuelve a cargar dinámicamente y nos quedamos en nuestra página actual.

Contando los votos

Volvamos al archivo rate.py, y nos enfocamos en la implementación de la función like.

Cuando un usuario hace clic en el botón «Like», necesitamos obtener el contenido actual que el usuario votó, comprobar cuántos votos ya tiene y agregar un nuevo voto.

Rapido permite crear registros, por lo que crearemos un registro para cada elemento de contenido, y usaremos la ruta del contenido como un id.

Así que reemplacemos nuestra implementación actual por:

def like(context):
    content_path = context.content.absolute_url_path()
    record = context.app.get_record(content_path)
    if not record:
        record = context.app.create_record(id=content_path)
    total = record.get('total', 0)
    total += 1
    record['total'] = total

context.content devuelve el actual contenido de Plone y absolute_url_path es un método Plone que devuelve la ruta de un objeto Plone.

context.app permite acceder a la actual aplicación Rapido, por lo que podemos fácilmente utilizar la API de Rapido, como create_record o get_record.

Un registro Rapido contiene elementos. El método get(item, default=none) devuelve el valor del elemento solicitado o el valor predeterminado si el elemento no existe.

Mostrando los votos

Ahora somos capaces de almacenar votos, también queremos mostrar el total de votos.

Primero, vamos a cambiar el diseño del bloque en el archivo rate.html:

<p>{display}</p>
<p><i>If you like what you read, say it! {like}</i></p>

Así que ahora tenemos un nuevo elemento display en nuestro bloque.

Debemos declararlo en el archivo rate.yaml:

target: ajax
elements:
    like:
        type: ACTION
        label: Like
    display: BASIC

Y vamos a implementarlo en el archivo rate.py:

def display(context):
    content_path = context.content.absolute_url_path()
    record = context.app.get_record(content_path)
    if not record:
        return ''
    return "&#10084;" * record.get('total', 0)

Obtenemos el registro correspondiente al contenido actual, y devolvemos tantos símbolos de ❤ como votos que hemos almacenado.

_images/screen-5.png

¡Eso es! Nuestra función de clasificación está lista para ser utilizada.

Depuración

Como estamos escribiendo código, nosotros podríamos (vamos a) cometer errores. En este caso, siempre es útil leer los mensajes de error devueltos por el sistema.

También es muy útil poder registrar mensajes de nuestro código, asi entendemos lo que está sucediendo exactamente cuando se ejecuta.

Rapido provee el método context.app.log() que registrará mensajes de cadena o cualquier objeto serializable (diccionarios, arreglos, etc.).

Los mensajes de registro y los mensajes de error están visibles en el registro del servidor (pero es posible que no podamos acceder a él), sino también en la consola javascript de nuestro navegador.

Lo primero que debemos hacer es activar el modo de depuración en nuestra aplicación. Para ello, necesitamos crear un archivo settings.yaml dentro de la carpeta /rapido/rating:

debug: true

Y ahora, cambiemos nuestra función display:

def display(context):
    content_path = context.content.absolute_url_path()
    record = context.app.get_record(content_path)
    if not record:
        return ''
    context.app.log(record.items())
    return "&#10084;" * record.get('total', 0)

Veremos lo siguiente en la consola de nuestro navegador:

_images/debug-1.png

Imaginemos ahora que cometimos un error como olvidar el carácter : al final de la sentencia if:

def display(context):
    content_path = context.content.absolute_url_path()
    record = context.app.get_record(content_path)
    if not record
        return ''
    return "&#10084;" * record.get('total', 0)

Entonces tenemos esto en la consola:

_images/debug-2.png

Listado de los 5 elementos mas votados

También nos gustaría ver los 5 elementos de contenido más votados en la página principal del sitio.

Lo primero que necesitamos es indexar el elemento total.

Declaramos ese modo índice en el archivo rate.yaml:

target: ajax
elements:
    like:
        type: ACTION
        label: Like
    display: BASIC
    total:
        type: NUMBER
        index_type: field

Para indexar los valores almacenados previamente, debemos actualizar el índice de almacenamiento llamando a la siguiente dirección URL:

Y para asegurarnos de que los cambios futuros serán indexados, necesitamos arreglar la función like en el bloque rate: la indexación se dispara cuando llamamos al método save del registro:

def like(context):
    content_path = context.content.absolute_url_path()
    record = context.app.get_record(content_path)
    if not record:
        record = context.app.create_record(id=content_path)
    total = record.get('total', 0)
    total += 1
    record['total'] = total
    record.save(block_id='rate')

Ahora podemos crear un bloque para mostrar los 5 contenidos mas votados:

  • el archivo top5.html:
<h3>Our current Top 5!</h3>
{top}
  • el archivo top5.yaml:
elements:
    top: BASIC
  • el archivo top5.py:
def top(context):
    search = context.app.search("total>0", sort_index="total", reverse=True)[:5]
    html = "<ul>"
    for record in search:
        content = context.api.content.get(path=record["id"])
        html += '<li><a href="%s">%s</a> %d &#10084;</li>' % (
            content.absolute_url(),
            content.title,
            record["total"])
    html += "</ul>"
    return html

El método search nos permite consultar nuestros registros almacenados. Los identificadores de registro son las rutas de contenido, por lo que usando API de Plone (context.api), podemos obtener fácilmente el contenido correspondiente y luego obtener sus direcciones URL y títulos.

Nuestro bloque funciona ahora en la siguiente dirección URL:

Finalmente, tenemos que insertar nuestro bloque en la página de inicio. Eso se hará en rules.xml:

<rules css:if-content=".section-front-page">
    <before css:content=".documentFirstHeading">
        <include css:content="form" href="/@@rapido/rating/blocks/top5" />
    </before>
</rules>
_images/screen-6.png

Creación de una nueva página para reportes

Por ahora, acabamos de añadir trozos pequeños de HTML en las páginas existentes. Pero Rapido también nos permite crear una nueva página (un desarrollador de Plone la nombraría una nueva view o vista).

Supongamos que queremos crear una página de reportes sobre los votos sobre el contenido de una carpeta.

Primero, necesitamos un bloque, el archivo llamado report.html:

<h2>Rating report</h2>
<div id="chart"></div>

Queremos que este bloque sea el contenido principal de una nueva vista.

Necesitamos declarar en un nuevo archivo YAML llamado report.yaml:

view:
    id: show-report
    with_theme: true

Ahora si visitamos por ejemplo:

vemos nuestro bloque como contenido de la página principal.

Ahora necesitamos implementar nuestro contenido de reporte. Podríamos hacerlo con un elemento Rapido como lo hicimos en el bloque Top 5.

Vamos a cambiar nuestro enfoque e implementar una gráfico de pastel bonita utilizando la increíble librería D3js y la API REST de Rapido.

Necesitamos crear un archivo Javascript (report.js) en la carpeta /rapido/rating:

// Source: http://rapidoplone.readthedocs.io/en/latest/tutorial.html#creating-a-new-page-for-reports
/* It is a feature of the RequireJS library
 * (provided with Plone by default) to load
 * our dependencies like:
 * - mockup-utils, which is a Plone internal resource,
 * - D3js (and we load it by passing its remote URL to RequireJS).
 */
require(['mockup-utils', '//d3js.org/d3.v3.min.js'], function(utils, d3) {
    /* Get the Plone getAuthenticator method
     * mockup-utils allows us to get the authenticator token
     * (with the getAuthenticator method), we need it to use
     * the Rapido REST API.
     */
    var authenticator = utils.getAuthenticator();
    // Get the local folders path
    var local_folder_path = location.pathname.split('/@@rapido')[0];
    // Get SVG element from the rapido block html named 'report.html'
    var width = 960,
        height = 500,
        radius = Math.min(width, height) / 2;

    /* d3.js Arc Generator
     * Generates path data for an arc (typically for pie charts).
     */
    var arc = d3.svg.arc()
        .outerRadius(radius - 10)
        .innerRadius(0);

    /* d3.js Pie Chart Generator
     * Generates data from an array of data.
     */
    var pie = d3.layout.pie()
        .sort(null)
        .value(function(d) { return d.value; });

    var svg = d3.select("#chart").append("svg")
        .attr("width", width)
        .attr("height", height)
        .append("g")
        .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");

    // d3.json() calls the Rapido endpoint @@rapido/rating/search (a rest api endpoint)
    d3.json("@@rapido/rating/search")
        // d3.json() puts the authenticator token in the X-Csrf-Token header,
        .header("X-Csrf-Token", authenticator)
        // and d3.json() passes the search query in the request BODY.
        .post(
            JSON.stringify({"query": "total>0"}),
            function(err, results) {
                var data = [];
                var color = d3.scale.linear().domain([0,results.length]).range(["#005880","#9abdd6"]);
                var index = 0;
                results.forEach(function(d) {
                    var label = d.items.id.split('/')[d.items.id.split('/').length - 1];
                    data.push({
                        'i': index,
                        'value': d.items.total,
                        'label': label
                    });
                    index += 1;
                });

                // add arc element
                var g = svg.selectAll(".arc")
                    // call pie() function
                    .data(pie(data))
                    // add g element
                    .enter().append("g")
                    .attr("class", "arc");

                // add path element
                g.append("path")
                    .attr("d", arc)
                    .style("fill", function(d) { return color(d.data.i); });

                // add text element
                g.append("text")
                    .attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
                    .attr("dy", ".35em")
                    .style("text-anchor", "middle")
                    .text(function(d) { return d.data.label; })
                    .style("fill", "white");
            }
        );
});

Ese es un script bastante complejo, y no detallaremos aquí los aspectos relacionados con D3js (es sólo un ejemplo típico para dibujar un gráfico circular tipo pastel); Nos centraremos en la forma en que obtenemos los datos.

Lo primero que debe notar es la función require. Es una característica de la librería RequireJS (provista con Plone por defecto) para cargar nuestras dependencias.

Tenemos 2 dependencias:

  • mockup-utils, que es un recurso interno de Plone,
  • La librería D3js (y lo cargamos pasando su URL remota a RequireJS).

mockup-utils nos permite obtener el token de autenticador (con el método getAuthenticator), lo necesitamos para usar la API REST de Rapido.

Nota

  • RequireJS o mockup-utils no son obligatorios para usar el API REST de Rapido, si estuviéramos fuera de Plone (utilizando Rapido como backend remoto), nosotros deberíamos hacer una llamada a ../@@rapido/rating que devuelve el token en una cabecera HTTP. Solo los utilizamos porque son proporcionados por Plone por defecto, y facilitan nuestro trabajo.
  • En vez de la forma de cargar directamente D3 desde su CDN, podríamos haber puesto el archivo d3.v3.min.js en la carpeta /rapido/rating, y servirlo localmente.

La segunda parte interesante es la llamada al metodo d3.json():

  • ese llama al endpoint @@rapido/rating/search,
  • ese pone el token de autenticador en el encabezado X-Csrf-Token,
  • y pasa la consulta de búsqueda en la solicitud BODY.

Eso es básicamente lo que necesitamos hacer en cualquier framework JS que usamos (aquí usamos D3, pero podría ser un framework generalista como Angular, Backbone, Ember, etc.).

Ahora sólo necesitamos cargar este script desde nuestro bloque:

<h2>Rating report</h2>
<div id="chart"></div>
<script src="++theme++tutorial/rapido/rating/report.js"></script>

Y podemos visitarlo en la siguiente dirección URL:

para ver un gráfico circular de pastel de los votos en todos los tipos de contenido Noticias en la sección del sitio llamada News!!!

_images/screen-7.png

Descargue los códigos fuentes de este tutorial.

Nota

Este archivo .zip se puede importar en el editor de temas, pero no se puede activar como un tema regular, ya que sólo contiene nuestra aplicación Rapido. La aplicación puede utilizarse desde nuestro tema principal añadiendo un archivo rating.lnk en la carpeta rapido nuestro tema actual, que contiene:

tutorial

indicando que la aplicación Rapido denominada rating se almacena en el tema denominado tutorial. Y luego podemos activar nuestras reglas específicas añadiendo:

<after css:content=".documentFirstHeading">
    <include css:content="form" href="/@@rapido/rating/blocks/rate" />
</after>

<rules css:if-content=".section-front-page">
    <before css:content=".documentFirstHeading">
        <include css:content="form" href="/@@rapido/rating/blocks/top5" />
    </before>
</rules>

en nuestro principal archivo rules.xml del tema Diazo.