Noticias Weblogs Código

Arragonán

Semana 325

September 02, 2014 12:15 AM

Esta semana fue bastante productiva, con la mezcla que aún siendo agosto la gente anda a medio gas y que tenía la semana bastante bien organizada (raro en mi :P), hasta me pude escapar una tarde de piscineo. Cosa que se agradecía en una semana calurosa como la pasada.

El jueves tuvimos quedada, un tanto furtiva, de Agile Aragón. Supongo que, de nuevo, por las fechas y porque tampoco se pretendía hacer algo más allá que tomar unas cervezas en petit comité no tuvo mucho quórum; aunque me dio que pensar que a nivel de comunidad quizás deberíamos usar más los canales existentes para comunicar mejor estos eventos un tanto espontáneos y que pueda venir más gente.

Finalmente la propuesta enviada la semana pasada no salió, es algo dentro de lo habitual, pero en esta ocasión se nos comunicó en 2-3 días.

Aprovecho para comentar mi experiencia con esto, por si a alguien le sirve: El tema de aceptación de propuestas se asimila mucho a los procesos de selección de personal. Se queda un poco en el aire y lo normal es que no se comunique que una propuesta ha sido rechazada, como cuando te hacen una entrevista y te dicen que ya te llamaremos. Aunque a diferencia de muchos procesos de selección que pueden llevar bastantes semanas, por norma general, si no se me acepta una propuesta en 1-2 semanas la doy por rechazada y me olvido de ella.

En lo que sí estamos:

  • En mhop estuve en cuestiones de soporte para el resto del equipo, pero nada de desarrollo.
  • En Nort estuve trabajando tanto en la aplicación web grails como en la móvil híbrida con cordova. Trabajando en cambios de frontend metiendo mucho rollo ajax a alguna interfaz, en temas relacionados con integración con facebook y en resolver algunos bugs.
  • No es que trabajara mucho en minchador, pero estuvimos viendo el diseño casi-definitivo de la nueva landing. Poco a poco se ve más cerca el poder hacerla pública.

Buena semana.

» Leer más, comentarios, etc...

Bitácora de Javier Gutiérrez Chamorro (Guti)

Casio FX-82ES Plus

September 01, 2014 11:29 AM

Desde la Casio fx-17, me han gustado las calculadoras, en especial las científicas de Casio. No debemos olvidar que Casio fueron los creadores de la primera calculadora eléctrica en 1957 (14-A), y posteriormente en 1972 de la conocida Mini CM-602, que miniaturizando sus componentes supondría un éxito de ventas en todo el mundo. En mi [...]

» Leer más, comentarios, etc...

Koalite

No subestimes el poder de un log

September 01, 2014 05:06 AM

Desarrollar software sería mucho más fácil (aunque tal vez menos divertido) si todo funcionase siempre. La realidad es que, por muchos tests automátizados que escribas y muchos motivos que tengas para cuidar la calidad del software, antes o después, te encontrarás con problemas de lo más exótico que jamás hubieses pensado que podrían llegar a producirse.

Si tienes suerte, darás con un usuario muy colaborador que te proporcionará una lista exacta de los pasos necesarios para reproducir el problema, podrás reproducirlo en un entorno de desarrollo y, usando toda tu artillería pesada de depuradores, monitores, sniffers y demás, conseguirás solucionarlo.

Si vives en el mundo real, es probable que muchos de esos problemas se produzcan en condiciones extrañas, con usuarios que “no saben que han hecho, pero la aplicación falla”, y sean muy difíciles de diagnosticar (no digamos ya de reproducir en un entorno de desarrollo).

Para estos casos, hay una herramienta que, por su simplicidad, resulta muy útil: un buen log.

Existen muchas formas de obtener información sobre el comportamiento de una aplicación una vez que llega a producción. Por ejemplo, puedes conseguir estadísticas de uso, puedes guardar información de errores o puedes mantener un histórico con las acciones realizadas por los usuarios con fines de auditoría. Todo esto lo puedes hacer de forma “artesanal” o integrándolo con sistemas más potentes y estandarizados como contadores de rendimiento, eventos de windows, syslogs, etc.

Teniendo en cuenta que existen todos estos sistemas que permiten mantener enormes históricos y realizar complejas consultas y análisis, ¿tiene sentido escribir un triste log en un fichero de texto?

El poder de la simplicidad

La ventaja fundamental de un fichero de log frente a sistemas más potentes es su simplicidad.

Generar un fichero de texto plano es fácil desde cualquier lenguaje de programación, no requiere desplegar servicios externos (o usar servicios de terceros) para almacenar la información, se puede consultar con cualquier editor de texto en cualquier dispositivo y se puede compartir de mil formas diferentes, desde un email hasta una memoria USB.

Esto permite que generar un log suponga una inversión inicial muy baja y no tengamos que dedicar mucho esfuerzo a montar una infraestructura más potente hasta que no sea necesaria (vamos, YAGNI y todas esas siglas que molan tanto).

No obstante, con el paso del tiempo, es posible que merezca la pena automatizar ciertos procesos relativos a la obtención de los logs. Por ejemplo, puede ser interesante poder realizar un envío automático de logs desde la aplicación (o con una herramienta externa, por si la aplicación ni siquiere arranca), solicitar remotamente el envío de esos logs para equipos que no están siempre conectados, o al menos ayudar al usuario a preparar un zip con los logs que luego pueda copiar a una memoria USB.

Aprovechar esta simplicidad no quiere decir que dejemos de lado otros mecanismos más potentes. Por suerte, la mayoría de librerías de logging disponibles permiten enviar información a distintos almacenes, y por el mismo precio que generamos nuestro humilde fichero de log podemos ir alimentando una base de datos, el visor de sucesos de windows o un syslog.

Qué información debe contener un buen log

La utilidad de un log va a depender directamente de la calidad de la información que contenga. Si partimos de la base de que nuestro objetivo fundamental con el log es resolver problemas, parece claro que la información que debemos incluir en el log es aquella información que nos gustaría tener cuando ocurra un problema.

El log debe contener información relevante para la aplicación, que nos permita saber qué está ocurriendo en todo momento en la aplicación, por ejemplo, cuando si estuviésemos hablando de un cliente de twitter, un log debería poder mostrar los intentos de enviar un tweet que hace el usuario o el número de tweets descargados cada vez que se conecta al servidor.

Junto a este tipo de información, también puede resultar útil almacenar información del entorno de ejecución: versiones de aplicación, sistema operativo y plataforma (CLR, JRE, etc.), usuario de sistema que ejecuta la aplicación, ruta de ejecución, etc. Además, podemos incluir una monitorización cada cierto tiempo para controlar aspectos como la cantidad de memoria disponible, el nivel de batería (si procede), si hay conexión disponible o no…

Todo esto depende mucho del tipo de aplicación, pero a veces es necesario contar con esta información para tener un cierto contexto que nos ayude a comprender por qué se produjo ese OutOfMemoryException o por qué dejó de responder la resolución DNS.

A la hora de presentar esta información en el log debemos tener en cuenta que debe ser fácilmente legible para un humano (probablemente nosotros) y sería de agradecer que fuese más o menos sencillo buscar cosas en él desde un editor de texto normal, ya sea con subcadenas o con expresiones regulares.

Merece la pena dedicar un poco de tiempo a formatear correctamente los mensajes en el log para que sean homogéneos y realmente nos ayuden a seguir lo que está pasando en la aplicación.

Es especialmente importante registrar en el log la forma en que se desarrollan procesos desatendidos. Volviendo al cliente de twitter, si va a estar actualizando datos en background, es importante registrar esos intentos de actualización para poder diagnosticar problemas cuando el usuario nos diga “no recibo nuevos tweets” y tengamos que decidir si es que se ha colgado la hebra que los actualiza, hemos dejado de recibir notificaciones push, no hay cobertura, el servidor está caído, o realmente no hay ningún tweet que recibir.

Otro punto en el que un buen log puede salvarnos muchas horas de depuración es en la integración con sistemas externos. Tener información del tiempo que tardan en respondernos o la información que se intercambia con ellos (en la medida de lo posible) puede ser vital a la hora de decidir si el problema está en el sistema externo o en nuestra aplicación (sí, nuestras aplicaciones también puedes tener problemas).

Qué información NO pertenece a un log

Hay información que no pertenece a un fichero de log (aunque podamos incluirla en él), sino que forma parte de los requisitos propios de la aplicación. Por ejemplo, si nuestra aplicación debe mantener un histórico de qué usuario modifica qué productos, eso deberá tratarse de forma adecuada en la aplicación y no sólo como entradas en el log.

Podemos tener casos en que necesitamos tener información para realizar una monitorización proactiva del sistema (memoria en uso, peticiones por segundo, etc.). En ese caso esta información deberá estar disponible por otros medios más adecuados que un log, que no deja de ser una herramienta reactiva, es decir, para cuando ya ha ocurrido el problema.

Por último, tampoco debemos incluir en el log toda la información de llamadas a métodos que se realizan en la aplicación. Esto, que es muy tentador cuando uno empieza a jugar con AOP y cosas como PostSharp, en realidad forma parte de una traza, no de un log, y en el log lo más que puede hacer es generar ruido y dificultarnos encontrar la información relevante para el problema que necesitamos resolver.

Conclusiones

Cuando no tenemos la opción de engancharnos a un depurador para ver qué está pasando con una aplicación que da problemas, utilizar un simple fichero de log puede ser extremadamente útil para resolver el problema.

A la hora de generar el log debemos definir claramente qué tipo de información necesitamos guardar en él y hacerlo de forma ordenada para que, cuando sea necesario revisarlo, realmente resulte útil y no sea una amalgama sin sentido llena de ruido e información redundante.

Al final, la última línea de soporte somos nosotros, y cuando llega el momento de solucionar un problema, contar con las herramientas adecuadas puede ser la diferencia entre resolver un problema en un rato o sufrirlo durante días.

No hay posts relacionados.

» Leer más, comentarios, etc...

Blog Bitix

Actualización del libro PlugIn Tapestry

August 29, 2014 10:16 PM

Apache Tapestry

Hace un poco más de una año publiqué el libro PlugIn Tapestry que como su nombre ya adelanta versa sobre el framework Apache Tapestry para el desarrollo de aplicaciones y páginas web con Java. Casi 300 páginas de documentación que describen este framework, como usarlo y va más allá explicando como realizar algunas de las tareas comunes que es necesario hacer en casi todas las aplicaciones web independientemente del framework que elijamos pero en este caso visto desde el punto de vista de Tapestry.

En esta actualización he revisado capítulos como el Inicio rápido para empezar a desarrollar en unos pocos minutos, la sección que trata sobre Plantillas para dar a las páginas un aspecto común, la sección que trata sobre unas Convenciones para los archivos de literales, ampliada la sección de Principios en la que se detalla cuales son las ideas y objetivos por los que se rige el framework, también reescrita la sección de Integración con Spring, una de las librerías más usadas y que facilita el desarrollo enormemente.

Pero además he incluido nuevo contenido que he publicado en diferentes artículos en mi bitácora desde la plublicación original del libro. Estos han sido Como ejecutar Apache Tapestry en un servidor de aplicaciones JBoss o Wildfly, explicada la página Dashboard que nos permite obtener información interesante mientras desarrollamos como que páginas y componentes incluye la aplicación o las estadísticas de Hibernate, explicadas las diferencias del modelo «pull» en el que se basa Tapestry del modelo «push» en el que se basan la mayoría de frameworks orientados a acciones independientemente del lenguaje (Grails, Django, Symfony, ASP.NET MVC), como servir los recursos estáticos desde una red de contenidos (CDN, Content Delivery Network), la anotación Cached que permite evitar invocaciones a métodos devolviendo el resultado obtenido en la primera invocación, como usar Tapestry en una aplicación de forma «standalone» fuera del contexto de un servidor de aplicaciones, la anotación Secure y como añadir más seguridad usando el protocolo seguro HTTPS y finalmente añadido un nuevo formato en el que se puede leer el libro, HTML, además de PDF y los formatos para libro electrónico EPUB y MOBI.

¡Y luego compártelo!

» Leer más, comentarios, etc...

Blog Bitix

Doble barra de botones con Apache Tapestry

August 29, 2014 11:04 AM

Apache Tapestry

Por motivos de usabiliad en algunas páginas se incluyen dos barra de botones, una antes de una larga sección y otra después. El motivo de la doble barra de botones inicial es que no sea necesario hacer «scroll» hasta el final de la página para acceder a los botones y realizar la acción que permitan. Por el contrario, inlcuir la barra de botones al final de la página permite que una vez seleccionados los elementos o revisado el contenido de la sección hacer disponibles las acciones que es probable que se quieran realizar.

Dependiendo del framework web que utilicemos podremos hacerlo de una o varias formas pero de lo que estamos seguros es que copiar y pegar haciendo que el código esté duplicado no es una buena idea por los problemas de mantenimiento que puede suponer. Pero crear una pequeña plantilla o archivo exclusivo para incluir la barra de botones tampoco es la solución ideal, ¿por que? pues porque creando un archivo específico con la botonera puede que nos ocasione un problema que denominaré de «microgestión», es decir, nos obliga a crear un montón de pequeños archivos pequeñitos y hacer referencia o utilizar el mecanismo de inclusión que dispongamos para usar el contenido en unos de otros. En una aplicación grande esta microgestión si nos vemos obligados a ella puede llegar a ser molesta al desarrollar cuanto menos.

¿Como se puede evitar? En el framework Apache Tapestry la doble botonera puede hacerse de varias formas una de ellas es crear un componente pero esto nos obliga a crear un archivo para la clase java y probablemente tambien un archivo de plantilla con el contenido html causando el problema de la microgestión. Pero en Tapestry también podemos hacer uso del componente block que sirve para incluir en él cierto contenido y el componente delegate que sirve para emitir el contenido entre otras cosas de un componente block. Usando estos dos componentes podemos evitar la microgestión definiendo todo en un mismo archivo, además al tener todo en un mismo archivo el código será más fácilmente legible, quedándonos en un ejemplo de la siguiente forma:

<noscript><pre><code>&lt;!DOCTYPE html&gt; &lt;html t:type=&quot;layout&quot; titulo=&quot;Administración de productos&quot; xmlns:t=&quot;http://tapestry.apache.org/schema/tapestry_5_3.xsd&quot; xmlns:p=&quot;tapestry:parameter&quot;&gt; ... &lt;t:block id=&quot;edicionBlock&quot;&gt; &lt;t:remove&gt; En otros frameworks la lógica para obtener el título del bloque según se trate de un alta o una modificación, probablemente se hiciese metiendo lógica en la plantilla de presentación, dado que Tapestry permite llamar a métodos de la clase Java asociada al componente es mejor dejar esa lógica en el código Java de esta manera la plantilla será más sencilla y clara además de aprovecharnos del compilador. labels es un método definido en la página admin.producto que devuelve un mapa. &lt;/t:remove&gt; &lt;h1&gt;${labels.get('titulo')}&lt;/h1&gt; &lt;t:form t:id=&quot;form&quot; context=&quot;producto.id&quot; validate=&quot;producto&quot; clientValidation=&quot;none&quot; class=&quot;well&quot; role=&quot;form&quot;&gt; &lt;t:errors class=&quot;literal:alert alert-danger&quot; /&gt; &lt;t:delegate to=&quot;botonesEdicionBlock&quot;/&gt; &lt;div style=&quot;margin-top: 10px;&quot;&gt; &lt;div class=&quot;form-group&quot;&gt; &lt;t:label for=&quot;nombre&quot; /&gt; &lt;div class=&quot;controls&quot;&gt; &lt;input t:type=&quot;textfield&quot; t:id=&quot;nombre&quot; value=&quot;producto.nombre&quot; size=&quot;100&quot; label=&quot;Nombre&quot; /&gt; &lt;/div&gt; &lt;/div&gt; &lt;div class=&quot;form-group&quot;&gt; &lt;t:label for=&quot;descripcion&quot; /&gt; &lt;div class=&quot;controls&quot;&gt; &lt;input t:type=&quot;textarea&quot; t:id=&quot;descripcion&quot; value=&quot;producto.descripcion&quot; label=&quot;Descripción&quot; /&gt; &lt;/div&gt; &lt;/div&gt; &lt;div class=&quot;form-group&quot;&gt; &lt;t:label for=&quot;cantidad&quot; /&gt; &lt;div class=&quot;controls&quot;&gt; &lt;input t:type=&quot;textfield&quot; t:id=&quot;cantidad&quot; value=&quot;producto.cantidad&quot; size=&quot;4&quot; label=&quot;Cantidad&quot; class=&quot;numeric&quot;/&gt; &lt;/div&gt; &lt;/div&gt; &lt;div class=&quot;form-group&quot;&gt; &lt;t:label for=&quot;fecha&quot; /&gt; &lt;div class=&quot;controls&quot;&gt; &lt;div class=&quot;input-group&quot;&gt; &lt;input t:type=&quot;textfield&quot; t:id=&quot;fecha&quot; type=&quot;date&quot; value=&quot;producto.fecha&quot; label=&quot;Fecha&quot; /&gt; &lt;span class=&quot;input-group-addon&quot;&gt;&lt;span class=&quot;glyphicon glyphicon-calendar&quot;&gt;&lt;/span&gt;&lt;/span&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;t:delegate to=&quot;botonesEdicionBlock&quot;/&gt; &lt;/t:form&gt; &lt;/t:block&gt; &lt;t:block id=&quot;botonesEdicionBlock&quot;&gt; &lt;div class=&quot;btn-toolbar&quot;&gt; &lt;input t:type=&quot;submit&quot; class=&quot;btn btn-primary&quot; value=&quot;prop:labels.get('guardar')&quot; role=&quot;button&quot;/&gt; &lt;t:if test=&quot;producto.id&quot;&gt; &lt;a t:type=&quot;eventlink&quot; event=&quot;eliminar&quot; context=&quot;producto.id&quot; role=&quot;button&quot; class=&quot;btn btn-danger&quot; style=&quot;color: white;&quot;&gt;Eliminar&lt;/a&gt; &lt;/t:if&gt; &lt;input t:type=&quot;submit&quot; class=&quot;btn btn-danger&quot; value=&quot;Cancelar&quot; mode=&quot;cancel&quot; role=&quot;button&quot;/&gt; &lt;/div&gt; &lt;/t:block&gt; &lt;/html&gt;</code></pre></noscript>

En Grails por poner un ejemplo de un framework que no usa el concepto de componentes la forma habitual de hacerlo es usando un g:include y con ello teniendo microgestión. Pero retorciendo un poco (creo) en este caso el uso de Grails podemos emplear la etiqueta g:set para establecer el contenido de la botonera y emitir su contenido dos veces en el gsp.

<noscript><pre><code>... &lt;g:set var=&quot;botonesEdicionBlock&quot;&gt; &lt;div class=&quot;btn-toolbar&quot;&gt; &lt;input type=&quot;submit&quot; class=&quot;btn btn-primary&quot; value=&quot;${message(code: 'guardar')}&quot; role=&quot;button&quot;/&gt; &lt;g:if test=&quot;${producto.id}&quot;&gt; &lt;a action=&quot;eliminar&quot; params=&quot;${[id: producto.id]}&quot; role=&quot;button&quot; class=&quot;btn btn-danger&quot; style=&quot;color: white;&quot;&gt;Eliminar&lt;/a&gt; &lt;/t:if&gt; &lt;input type=&quot;submit&quot; class=&quot;btn btn-danger&quot; value=&quot;{message(code: 'guardar')}&quot; role=&quot;button&quot;/&gt; &lt;/div&gt; &lt;/g:set&gt; &lt;g:form ...&gt; ... ${botonesEdicionBlock} &lt;div style=&quot;margin-top: 10px;&quot;&gt; ... &lt;/div&gt; ${botonesEdicionBlock} &lt;/t:form&gt;</code></pre></noscript>

El código completo de este ejemplo del caso de Tapestry está en un repositorio de GitHub. Si estás interesado en conocer más en profundidad como funciona Tapestry y sus múltiples «killer features», bastantes mucho más importantes que lo explicado en este artículo, puedes descargarte el libro PlugIn Tapestry que he escrito, de forma gratuita, sin registros, y en varios formatos ¿que más puedes pedir?. Y si te interesa el tema puedes suscribirte al canal RSS de esta bitácora para no perderte nada del nuevo contenido que publique, no solo sobre Tapestry, sino también sobre Java, Linux, …

Referencia:
Libro PlugIn Tapestry
Más artículos sobre Apache Tapestry

» Leer más, comentarios, etc...

Picando Código

Reseña: Tortugas Ninja

August 27, 2014 04:00 PM

TMNT

TMNT

Este mes se estrenó una nueva película de las amadas Tortugas Ninja. Fue dirigida por Jonathan Liebesman (si no les suena conocido no se sientan mal), y producida por la empresa de Michael Bay (conocido por destruir franquicias de nuestra infancia).

Con el amigo Hank Scorpio de Multiverseros la vimos en el cine. Aprovechando que fuimos de los pocos en hacer el sacrificio y soportar toda la película, escribimos una reseña para la columna La Reseña Conversada en el sitio de Multiverseros. Pueden leer la nota acá:
TMNT: La Reseña Conversada

Por si mis sutiles comentarios sobre los encargados no les han dado una pista todavía, les comento que la película no me gustó para nada. Vean más al respecto en TMNT: La Reseña Conversada.

Si quieren seguir a nuestras queridas Tortugas Ninja en algún medio actual, les recomiendo la serie de animación en Nickelodeon o alguna de los tantos títulos publicados en cómic. Pueden leer un buen repaso de la historia de las Tortugas Ninja en sus varias interpretaciones, en el artículo Hank y su cajón de juguetes: Las Tortugas Ninja.

» Leer más, comentarios, etc...

Arragonán

Semana 324

August 25, 2014 11:23 PM

Los días post-fiestas de pueblo suelen hacerse cuesta arriba, y la semana pasada no fue una excepción, el lunes fue relajado pero el resto de la semana hubo que ponerse las pilas.

Estuvimos analizando el alcance de un proyecto web de gestión de contenidos y una pequeña parte de gestión documental, para hacer una propuesta tanto para el desarrollo de la parte backend como la de frontend.

Los proyectos actuales:

  • Pequeños cambios en minchador, básicamente mejorar el mailing entre restaurante y comensal.
  • Resolver errores de copy en mhop y alguna que otra tarea de mantenimiento.
  • De nuevo fue Nort lo que se llevó el grueso de la dedicación, estuve implementando varias historias de usuario relacionadas con edición de perfiles, tanto para la versión web como la móvil.
  • Empezar con la web de Biera Solutions, estuve trabajando en el contenido, que siempre es algo que se me hace difícil.

Buena semana

» Leer más, comentarios, etc...

Koalite

Utilizar Chart.js con Knockout.js

August 25, 2014 05:06 AM

Dentro del caótico vibrante mundo de las librerías para Javascript, existen muchas alternativas para mostrar gráficos en pantalla, pero la que más me gusta, por su equilibro entre facilidad de uso y vistosidad de resultados es chart.js.

Micro introducción a Chart.js

En la completa documentación de chart.js podéis encontrar información de sobra para manejarla con soltura, pero para que os hagáis una idea, para mostrar un gráfico de tarta típico sólo hace falta el siguiente código (podéis jugar con él en jsfiddle):

<canvas id="sales-chart" width="400" height="400"></canvas>
var context = document.getElementById('sales-chart').getContext('2d');
var chart = new Chart(context).Pie([
  { label: 'Red', value: 75, color: '#F7464A' },
  { label: 'Not-Red', value: 23, color: '#4D5360' }
]);

Lo único que necesitamos es tener un elemento canvas (como el que usamos para implementar el juego de la vida en Javascript) y crear un objeto Chart sobre él. Este objeto cuenta con distintos métodos para crear cada tipo de gráfico, y cada método recibe dos objetos, uno obligatorio con los datos a mostrar, y otro opcional con la configuración del gráfico (ejes, leyendas, etc.).

Disclaimer

Lo lógico sería haber escrito este post explicando cómo usar Chart.js en una directiva de angularjs que para eso es el framework de moda (con permiso de ReactJS), pero como mi relación con angular está de capa caída y la aplicación donde quería integrarlo usa Knockout.js, ha tocado Knockout.js.

En ningún momento pretendo incentivar el uso de Knockout.js ;-)

Creando un custom binding para Knockout.js

Ya expliqué en su momento cómo crear custom bindings con Knockout.js, así que no me extenderé mucho, pero la idea básica es que podemos añadir nuevas propiedades al objeto ko.bindingHandlers y a través de las funciones init y update indicar cómo debe comportarse nuestro binding.

La estructura es la siguiente:

ko.bindingHandlers.myCustomBinding = {
  init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
    // Función invocada cuando se crea el binding. 
    // Úsala para modificar el DOM, enganchar manejadores de eventos, etc.
  },
  update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
    // Función invocada cada vez que cambia el valor de la propiedad enlazada
    // Úsala para actualizar la información mostrada en el DOM
  }
};

Viendo esto, definir un custom binding no parece muy complicado. Realmente lo más difícil en este caso es decidir cómo queremos construir el API para definir nuestro binding.

Chart.js permite configurar infinidad de parámetros tanto a través de los datos que se le pasan, como a través de las opciones del gráfico, pero para que sea relativamente cómodo de manejar, he optado por crear la siguiente sintaxis:

<canvas width="400" height="400" 
  data-bind="chartType: 'Pie', chartData: quarterSales, chartOptions: salesFormat">
</canvas>

La idea es definir no uno, sino tres bindings para indicar por separado el tipo de gráfico, los datos que queremos mostrar y las opciones del gráfico. Así podremos enlazarlas por separado y, por ejemplo, permitir al usuario mostrar la misma información como gráfico de tarta o de barras, o actualizar la información en tiempo real según se reciben nuevos datos.

El código para implementar esto es el siguiente:

(function(ko, Chart) {

  ko.bindingHandlers.chartType = {
    init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
      if (!allBindings.has('chartData')) {
        throw Error('chartType must be used in conjunction with chartData and (optionally) chartOptions');
      }
    },
    update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
      var ctx = element.getContext('2d'),
        type = ko.unwrap(valueAccessor()),
        data = ko.unwrap(allBindings.get('chartData')),
        options = ko.unwrap(allBindings.get('chartOptions')) || {},
        chart = new Chart(ctx);

      chart[type](data, options);
    }
  };

  ko.bindingHandlers.chartData = {
    init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
      if (!allBindings.has('chartType')) {
        throw Error('chartData must be used in conjunction with chartType and (optionally) chartOptions');
      }
    }
  };

  ko.bindingHandlers.chartOptions = {
    init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
      if (!allBindings.has('chartData') || !allBindings.has('chartType')) {
        throw Error('chartOptions must be used in conjunction with chartType and chartData');
      }
    }
  };

})(ko, Chart);

En realidad, todo el proceso se hace a en el binding chartType, desde el cual podemos acceder a los valores enlazados en chartData y chartOptions a través del objeto allBindings. Los otros dos bindings se limitan a validar que la configuración de bindings es correcta para hacer la cosa un poco más amigable para el usuario.

El código es bastante sencillo, aunque tener que usar ko.unwrap para tratar con los observables de Knockout.js lo ensucia un poco (nadie dijo que Knockout.js fuese bonito). Al final todo se reduce a obtener los valores enlazadas e invocar “dinámicamente” el método del objeto Chart correspondiente al valor enlazado como chartType.

Podéis verlo en funcionamiento (y jugar con él) en este jsfiddle:

El código es tan simple que podéis copiarlo del propio post, pero si lo queréis un poco más limpio, aquí tenéis el código fuente completo.

Una nota final

Hay que tener en cuenta que en el diseño del binding ha primado la comodidad de uso frente a la eficiencia. Tal y como está diseñado, cada vez que se modifica algo del gráfico, éste se redibuja por completo, lo que puede ser un problema si estamos actualizándolo en tiempo real. Chart.js expone métodos optimizados para ese caso de uso y, si te enfrentas a ese escenario, sería bueno echarles un vistazo.

Posts relacionados:

  1. Tutorial jQuery Mobile + Knockout (III): Definiendo el ViewModel con Knockout
  2. Tutorial jQuery Mobile + Knockout (I): Sentando las bases
  3. Tutorial jQuery Mobile + Knockout (y IV): Configurando el Data Binding

» Leer más, comentarios, etc...

Blog Bitix

Forzar el uso del protocolo https en Apache Tapestry

August 22, 2014 09:09 AM

Apache Tapestry

El protocolo seguro https hace que los datos que viajan entre el servidor y el cliente a través de internet estén cifrados de modo que nadie más pueda saber cual es es la información intercambiada ni se pueda alterar sin el conocimiento entre las dos partes. Estas propiedades nos son de interés para ciertas partes de una aplicación o en algunos casos la aplicación entera. ¿Cuales son estos casos? Son aquellos en los que queramos garantizar una mayor seguridad, estos pueden ser para proteger usuarios y contraseñas de autenticación para iniciar sesión, ciertos datos sensibles como datos personales, datos de tarjetas de crédito, … evitando que una tercera parte los obtenga y los utilice para su provecho propio y supongan un problema de seguridad en la aplicación.

Es casi obligatorio forzar a que ciertas páginas de una aplicación o página web funcionen mediante el protocolo seguro https como las páginas de inicio de sesión donde los usuarios se autentican normalmente introduciendo su usuario y contraseña, páginas de compra donde los usuarios introducen los datos de su tarjeta de crédito o algunas secciones de una aplicación como las secciones de las cuentas de los usuarios o un backoffice.

En Apache Tapestry hay varias formas de forzar a que una determinada página use el protocolo seguro de modo que si se accede por el protocolo no seguro http la aplicación obligue a usar https haciendo una redirección. Una de ellas es utilizar la anotación @Secure en las páginas que queramos obligar a usar https. Basta con anotar las clases de las páginas con @Secure y Tapestry automáticamente hará la redirección al protocolo https cuando se acceda con http a la página.

<noscript><pre><code>package es.com.blogspot.elblogdepicodev.plugintapestry.pages; ... @Secure public class Login { ... } </code></pre></noscript>

Probablemente nos interese configurar el puerto y el host que usará Tapestry al hacer la redirección para que coincidan con el usado en el servidor al que accede el usuario, sobre todo si en la aplicación usamos un servidor web proxy como apache, lighttpd o nginx delante del servidor de aplicaciones donde realmente se ejecuta la aplicación web. El puerto seguro del protocolo https predeterminado es 443 pero en el servidor de aplicaciones tomcat por defecto es 8443. Esto en tapestry lo indicamos configurando con ciertos símbolos.

<noscript><pre><code>package es.com.blogspot.elblogdepicodev.plugintapestry.services; ... public class AppModule { public static void contributeApplicationDefaults(MappedConfiguration&lt;String, Object&gt; configuration) { ... configuration.add(SymbolConstants.SECURE_ENABLED, true); configuration.add(SymbolConstants.HOSTPORT, 8080); configuration.add(SymbolConstants.HOSTPORT_SECURE, 8443); ... } ... }</code></pre></noscript>

Para probar mientras desarrollamos, al menos en nuestro equipo, que la redirección se hace correctamente empleando el plugin de gradle para tomcat podemos hacer que el servidor de desarrollo se inicie con el puerto https disponible. Para usar https se necesita un certificado digital que el plugin de gradle para tomcat se encarga de generar al iniciar la aplicación, aunque sea autofirmado y el navegador alerte que no lo reconoce como firmado un una autoridad en la que confíe, si lo aceptamos podemos acceder a la aplicación sin más problema. Usando gradle la configuración que podemos emplear es:

<noscript><pre><code>... buildscript { repositories { mavenCentral() jcenter() } dependencies { classpath 'org.gradle.api.plugins:gradle-tomcat-plugin:1.2.4' } } ... tomcat { httpPort = 8080 httpsPort = 8443 enableSSL = true } ...</code></pre></noscript>

La anotación @Secure en Tapestry es suficiente pero podemos hacer lo mismo empleando Shiro. Integrando Shiro con Tapestry nos permite realizar autenticación y autorización, pero además empleando Shiro también podemos obligar a usar el protocolo https del mismo modo que lo hacemos con la anotación Secure. Cualquiera de las dos formas es perfectamente válida y depende más de cual prefiramos. Con la anotación @Secure deberemos anotar cada página, con Shiro podemos tener centralizado en un único punto en que páginas requerimos https. Con Shiro la configuración se hace con una contribución al servicio SecurityConfiguration y usando el método contributeSecurityConfiguration del módulo y la clase SecurityFilterChainFactory y su método ssl(). Un ejemplo es el siguiente:

<noscript><pre><code>package es.com.blogspot.elblogdepicodev.plugintapestry.services; ... public class AppModule { .... public static void contributeSecurityConfiguration(Configuration&lt;SecurityFilterChain&gt; configuration, SecurityFilterChainFactory factory) { configuration.add(factory.createChain(&quot;/admin/**&quot;).add(factory.authc()).add(factory.ssl()).build()); } .... } </code></pre></noscript>

En cualquiera de los dos casos mostrados en este ejemplo se obliga a usar https en la página de login:

El ejemplo completo puede encontrarse en la aplicación del libro PlugIn Tapestry que puedes descargar y probar de mi repositorio de GitHub. Por supuesto también puedes descargar el libro PlugIn Tapestry si quieres conocer de forma más profunda cuales son las características y como funciona Apache Tapestry.

Referencia:
Configurar SSL en un servidor Tomcat, JBoss, WildFly, Lighttpd, nginx o Apache
Generar y convertir claves y certificados con OpenSSL
Libro PlugIn Tapestry

» Leer más, comentarios, etc...

Arragonán

Semana 323

August 18, 2014 10:13 PM

Como todos los meses de agosto, que cantaban los Ixo Rai, llega la semana del puente del 15 de agosto donde son fiestas en muchos pueblos, entre ellos en el mío. Y claro, como las fiestas del pueblo de uno no hay, hay que estar y por eso me tomé festiva la segunda mitad de la semana.

Calle Estaño

Antes de entrar en la realidad paralela que son unas fiestas de pueblo, saqué ratos para empezar a mover la organización de una charla de CachiruloValley a inicios de septiembre. También saqué un rato para pegarle un vistazo a oDesk y completar un poco mi perfil, por ahora muy poco tiempo y sigo sin tener opinión de qué tal es como fuente de proyectos, pero conozco a varios freelancers que les funcionan bastante bien estos servicios de intermediación.

En cuanto a proyectos, algún mail respecto a otros, pero en realidad sólo le di una dedicación seria a Nort:

  • Estuve trabajando tanto en la aplicación móvil, puliendo y refactorizando para hacer el merge de la rama, como en la aplicación web comenzando con una nueva feature. Además la semana pasada tocaba hacer demo al cliente.

Buena semana.

» Leer más, comentarios, etc...

Ingenieria de Software / Software Engineering / Project Management / Business Process Management

Tracking Projects

August 18, 2014 08:34 PM

Es muy fácil que como PM nos perdamos en el día a día, en lo urgente y no en lo importante pero es vital que los PMs den un seguimiento adecuado a sus proyectos a través de sus principales métricas como son los riesgos. Aquí un buen artículo con algunos tips.


» Leer más, comentarios, etc...

Picando Código

RubyConf Argentina 2014 – Entradas Pajarito y Convocatoria a charlas

August 13, 2014 03:22 PM

RubyConf ArgentinaYa están a la venta las entradas Pajarito ($ 650 pesos argentinos) de una de las mejores conferencias de Ruby del universo conocido: RubyConf Argentina. La conferencia se realiza el 24 y 25 de Octubre en el Centro Cultural Konex en Buenos Aires.

RubyConf Argentina es uno de los mayores eventos de sudamérica sobre desarrollo de software, donde más de 450 desarrolladores se reúnen para aprender, discutir y compartir ideas sobre software libre, web, aplicaciones móviles, seguridad, robótica, y mucho más.

La conferencia reune rubistas y no rubistas de todas partes del mundo, y es una de las puertas de entrada más directas a la cariñosa y cálida comunidad Ruby de América Latina. Va a ser la tercera edición a la que voy, y desde un principio la disfruté como si fuera mi navidad. Recomendada ampliamente.

Como comentaba más arriba están disponibles las entradas a precios promocionales que pueden ir comprando acá. Por otro lado, está abierto el llamado a charlas, así que si tienen ganas de presentar algo para hablar en la conferencia, es su oportunidad.

Uruguayos interesados en cruzar el charco para asistir a la conferencia somos varios. Desde la comunidad de rubistas estamos alentando a más gente a unirse a nosotros y sumarse a la ida en masa al Centro Cultural Konex. De repente conseguimos algun descuento en transporte y alojamiento al ser muchos. Así que si consideraban ir a RubyConf Argentina pero tenían dudas, ya no hay excusas, contáctennos, súmense al meetup, y vénganse con nosotros a RubyConf Argentina \o/

Office Space

 

» Leer más, comentarios, etc...

Arragonán

Semanas 321 y 322

August 12, 2014 05:26 PM

No, no es que haya estado de vacaciones, que por aquí aún se van a hacer esperar al menos un par de meses; aunque algún día suelto caerá, habiendo compromisos ya se sabe como es la vida del autónomo. Y ahí va nueva retro, quincenal otra vez.

Esta vez empecé en Noja hace 2 semanas, viaje para trabajar en la conceptualización de un nuevo proyecto y definir el alcance de un pequeño MVP inicial, estuve asimilando lo que hace una herramienta offline para pasar parte de su funcionamiento al online. Por el momento no puedo decir gran cosa de la temática, como mínimo hasta Septiembre.

Después estuve en Bilbao un par de días, trabajando en las oficinas de mhop y en las de init. Aprovechando también para ver gente, al ser verano estaba claro que iba a ser más complicado encontrarse con muchos; pero aún nos juntamos unos cuantos del gremio a tomar unos zuritos y unos pintxos.

A la vuelta a Zaragoza y sin mucho descanso, como todos los últimos jueves del mes, había zaragozarb, en esta ocasión con una charla/resumen del Rails Open Space que organizó AspGems. Y el finde estuve de visita en mi pueblo, que necesitaba recuperar tras una semana bastante agotadora… aunque terminé hablando de posibles proyectos.

La siguiente semana fue bastante más tranquila, que los viajes me descolocan bastante, ya no estoy acostumbrado de ir de aquí para allá. Saqué tiempo para pasarme a trabajar un rato por por The Garage Of Code, al fin, que hacía mucho que tenía pendiente pasar a visitar su oficina y asistí a las GeeksTalks de este mes.

Sobre proyectos actuales, aparte del que estuvimos conceptualizando:

  • En mhop trabajé en resolver un par de bugs, implementado algunas pequeñas funcionalidades demandadas desde gestión de contenidos y estuve haciendo modificaciones en la gestión de productos por parte de los diseñadores.
  • Estuvimos viendo los cambios en la nueva landing de minchador y estuve hablando con algunos restaurantes. Desde luego que son malas fechas para los restaurantes con los que he hablado: o están de temporada alta o están cerrados/van a cerrar en Agosto. Que no están por la labor, vamos.
  • El grueso de mi dedicación fue de nuevo para Nort. La mayor parte trabajando en la aplicación móvil, principalmente para acceder a varias características nativas y teniendo que usar varios plugins de Apache Cordova para ello. También me tocó hacer algunos pequeños cambios en la aplicación Grails para ajustar algunas cosas del backend.
  • Dediqué algo de tiempo en sentarme a trabajar en la web de Biera Solutions. Empezar a definir objetivos y hacer algunos bocetos en papel para trabajar algunas ideas.

Buena semana.

» Leer más, comentarios, etc...

Bitácora de Javier Gutiérrez Chamorro (Guti)

PhpStorm

August 10, 2014 08:14 AM

En contra de lo que su nombre parece indicar, PhpStorm, no es solamente un entorno de desarrollo PHP, sino que además soporta desarrollo web en general. Es decir, combina las WebStorm con funcionalidades específicas para PHP. Parte de la misma base de WebStorm, por lo que mucha de las cosas que ya viéramos se siguen [...]

» Leer más, comentarios, etc...

Cuaderno de software

¿Scrum o Kanban?

August 08, 2014 06:23 AM

TL; DR: Scrum y Kanban. Se complementan.

Habitualmente cuando participo en una conversación donde se afirman cosas como “yo prefiero Scrum”, pues yo prefiero “Kanban” lo que realmente se suele estar diciendo es “a mí me gusta hacer sprints”, “pues yo prefiero no hacerlos”. Si rascas un poco, resulta que el “equipo kanban” hace retrospectivas y el “equipo scrum” tiene un panel visualizando el Value Stream Map.

Según voy practicando y profundizando en Scrum, Kanban o lo que sea, voy cambiando mi visión sobre en qué consiste exactamente la técnica. Esto me ocurre a todos los niveles y todo el rato. Y me seguirá ocurriendo hasta el fin de los tiempos. En concreto, ahora mismo de Scrum y Kanban pienso lo siguiente:

kanban

scrum

Si eres de los que todavía mantiene peleas internas(o externas) sobre si aplicar Scrum o aplicar Kanban porque piensas que son diferentes sabores del agilismo, que sepas que desde mi punto de vista son totalmente combinables. Sobre como se relacionan o si son combinables, por supuesto, hay mucho escrito:

 

 

» Leer más, comentarios, etc...

Picando Código

Por Mi Barrio – Aplicación para mejorar Montevideo de un clic a la vez

August 07, 2014 04:00 PM

Persiguiendo su objetivo de hacer del mundo un lugar mejor, el grupo de super héroes conocido como Los Veng… D. A. T. A. presentó la aplicación Por Mi Barrio, aplicación basada en el software libre Fix My Street de My Society.

¿Recuerdan todas esas veces que gritaron a los cuatro vientos al encontrar un pozo enorme andando en bicicleta o un caño roto desperdiciando agua a litros o un semáforo que estuvo sin funcionar durante días? Por Mi Barrio es la aplicación que pone al alcance de un clic el poder de hacer algo productivo con esa queja al viento.

Por Mi Barrio

Por Mi Barrio

Del sitio web de D.A.T.A.:

Por Mi Barrio, una plataforma que permitirá a las personas que viven en Montevideo enviar reportes sobre daños, desperfectos, vandalismo y otros problemas de nuestra ciudad desde su computadora o celular.

Es decir, cuando encuentres un problema en tu barrio (un pozo en la calle, problemas de arbolado o de iluminación, contenedores de basura rotos, etc.) vas a poder entrar en pormibarrio.uy desde tu computadora (ceibalitas incluidas) o celular y reportarlo. Para hacer esto, sólo tenés que localizar el problema en un mapa. También se pueden subir fotos y comentarios.

Cuando vuelvas a entrar a la página web vas a poder ver tu reporte marcado en un mapa, así como los de los/as demás usuarios/as. También podrás ver los reportes que la Intendencia de Montevideo (IM) marcó como solucionados y los que todavía son problemas para el barrio.

Por Mi Barrio se conecta al Sistema Único de Reclamos de la IM, por lo que se asegura que los reportes denunciados van a llegar a la división correspondiente de la comuna y permite recibir notificaciones sobre respuestas de la IM. ¿Se dan cuenta el poder que le brinda a los ciudadanos? Al reportar los incidentes, los ciudadanos obtienen total transparencia en el progreso del reporte, teniendo visibilidad sobre el trabajo de su gobierno local. Con herramientas como esta, no hay excusas para no involucrarse y formar parte de la sociedad civil.

El proyecto es parte de una iniciativa a mayor escala, así que probablemente habrán novedades entorno al proyecto y se irán viendo proyectos similares. Cuanto más gente lo use, mejor.

Pueden mantenerse al tanto de las noticias de DATA en su sitio web o su cuenta de Twitter.

» Leer más, comentarios, etc...

Picando Código

Middleman: Generador de sitios web estáticos

August 06, 2014 04:00 PM

fernandobriano.com

fernandobriano.com

Vengo usando mi sitio personal como “tarjeta de presentación” desde hace un buen tiempo, para dirigir potenciales relaciones laborales a ese sitio. Hace mucho que venía con ganas de actualizarlo así que volví a arrancar de cero (por tercera o cuarta vez).

El contenido es bastante simple, debía ser un sitio que mostrara parte de mi trabajo y experiencia de manera sencilla y directa. No inclui mi CV. Tengo perfiles en LinkedIn y StackOverflow Careers, por lo que mostrar un CV online implica repetir información que ya está en otros lugares. Con cada cambio debería mantener la información actualizada en varios sitios. Así que agregué los links.

También agregué el link a GitHub, donde se puede ver gran parte del trabajo Open Source que he hecho, y otros perfiles online como Lanyrd o Speakerdeck. Las empresas se están acostumbrando a  que pueden encontrar mucha información de un programador online sin tener que pedir específicamente un archivo PDF con la especificación técnica de la persona.

Sería interesante interactuar con la API de LinkedIn o StackOverflow Careers (si la tiene), para tener un CV actualizado “dinámicamente”. Es algo a pensar, junto a la cantidad indeterminada de veces que pienso en borrar mi perfil de LinkedIn en el día. No me aporta mucho más que Spam de gente que no conozco y con la que nunca tuve relación laboral agregándome con el mensaje por defecto de “Me gustaría agregarte a mi red profesional” o algo así. En un momento pensé empezar a responder a cada una de las “invitaciones” con un simple: “¿Por qué?”, pero seguro encuentro cosas mejores que hacer con mi tiempo.

Desde la parte técnica, aprovecho cada reboot de mi sitio personal para usar al menos una tecnología que no conociera de antes. Mi caso de uso era bastante sencillo:

  • HAML para el HTML (por preferencia personal nomás)
  • Markdown para el contenido (ídem anterior)
  • Contenido internacionalizado para Inglés y Español
  • No voy a actualizar el sitio tan seguido ni tengo tanta información, un CMS o similar es demasiado
  • Tiene que ser rápido – El CSS y (en caso de usarlo) JavaScript debe ser lo mínimo necesario.

Tecnología

Vamos a las decisiones tecnológicas que tomé y el por qué. Para empezar, decidí que iba a usar Cuba y me iba a armar un “mini-cms” como para mantener el sitio actualizado cuando quiera agregar alguna cosa nueva. El sitio era una aplicación Sinatra alojada en Heroku. No usaba base de datos, al ser poco el contenido se encontraba en archivos de texto (yml). Así que empezar de cero con Cuba no iba a cambiar mucho el tema de hosting, podía usar Postgres o alguna otra cosa en Heroku.

A mitad de camino volví a pensar en generadores de sitios estáticos. Ya había hecho algunas pruebas antes con Jekyll, y empecé a leer sobre Middleman. Navegando un poco por la documentación, me decidí a usarlo. Al producir páginas estáticas, podía alojar el sitio en GitHub Pages en vez de Heroku. De esta forma, dependo de un repo menos.

ruby-middleman

Templates

Middleman soporta templates Slim, ERB, HAML y más. También trae soporte para Markdown con distintos motores, así que resolvía dos de las cosas que quería en el sitio. Trae también soporte para partials y Template Helpers que nos facilitan algunas tareas y definir helpers propios es bastante sencillo. En mi caso usé los helpers propios para incluir algunos fragmentos de código Ruby que tenía por ahí para recuperar información de distintos sitios: RubyGems, GitHub y WordPress.

Esto genera una lista “dinámica” obteniendo información de cosas que he publicado (gemas, plugins, repos) y lo muestra en el sitio.

Contenido

Haml es malo para escribir contenido, y prefiero usar Markdown siempre que puedo. La idea era combinar estas dos cosas con la internacionalización. Así que el contenido se encuentra en archivos yml correspondientes a los locales ‘es’ y ‘en’ para español e inglés respectivamente. Al usar i18n, llamamos a los textos con el método t(:clave_del_texto). El siguiente paso era parsear el texto como markdown, por lo que hice un helper parecido llamado text, con el cual renderizo como Markdown lo que me devuelve el método t:

def text(string)
  markdown = Redcarpet::Markdown.new(
                                     Redcarpet::Render::HTML,
                                     autolink: true,
                                     no_intra_emphasis: true,
                                     lax_spacing: true)
  markdown.render(t(string))
end

CSS y JavaScript

El CSS es muy sencillo, menos de 200 líneas aunque inclui el framework SimpleGrid para las grillas y diseño responsivo. Otras veces había usado PureCSS, pero decidí usar algo distinto esta vez. También agregué los íconos de Octicons y Genericons para darle algo más de vida al sitio.

Middleman incluye Sprockets, un sistema de empaquetamiento de recursos (CSS, JavaScript, CoffeeScript). Permite incluir otros archivos en uno, empaquetándolos y minificándolos (opcionalmente) a un solo archivo. De esta forma, puedo incluir simplegrid.css, octicons.css y genericons.css en mi archivo css principal con las siguientes sentencias:

/*
 *= require _simplegrid
 *= require _octicons.css
 *= require _genericons.css
 */

Al agregar el guión bajo antes del nombre de cada archivo, estoy pidiendo que los contenidos de ese css sean concatenados al contenido de mi css para tener todo en un mismo archivo.

Lo mismo se puede hacer con JavaScript. Lo bueno de esto es que a pesar de tener muy poco código, puedo organizarlo en archivos distintos. Y nada de esto afecta al rendimiento final del sitio ya que todo el contenido se va a servir como recursos estáticos generados previamente.

Publicación en GitHub Pages

Una parte que le falta al código es la generación automática de los archivos estáticos de manera periódica. Por ahora estoy usando la gema middleman-gh-pages desarrollada por algunos ex Neo y otros desarrolladores de la comunidad. La funcionalidad básica que provee la gema, es una tarea Rake para poder publicar a la branch gh-pages en GitHub con un único comando:

$ rake publish

Conclusión

Llegué a un proceso de actualización bastante dinámico y el resultado con un sitio estático alojado en GitHub Pages es suficientemente bueno. Cumple su cometido de ser un centro de enlaces a distintos recursos que permitirían a potenciales colegas o contratadores conocer un poco sobre mi perfil laboral.

El resultado final está en fernandobriano.com y el código fuente en GitHub. Cosas para mejorar siempre hay, ya habrá tiempo para eso :)

 

» Leer más, comentarios, etc...

Cuaderno de software

El problema al escribir

August 06, 2014 06:20 AM

Lo hablaba ayer con David:

  • mientras no sé, es cuando tengo el cerebro a tope y me apetece escribir. Entonces escribo sobre lo que no sé.
  • cuando ya sé, no me da ninguna vidilla escribir. No escribo sobre lo que realmente sé.
  • si escribo cosas complicadas y sesudas, no explico los conceptos en los que baso, y no se entiende.
  • si escribo sobre los conceptos básicos, siento que el post es demasiado básico como para ser útil….

Y así, todo el día… :-P

Mi conclusión es que prefiero escribir, aunque sea artículos tontainas como éste, pero intentar mantener un flujo continuo de artículos que me ayuden a ir rumiando ideas e ir contrastándolas con la gente…

» Leer más, comentarios, etc...

Poesía Binaria

Deadlock, bloqueo mutuo, abrazo mortal o cómo colgar un proceso con esperas infinitas. ☠ ☠ ☠

August 05, 2014 08:49 AM


Es un ejercicio típico, pero que casi siempre se ve teóricamente, pero mira, vamos a traerlo a la práctica. Lo vamos a implementar con semáforos. Como vemos aquí y aquí, los semáforos, entre otras cosas nos servirán para bloquear un proceso o un thread que trata de acceder a un recurso crítico que está siendo usado por otro proceso, un ejemplo típico es un baño público, cuando la puerta no está bloqueada, pasas, la bloqueas, haces lo que tengas que hacer allí, y luego desbloqueas la puerta y te vas. Aquí todo funciona parecido, cuando tienes que acceder a un recurso, miras si el semáforo está abierto, y si es así, lo cierras, utilizas ese recurso y cuando terminas lo abres.
Pero claro, podemos crear todos los semáforos que queramos y tenemos total libertad para hacer que los procesos esperen que los semáforos estén abiertos o los abran, por lo que podemos crear multitud de situaciones.

Pero claro, ¿qué puede pasar si tenemos tres procesos (P1, P2 y P3) y se da el caso en el que P1 esté esperando a P2, P2 esté esperando a P3 y P3 esté esperando a P1? Pues nada, estaremos esperando indefinidamente.
El caso típico es el siguiente:

  • Tenemos dos procesos (P1 y P2)
  • Tenemos dos recursos (R1 y R2) de acceso exclusivo (sólo pueden ser accedidas por un proceso cada vez)
  • P1 quiere acceder a R1 por lo tanto cierra su semáforo
  • P2 quiere acceder a R2 por lo tanto cierra su semáforo
  • P1 quiere acceder también a R2 por lo que espera a que su semáforo esté abierto
  • P2 quiere acceder también a R1 por lo que espera a que su semáforo esté abierto

Como P1 está esperando a que P2 libere el semáforo de R2 y P2 está esperando a que P1 libere el semáforo de R1, los dos se van a quedar indefinidamente así.

Si queremos comprobarlo:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <sys/mman.h>
#include <semaphore.h>
#include <string.h>

int main()
{
  sem_t *sem1 = mmap(NULL, sizeof(sem_t), PROT_READ | PROT_WRITE,
             MAP_SHARED | MAP_ANONYMOUS, -1, 0);
  sem_t *sem2 = mmap(NULL, sizeof(sem_t), PROT_READ | PROT_WRITE,
             MAP_SHARED | MAP_ANONYMOUS, -1, 0);

  int child;

  sem_init (sem1, 1, 1);
  sem_init (sem2, 1, 1);

  child = fork();
  if (child==-1)
    exit(1);
  else if (child==0)
    {
      while(1)
    {
      printf ("[%d] Child waits for sem1...\n", getpid());
      sem_wait(sem1);
      printf ("[%d] Child passes sem1.\n", getpid());
      printf ("[%d] Child waits for sem2...\n", getpid());
      sem_wait(sem2);
      printf ("[%d] Child passes sem2.\n", getpid());
      usleep(100);
      printf ("[%d] Child posts sem2\n", getpid());
      sem_post(sem2);
      printf ("[%d] Child posts sem1\n", getpid());
      sem_post(sem1);
    }
      exit(2);
    }
  else
    {
      while(1)
    {
      printf ("[%d] Main waits for sem2...\n", getpid());
      sem_wait(sem2);
      printf ("[%d] Main passes sem2.\n", getpid());
      printf ("[%d] Main waits for sem1...\n", getpid());
      sem_wait(sem1);
      printf ("[%d] Main passes sem1.\n", getpid());
      usleep(100);
      printf ("[%d] Main posts sem1\n", getpid());
      sem_post(sem1);
      printf ("[%d] Main posts sem2\n", getpid());
      sem_post(sem2);
    }
    }
  while (wait(NULL)>=0);

  munmap(sem1, sizeof(sem_t));
  munmap(sem2, sizeof(sem_t));

  return 0;
}

Si compilamos (con pthread: gcc -o deadlock deadlock.c -lpthread) veremos algo como esto:

$ ./deadlock
[30643] Main waits for sem2…
[30643] Main passes sem2.
[30643] Main waits for sem1…
[30643] Main passes sem1.
[30644] Child waits for sem1…
[30643] Main posts sem1
[30643] Main posts sem2
[30643] Main waits for sem2…
[30643] Main passes sem2.
[30644] Child passes sem1.
[30643] Main waits for sem1…
[30644] Child waits for sem2…

También es verdad que no siempre es tan rápido, a veces pueden darse muchas iteraciones hasta que se produce el bloqueo, otras veces a la primera iteración se produce, depende de cómo de rápidos sean los procesos, pero tarde o temprano se va a dar. Para más información sobre esto, tenemos la página de Wikipedia.

Para dar algo de teoría, en este ejemplo se han cumplido las 4 condiciones necesarias (tienen que cumplirse todas ellas) de Coffman:

  • Exclusión mutua: Los recursos R1 y R2 son exclusivos, sólo pueden ser accedidos por un proceso cada vez
  • Retención y espera: P1 ha adquirido un recurso R1 y lo retiene mientras espera a que R2 (adquirido por P2) se libere
  • No expropiación: P1, por ejemplo, no puede quitarle R2 a P2 ni al revés.
  • Espera circular: P1 está esperando a P2 y P2 espera a P1.

¿Cómo lo arreglamos?
Aquí podemos buscar tantas soluciones como nos dé la imaginación, pero vamos a proponer algunas más típicas:

Guardar el orden de reserva de recursos

Si siempre vamos a utilizar R1 y R2, los pedimos siempre en el mismo orden:

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
40
...
  child = fork();
  if (child==-1)
    exit(1);
  else if (child==0)
    {
      while(1)
    {
      printf ("[%d] Child waits for sem1...\n", getpid());
      sem_wait(sem1);
      printf ("[%d] Child passes sem1.\n", getpid());
      printf ("[%d] Child waits for sem2...\n", getpid());
      sem_wait(sem2);
      printf ("[%d] Child passes sem2.\n", getpid());
      usleep(100);
      printf ("[%d] Child posts sem2\n", getpid());
      sem_post(sem2);
      printf ("[%d] Child posts sem1\n", getpid());
      sem_post(sem1);
    }
      exit(2);
    }
  else
    {
      while(1)
    {
      printf ("[%d] Main waits for sem1...\n", getpid());
      sem_wait(sem1);
      printf ("[%d] Main passes sem1.\n", getpid());
      printf ("[%d] Main waits for sem2...\n", getpid());
      sem_wait(sem2);
      printf ("[%d] Main passes sem2.\n", getpid());
      usleep(100);
      printf ("[%d] Main posts sem2\n", getpid());
      sem_post(sem2);
      printf ("[%d] Main posts sem1\n", getpid());
      sem_post(sem1);
    }
    }
...

No liarla demasiado

Utilizar la mínima cantidad de semáforos posible, si vemos que con sólo uno basta, pues ponemos uno. En este caso sí bastaría con uno, pero habrá casos en los que no se pueda y tendremos que hacer otras cosas…

Si se puede se puede, si no, no

Consiste en utilizar sem_trywait(), si el recurso está ocupado devolverá error, pero no bloqueará. Sólo haciéndolo en uno de los procesos vale, pero claro este proceso entrará menos veces en la sección crítica:

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
40
41
42
43
44
45
...
  child = fork();
  if (child==-1)
    exit(1);
  else if (child==0)
    {
      while(1)
    {
      printf ("[%d] Child waits for sem1...\n", getpid());
      sem_wait(sem1);
      printf ("[%d] Child passes sem1.\n", getpid());
      printf ("[%d] Child waits for sem2...\n", getpid());
      try = sem_trywait(sem2);
      if (try==0)
        {
          printf ("[%d] Child passes sem2.\n", getpid());
          usleep(100);
          printf ("[%d] Child posts sem2\n", getpid());
          sem_post(sem2);
        }
      else
        printf ("[%d] sem2 busy\n", getpid());
      printf ("[%d] Child posts sem1\n", getpid());
      sem_post(sem1);
    }
      exit(2);
    }
  else
    {
      while(1)
    {
      printf ("[%d] Main waits for sem2...\n", getpid());
      sem_wait(sem2);
      printf ("[%d] Main passes sem2.\n", getpid());
      printf ("[%d] Main waits for sem1...\n", getpid());
      sem_wait(sem1);
      printf ("[%d] Main passes sem1.\n", getpid());
      usleep(100);
      printf ("[%d] Main posts sem1\n", getpid());
      sem_post(sem1);
      printf ("[%d] Main posts sem2\n", getpid());
      sem_post(sem2);
    }
    }
...

Timeouts

De la misma forma que probamos si estaba disponible el recurso o no, ahora probamos con un timeout, hacemos que el proceso sea capaz de esperar un tiempo a ver si el recurso se libera, aunque pasado ese tiempo dejará de esperar:

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
40
41
42
43
44
45
  child = fork();
  if (child==-1)
    exit(1);
  else if (child==0)
    {
      while(1)
    {
      printf ("[%d] Child waits for sem1...\n", getpid());
      sem_wait(sem1);
      printf ("[%d] Child passes sem1.\n", getpid());
      printf ("[%d] Child waits for sem2...\n", getpid());
      timeout.tv_sec=0;
      timeout.tv_nsec=100000;
      try = sem_timedwait(sem2, &timeout);
      if (try==0)
        {
          printf ("[%d] Child passes sem2.\n", getpid());
          usleep(100);
          printf ("[%d] Child posts sem2\n", getpid());
          sem_post(sem2);
        }
      else
        printf ("[%d] sem2 busy\n", getpid());
      printf ("[%d] Child posts sem1\n", getpid());
      sem_post(sem1);
    }
      exit(2);
    }
  else
    {
      while(1)
    {
      printf ("[%d] Main waits for sem2...\n", getpid());
      sem_wait(sem2);
      printf ("[%d] Main passes sem2.\n", getpid());
      printf ("[%d] Main waits for sem1...\n", getpid());
      sem_wait(sem1);
      printf ("[%d] Main passes sem1.\n", getpid());
      usleep(100);
      printf ("[%d] Main posts sem1\n", getpid());
      sem_post(sem1);
      printf ("[%d] Main posts sem2\n", getpid());
      sem_post(sem2);
    }
    }

Aquí con un struct timespect timeout donde ponemos el tiempo en nanosegundos (variable tv_nsec) creamos una espera de un tiempo máximo para el recurso reservado.

Otros algoritmos

Para solucionar esto, dependiendo de los casos tenemos otros algoritmos, que espero dedicar posts próximamente.
Foto: Moosealope (Flickr) CC-by

La entrada Deadlock, bloqueo mutuo, abrazo mortal o cómo colgar un proceso con esperas infinitas. ☠ ☠ ☠ aparece primero en Poesía Binaria.

» Leer más, comentarios, etc...

Bitácora de Javier Gutiérrez Chamorro (Guti)

La apuesta de Fortran

August 04, 2014 06:56 AM

Si os digo que FORTRAN (Formula Translating System) fue un lenguaje diseñado por IBM en los años 50 para poder realizar programas científicos que resultaran eficientes, probablemente no os estaré contando nada que no sepáis. En aquellos años solamente existía el ensamblador, y Fortran representaba el primer lenguaje más o menos estandarizado de alto nivel. [...]

» Leer más, comentarios, etc...

Cuaderno de software

Por qué no voy a usar Chef (por ahora)

August 02, 2014 10:14 AM

Infrastructure as code, mantener todos los cambios en tus servidores bajo control completo, configurar tus servidores escribiendo recetas, tener tu infraestructura en tu sistema de control de versiones, que tus desarrolladores puedan utilizar una máquina igual que la de producción en su equipo para evitar el “en mi máquina funciona”.
Bien, compro. ¿Donde hay que firmar?. Take my money.
Pero no. Por ahora no.

Te invito a hacerte estas preguntas, que yo me hice demasiado tarde:
-¿Cada cuanto tiempo levantas un servidor nuevo?
Nosotros usamos Amazon AWS, si, es cloud, pero eso no quiere decir que andemos escalando y desescalando nuestra arquitectura cada 15 minutos. Además, cuando necesito un servidor igual que el de producción levanto una imagen del propio servidor de producción. Además nuestras aplicaciones no están preparadas para escalar horizontalmente por ahora, nuestros servidores no son servidores stateless.
-¿Cuanto tiempo te lleva configurar un servidor nuevo?
A día de hoy, un servidor para correr un stack LAMP es relativamente trivial, con mantener el fichero de configuración de Apache en un control de versiones tendrías todo bastante avanzado. Para software mas complejo como stacks Java tras un Tomcat y un Apache la cosa no es mucho mas complicada y si lanzas un producto cada 6 meses puede que escribir la receta te lleve mas tiempo.
-¿Cuanto tiene que cambiar tu equipo de desarrollo para que todo esto funcione correctamente?¿Qué les aporta a tus desarrolladores disponer de un entorno igual al de producción en su máquina? y por último ¿podrian levantar un entorno de esas características en sus máquinas?
Si trabajas con lenguajes interpretados y demás (PHP por ejemplo) es muy bonito, levantas una máquina compartes la carpeta en la que va el código apuntas tu IDE a dicha carpeta y voilá, pero ¿y si trabajas con Java? ¿para debuggear como lo haces? puedes levantar el Tomcat en modo debug o abrir el puerto, pero ya estás empezando a modificar la máquina y no se parecerá tanto a la máquina de producción.
Si tu producto es la conjunción de una serie de productos ¿haces levantar al desarrollador tus API, tu servicio de SSO…?
-¿Cuanto te va a costar formar a la gente suficiente para que esto sea sostenible?
Infrastructure as code es un matrimonio en toda regla, te comprometes estrictamente a gestionar tu infraestructura de esta manera ya que sino las incosistencias te volverán loco (hay textos que sugieren que ni siquiera te proporciones acceso SSH a tus propios servidores para evitar tocar a mano).

A día de hoy, estas barreras no me aportan la suficiente mejora sobre lo que tengo hasta ahora, me supone mas problema mantener bajo control los ficheros de configuración de mi aplicación que el software que instalo en mis máquinas. Me ha resultado muy costoso adaptar toda la infraestructura ya existente a “recetas” que pudiesen crearla desde cero y no solo eso, sinó que los servidores que ya he instalado “a mano” tendrian que entrar en dicho ciclo o bien debería levantarlos en paralelo con las nuevas recetas y hacer el switch, por ahora el beneficio no me justifica el riesgo.

Pero ojo, evidentemente y si tanto se oye hablar de ello, por algo será y he visto muy buenas motivaciones para adoptar estas técnicas y tecnologías.
-Imagina que realizas instalaciones en servidores alojados en las instalaciones de tus clientes (appliance), tener perfectamente controladas las versiones y modificaciones que realizas en todos y cada uno, poder propagar los cambios basados en roles u otras características.
-Necesitas levantar máquinas automáticamente en un proveedor de hosting que no te proporcione características como la creación de imágenes personalizadas para poder escalar horizontalmente.
-Si tu stack tecnológico te lo permite para trabajar en local, en uno de nuestros proyectos el stack encajaba muy bien y con esto hemos proporcionado a los desarrolladores una manera de probar en un servidor con la misma configuración que el de producción todo el producto.

» Leer más, comentarios, etc...

Cuaderno de software

Mi página web va lenta

August 02, 2014 09:47 AM

IMG_20140802_110438

Repaso en voz alta los diferentes pasos a la hora de servir una web (lo siento pero no soy capaz de dibujar con un ordenador con la misma comodidad que lo hago con papel y boli):

t0: tiempo de envío de la petición desde el navegador al servidor, desde que hacemos click hasta que dicha petición llega al servidor, este factor es ajeno a la web como tal, pero hay que saber identificarlo porque el problema estará en la conexión del usuario y por lo tanto no tendrá que ver con nuestra aplicación directamente.
t1: tiempo de procesado de la petición, primera fase de la petición, recepción de petición, cookies y demás información, procesado de esta, checkeo de la corrección, los factores que pueden influenciar en ella son: código que es demasiado lento o hardware que no soporta el número de peticiones o tipo de peticiones que se reciben.
t2: tiempo para enviar la petición desde el código hasta el sistema de persistencia (generalmente una BBDD). Es habitual que el código y la información no estén en la misma máquina, en ese momento la conectividad entre estas es un punto mas a medir, es un problema de red.
t3: tiempo de procesado de los datos en el sistema de persistencia. ¿Que tipo de consultas hacemos a la BBDD? ¿pedimos sólo lo que necesitamos? ¿está la consulta hecha de la mejor manera? ¿están los datos estructurados de la mejor manera? Si recibo los datos con frecuencia de quince minutos pero solo permito consultarlos por franjas horarias (el datepicker de la interfaz no hila mas fino) ¿tiene sentido almacenar un registro por cada 15 minutos?

timestamp0,16:00,v0
timestamp1,16:15,v1
timestamp2,16:30,v2
timestamp3,16:45,v3

Vs.

timestamp0,16:00,{v0,v1,v2,v3}

En este sencillo ejemplo, en la segunda estructura se “buceará” entre 1/4 del número de registros (extrapolemos el mismo caso para registros minutales o segundales) para obtener la misma información que si se bucease en la primera. Esta decisión está relacionada con las estructuras de la información (inferidas de un entendimiento claro de la solución que queremos dar al cliente), de una consulta correcta al sistema de persistencia y por último del hardware.
t4: devolución de datos al código, aquí entran dos factores, la red, como en el momento del envío de la query y por otro lado el volúmen de datos que se devuelvan (v4), si volvemos al ejemplo del punto t3 el volúmen de datos a transferir será menor. Aquí tendremos que buscar la relación correcta entre lo que cueste conseguir los datos lo mas filtrados posible y el volumen que nos permita transferir el ancho de banda en un tiempo aceptable.
t5: procesado de datos para generar la respuesta, se trata de un problema de código intimamente relacionado con el volumen de datos que haya llegado del sistema de persistencia, habrá ocasiones en las que traer mas datos y procesarlos por código sea mas interesante que traer los datos estrictamente necesarios a costa de haber trasladado demasiada carga al sistema de persistencia, es un problema de código y hardware.
t6: tiempo de devolución de datos, se trata del volumen de información (v6) que se envía al navegador, cuanto menor mejor evidentemente, aquí influye nuestro código y también el ancho de banda.
t7: tiempo de renderizado en el navegador, si enviamos mucha información al navegador en un formato que depués deba ser procesada (una librería de graficado JS a la que servimos listas de puntos) tendremos esa sensación de “la página ha cargado pero no veo nada”.
#: número de consultas al sistema de persistencia que se realizan para poder obtener toda la información del sistema de persistencia, aquí hay que realizar las pertinentes pruebas para cada caso, hay veces que 5 consultas sencillas serán mas rápidas que una consulta compleja (clásicos JOIN en BBDD relacionales que unen tablas de tamaños bestiales para ahorrar hacer unas pocas queries contra tabla única). He marcado este punto en rojo porque aquí hay un problema serio con el código, la utilización de grandes librerias de acceso a datos a veces genera una “niebla” sobre lo que realmente ocurre con nuestra BBDD. Un caso real es el del borrado de todos los hombres de una tabla usuarios que dispone de celda sexo que se realizó obteniendo los id únicos de cada usuario de sexo hombre para realizar el borrado en lugar de un simple borrado por sexo.

» Leer más, comentarios, etc...

Picando Código

Defensa personal del correo electrónico

August 01, 2014 02:00 PM

Recupera tu privacidad con GnuPg

Recupera tu privacidad con GnuPg

La vigilancia indiscriminada viola nuestros derechos fundamentales y pone en peligro la libertad de expresión. Esta guía te enseñará una destreza básica de la defensa personal contra la vigilancia: el cifrado del correo electrónico. Una vez la hayas finalizado, serás capaz de enviar y recibir correos electrónicos codificados para evitar que un vigilante o un ladrón que intercepte tu correo electrónico pueda leerlo. Todo lo que necesitas es una computadora con conexión a Internet, una cuenta de correo electrónico y aproximadamente media hora.

Incluso si no tienes nada que esconder, el cifrado ayuda a proteger la privacidad de las personas con las que te comunicas, y les pone las cosas difíciles a los sistemas de vigilancia indiscriminada. Si tienes algo importante que esconder, estás en buena compañía: estas son las mismas herramientas que utilizó Edward Snowden para compartir sus famosos secretos sobre la NSA.

Además de utilizar el cifrado, hacer frente a la vigilancia requiere una lucha política para reducir la cantidad de datos que se recogen sobre nosotros, pero el primer paso imprescindible es protegerte a ti mismo y hacer que la vigilancia de tus comunicaciones sea lo más dificil posible.

Lee el resto de esta guía en el sitio Defensa Personal del correo electrónico y ¡empieza a cifrar tu correo electrónico!

Defensa personal del correo electrónico

Copyright © 2014 Free Software Foundation, Inc. Política de privacidad. Hazte socio.

Fuente: https://emailselfdefense.fsf.org/es/

Las imágenes tienen una licencia Creative Commons Attribution 4.0 (o una versión posterior), y el resto tiene una licencia Creative Commons Attribution-ShareAlike 4.0 (o una versión posterior). — ¿Por qué estas licencias?

» Leer más, comentarios, etc...

Cuaderno de software

Análisis parálisis

August 01, 2014 08:31 AM

Escribía el otro día sobre cómo hay que atacar de manera distinta los problemas complejos y los complicados. Con los complicados hay que pensar fuerte antes de ponerte a hacer algo. Con los complejos hay que ir poco a poco haciendo cosas y capturando feedback. Amigos agilistas: no todo es parálisis por análisis. A  veces con problemas realmente complicados hay que pararse y pensar fuerte. Me gusta tanto este video para ilustrarlo, que lo he convertido en post… :-)

» Leer más, comentarios, etc...

Meta-Info

¿Que es?

Planeta Código es un agregador de weblogs sobre programación y desarrollo en castellano. Si eres lector te permite seguirlos de modo cómodo en esta misma página o mediante el fichero de subscripción.

rss subscripción

Sponsors

Puedes utilizar las siguientes imagenes para enlazar PlanetaCodigo:
planetacodigo

planetacodigo

Si tienes un weblog de programación y quieres ser añadido aquí, envíame un email solicitándolo.

Idea: Juanjo Navarro

Diseño: Albin