Mejorando el Blog de Odoo

Sobre como implementamos mejoras a nuestro Blog

Ignacio Buioli
- 13/01/2020 - 10 min. de lectura

No tenemos dudas respecto a que Odoo es un gran Framework. Y, a pesar de eso, en donde podría resaltar sobre otros frameworks que le compiten, muchas veces hace agua, especialmente con lo relacionado al website. Pensarán que Odoo no está pensado como un CMS sino más bien como un ERP, y es cierto. Pero en esta época desarrollar sobre dos frameworks sincronizando datos (con toda la labor que eso trae consigo) no es tan rentable en equipos chicos de trabajo (una tendencia cada vez mayor). Para peor, Odoo tiene website integrado, con slides, ecommerce y blog. De este último vengo a escribir, porque como ya se habrán dado cuenta escribimos en el blog desde el año 2016. Y sí, es un blog 100% de Odoo. Debo aclarar que será un posteo largo, pero con mucha información y mucho código si lo que están haciendo es desarrollar su propio Blog personalizado en Odoo.

Para empezar una breve aclaración y es que entre finales de 2016 y mediados de 2018 nuestro Blog funcionaba en un sistema Joomla que tenía miles de opciones de configuración y widgets. Cuando migramos gran parte de nuestros datos a nuestro Odoo tomamos la decisión de emplear el Blog íntegramente en Odoo. La migración fue sencilla, pero nuestra sorpresa fue mayúscula al notar las pocas opciones que tiene por defecto el blog de Odoo. Por lo tanto, hicimos lo que hacemos mejor: desarrollar.

¿Qué tiene un blog de Odoo sin alterar?

El blog de Odoo, así sin más, funciona como una especie de Twitter para la empresa / organización pero de Tweets más largos. Realmente parece tener una función de comunicar y nada más. Tiene un par de opciones como etiquetas, un listado ordenado por año, y alguna cosa más de interés como snippets para usar en otras partes del website (últimos post, etc). Pero en líneas generales se queda solamente en eso, un Blog para simples actualizaciones. Puede ser muy útil al principio, pero cuando se lleva la tarea de publicar semanalmente artículos de interés empieza a quedarse muy corto. Tiene muy pocas opciones, además, en todo lo relacionado a compartir los posteos en redes sociales, el añadido más interesante es poder twittear al resaltar una parte del contenido (un añadido muy simpático pero que carece de utilidad si no se lo complementa con otras cosas).

Fecha en los Posteos

¿Por qué los posteos no tienen fecha? No lo sé, pero así parece estar decidido. Cuando se ingresa a cualquier post particular debajo del título y el subtítulo figura el avatar y el nombre de quien lo escribió (o lo que es lo mismo, del indicado como author_id). Esto siempre y cuando se active la opción, pero en líneas generales estará activado en un principio. Para nosotros es realmente importante que la fecha figure, ya que tenemos una actualización permanente de contenido muy ligada a las fechas. En nuestro caso lo que hicimos fue heredar la vista del blog:

website_blog.blog_post_complete

Y agregar, posicionado luego del nombre, el campo de la fecha de publicación:

<span t-field="blog_post.post_date" t-options='{"widget": "date"}' />

Tiempo de Lectura en los Posteos

Uno de los objetivos de escribir en un blog es ampliar la audiencia y generar contenido de calidad siempre actualizado. Por lo tanto, es deseable ofrecer a los lectores la mayor cantidad de herramientas posibles. En blogs masivos como Medium incluyen una pequeña leyenda arriba de todo, antes de comenzar a leer el posteo, que informa sobre la cantidad de minutos aproximados que llevará la lectura. Esto resulta especialmente útil para un lector, pudiendo administrar mejor su paso por el blog. Por supuesto, Odoo no incluye algo que haga esto de manera automática, por lo tanto nuestra solución fue hacer un campo computado que calcule los minutos utilizando una formula estudiada globalmente. Ya que el promedio global de palabras leídas por minuto es de 200, un cálculo muy simple sería dividir las cantidad total de palabras por 200. La función computada que implementamos es la siguiente:

def _compute_time(self):
    for post in self:
        time = int(len(post.content.split()) / 200)
        if time == 0:
            time = 1
        post.time_read = time

Seguramente no es un cálculo super preciso, pero no pretende serlo. Pretende buscar un valor aproximado de una forma elegante. En este caso se emplea el campo content, el cual no es un string plano sino que tiene código HTML. Por lo tanto, el recuento con la función len() nunca es, exactamente, el número total de palabras real. Siempre será un número levemente mayor. Se la puede complejizar en caso de necesitar el valor exacto, haciendo un replace de todas las tags HTML, pero lo veo un sin sentido. Finalmente, agregamos una condicional para que nunca pueda dar 0 minutos, siempre al menos pone 1 como valor mínimo, y lo imprimimos en la vista de posteo al lado de la fecha.

Acerca del Autor (author_id)

Este feature es un clásico en cualquier blog. Y tiene sentido, los lectores son curiosos, quieren saber quien escribe. En blogs compartidos (como el nuestro) donde no sólo escribimos los desarrolladores sino que a veces colaboran invitados externos resulta muy importante, también para que el propio invitado tenga un incentivo para escribir y aportar. Una pequeña foto y un nombre dicen muy poco para cierto tipo de lector, que busca interpretar a quien decide dedicar tiempo a escribir. En este caso la solución fue crear un campo en el res.partner (que referencia al author_id en el modelo de blog.post de Odoo) llamado abstract. Para usarlo en la vista y, por un tema de permisos, fue necesario hacer un campo llamado author_abstract relacionado con author_id.abstract:

author_abstract = fields.Text(related='author_id.abstract', string='Author Abstract')

De esta manera se puede llamar en una vista heredada del website_blog.blog_post_complete de la siguiente manera:

<p t-field="blog_post.author_abstract" />

En principio será una especie de mini-bio muy básica, pero se le pueden agregar más cosas, como el nombre y la imagen de perfil. en nuestro caso, ya que el campot author_avatar se relaciona con el campo image_small del res.partner, lo que hicimos fue sobre-escribirlo para que se relacione con image_medium y así tener un poco más de resolución de imagen.

Posteos Relacionados

Otro feature muy visto y que Odoo no tiene, un simple cuadro donde figuren tres o cuatro posteos relacionados con el que uno está leyendo. Esto incentiva la lectura de otros posteos, el lector pasa más tiempo en nuestro sitio y adquiere más información relacionada al tema que le interesa. En este caso no se trata de soluciones simples como las anteriores, sino de una combinación de tecnologías: controllers + vistas + javascript (mucho javascript). Lo primero que hay que hacer es crear un controller, en nuestro caso fue así:

@http.route(['/blog/render_related_posts'], type='json', auth='public', website=True) 
def render_related_posts(self, template, id, limit=None):
    post = request.env['blog.post'].search([('id', '=', id)])
    if post.tag_ids:
        posts = request.env['blog.post'].search([('website_published', '=', True),('id', '!=', id),('tag_ids', 'in', post.tag_ids[0].id)], limit=limit)
    else:
        posts = request.env['blog.post'].search([('website_published', '=', True),('id', '!=', id)], limit=limit)
    return request.env.ref(template).render({'posts': posts})

Es algo simple pero tiene su lógica. Para empezar aclaro que la forma en la cual se eligen los posteos relacionados es la más simple posible (al menos de momento), simplemente compara las etiquetas. El nombre de la ruta da igual, lo importante es que sea type=json para acceder con JavaScript, y auth=public para que un usuario no logueado obtenga el dato. La función espera recibir una serie de parámetros, el template será el ID que va a renderizar la data, el id será el ID del posteo en el blog y limit será la cantidad de registros que queremos recibir, el cual por defecto es un None para traernos todo (es una mala idea, lo mejor sería que por defecto traiga un número pequeño, pero el limit se va a forzar desde javascript, así que en este caso no me preocupa). El siguiente paso es crear la vista que va a renderizar la información:

<template id="s_related_list_template">
    <t t-foreach="posts" t-as="p">
        <div class="col-4 media s_latest_posts_post" style="opacity:0.8;">
            <div class="media-body ml-3 pb-2">
                <h5> <a t-attf-href="/blog/#{p.blog_id.id}/post/#{p.id}">
                    <span t-field="p.name" />
                </a> </h5>
                <p t-field="p.teaser" />
                <a class="btn btn-sm btn-primary" t-attf-href="/blog/#{p.blog_id.id}/post/#{p.id}">Seguir leyendo</a>
            </div>
        </div>
    </t>
</template>

En nuestro caso es algo sencillo, posts es un array de objetos blog.post. Hacemos un simple foreach y renderizamos los datos que queramos. No es complejo, pero no es la única vista que vamos a necesitar, eso lo veremos en breve, antes hay un tema importante en JavaScript. Nos disponemos a crear y listar un nuevo archivo JS, y luego de definir el objeto vamos a llamar a los imports:

var core = require('web.core');
var sAnimation = require('website.content.snippets.animation');
var _t = core._t;

No voy a extenderme en todo el JavaScript porque es muy extenso, está basado en código del módulo website_blog de Odoo, así que tampoco es imposible de comprender. Lo importante es esto:

sAnimation.registry.js_get_posts = sAnimation.Class.extend({
    selector : '.js_get_this_posts',
    /*** @override*/
    start: function () {
        var self = this;
        var id = Number(self.$target[0].id);
        var template = 'ctmil_website.s_related_list_template';
        var loading = self.$target.data('loading');this.$target.empty();
        this.$target.attr('contenteditable', 'False');

        var def = $.Deferred();
        this._rpc({route: '/blog/render_related_posts',
        params: {
            template: template,
            limit: 3,
            id: id
        },
        }).then(
        function (posts) {
            var $posts = $(posts).filter('.s_latest_posts_post');
            if (!$posts.length) {
                self.$target.append($('<div/>', {class: 'col-md-6 offset-md-3'}).append($('<div/>', {
                    class: 'alert alert-warning alert-dismissible text-center',text: _t("No blog post was found. Make sure your posts are published."),
                })));
                return;
            }
            if (loading && loading === true) {
                self._showLoading($posts);
            } else {
                self.$target.html($posts);
            }
        }, function (e) {
            if (self.editableMode) {
                self.$target.append($('<p/>', {
                    class: 'text-danger',text: _t("An error occured with this latest posts block.")
                }));
        }).always(def.resolve.bind(def));
         return $.when(this._super.apply(this, arguments), def);
    },
})

Claramente no se trata del código completo, pero tiene lo más importante. Me concentro principalmente en el start ya que tiene lo fundamental. Y acá puede verse el uso del objeto _rpc que se usa para llamar a la ruta que creamos con el controller previamente, y le enviamos los parámetros. El dato más importante es el valor del selector, ya que será la clase de HTML que usaremos para disparar todo este "efecto dominó". El parámetro template es el nombre del template (junto con el nombre del módulo en el cual se encuentra, la típica denominación de Odoo). El limit lo forzamos porque no nos interesa hacerlo dinámico (al menos por ahora) y el id lo vamos a sacar del propio id del div donde se va a imprimir en contenido de post. Parece más complejo de lo que termina siendo. Finalizamos esta implementación heredando website_blog.blog_post_complete y agregando el siguiente contenido:

<xpath expr="//div[@class='o_blog_post_complete o_sharing_links']" position="after">
    <div class="js_get_this_posts jumbotron bg-light row" t-att-id="blog_post.id"></div>
</xpath>

Acá está la clave final, un simple DIV con la clase js_get_this_posts (que es la misma que la del selector, y debe serlo) y un t-att-id que trae el ID del posteo como ID del DIV para poder usarlo en la parte de JS. Es todo un gran entramado, que parece complejo pero no lo es tanto, lo complejo será luego desde el controller hacer una mejor interpretación de posteos relacionados. Tenemos el plan de que no solo filtre por una Tag igual, sino que busque coincidencia con todas, e incluso utilice datos del título o la propia longitud del posteo para poder hacer una mejor selección.

Widget de Posteos más Populares

En sí el blog de Odoo tiene un widget de últimos posteos en el blog, trae los últimos 3. Puede servir en la homepage pero dentro del Blog es una ridiculez. Incluso en pantallas chicas los 3 posteos más recientes se pueden ver sin necesidad de hacer scroll. Mejor sería que muestre los posteos más populares, de esas forma incentiva su lectura. Sería cuestión de modificarlo, nosotros para no tocar el módulo website_blog directamente copiamos los archivos que hacen el widget en cuestión, el más importante es este: 

addons/website_blog/static/src/js/_latest_posts_frontend.js

En este está lo fundamental, es muy similar a lo de Post Relacionados, pero mucho más simple. También requiere un controller, pero la modificación fundamental está en el domain (que puede hacerse desde JS o desde el controller). Nosotros lo hicimos desde el Controller, simplemente agregando un order='visits desc'

request.env['blog.post'].search(domain, limit=limit, order='visits desc')

De esta forma va devolver todos los posteos en orden descendiente ordenado por el campo visits.

Próximas cosas a agregar

Yo en lo personal tengo varias ideas.  Me interesan funciones para compartir los posteos por redes sociales, nada elegante, solo Twitter y Facebook. Actualmente Odoo dispone una caja de "compartir en las redes", pero está al final de cada posteo, y además hay otras APIs que se pueden usar. En otros blogs utilizan un botón de "Save", que depende el uso a veces se guarda como en una Cookie o en un Usuario. Es muy interesante, porque podría utilizarse para usar notificaciones que indiquen que el posteo se encuentra guardado, o incluso con las notificaciones del browser cuando un posteo nuevo se publica. Sin embargo, la opción que más ganas tenemos de implementar es un cuadro de búsqueda. En nuestro caso, con la cantidad de posteos que llevamos, se hace necesario una forma de buscar la información a potenciales lectores. Quien sabe, a lo mejor haremos un módulo para Odoo con las mejoras para el Blog en un futuro cercano.

Acerca de:

Ignacio Buioli

Licenciado en Artes Multimediales. Ha desarrollado numerosos proyectos de Multimedia así como también escrito artículos y traducido textos del mencionado tema. En Moldeo Interactive es Socio y Programador; encargándose, además, de gran parte de las redes y los cursos online.