Quantcast
Channel: Adictos al trabajo
Viewing all 990 articles
Browse latest View live

Automatiza tus pruebas desde 0 con Selenium, WebDriver e IntelliJ – Parte I

$
0
0

A lo largo de los siguientes post se pretende instruir en la automatización de pruebas funcionales en entornos webs con Selenium, WebDriver bajo el IDE IntelliJ y usando como lenguaje de programación Java.

Vamos a empezar desde 0, explicando cada una de las herramientas utilizadas, realizando su instalación en entornos Windows y OS X. Así como como unas pautas para su uso.

Indicar que los ejercicios aunque se explica la configuración de las herramientas tanto para Windows como para OS X, los ejercicios están desarrollados bajo y OS X y como veremos más adelante existen ligeras modificaciones con respecto a Windows.

Índice

Introducción

¿Qué es Selenium? Selenium es una Herramienta de automatización de pruebas de código abierto. Existen varias herramientas en el mercado, pero Selenium ha conseguido ser de las más usadas debido a que es gratuita y de código abierto.

¡OJO! Selenium no está diseñado para automatizar las pruebas de escritorio o mainframe, esto se puede ver como una ventaja o como una desventaja.

Una de las mayores ventajas de Selenium es que trabaja sobre varios sistemas operativos. Es la única herramienta que trabaja sobre Windows, OS X, Solaris y Linux. Actualmente también se da soporte a dispositivos móviles, existen APIs adaptadas que son extensiones de Selenium que dan soporte a ambas plataformas: IOs y Android

Selenium se puede escribir en cualquiera de los siguientes lenguajes de programación:

  • Java → El curso está diseñado bajo JAVA
  • C#
  • Ruby
  • Python → Es otro de los lenguajes más utilizados con Selenium
  • PHP
  • Perl

Debes tener en cuenta que el lenguaje utilizado para desarrollar tu software CORE (el build) es independiente del lenguaje utilizado para automatizar las pruebas con Selenium.

Por último, Selenium soporta múltiples navegadores, proporcionando la misma estabilidad en ellos:

  • Internet Explorer (Edge) Bastante problemático su WebDriver bajo OS X.
  • Firefox
  • Chrome
  • Safari

1. Instalación de la última versión disponible de JAVA

Previamente en la línea de comandos escribir java -version en caso de que no exista o no sea una versión 8 o superior continuar con:

Escribimos en Google «java jdk download«. La última versión actualmente es la 12 aunque la versión 8 es la más estable. Sólo descargad la versión JDK del S.O. correspondiente.

Oracle Technology Network / Java / Java SE / Downloads:

https://www.oracle.com/technetwork/java/javase/downloads/index.html

    1. https://www.oracle.com/technetwork/java/javase/downloads/jdk12-downloads-5295953.html

Se podrá descargar tras aceptar los acuerdos de licencia.

2. Configurar JAVA Path en las variables del sistema

Windows 10

Ir a Program Files > Java > jdk.1.8.0_XXX > bin  → Copiar la ruta

Posteriormente vamos a Panel de Control > Sistema y Seguridad > Sistema > Pestaña “Avanzado” > Variables de Entorno > Variables de Sistema > Selecciona “Path” > New > Pega la Ruta anterior y Guarda. 

OS X

Agregar la variable de entorno al PATH en OS X en el documento bash_profile agregar la línea:

export JAVA_HOME=$(/usr/libexec/java_home)

3. Instalación de la última versión disponible de IntelliJ

A continuación, procedemos con la instalación del entorno de desarrollo (IDE) de IntelliJ: 

    1. https://www.jetbrains.com/toolbox/

Seleccionamos IntelliJ IDEA → Descargamos e Instalamos

Posteriormente comprobamos que conoce el PATH de nuestra versión de Java instalada: 

File > Project Structure > Platform Settings > JDK

4. Crear un nuevo proyecto en IntelliJ

Para ello vamos a File > New > Project

Marcamos Java como librería adicional:

En la siguiente ventana le indicamos el Group ID y Artefact ID que queramos:

En el siguiente paso marcamos Create separate module per source set e importante seleccionar Use default Gradle wrapper (recommended):

Para finalizar, indicamos el nombre del proyecto y la ubicación:

Y después de aceptar se nos indicará si queremos que se cree en la ventana actual o en una nueva. Esto ya según el gusto del consumidor.

5. Selenium JARs download

¿Qué es Gradle?

Gradle, es una herramienta que permite la automatización de compilación de código abierto, la cual se encuentra centrada en la flexibilidad y el rendimiento. Los scripts de compilación de Gradle se escriben utilizando Groovy o Kotlin DSL (Domain Specific Language).

Gradle tiene una gran flexibilidad y nos deja hacer usos otros lenguajes y no solo de Java, también cuenta con un sistema de gestión de dependencias muy estable. Gradle es altamente personalizable y rápido ya que completa las tareas de forma rápida y precisa reutilizando las salidas de las ejecuciones anteriores, sólo procesar las entradas que presentan cambios en paralelo.

Además, es el sistema de compilación oficial para Android y cuenta con soporte para diversas tecnologías y lenguajes.

Todas las características de Gradle e información adicional las tenéis aquí:

https://openwebinars.net/blog/que-es-gradle/

6. Configurar Selenium JARs (y Sikuli) en el Project Build Path

OS X

En build.gradle > dependencias agregamos:

implementation "com.sikulix:sikulixapi:1.1.4-SNAPSHOT"
implementation "org.seleniumhq.selenium:selenium-java:3.141.59"

NOTA para que podamos utilizar la API de Sikuli no basta con poner la Dependencia, además debemos incluir la ruta del repositorio Maven que podemos encontrar buscando en Google “Sikuli repository Url”. Adjunto la ruta donde se encuentra en la documentación de Sikuli:

https://sikulix-2014.readthedocs.io/en/latest/_sources/faq/030-java-dev.rst.txt

De este modo tendremos:

repositories {
   mavenCentral()
   maven {
       url 'https://repository.mulesoft.org/nexus/content/repositories/public/'
   }
   maven {
       url 'https://oss.sonatype.org/content/repositories/snapshots/'
   }
}
dependencies {
   implementation "org.seleniumhq.selenium:selenium-java:3.141.59"
   implementation "com.sikulix:sikulixapi:1.1.4-SNAPSHOT"
   testCompile group: 'junit', name: 'junit', version: '4.12'
}

Windows

Se deberán descargar los JARs files de las webs correspondientes y agregarlos como Librerías externas a nuestro proyecto.

https://www.seleniumhq.org/download/

7. Elegir el Navegador donde ejecutar las pruebas y su WebDriver

Para los ejercicios que se vana a realizar vamos a utilizar los navegadores Chrome (por su estabilidad) y Firefox. En OS X actualmente se está integrando el navegador Edge (antiguo Internet Explorer) pero está en fase experimental con lo cual lo descartamos. 

https://www.seleniumhq.org/download/

¡OJO! Antes de descargar el WebDriver es necesario conocer la versión del navegador que tenemos instalado.

WebDriver de Chrome:

En el navegador Chrome pulsamos en:

A continuación,

Pulsamos en «Ayuda»:

Y ahora en «Información de Google Chrome»:

Una vez que conozcamos la versión de nuestro navegador Chrome, descargamos el WebDriver correspondiente de:

https://sites.google.com/a/chromium.org/chromedriver/downloads

En nuestro caso:

WebDriver de Firefox

Para Firefox simplemente mantén el navegador actualizado a la última versión y descarga el WebDriver de la siguiente URL:

https://github.com/mozilla/geckodriver/releases

Descargamos el WebDriver de Firefox y el de Chrome y los almacenamos en una carpeta por ejemplo en el escritorio. Posteriormente copiamos la ruta y volvemos al proyecto.

Para configurar la Property en JAVA, necesitamos dos parámetros. El 1ª es la KEY que viene a identificar el WebDriver del navegador que vamos a usar pudiendo ser:

  •  webdriver.chrome.driver     → Para Chrome
  •  webdriver.gecko.driver       → Para FireFox
  •  webdriver.ie.driver             → Para I.E.
  • webdriver.edge.driver         → Para Edge

El 2º parámetro es el valor que debe ser la ubicación del WebDriver. Teniéndo en cuenta de que las rutas se expresan diferentes si se trata de OS X o de Windows:

  • En Mac es por ejemplo /Users/Autentia/Desktop/Sikuli/chromedriver
  • En Windows es necesario indicar la unidad y el tipo de archivo .exe 

C:/Users/Autentia/Desktop/Sikuli/chromedriver.exe

Por tanto, usamos el comando en la clase Java que crees y que será tu Script para automatizar las pruebas:

    System.setProperty("webdriver.chrome.driver","/Users/Autentia/Desktop/Sikuli/chromedriver");

Y listo el WebDriver del navegador Chrome en este caso.

Hasta aquí la 1ª parte de la formación de Selenium + Webdriver + IntelliJ formada por la configuración del entorno de desarrollo y todas las herramientas a usar.

En el próximo post crearemos los Objetos Driver basados en el explorador elegido y haremos los primeros ejercicios prácticos.

La entrada Automatiza tus pruebas desde 0 con Selenium, WebDriver e IntelliJ – Parte I se publicó primero en Adictos al trabajo.


Cómo convertir un diseño a HTML con Sketch

$
0
0

Hoy os traigo un plugin que a más de un diseñador de interfaces le será muy útil.

Alguna vez como diseñadores de interfaces nos han pedido ver un diseño interactivo de una página web que nosotros mismos hemos diseñado.

Es difícil encontrar un diseñador UI que también sepa maquetar con HTML, CSS y Javascript. Los diseñadores saben diseñar interfaces, ya sea con Sketch, Adobe XD, Photoshop, etc. pero cuando les dices de maquetar, pocos saben.

En este caso los diseñadores que trabajáis con Sketch estáis de suerte.

Gracias al plugin Anima Tool Kit podemos convertir nuestro diseño en código HTML y CSS.

He de decir de antemano que el código que genera el plugin no es un código válido para implementar, así que si estáis pensando en presentar a cliente una web maquetada con esto mejor que no lo hagáis, pero sí que nos puede sacar de un aprieto puntual, sobre todo para mostrar a nivel de funcionalidad como sería la web.

Como ejemplo, vamos a realizar una web responsive.

Vamos al lío.

Para ello tendremos que crear dos artboards, uno móvil y otro desktop. Cada artboard será una pantalla y para hacer que se vea responsive y se comuniquen utilizaremos «pins».

Una vez tengamos los artboards creados  y antes de usar los pins, debemos establecer un artboard como «HOME». En este caso, el artboard desktop será nuestra «HOME».

Acto seguido hay que enlazar el desktop con móvil, para ello usaremos los «Breakpoints»:

 

Es turno de los pins.

La función de los pines es fijar los elementos a las siguientes posiciones:

  • Arriba
  • Abajo
  • Derecha
  • Izquierda
  • Vertical
  • Horizontal

Aquí es cuando debemos jugar con las capas y alinear los objetos en función de cómo queremos que interactúen.

Os dejo un vídeo de como funcionan los pins:

Si tuvieseis más dudas, aquí se explica bien cómo usar los pins.

Una vez hemos configurado los pins vamos a ver cómo se ve.

Presionamos Preview In Browser.

Se abrirá una ventana en tu navegador web y podremos ver el diseño convertido.

Si queremos el código HTML y CSS es tan sencillo como presionar

Export – Code

Nos creará un fichero zip con todos los archivos correspondientes.

 

Os dejo un simple ejemplo que he realizado para que lo descarguéis – Anima Ejemplo.

 

La entrada Cómo convertir un diseño a HTML con Sketch se publicó primero en Adictos al trabajo.

Automatiza tus pruebas desde 0 con Selenium, WebDriver e IntelliJ – Parte II

$
0
0

Introducción

Continuamos el Tutorial empezado en el post Automatiza tus pruebas desde 0 con Selenium, WebDriver e IntelliJ – Parte I, y lo hacemos creando el primer WebDriver y los primeros ejemplos.

Índice

1. Crear el Objeto Driver basado en el explorador elegido

WebDriver de Chrome

Creamos una clase de Java en el proyecto anteriormente generado en IntelliJ y sobre la que vamos a trabajar.

Recordemos que lo primero que debemos hacer es crear la Property para el WebDriver a usar, asignándole la Key del navegador a emplear y el Valor que será la ruta de su ubicación.

Aunque las Keys ya se mencionaron en el post anterior, las vuelvo a dejar aquí ya que las vamos a necesitar e indicar que además están disponibles en la web de Selenium:

  • webdriver.chrome.driver     → Para Chrome
  • webdriver.gecko.driver       → Para FireFox
  • webdriver.ie.driver             → Para I.E.
  • webdriver.edge.driver        → Para Edge

WebDriver de Chrome

System.setProperty("webdriver.chrome.driver","/Users/Fbogas/Desktop/Sikuli/chromedriver");
WebDriver driver = new ChromeDriver();
driver.get("http://google.com");
System.out.println(driver.getTitle());

Con el Script anterior generamos un WebDriver llamado driver para controlar el navegador Chrome y le indicamos que alcance la web de Google. Posteriormente se escribe en la consola de comandos de IntelliJ el título asignado a la web (¡OJO! que no su URL).

WebDriver de Firefox

System.setProperty("webdriver.gecko.driver","/Users/Fbogas/Desktop/Sikuli/geckodriver");

Creamos el objeto driver. Para ello accedemos a la web de selenium para comprobar la clase que debemos utilizar para crear el objeto correspondiente al navegador seleccionado. En este caso Firefox. https://www.seleniumhq.org/projects/webdriver/

WebDriver driver = new FirefoxDriver(); driver.get("http://google.com"); 
System.out.println(driver.getTitle());

WebDriver de Edge

System.setProperty("webdriver.edge.driver","/Users/Fbogas/Desktop/Sikuli/msedgedriver"); 
WebDriver driver = new EdgeDriver(); 
driver.get("http://google.com"); 
System.out.println(driver.getTitle());

Error típico en el código de invocación del explorador:

Si cometemos algún error al invocar el explorador recibiremos un mensaje de error del estilo:

The path to the driver executable must be set by the webdriver.xxxxx.driver system…

Normalmente el error será porque la ruta de ubicación del webdriver no es correcta, o el webdriver no es el del navegador invocado.

Otras veces puede ser que Selenium no de soporte a las últimas versiones del navegador mediante el que se quieren automatizar las pruebas (Actualmente ocurre con el navegador Edge bajo OS X).

La mayoría de las veces que ocurre esto se da en las últimas versiones del navegador Firefox.

El navegador con mayor compatibilidad con Selenium en la actualidad es Chrome.

2. Ejemplos para poner en practica lo visto hasta ahora.

Un primer ejercicio y al mismo tiempo, una buena práctica, es determinar si hemos aterrizado en la web correcta o no. Hasta ahora hemos estado usando:

System.out.println(driver.getTitle());

Pero este paso no es suficiente para determinarlo. ¿Cómo leer entonces la URL actual en Selenium?

Antes de dar la solución quiero recomendar al lector que se valga de la ayuda de IntelliJ para ver los métodos de la clase driver, escribiendo:

System.out.println(driver.

Y viendo las opciones mostradas intenta deducir cuál sería la que buscamos.

Si has hecho este ejercicio habrás llegado fácilmente a la solución:

System.out.println(driver.getCurrentUrl());

Mirando en la consola de IntelliJ tras la ejecución observaremos que nos muestra la URL exacta del sitio web:

Con el comando actual verificamos que hemos aterrizado justo en la web que queríamos.

Hay veces que el desarrollador o propietario de la web desactiva que hagas click derecho en la web para impedirte que puedas conocer el código fuente de la web. Con Selenium esto no es problema ya que podemos usar:

System.out.println(driver.getPageSource());


3. Métodos Básicos con webDriver II

¿Qué ocurre si le decimos a Selenium que alcance 2 webs diferentes a la vez?

driver.get("http://google.com"); 
driver.get("http://yahoo.com");

Si has probado a ejecutarlo en IntelliJ habrás comprobado que carga las URL en orden secuencial tal como las encuentra pero en la misma ventana. Con lo que en el caso anterior primeramente cargará Google y posteriormente Yahoo!.

¿Cómo podemos hacer para  volver a la página anteriormente cargada en el navegador?

driver.navigate().back();

Hasta ahora hemos realizado varios ejemplos y si nos has seguido habrás visto que una vez ejecutadas las pruebas el navegador se quedaba abierto. Para cerrar la sesión en el navegador una vez finalizadas las pruebas:

driver.close(); // Cierra el navegador actual abierto por el Script 
driver.quit();  // Cierra todas las sesiones abiertas por el Script

4. Técnicas de Localización (Locator) y Herramientas utilizadas para identificar un Objeto

Para proceder con las técnicas Locator vamos a acceder a una web cualquiera, por ejemplo, facebook. Una vez en ella vamos a observar sus elementos y cómo los desarrolladores identifican los elementos contenidos en ella. 

Vamos a seleccionar el elementos de login de email or phone > click derecho > inspeccionar.

Al hacer click se nos desplegará la versión web para desarrollador. En ella aparece el código fuente de la web y resaltado el mismo código fuente que hace referencia al elemento seleccionado.

Es importante aquí reconocer varias partes del código:

<input type="text" class="inputtext _58mg _5dba _2ph-" data-type="text" name="firstname" 
value="" aria-required="true" placeholder="" aria-label="Nombre" id="u_0_l" 
style="background-image: XXXXX background-repeat: no-repeat; background-attachment: 
scroll; background-size: 16px 18px; background-position: 98% 50%; cursor: auto;">

En verde tenemos la etiqueta (tag) y en azul oscuro los atributos.

Conozcamos ahora los Locators soportados por Selenium:

  • ID
  • ClassName
  • Name
  • LinkText
  • Xpath
  • CSS

Si echamos un vistazo a la etiqueta anterior vemos que podemos identificar el ID, el ClassName y el Name claramente del objeto.

Vamos a hacer un ejercicio en el que vamos a introducir nuestro email en dicha caja de texto.

System.setProperty("webdriver.chrome.driver","/Users/Fbogas/Desktop/Sikuli/chromedriver");

WebDriver driver= new ChromeDriver(); 
driver.get("http://facebook.com"); //URL in the browser 

driver.findElement(By.id("email")).sendKeys("micorreo@autentia.com"); 
driver.findElement(By.id("pass")).sendKeys("mipassword"); 
//driver.findElement(By.id("u_0_a")).click();  
// ¡OJO! Con usar ID alfanuméricos ya que cambian con cada refresh de la web 

driver.findElement(By.id("loginbutton")).click();  
// Pero ¿Qué Ocurre cuando no disponemos ninguno de los locators?

Veamos ahora otro ejemplo pero utilizando el LinkText. En la misma web de Facebook vemos que existe un link que dice: ¿Has olvidado los datos de la cuenta?

Con el siguiente comando podemos hacer click en los enlaces:

driver.findElement((By.linkText("¿Has olvidado los datos de la cuenta?"))).click();

NOTA: cuando buscamos un elemento por clase debemos tener en cuenta que no se pueden buscar cuando el nombre de la clase tiene espacios. Por ejemplo: 

class =”input r4 wide mb16 mt8 username”

Si lo usamos obtendremos el mensaje de error:

Compound class names not permitted

5. Identificando XPath en los navegadores Chrome y Firefox. Utilizando las herramientas del navegador.

¿Qué es XPATH?

XPath (XML Path Language) es un lenguaje que permite construir expresiones que recorren y procesan un documento XML. La idea es parecida a las expresiones regulares para seleccionar partes de un texto sin atributos (plain text). Este nos permite buscar y seleccionar teniendo en cuenta la estructura jerárquica del XML. XPath fue creado para su uso en el estándar XSLT, en el que se usa para seleccionar y examinar la estructura del documento de entrada de la transformación. Por último, indicar que fue definido por el consorcio W3C.

Para poder utilizarlo en un navegador, primero debemos tener activadas las Herramientas para desarrolladores.

Posteriormente para conocer el XPATH del elemento en cuestión lo seleccionamos.

Veremos que el código fuente queda resaltado. En este código fuente hacemos click con el botón derecho del ratón y seleccionamos Copy > Copy XPath.

De esta forma ya tendremos en el portapapeles el XPath a usar:

driver.findElement(By.xpath("//*[@id=\"u_0_a\"]")).click();

¡OJO! se ha puesto la \ como símbolo de escape. En otro caso se debería escribir con comillas simples: By.xpath(«//*[@id=’u_0_a’]»).

6. Identificando CSS selector en los navegadores Chrome y Firefox. Utilizando las herramientas del navegador.

CSS Locator es similar a XPath. En este caso procederemos de igual forma que hacemos para obtener el XPath. Pero en esta ocasión seleccionamos Copy Selector en Chrome y Copy CSS Selector en Firefox.

Obteniendo:

Aunque también podemos obtenerlo buscando en la ToolBar de las herramientas de desarrollador.

7. ChroPath, la extensión del navegador Chrome que te facilitará la vida.

Tanto como para Chrome como para Firefox existen extensiones o plugin que te pueden facilitar la vida más de lo que te imaginas. En el momento de escribir el artículo la última versión de ChroPath es soportada por Firefox, Chrome y Opera:

Lo que nos trae esta extensión es tener al alcance de un click los Locators de todos los objetos de la web. Si en el ejemplo anterior de Facebook hacemos click sobre el casillero del password:

Veremos en el ChroPath todos los Locators:

Fácil, ¿verdad?

Por tanto, cerraremos este segundo post con un ejercicio para poner en práctica todo lo aprendido hasta ahora que no es poco.

8. Ejercicio Práctico

Para este ejercicio se solicita capturar un mensaje de error y mostrarlo por la consola de comandos. Para ello en la página web de Facebook vamos a loguearnos con un usuario ficticio y vamos a capturar el mensaje de error mostrado.

Una posible solución al ejercicio es:

public static void main(String[] args) {

System.setProperty("webdriver.chrome.driver","/Users/Fbogas/Desktop/Sikuli/chromedriver");

WebDriver driver= new ChromeDriver();
driver.get("http://facebook.com"); //URL in the browser

driver.findElement(By.id("email")).sendKeys("micorreo@autentia.com");
driver.findElement(By.id("pass")).sendKeys("mipassword");     
driver.findElement(By.xpath("//*[@id=\"u_0_a\"]")).click(); 

System.out.println(driver.findElement(By.xpath("//div[@class='_4rbf _53ij']")).getText());   

}

9. Avance del siguiente post

Hasta aquí el segundo post sobre Automatización de pruebas con Selenium. Recordad que para ampliar más conocimiento sobre los métodos empleados así como de la librería completa podéis visitar la documentación existente:

https://www.seleniumhq.org/docs/

Para el siguiente post continuaremos aprendiendo a generar XPath y CSS Selectors personalizados desde los atributos HTML. Además, se introducirá Sikuli y veremos su integración con Selenium mediante ejercicios prácticos.

Nos vemos en el siguiente post.

La entrada Automatiza tus pruebas desde 0 con Selenium, WebDriver e IntelliJ – Parte II se publicó primero en Adictos al trabajo.

PostGIS para entender las bases de datos espaciales

$
0
0

En esta entrada vamos a ver qué son las bases de datos espaciales, para qué se utilizan y diferentes ejemplos con PostGIS para entender su funcionamiento.

Índice de contenidos.

¿Qué son las bases de datos espaciales? ¿Cómo surgieron?

La necesidad de trabajar con datos en espacios de coordenadas no es nueva. Hace ya años desde que surgió el concepto de GIS (Sistema de Información Geográfica en inglés o SIG) y comenzaron a implementarse los primeros. A grandes rasgos, un GIS es un sistema software que permite el procesamiento y la analítica de datos geoespaciales.

En los inicios se utilizaron ficheros con formatos concretos (a destacar los Shapefiles) que los GIS podían leer y analizar. Las desventajas son claras: se necesita un software a medida, el acceso concurrente es problema si hay escrituras y los tratamientos complejos, como por ejemplo relacionar datos entre sí, son muy tediosos de hacer a mano.

Con el tiempo empezaron a utilizarse bases de datos relacionales para guardar estos datos. Lo único que se hacía era almacenar los datos con tipos convencionales sin ningún tratamiento especial, por ejemplo columnas numéricas para latitud y longitud. Se seguía necesitando software especial para interpretar y procesar correctamente esta información. Sin embargo, es cierto que se mitigaron algunos problemas. Desde entonces, en la mayoría de casos, los ficheros han quedado relegados a una forma de importar y exportar los datos.

Pero lo que había hasta entonces no era suficiente así que se dio un paso más y llegaron las bases de datos espaciales. De forma resumida son aquellas desarrolladas con el uso de datos basados en coordenadas en mente. Lo novedoso es que estos datos no son tratados de la forma tradicional. Existen tipos, funciones y mecanismos de optimización internos creados expresamente para facilitar su procesamiento. De esta forma podemos trabajar sin problemas con puntos, líneas o áreas en un sistema de coordenadas.

Nosotros vamos a trabajar con PostGIS

Actualmente hay bastantes alternativas en el mercado de las bases de datos espaciales. Tenemos tanto aquellas creadas desde cero con este propósito como extensiones de otras ya existentes. Tanto relacionales como NoSQL, aunque por lo general las relacionales están más maduras en este aspecto.

Obviamente se deberá escoger una u otra en función de las necesidades específicas. No es mi objetivo aquí hacer un estudio de cada una, simplemente aprender los conceptos. Por lo tanto voy a elegir PostGIS, que es una de las más usadas y normalmente la primera opción que se viene a la mente al hablar de este tipo de tecnología.

Se trata de una extensión de PostgreSQL que le permite usar tipos, funciones e índices espaciales. De esta forma tenemos toda la potencia de la base de datos original más la flexibilidad de estas herramientas, incorporando casi todo lo que hacen el resto de alternativas y lo hace de forma nativa. Además implementa el estándar OGC SFSQL (que vamos a ver más adelante), lo que facilita la interacción con otros sistemas. 

¿Cómo funcionan?

Ya con la introducción hecha, vamos a ver cómo funcionan las bases de datos espaciales, tomando PostGIS como ejemplo. Lo que veremos es particular de esta base de datos, pero las ideas son generalizables a las demás. Mi objetivo es dejar los conceptos básicos claros para poder aprender cualquier alternativa sin demasiado esfuerzo. Y además de paso que podáis manejaros mínimamente con PostGIS.

Como ya hemos dicho, las principales características de las base de datos espaciales que las diferencian de las tradicionales son los tipos, las funciones y los índices. A continuación vamos a entrar en detalle cada uno.

Tipos

Lo primero que siempre se menciona de las bases de datos espaciales es que permiten almacenar nuevos tipos. Al fin y al cabo una base de datos sirve principalmente para eso: guardar datos.

Con las bases de datos espaciales también surgieron otros estándares. Uno de ellos es Simple Features, promovido por el Consorcio Geoespacial Abierto (más conocido por sus siglas en inglés OGC, de Open Geospatial Consortium). Concretamente a nosotros la parte que nos afecta es el Simple Features for SQL o SFSQL. En este estándar se define el modelo de los tipos de datos espaciales más comunes en dos dimensiones. Además también especifica los dos formatos principales para representarlos: Well-Known Text (WKT) y Well-Known Binary (WKB).

Esta es la jerarquía de tipos que se definen en el estándar, en la que todos ellos extienden del tipo Geometry:

Jerarquía de tipos geométricos

PostGIS implementa SFSQL, pero además lo amplía con con SQL/MM. De esta forma añade soporte para datos de hasta 4 dimensiones (3DM, 3DZ y 4D) a todos los tipos de datos que definen las Simple Features.

A modo de resumen, los tipos de datos más importantes que utiliza PostGIS, junto con su representación en WKT, son:

  • Punto: representa un punto en un espacio de N dimensiones.

Imagen representativa de un punto

  • Multilínea: representa una línea compuesta de varios segmentos rectos, por lo que se definirá con varios puntos. Pueden ser abiertas o cerradas.

Imagen representativa de una multilínea

  • Curva: representa lo mismo que la multilínea pero en lugar de tener segmentos rectos son curvas representadas por los puntos de origen, destino y uno intermedio para dar curvatura.

Imagen representativa de una curva

  • Polígono: representa un polígono cerrado, que puede estar completo o tener agujeros en su interior. Se representa como un conjunto de multilíneas cerradas, siendo la primera el polígono principal y las demás los agujeros que pueda tener.

Imagen representativa de un polígono

  • Colecciones: tenemos tanto la colección genérica GeometryCollection que puede almacenar cualquier tipo de datos de tipo Geometry como los específicos MultiPoint, MultiLineString y MultiPolygon.

Imagen representativa de una colección de puntos

Como vimos, todos extienden de Geometry, que es el padre y puede usarse para tipar una columna de una tabla y tener polimorfismo. Al usar estos tipos, estamos trabajando sobre un espacio de coordenadas cartesianas. La característica más importante para nosotros es que una distancia de una unidad sobre el eje X va a medir siempre lo mismo da igual en qué posición del eje Y estemos. 

Ejemplo de coordenadas cartesianas

Pero además, también podemos trabajar con espacios esféricos. Para entender la diferencia pensemos en dos meridianos cualesquiera de la Tierra (las líneas que van de norte a sur). La distancia entre ambos siempre será constante si lo medimos en grados, que es como se expresa un par de coordenadas en una esfera. Sin embargo, se van acercando los unos a los otros hasta converger en los polos. Aunque la distancia en grados sea la misma, la distancia real en kilómetros es distinta según la latitud. Nuestros dos meridianos estarán más separados a la altura del Mediterráneo que por la zona de Islandia, por ejemplo.

Imagen del globo terráqueo

Así como tenemos los tipos de datos Geometry para coordenadas cartesianas, también están los tipos de datos Geography para trabajar en espacios con coordenadas esféricas. Tratar con ellos es bastante más pesado internamente pero sus cálculos son mucho más precisos en determinadas circunstancias, por lo que puede llegar a ser interesante usarlos. De todos modos no voy a entrar más en detalle  para no hacer la entrada demasiado extensa. 

Funciones

Ahora que ya tenemos los datos, es importante también poder tratarlos. Para ello disponemos de una serie de funciones especialmente diseñadas y que en PostGIS se agrupan en varios tipos. Vamos a ver algunos ejemplos para que sepáis lo básico que se puede hacer, pero os recomiendo leer la documentación para poder verlas todas o conocer más en detalle alguna de ellas.

En cada grupo hay sentencias SQL de ejemplo con las que poder ver algunas funciones en acción. Si queréis, podéis leer el principio del punto “Preparación del entorno” del ejemplo práctico para levantar una instancia de PostGIS y crear una base de datos con la extensión en la que podréis ejecutar todos los ejemplos propuestos.

Funciones de salida

Es común querer ver los datos de nuestras tablas en una forma que podamos entender o exportarlos para tratarlos en otros lados. Estas funciones nos permiten, a partir de un tipo de dato geométrico, obtener su representación en una variedad bastante amplia de formatos: binario, texto, GeoJSON, GML, SVG… Digamos que un equivalente del día a día sería la serialización de un objeto en memoria a JSON.

SELECT ST_AsText('POINT(0 0)'); -- Muestra nuestro punto en WTK (Well-Known Text).

SELECT ST_AsEWKT('010100000000000000000000000000000000000000'); -- Lo mismo que antes, pero en este caso hemos pasado cómo se representa el punto si lo obtenemos de la tabla directamente.

SELECT ST_AsGeoJSON('LINESTRING(1 2 3, 4 5 6)'); -- Muestra esta multilínea de un espacio de tres dimensiones en formato GeoJSON.

SELECT ST_AsSVG('POLYGON((0 0,0 1,1 1,1 0,0 0))'); -- Muestra el polígono como un path data de SVG, que es el formato en el que se especifica cómo se dibuja una imagen SVG. Para más información podéis consultar https://www.w3.org/TR/SVG/paths.html#PathDataBNF

SELECT ST_AsBinary('POLYGON((0 0,0 1,1 1,1 0,0 0))'::geometry); -- Devuelve la representación en binario del polígono (Well-Known Binary).

 

Funciones de construcción

Como su nombre indica, permiten crear objetos geométricos a partir de una entrada, que puede estar representada en una amplia variedad de formatos. Si las funciones de salida eran la serialización entonces las de construcción serían las de deserialización.

Si intentásemos mostrar directamente el resultado de cualquier consulta que devuelva un objeto geométrico solo veríamos el chorro de número y letras que es su representación interna en base de datos pero que nosotros no podemos entender. Así que en los ejemplos de ahora en adelante siempre que una función nos devuelva un objeto geométrico usaremos la función ST_AsText para convertirlo en algo entendible a simple vista.

SELECT ST_AsEWKT(ST_GeomFromText('LINESTRING(0 0, 1 1, 1 2, 3 2)')); -- Crea una multilínea que pasa por los puntos (0, 0), (1, 1), (1, 2) y (3, 2).SELECT ST_AsText(ST_GeomFromGeoJSON('{"type":"LineString","coordinates":[[0,0],[1,1],[1,2],[3,2]]}')); -- Crea la misma línea pero expresando la entrada en formato GeoJSON.

SELECT ST_AsEWKT(ST_LineFromMultiPoint('MULTIPOINT(0 0, 1 1, 1 2, 3 2)')); -- Misma línea pero a partir de un conjunto de puntos.

Funciones de acceso

Nos permiten obtener datos de un objeto concreto, como el tipo de geometría, el número de puntos que tiene, la longitud, el área… Muchas de estas funciones no sirven para todos los tipos de datos, por ejemplo tiene sentido saber si una multilínea es cerrada (si su origen y destino es el mismo) pero no aplica a un único punto que siempre se considera como cerrado.

SELECT GeometryType('POINT(1 2 3)'::geometry); -- Devuelve el tipo concreto de geometría que es. En este caso un punto.SELECT ST_Dimension('GEOMETRYCOLLECTION(LINESTRING(1 1,0 0),POINT(0 0))'); -- Devuelve la dimensión de la geometría. Los puntos tienen dimensión 0, las líneas 1, los polígonos 2 y las colecciones tienen la mayor dimensión entre todos sus componentes.

SELECT ST_IsClosed('LINESTRING(0 0, 1 1)'); -- Devuelve si la multilínea es cerrada.

SELECT ST_AsText(ST_ExteriorRing('POLYGON((1 1, 2 5, 7 5, 7 1, 1 1), (3 3, 3 4, 5 4, 4 3, 3 3))')); -- Devuelve el anillo exterior del polígono.

Funciones de edición

A veces en vez de crear nuevos datos desde cero podemos querer basarnos en alguno ya existente y modificarlo para que se ajuste a nuestras necesidades. Los casos más típicos son los de rotar o escalar una figura.

SELECT ST_AsText(ST_Rotate('LINESTRING (50 160, 50 50, 100 50)', pi()/2)); -- Rota la multilínea que le hemos pasado en 90 grados en sentido antihorario.SELECT ST_AsText(ST_Scale('LINESTRING(1 2 3, 1 1 1)', 1, 2, 3)); -- Escala la multilínea para que mantenga sus coordenadas en el eje X, se duplique en el Y y se triplique en el eje Z.

SELECT ST_AsText(ST_AddPoint('LINESTRING(0 0 1, 1 1 1)', 'POINT (1 2 3)')); --&nbsp; Añade un nuevo punto a una multilínea.

SELECT ST_AsText(ST_Force2D('LINESTRING(0 0 1, 1 1 1)')); -- Fuerza la geometría a tener solo dos coordenadas, lo que permite adaptarse al estándar de OGC.

Funciones de medidas y relaciones

Algunas de las funciones más importantes que tenemos nos permiten relacionar los datos con su entorno, tanto con medidas individuales como con otros elementos.

SELECT ST_Distance('POINT (1 1)', 'LINESTRING(0 0, 2 0)'); -- Calcula la distancia en dos dimensiones entre dos elementos.SELECT ST_3DDistance('POINT (1 1 1)', 'LINESTRING(0 0 0, 2 0 0)'); -- Calcula la distancia pero esta vez en un espacio de 3 dimensiones.

SELECT ST_Within('POINT(2 2)'::geometry, 'LINESTRING(1 1, 3 3)'::geometry); -- Determina si todos los puntos del primer elemento se encuentran dentro del segundo.

SELECT ST_Equals('LINESTRING(0 0, 10 10)', 'LINESTRING(0 0, 5 5, 10 10)'); -- Determinar si dos elementos son iguales, es decir, si se se cumple que el primero está dentro del segundo y el segundo dentro del primero. Esto quiere decir que, como en el ejemplo, no es necesario que estén creados igual.

SELECT ST_Length('LINESTRING(0 0, 1 0, 1 1)'); -- Devuelve la longitud total de una multilínea.

Funciones de procesamiento geométrico

Por último vamos a ver funciones que, a grandes rasgos, permiten crear figuras a partir de propiedades de uno o más elementos.

SELECT ST_AsText(ST_Buffer('POINT(100 90)', 50)); -- Devuelve el área que representa todos los puntos que están a una distancia de 50 unidades del punto.

SELECT ST_AsText(ST_Union('POLYGON((1 2, 2 4, 8 5, 9 4, 9 2, 1 2))'::geometry, 'POLYGON((8 4, 8 6, 10 6, 10 4, 8 4))'::geometry)); -- Devuelve el polígono fruto de la unión de los dos parámetros.

 

Índices

La última, pero no la menos importante, de las características de una base de datos espacial es que implementa índices enfocados especialmente a tratar con estos datos. Hay bastantes (Quadtree, Grid, Octree…) y cada base de datos utiliza el suyo. En concreto aquí vamos a ver R-Tree porque es el utilizado por PostGIS (y algunos otros sistemas como Oracle Spatial) y por tanto el que se va a utilizar por debajo en nuestro ejemplo.

El funcionamiento tradicional de una cláusula where es comprobar uno por uno todos los registros para ver si cumplen una cierta condición. Sin embargo, los índices permiten organizar los datos de una cierta forma (por ejemplo ordenados ascendentemente) para encontrar el grupo de posibles candidatos para los que la cláusula se va a cumplir. En lugar de comprobar todos los registros solo se hace con los que es probable que sean válidos. Esto es posible gracias a que los tipos tradicionales (números, cadenas de caracteres y fechas) son fáciles de ordenar y comparar unos con otros. Pero no es tan trivial con datos espaciales. ¿Cómo se define si un punto es mayor que otro? ¿Y un área? ¿Y si un punto está dentro de un polígono? Se necesita una aproximación diferente para organizar estos datos.

Los árboles R (o R trees) son la estructura de datos en la que se apoya PostGIS para crear y mantener sus índices. Se basa en dos conceptos: usar rectángulos delimitadores de superficie mínima (Minimum Bounding Rectangle o MBR en inglés) y agrupar los objetos cercanos en áreas cada vez más pequeñas. Ahora hablaremos de cómo se estructuran estos árboles, pero primero veamos de qué sirven los rectángulos y qué se busca. 

Pongamos que tenemos el siguiente ejemplo, con las líneas y el cuadrado verdes inicialmente en nuestra base de datos. Queremos buscar todos los objetos que están en contacto con un segundo polígono azul, también en la imagen.

Imagen de la búsqueda que vamos a hacer

Sin índices tendríamos que comprobar uno por uno todos los objetos. Sin embargo, este tipo de cálculos con polígonos y líneas sin una forma predefinida con la que optimizar son bastante costosos. Por este motivo se rodea cada elemento con su MBR. Nos quedaría algo así:

MBR aplicados a todas las figuras

Ahora ya podemos aligerar los cálculos. Es muy sencillo saber si dos rectángulos se superponen en algún punto y computacionalmente no lleva nada de tiempo. Sabemos a ciencia cierta que si dos rectángulos no se están tocando es imposible que sus contenidos lo estén haciendo. Así hacemos un primer filtrado para, de una manera rápida, quitarnos todos los datos que sabemos que no cumplirán la condición sin necesidad de un análisis exhaustivo. En nuestro ejemplo vemos que la línea de la izquierda es completamente imposible que se tope con el polígono azul. De lo que no podemos afirmar nada es de la línea a la derecha y el cuadrado verde. Aquí ya sí que se hará una comprobación real en la que se calculan todos los puntos de posible contacto.

Esto es, de forma simplificada lo que hace el árbol R. Con un primer filtro se descartan todos los objetos que se sabe de antemano que no van a cumplir la cláusula que se está buscando. A continuación se mira en detalle cada uno de los elementos no descartados, pero con un poco de suerte el porcentaje de estos elementos será muy bajo.

Pero, ¿cómo hace el filtrado para saber con cuáles se queda? ¿Comprueba los rectángulos de cada elemento uno por uno? Para nada. De hecho no necesitaríamos ninguna estructura especial de datos si este fuese el caso. Un árbol R (la R viene de rectángulo) se trata de un árbol balanceado de búsqueda que divide el espacio en rectángulos cada vez más pequeños que agrupan todos los objetos dentro de ellos y que pueden llegar a superponerse. Creo que lo mejor es tener primero una imagen en mente y luego lo explicamos.

Ejemplo de un árbol R

En esta imagen tenemos varios objetos, representados ya por su MBR en rojo. Esos son los elementos a indexar y sobre los que haremos las consultas finales. El área entera que los engloba a todos está representada por el nodo raíz, que divide este área en R1 y R2. Estos rectángulos cada uno está representado por un nodo intermedio que a su vez los divide en áreas más pequeñas. El proceso se sigue hasta que se llega a los nodos hojas, que ya contienen los MBR de los elementos reales.

Al estar organizados de esta manera se pueden descartar áreas enteras hasta quedarnos con el subconjunto más pequeño posible de elementos para los que es posible que se cumpla la cláusula. Vamos a verlo con un ejemplo.

Digamos que vamos a buscar todos los elementos que tocan en algún punto a la línea verde de la imagen.

Árbol R con nuevo elemento

Lo primero que se hará es, obviamente, enmarcar nuestra línea en su MBR.

Nuevo elemento encuadrado en su MBR

A continuación empezamos a recorrer el árbol y comprobamos el nodo raíz. Vemos que el MBR de la línea verde está dentro de R1, pero no de R2. Descartamos de un solo golpe todos los elementos que están en R2. A continuación nos vamos al nodo de R1 y comprobamos que en concreto solamente está en contacto con R4 y R5, pero no con R3 . Comprobamos en detalle los elementos de estos nodos, que ya son hojas, y descartamos todo menos R11 y R14. De una forma muy poco costosa computacionalmente hemos reducido en gran medida el grupo de objetos a comprobar. De 12 elementos que tenemos en nuestra tabla solamente es necesario que hagamos cálculos pesados con 2 de ellos, lo que sin duda es nos va a ahorrar mucho tiempo.

Cómo se generan exactamente los rectángulos a partir de un conjunto de datos dado y cómo se actualizan cuando se insertan nuevos o se borra alguno ya existente depende del algoritmo concreto que se esté usando. Algunos favorecen la velocidad de construcción o modificación mientras que otros buscan que las distintas áreas se solapen lo menos posible. Además hay variantes de este tipo de índices como el R* tree que, por ejemplo, tienen mejoras a la hora de la inserción.

Hora de un ejemplo práctico

Ya tenemos las nociones básicas de qué es una base de datos espacial y cómo trabajar con ella. Vamos a ver un ejemplo para afianzar  los conceptos.

Nos encontramos en una torre de control marítima en Australia. La finalidad principal de la torre consiste en monitorizar un área a su alrededor, que está dividida en varias regiones, en la que viven familias de delfines. Por lo general las familias viven en una región, pero hay veces que alguno de sus miembros sale a explorar a otras regiones e incluso se adentra fuera del área que llegamos a monitorizar. Además las familias viajan entre regiones y es frecuente que decidan cambiar con el tiempo. Sin embargo, se puede saber que están ocupando una región en particular porque al menos la mitad de la familia siempre se queda en ella mientras el resto de miembros están fuera.

El sistema es un poco antiguo, por lo que por el momento los datos que se están guardando en la plataforma se cargan de forma periódica de un fichero que contiene todo. Es decir, sólo se almacena una fotografía del estado en un momento exacto del tiempo.

Preparación del entorno

No quiero centrarme en cómo instalar PostGIS, así que para nuestro ejemplo he optado por utilizar esta imagen de Docker en la que ya tenemos un PostgreSQL con PostGIS preparado. En ese mismo enlace hay instrucciones para levantar la imagen y conectarse a ella.

Definimos las tablas

Una vez estemos dentro de nuestra imagen lo primero que tenemos que hacer es crearnos una nueva base de datos y decir que utilice la extensión de PostGIS. Para ello ejecutamos las siguientes instrucciones:

CREATE DATABASE sea_tower; -- Creamos una nueva base de datos.
\c sea_tower; -- Nos conectamos a ella.
CREATE EXTENSION postgis; -- Decimos que tiene que utilizar la extensión de PostGIS.
SELECT PostGIS_Full_Version(); -- Comprobamos que PostGIS está funcionando.

El siguiente paso es preparar las tablas con las que vamos a trabajar. En concreto los datos que tendremos son:

  • Una tabla de regiones en la que tenemos el identificador numérico de la región, un nombre en clave, el polígono que representa dicha región y el nombre de la familia que está actualmente asentada en ella (o null si no hay ninguna).
  • Una tabla de delfines en la que almacenamos su identificador, la familia a la que pertenece, el último punto en el que se le detectó y el movimiento que ha seguido desde que empezaron las mediciones.

La definición de las tablas será la siguiente:

CREATE TABLE Regions (
	id 		SERIAL PRIMARY KEY,
	name 	        VARCHAR(20) NOT NULL,
	area		GEOMETRY NOT NULL,
	settled_family  VARCHAR(20)
);

CREATE TABLE Dolphins (
	id 		SERIAL PRIMARY KEY,
	family_name	VARCHAR(20) NOT NULL,
	last_position	GEOMETRY NOT NULL,
	movement 	GEOMETRY NOT NULL 
);

Veréis que hemos creado las columnas como GEOMETRY en lugar de como puntos, líneas o polígonos. Para especificar correctamente el subtipo concreto se debe añadir la columna a la tabla después de haberla creado llamando a la función AddGeometryColumn. A esta función se le pasa la tabla y el tipo de dato que será la nueva columna, pero como introduce el concepto de SRID que no he comentado aquí he preferido crear las columnas con el tipo genérico y no liar a nadie.

Cargamos el estado inicial

Con las tablas ya creadas es hora de cargar una serie de datos que reflejarán el estado actual del área marítima controlada por la torre. Esta es una imagen representativa de nuestros datos para poder tener un contexto gráfico en mente mientras estemos trabajando:

Plano del área marítima controlada

En este diagrama tenemos las 5 regiones con las que vamos a trabajar y los datos de los delfines. Los delfines rojos pertenecen a la familia de los Javadelphis, los azules son los Lagenonaut y los verdes los Delphinuspring. Además a cada delfín le tenemos asociado su histórico de movimientos, que es la línea negra que llega a ellos.

Estos son los datos con los que vamos a trabajar, por lo que tenemos que inicializar nuestras tablas para que reflejen lo mismo. Estas son las regiones que se están monitorizando:

INSERT INTO Regions (name, area, settled_family) VALUES 
('Alpha',  'POLYGON((2 17, 7 22, 13 22, 13 15, 7 15, 2 17))', 'Javadelphis'), 
('Bravo',  'POLYGON((13 22, 22 22, 22 15, 13 15, 13 22))', null),
('Charlie',  'POLYGON((2 10, 2 17, 7 15, 13 15, 11 10, 2 10))', 'Lagenonaut'),
('Delta',  'POLYGON((11 10, 13 15, 22 15, 23 10, 19 4, 11 10))', null),
('Echo',  'POLYGON((2 10, 11 10, 7 1, 3 1, 2 10))', null),
('Foxtrot',  'POLYGON((11 10, 19 4, 17 1, 7 1, 11 10))', 'Delphinuspring');

Así mismo, los delfines que cuya actividad estamos estudiando vendrían dados por estos datos:

INSERT INTO Dolphins (family_name, last_position, movement) VALUES 
('Javadelphis', 'POINT(7 18)', 'LINESTRING(6 18, 7 18)'),
('Javadelphis', 'POINT(11 20)', 'LINESTRING(12 18, 16 18, 16 20, 11 20)'),
('Javadelphis', 'POINT(10 17)', 'LINESTRING(10 17, 9 18, 11 18, 10 17)'),
('Javadelphis', 'POINT(14 19)', 'LINESTRING(9 17, 10 13, 16 13, 20 17, 20 21, 14 19)'),
('Javadelphis', 'POINT(25 25)', 'LINESTRING(8 18, 10 20, 10 24, 17 21, 25 25)'),
('Lagenonaut', 'POINT(7 12)', 'LINESTRING(4 15, 7 12)'),
('Lagenonaut', 'POINT(8 13)', 'LINESTRING(12 14, 10 12, 9 12, 8 13)'),
('Lagenonaut', 'POINT(11 14)', 'LINESTRING(9 14, 9 16, 11 16, 11 14)'),
('Lagenonaut', 'POINT(22 12)', 'LINESTRING(9 11, 15 10, 22 12)'),
('Delphinuspring', 'POINT(12 5)', 'LINESTRING(12 3, 13 4, 12 5)'),
('Delphinuspring', 'POINT(14 5)', 'LINESTRING(16 2, 14 3, 14 5)'),
('Delphinuspring', 'POINT(5 12)', 'LINESTRING(10 3, 10 4, 5 11, 5 12)');

 

Hora de trabajar

Inicialmente el sistema de control ofrecerá un breve resumen que permita saber de un vistazo si todo está yendo correctamente. El primero de los datos que se muestran es una gráfica indicando, para cada región el número de delfines que hay en ese momento en ella. Esto no es ningún problema con lo que hemos aprendido y podríamos solucionarlo con una consulta similar a la siguiente:

SELECT r.id AS region_id, r.name AS region_name, COUNT(d.id) AS number_of_dolphins
FROM Regions r
LEFT JOIN Dolphins d 
ON ST_Within(d.last_position, r.area) 
GROUP BY r.id;

Además de esto también se muestra, para cada familia el número de delfines que la componen y cuál es la región en la que está asentada, junto con la distancia media entre los delfines de una misma familia. El listado aparece en orden descendente por esta distancia. En este caso la consulta es ligeramente más compleja por tener que juntar distintos datos pero esta podría ser una forma de obtener lo que se nos pide. Quizá no sea la más elegante ni la más óptima pero permite entender el proceso sin demasiadas dificultades.

SELECT families.family_name, number_of_dolphins, region_name, average_distance
FROM 
	(SELECT family_name, COUNT(*) AS number_of_dolphins FROM Dolphins GROUP by family_name) AS families
	JOIN (SELECT settled_family, name AS region_name from Regions) AS regions ON families.family_name = regions.settled_family
	JOIN (
		SELECT family_name, avg(distance) AS average_distance
		FROM (
			SELECT ST_Distance(a.last_position, b.last_position) AS distance, a.family_name AS family_name
			FROM Dolphins a JOIN Dolphins b
			ON a.family_name = b.family_name AND a.id < b.id
		) AS familiar_dolphins
		GROUP BY family_name
	) AS average_distances ON  families.family_name = average_distances.family_name
ORDER BY average_distance DESC;

Además de esto también tienen una serie de alertas que les avisa cuando los delfines tienen comportamientos extraños. Por un lado son capaces de saber cuándo un delfín ha salido fuera del área controlada. Esto se consigue con la siguiente consulta, que nos devuelve los delfines que cuya última posición conocida no está dentro de la unión de las áreas de todas las regiones:

SELECT id, family_name, ST_AsText(last_position) as last_position
FROM Dolphins
WHERE NOT ST_Within(last_position, (SELECT ST_Union(area) FROM Regions));

Por otro lado también es interesante saber qué regiones han sido menos visitadas por los delfines, puesto que eso podría suponer que hay algo raro y un equipo de investigación debería ir lo antes posible a comprobarlo. Para lograrlo sería suficiente con una consulta similar a esta, en la que por tener más datos para el ejemplo estamos mostrando las distintas regiones con el número de delfines que las han cruzado pero se podría limitar al primer resultado:

SELECT r.name, COUNT(d.id) as number_of_dolphins
FROM Regions r
LEFT JOIN Dolphins d
ON ST_Intersects(r.area, d.movement) 
GROUP BY r.name
ORDER BY number_of_dolphins;

La última de las alertas que tienen les indica cuándo un delfín se ha alejado demasiado de los demás. Los delfines son bastante sociables y no suelen viajar solos, así que esto también podría ser una mala señal. La aproximación que yo propongo utiliza la función ST_Buffer para fines didácticos, pero también podría calcularse la distancia mínima entre los distintos delfines y seguir en esa dirección.

SELECT a.id, a.family_name AS family_name, ST_AsText(a.last_position) AS last_position
FROM Dolphins a
LEFT JOIN Dolphins b
ON a.id != b.id AND ST_Within(b.last_position, ST_Buffer(a.last_position, 9))
WHERE b.id IS NULL;

-- Cálculo de la distancia mínima
SELECT first_id, min(distance)
FROM (
	SELECT a.id AS first_id, b.id, ST_Distance(a.last_position, b.last_position) AS distance
	FROM Dolphins a JOIN Dolphins b
	ON a.id != b.id) AS distances
GROUP BY first_id;

Una vez hemos entendido el sistema nos avisan de que por fin van a empezar a recibir datos de la última posición conocida de cada delfín en tiempo casi real. Nos piden que, para empezar, cuando nos llegue un nuevo dato actualicemos tanto la posición actual como el registro de movimientos de cada delfín. Un simple update con una de las funciones que ya hemos visto debería ser suficiente para lograr nuestro cometido:

UPDATE Dolphins 
SET last_position = 'POINT(18 18)', movement = ST_AddPoint(movement, 'POINT(18 18)') 
WHERE id = 5;

Para dejar coherentes todos los datos, deberíamos ser capaces de actualizar también las regiones en caso de que una familia se haya trasladado a otra. Puesto que la complejidad en este caso está más orientada a SQL puro y duro y no vamos a ver nada nuevo de PostGIS os lo dejo a vosotros si os apetece resolverlo.

Conclusiones

Soy consciente de que hemos visto muchas cosas y ahora mismo debéis tener la cabeza a punto de echar humo, pero ya estamos terminando.

Me gustaría que al terminar de leer esto tengáis al menos los conceptos claros y sepáis que este tipo de bases de datos existen. Las bases de datos espaciales son bastante desconocidas por norma general, en gran parte debido a que tienen usos muy concretos y no solemos necesitarlas. Sin embargo, tienen una potencia enorme y abren todo un abanico de posibilidades en cuanto al almacenamiento y tratamiento de los datos. Ya no solo en el aspecto funcional, puesto que llegado el caso podríamos simularlo nosotros a mano con una base de datos normal y código externo, sino por la facilidad para trabajar con ellas y las optimizaciones que implementan a la hora de trabajar con los datos espaciales. Fijaos que, estando yo lejos de ser un experto, hemos montado en un momento un ejemplo bastante curioso con el que trabajar mano a mano con esta tecnología.

Espero que hayáis aprendido algo nuevo y quizá os haya picado el gusanillo para investigar algo más. Por mi parte, y si veo que ha gustado, quizá en el futuro haga un par más de entradas profundizando en temas que he tenido que dejar de lado en esta ocasión.

Imágenes

La entrada PostGIS para entender las bases de datos espaciales se publicó primero en Adictos al trabajo.

Automatiza tus pruebas desde 0 con Selenium, WebDriver e IntelliJ – Parte III

$
0
0

Introducción

Llegamos al último de los post sobre la Automatización de Pruebas que empezamos con Automatiza tus pruebas desde 0 con Selenium, WebDriver e IntelliJ – Parte I y continuamos con Automatiza tus pruebas desde 0 con Selenium, WebDriver e IntelliJ – Parte II.

En esta ocasión aprenderemos a generar XPath y CSS personalizados desde los atributos HTML, identificaremos objetos usando los Locators de ambos y finalizaremos con un par de ejercicios en los que veremos la integración de Sikuli con Selenium y WebDriver.

No es objeto de este tutorial profundizar en Sikuli aunque sí se darán unas pinceladas en los comentarios del código para entender mejor los ejercicios.

Índice

1. Cómo generar XPath personalizados desde los atributos HTML.

Un XPath como hemos anteriormente es la ruta entre las etiquetas html que debemos seguir para identificar a un elemento concreto de la web.

El formato para generar un XPath personalizado es:

//TagName[@attribute=’Value’]

//TagName[@attribute=”Value”]

Ambos casos son válidos, con la única diferencia de que en el segundo se ha utilizado el caracter de escape para poder usar las comillas dobles en Java.

Continuando con el ejemplo que venimos usando desde los post anteriores y en la ventana principal, donde se nos solicita ingresar nuestro correo y password, vamos a generar el XPath personalizado para el objeto que nos solicita el email.

Echando un vistazo al código del objeto desde la Herramientas para el Desarrollador observamos:

Nuestro TagName es input, como attribute podemos seleccionar cualquiera de los disponibles en color marrón. Seleccionamos el id. Por último, añadimos el Value (valor) del atributo seleccionado. Por tanto, nuestro XPath personalizado queda:

//input[@id=’email’]

Otras variantes validas también pueden ser:

//input[@class=’email’]

//input[@name=\»email]

Si usamos ChroPath, la extensión del navegador que nos facilita la tarea podemos visualizar que el XPath es el indicado en el primer ejemplo. Además, podemos visualizar la ruta absoluta del objeto:


2. Cómo generar CSS personalizados desde los atributos HTML

Una vez que se conoce como generar el XPath personalizado generar el CSS no tiene mucho misterio, este se hace de la misma forma pero pierde la @:

//TagName[attribute=’Value’]

//TagName[attribute=”Value”]

Continuando con el ejemplo anterior de Facebook:

Comprobamos que generar el CSS no tiene mucha historia:

//input[id=’email’]

3. Identificando objetos con Texto usando XPath Locators y LinkText

A veces nos interesará buscar objetos por su texto o bien por un enlace que tengan asignado para estas situación podremos encontrar el objeto con:

//*[text()=’Texto_a_buscar’]

Sólo se recomienda utilizar cuando no se tengan atributos. Tened en cuenta que estáis dependiendo de un texto variable.

En caso de un enlace que se quiera buscar podemos usar el Locator LinkText. Volviendo al ejemplo de Facebook, suponiendo que queramos hacer click sobre la frase «¿Has olvidado los datos de la cuenta?» (que es un enlace), se utilizará:

driver.findElement((By.linkText("¿Has olvidado los datos de la cuenta?"))).click();


4. Identificando objetos con CSS Selectors Locators

Ya vimos anteriormente que la estructura de que debe mantener nuestro CSS Locator personalizado debe ser:

TagName[attribute=’Value’]

con la particularidad de que en CSS se hace más simple aún al no ser necesario indicar el TagName:

[attribute=’Value’]

En el ejemplo anterior de Facebook:

[class=’inputtext’]

Con lo cual si queremos introducir de forma automatizada nuestro email en la caja correspondiente podríamos hacerlo así:

public static void main(String[] args) {
System.setProperty("webdriver.chrome.driver","/Users/Fbogas/Desktop/Sikuli/chromedriver");

WebDriver driver = new ChromeDriver();
driver.get("http://facebook.com"); //URL in the browser
driver.findElement(By.cssSelector("[class=’inputtext’]")).sendKeys("micorreo@autentia.com");
}

5. Ejercicio Práctico I. Integración de Sikuli con Selenium y WebDriver

Para poder realizar el siguiente ejercicio se puede utilizar tanto IntelliJ como SikuliX, siendo con este último una automatización mucho más rápida (apenas lleva 2 minutos).

En nuestro caso vamos a resolverlo con IntelliJ empleando las librerías tanto de Selenium como de Sikuli y veremos que ambos se integran perfectamente sin generar conflictos.

El ejercicio consiste en acceder a una Web aceptando las Cookies y cambiando la configuración de la misma a modo oscuro. La web en cuestión es: www.finofilipino.org

Previamente, vamos a capturar las imágenes que se van a utilizar en el proceso de automatización (emplead para ello la capturadora de SikuliX o bien cualquier herramienta para el caso ya que las nativas de Windows y OS X suelen dar problemas). Y posteriormente se almacenan en una carpeta de fácil acceso.

En mi caso sólo necesito las siguientes:

El código para automatizar el proceso es:

package com.autentia.sikulix;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.sikuli.basics.Settings;
import org.sikuli.script.FindFailed;
import org.sikuli.script.Pattern;
import org.sikuli.script.Screen;

public class Sikuli {
public static void main(String[] args) throws FindFailed, InterruptedException {

//Primero creamos un objeto Screen para interactuar con la pantalla
Screen screen = new Screen();
//A continuación generamos un patrón por cada imagen con la que vamos a interactuar
//en nuestro caso con la imagen ACEPTO, para aceptar las Cookies y la imagen de la
//bombilla que cambiara al modo oscuro.
Pattern pattern = new Pattern("/Users/Fbogas/Desktop/Images/acepto.png"); 
Pattern pattern2 = new Pattern("/Users/Fbogas/Desktop/Images/bombilla.png");

//Creamos nuestro WebDriver de Chrome
System.setProperty("webdriver.chrome.driver","/Users/Fbogas/Desktop/Sikuli/chromedriver");
WebDriver driver = new ChromeDriver();

//Accedemos a la web indicada
driver.get("https://finofilipino.org/");
System.out.println(driver.getTitle());
System.out.println(driver.getCurrentUrl());
System.out.println(driver.getPageSource());
//Le indicamos que haga click en el primer patrón, la imagen de ACEPTO
screen.click(pattern);
Settings.MoveMouseDelay = 3; //Da a la aplicación 3 segundos antes de hacer la siguiente acción
//Hacemos doble click sobre la imagen de la bombilla, patrón 2 y cambiamos al modo oscuro.
screen.doubleClick(pattern2);
   }
}

6. Ejercicio Práctico II. Acceder a la cuenta de Gmail y borrar la carpeta Spam

En este otro ejercicio lo que se pide es automatizar el borrado de la carpeta Spam de una cuenta de correo de Gmail utilizando Sikuli y Selenium bajo el IDE de IntelliJ.

Para este ejercicio las imágenes que se han capturado han sido:

El lector puede detenerse aquí e intentar realizar el ejercicio por su cuenta antes de leer la solución.

Para la solución mostrada, se da por hecho que las credenciales de acceso a la cuenta de gmail están almacenadas y por tanto no se deben introducir. Siendo el acceso a la bandeja de entrada del correo directo.

Solución:

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.sikuli.basics.Settings;
import org.sikuli.script.FindFailed;
import org.sikuli.script.Pattern;
import org.sikuli.script.Screen;

import static org.sikuli.script.Mouse.WHEEL_UP;

public class Sikuli_Gmail {
    public static void main(String[] args) throws FindFailed, InterruptedException {
        Screen screen = new Screen();
        Pattern pattern = new Pattern("/Users/Fbogas/Desktop/SikuliX/Spam/recibidos.png");
        Pattern pattern2 = new Pattern("/Users/Fbogas/Desktop/SikuliX/Spam/mas.png");
        Pattern pattern3 = new Pattern("/Users/Fbogas/Desktop/SikuliX/Spam/menos.png");
        Pattern pattern4 = new Pattern("/Users/Fbogas/Desktop/SikuliX/Spam/spam.png");
        Pattern pattern5 = new Pattern("/Users/Fbogas/Desktop/SikuliX/Spam/selector.png");
        Pattern pattern6 = new Pattern("/Users/Fbogas/Desktop/SikuliX/Spam/eliminar.png");


        System.setProperty("webdriver.gecko.driver","/Users/Fbogas/Desktop/Sikuli/geckodriver");
        WebDriver driver = new FirefoxDriver();

        driver.get("http://gmail.com"); //Suponemos que ya están introducidas las credenciales.

        screen.wheel(pattern, WHEEL_UP, 10); //Se va a la imagen recibidos y hace un Scroll hasta ver el "Más"
        screen.click(pattern2);  //Hacemos click en el "Más"
        Settings.MoveMouseDelay = 2; //Da a la aplicación 2 segundos antes de hacer la siguiente acción
        screen.wheel(pattern3, WHEEL_UP, 10);  //Ahora que se ha convertido en "Menos" buscamos "SPAM"
        screen.click(pattern4);         //Hacemos click en SPAM
        Settings.MoveMouseDelay = 1;    //Esperamos 1 segundo
        screen.click(pattern5);         //Hacemos click en el selector
        Settings.MoveMouseDelay = 1;    //Esperamos 1 segundo
        screen.click(pattern6);         //Hacemos click en "eliminar definitivamente"
        Settings.MoveMouseDelay = 1;    //Esperamos un segundo
        driver.close();                 //Cerramos el navegador

    }
}

Solución desde el IDE Sikulix:

Ejecución del Script:

 

7. Conclusiones

A lo largo de esta serie de post hemos partido desde 0 en la automatización de pruebas, instalando las herramientas necesarias, configurándolas, hemos aprendido un poco más de ellas y hemos practicado con ejercicios didácticos lo aprendido.

Es muy probable que el lector en este punto tenga muchas preguntas sobre cómo puede automatizar ciertas tareas. En este punto recomiendo que se practique lo aprendido y se profundice en el aprendizaje mediante el estudio de la documentación y los foros existentes.

Si pensabas que la automatización de pruebas con Selenium y/o Sikuli acababa aquí te indico que acabas de ver la punta del iceberg ya que es bastante más amplio y este tutorial es sólo una introducción. Fuera se han quedado por ejemplo la automatización de pruebas para sistemas Android e IOs y las herramientas que se utilizan; por mencionar una destacaría Appium que utiliza el WebDriver de Selenium.

Si quieres saber más sobre pruebas te remito a los últimos artículos sobre Test escritos en Adictos y que puedes acceder desde aquí.

La entrada Automatiza tus pruebas desde 0 con Selenium, WebDriver e IntelliJ – Parte III se publicó primero en Adictos al trabajo.

Como configurar un proyecto SCRUM con Redmine y Agile plugin

$
0
0

1. Introducción

Voy a partir de la instalación rápida que había comentado en la entrada anterior Como instalar Redmine y su Agile Plugin utilizando Docker y la interfaz gráfica Kitematic.

Doy por hecho que tenemos un Redmine levantado y funcionando, pero aún no hemos realizado ninguna configuración. Lo que vamos a hacer a continuación es realizar la configuración necesaria par tener un proyecto SCRUM. Será necesario tocar configuración del propio Redmine y del plugin, justo en eso es en lo que se va a centrar esta publicación.

¡Super-importante! Cuando accedamos por primera vez a la Administración del proyecto nos aparecerá la siguiente pantalla:

Imagen que muestra la confirmación para la carga de configuración por defecto
Redmine carga de configuración por defecto

Si no pulsamos Load the default configuration, tendremos que configurar posteriormente a mano desde cero los roles, trackers, estados y el workflow de trabajo. En este tutorial se usan los elementos de la configuración por defecto aunque se adapten a las necesidades.

Si ya estás familiarizado con Redmine y cuentas con una instancia configurada, te recomiendo saltar directamente a los puntos 3.1 y 3.3.

2. Entorno

Para realizar la instalación y el tutorial he utilizado:

Hardware: MacBook Pro 15″ 2018 – 2,2GHz i7 – 32GB (macOS Mojave 10.14.4)

Software:

3. Configuración

Vamos a empezar a trabajar con el plugin Agile. En mi caso se trata de la version gratuita Light que tiene algunas limitaciones respecto a la versión PRO, podéis consultar el detalle de estas limitaciones aquí.

Características de la versión PRO de Redmine
Lista de características adicionales de la versión PRO del plugin agile de redmine

 

Las funcionalidad que podemos echar de menos bajo mi punto de vista son las siguientes:

  • Guardar paneles con columnas y ajustes customizadas, sí, lo confieso, soy muy fan de tocar los paneles y ajustarlos.
  • Diagramas de calles horizontales, también conocidos como swimlanes. En equipos con especialistas o proyectos por componentes, son muy útiles.
  • Límites del Trabajo en Proceso, el mal llamado WIP (mal llamado por que la traducción directa sería Work In Progress y realmente es un limite que aplicamos a este) para controlar que cerremos cosas y no solo abramos teniendo una falsa sensación de avance.
  • Campos de tarjeta adicionales, no voy a entrar en detalle, aunque muchas veces es muy útil por las características de nuestra organización o de nuestro equipo contar con campos específicos.
  • Gráficas adicionales: Burnup, Flujo acumulativo, Velocidad, Plazo. Para mí la velocidad y en diagrama de flujo acumulado son dos herramientas muy útiles para ver cómo estamos trabajando y como nos llega la demanda.

3.1 Configuración general del plugin

Como adelantaba unas lineas más arriba, la configuración necesaria podemos diferenciarla en varios bloques, configuración propia del plugin agile, configuración de aspectos generales de Redmine y  configuración y configuración específica del proyecto.

Vamos a comenzar con la configuración del plugin, para ello, desde el menú de la parte superior izquierda vamos a Administration > Agile.

Pantalla principal de Redmine
Pantalla principal de Redmine

En la pantalla de configuración voy a establecer los siguientes parámetros:

Parámetros de configuración del plugin agile de Redmine
Parámetros de configuración del plugin agile de Redmine
  • Agile board items limit: Mantenemos el valor por defecto 500
  • Default card fields: Vamos a añadir a los campos por defecto (Tracker y Assignee) los campos, IssueID para poder tener identificada cada tarjeta, Parent task para poder tener relacionada la Épica (en el caso de utilizar historias de usuario) o su Tarea agrupadora (padre) y Story points.
  • Estimate units: Es el sistema de estimación para  planificar el trabajo que queremos implementar, seleccionamos Story points en lugar de horas.
  • Tracker for story points: Solo tenemos disponible la opción All, así que no hay duda.
  • Default chart: Es el informe de referencia que usaremos para el seguimiento de los avances, ya que hemos seleccionado como unidad de estimación los story points, tenemos que marcar aquí Burndown chart (Story points).
  • Exclude weekends from ideal work: Para no presentar los fines de semana en las gráficas (asumiendo que nuestro equipo trabaja de lunes a viernes).
  • Time entries based charts issues limit: Mantenemos el valor por defecto 1000
  • Hide closed issue data: Para que no se muestre la información de las tareas que hayamos finalizado lo marcamos con el check.
  • Auto assign on move: Si queremos simplificar un poco la vida a las personas del equipo esta función nos viene muy bien, de forma que cuando transiten una tarea, esta se asignará automaticamente a la persona que la está moviendo, nos ahorramos una operación de asignación.

OJO: Esto aplica si el equipo es auto organizado, si hay una asignador, lo que hará es entorpecer su tarea).

  • Inline comment: Permite editar los campos de nuestra issue/tarea sin tener que abrirla para editar, nos hace más rápido el introducir información.

Recordad pulsar el botón Apply para que se guarden los cambios.

3.2 Configuración general de Redmine

3.2.1 Creación de grupos

Para realizar esta configuración vamos a la sección Administration > Groups > New group. Por defecto nos aparecerán los grupos Anonymous users y Non member users.

Imagen que muestra la pantalla de administración de grupos de Redmine
Administración de grupos de Redmine

Voy a usar una estructura propia de SCRUM, es decir crearemos los grupos:

  • Development Team
  • Scrum Master
  • Product Owner

En la parte superior derecha de la zona de trabajo (espacio blanco, ver imagen anterior) tenemos la opción New group. Lo único que tenemos que hacer es introducir un nombre y pulsar Create (Create and Continue nos permitirá crear varios grupos seguidos). Nos aparecerá un mensaje indicando que se ha creado correctamente y veremos listados los grupos.

Imagen que muestra la lista de grupos de usuarios de Redmine
Lista de grupos de usuarios de Redmine

3.2.2 Creación de usuarios

El siguiente paso es añadir los usuarios. Se hace desde administración: Administration > Users  > New user.

Imagen que muestra una pantalla con los campos a rellenar al crear un nuevo usuario en Redmine
Pantalla de nuevo usuario en Redmine

Introducimos todos los datos, prestando atención a:

  • Hide my email address. Si es un proyecto donde acceden personas de diferentes empresas o dominios, tenemos que asegurarnos no estar afectados por ninguna política de privacidad tipo RGPD. Por defecto recomiendo activarla.
  • Must change password at next logon. Es una buena práctica que el usuario modifique la clave en su próximo acceso (nos aseguramos de que nadie sabe nuestra clave aunque sea el admin). Es algo que en usuario genéricos no aplicaría, pero este no es el caso.
  • Email notifications. Son los avisos que nos va a enviara Redmine según la actividad del proyecto o proyectos en los que estemos trabajando.
    • I don’t want to be notified of changes that I make myself. Activamos esta opción para que Redmine no nos avise de nuestras propias acciones. ¿Si lo acabo de hacer no sabré ya que lo he  hecho? (Tiene casos de aplicación y puede ayudarnos a darnos cuenta de errores que comentemos, pero bajo mi punto de vista es una sobrecarga de información vs el beneficio que aporta).
    • Opciones de notificación:
      • For all events in all my projects. Si pensamos en un equipo de 6 personas trabajando con la herramienta a diario ya podemos pensar en un volumen alto de avisos, si multiplicamos por más personas y/o proyectos… No aconsejo esta opción.
      • Only for things I watch or I’m involved in. Esto implica que si te han mencionado en una tarea vas a recibir notificaciones. Permite no perder comentarios  si alguien necesita de tu ayuda aunque no estés asignado a la tarea o la hayas creado tu. Particularmente me parece que  hay mecanismos mejores para gestionarlos (hablar) y reducir esta carga asíncrona de comunicación. Pero cada uno debe considerar su caso.
      • Only for things I watch or I’m assigned to. Solo si me han asignado una tarea y hay cualquier cambio nos será notificado.
      • Only for things I watch or I am the owner of. Solo si soy el creador de una tarea estaré informado de los cambios.

Mi recomendación es usar la opción «assigned to» y utilizar la opción watch para aquellas tareas que creo personalmente. De esta forma si alguien me asigna algo seré consciente y si creo yo tareas también estaré al tanto de los cambios. En cualquier caso siempre debéis considerar la necesidades y el rol que desempeña el usuario en el proyecto.

Con esto nos quedaría algo así:

Imagen que muestra la pantalla de creación de usuario de Redmine con los campos cumplimentados
Ejemplo de usuario de Redmine

Podemos crear tantos usuario como consideremos para nuestro proyecto.

Si desde el listado de usuario hacemos clic en el que acabamos de crear veremos que todos estos cambios afectan únicamente a la pestaña General. Hacemos clic en la pestaña Groups, seleccionamos a que grupo de los que hemos creado queremos que pertenezca este usuario y pulsamos Save. En mi caso voy a asignarlo al grupo de Product Owners.

Imagen que muestra como asociar un usuario a un grupo de Redmine
Cómo asociar un usuario a un grupo de Redmine

Veréis que  aún queda otra pestaña Project a la que volveremos una vez hayamos creado nuestro proyecto que puedan trabajar con él.

A nivel de Grupos y Roles decimos que puede hacer de forma general un usuario/grupo, pero el acceso al proyecto determina si pueden hacerlo sobre ese. Aunque un usuario tenga grupos y roles, si no tiene permiso sobre un proyecto determinado no podrá hacer nada.

3.2.3. Creación de Roles y Permisos

Ya tenemos los grupos de usuarios de nuestro proyecto y al menos un usuario para trabajar. Ahora lo importante es decir qué pueden hacer las personas o grupos de personas en nuestro proyecto y para ello tenemos que configurar los Roles. Accedemos desde Administration > Roles and permissions

Para simplificar os recomiendo que exista un rol asociado a cada uno de los grupos que habéis creado (para no empezar de cero, he modificado los tres roles por defecto que trae Redmine renombrandolos), de esta forma es más sencillo gestionar posteriormente la configuración:

Imagen que muestra el listado de roles disponibles en Redmine
Roles disponibles en Redmine para configurar sus permisos.

Muchos pensaréis que esto es obvio, pero prefiero comentarlo para los menos acostumbrados. Si usamos grupos, asociamos los usuarios a estos, y los permisos los aplicamos sobre un grupo, de esta forma todos los miembros obtienen la misma configuración sin que tengamos que gestionar uno a uno. Pensad en grupos de 20-30 personas, sería una locura.

Cada uno de los roles tiene asociados unos permisos de Redmine. Podemos verlos en detalle pulsando en uno de los roles, en mi caso el de Product Owner. Podemos ver que hay tres bloques bien diferenciados que podemos configurar:

Pantalla que muestra el detalle de los permisos de un rol de Redmine
Detalle de los permisos de un rol de Redmine

Cómo entrar en detalle de todas las configuraciones sería demasiado extenso, voy indicar únicamente aquellas que considero interesantes o aquellas que es necesario activar para un proyecto agile.

  • Role:
    • Issues can be assigned to this role, este check indica si es un role al que se pueden asignar tareas o no. (ej, podemos tener una persona de la organización que pueda acceder al proyecto a ver cosas, pero no queremos que se le asignen tareas). En nuestro caso al Product Owner sí que va a poder asignarse  tareas
      • Issues visibility, All non private issues, de esta forma podrá ver todos los issues del proyecto.
      • Time logs visibility, vamos
        • All time entries
      • Users visibility,
        • Members of visible projects
Imagen que muestra el detalle de los permisos generales de visibilidad de issues de un proyecto Redmine
Detalle de los permisos generales de visibilidad de issues de un proyecto Redmine
  • Permissions: Dejamos la configuración que muestra las siguientes imágenes.

Imagen que muestra el detalle de los permisos de Proyecto, Agile, Forums, Calendar, Documents, Files y Gantt de Redmine

    Detalle de los permisos de Proyecto, Agile, Forums, Calendar, Documents, Files y Gantt de Redmine
Imagen que muestra los detalles de los permisos de seguimiento de tareas de Redmine
Detalles de los permisos de seguimiento de tareas de Redmine
Imagen de detalle de los permisos de News, Repository, Time tracking y Wiki de Redmine
Detalle de los permisos de News, Repository, Time tracking y Wiki de Redmine
  • Issue tracking: Antes de configurar este punto es necesario establecer los «trackers» del proyecto, es decir los tipos de issues que queremos utilizar. Pulsamos Save antes de hacer nada para no perder los cambios de configuración. Ahora vamos a Administration > Trackers. Por defecto habrá 3 tipos, Errores, Tareas y Soporte.

Imagen que muestra los trackers (tipos de issues) de un proyecto Redmine

Aunque no sea específico de SCRUM voy a usar un esquema de Historias de usuario (podemos emplear esquemas para productos/proyectos mucho mas complejos como este o utiliza simplemente de tareas, pero quería diferenciarlo un poco de un proyecto estándar), por lo que tengo que modificar estos tipos. Voy a emplear los tipos:  Épica,  Historia, y Subtarea, y conceptualmente podemos considerarlos en tres niveles jerárquicos diferentes (de mayor a menor).

Hacemos clic en el tracker Errores y modificamos la configuración para que quede así (recordad pulsar Save al terminar):

Imagen que muestra la configuración de un tracker (issue type) en Redmine
Configuración de un tracker (issue type) en Redmine

Repetimos la operación para los otros dos tipos de tareas con las siguientes configuraciones.

Imagen que muestra la configuración de una tarea de tipo historia en Redmine
Configuración de una tarea de tipo historia en Redmine
Imagen que muestra la configuración de una tarea de tipo subtarea en Redmine
Configuración de una tarea de tipo subtarea en Redmine

Ahora que hemos terminado volvemos a Administration > Roles and permissions y seleccionamos de nuevo el usuario que estábamos editando (Producto Owner en mi caso). Nos desplazamos hasta la parte inferior donde está la sección issue tracking y marcamos los permisos sobre estos tipos de tareas de la siguiente manera:

En mi caso no quiero que las subtareas sean funcionales, y quiero que sean únicamente técnicas por lo que no le he otorgado permisos al Producto Owner para poder gestionarlas, aunque si para poder verlas y hacer comentarios sobre ellas por si el equipo de desarrollo los necesita.

configuración de permisos de un role sobre los tipos de tareas de un proyecto Redmine.
Configuración de permisos de un role sobre los tipos de tareas de un proyecto Redmine.

3.2.4. Issues statuses

Los estados podemos decir que es la situación en la que se encuentra un trabajo a lo largo del tiempo hasta que se termina, vamos su ciclo de vida. Este ciclo es muy peculiar para cada equipo, y en  mi caso no voy a hacer grandes cambios al dee por defecto que trae los estados: Nueva, En curso, Resuelta, Comentarios, Cerrada y Rechazada. El único que voy a modificar es Comentarios, y lo voy a sustituir por Blocked (sí, a sabiendas que esto rompe el flujo del panel Kanban) porque quiero reflejar el caso de un equipo con un elevado grado de dependencias.

Imagen que muestra los posibles estados por lo que pasan las tareas de un proyecto Redmine.
Estados por lo que pasan las tareas de un proyecto Redmine.

NOTA: Lo importante de esta configuración es el check issue closed, que establecerá cuando se han terminado las actividades relacionadas con esa tarea. Tanto al añadir nuevos estados con la función New status, como al editar haciendo clic un estado existen, recordad tras realizar cambios pulsar en Save.

3.2.5. Workflows

Esta sección no la vamos a modificar para no alargar en exceso el tutorial, en ella podemos ajustar dos aspectos interesante. Las transiciones entre estados que puede hacer un rol (podemos no dejar que el equipo de desarrollo de por finalizado el trabajo sin que lo vea el Product Owner, para ello no permitiríamos la transición de Resuelta a Cerrada para el Role de Development Team).

Y por otro lado también podemos controlar los permisos sobre los campos de una tarea, limitando el acceso a algunos de ellos.

Hay algunas secciones más de configuración como Campos personalizados y Enumeraciones, pero en mi caso me quedaré con la configuración estándar de estos.

Sí queréis profundizar en los aspectos de configuración os recomiendo leer la documentación oficial que está muy bien, la única «pega» es que está en inglés.

3.3 Configuración de un nuevo proyecto

Para crear un nuevo proyecto nos vamos a la opción Projects, a la izquierda en el menú superior.

Imagen que muestra la pantalla de Administración de proyectos en Redmine
Administración de proyectos en Redmine

En la parte superior derecha del área de trabajo veremos una funcionalidad New project, hacemos clic en ella.

Se cargará la pestaña de configuración Projects introduciremos los datos que nos pide como obligatorios y estableceremos las siguientes opciones:

  • Desactivar Time tracking
  • Desactivar Gantt
  • Descativar Forums
  • Activar Agile: Esto es fundamental, sino el proyecto será un proyecto tradicional de Redmine. Ya tendremos nuestro  proyecto creado.
Imagen que muestra la pantalla de creación de un proyecto en Redmine
Pantalla de creación de un proyecto en Redmine

Ahora vamos a personalizar nuestro proyecto, veremos que se carga un menú de segundo nivel con las siguientes opciones:

Members, desde donde podemos definir qué miembros y roles de Redmine podrán trabajar con nuestro proyecto. Pulsamos la opción New member y en la capa modal seleccionamos los grupos y roles de SCRUM que habíamos creado.

Pantalla que muestra como añadir los grupos de usuarios que pueden acceder a un proyecto Redmine.
Grupos de usuarios y roles que pueden acceder a un proyecto Redmine.

Issue tracking, Aquí establecemos que tipos de tareas queremos seguir. Por defecto aparece marcado Historias, pero activamos también el resto. No vamos a marcar ningún asignado por defecto a las tareas (cada miembro del equipo irá cogiendo), y tampoco establecemos una versión por defecto.

Imagen que muestra la configuración por defecto para los tipos de tareas de un proyecto Redmine.
Configuración por defecto para los tipos de tareas de un proyecto Redmine.

Versions, lo fundamental es saber que las versiones son las que nos van a permitir gestionar los sprints de SCRUM. Aquí ya dependerá del modo de trabajo del equipo, podemos tener una numeración de versión software «tradicional» o podemos tener un identificar de agrupación funcional, yo me he inclinado por este último y he creado los siguientes:

Imagen que muestra las versiones que agrupan nuestras tareas en un proyecto Reedmine
Versiones que agrupan nuestras tareas en un proyecto Redmine

En este caso como Redmine gestiona Sprints no va a ser un versionado ajustado a una realidad, ya que es muy difícil que cada sprint podamos cumplir con tener un MVP, MMP, MMR… Si queréis conocer más detalles sobre agrupadores funcionales podéis consultar en este enlace que he mencionado anteriormente.

La configuración de ejemplo para la versión MPV o MVP podría ser:

Imagen que muestra el detalle de configuración de una versión de tipo MVP en Redmnine.
Configuración de una versión de tipo MVP en Redmnine.

No vamos a tocar la configuración de las secciones Issue categories y Repositories.

3.5 Trabajar con el proyecto

3.5.1 Crear issues

Lo primero que vamos a hacer es entrar en nuestro proyecto. Desde el menú de la cabecera accedemos a Projects > Proyecto SCRUM > Issues

Aquí vamos a crear una Épica, tres historias en las que subdivida y tres tareas para nuestra primera historia. Lo del 3 ha sido casualidad =)

Hacemos clic en New issue en la parte superior derecha de la zona de trabajo. a la hora de crear el issue tenemos que tener en cuenta el tipo, es decir el tracker. Vamos a crear la primera de tipo Épica como se muestra en la imagen siguiente.

Imagen que muestra la creación de una Épica en Redmine
Creación de una Épica en Redmine

Ahora vamos a crear una Historia que pertenezca a esta Épica. Repetimos el proceso anterior, pero al crearla, nos aseguramos que el campo Parent task lo rellenamos con la referencia a su Épica, podemos hacerlo con el numérico sí lo sabemos o usando el campo de búsqueda introduciendo el título de la épica.

Imagen que muestra la creación de una Historia en Redmine
Creación de una Historia en Redmine

Repetimos el proceso con alguna historia y podremos ver en el listado de issues cómo van apareciendo.

Imagen que muestra el listado de issues de un proyecto Redmine
Listado de issues de un proyecto Redmine

Si accedemos a la Épica, vemos como nos aparecen las historias asociadas como subtareas aun sin ser trackers de tipo subtarea.

Imagen que muestra las historias que contiene una Épica en Redmine
Historias que contiene una Épica en Redmine

Vamos a pasar al ultimo nivel, la creación de las subtareas, para ello abrimos la historia que hemos creado, en mi caso la Historia 2. Para añadir una subtarea, hacemos clic en la parte derecha del bloque Subtasks en la opción Add.

Imagen que muestra el detalle de una issue de tipo subtarea en Redmine
Detalle de una issue de tipo subtarea en Redmine

La opción Add nos abrirá una pantalla de creación de issue como las que hemos visto donde poder dar de alta la subtask. Podemos ver que ya tiene asociada la Parent task de forma automática.

OJO: Tenéis que prestar atención al campo Tracker, ya que mantiene la selección por defecto que hay en el proyecto, en mi caso Historia, si no lo ajustas no te va a crear Subtareas, sino Historias.

Según vayamos creando las subtareas, irán apareciendo en el bloque de Subtasks de la Historia.

Imagen que muestra las subtareas de una historia en Redmine
Subtareas de una historia en Redmine

Nuestra pantalla de issues debería tener un aspecto más o menos como este:

Imagen que muestra la vista de issues en un proyecto Redmine
Vista de issues en un proyecto Redmine

3.5.2 Crear el sprint

Ahora que ya tenemos nuestras primeras issues lo que tenemos que hacer es crear el Sprint para la iteración de trabajo. Recordad que habíamos dicho que esto se hacía a través de las versiones. Para ello vamos a Projects > Proyecto SCRUM > Settings > Versions.

Lo único que tenemos que hacer es dar la «fecha de finalización del sprint» a cada versión, indicando una fecha en el campo Due date. Yo he tomado las 3 versiones que había creado antes y he establecido fechas de finalización con un decalaje de 2 semanas tomando este tiempo como la duración del sprint.

Imagen que muestra la lista de versiones/sprints en un proyecto Redmine
Lista de versiones/sprints en un proyecto Redmine

3.5.3 Visualización del sprint en el panel

Ya tenemos issues y sprints, ahora lo que necesitamos es que hacer nuestro sprint planning es decir, qué trabajo se compromete el equipo para la iteración. Para ello asociamos la versión a las issues. Vamos a Projects > Proyecto SCRUM > Issues.

Editamos las historias que queremos que formen parte del sprint y les asociamos el Target version  que se corresponde con el sprint. Y aprovechamos también para dar los story points correspondientes.

Imagen que muestra la asociación del sprint a una issue en Redmine.
Asociación del sprint a una issue en Redmine.

Como siempre acordaros de finalizar la acción para que se guarden los cambios, en este caso con Submit. Yo he asociado dos historias a mi primer sprint, y tengo como tentativa una tercera para que entre en el siguiente.

Ya podemos ir a la sección Agile dentro del proyecto, donde podremos ver nuestro panel Kanban (a más de uno le va a «cortocircuitar» esto =) ) para el seguimiento de nuestra iteración de SCRUM.

Imagen que muestra la vista del panel ágil de tareas de un proyecto Redmine
Vista del panel ágil de tareas de un proyecto Redmine

Como podéis ver, todas las issues están en el lateral izquierdo, en la columna Nueva. Si recordáis esta era la configuración que habíamos mantenido en nuestro proyecto para las nuevas issues, por defecto entraban en el estado Nueva.

Ahora mismo en esta visualización tenemos varias cosas mezcladas, Épicas, Historias y Tareas. Si queremos, para simplificar un poco vamos a visualizar solo las Épicas y las Historias (aquí ya «feel free» para determinar que queréis ver vosotros).

Justo encima del panel, encontraréis la sección Filters, y a la derecha la opción Add filter. Aparecerá un desplegable desde el que seleccionar qué concepto queréis filtrar, seleccionamos Tracker.

Imagen que muestra la creación de un filtro en un panel ágil de un proyecto Redmine
Creación de un filtro en un panel ágil de un proyecto Redmine

Ahora configuramos ese filtro para que nos muestre todo lo que no sean subtareas.

Imagen que muestra el filtrado de subtareas en un panel ágil de Redmine
Filtrado de subtareas en un panel ágil de Redmine

Ya tenemos nuestro panel listo y podemos empezar a transitar las issues, voy a desactivar el filtro y comenzaré a mover tareas. Para moverlas solo es necesario arrastrar cada tarjeta a la columna correspondiente.

 

3.5.4 Visualización del Roadmap/Calendario

Adicionalmente al panel ágil, tenemos dos visualizaciones interesantes:

Roadmap, que es una visualización por hitos, donde vemos las versiones/sprints como hitos y el avance de las tareas asociadas.

imagen que muestra la visualización de Rodmap en un proyecto ágil de Redmine
Visualización de Rodmap en un proyecto ágil de Redmine

Calendario, en la que se muestran las fechas de finalización de los sprints (versiones) y podemos navegar directamente para ver qué tareas integran un sprint concreto.

 

3.5.5 Burndown chart

Desde la sección Agile del proyecto seleccionamos en la parte superior derecha de la área de trabajo la opción Charts, donde es posible visualizar el Burndown chart del proyecto.

Tenemos que ajustarlo un poco si lo que queremos es ver el avance del sprint. Para ello en Filters, usamos el desplegable Add filter para seleccionar Target version y poder seleccionar así el sprint que queremos revisar. En mi caso he seleccionado el primero que había llamado MPV.

En Options, modificamos el valor Units para que presente Story points en lugar de issues.

Imagen que muestra un Burndown chart de un proyecto ágil de Redmine
Burndown chart de un proyecto ágil de Redmine

3.6 Conclusiones y consideraciones

Con esto ya tenemos un juego básico de funcionalidad para nuestros proyectos SCRUM.

Redmine es una herramienta no muy potente a nivel visual, pero sí es relativamente intuitiva en su configuración, sin una cantidad de funciones desmesurada que generen una curva de aprendizaje muy grande. Como punto de entrada gratuito es una buena propuesta.

No lo he comentado hasta el momento, pero para una gran cantidad de los ajustes que hemos hecho es necesario contar con los permisos de administrador de Redmine. Si con el rol que tenéis en la actualidad no podéis hacer los ajustes tendréis que hablar con la persona que administre la herramienta.

Para mi gusto, la Versión PRO es casi obligatoria, por lo que si empezáis a ver que se os queda pequeña la funcionalidad, os recomiendo que probéis esta versión antes de plantearos cambiar a herramientas más complejas.

Hay más posibilidades de configuración y visualización, ya es cuestión de que «juguéis» con las opciones hasta adaptarla a vuestras necesidades. Espero que os haya parecido interesante y os ayude a tomar contacto con esta herramienta.

La entrada Como configurar un proyecto SCRUM con Redmine y Agile plugin se publicó primero en Adictos al trabajo.

Data Binding con Recycler View en Android

$
0
0

Índice de contenidos

1. Data Binding Library

Data Binding Library es una librería que permite enlazar componentes visuales de Android (Views) a un modelo de datos. Una de las ventajas es que reduce el boilerplate de las llamadas findViewById aunque si estás familiarizado con Kotlin, en Android la paquetería kotlinx.android.synthetic soluciona este problema importando los componentes de un layout específico.

2. Recycler View

Recycler View es un componente de Android que permite mostrar listas más avanzadas. Digo más avanzadas porque podemos inflar un layout personalizado por cada componente de la lista. Si a este componente añadimos Data Binding, podemos conseguir una funcionalidad más acoplada a la capa de presentación.

3. Dependencias

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.41"
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation "android.arch.lifecycle:extensions:1.1.1"
    implementation "android.arch.lifecycle:viewmodel:1.1.1"
    implementation "androidx.core:core-ktx:1.2.0-alpha03"
    implementation "androidx.recyclerview:recyclerview:1.1.0-beta03"
    implementation 'com.google.android.material:material:1.1.0-alpha09'
}

build.gradle del módulo app

4. Configuración Gradle

apply plugin: 'kotlin-kapt'

...

android{
    dataBinding {
    enabled true
    }
}

build.gradle del módulo app

5. Objetivo

Vamos a mostrar un listado personalizado de posts con un título, contenido y un botón que permita eliminar un post.

recycler view list

6. Layout personalizado

Aquí tenemos el fichero post_view.xml con la vista de cada post.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="@dimen/padding_medium">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/post_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintLeft_toRightOf="@id/remove_post_button"
        app:layout_constraintTop_toBottomOf="@id/remove_post_button">

        <com.google.android.material.textview.MaterialTextView
            android:id="@+id/body_post_value"
            style="@style/content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/body_post_label"
            tools:text="Contenido" />

        <com.google.android.material.textview.MaterialTextView
            android:id="@+id/title_post_label"
            style="@style/title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/title_post_label"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <com.google.android.material.textview.MaterialTextView
            android:id="@+id/title_post_value"
            style="@style/content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="18sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/title_post_label"
            tools:text="Título de ejemplo" />

        <com.google.android.material.textview.MaterialTextView
            android:id="@+id/body_post_label"
            style="@style/title_2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/body_post_label"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/title_post_value" />

    </androidx.constraintlayout.widget.ConstraintLayout>

    <com.google.android.material.button.MaterialButton
        android:id="@+id/remove_post_button"
        style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:icon="@drawable/ic_remove_circle_outline_black_24dp"
        app:iconSize="48dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="@id/remove_post_button" />

</androidx.constraintlayout.widget.ConstraintLayout>

El resultado es el siguiente:
recycler view layout

7. Recycler View

Si conoces Recycler View, el siguiente código te resultará familiar porque no hay nada nuevo a añadir.

class CustomRecyclerViewAdapter(private val posts: MutableList) :
    Adapter() {

    fun removePost(post: Post) {
        val index = posts.indexOf(post)
        if (index != -1) {
            posts.remove(post)
            notifyItemRemoved(index)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
        return CustomViewHolder(
            LayoutInflater.from(parent.context).inflate(
                R.layout.post_view,
                parent,
                false
            )
        )
    }

    override fun getItemCount(): Int = posts.size

    override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
        holder.bind(posts[position])
    }

    inner class CustomViewHolder(view: View) : ViewHolder(view) {

        fun bind(post: Post) {
            itemView.findViewById(R.id.title_post_value).text = post.title
            itemView.findViewById(R.id.body_post_value).text = post.body
            itemView.findViewById(R.id.remove_post_button).setOnClickListener {
                removePost(post)
            }
        }
    }
}

8. Main Activity

En la actividad principal instanciamos el adaptador y lo asignamos al Recycler View definido en el fichero activity_main.xml de nuestra actividad principal. Nada nuevo, ¿verdad?

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        posts_recyclerView.apply {
            layoutManager = LinearLayoutManager(context)
            adapter = CustomRecyclerViewAdapter(mutableListOf())
        }
    }
}

9. View Model

Antes de meternos con data binding me gustaría hablar un poco sobre ViewModel. Es un componente de arquitectura de Android que nos permite manejar los datos de una vista dentro de los ciclos de vida de Android. Android dispone de una serie ciclos de vida que anteriormente, para poder almacenar el estado de una vista, se tenía que realizar a través de los Bundles, que es algo laborioso de manejar entre diferentes estados de una actividad. Gracias a ViewModel resulta mucho más fácil porque el scope es el que se muestra en la siguiente imagen:

Viewmodel lifecycle

Gracias a este componente podemos almacenar el estado de una vista en cualquier ciclo de vida de la actividad y, además, permite compartir el mismo view model entre diferentes vistas. ¡Gracias, Google!

Dentro de nuestro ViewModel tenemos un MutableLiveData con una lista de Posts. Quizás te preguntaras qué es un MutableLiveData. La respuesta es sencilla, un observable:

class MainActivityViewModel(application: Application) : AndroidViewModel(application) {

    var posts: MutableLiveData = MutableLiveData()

    fun loadPosts() {
        posts.value = listOf(
            Post(1, 1, "Title 1", "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum"),
            Post(2, 2, "Title 2", "Body 2 Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum"),
            Post(3, 3, "Title 3", "Body 3 Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum"),
            Post(4, 4, "Title 4", "Body 4 Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum"),
            Post(5, 5, "Title 5", "Body 5 Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum"),
            Post(6, 6, "Title 6", "Body 6 Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum"),
            Post(7, 7, "Title 7", "Body 7"),
            Post(8, 8, "Title 8", "Body 8"),
            Post(9, 9, "Title 9", "Body 9"),
            Post(10, 10, "Title 10", "Body 10 Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum"),
            Post(11, 11, "Title 11", "Body 11 Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum"),
            Post(12, 12, "Title 12", "Body 12")
        )
    }

}

10. Implementación de Data Binding

En este apartado voy a explicar como asociar el modelo a la vista.

10.1. Asociar el modelo con la vista

10.1.1. activity_main.xml

Primero establecemos con qué vista vamos a realizar el data binding, que en este ejemplo es la actividad principal. Para ello tenemos que añadir las etiquetas layout y data en activity_main.xml. Dentro de data podemos crear variables que posteriormente utilizaremos en nuestra vista. Si nos fijamos en la etiqueta variable tenemos una variable llamada activityViewModel que es de tipo MainActivityViewModel.

<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <Button
            android:id="@+id/press_me_button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Mostrar entradas"
            app:layout_constraintBottom_toTopOf="@id/posts_recyclerView"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/posts_recyclerView"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:data="@{activityViewModel.posts}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="@id/press_me_button"
            app:layout_constraintTop_toBottomOf="@id/press_me_button"
            tools:listitem="@layout/post_view" />

    </androidx.constraintlayout.widget.ConstraintLayout>

    <data>

        <variable
            name="activityViewModel"
            type="com.autentia.demo.databinding.MainActivityViewModel" />
    </data>

</layout>

En la etiqueta RecyclerView el atributo app:data=»@{activityViewModel.posts}» le estamos indicando que el atributo data es la lista del viewModel, y no, no carga los datos mágicamente en el RecyclerView. ¡Ojalá fuese tan fácil!

Cuando declaramos ambas etiquetas, automáticamente se genera una clase Java que hace referencia al fichero activity_main.xml y que es nombrada como: [Nombre fichero xml][Binding] en este ejemplo sería ActivityMainBinding.

10.1.2. MainActivity.class

Una vez que ya tenemos los datos que queremos representar en la vista, necesitamos realizar la relación entre la vista y el modelo. Para ello abrimos MainActivity y añadimos lo siguiente en el método onCreate() :

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val mainActivityViewModel =
        ViewModelProviders.of(this).get(MainActivityViewModel::class.java)

    val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)

    binding.activityViewModel = mainActivityViewModel
    binding.lifecycleOwner = this

    posts_recyclerView.apply {
        layoutManager = LinearLayoutManager(context)
        adapter = CustomRecyclerViewAdapter(mutableListOf())
    }

    press_me_button.setOnClickListener {
        mainActivityViewModel.loadPosts()
    }
}

  • Obtenemos una instancia del ViewModel:val mainActivityViewModel = ViewModelProviders.of(this).get(MainActivityViewModel::class.java)
  • Inflamos el layout y obtenemos la actividad principal con el binding a las varibales. Gracias a esto podemos establecer los valores de las variables declaradas en el fichero activity_main.xml:val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
  • Realizamos la asignación:binding.activityViewModel = mainActivityViewModel
  • Es necesario cuando utilizamos componentes de arquitectura de Android:binding.lifecycleOwner = this
  • Definir un manejador de vistas y el adaptador al RecyclerView: posts_recyclerView.apply { layoutManager = LinearLayoutManager(context) adapter = CustomRecyclerViewAdapter(mutableListOf()) }
  • Cuando se haga clic sobre el botón de mostrar entradas cargamos la lista de posts:press_me_button.setOnClickListener { mainActivityViewModel.loadPosts() }

Bueno, pues al principio podemos pensar que ya hemos terminado, pero no es del todo así… Si nos fijamos bien en el código, el adaptador que le hemos pasado al RecyclerView tiene una lista vacía y por defecto no mostraría nada. Si pulsamos sobre el botón de mostrar entradas, debería cargar los datos en el RecyclerView. En este caso debería añadir más lógica en la actividad principal para poder visualizar los nuevos elementos, pero no es el objetivo de este tutorial. Como opinión personal, el propio componente debería de tener la responsabilidad de actualizar la vista cuando los datos cambien.

10.2. Manipular la lista del adaptador

Crear un adaptador nuevo cada vez que se pulse el botón para mostrar de nuevo el listado es una mala práctica y para ello vamos a añadir un método setData(data) en el adaptador CustomRecyclerViewAdapter para poder manipular la lista.

fun setData(data: MutableList) {
    posts.clear()
    posts.addAll(data)
    notifyDataSetChanged()
}

  1. Limpiamos la lista.
  2. Añadimos los nuevos elementos.
  3. Notificamos los cambios para que el RecyclerView vuelva a renderizar los elementos.

10.3. Observar los cambios de MutableLiveData

Te podrías preguntar para qué se necesita observar los cambios. La respuesta es muy sencilla: cuando los datos cambian en MutableLiveData nadie es notificado de estos cambios y por tanto no se actualizarían los cambios en la vista.

Además te podrías cuestionar si añadir la lógica en el ViewModel o en ActivityMain. No, tranquilos, Google está al tanto de todo y ha creado un componente llamando BindingAdapter que también forma parte de Data Binding y cuyo principal objetivo es estar atento a los cambios de un atributo declarado en el archivo activity_main.xml. Su implementación sería la siguiente:

@BindingAdapter("data")
fun setRecyclerViewProperties(recyclerView: RecyclerView?, data: MutableList?) {
    val adapter = recyclerView?.adapter
    if (adapter is CustomRecyclerViewAdapter && data != null) {
        adapter.setData(data)
    }
}

Si analizamos un poco el código, veremos que el método setRecyclerViewProperties será ejecutado cada vez que cambie de valor del atributo data, lo cual sucede cuando hacemos clic sobre el botón de mostrar entradas.

Es obligatorio que los parámetros RecyclerView y MutableList sean nullables, en caso contrario solo tendremos dolores de cabeza…

En la segunda parte del código, si el adaptador es de tipo CustomRecyclerViewAdapter, ejecutamos el método setData(data) para modifciar la lista de los Posts que tiene el adaptador (por defecto lista vacía).

¡Wuala! Si has seguido todos los pasos, nuestro Recycler View con Data Binding estará implementado. Dejo un pequeño vídeo con la implementación.

11. Conclusión

Como conclusión personal podría decir que esta implementación de Recycler View podría ser la más cercana a lo que realmente es el componente. Es decir, la responsabilidad de que los datos se han modificado de la lista y actualizar no es del programador, es del propio componente. Imagina hacer lo mismo para un TextView, ¿deberíamos montar toda esta implementación solo para escribir en el campo y notificar los cambios al componente para poder visualizarlos? Menos mal que no es así, pero es la misma historia.

12. Referencias

Recycler View

Data Binding

La entrada Data Binding con Recycler View en Android se publicó primero en Adictos al trabajo.

Diferencias entre Wireframes y Mockups

$
0
0

Hay muchos casos en los que en el entorno de trabajo ya sea entre diseñadores, programadores, marketing, etc. no tienen claro qué diferencia hay entre un Wireframe y un Mockup.

Hoy les vengo a explicar la diferencia entre ellos.

Índice


Wireframes

Los wireframes son una representación inicial de un diseño normalmente en baja fidelidad. ¿Qué quiere decir esto? Quiere decir que no es un diseño final, viene a ser el esqueleto de un diseño inicial, en el cual se puede visualizar de forma rápida y sencilla cómo irán organizados los diferentes componentes de nuestro diseño. Los wireframes suelen representarse en escala de grises con cajas, círculos, líneas, etc.

¿Para qué sirven?

Una de las principales funciones de los wireframes es ahorrar tiempo y dinero, un wireframe lleva poco tiempo hacerlo y aporta mucho valor informativo. En un proyecto real, realizar unos wireframes no supone esfuerzo, en cambio tener que realizar un desarrollo para tener una idea de cómo va a quedar es muy costoso.

¿Cuándo usarlos?

La utilización de los wireframes suele usarse en tempranas edades del proyecto. Sirve como nexo con el cliente para mostrar cómo va a ser la estructura básica del diseño del producto final.

Tipos de wireframes

Yo los separo en dos tipos, wireframes en baja fidelidad y wireframes en alta fidelidad.

Baja fidelidad:  los wireframes de baja fidelidad suelen ser esquemas planos en papel o hechos incluso con algún programa de diseño gráfico. No suelen tener mucho lujo de detalles y sirven como una primera idea general de la estructura del producto.



Alta fidelidad:  los wireframes de alta fidelidad suelen ser esquemas más elaborados en escala de grises. Estos suelen contener anotaciones que explican qué hace cada botón, sección, etc. El texto que se utiliza podría ser texto real. No debe confundirse con un diseño final. Aquí sí que suelen hacerse con un editor gráfico.

Hay herramientas de pago para hacer wireframes donde vienen muchos elementos para simplemente copiar y pegar  (Balsamiq, Mockuplus, Mockflow, Axure), suelen disponer de versión gratuita por un periodo de días. Pero para hacer wireframes tampoco hemos de comernos la cabeza, con un simple papel y lápiz se puede hacer un wireframe.

Mockups

El mockup a diferencia del wireframe da un paso más allá y dejamos de tener la baja fidelidad que teníamos en el wireframe a pasar a una alta fidelidad con colores, imágenes tipografía, sombras, etc.

Un mockup es una representación de un diseño final a pixel perfect en un ecosistema real, ya puede ser un smartphone, una tablet, un desktop, etc.

Son de gran ayuda cuando se trabaja en equipo y sirven para recibir feedback y mostrar al cliente el diseño final del producto. En definitiva los mockups es la parte visual que más se asemeja al resultado final del producto.

Diferencias 

Para la creación de mockups puedes utilizar cualquier programa de diseño gráfico, los más utilizados son (Sketch, Figma, Adobe xd, Photoshop), estos programas son de pago pero como siempre suelen disponer de un periodo de prueba gratuito.

Mockup con diseño final

Para finalizar, como has podido comprobar, ya sea el wireframe, el mockup  o el diseño final, cada uno de ellos tiene sus características y especificaciones y el uso de estos dependerá del estado del proceso en el que estemos. Añadir que van complementados los unos de los otros.

La entrada Diferencias entre Wireframes y Mockups se publicó primero en Adictos al trabajo.


AA – Arduino&Android

$
0
0

1.Introducción

El objetivo de este tutorial es mostrar un proyecto personal en Arduino y su interacción con una aplicación Android.

El proyecto consiste en un Carro Robot basado en Arduino. A partir de ahora y a lo largo del documento lo denominaremos “Carrimoto”, el cual tendrá dos tipos de modos:

  • Manual: A través de de una aplicación móvil, se le indicará al Carrimoto en que dirección deberá moverse.

  • Automático: El propio Carrimoto se moverá de forma autónoma. Mediante un sensor de proximidad buscará cual es la ruta más óptima fuera de obstáculos.

Empezamos ……………

2.Entorno

Este tutorial está escrito utilizando el siguiente entorno:

  • Hardware: Dell Inspiron 15’ (2.50 GHz Intel Core i7, 16 GB DDR3 )
  • Sistema Operativo: Ubuntu 18.04.2 LTS
  • Entorno de desarrollo: Android Studio 3.3 + IDE Arduino 1.8.9
  • Placa: Arduino Nano

3.¿Qué es Arduino?

 

A estas alturas de la película, prácticamente todo el mundo sabe lo que es Arduino y lo que se puede llegar a hacer con él, por tanto la mejor descripción la podemos encontrar en la propia página de Arduino.

Arduino es una plataforma de desarrollo basada en una placa electrónica de hardware libre que incorpora un microcontrolador re-programable y una serie de pines hembra, los que permiten establecer conexiones entre el microcontrolador y los diferentes sensores y actuadores de una manera muy sencilla.

Las principales ventajas de utilizar Arduino son las siguientes:

  • Gran comunidad: Gracias a su alcance, existe una gran comunidad trabajando con esta plataforma, lo cual genera una cantidad muy extensa de documentación.

  • Entorno de programación multiplataforma: Arduino dispone de un IDE, que se puede instalar y ejecutar en diferentes sistemas operativos (Windows, Mac OS y Linux).

  • Lenguaje de Programación sencillo: Dispone de un lenguaje de programación basado en C++ fácil de comprender.

  • Bajo Costo: El precio de las diferentes placas oscila, en función del modelo, entre los 6 y 50 Euros. Se trata de precios bastante asequibles. Existen Kit de iniciación muy completos.

  • Re-usabilidad y versatilidad: Re-utilizable, ya que una vez finalizado el proyecto es muy fácil desmontar los componente y empezar con uno nuevo. Todos los pines del micro-controlador están accesibles a través de conectores hembra, lo cual permite sacar partido de todas las característica del micro-controlador con un riesgo muy bajo de hacer una conexión errónea.

Existe una gran familia de placas. En la propia página de Arduino podéis ver las principales características y especificaciones de cada una de ellas:

  • UNO

  • NANO

  • MEGA

  • LEONARDO

  • YUN

4.Componentes

Arduino dispone de una gran variedad de componentes (sensores, motores,…). La idea de esta presentación no es hacer referencia a todos ellos, si no hacer un pequeña referencia a los implicados en este proyecto.

Arduino NANO

Es una de las placas Arduino más pequeñas. Esta basado en el microcontrolador ATmega328 y tiene una entrada mini-usb a través de la cual se puede subir el código fuente para la ejecución de los comandos. Viene con 14 puertos digitales de entrada/salida, 8 puertos analógicos, una memoria de 16 KB, 1 KB de SRAM y 512 bytes de EPROM.

Servo Motor

Un servomotor o comúnmente llamado servo, es un motor DC con la capacidad de ubicar su eje en una posición o ángulo determinado. Internamente tiene una caja reductora la cual le aumenta el torque y reduce la velocidad, un potenciómetro encargado de sensorizar la posición del eje y una pequeña tarjeta electrónica que junto al potenciómetro forman un control de lazo cerrado.

Sensor Ultrasonito HC-SR04

El sensor HC-SR04 es un sensor de distancia de bajo costo que posee dos transductores: un emisor y un receptor piezo-eléctricos, además de la electrónica necesaria para su operación.

El funcionamiento del sensor es el siguiente:

  1. El emisor piezoeléctrico emite 8 pulsos de ultrasonido (40KHz) después de recibir la orden en el pin TRIG, las ondas de sonido viajan en el aire y rebotan al encontrar un objeto.

  2. El sonido de rebote es detectado por el receptor piezoeléctrico.

  3. El pin ECHO cambia a Alto (5V) por un tiempo igual al que demoró la onda desde que fue emitida hasta que fue detectada, el tiempo del pulso ECO es medido por el microcontrolador y así se puede calcular la distancia al objeto.

Puente H L298N

Placa para control de motores mediante el chip L298N, de doble H-Bridge, que permite manejar dos motores de corriente continua o uno de paso a paso.

Es una placa ideal para construir pequeños Robots móviles mediante un par de motores de corriente continua y una plataforma base.

Protoboard

Placa de pruebas en las que se pueden insertar elementos electrónicos y cables con los que se arman circuitos sin la necesidad de soldar ninguno de los componentes. Las Protoboards tienen orificios conectados entre si por medio de pequeñas laminas metálicas. Usualmente, estas placas siguen un arreglo en el que los orificios de una misma fila están conectados entre si y los orificios en filas diferentes no.

2x Motor reductor DC 5V

Los motorreductores son ampliamente usados en la actualidad pueden ir desde un pequeño motorreductor capaz de cambiar y combinar velocidades de giro en un reloj de pulsera, cambiar velocidades en un automóvil, hasta enormes motorreductores capaces de dar tracción en buques de carga, molinos de cemento, grandes máquinas cavadoras de túneles o bien en molinos de caña para la fabricación de azúcar.

Un motorreductor cuenta con un motor acoplado directamente a un sistema de engranajes, esto permite al motor reducir o aumentar su velocidad dependiendo de la configuración de los engranes que se usen.

Chasis

Soporte compuesto:

  • Estructura donde se va a montar el proyecto.

  • Dos ruedas donde se anclan los motores reductores.

  • Rueda loca para la parte posterior de la estructura.

  • Una batería de 4 pilas para alimentar los motores.

Módulo BlueTooth HC05

El módulo Bluetooth HC-05 es ideal para utilizarlo en todo tipo de proyectos donde necesites una conexión inalámbrica fiable y sencilla de utilizar. Se configura mediante comandos AT y tiene la posibilidad de hacerlo funcionar tanto en modo maestro como esclavo.

Batería Externa

Esta batería se utilizará para alimentar el Arduino.

5.Conexionado del Proyecto

En este apartado voy a mostrar un esquema de conexión de los diferentes componentes que han intervenido en el proyecto, para ello he utilizado Frizing (https://fritzing.org)

Frizing es un programa libre de automaticación de diseños eléctricos, que permite a los diseñadores crear su prototipos. En este caso lo voy a utilizar para mostrar el diseño planteado en Arduino.

6.Codificación

Todo el código de este proyecto lo podéis encontrar en mi Github (https://github.com/vtcmer/arduino-project).

En el repositorio vais a encontrar tres sub-proyectos:

  1. IRobotApp: Aplicación Android para el control de Carrimoto.

  2. bt_remote: Aplicación Arduino.

  3. sk_xy_remote: Es una aplicación que genera todo el código automático para el control del Carrimoto.

En este caso no voy a mostrar todo el código. Voy a explicar las diferentes fases por las que he ido evolucionando el proyecto.

6.1. Fase I: Desplazamiento Automático

En esta fase inicial, se realizó las mayor parte de montaje:

Como se puede observar en las fotos, el sensor Ultrasónico está acoplado al soporte impreso en 3D, el cual, a su vez descansa sobre un brazo del servo motor.

El movimiento del Carrimoto siempre es en línea recta hacia adelante, cuando detecta un obstáculo cercano realiza las siguientes acciones:

  1. Detiene el movimiento.

  2. De mueve ligeramente hacia atrás, para poder girar más fácilmente si está muy próximo al obstáculo.

  3. El servo motor mueve al sensor Ultrasónico hacia la derecha e izquierda para identificar cual es la dirección más óptima a seguir (en función de la distancia más lejana del próximo obstáculo).

  4. Cuando identifica cual es la ruta más óptima, se realiza el giro hacia el lado indicado por el sensor Ultrasónico.

  5. En caso de tener obstáculo muy cercanos a la derecha o izquierda se volverá a realizar un chequeo de posicionamiento volviendo al punto 2 de estas acciones.

6.2.Fase II: Desplazamiento Manual (I)

En esta segunda fase se introdujo la comunicación a través de Bluetooth.

Se realizó el conexionado del componente Bluetooth (HC 05) a la placa Arduino.

Como primera aproximación a este desarrollo se utilizó RemoteXY (http://remotexy.com/).

RemoteXY es una manera sencilla de hacer y usar una interfaz gráfica de usuario móvil para tarjetas controladoras (en este caso Arduino). Usando el editor de interfaces gráfica que proporciona, se puede crear una interfaz de usuario fácilmente (Botones, Interruptores, Leds, ….), que se podrá cargar en el controlador.

Desde RemoteXY se generará el código (en este caso Arduino) que se cargará en el controlador. Por supuesto se tendrá que incluir el código específico de cada proyecto, en mi caso el movimiento del Carrimoto. Posteriormente tendrás que descargar la aplicación móvil RemoteXY en tú dispositivo móvil y conectarte vía Bluetooth al Arduino.

En esta segunda fase del proyecto, creé en RemoteXY una interfaz sencilla, que consistía en un conjunto de botones que a través de Bluetooth envía al Carrimoto la dirección en la que se tendrá que desplazar.

El código correspondiente a esta fase lo podéis encontrar aquí https://github.com/vtcmer/arduino-project/tree/master/i-robot/sk_xy_remote .

6.3.Fase III: Desplazamiento Manual (II)

En esta tercera y última Fase, desarrollé una aplicación móvil en Android que se comunica con Arduino vía Bluetooth.

El código correspondiente a esta fase lo podéis encontrar aquí https://github.com/vtcmer/arduino-project/tree/master/i-robot/IRobotApp .

La aplicación es muy simple, consiste en:

  • Activity para seleccionar el dispositivo Bluetooth: se mostrará la lista de dispositivos Bluetooth disponibles y se tendrá que seleccionar el del Carrimoto.

  • Activity de Control del Carrimoto, donde se podrán hacer las siguientes acciones:

    • Cambio del modo de movimiento manual ó automático.

    • Parar el Carrimoto.

    • Desplazamiento del Carrimoto (derecha/izquierda).

    • Controlar la velocidad de Carrimoto.

  • Una serie de servicios para establecer la comunicación con el Bluetooth y el intercambio de mensajes entre la aplicación móvil y el Carrimoto.

En esta fase se introdujo una comunicación bidireccional entre los dos dispositivos:

  • En modo manual, se envía desde la aplicación al Arduino la dirección en la que se tiene que mover el Carrimoto.

  • En modo automático el Arduino envía a la aplicación hacia donde se mueve el Carrimoto y en la aplicación se resalta la flecha correspondiente al movimiento.

7.Conclusiones

La tecnología avanza a una velocidad vertiginosa. Está ahí para poder ser utilizada, transformada y combinada … Trastea con ella.

Cualquier proyecto por pequeño y sencillo que sea, si te proporciona una satisfacción personal ya es un gran crecimiento profesional para ti.

A continuación os dejo algunos enlaces de interés relacionados:

https://www.arduino.cc/

https://www.xataka.com/makers/empezar-arduino-que-placa-kits-iniciacion-comprar

https://www.prometec.net/indice-tutoriales/

https://www.youtube.com/channel/UC9TXm_X_JeVPLZtLEK1S3xA

https://www.luisllamas.es/tutoriales-de-arduino/

La entrada AA – Arduino&Android se publicó primero en Adictos al trabajo.

Ser o no ser agile

$
0
0

Muchas empresas quieren subirse al tren de agile. Quizás sea por moda, y quieren cambiar su imagen de marca; tal vez crean que esa transformación va a marcar un antes y un después, haciéndolas más competitivas o innovadoras. Pero, ¿vale la pena una transformación agile? Con permiso de Shakespeare… ¿ser o no ser agile?, esa es la cuestión. ¡Vamos a analizarlo!

Cuando una empresa inicia un proceso de cambio hacia modelos ágiles, debe tener un buen motivo. Como dice Simon Sinek, “comienza con el porqué”, porque cambiar no es fácil, ni rápido, ni barato. Olvídate de que llegue un agile coach, cual hada madrina con su varita mágica, y que en unas pocas semanas haya transformado tu organización de pies a cabeza.

Pero antes de meternos con el “porqué”, nos ayudará haber revisado el “cuándo”.

La importancia del contexto

Cada empresa, cada equipo, cada persona es única. Las soluciones “universales” rara vez lo son, no existen recetas mágicas (quitando el conocimiento arcano que tienen lo informáticos de “apágalo y vuélvelo a encender” en sus múltiples variantes).

El país de origen de tu organización, los segmentos del mercado a los que ataca, sus principios y valores, su misión y visión, y en definitiva, su cultura organizacional componen un contexto único bajo el cual habrá que evaluar si tiene sentido iniciar una transformación agile.

Recordemos que donde mejor aplica el paradigma ágil es para los contextos de negocio VUCA, es decir, volátiles, inciertos, complejos y ambiguos. Esto se debe a que bajo el paraguas agile hay una gran diversidad de principios, valores y prácticas que nos ayudan a manejar dichos contextos (adaptarnos al cambio, mejora continua, entrega temprana y frecuente de valor, bucles de feedback cortos, etc.). En la sociedad actual, los cambios cada vez suceden más a menudo, son más rápidos y más disruptivos; lo que antes sólo afectaba a unos pocos sectores (tecnológico principalmente) se está extendiendo a otros tradicionalmente más estables. Se está “VUCAtizando” nuestro mundo y, quizás por eso, agile está penetrando más que nunca.

“Las especies que sobreviven no son las más fuertes, ni las más rápidas, ni las más inteligentes; sino aquellas que se adaptan mejor al cambio. En la lucha por la supervivencia, los más aptos ganan a expensas de sus rivales porque consiguen adaptarse mejor a su entorno.” Charles Darwin

No obstante, para que una transformación ágil sea efectiva, el factor organización o empresa es fundamental. Tanto su cultura, como veíamos antes, como su estructura y modelo de negocio harán de este proceso más o menos fácil.

Sintetizando, el éxito de la transformación agile depende tanto del contexto interno como externo de la organización. Y esto nos lleva al siguiente punto, ¿qué es el éxito?

Éxito y Expectativas

Ahora sí, vamos con el por qué. ¿Por qué abordar un proceso de cambio sabiendo que no va a ser fácil, ni rápido, ni barato? Asumiendo que el contexto es favorable, podríamos generalizar diciendo que una empresa ágil va a adaptarse mejor a los cambios, más rápido que las tradicionales, desperdiciando menos recursos y con un foco total y absoluto en la entrega de valor a su cliente final.

Entregar más valor es una causa, y el efecto puede ser tener más beneficios (en una empresa con ánimo de lucro) o tener un mayor impacto social (en una organización sin ánimo de lucro). 

Consideraremos un éxito la transformación agile si, gracias a ella, somos más efectivos en el camino de lograr los objetivos estratégicos de la empresa (derivados de su visión y misión), respetando los valores de la misma.

Pero el cambio lo llevan a cabo y afecta a distintos grupos de personas, cada uno con sus expectativas, intereses y manera de ver las cosas. Te invito a ponerte las gafas de cada uno de ellos.

Los trabajadores

Podríamos clasificarlos en dos categorías principales:

  • Vocacionales: son los que aman lo que hacen. Para ellos su profesión y la labor que realizan en la empresa les otorga sentido. Generalmente ofrecerán menor resistencia al cambio que el siguiente grupo, siempre y cuando se les muestren los beneficios de la mejora continua, excelencia en el trabajo, eliminación de desperdicios y mayor aportación de valor. Hay que tener especial cuidado con los grandes expertos (si tienen un gran ego) para que no generen la expectativa de que la transformación va a interferir negativamente en su trabajo.
  • Pragmáticos: son los simplemente tienen un trabajo nutritivo. Quieren un trabajo estable y un sueldo a fin de mes. Presentarán más resistencia al cambio, especialmente si ven que fallar puede traer consecuencias negativas.

Los mandos intermedios

Si creen que trabajar con equipos auto-organizados es algo que cuestiona la necesidad de su puesto, pueden estar en contra e incluso sabotear la transformación.

Tanto si conocen el mindset ágil o no, es importante que entiendan sus beneficios y el papel que tienen dentro de una estructura menos jerárquica y más colaborativa. Puede ser un reto pasar del command & control al liderazgo por servicio; en estos casos puede ser de gran ayuda un buen agile coach con el que trabajar uno a uno.

Los directivos

Generalmente buscan que se cumplan los objetivos para maximizar su variable. Tienen un gran poder en la toma de decisiones y en muchas empresas caen en la trampa de la visión a corto plazo.

Una transformación puede ser larga y con resultados que tarden en verse. En este sentido, tener un plan del proceso de cambio con algunas victorias rápidas (quick-wins) puede ponerlos a su favor.

Los accionistas 

Este grupo generalmente estará interesado en que mejoren los beneficios de la empresa para que se revaloricen sus acciones y/o los repartan en forma de dividendos. Si la transformación es global para toda la organización es clave que se comunique adecuadamente a este grupo y se refleje en el plan estratégico de la empresa.

Los propietarios o socios mayoritarios

Buscan la creación de valor en la empresa, pero con una visión a largo plazo. Son grupos generalmente más estables y con un compromiso con la misión y visión de la organización.

Pueden no estar tan involucrados en la actividad de la empresa como directivos, mandos intermedios y trabajadores, pero hacerles ver que la transformación agile puede generar grandes ventajas y es una de las claves para la sostenibilidad de la empresa a largo plazo los pondrá a su favor.

Los clientes

Quería mencionar a este grupo porque, aunque sea indirectamente, se verán afectados por la transición. Con las prácticas ágiles focalizamos en la entrega de valor al cliente, recibiendo feedback más a menudo y, en consecuencia, ofreciendo los productos y servicios que realmente necesita.

Que cambie la percepción del cliente puede tener un gran impacto en el negocio. Y este impacto se acabará reflejando en la cuenta de resultados.

Estas visiones de los diferentes grupos podrán verse afectadas según quien sea el promotor del cambio, cómo se ha comunicado a los demás grupos y en qué momento. En cualquier caso, ser ágil traerá beneficios a cada uno de estos grupos.

Bucles de feedback

Las diversas metodologías y frameworks ágiles utilizan bucles de feedback cortos para así convertirse en sistemas adaptativos, con uno o varios bucles cerrados. Como menciona Gunther Verheyen en su libro Scrum, A Pocket Guide, “en un sistema de bucle cerrado, la salida actual del sistema se compara regularmente con la salida deseada, para eliminar o reducir gradualmente las variaciones no deseadas”.

Sistema de bucle cerrado
Sistema de bucle cerrado

En los últimos años ha tomado gran relevancia el método Lean Startup de Eric Ries, en el cual se parte del método científico para definir un bucle de feedback conocido como el principio build-measure-learn

Resumiendo mucho, este principio funciona de la siguiente manera: 

  1. Tenemos una idea que creemos que nos va a ayudar (hipótesis), pero como no estamos seguros de ello, vamos a realizar un experimento.
  2. Construimos un producto, el mínimo necesario para validar la hipótesis. Es el conocido MVP o producto mínimo viable.
  3. Medimos los resultados que obtenemos con el producto construido, capturando toda una serie de datos.
  4. Analizamos los datos obtenidos y aprendemos de ellos. Nuestra hipótesis será validada totalmente, parcialmente o invalidada. En función de ello, perseveraremos, pivotaremos o desecharemos la hipótesis.
El principio build-measure-learn
El principio build-measure-learn

Este enfoque empírico, obtener aprendizaje validado, permite a las empresas ágiles adaptarse a su entorno mejor que sus competidoras tradicionales.

Y al igual que podemos aplicar un bucle de feedback a nivel de empresa o producto, lo podemos hacer a nivel de equipo o personal. Es común a las metodologías ágiles hacer reuniones de retrospectiva periódicas; en ellas, se reflexiona sobre lo sucedido en un determinado periodo (measure), se identifican puntos a mejorar (learn) y se traza un plan de acción (build). Es el camino del kaizen, o mejora continua.

Resumen y conclusiones

El mundo que nos ha tocado vivir está en proceso de aceleración continua, generando un entorno VUCA en el que los cambios ocurren cada vez más rápido y son más disruptivos. Bajo el paradigma ágil, una organización puede adaptarse mejor a estos cambios y entregar más valor a sus clientes. Para ello, es necesario un enfoque empírico en el que los bucles de feedback cortos son fundamentales.

Además, también se beneficiarán de la transformación ágil trabajadores, mandos intermedios, directivos e incluso accionistas y propietarios. 

¿Ser o no ser agile? Bajo mi criterio, SER, sin duda, en la mayor parte de casos. No sobrevive el más fuerte , sino el que mejor se adapta (¿recuerdas a Nokia o Kodak?). Y ser ágil no sólo aplica a nivel de organizaciones, también para equipos y a nivel personal.

Otra cuestión sería ¿podemos medir el éxito de una transformación agile? Lo dejo para otro artículo, en el que os contaré una aproximación muy interesante.

¡Muchas gracias por haber llegado hasta aquí! Me encantará leer y responder tus comentarios, así que ¡no dudes en hacerlo! Y si te ha gustado el artículo, compártelo en redes sociales.

Be agile, my friend!

La entrada Ser o no ser agile se publicó primero en Adictos al trabajo.

Introducción a Data Science con R y RStudio

$
0
0

El Data Science con R y Rstudio se puede trabajar de forma muy sencilla. Son respectivamente el lenguaje y el IDE que he elegido para trabajar estos temas que tan en voga están ahora como el Machine Learning, Big Data o Data Science.

Índice de contenidos

1. La importancia de hacerse las preguntas clave.

Hace poco tiempo vi un concurso que me llamó mucho la atención. Se llamaba Zara Challange, y estaba organizado por la marca insignia del grupo Inditex. El concurso buscaba la respuesta a una pregunta muy pertinente, y el premio creo recordar que era incorporarse a la plantilla del grupo.

Lo que a mi me interesó de verdad era la pregunta: «¿Cuál será el producto más vendido al día siguiente?»
Te daban los datos de las ventas previas, y tenías que pronosticar cual iba a ser el resultado. Ese proceso, se iteraba tres días consecutivos. Y la búsqueda de ese resultado se me antojó que debía ser un juego divertidísimo. Pero además, recalé en lo pertinente de esa pregunta para cualquier empresa que se dedique a vender un producto.

Y me imaginé como me enfrentaría yo a ese problema. Las matemáticas en seguida vinieron en mi ayuda, y tiré de memoria. Lo primero que se me ocurrió fue pensar en hacer una aproximación por mínimos cuadrados, pero al rato recordé que había unos algoritmos de suavizamiento exponencial basados en series temporales, que servían precisamente para pronosticar la demanda de un producto. Y los busqué, y así fué como llegué al modelo de Holt-Winters y de Box-Jenkins.

Y me di cuenta que los problemas de programación a los que me enfrento cada día no suelen requerir de este tipo de conocimientos, y que por el desuso estaba perdiendo agilidad y necesitaba volver a refrescar estos temas.

Además, necesitaba una herramienta con la que poder aplicar este tipo de algoritmos, y los lenguajes que suelo usar Java y JavaScript no parecían los más apropiados. Así que decidí aprender R para utilizarlo en el refresco mis oxidadas matemáticas para aplicarlo en cuestiones de Machine Learning y es que el Data Science con R y Rstudio se puede trabajar de forma muy sencilla.

2. R y RStudio.

R es un lenguaje muy orientado al análisis estadístico. Viene muy preparado para trabajar com modelos lineales, algoritmos de clusterización (clasificación y agrupamiento), análisis de series temporales, etc… Además viene preparado para poder generar gráficas de los elementos con los que trabaja. El lenguaje se puede extender mediante librerías, y lo cierto, es que hay librerías para casi todo.

RStudio es un IDE que nos permite ejecutar R en un entorno visual para casi cualquier sistema operativo.

2.1. Instalar R

Lo podemos descargar de la página web oficial https://www.r-project.org/ y ahí nos vamos a la página de descargas que nos mostrará una serie de mirrors para que lo descargues. Yo como estoy en Madrid, elijo el de España, que apunta a https://cran.rediris.es/ Lo descargamos y lo instalamos sin más.

2.2. Instalar RStudio

Esto nos ha instalado el lenguaje R sin más. Pero es mucho más cómodo trabajar con un IDE. El que yo he elegido es RStudio que podemos descargar la licencia OpenSource de RStudio Desktop de https://www.rstudio.com/.

3. Leyendo un fichero CSV

Bien. Ya hemos instalado R y el IDE con el que vamos a trabajar. De momento, vamos a suponer que sabemos R, que el lenguaje es similar a cualquier otro que conocemos y que este IDE es parecido a cualquiera de los IDEs con los que trabajamos habitualmente. Abrimos el IDE, y en «file» creamos un nuevo script de R. Lo que necesitaremos a continuación es un conjunto de datos con el que jugar.

En internet hay infinidad de dataSets publicos para obtener datos con los que jugar con la finalidad de irnos familiarizando con R. Algunos relacionados con la zona donde vivo son:

setwd("/lab/adictosaltrabajo/RStudio/")

leemos el CSV, como esta separado por ; y no por , lo indicamos

data <- read.csv("./monumentos-ciudad-madrid.csv", header = T, sep = ";")

Vemos que el objeto data es un dataFrame de 1854 onservaciones con 30 variables. Podemos echar un vistazo a las 5 primeras filas con:

head(data)

El Data Science con R y Rstudio se puede trabajar de forma muy sencilla

Y vemos que es abrumador. Lo primero que vamos a aprender es a trabajar un poco con ese dataFrame.

4. Trabajando con un DataFrame

Supongamos que queremos la fila 5 y sólo las dos primeras columnas

data[5, c(1,2)]

data tiene dos coordenadas: la primera son las filas, y la segunda las columnas. Se puede definir conjuntos con la función c().
Por ejemplo, un conjunto del 1 al 5 sería

c(1:5)
, o
c(1,2,3,4,5)
. Fijaros que empieza en 1, y es que los vectores, arrays, matrices, etc… en R comienzan en 1.
También se podría hacer
c(1:2,5,20:21,24:25)
.

Una vez dicho esto, vamos a quedarnos sólo con las columnas que nos interesan del dataFrame. Podríamos definir el conjunto por colName o por el id de la columna. Así que ambas fórmulas serían equivalentes:

#me quedo con todas las filas y un conjunto de nombres de columnas
monumentos <- data[ , c("PK","NOMBRE","FECHA","BARRIO","DISTRITO","LATITUD","LONGITUD")]

#me quedo con todas las filas y un conjunto de IDs de columnas
monumentos2 <- data[ , c(1,2,5,20,21,24,25)]

Vemos que ambos son dataFrames de 1854 observaciones con 7 variables.

Pero ¿y si quisiéramos el resto del dataFrame con las 23 columnas restantes?
Sería suficiente con poner un «menos» delante del conjunto

monumentos3 <- data[ , -c(1,2,5,20,21,24,25)]

Esto nos devuelve un dataFrame de 1854 observaciones y 23 variables.

Una vez ya tenemos el objeto con el que vamos a trabajar, fijémonos en la naturaleza de las variables que hay en su interior: int, num y Factor… ¿y esto?

Bueno, esto es porque la función read.csv al leer los strings, por defecto los intenta convertir a factores. Es una especie de categorización de las cadenas de texto. Por ejemplo, en DISTRITOS tiene sentido que sean un factor. Porque hay 28 distintos y nos sirven para categorizar y clasificar. Pero en el NOMBRE del monumento, en el BARRIO, o en la FECHA, pues no tiene mucho sentido.

Además, hay ciertos valores molestos por en medio que muestran «NA». Esto es el equivalente a «nulo» de una columna en una BBDD. Significa Not Available, y sería conveniente quitarlos. La función read.csv trae parámetros para tratar ambas cosas. Le indicamos además que no nos convierta las cadenas de texto a factores.

data <- read.csv("./monumentos-ciudad-madrid.csv", header = T, sep = ";", stringsAsFactors = F)
monumentos <- data[,c("PK","NOMBRE","FECHA","BARRIO","DISTRITO","LATITUD","LONGITUD")]
monumentos <- na.omit(monumentos)

Aún así, hemos quitado los NA pero no algunos valores sin sentido. Echando un vistazo a los datos vemos que hay algunos BARRIOS o DISTRITOS sin nombre

monumentos <- monumentos[monumentos$BARRIO != "" & monumentos$DISTRITO != "" , ]

También me quiero quedar sólo con los que la fecha es exacta

monumentos <- monumentos[nchar(monumentos$FECHA)==4,]

5. Guardando nuestro trabajo en ficheros RDS

Echamos un vistazo al dataframe y efectivamente vemos que hay 1223 observaciones de 7 variables.

View(monumentos)

Una parte muy importante del científico de datos es limpiar el conjunto de datos y quedarse sólo con una parte muy representativa.

Y limpiar de la memoria las variables que no usemos.

data <- NULL
monumentos2 <- NULL
monumentos3 <- NULL

Ahora, al haber leido el fichero con

stringsAsFactors = F
, todos los strings se han considerado como cadenas de texto en lugar de categorías. Es muy posible que quisiéramos manipular los distritos como categorías y convertirlos por tanto a factores.

#Y los distritos los categorizamos como factores (hay 26 categorías)
monumentos$DISTRITO <- factor(monumentos$DISTRITO)

View(monumentos)

Ahora que ya hemos trabajado el conjunto de datos, nos conviene guardar el dataFrame como un objeto de R para poderlo recuperar más tarde o poder compartírselo a un compañero.

# Grabar un objeto a un fichero 
saveRDS(monumentos, file = "monumentos.rds")

# Leer un objeto y cargarlo en memoria
monumentos <- readRDS(file = "./monumentos.rds")

6. Conclusiones

Esto ha sido una toma de contacto muy somera con el Data Science con R y Rstudio. Hemos aprendido a leer ficheros CSV, a filtrar filas y columnas. A trabajar un poco el dataFrame limpiando valores extraños, quitando NAs, etc… Y a guardar un objeto en un fichero en memoria. Si nos damos cuenta, aún no hemos hecho nada con los datos que hemos obtenido. Esto lo presentaré en próximos artículos donde veremos que el Data Science con R y RStudio se puede trabajar de forma muy sencilla.

7. Enlaces y Referencias

La entrada Introducción a Data Science con R y RStudio se publicó primero en Adictos al trabajo.

Cómo medir el éxito de una transformación agile con gestión basada en evidencias (EBM)

$
0
0

Embarcarse en un proceso de transformación no se hace sin un buen motivo. Que lo estén haciendo otras empresas puede despertar el interés, pero siempre queda la duda razonable de si va a ser beneficioso en tu situación y contexto particulares. 

Las estadísticas, casos de éxito y, en general, la prueba social son un primer paso. Después, lo más habitual es hacer una prueba piloto, un pequeño experimento con algún equipo o proyecto. Ver cómo sale y, si es satisfactorio, extenderlo progresivamente al resto de la organización.

Y aquí llegamos a las palabras clave: “si es satisfactorio”. ¿Cómo lo sabemos?, ¿se puede medir el éxito de una transformación agile?, ¿y el retorno de la inversión? Ten por seguro que no va a ser una tarea fácil determinarlo.


Tabla de contenidos

1. Introduciendo EBM
2. Métricas de valor
3. Mejora empírica con EBM
4. Midiendo el éxito: la realidad no siempre es tan fácil
5. Conclusiones


Hace unas semanas, hablábamos sobre ser o no ser agile. En dicho artículo (¡léelo si no lo has hecho ya!), te explicaba las razones por las que considero que, en el contexto que nos ha tocado vivir, ser agile puede considerarse una ventaja competitiva, una cuestión de supervivencia para las empresas. 

Una de las características fundamentales del paradigma ágil es la adaptabilidad, que se consigue mediante un enfoque empírico con bucles de feedback cortos. De esta manera es como las empresas, equipos y personas aprenden rápido. 

¿Podemos usar esta idea para medir el éxito de una transformación agile? Claro, es la propuesta de EBM. Es más, ¡lo mejor sería utilizar este enfoque no sólo para medir sino como parte del proceso de transformación!

Introduciendo EBM

La gestión basada en evidencias, EBM (evidence-based management) en adelante, nos puede ayudar a medir el impacto de introducir prácticas ágiles en las organizaciones. Se centra en medir el valor de negocio, pero antes de seguir… ¿qué es valor?

No hay una respuesta única, ya que depende del punto de vista. Don McGreal y Ralph Jocham en su libro The Professional Product Owner mencionan tres: 

  • Como personas, es valioso lo que en última instancia nos proporciona felicidad.
  • Como empresa, es valioso lo que en última instancia nos proporciona dinero (beneficios).
  • Como organización no lucrativa, es valioso lo que en última instancia impacta positivamente en la sociedad o el planeta.

Agile pone el foco en la entrega de valor al cliente (causa), lo cual llevará a generar valor al productor o empresa (efecto). Vamos, hagamos felices a nuestros clientes y ellos estarán más que dispuestos a pagar por ello.

Entonces, ¿cómo medimos la entrega de valor? EBM propone hacerlo mediante un enfoque empírico usando métricas de valor, sobre las cuales hacer un seguimiento temporal. Esto nos ayudará a ser conscientes de la situación actual y situación objetivo, y a tomar las decisiones oportunas para ir ajustando nuestra ruta a lo largo del camino. Estas métricas permiten tener evidencias empíricas sobre la creación de valor.

El problema es que hay muchas cosas que medir. Veamos el panorama general y, si después quieres profundizar, te invito a leer la guía de EBM (versión sólo disponible en inglés) de Scrum.org.

Métricas de valor

EBM organiza las métricas de valor (KVM’s o Key Value Measures) en cuatro áreas clave de valor (KVA’s o Key Value Areas).

Métricas de entrega y de propietario

Antes de pararnos en las áreas y métricas de valor, conviene aclarar que hay dos tipos de métricas de negocio que suelen utilizarse:

  1. Métricas de entrega (delivery metrics). Son importantes para guiar las prácticas operativas, pero peligrosas si se usan como falsas representaciones del valor y se fijan como objetivos. Eso sí, son más fáciles de medir normalmente. Deberían considerarse neutrales en cuanto a la creación de valor (afectan, pero indirectamente).
  2. Métricas de propietario (owner metrics). Reflejan los verdaderos resultados del negocio, siendo verdaderas métricas de VALOR con mayúsculas.

En el mundo del desarrollo software, tendríamos por ejemplo:

Métricas de entrega Métricas de propietario
Velocidad Ingresos
Número de tests Gastos
Cobertura de código Satisfacción del cliente
Defectos Satisfacción de los empleados
Acoplamiento y cohesión Lead y Cycle time
Complejidad ciclomática Ratio de innovación (nuevo vs mantenimiento)
Fallos haciendo la build Uso de funcionalidades
Adherencia a procesos establecidos
Líneas de código

Ahora sí, veamos las áreas KVA y algunas de sus métricas según EBM.

Key Value Areas y Key Value Measures

Las cuatro áreas clave de valor son: Current Value (valor actual), Time to Market (tiempo para lanzar al mercado), Ability to Innovate (capacidad de innovación) y Unrealized Value (valor no realizado o potencial). Paso a explicar brevemente cada una de ellas.

Las cuatro áreas clave de valor de EBM
Las áreas clave de valor de EBM promueven la agilidad y la entrega de valor de negocio

Current Value (CV)

Revela el valor que la empresa (o el producto) entrega a sus clientes, hoy. El objetivo es maximizar la entrega de valor a clientes, empleados y accionistas considerando lo que existe actualmente.

Entre sus métricas clave de valor (KVM) más relevantes están:

  • Ingresos por empleado: Ingresos brutos / número de empleados.
  • Ratio de costes de producto: total de gastos del producto/sistema medido comparados con los ingresos.
  • Satisfacción del empleado. Puede reflejarse en un índice global, basado en datos de encuestas o incluso de otras técnicas como las retrospectivas o los calendarios Niko-Niko
  • Satisfacción del cliente. Puede reflejarse en un índice global, basado en datos de encuestas, de redes sociales, antigüedad media de clientes (fidelidad), etc.

Time to Market (T2M)

Expresa la capacidad de la organización para entregar rápidamente nuevos productos o servicios, o nuevas funcionalidades y capacidades de los ya existentes. Cuanto más se acorte el T2M, más rápido se podrá adaptar a los cambios del mercado.

Entre sus métricas clave de valor (KVM) más relevantes están:

  • Frecuencia de Releases: cada cuánto se entrega una nueva versión de producto.
  • Periodo de estabilización de Release: tiempo dedicado corrigiendo problemas del producto desde que los desarrolladores indican que se puede lanzar una release y cuando realmente se lanza a los clientes.
  • Cycle Time: tiempo desde que se inicia un trabajo hasta que se entrega en una release. 

Ability to Innovate (A2I)

La capacidad de innovar es lo que permite a la organización y a sus productos o servicios cubrir mejor las necesidades de sus clientes y no quedar desfasados. Es un factor clave para la sostenibilidad a largo plazo del negocio, la captación y fidelización de clientes.

Entre sus métricas clave de valor (KVM) más relevantes están:

  • Índice de uso de características: medida de uso de las características del producto. Permite identificar las más usadas y las que rara vez o nunca se usan.
  • Tasa de innovación: porcentaje de esfuerzo o coste dedicado a desarrollar nuevas capacidades del producto, dividido entre el esfuerzo o coste total del producto.
  • Tendencia de defectos: aumento o disminución de defectos desde la última medición.
  • Índice de trabajo en el producto: porcentaje de tiempo que los equipos dedican trabajando en el producto para generar valor.
  • Índice de versiones instaladas: número de versiones de un producto que son soportadas simultáneamente. Lo ideal es que sea sólo una, para minimizar el coste de mantenimiento. Cuantas más versiones se tengan, menos tiempo se tendrá para la innovación.

Unrealized Value (UV)

Sugiere el valor futuro potencial que podría alcanzarse si la organización pudiese cubrir perfectamente las necesidades de todos sus clientes potenciales.

Entre sus métricas clave de valor (KVM) más relevantes están:

  • Cuota de mercado: el porcentaje del mercado controlado por el producto.
  • Gap de satisfacción del cliente o usuario: brecha o diferencia entre la experiencia deseada del cliente o usuario y su experiencia real.

Las organizaciones que cuidan estas cuatro áreas podrán crear valor de manera sostenible. Entregar valor, tener clientes y stakeholders contentos, y empleados satisfechos (CV) es importante, pero las organizaciones deben asegurarse también de poder cubrir las demandas del mercado a tiempo (T2M), siendo capaces de innovar a lo largo del tiempo (A2I). Continuar invirtiendo en el producto o servicio, dotándolo de las capacidades adecuadas, se justifica si hay potencial o margen de mejora (UV).

Mejora empírica con EBM

Llegados a este punto te preguntarás: ¿y por dónde empiezo? EBM propone utilizar un bucle de feedback para el aprendizaje. Consta de los siguientes pasos:

  1. Cuantificar el valor, en forma de métricas KVM. No es más que decidir qué vamos a medir, y para ello hay que tener en mente los objetivos de la organización.
  2. Medir KVM’s. Una vez seleccionadas, y antes de hacer ningún cambio, se deben medir las métricas KVM seleccionadas en el paso anterior para establecer una línea base. Vamos a sacar una foto de la situación de partida para poder valorar posteriormente los avances.
  3. Seleccionar áreas KVA a mejorar. Siendo conscientes de la situación inicial, con medidas reales de las métricas de interés (KVM’s), se podrá tomar una decisión informada sobre las áreas de mejora sobre las que primero actuar. En cada iteración del bucle no deberíamos intentar abordar muchos cambios sobre múltiples áreas KVA; es mejor acortar la duración de la iteración y hacer pequeños cambios que podamos medir rápidamente.
  4. Realizar experimentos, que sean prácticos para mejorar las áreas KVA objetivo. Tras elegir el área a mejorar, seleccionaremos unas pocas prácticas que creamos que mejorarán las métricas KVM asociadas, y ejecutaremos un experimento. Por ejemplo, una empresa de desarrollo software que quiera mejorar la calidad, podría poner el foco en reducir los valores de la métrica KVM “Tendencia de defectos”, incorporando la práctica de XP Test-Driven Development (TDD).
  5. Evaluar resultados. Una vez medidos los resultados el experimento, deben compararse con los valores previos. Si han mejorado las métricas KVM, los cambios realizados se mantienen; en otro caso, se probarían otras mejoras. El bucle continuaría hasta que el área KVA refleje los resultados deseados.

Para el seguimiento de las métricas, se podrán construir cuadros de mando u otro tipo de herramientas visuales que nos faciliten digerir toda la información. Por ejemplo, el siguiente diagrama de araña…

Evolución temporal de métricas KVM
Uso de un diagrama de araña para el seguimiento de métricas KVM

Hasta aquí hemos visto cómo plantear un ciclo para mejorar las áreas clave de valor o KVA’s, de una en una y poco a poco. Estos pequeños pasos, centrados en mejorar la entrega de valor, esperamos que afecten positivamente a nuestro negocio.

Midiendo el éxito: la realidad no siempre es tan fácil

Con el planteamiento de EBM, deberíamos poder abordar la transformación organizacional empíricamente, siguiendo los pasos descritos en el apartado anterior.

¿Y esto es fácil? Bajo mi criterio, en la mayoría de las organizaciones no. Muchas veces te encuentras con empresas en medio de una transformación y ¡se desconoce la situación inicial! Otras veces, no están claros los objetivos, son demasiado difusos o incluso diferentes según a quien preguntes. Y por último, en otras ocasiones hay tantos cambios en paralelo que es prácticamente imposible determinar la causa de los resultados que se están obteniendo.

Por otra parte, tenemos el problema de que algunas acciones generan resultados a largo plazo. Los resultados a largo plazo, siempre que la empresa sobreviva, son los que más afectan al devenir de la misma, pero las acciones que los provocaron se nos solaparán con otras, teniendo de nuevo el problema de no poder establecer una relación de causalidad.

Entonces, ¿tiramos la toalla y pasamos de medir? Yo creo que no; aunque no tengamos certezas en muchos casos, tendremos más probabilidades de acertar, seremos más conscientes de nuestra situación pasada, actual y futuras, veremos tendencias y evoluciones. Vale, seremos miopes, pero no ciegos. Y eso puede marcar la diferencia.

Conclusiones

Volviendo a las preguntas iniciales, y esta es sólo mi opinión… 

¿Podemos medir el éxito de una transformación agile? Sí, utilizando métricas de valor y bucles de feedback cortos. La clave está en saber la situación de partida y nuestros objetivos, e ir corrigiendo el rumbo frecuentemente con la retroalimentación obtenida. La gestión basada en evidencias o EBM nos puede ayudar a esto. Por cierto, si quieres una referencia de las métricas ágiles más habituales para tenerla a mano, hace un tiempo creé unas fichas listas para imprimir que están disponibles para su descarga en la web de Autentia aquí.

¿Cuál es el retorno de la inversión?, ¿se puede cuantificar su impacto en la cuenta de resultados? Me encanta hacer números y proyecciones, pero no serían más que cábalas; no habría certezas absolutas porque, salvo excepciones, los resultados empresariales se ven afectados por múltiples factores, tanto internos como externos, entre los cuales implantar prácticas ágiles es sólo uno más.

¿Me dejo algo en el tintero? ¿Hay algo que no he tenido en cuenta? ¡Comenta y lo seguimos hablando! Recuerda que todo cambia; quizás también lo haga mi opinión… o la tuya. Y si te ha gustado el artículo, compártelo en redes sociales.

Be agile, my friend!

La entrada Cómo medir el éxito de una transformación agile con gestión basada en evidencias (EBM) se publicó primero en Adictos al trabajo.

Leer un JSON con R (RStudio)

$
0
0

En este artículo aprenderemos cómo leer un JSON con R usando RStudio. Trataremos un JSON desde internet y lo cargaremos en un dataFrame con el que poder trabajar. Conoceremos la librería jsonlite y curl, además de aprender cómo escapar caracteres de una URI, y cómo concatenar cadenas de texto. También aprenderemos a mezclar dos dataFrames y a verificar que son idénticos o no.

Índice de contenidos

1. Introducción

En el artículo anterior vimos cómo leer un archivo CSV con RStudio y a cargar los datos en un dataFrame. También aprendimos a filtrar estos datos por filas y columnas. A limpiar los datos de valores extraños y a guardar un objeto, normalmente un dataFrame, en un fichero entendible luego por R.

Antes de meternos en harina, creo que debemos seguir aprendiendo los rudimentos de R y aprender cómo leer un JSON con R y tratarlo.

2. Leer un JSON con R desde una URI

Desgraciadamente no siempre tendremos un fichero con el que trabajar, si no que tendremos que cargar la información directamente de internet. Hay multitud de formatos, pero sin duda el más extendido es JSON.

Para trabajar este ejemplo nos vamos a basar en una página web que expone públicamente un dataset con la información de las paradas de metro de Madrid
DataSet con las paradas del Metro de Madrid

Y nos ofrece una API para consultarla:
https://idealista.carto.com:443/api/v2/sql?q=select * from public.paradas_metro_madrid

Por otro lado, hay otro dataSet que nos dice el precio por m2 de compra y alquiler a 500m de dichas paradas de metro (datos de mayo de 2016).
https://idealista.carto.com:443/api/v2/sql?q=select * from public.precio_metro_201605

Si echamos un vistazo a lo que nos devuelven estas peticiones vemos que de las paradas de metro nos interesan sólo algunos campos. Fijémonos en la primera fila

{
  "cartodb_id":1,
  "the_geom":"0101000020E610000073F38DE89E550DC07D5D86FF743D4440",
  "the_geom_webmercator":"0101000020110F00008DE9305AECE918C1D9818D4B46D45241",
  "place":1,
  "id":1,
  "name":"Pinar de Chamartín",
  "line":"L1",
  "lat":40.4801329175,
  "lng":-3.6668070437
}

  • cartodb_id
    : es la PK o un índice númerico propio de la BBDD.
  • the_geom
    y
    the_geom_webmercator
    son campos propios de un estándar GIS llamado GeoJSON (ver https://es.wikipedia.org/wiki/GeoJSON)
  • place
    : es el lugar que ocupa la estación en la línea. Esta es la primera estación
  • id
    : huele a identificador único de todas las estaciones
  • line
    : Línea de metro.
  • lat
    y
    lng
    : son coordenadas terrestres (latitud y longitud)

De estos campos, yo creo que para trabajar nos vamos a quedar sólo con ID, NAME, LINE, PLACE, LAT y LNG

https://idealista.carto.com:443/api/v2/sql?q=select ID, NAME, LINE, PLACE, LAT, LNG from public.paradas_metro_madrid

3. Cargar estaciones de Metro desde R

Lo primero que haremos será crearnos un script de R y definir nuestro directorio de trabajo. Luego instalaremos y cargaremos alguna librería que nos permita leer un JSON con R. Yo he elegido

jsonlite
. Como en lugar de trabajar con un fichero, yo quiero consultar directamente de internet, instalo una librería que me permita trabajar con URLs y la cargo en memoria. Después de esto ya estaremos en condiciones de cargar un JSON en un dataFrame.
#seteamos el directorio de trabajo
setwd("/lab/adictosaltrabajo/rstudio/02")

# como vamos a trabajar con JSON me instalo la librería
# y la cargo en memoria
install.packages("jsonlite")
library(jsonlite)

# vamos a trabajar con URLs en lugar de ficheros, así que cargamos la libreria
# y la cargamos en memoria
install.packages("curl")
library(curl)

Definimos una variable con la URI que queremos consultar y llamamos a la funcion de la librería jsonlite

uri_paradas_metro <- "https://idealista.carto.com/api/v2/sql?q=select ID, NAME, LINE, PLACE, LAT, LNG from public.paradas_metro_madrid"
paradas_metro <- fromJSON(uri_paradas_metro)

Y vemos que la consola nos da el siguiente error:
Error in open connection al leer un JSON con R

Nos ha dado un error de tipo 400. Si nos fijamos, nos damos cuenta, que en el navegador, al pegar la URI, los espacios en blanco de la query se reemplazan por «%20». Lo que tendremos que hacer es escapar la URL. Seguro que la librería

curl
nos proporciona alguna función que lo haga.
q <- curl_escape("select ID, NAME, LINE, PLACE, LAT, LNG from public.paradas_metro_madrid")
uri_paradas_metro <- paste("https://idealista.carto.com/api/v2/sql?q=",q, sep="")
paradas_metro <- fromJSON(uri_paradas_metro)

En el bloque anterior vemos dos funciones nuevas. Por un lado

curl_escape
que hace un encoding y escapa los caracteres de la URL dada, y por otro
paste
, que nos sirve para concatenar cadenas indicándole un separador.

Y efectivamente, vemos que tenemos un nuevo objeto que es una Lista de 4 elementos, del cual nos interesa

rows
que es un dataFrame.

paradas_metro <- paradas_metro[["rows"]]

¿Y las paradas que son transbordos? ¿Cómo salen? ¿aparece una única fila con dos datos en el campo línea? ¿o son dos filas? ¿una fila con una línea y otra fila con otra?
Vamos a verlo fijándonos en un transbordo, que no deja de ser una estación que sabemos que está en dos líneas.

View(paradas_metro[paradas_metro$name == "Puerta del Sur",])

4. Cargar precios de la vivienda por estaciones de Metro desde R

Al principio referimos otra URL que nos muestra los precios por m2 para alquilar o comprar a 500 metros de distancia de las estaciones de Metro. Nuestro objetivo ahora es cargar en otro dataFrame esos precios de las viviendas cercanas a las estaciones de metro. Lo mismo que antes, sólo que ahora nos interesa ID, NAME, SALE y RENTAL

p <- curl_escape("select ID, SALE, RENTAL from public.precio_metro_201605")
uri_precios_estaciones <- paste("https://idealista.carto.com/api/v2/sql?q=",p, sep="")
precios_estaciones <- fromJSON(uri_precios_estaciones)

precios_estaciones <- precios_estaciones[["rows"]]

Lo primero que nos damos cuenta es que en el dataframe de precios hay una observación más que en el de paradas de metro. Luego no coinciden exactamente: hay una parada de metro que está en uno y no está en el otro. Sería interesante descubrir qué parada es.
Al leer un JSON con R de precios nos damos cuenta que hay una estación de metro más que precios

Por otro lado deberíamos tener la información de ambos dataframes, el de paradas de metro y el de precios, unificada en un único dataframe.

paradas_metro_precio <- merge(paradas_metro, precios_estaciones)

Esto va a mezclar ambos dataframes haciendo un matching por los campos que se llaman igual, en este caso «id». Vemos que hay 325 observaciones.
Merge hecho con los dos dataframes

5. Dando una vuelta de tuerca a la API

He querido hacerlo así para mostrar como mezclar dos dataframes, pero lo cierto es que podíamos haber obtenido directamente esta información ya unificada desde la propia API, con un INNER JOIN entre las dos tablas.

query <- paste("select ",
                  "pmm.ID, pmm.NAME, pmm.LINE, pmm.PLACE, pmm.LAT, pmm.LNG, pr.SALE, pr.RENTAL ",
               "from ",
                  "public.paradas_metro_madrid pmm ",
                  "inner join public.precio_metro_201605 pr ",
                  "  on pmm.id = pr.id ",
                "order by pmm.ID",
                sep="")
uri <- paste("https://idealista.carto.com/api/v2/sql?q=",curl_escape(query), sep="")
precios <- fromJSON(uri)
precios <- precios[["rows"]]

Y si usamos la función identical() del paquete base que nos compara dos dataframes veremos que

identical(paradas_metro_precio,precios)

por la consola nos devuelve

TRUE

Voy a categorizar el dataframe por lineas, así que las convierto a factores. Y como es posible que use este objeto más adelante, lo guardo a un fichero.

precios$line <- as.factor(precios$line)  

saveRDS(precios, file = "precios.rds")

6. Decidiendo dónde invertir

La motivación para hacer este estudio desde luego es aprender, pero supongamos por un momento que la pregunta que nos estamos haciendo es dónde invertir nuestro dinero adquiriendo una vivienda donde maximicemos su rentabilidad a la hora de alquilar.

Vamos a limpiar todas las variables y nos quedamos solo con el dataframe de precios. Y nos fijamos un poco en los datos del dataframe. Vemos que en la columna

SALE
tenemos el precio del m2 si deseamos adquirir la vivienda. Y en
RENTAL
cuanto costaría el m2 en la opción del alquiler. Supongamos que tenemos un dinero ahorrado y quiséramos comprar un piso para invertir en él. En este caso querríamos un piso que nos costara lo mínimo posible pero que pudiéramos alquilarlo por lo máximo. Aunque intervienen otros valores, como los gastos fijos de un piso que tendría que pagar el propietario (IBI, seguro, comunidad de propietarios, etc…) vamos a obviarlos en aras de obtener un modelo más simple.

Se observa que hay una función de densidad entre el precio para vender y el de alquiler. Si lo queremos anualizar, para ver el rendimiento anual sería:

(12 meses * precio menusla/m2 del alquiler) / (precio/m2 de compra)

Creamos una columna con el rendimiento anualizado

precios$rendimiento <- (12 * precios$rental)/precios$sale

Creamos otra columna con los beneficios anuales brutos por cada 100.000 euros invertidos

precios$beneficio100k <- round(precios$rendimiento * 100000,0)

Con esta información ya sabríamos dónde nos cunde más el dinero para invertir.

dataFrame resultante tras trabajarlo

Supongamos que queremos restringir la búsqueda sólo a la línea 10 de metro, por la razón que sea.

precios_linea10 <- precios[precios$line == "L10",]

Vemos que sólo hay 31 observaciones que se corresponden con las estaciones de metro de la Línea 10.

Si ordenamos por beneficio vemos que Batán, Casa de Campo, Colonia Jardín y Puerta del Sur son los más rentables.
De leer un JSON con R a ver cual es la rentabilidad mayor en la Linea 10

7. Conclusiones

En este ejercicio:

  • hemos aprendido a leer un JSON con R
  • hemos conocido la librería jsonlite y curl
  • ahora sabemos cómo escapar caracteres para una URL
  • y cómo concatenar cadenas de texto
  • hemos aprendido a mezclar dos dataFrames
  • y a verificar que dos dataFrames son idénticos

Por otro lado, los datos en que nos hemos basados para este ejercicio no están actualizados. Espero que nadie tome una decisión real basándose en este ejercicio.

Enlaces y Referencias

La entrada Leer un JSON con R (RStudio) se publicó primero en Adictos al trabajo.

Anchore – Análisis de vulnerabilidades de docker

$
0
0

Índice de contenidos

1.​ Introducción

Este tutorial explica la herramienta para análisis de vulnerabilidades de imagen de docker Anchore. Aprenderemos cómo configurarlo para descargar una imagen de un repositorio de docker, ya sea público o privado (añadiendo las credenciales del repositorio a anchore). También cómo analizar imágenes de docker en local, obtener informes con toda la información que Anchore proporciona y ejecutar una evaluación de la imagen contra distintas políticas para comprobar si la imagen supera el umbral de calidad de la política activa en Anchore.

Además de cómo configurar Anchore como una fase de integración continua (usaremos Gitlab CI en este tutorial) para automatizar el proceso de análisis de imágenes cada vez que haya una subida al repositorio.

Vamos a ver dos variantes de uso de Anchore en integración continua: 

  • Subir la imagen a un repositorio de docker, analizarla desde ahí y utilizar Anchore como medida preventiva para no desplegar esa imagen en kubernetes 
  • Solo subir la imagen de docker al repositorio si esta supera el umbral de la política activa.

Puedes encontrar el código de este tutorial en este repositorio.

​2.​ ¿Qué es Anchore?

Anchore es un herramienta de código abierto (tambien tiene version empresarial, pero no la vamos a cubrir en este tutorial) que inspecciona, analiza y certifica las imágenes de Docker. Este análisis lo hace contra una base de datos propia (Postgres) formada por la recopilación de información de vulnerabilidades y problemas de seguridad (CVE) de distribuciones de sistemas operativos y también recopila esa misma información de los registros de paquetes populares como Node.JS, NPM, Ruby.

Anchore puede descargar cualquier imagen de un registro compatible con docker V2 y el resultado del análisis genera un reporte con los detalles de la imagen, un listado de artefactos (npm, gem, python y java), un listado de paquetes del sistema operativo, el listado de ficheros de la imagen y un listado de vulnerabilidades (a elegir entre todas, solo del sistema operativo o solo las que no tengan que ver con el sistema operativo).

 

3.​ Cómo funciona Anchore

Anchore analiza cada imagen basándose en datos y la aplicación de políticas. Anchore sigue las siguientes fases en cada análisis:

  1. Obtiene y extrae el contenido de la imagen, sin ejecutarlo.
  2. Analiza el contenido, extrayendo y clasificando la mayor cantidad posible de metadatos.
  3. Guarda el resultado del análisis anterior en la base de datos.
  4. Evalúa las políticas contra el resultado del análisis, incluidas las coincidencias de vulnerabilidades en los artefactos descubiertos en la imagen.
  5. Actualiza los datos utilizados para la evaluación de políticas y vulnerabilidades y si hay algún cambio en estos datos, actualiza los resultados del análisis de imágenes aplicando estos nuevos datos.
  6. Muestra al usuario los resultados

​3.1.​ Origen base de datos de vulnerabilidades

La base de datos de Anchore solo cuenta por defecto con información de vulnerabilidades, pero se puede indicar a Anchore para que también incluya información de paquetes como npm y gem, además de bases de datos como nvd u otras externas.

En esta tabla se pueden ver los distintos origen de la fuente de datos, el tipo de datos y el controlador al que pertenecen:

Driver Feed Type External Data Source
alpine vulnerabilities https://github.com/alpinelinux/alpine-secdb/archive/master.tar.gz
centos vulnerabilities https://www.redhat.com/security/data/oval/com.redhat.rhsa-all.xml.bz2
debian vulnerabilities https://security-tracker.debian.org/tracker/data/jsonhttps://salsa.debian.org/security-tracker-team/security-tracker/raw/master/data/DSA/list
oracle vulnerabilities https://linux.oracle.com/security/oval/com.oracle.elsa-all.xml.bz2
ubuntu vulnerabilities https://launchpad.net/ubuntu-cve-tracker
gem packages https://s3-us-west-2.amazonaws.com/rubygems-dumps
npm packages https://replicate.npmjs.com
nvd nvd https://nvd.nist.gov/vuln/data-feeds
snyk snyk https://data.anchore-enterprise.com

 

4.​ Cómo usar Anchore

Anchore está disponible como una imagen de Docker. Esta imagen se puede ejecutar de forma independiente con docker-compose en una máquina local, con plataformas de orquestación como Kubernetes o como parte de integración continua en Gitlab CI, Jenkins, Travis CI, etc.

Para usarlo por primera vez y probar la herramienta recomiendo ejecutarlo en local con docker-compose al ser la forma mas rapida para poder ejecutar todos los comandos y ver la funcionalidad.

Después de haberlo probado con docker-compose, la mayor utilidad de Anchore es usarlo como parte de una fase de integración continua que se ejecute con cada cambio de una imagen de docker para comprobar que no hay nuevas vulnerabilidades o se han corregido las que ya había.

4.1.​ Anchore en local con docker-compose

Para poder usar Anchore hay que descargar (con el comando de abajo) la última imagen del contenedor de Anchore Engine que contiene el docker-compose.yaml y demás ficheros de configuración.

docker pull docker.io/anchore/anchore-engine:latest

Vamos a copiar el fichero docker-compose.yaml que hay dentro de anchore-engine a la carpeta ~/anchore/

docker create --name anchore docker.io/anchore/anchore-engine:latest
docker cp anchore:/docker-compose.yaml ~/anchore/docker-compose.yaml
docker rm anchore

Ahora entramos a la carpeta y descargamos los contenedores descritos dentro del fichero docker-compose.yaml que hay en la carpeta.

cd ~/anchore
docker-compose pull

Por defecto todos los servicios, incluida la base de datos no son persistentes y al reiniciar o apagar se perderán, pero se puede hacer persistente con tan solo poner a true la propiedad external dentro de anchore-db-volume y haber previamente creado un volumen de docker ejecutando

docker volume create anchore-db-volume

Ejecutamos el docker-compose:

docker-compose up -d

Una vez levantado Anchore podemos comprobar que funciona con docker-compose ejecutando

docker-compose exec engine-api anchore-cli system status

Tenemos todo listo para empezar a ejecutar el flujo básico de análisis de una imagen, solo hay que ejecutar estos comandos de abajo en orden sustituyendo el entrecomillado imagen por el nombre:tag o el digest de la imagen a analizar. Para probarlo inicialmente podemos analizar cualquier imagen de docker hub como por ejemplo “adoptopenjdk/openjdk11:latest” en lugar de “imagen”.

docker-compose exec engine-api anchore-cli system wait
docker-compose exec engine-api anchore-cli image add “imagen”
docker-compose exec engine-api anchore-cli image wait “imagen”
docker-compose exec engine-api anchore-cli image vuln “imagen” all
docker-compose exec engine-api anchore-cli evaluate check “imagen” --detail

Es recomendable usar el digest de la imagen en vez de solo el nombre para evitar problemas de ambigüedad en caso de que la imagen cambie pero el nombre y tag. En este caso queremos analizar solo la última versión de la imagen, no queremos una imagen anterior especifica, así que podemos hacerlo solo poniendo el nombre.

Cuando se aplica el bundle de políticas por defecto (más adelante explicado) de Anchore el estado de éxito puede darse si:

  • La imagen evaluada está totalmente limpia de problemas, 
  • Solo hay warnings 
  • La imagen evaluada está en la lista blanca (no se evalúa). 

El estado fallido se da si: 

  • La imagen evaluada tiene al menos una vulnerabilidad que en el bundle de políticas está marcada como Stop
  • La imagen estaba en la lista negra (falla sin analizarla)

4.2.​ Anchore en integración continua (Gitlab CI)

La mayor virtud de Anchore es la facilidad de usarlo en un ciclo de integración continua para securizar los contenedores docker desplegados. De esta forma añadimos una capa de seguridad a la entrega continua.

Para mostrar un ejemplo de implementación de Anchore, he elegido Gitlab CI por ser gratuito (2000 horas al mes de ejecución de integración continua) y fácil de configurar para que cualquier commit que se suba al repositorio ejecute la integración continua con la fase de Anchore.

Anchore también se puede integrar con Jenkins, Travis CI o cualquier otro sistema de integración continua, contando con utilidades y ejemplos para facilitar la integración. Además de usarlo como imagen de docker, Anchore también se puede desplegar en Kubernetes como un pod que estará en el cluster de kubernetes siempre disponible. De tal forma que se pueda usar Anchore para otros propósitos solo usando la url del despliegue en vez de tener Anchore ejecutándose como una imagen de docker solo activa en el momento de la integración continua.

Según el objetivo que queramos conseguir podemos utilizar Anchore de dos formas distintas. La diferencia entre ellas es que para el primer método necesitamos que la imagen de docker esté subida a un repositorio para que Anchore la pueda descargar y analizar y para el segundo solo necesitamos dar a Anchore el dockerfile con el que construir la imagen de docker y esa imagen construida en local.

 

4.2.1.​ Analizar imágenes de docker una vez han sido subidas a un repositorio

Una vez almacenada la imagen de docker podemos analizar las vulnerabilidades que tiene esa imagen y decidir si es o no apta para desplegarla al entorno que elijamos. Después de esta fase de análisis podemos poner la fase de despliegue de esa imagen, asi solo si pasa el análisis se podrá ejecutar la fase de despliegue

repo_anchore_scan:
 image: anchore/engine-cli:latest
 variables:
   GIT_STRATEGY: none
   ANCHORE_CLI_URL: "http://anchore-engine:8228/v1"
   ANCHORE_SCAN_IMAGE: jesus12345/$IMAGE_NAME
   ANCHORE_TIMEOUT: 300
   ANCHORE_FAIL_ON_POLICY: "true"

 services:
   - name: docker.io/anchore/inline-scan:latest
     alias: anchore-engine
     command: ["start"]

 script:
   - anchore-cli --debug system wait
   - echo "Adding image to Anchore engine"
   - anchore-cli image add ${ANCHORE_SCAN_IMAGE}
   - echo "Waiting for analysis to complete"
   - anchore-cli image wait ${ANCHORE_SCAN_IMAGE} --timeout ${ANCHORE_TIMEOUT}
   - echo "Analysis complete"
   - anchore-cli --json image content ${ANCHORE_SCAN_IMAGE} os > image-packages.json
   - anchore-cli --json image content ${ANCHORE_SCAN_IMAGE} npm > image-npm.json
   - anchore-cli --json image content ${ANCHORE_SCAN_IMAGE} gem > image-gem.json
   - anchore-cli --json image content ${ANCHORE_SCAN_IMAGE} python > image-python.json
   - anchore-cli --json image content ${ANCHORE_SCAN_IMAGE} java > image-java.json
   - anchore-cli --json image vuln ${ANCHORE_SCAN_IMAGE} all > image-vulnerabilities.json
   - anchore-cli --json image get ${ANCHORE_SCAN_IMAGE} > image-details.json
   - anchore-cli --json evaluate check ${ANCHORE_SCAN_IMAGE} --detail > image-policy.json || true
   - echo "Reporting completed"
   - if [ "${ANCHORE_FAIL_ON_POLICY}" == "true" ] ; then anchore-cli evaluate check ${ANCHORE_SCAN_IMAGE} --detail; fi
 
artifacts:
   name: anchore_scan-$CI_PROJECT_NAME
   paths:
     - image-policy.json
     - image-details.json
     - image-vulnerabilities.json
     - image-java.json
     - image-python.json
     - image-gem.json
     - image-npm.json
     - image-packages.json

 retry:
   max: 2
   when:
     - stuck_or_timeout_failure
     - runner_system_failure

Para poder ejecutar esta fase se necesitan las imágenes de Engine cli y Anchore engine. Engine cli contiene los comandos de Anchore. Inline scan contiene Anchore engine, que lo inyectamos como servicio para poder ejecutar los análisis.

Explicación de la fase:

  • Esperamos a que el engine de Anchore se inicie para evitar que la fase falle por ejecutar un comando sin tenerlo iniciado
  • Añadimos la imagen al engine de Anchore
  • Esperamos a que Anchore la analice
  • Ejecutamos todos los comandos que reportan información en los distintos ficheros
  • Ejecutamos la evaluacion de politicas si hemos puesto la variable del if a True

Resultado de la fase:

Image Digest: 
sha256:7b022e8c46d0b41e60f46d4b6668345818c64c820498dbccbe99bf7e8c1a325a
Full Tag: docker.io/jesus12345/helm-docker-aws-cli-iam-authenticator:latest
Image ID: e6d6854c0ef83a78bcdeda807e9726bb439aa06ed82345825b62517c75dcf703
Status: pass
Last Eval: 2019-06-05T09:23:59Z
Policy ID: 2c53a13c-1765-11e8-82ef-23527761d060
Final Action: warn
Final Action Reason: policy_evaluation

Gate              Trigger Detail                                                                                     Status       
dockerfile        instruction Dockerfile directive 'HEALTHCHECK' not found, matching condition 'not_exists' check     warn

La evaluación muestra información general de la imagen junto con el resultado, exitoso en este caso al tener el status pass, esto implica que la imagen ha superado la evaluación contra la política por defecto (al no haber configurado una política distinta aun) de Anchore.

Además del status, también se muestra que hay cosas a mejorar, al tener un warn en el final action, indicando que el warning lo lanzó la política evaluada y abajo muestra detalladamente dónde está el problema para que se pueda solucionar de cara a la siguiente vez.

 

4.2.2.​ Analizar la imagen de docker y dockerfile antes de subirlo a un repositorio

Podemos utilizar Anchore para evitar almacenar una imagen de docker que no cumple con las políticas que hayamos establecido.

En vez de mostrar solo un ejemplo de ejecución, voy a mostrar dos fases que analizan la imagen en local. La diferencia radica en que en la primera fase se aplica una política donde sólo fallará el análisis si se descubre alguna vulnerabilidad crítica y en la segunda fase solo fallará si se descubren vulnerabilidades altas o críticas sin fix en el momento del análisis.
Esta comparativa es muy útil para mostrar la importancia de la política que se usa para analizar la imagen pues el resultado varía enormemente partiendo de la misma imagen.

variables:
  IMAGE_NAME: "helm-docker-aws-cli-iam-authenticator"

only-critical_inline_anchore_scan:
 image: docker:stable
 script:
   - apk update && apk --no-cache add curl bash
   - docker build -t example-image:latest -f $IMAGE_NAME/Dockerfile .
   - curl -s https://ci-tools.anchore.io/inline_scan-v0.3.3 | bash -s -- -f -d $IMAGE_NAME/Dockerfile -b anchore-policies/policy_bundle_only_critical.json example-image:latest

stop-high_inline_anchore_scan:
 image: docker:stable
 script:
   - apk update && apk --no-cache add curl bash
   - docker build -t example-image:latest -f $IMAGE_NAME/Dockerfile .
   - curl -s https://ci-tools.anchore.io/inline_scan-v0.3.3 | bash -s -- -f -d $IMAGE_NAME/Dockerfile -b anchore-policies/policy_bundle_stop_high_no_fix.json example-image:latest

Explicación de las fases:

  • Al partir de la imagen de docker hay que instalar curl y bash para que funcionen los comandos de ejecución del script.
  • Construimos la imagen de docker.
  • Ejecutamos el script de Anchore pasando el dockerfile, una política propia (esto es opcional y es la diferencia entre las fases, tambien se podria eliminar el flag y dejar que se use la politica por defecto) y el nombre de la imagen construida en el paso anterior.

Resultado de la fase (only-critical):

Image Digest: 
sha256:1d056e0a40284a38ee840c416143c1a33b49f2d7eea6bf7b2fecb23e733c1deb
Full Tag: anchore-engine:5000/example-image:latest
Image ID: d0349b1762b0b913eb831c5cd7300f95b664404f2c84bfb7f7f20ddcd933d816
Status: pass
Last Eval: 2019-06-05T09:36:46Z
Policy ID: only_critical
Final Action: go
Final Action Reason: policy_evaluation

Image Digest: sha256:1d056e0a40284a38ee840c416143c1a33b49f2d7eea6bf7b2fecb23e733c1deb
Full Tag: anchore-engine:5000/example-image:latest
Status: pass
Last Eval: 2019-06-05T09:36:47Z
Policy ID: only_critical

Este resultado tiene la peculiaridad de que se ha aplicado una política muy poco restrictiva, sólo mostrando errores cuando la imagen tiene una vulnerabilidad crítica e ignorando el resto de vulnerabilidades en el resultado, debido a esto el resultado ha sido exitoso, dando un status pass y además no hay ningún problema detectado pues la acción es go.

 

Resultado de la fase (stop-high):

Image Digest: 
sha256:a2ca6b81deba65acb84362f105f829c9b015227271cffa82ecbd29c19bf3e917
Full Tag: anchore-engine:5000/example-image:latest
Image ID: c31f418e86b3e7e357e0e8a366799414e88a9ef9db165e3f0a145ff735ae8b24
Status: fail
Last Eval: 2019-06-05T09:49:51Z
Policy ID: stop_high_and_critical_with_fix
Final Action: stop
Final Action Reason: policy_evaluation

Gate                   Trigger Detail                                                                                                                                                                            Status       
dockerfile             instruction Dockerfile directive 'HEALTHCHECK' not found, matching condition 'not_exists' check                                                                                                        
vulnerabilities        package MEDIUM Vulnerability found in non-os package type (python) - /usr/lib/python2.7/dist-packages/pycrypto (CVE-2018-6594 - https://nvd.nist.gov/vuln/detail/CVE-2018-6594)            warn         
vulnerabilities        package MEDIUM Vulnerability found in os package type (dpkg) - curl (CVE-2019-5436 - https://security-tracker.debian.org/tracker/CVE-2019-5436)                                            warn
vulnerabilities        package MEDIUM Vulnerability found in os package type (dpkg) - python2.7-minimal (CVE-2019-9947 - https://security-tracker.debian.org/tracker/CVE-2019-9947)                               warn         
vulnerabilities        package MEDIUM Vulnerability found in os package type (dpkg) - python2.7-minimal (CVE-2019-9948 - https://security-tracker.debian.org/tracker/CVE-2019-9948)                               warn         
vulnerabilities        package MEDIUM Vulnerability found in os package type (dpkg) - unzip (CVE-2018-1000035 - https://security-tracker.debian.org/tracker/CVE-2018-1000035)                                     warn         
vulnerabilities        package HIGH Vulnerability found in os package type (dpkg) - libssh2-1 (fixed in: 1.7.0-1+deb9u1) - (CVE-2019-3855 - https://security-tracker.debian.org/tracker/CVE-2019-3855)            stop         
vulnerabilities        package HIGH Vulnerability found in os package type (dpkg) - libsystemd0 (fixed in: 232-25+deb9u10) - (CVE-2018-15686 - https://security-tracker.debian.org/tracker/CVE-2018-15686)        stop         
vulnerabilities        package HIGH Vulnerability found in os package type (dpkg) - libudev1 (fixed in: 232-25+deb9u10) - (CVE-2018-15686 - https://security-tracker.debian.org/tracker/CVE-2018-15686)           stop         

Image Digest: 
sha256:a2ca6b81deba65acb84362f105f829c9b015227271cffa82ecbd29c19bf3e917
Full Tag: anchore-engine:5000/example-image:latest
Status: fail
Last Eval: 2019-06-05T09:49:52Z
Policy ID: stop_high_and_critical_with_fix

En contraposición con el resultado de la fase only-critical, en este resultado aparecen bastantes vulnerabilidades, la mayoría de ellas con status warn y alguna con status stop, dando como resultado y status final stop. 

Estos dos ejemplos muestran la gran diferencia al aplicar distintas políticas a la misma imagen y la importancia de la política que usamos al leer los resultados que da la evaluación. Si se mira el primer resultado sin saber la política, se puede llegar a la errónea conclusión de que la imagen es perfecta y no contiene ninguna vulnerabilidad, mientras que en la segunda se ven vulnerabilidades que se pueden arreglar.

 

5.​ Añadir repositorios privados a Anchore

Además de poder descargar imágenes de repositorios públicos Anchore permite añadir las credenciales de un repositorio privado. Al ejecutar el comando add image, Anchore automáticamente intentará descargar la imagen de docker.io o de la url indicada en el comando, sin requerir ninguna configuración extra. Si esta descarga falla, Anchore mostrará un mensaje de error. Por ejemplo si ejecuto anchore-cli image add jesus12345/alpine-private-repo Anchore me devuelve:

Error: cannot fetch image digest/manifest from registry
HTTP Code: 400
Detail: {'error_codes': ['REGISTRY_PERMISSION_DENIED']}

El mensaje muestra que o bien el repositorio no existe o tiene el acceso prohibido por falta de credenciales. En este último caso, Anchore nos facilita el poder añadir las credenciales para que al intentar acceder, use la lista de credenciales que tenga que pertenezcan a ese dominio.

Para añadir las credenciales de un repositorio tan solo hay que ejecutar el comando

anchore-cli registry add “urlRepositorio” “usuario” “contraseña”

Por ejemplo para poder descargar la imagen que antes ha dado fallo he añadido las credenciales de mi repositorio de github

anchore-cli registry add docker.io jesus12345 ********

Se pueden mirar la URL, tipo y el usuario de cada repositorio que Anchore tiene guardado con

anchore-cli registry list

En el ejemplo anterior si ejecutamos el comando, el resultado es:

Registry         Type User              
docker.io        docker_v2 jesus12345

Ahora podemos añadir la imagen y Anchore la descarga del repositorio sin errores

 

6.​ Políticas para la evaluación de imágenes

Además de proporcionar información sobre una imagen, Anchore puede realizar una evaluación de la imagen según las políticas definidas por el usuario. 

Una política se compone de un conjunto de reglas que se utilizan para realizar una evaluación de una imagen de contenedor. Estas reglas pueden incluir comprobaciones de vulnerabilidades de seguridad, listas blancas y negras de imágenes, contenido del archivo de configuración, presencia de credenciales en la imagen, puertos expuestos u otras comprobaciones definidas por el usuario. Estas políticas pueden aplicarse globalmente o personalizarse para imágenes específicas o categorías de aplicaciones.

Los bundles (paquetes) son la unidad de definición y evaluación de políticas en Anchore. Un usuario puede tener varios bundle de políticas pero solo se usa una para la evaluación de una imagen, esta puede ser la que esté activa en el motor de Anchore al realizar el análisis o la que se indique en el inline scan.

Un bundle contiene cero o más políticas. Las políticas de un paquete definen las comprobaciones que deben realizarse contra una imagen y las acciones que se recomiendan si las comprobaciones encuentran una coincidencia.

Un bundle de políticas es un documento JSON compuesto por:

  • Políticas: reglas y acciones
  • Listas blancas: exclusiones de reglas para anular una coincidencia de alguna política
  • Mapeos: determinar qué políticas y listas blancas deben aplicarse a una imagen específica en el momento de la evaluación. De tal manera que podemos aplicar más o menos reglas según la imagen.
  • Lista blanca de imágenes: Imágenes que pasarán automáticamente la evaluación, sin importar si cumplen o no las políticas
  • Lista negra de imágenes: reemplaza las imágenes específicas para establecer de forma estática el resultado final en un error, independientemente del resultado de la evaluación de la política

 

​6.1.​ Política por defecto

Anchore cuenta con una política activada por defecto para poder ejecutar el análisis de imágenes sin tener que hacer ninguna configuración extra. Esta política comprueba que no haya ninguna vulnerabilidad alta o crítica, si la hay lo marca en los logs y hará que falle la integración continua si se está ejecutando el análisis en una fase. Las vulnerabilidades medias o bajas las marca con un warning, pero estas vulnerabilidades no harán fallar la integración continua.

Para poder saber qué política está usando Anchore, podemos listar las políticas que hay en anchore-cli, mostrando el id y si están o no activas con el comando:

anchore-cli policy list

Si queremos más información sobre una política en especial, podemos listar todos los datos de una política usando el Id de la política con el comando:

policy get 715a6056-87ab-49fb-abef-f4b4198c67bf --detail

 

6.2.​ Cómo crear una política propia y activarla

Para crear una política, necesitamos crear un bundle y activarlo. Al hacer una bundle propio de políticas hay que asegurarse que el ID principal del bundle no exista en la lista de políticas de Anchore y que los IDs internos de cada mapeo, policies y reglas no coincidan entre ellos porque dará fallo al intentar usarlo como política y Anchore usará la política por defecto.

Otra cosa importante a tener en cuenta es que el ID general del bundle puede ser cualquier cadena de caracteres, por ello aconsejo poner un nombre descriptivo para luego poder reconocer nuestro bundle por el ID facilmente al listar las políticas poder diferenciarlas.

Cuidado, el ID no puede tener espacios en blanco o al ejecutar el comando para activar la política fallará aunque al crear el bundle no nos haya dado problema.

Este es un ejemplo de bundle que podemos aplicar para ignorar fallos de vulnerabilidades high que no tengan fix para poder pasar el umbral de calidad al analizar la imagen (la politica por defecto no pasara el umbral si se descubre una vulnerabilidad high aunque no tenga fix).

{
   "blacklisted_images": [],
   "comment": "Personalized bundle ignoring unfixable vulnerabilities",
   "id": "stop_high_and_critical_with_fix",
   "mappings": [],
   "name": "Personalized bundle",
   "policies": [
       {
           "comment": "Personalized policy ignoring unfixable vulnerabilities",
           "id": "48e6f7d6-1765-11e8-b5f9-8b6f228548b6",
           "name": "IgnoreUnfixablePkgs",
           "rules": [
               {
                   "action": "STOP",
                   "gate": "vulnerabilities",
                   "id": "80569900-d6b3-4391-b2a0-bf34cf6d813d",
                   "params": [
                     { "name": "package_type", "value": "all" },
                     { "name": "severity_comparison", "value": ">" },
                     { "name": "severity", "value": "medium" },
                     { "name": "fix_available", "value": "true"}
                   ],
                   "trigger": "package"
                 }
           ],
           "version": "1_0"
       }
   ],
   "version": "1_0",
   "whitelisted_images": [],
   "whitelists": []
}

La parte importante es el objeto dentro de rules, donde definimos que se pare la ejecución si se encuentra una vulnerabilidad que cumpla con los parámetros definidos. 

Una vez tenemos el bundle listo se añade un bundle de una política

anchore-cli policy add bundle.json

El resultado del comando de arriba nos devolverá un id que es el que usamos para activar, si no se activa Anchore no la tendrá en cuenta al ejecutar el análisis

anchore-cli policy activate 2170857d-b660-4b56-a1a7-06550bf02eb2

Para mas informacion sobre como crear una politica: https://anchore.freshdesk.com/support/solutions/articles/36000074706-policies

 

6.3.​ Descargar y activar una política creada por la comunidad de Anchore

Anchore Policy Hub es un repositorio centralizado que ofrece una bundles con políticas predefinidas que están disponibles públicamente y pueden ser cargados / consumidos por Anchore. Este sistema sirve como un almacén de políticas para Anchore, donde bundles con políticas predefinidas se pueden buscar y descargar fácilmente, además de servir como un punto de partida para crear una politicas propia y como punto de encuentro donde los usuarios de Anchore puedan enviar y compartir nuevos paquetes de políticas.

Para poder usar Anchore Policy Hub solo hay que tener anchore-engine funcionando y anchore-cli con la versión 0.3.2 o superior.

Pasos para usar una política del hub:

  1. Comprobar la versión

anchore-cli --version

2. Mostrar las políticas disponibles en Anchore Policy Hub:

anchore-cli policy hub list

Name                           Description                                                         

anchore_security_only          Single policy, single whitelist bundle for performing               
                               security checks, including example blacklist known malicious        
                               packages by name.                                                   

anchore_default_bundle         Default policy bundle that comes installed with vanilla             
                               anchore-engine deployments.  Mixture of light vulnerability         
                               checks, dockerfiles checks, and warning triggers for common         
                               best practices.                                                     

anchore_cis_1.13.0_base        Docker CIS 1.13.0 image content checks, from section 4 and 5 
                               NOTE: some parameters (generally are named 'example...') must 
                               be modified as they require site-specific settings

3. Descargar una política del listado

anchore-cli policy hub get anchore_cis_1.13.0_base

Name: anchore_cis_1.13.0_base
Description: Docker CIS 1.13.0 image content checks, from section 4 and 5. NOTE: some parameters (generally are named 'example...') must be modified as they require site-specific settings

4. Instalar la política

anchore-cli policy hub install anchore_cis_1.13.0_base

Policy ID: anchore_cis_1.13.0_base
Active: False
Source: local
Created: 2019-01-31T18:42:50Z
Updated: 2019-01-31T18:42:50Z

5. Activar la política para que sea la que esté en uso

anchore-cli policy activate anchore_cis_1.13.0_base

6. Comprobar que la política se ha instalado correctamente y está activa

anchore-cli policy list

Policy ID: anchore_cis_1.13.0_base
Active: True
Source: local
Created: 2019-01-31T18:42:50Z
Updated: 2019-01-31T18:42:50Z

 

​7.​ Listado de comandos

Aqui podreis ver todos los comandos que más frecuentemente podréis usar junto con una breve explicación de lo que hacen:

Muestra todas las imágenes que se han analizado con Anchore y sobre las que puedes obtener información con image vuln

anchore-cli image list

Obtiene información resumen de la imagen, la misma que cuando se termina de ejecutar el comando anchore-cli image wait “nombreImagen” wait de la imagen

anchore-cli image get “nombreImagen”

Muestra la lista de sitios que proveen a Anchore la información sobre vulnerabilidades

anchore-cli system feeds list

Espera hasta que el motor y el servicio de Anchore este operativo, útil como primer comando en la integración continua para esperar a que se inicie todo antes de ejecutar un comando. Si no se usa podría suceder que se ejecute un comando de Anchore y de error por no haber esperado a que se inicialice

anchore-cli system wait

Añadir la imagen para que Anchore la analice, Anchore bajara del repositorio de docker hub si solo se especifica el nombre en vez de la url

anchore-cli image add “urlImagen/nombreImagen”

Muestra el contenido del tipo que se especifique, a elegir la lista de abajo.

anchore-cli image content “nombreImagen” “tipo”

Siendo tipo uno de estos posibles:

  • os: paquetes del sistema operativo de la imagen
  • files: sistema de ficheros de la imagen
  • npm: paquetes npm que la imagen contiene
  • gem: paquetes gem que la imagen contiene
  • python: paquetes python que la imagen contiene
  • java: paquetes java que la imagen contiene

 

Muestra información del bundle de políticas

anchore-cli policy hub get “nombrePolitica”

Muestra toda lista de políticas predefinidas por Anchore. anchore_default_bundle viene preinstalado para ejecutar la evaluación de imagen contra esa política

anchore-cli policy hub list

Instala el bundle de políticas, pero no las activa, solo puede haber una política activada

anchore-cli policy hub install “idPolitica”

Añade una política desde un fichero json

anchore-cli policy add “idPolitica”

Activa cualquier política de la lista de políticas disponibles, solo puede haber una política activa a la vez

anchore-cli policy activate “idPolitica”

Ejecutar comprobación de que la imagen cumple las políticas definidas y mostrar los detalles de las vulnerabilidades encontradas que han causado que la imagen sea suspendida en la evaluación.

anchore-cli evaluate check “nombreImagen” --detail

Añadir las credenciales de un repositorio

anchore-cli registry add “urlRepositorio” “usuario” “contraseña”

Borra las credenciales que Anchore tiene guardada bajo el nombre del registro

anchore-cli registry del “nombreRegistro”

La entrada Anchore – Análisis de vulnerabilidades de docker se publicó primero en Adictos al trabajo.

Obtener datos con R (HTML, XML y FWF)

$
0
0

Aprenderemos a construir un dataFrame, a obtener datos con R a través de la librería RCurl, a leer un fichero de formato de ancho fijado (FWF), tablas HTML y a parsear XMLs. También presentaremos la sintaxis del $, y comenzaremos a hacer nuestros primeros pinitos con gráficos,

Índice de contenidos

1. Introducción

Este artículo está enmarcado dentro de una serie de tutoriales sobre R y RStudio. En capítulos anteriores se vio cámo leer un archivo CSV con RStudio, y cómo cargar un JSON desde internet en un dataFrame con el que poder trabajar. Tuvimos una primera toma de contacto con las librerías

jsonlite
y
curl
, y aprendimos algunas funciones básicas como es concatenar cadenas de texto y escapar caracteres de una URI. También aprenderemos a mezclar dos dataFrames por un índice y a verificar que son idénticos.

El objetivo de este tercer post es ir acabando la parte introductoria, que la verdad, ya se está haciendo larga. Pero primero aún tenemos que aprender unas pocas cositas más antes de dar el siguiente paso.

2. Construir un DataFrame

En alguna ocasión, nos puede interesar construir nuestro propio DataFrame en base a datos que ya tengamos, o que nosotros mismos fabriquemos. Es muy fácil, pero lo cierto es que aún no lo he explicado en ninguno de los post anteriores.

Para ejemplificar como crear un dataFrame, vamos a construir uno muy sencillo con algunos datos reseñables de los municipios de Madrid con más de 150.000 habitantes.

Lo primero que haremos será definir el conjunto de nombres de esos municipios. Recordemos que para definir un conjunto se usa la función c() pasándole por parámetros los elementos de dicho conjunto.

nombre <- c("Madrid","Móstoles","Alcalá de Henares","Fuenlabrada","Leganés","Getafe","Alcorcón")

El siguiente paso será declararemos una variable que guarde un conjunto con la población de cada municipio. Esto es un conjunto con el número de habitantes de cada ciudad.

poblacion <- c(3265038,205015,203686,198560,186552,170115,168523)

Repetiremos la operación con la superficie en Km2 y la altitud en metros sobre el nivel del mar.

superficie <- c(605.77, 45.36, 87.72, 39.41, 43.09, 78.38, 33.73)
altitud <- c(657, 660, 587, 662, 667, 622, 711)

Y por fin, ya estamos es disposición de construir el dataFrame municipios como la combinación de todas estas variables.

municipios <- data.frame(nombre, poblacion, superficie, altitud)

dataFrame creado

3. Leer un fichero de ancho fijo

Sin embargo, hay ocasiones en que la estructura de la información no viene de una forma tan intuitiva, y los campos de cada fila vienen definidos en base a su longitud, y a la posición que ocupa en la línea. Por ejemplo

12345678FManolito Gafotas Gafotas       915555555MADRID

Donde la posición:

  • [1 – 9] es el DNI
  • [10 – 40] el nombre
  • [41 – 49] el teléfono
  • [50 – 65] la localidad

He buscado y buscado ejemplos de ficheros de este tipo en Internet sin demasiado éxito. Sin embargo, me consta que se siguen usando una barbaridad, pero se suelen consumir de forma interna, entre sistemas middleware y no es habitual que se expongan públicamente en ninguna web. He encontrado uno que puede servir a nuestros fines. Vamos a ver como leer un fichero de este tipo, que llamaremos FWF – Fixed Width Format.

obtener datos con R. Ejemplo fichero de formato de ancho fijado

En esta ocasión se ve claramente que la información está dispuesta en 5 columnas. Que las 4 primeras filas las podríamos saltar, pues no nos aportan información a los datos. Usamos una función para leer los ficheros de tipo Fixed Width Format, e indicamos la longitud de las columnas:

data <- read.fwf("https://www.cpc.ncep.noaa.gov/data/indices/wksst8110.for",
         widths=c(15, 13, 13, 13, 8), 
         skip=4,
         col.names = c("Week", "Nino1+2", "Nino 3", "Nino 34", "Nino 4"))

No hay trim y son factores

El resultado se acerca a lo que queremos, pero hay algunos matices que nos convendría reseñar. Démonos cuenta que los valores de las columnas tienen espacios a izquierda y derecha. Además la función los ha cargado como factores en lugar de strings. Quizás sería interesante hacer el trim y convertirlos a strings.

# los valores contienen espacios delante y detrás.
# con el simbolo menos, indicamos el ancho, pero que no queremos la columna
# y además indicamos que los strings no son factores
data <- read.fwf("https://www.cpc.ncep.noaa.gov/data/indices/wksst8110.for",
                 widths=c(-1, 9, -5, 8, -5, 8, -5, 8, -5, 8), 
                 skip=4,
                 col.names = c("Week", "Nino1+2", "Nino3", "Nino34", "Nino4"),
                 stringsAsFactors = F
                 )

De esta forma, el resultado sí es como deseamos.

4. Leer una tabla HTML de la Web

Con frecuencia las fuentes de datos están incrustadas en una web y necesitamos adquirir dicha información para trabajar con ella en un dataFrame.

En este ejemplo, vamos a conectarnos a la página de cotizaciones de El País, para coger los valores del Ibex 35. Para ello utilizaremos la librería XML.

install.packages("XML")
library(XML)
urlDeEjemplo <- "https://cincodias.elpais.com/mercados/bolsa/ibex_35/582/"
doc <- readHTMLTable(urlDeEjemplo)

La petición anterior nos ha devuelto un error:

# Warning message: XML content does not seem to be XML:
.

Esto error ocurre porque la función readHTMLTable no es capaz de leer HTTPS. Para eso usaremos la librería

RCurl
, que nos permite obtener datos con R y guardarlos en un objeto en memoria, aunque el contenido esté en una URL segura.
install.packages("RCurl")
library(RCurl)

# y obtenemos el HTML y lo guardamos en una variable de R
htmlData <- getURL(urlDeEjemplo)

# ahora buscamos las tablas existentes en el HTML
tables <- readHTMLTable(htmlData)

# tables es una lista de dos dataFrames (hay dos tablas)
# nos quedamos con el que se llama indice_valores
ibex35 <- tables[["indice_valores"]]

# visualizamos los datos que hemos obtenido
View(ibex35)

Obtener datos con R: Ibex 35

Con RCurl, podemos obtener datos con R, ya sean XML, HTML, FWF o cualquier otro tipo de formato. Merece la pena echar un vistazo a la documentación de RCurl para comprender sus posibilidades.

5. Leer un XML de la Web

Otro ejemplo habitual que nos quedaría tratar es el de leer un XML de la WEB. El ejemplo que hemos elegido es un XML de la Biblioteca Nacional.

# instalamos/actualizamos la librería y la cargamos
install.packages("XML")
library(XML)

# Número de registros de autoridad en el catálogo de la BNE en 2018
# http://www.bne.es/media/datosgob/estadisticas/autoridades/Autoridades_2018.xml

urlBNE2018 <- "http://www.bne.es/media/datosgob/estadisticas/autoridades/Autoridades_2018.xml"
xmlDocument <- xmlParse(urlBNE2018)
rootNode <- xmlRoot(xmlDocument)

# es un XML muy sencillo que no tiene atributos y sólo tiene valores
# Lo que queremos hacer es por cada ITEM extraer los pares clave-valor
# usamos xmlSApply que a cada elemento le aplica una función. 
catalogo <- xmlSApply(rootNode, function(x) {
    xmlSApply(x, xmlValue)
})

# la hemos aplicado dos veces. Vemos la pinta que tiene
View(catalogo)

matriz cambiada filas x columnas

# Se observa que las filas están cambiadas por columna.

# transponemos la matriz catalogo y la guardamos en la misma variable
catalogo <- t(catalogo)

# y lo convertimos en un dataFrame
catalogo <- data.frame(catalogo)

Matriz transpuesta

# pero los nombres de las filas son item.1 ... item.10.
# podemos solventarlo poniéndolos a NULL y será 1 ... 10
row.names(catalogo) <- NULL

# pero a lo mejor preferimos usar como rowNames el valor de la columna "Datos"
# y quitar dicha columna que no aporta información numérica
row.names(catalogo) <- catalogo$Datos
catalogo$Datos <- NULL

Obtener datos con R

6. La sintaxis del $

Sin apenas darnos cuenta me temo que ya hemos utilizado la sintaxis del dólar. Lo hemos hecho de una forma intuitiva para acceder a valores de columnas.

# por ejemplo
row.names(catalogo) <- catalogo$Datos

Como muestra, utilizaremos una página con algunos datos de Alcorcón y me voy a fijar en la población.

install.packages("jsonlite")
library(jsonlite)
poblacion_alcorcon <- fromJSON("poblacion_alcorcon.json")

# La variable es una "List of 5". Veamos que aspecto tiene la lista

Obtener datos con R: poblacion de Alcorcón

En la parte inferior de la imagen se nos indica la ruta hasta llegar a esa variable en forma de array asociativo.

# Al ser una lista podemos recorrerla como array asociativo
pa1 <- poblacion_alcorcon[["Datos"]][["Metricas"]][["Datos"]][[1]]

Pero es mucho más cómodo acceder por nombre de variable al interior de cada uno de los objetos usando el operador $.

# pero podemos usar la potencia del operador $ (RStudio nos autocompleta)
pa2 <- poblacion_alcorcon$Datos$Metricas$Datos[[1]]

Comprobemos que ambos dataFrames son idénticos.

identical(pa1,pa2)

Y es mucho más cómodo recorrer la estructura anidada mediante el operador $ que como array asociativo.

DataFrame población Alcorcón

7. Primeros gráficos con R

R trae de serie un buen conjunto de dataSets para que podamos hacer nuestras pruebas, y muchos de los ejemplos que encontraréis en internet usan estos dataSets, que se encuentran en el package:dataSets

Elijo el dataSet

airquality
que nos dice la calidad del aire en New York de mayo a septiembre de 1973. Echemos un vistazo a este dataSet.
View(airquality)

Calidad del aire en NY

Si queremos un histograma con la densidad de frecuencias de temperatura sería:

hist(airquality$Temp)

Se observa que el gráfico es bastante simple, y que no hemos podido indicar el número de barras que queremos. En el eje Y aparece la frecuencia de cada rango de temperaturas, y en el eje X unas franjas de temperatura.

Histograma por defecto

Sin duda, podemos mejorarlo. Lo primero de todo es indicar el número de barras que queremos. Entre la temperatura mínima y la máxima hay 41 ºF de diferencia. Podemos usar esta medida como punto de partida para establecer el número de barras. Le podemos añadir un título con el parámetro main, y las etiquetas que deberán tener el eje X e Y. Además de jugar un poquito con los colores de relleno y de las líneas.

numsaltos <-  max(airquality$Temp) - min(airquality$Temp)

hist(airquality$Temp, 
    breaks = numsaltos/2,
    col = "lightgoldenrodyellow",
    border = "lightgoldenrod3",
    xlab = "Temperatura (ºF)",
    ylab = "Frecuencia",
    main = "Frecuencia de temperaturas en NY (1973)"
    )    

# para conocer el nombre de los colores podemos usar
colors()

histograma a color

8. Conclusiones

Aún no hemos hecho grandes cosas. Estamos asentando los cimientos con los que trabajaremos en R. Pero no es baladí lo aprendido en este artículo:

  • hemos aprendido a construir un dataFrame a mano
  • ya sabemos como obtener datos con R
  • somos capaces de leer ficheros con formateo de ancho fijo (fixed width format)
  • podemos leer una tabla HTML de la Web, que nos introduce en el Web Scraping
  • hemos usado la librería RCurl para obtener ficheros a través de HTTPS
  • aprendimos a leer XML de la Web y parsear documentos XML
  • hicimos un acercamiento a la función xmlSApply
  • aprendimos a obtener la transformada de una matriz
  • conocimos la sintaxis del dolar ($)
  • y tuvimos un primer acercamiento a los gráficos en R, en concreto con el histograma

Enlaces y Referencias

La entrada Obtener datos con R (HTML, XML y FWF) se publicó primero en Adictos al trabajo.


¿Dónde empieza la transformación?

$
0
0

 

Siempre se ha dicho que el activo más preciado en una organización es su gente. Sin embargo, muchas veces nos perdemos en esta afirmación y dejamos de lado esos aspectos del entorno que afectan a las personas. Las empresas que tienen como norte una transformación digital, se esfuerzan en formar equipos con personas talentosas, dispuestas a contribuir con una cultura flexible, abierta y capaz de desarrollarse en entornos cambiantes. Nosotros, las personas, somos quienes damos vida a la organización y hacemos realidad las soluciones que satisfacen las necesidades existentes en el mercado, incluso somos mercado; por tanto debemos tener buena actitud y competencias acorde a las exigencias que se nos van presentando.

Hacia dónde vamos con una transformación digital

Hablar de transformación digital a veces resulta polémico porque no está claro todo aquello que puede abarcar. De manera general su objetivo es enfocar a las organizaciones en el desarrollo del negocio y la adaptación al cambio.

La transformación digital busca, entre otras cosas, incorporar tecnología como parte del ADN de la empresa, haciendo que la organización pueda dedicar esfuerzos en aspectos estratégicos: desarrollar el negocio, ser más competitivos, mejorar los procesos, entender las necesidades del cliente, buscar más y mejores experiencias, entre otros. Este proceso de cambio impacta principalmente a las personas y para que sea efectivo se debe evolucionar a una cultura más ágil, tolerante a riesgos y experimental, porque requiere que la organización modifique su dinámica de trabajo y piense diferente.

Una disrupción digital necesita, sin lugar a duda, líderes entrenados con disposición a aprender continuamente y a adaptarse. Se debe tener presente que este proceso no es solo responsabilidad de la alta gerencia y que demanda la participación e influencia de todos los que conforman la organización, se necesita la creación y comunicación de una visión estratégica, la generación de confianza en los clientes y stakeholders, las ganas de probar y experimentar al estilo de las startups y sobre todo mantener las mejores experiencias del pasado para construir el futuro.

Finalmente, es importante mencionar que las organizaciones deben tener la habilidad de adaptar la gestión de las personas y ser capaces de atraer y retener talento que esté dispuesto a afrontar este nuevo modelo y que posea competencias particulares que hagan más llevadero este proceso de transformación.

Las personas como elemento clave de la transformación digital

Existen competencias duras (hard skills), blandas (soft skills) e híbridas (hybrid skills). Las organizaciones necesitan personas con una mezcla (blending) de cada tipo que les permitan cambiar, adaptarse, crecer, ser ágiles y hacer de sus organizaciones un entorno propicio para el crecimiento, la innovación y la creatividad.

Según mi experiencia y de acuerdo a los resultados obtenidos en un estudio realizado por el MIT & Deloitte, publicado en el libro Technology Fallacy las empresas que desean alcanzar con éxito un proceso de cambio deben tener las siguientes competencias claves:

Distribución de competencias
Competencias claves
    1. Orientación al cambio:

      Describe la capacidad de las personas para adaptarse a nuevas formas de hacer las cosas: ser ágil. Esta característica es importante en todos los niveles de la organización. Los equipos con esta competencia son capaces de innovar y buscar soluciones disruptivas. Se les hace más fácil conseguir los objetivos de la transformación y nunca pierden la motivación. Son una pieza importante para promover el cambio en la organización. Las personas con orientación al cambio están en constante movimiento y son agentes de cambio. Sin embargo, es importante ayudarles a mantener el foco en los objetivos que se quieren alcanzar porque puede que en su afán de cambio se disperse en muchos objetivos.

    2. Comprensión de la tecnología

      La transformación digital no se refiere a un cambio en el uso y aplicación de la tecnología sino más bien necesita que se tenga presente, se entienda y se sienta entusiasmo por ella. En este sentido, la idea es que las personas estén familiarizadas con las herramientas tecnológicas existentes para realizar su trabajo y es muy importante que tengan la capacidad y necesidad de aprender nuevas cosas. Ahora mismo, la tecnología está en constante evolución, por ejemplo los algoritmos de búsqueda en Instagram o Google cambian a diario. Las personas deben estar abiertos a aprender y ser curiosas de lo que pasa como un medio para optimizar los procesos y/o generar mejores resultados. La tecnología ha pasado de ser un departamento a uno de los pilares más importantes en la construcción de las empresas.

    3. Pensamiento estratégico:

      Sin importar el nivel o estructura de la organización, la estrategia debe ser el aire que se respira en la organización y sus colaboradores deben ser y sentirse parte de ella. A pesar que muchos equipos dan poca importancia o están poco involucrados con la estrategia, el reto de las organizaciones es ser transparentes e invitar a cada persona a contribuir en la construcción de resultados, esto ayudará a que las personas se sientan valiosas, orienta el camino dentro de la empresa, da sentido a lo que se hace y lo motiva a buscar soluciones. En otras palabras, es saber como se puede aportar valor a la organización y darle sentido al tiempo que dedico cada día, apoyar con la creación de experiencias del cliente a pesar de no estar directamente relacionados con ellos.

    4. Conocimiento general:

      El conocimiento general del entorno, la organización, los procesos son puntos claves para que las personas se sientan en confianza de participar y tengan un sentido de pertenencia de la organización y del equipo. Estar en contacto con otras áreas de negocio, conocer cómo funcionan las cosas dentro de la organización, indagar sobre los productos, las necesidades del entorno, formar parte de la empresa, entre otros, son puntos importantes y deben ser atendidos por cada integrante de un equipo ágil. Las personas deben crear empatía con el entorno. En la medida en que las organizaciones sean transparentes hará que las personas se auto organicen y se alineen en la misma dirección y esto influye notablemente en el valor que se genera.

    5. Relaciones interpersonales:

      La capacidad de poder relacionarse y tener empatía con el equipo, con la organización es una característica importante de las personas en el proceso de transformación. Los equipos deben ser capaces de colaborar entre ellos, trabajar juntos para alcanzar los objetivos y empatizar a pesar de las diferentes disciplinas que coexisten en un ecosistema de negocio. 

No olvidemos

En un mundo con cambios tan acelerados, se requiere que las organizaciones se doten de una infraestructura que les permita afrontar los retos que se plantean y siempre deben tener presente a las personas como su activo más poderoso. Las mejores estrategias digitales necesitan talento que sea capaz de adaptarse al cambio, esté dispuesto a aprender constantemente, sea cercano a la tecnología, piense estratégicamente, se familiarice con los objetivos de la organización y sobretodo que pueda mantener buenas relaciones interpersonales.

Sin embargo, a pesar de que es nuestra responsabilidad ser buenos profesionales y nuestra motivación es hacer las cosas lo mejor posible, las organizaciones deben hacer lo necesario por propiciar un entorno de confianza, comunicar lo que se espera y ser transparente en todo momento. En la medida que esto exista, se creará el sentido de pertenencia que empodere e impulse a las personas a luchar y alcanzar objetivos.

La entrada ¿Dónde empieza la transformación? se publicó primero en Adictos al trabajo.

Tests de integración automáticos Docker, Flyway y Maven

$
0
0

Índice de contenidos

Introducción

Crear tests de integración totalmente automáticos es el objetivo de muchos desarrolladores. Hasta no hace mucho, las bases de datos embebidas en memoria nos ofrecían la ventaja de atomizar completamente nuestros tests sin perder la capacidad de integrar con bases de datos (o cualquier otro servicio). Sin embargo, a las buenas o a las malas, muchos ya hemos aprendido que usar bases de datos en memoria no es lo mejor en cuanto a tests de integraicón se refiere.

Gracias a Docker, todo esto se vuelve mucho más sencillo. Levantar una base de datos realmente idéntica a la real se ha convertido en una nimiedad. Y, cuando se trata de ejecutar todo el histórico de scripts para actualizar una base de datos a la última versiòn, Flyway nos ofrece automatización total.

Como es bien sabido, Maven cuenta con infinidad de plugins entre los que no podían faltar plugins para Docker y Flyway. Aquí, en Adictos al trabajo, ya os hemos hablado de algunos plugins para docker y cómo configurarlos. Lo mismo pasa con Flyway, que ya es un viejo conocido en este sitio. Así que no voy a repetir lo mismo. Pero vamos a ir un pasito más allá.

Objetivo

Sí, crear una base de datos es muy sencillo con docker. En muchos entornos de desarrollo, tienes que levantarte tu base de datos (aunque se aun docker) y ejecutar tus scripts para crear los esquemas y actualizarla. Parece una tontería pero, ¿y si automatizamos todo esto? Imagínate descargarte el entorno de un proyecto y poder ejecutar los tests de integración automáticos solo con tener el daemon de docker en ejecución.

Docker

Para este ejemplo voy a utilizar un plugin diferente a los que ya habíamos visto antes: io.fabric8/docker-maven-plugin.

Tiene muy buena documentación, así que no te olvides de echarle un vistazo para conocer este plugin más en profundidad y poder adaptarlo perfectamente a tu proyecto.

La configuración que voy a utilizar yo es la siguiente:

<plugin>
  <groupId>io.fabric8</groupId>
  <artifactId>docker-maven-plugin</artifactId>
  <version>${docker-maven-plugin.version}</version>
  <executions>
    <execution>
      <id>prepare-it-database</id>
      <phase>pre-integration-test</phase>
      <goals>
        <goal>start</goal>
      </goals>
      <configuration>
        <images>
          <image>
            <name>mysql:5.7</name>
            <alias>mysql</alias>
            <run>
              <ports>
                <port>{mysql.integrationTests.port}:3306</port>
              </ports>
              <env>
                <MYSQL_ROOT_PASSWORD>root</MYSQL_ROOT_PASSWORD>
                <MYSQL_DATABASE>${flyway.db.name}</MYSQL_DATABASE>
                <MYSQL_USER>${flyway.db.user}</MYSQL_USER>
                <MYSQL_PASSWORD>${flyway.db.password}</MYSQL_PASSWORD>
              </env>
              <log>
                <date>long</date>
                <color>green</color>
              </log>
              <wait>
                <time>15000</time>
              </wait>
            </run>
          </image>
        </images>
      </configuration>
    </execution>
    <execution>
      <id>remove-it-databse</id>
      <phase>post-integration-test</phase>
      <goals>
        <goal>stop</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Esta configuración levantará un contenedor con la imagen mysql v5.7 en el puerto especificado.

En la sección <env> configuramos las variables de entorno para nuestro contenedor, tal y como haríamos con el flag -e al ejecutar nuestro contenedor por terminal.

La ejecución para levantar el contenedor se dará en la fase pre-integration-test y, en la fase post-integration-test, se parará y eliminará el contenedor. El proyecto usado para el ejemplo utiliza el plugin failsafe para controlar la ejecución de los tests de integración, por eso tenemos disponibles estas fases para antes y después de los tests de integración. Si en tu caso no es así, deberás adaptarlo para que funcione en tu caso concreto.

Un detalle que no quiero pasar por alto, aunque parezca ridículo, es el <wait> en el que especificamos que espere 15 segundos. Para ello debemos entener cómo funciona la imagen de mysql para Docker: al ejecutar el comando que arranca el contenedor, tarda apenas unos milisegundos en devolver la respuesta existosa. Pero esto significa solo que el contenedor se ha creado satisfactoriamente. Sin embargo, el servicio de mysql dentro de este contenedor todavía no ha terminado, ya que está ejecutando lo que le hemos indicado en las variables de entorno (crear la base de datos y el usuario con permisos).

En principio esto no siempre debe suponer un problema. No obstante, ese usuario, contraseña y base de datos son necesarios para que, justo después, actúe Flyway con la configuración que veremos más adelante. Si dejamos al plugin actuar inmediatamente, a velocidad de máquina, lo que pasará es que dará error porque el servicio de mysql todavía no habrá terminado de crear la base de datos y el usuario para Flyway, por lo que no podrá conectarse. Además, durante ese tiempo indicado en milisegundos, veremos la terminal del contenedor, tal y como está configurado en el apartado <log>. Y esto siempre viene bien para controlar si todo ha ido como debería.

Algunos otros detalles que viene bien tener en cuenta:

  • No elimina volúmenes por defecto. Existe la opción que permite eliminarlos y, si utilizamos volúmenes, debemos configurar el plugin adecuadamente.
  • Tampoco lo hace con las imágenes y las networks.
  • Por el funcionamiento de Maven, si el proyecto tiene submódulos que heredan la misma configuración a través del pluginManagement, debemos tener en cuenta que, se ejecuten o no tests de integración, siempre se levantará el contenedor en cada módulo. Por tanto, debemos controlar según el perfil la fase en la que se ejecuta el plugin. Aunque estemos funcionando, por ejemplo, con un perfil que no ejecuta tests de integración, la fase de los tests de integración no deja de existir, lo único es que failsafe «skipeará» los tests. Por tanto, como la fase sigue existiendo, el contenedor se levantará siempre. Un truco es, según el perfil, marcar la fase de ejecución a none para que no se levante. Aunque también existe el atributo skip que puedes configurar según el perfil.

Por último, la mejor recomendación que puedo hacerte acerca del plugin para Docker, es que visites su buena documentación y desarrolles la configuración que más sentido tenga para tu proyecto.

Flyway

Lo siguiente, tras levantar la base de datos, es configurar flyway para que ejecute todo el histórico y deje el esquema correcto preparado para lanzar nuestros tests de integración. Como os dije al princpio, no es la primera vez que os hablamos de cómo combinar Flyway y Maven. Además, cuenta con una buena documentación de obligada lectura para poder sacarle el máximo provecho.

Os dejo un ejemplo para poder trabajar en conjunto con Docker:

<plugin>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-maven-plugin</artifactId>
    <version>${flyway.version}</version>
    <configuration>
        <url>${dbUrlFlyway}</url>
        <user>${flyway.db.user}</user>
        <password>${flyway.db.password}</password>
        <locations>
            <location>${flyway.location.before}</location>
            <location>${flyway.location.profile}</location>
            <location>${flyway.location.after}</location>
        </locations>
    </configuration>
    <executions>
        <execution>
            <id>flyway</id>
            <phase>pre-integration-test</phase>
            <goals>
                <goal>clean</goal>
                <goal>migrate</goal>
            </goals>
        </execution>
    </executions>
    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>{mysql.version}</version>
        </dependency>
    </dependencies>
</plugin>

Con esta configuración, cargará los archivos ubicados en <locations> a nuestra base de datos según la configuración que le hemos indicado (usuari, contraseña…).

En este caso hemos configurado los goals clean y migrate. Normalmente es solo necesario con migrate, pero así os mostrábamos cómo es posible ejecutar en ese orden más de un goal.

Otras recomendaciones

Ambos plugins están configurados para ejecutarse en la fase pre-integration-test. Se ejecutarán por orden: si declaras primero flyway, intentará lanzarse y, como no estará todavía preparado docker, no funcionará.

Si tienes varios módulos que ejecutan tests de integración, por el propio funcionamiento de maven, creará y destruirá el contenedor según vaya construyendo cada módulo. Por tanto, si puedes agrupar todo en un módulo, ahorrarás tiempo de ejecución.

Conclusión

Automatizar al máximo los tests de integración siempre ha sido un TO DO en nuestra lista de desarrolladores. Hemos evolucionado y, a las buenas o a las malas, hemos aprendido que «mockear» nuestra base de datos en memoria no es lo más adecuado para los tests de integración. Gracias a Docker y a herramientas de control de versión para bases de datos como Flyway, hemos conseguido poder ejecutar los tests de integración de forma totalmente atutomática, sin tener levantanda previamente ninguna instancia preparada de base de datos.

La entrada Tests de integración automáticos Docker, Flyway y Maven se publicó primero en Adictos al trabajo.

Cómo automatizar el envío de correos desde Google Spreadsheets

$
0
0

A veces tenemos tareas que son un poco tediosas, o que tenemos que hacer cada cierto tiempo con bastante regularidad. Mecánicas, repetitivas, aburridas… lo cual nos hace más propensos a cometer errores. Me he encontrado en muchas ocasiones con un excel inmenso, lleno de gente a la que mandar una misma información con ligeras variaciones. Monto un correo, lo envío, marco en el excel que lo he enviado; siguiente fila: copio el correo anterior, cambio destinatarios, modifico un par de cosas, lo envío, marco como enviado en el excel; siguiente fila… (30 correos más tarde) Si te ha pasado como a mí, seguro que te viste pensando “¿no puedo hacer algo más valioso con mi tiempo?”. ¡Claro que sí! ¡Automatiza!


Tabla de contenidos

1. Tu hoja de cálculo
2. La primera prueba
3. Metiendo un poco de lógica
4. Poniéndolo bonito con HTML y templates
5. Tuneando un poco más
6. Un vistazo completo al código
7. Resumen y conclusiones


Lo dicho, hoy te voy a contar cómo automatizar el envío de correos desde Google Spreadsheets, la aplicación de hojas de cálculo gratuita de Google. Para eso tendrás que saber un poquito hojas de cálculo, HTML y Javascript, y aprender un par de cosas de Google Apps Script. Nada del otro mundo, ya verás. ¡Vamos allá!

1 Tu hoja de cálculo

Esta es la parte más fácil. Simplemente necesitas una hoja de cálculo en la que tengas el listado de destinatarios a los que quieras enviar los correos. Voy a enseñarte un ejemplo real para el cual lo he utilizado recientemente. El 25 de octubre estuve en la 4ª edición de la Tarugoconf con el equipo de Autentia. Montamos varios concursos a través de Kahoots, que tenían como premio para los ganadores un taller de Efectividad Personal. 

El lunes siguiente, me vi en la oficina con un largo listado de gente a la que contactar (los ganadores y los que se quedaron en lista de espera). Montada mi hoja de cálculo, quedó algo así:

Hoja de cálculo con la lista de candidatos

Ya ves, una fila por participante, en la que tenía el nick que habían usado en el concurso, nombre apellido, el identificador del kahoot en el que participaron y su puesto. Lo simplifico mostrando sólo 3 kahoots y 5 posiciones.

2  La primera prueba

Anteriormente ya había automatizado correos desde Excel y Outlook, pero no desde la suite de Google así que antes de nada hice una pequeña prueba de concepto. El objetivo era enviar un correo con texto plano a partir de los contenidos de la hoja de cálculo.

¿Por qué no la repetimos? Venga, créate una nueva hoja de cálculo de Google y empezamos a cacharrear… Una vez que la tengas, en el menú de herramientas lanzamos el “Editor de secuencias de comandos”.

Herramientas –> Editor de secuencia de comandos

En ese punto se nos abre un nuevo proyecto, al que le cambiaremos el título, con un archivo “Código.gs” que tiene el esqueleto de una función.

Nuevo proyecto de código
Nuevo proyecto de código de Google Apps script

Google Apps Script tiene un modelo de objetos y clases mediante el cual representa aplicaciones, como Google Spreadsheets o GMail. Cada aplicación, a su vez tiene una serie de métodos para acceder a otros objetos que representan sus componentes.

Vamos con el código de la primera prueba que así quedará más claro. Comenzamos creando la función enviarCorreos(), que va a ser la que orqueste toda la lógica del script.

/** @OnlyCurrentDoc */
function enviarCorreos() {
 const libro = SpreadsheetApp.getActiveSpreadsheet();
 libro.setActiveSheet(libro.getSheetByName("Candidatos"));
 const hoja = SpreadsheetApp.getActiveSheet();
 const filas = hoja.getRange("A2:E3").getValues();
  
 for (indiceFila in filas) {
   var candidato = crearCandidato(filas[indiceFila]);
   enviarCorreo(candidato);   
 }
}

En ella, obtenemos el “libro” de Google Spreadsheets activo (línea 3) y nos situamos en la “hoja Candidatos” (línea 4). Para acceder y manejar sus datos, utilizamos un rango (método getRange() de la hoja) y con getValues() tendremos una matriz (array bidimensional) con los datos (línea 6). A través de un bucle for, recorreremos cada una de las filas y enviaremos un correo (líneas 9-12). Para  hacerlo bonito, encapsulamos en un objeto “candidato” la información que tenemos en cada fila.

function crearCandidato(datosFila) {
  const candidato = {
    nick: datosFila[0],
    nombre: datosFila[1],
    email: datosFila[2],
    kahoot: datosFila[3],
    puesto: datosFila[4]
  };
  return candidato;
}

Finalmente, mandaremos el correo en la función enviarCorreo(), a la que le pasamos como parámetro el candidato.

function enviarCorreo(candidato) {   
  MailApp.sendEmail(candidato.email, "Taller de Efectividad Personal", "Eres un candidato. Responde si estás interesado.");
}

El correo se envía con el método sendMail() de la clase MailApp. Super fácil; tiene tres parámetros: email del destinatario, asunto y cuerpo (en texto plano).

Para lanzar el script, lo hacemos desde el menú de Ejecutar, invocando la función enviarCorreos.

ejecutar-enviar-correos
Cómo ejecutar la función enviarCorreos

Tras lo cual…

correo-simple
Correo recibido tras un envío automatizado desde Google Spreadsheets

¡Prueba de concepto superada! Enviamos un correo desde la hoja de cálculo. Vamos con la siguiente iteración…

3 Metiendo un poco de lógica

Como prueba, lo que hemos hecho nos vale, pero poco más. Ni está bonito, ni tiene en cuenta las distintas casuísticas. Por ejemplo, ¿y si nos falta la dirección de correo de alguien en la hoja de cálculo? Además, en nuestro concurso las plazas las ganaban los 3 primeros, quedando el resto en lista de espera. Con un par de if en el método enviarCorreo() lo arreglamos…

function enviarCorreo(candidato) { 
  if (candidato.email == "") return;
  var mensaje;
  if (candidato.puesto <= 3) {
    mensaje = "Tienes una plaza para el taller. Responde a este correo para confirmar tu asistencia.";
  } else {
    mensaje = "Estás en lista de espera para el taller. Responde a este correo si estás interesado en asistir.";
  }
  
  MailApp.sendEmail(candidato.email, "Taller de Efectividad Personal", mensaje);
}

Con el primero (línea 2), si está vacío el email del candidato salimos de la función sin llegar a enviar el correo. Con el segundo (líneas 4-8), variamos el mensaje a enviado según el puesto del candidato.

Podríamos afinar más, cosas como indicar el puesto en el que quedaron o el kahoot en el que participaron. Vamos con ello, pero al mismo tiempo que dejamos el correo más presentable.

4 Poniéndolo bonito con HTML y templates

Para formatear mejor el correo, tendremos que utilizar el método sendMails de otra manera, que nos permite pasarle el cuerpo del mensaje en HTML, añadir adjuntos, CC, CCO, etc. En vez de tres parámetros, recibe solamente uno del tipo message (echa un vistazo a la documentación de MailApp si quieres entrar en detalles). Con un poco de HTML y la nueva llamada, el correo resultante ya empieza a quedar mejor…

function enviarCorreo(candidato) { 
  if (candidato.email == "") return;
  var mensaje = "<p>Hola " + candidato.nombre + ",</p>";
  if (candidato.puesto <= 3) {
    mensaje += "<p><strong>Tienes una plaza para el taller</strong>. Responde a este correo para confirmar tu asistencia.</p>";
  } else {
    mensaje += "Estás en <strong>lista de espera</strong> para el taller. Responde a este correo si estás interesado en asistir.";
  }
  mensaje += "<p>Un saludo,<br/>Javier González</p>";
  
  MailApp.sendEmail({
    to: candidato.email,
    subject: "Taller de Efectividad personal con GTD y Personal Kanban",
    htmlBody: mensaje 
  });
}

Pero picar HTML directamente en nuestras variables no es lo más cómodo ni lo más recomendable. ¡Aquí entran en juego las plantillas! Primero creamos un nuevo archivo HTML y le ponemos un nombre que nos guste.

nuevo-archivo-html
Creando una plantilla html

La estructura de este archivo será la normal y corriente de un HTML de los de toda la vida. 

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
	<p>...te cuento de todo y lo dejo <strong>bonito</strong></p>
  </body>
</html>

No entraré en detalle, pero es importante que en el head tengamos el tag <base target="_top">.

¿Y el contenido con sus variaciones? Antes vamos a ver cómo utilizar la plantilla dentro del código Javascript. De nuevo, solo hay que tocar el método enviarCorreo().

function enviarCorreo(candidato) { 
  if (candidato.email == "") return;
  const plantilla = HtmlService.createTemplateFromFile('plantillaTallerGTDPK');
  plantilla.candidato = candidato;
  const mensaje = plantilla.evaluate().getContent();
  
  MailApp.sendEmail({
    to: candidato.email,
    subject: "Taller de Efectividad personal con GTD y Personal Kanban",
    htmlBody: mensaje 
  });
}

Creamos primero un objeto plantilla (línea 2), indicando el nombre del archivo HTML que creamos anteriormente (si en la extensión .html y estando en la misma ubicación que el archivo .gs). Después “le enchufamos” el objeto candidato para poder acceder a sus atributos desde la plantilla, en la línea 3. En tercer lugar, generamos el contenido HTML tras evaluar el objeto plantilla (línea 4).

Ahora completaremos los contenidos e incluiremos cierta lógica para tener un correo personalizado y bien formateado. Y como te puedes imaginar, en vez de picar el código HTML dentro de las variables de Javascript, usaremos nuestra plantilla HTML para generar el cuerpo del mensaje, que será mucho más cómodo. Dentro de ella tendremos pequeños scriptlets, que irán entre <? y ?> para sentencias y entre <?= y ?> para expresiones. Con ellos, podremos personalizar el contenido en base a los datos de nuestra hoja de cálculo, los cuales están disponibles con el DTO “candidato” que te mencioné antes. Veamos el código…

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <h2>¡Prepárete para disparar tu Efectividad Personal!</h2>
    <p>
    Hola <?= candidato.nombre ?> (<?= candidato.nick ?>),
    </p>
    <p>Gracias por haberte pasado por el stand de <strong>Autentia</strong> en la <strong>Tarugoconf2019</strong>.
    ¡No sólo eso!, participaste en uno de nuestros kahoots y no has quedado nada mal ;-)</p>
    <p>En tu caso en el puesto <?= candidato.puesto ?> del Kahoot#<?= candidato.kahoot ?>!!!</p>
    <? if (candidato.puesto <= 3) { ?>
      <p><strong>Has ganado una de las plazas</strong> disponibles 
    <? } else { ?>
      <p>Estás en <strong>lista de espera</strong> 
    <? } ?>
    para el taller de <strong>Efectividad personal con GTD y Personal Kanban</strong>. 
    En el taller veremos la importancia de gestionar adecuadamente nuestros recursos, crítica para los trabajadores del conocimiento. 
    No necesitamos  más trabajo duro, sino trabajo inteligente. Con <i>los 5 pasos de GTD</i> (capturar, procesar, organizar, revisar 
    y hacer) aprenderemos a ser más <u>efectivos</u> (hacer lo adecuado), <u>eficaces</u> (hacerlo correctamente) y <u>eficientes</u> 
    (minimizando nuestro esfuerzo). Suena bien, ¿no? Pues complementaremos el sistema con <i>Personal Kanban</i>, que nos ayudará a 
    ver todo más claro y a mantenernos enfocados.</p>
    <p>Será el próximo día <strong>14/11/2019 de 18:00 a 20:00</strong> en las instalaciones de <i>Autentia</i> 
    (<a href="https://goo.gl/maps/1aMhb3QLUVaNkCvY8">Edificio BestPoint, Avenida de Castilla, 1, Planta 2, Local 21B, 28830 
    San Fernando de Henares, Madrid</a>). 
    <? if (candidato.puesto <= 3) { ?>
      Pero antes necesito tu <strong>confirmación de asistencia</strong> así que...
    <? } else { ?>
      ¿Quieres que te <strong>avise</strong> si se libera alguna plaza? 
    <? } ?>
    </p>
    <p>Si estás interesado <strong>responde a este correo</strong> y te mantengo al tanto de todo.</p>
    
    <p>Un saludo,</p>
    <p>Javier González.</p>
  </body>
</html>

No es tan complejo, ¿verdad? Un poco de formateo con tags de HTML (h2, p, strong, i, u, …) y unos pocos scriptlets:

  • Para personalizaciones, como incluir el nombre (<?= candidato.nombre ?>)
  • Para meter lógica condicional, diferenciando los ganadores de plaza de los de la lista de espera (<? if (candidato.puesto <= 3) { ?>).

5 Tuneando un poco más

¿Quieres meter una firma chula en tu correo? Abre un correo tuyo desde GMail, selecciona tu firma, inspecciona y cópiate el HTML de la misma. Bastará con lo incluyas al final del body (en vez de <p>Javier González.</p> en la línea 37 del ejemplo).

¿Y meter un foto con el mapita de donde haremos el taller? Una línea de código para generarla dinámicamente y otra para adjuntarla al correo.

const mapa = Maps.newStaticMap().addMarker("Autentia");
   
MailApp.sendEmail({
  to: candidato.email,
  subject: "Taller de Efectividad personal con GTD y Personal Kanban",
  htmlBody: mensaje, 
  attachments:[mapa]    
});

¿Lanzar el script desde el menú? Claro, así lo podrán utilizar personas que les «dé cosa» eso de tocar código.

function onOpen() {
  const spreadsheet = SpreadsheetApp.getActive();
  const menuItems = [{name: 'Enviar', functionName: 'enviarCorreos'}];
  spreadsheet.addMenu('Enviar Correos', menuItems);
}

Sobreescribimos la función onOpen(), añadiendo la lógica para agregar un nuevo menú en el que asociamos la entrada «Enviar» con nuestro método enviarCorreos(). La primera vez que intentemos lanzar el script vía menú, Google nos pedirá permisos para poder ejecutar el código.

Con todo estoy el aspecto final de los correos queda así…

correo-generado-plantilla
Correo generado automáticamente a partir de una plantilla HTML

Y podríamos hacer muchas cosas más. Las posibilidades que tenemos son tan grandes como tu imaginación.

6 Un vistazo completo al código

Te dejo ahora el código final completo, por si lo quieres reutilizar o leer del tirón.

Primero el Javascript…

/** @OnlyCurrentDoc */
function enviarCorreos() {
  const libro = SpreadsheetApp.getActiveSpreadsheet();
  libro.setActiveSheet(libro.getSheetByName("Candidatos"));
  const hoja = SpreadsheetApp.getActiveSheet();
  const filas = hoja.getRange("A2:E16").getValues();
  const mapa = Maps.newStaticMap().addMarker("Autentia");
  
  for (indiceFila in filas) {
    var candidato = crearCandidato(filas[indiceFila]);
    enviarCorreo(candidato, mapa);
 }
}

function crearCandidato(datosFila) {
  const candidato = {
    nick: datosFila[0],
    nombre: datosFila[1],
    email: datosFila[2],
    kahoot: datosFila[3],
    puesto: datosFila[4]
  };
  return candidato;
}

function enviarCorreo(candidato, mapa) {
  if (candidato.email == "") return;
  const plantilla = HtmlService.createTemplateFromFile('plantillaTallerGTDPK');
  plantilla.candidato = candidato;
  const mensaje = plantilla.evaluate().getContent();
  
  MailApp.sendEmail({
    to: candidato.email,
    subject: "Taller de Efectividad personal con GTD y Personal Kanban",
    htmlBody: mensaje,
    attachments:[mapa]
  });
}

function onOpen() {
  const spreadsheet = SpreadsheetApp.getActive();
  const menuItems = [{name: 'Enviar', functionName: 'enviarCorreos'}];
  spreadsheet.addMenu('Enviar Correos', menuItems);
}

Y después la plantilla HTML…

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <h2>¡Prepárete para disparar tu Efectividad Personal!</h2>
    <p>
    Hola <?= candidato.nombre ?> (<?= candidato.nick ?>),
    </p>
    <p>Gracias por haberte pasado por el stand de <strong>Autentia</strong> en la <strong>Tarugoconf2019</strong>.
    ¡No sólo eso!, participaste en uno de nuestros kahoots y no has quedado nada mal ;-)</p>
    <p>En tu caso en el puesto <?= candidato.puesto ?> del Kahoot#<?= candidato.kahoot ?>!!!</p>
    <? if (candidato.puesto <= 3) { ?>
      <p><strong>Has ganado una de las plazas</strong> disponibles 
    <? } else { ?>
      <p>Estás en <strong>lista de espera</strong> 
    <? } ?>
    para el taller de <strong>Efectividad personal con GTD y Personal Kanban</strong>. 
    En el taller veremos la importancia de gestionar adecuadamente nuestros recursos, crítica para los trabajadores del conocimiento. 
    No necesitamos  más trabajo duro, sino trabajo inteligente. Con <i>los 5 pasos de GTD</i> (capturar, procesar, organizar, revisar 
    y hacer) aprenderemos a ser más <u>efectivos</u> (hacer lo adecuado), <u>eficaces</u> (hacerlo correctamente) y <u>eficientes</u> 
    (minimizando nuestro esfuerzo). Suena bien, ¿no? Pues complementaremos el sistema con <i>Personal Kanban</i>, que nos ayudará a 
    ver todo más claro y a mantenernos enfocados.</p>
    <p>Será el próximo día <strong>14/11/2019 de 18:00 a 20:00</strong> en las instalaciones de <i>Autentia</i> 
    (<a href="https://goo.gl/maps/1aMhb3QLUVaNkCvY8">Edificio BestPoint, Avenida de Castilla, 1, Planta 2, Local 21B, 28830 
    San Fernando de Henares, Madrid</a>). 
    <? if (candidato.puesto <= 3) { ?>
      Pero antes necesito tu <strong>confirmación de asistencia</strong> así que...
    <? } else { ?>
      ¿Quieres que te <strong>avise</strong> si se libera alguna plaza? 
    <? } ?>
    </p>
    <p>Si estás interesado <strong>responde a este correo</strong> y te mantengo al tanto de todo.</p>
    
    <p>Un saludo,</p>
    <p>Javier González</p>
</body>
</html>

Con unas 80 líneas de código en total hacemos un montón cosas. ¡No está mal!

7 Resumen y conclusiones

Hay veces que nos toca hacer tareas repetitivas y poco creativas. En vez de actuar como autómatas, una mejor alternativa bajo mi criterio es preguntarnos: “¿lo puedo automatizar?, ¿vale la pena es esfuerzo?”. En caso afirmativo, si utilizas la suite de aplicaciones de Google, Google Apps Script será tu mayor aliado. No hace falta ser un developer experto, con un poco de interés y ganas de aprender te bastará para muchos escenarios.

En el artículo de hoy te conté cómo automatizar el envío de correos desde Google Spreadsheets, mediante el uso de pequeños fragmentos de código de Google Apps Script. El modelo de objetos para acceder a los datos de una hoja de cálculo es bastante intuitivo. Si te manejas habitualmente con ellas y conoces conceptos como los rangos, no te costará nada pillarlo. Por otra parte, la integración básica con GMail es todavía más fácil. Incluso para crear correos a partir de plantillas HTML la cosa no se complica demasiado. Eso sí, Google nos limita el envío de correos automatizados a 100/día (GMail personal y G Suite free edition) ó 1.500/día (G Suite basic o superior).

Las herramientas están ahí para utilizarlas y hacer nuestro día a día más fácil, para apalancarnos en ellas. Creo que para nuestro trabajo, a día de hoy y todavía más en el futuro, hay dos palabras clave: sistematizar y automatizar. Y tú, ¿cómo lo ves?

Si te ha gustado el artículo no olvides compartirlo por redes sociales y/o dejar tus comentarios.

Be agile, my friend!

La entrada Cómo automatizar el envío de correos desde Google Spreadsheets se publicó primero en Adictos al trabajo.

Detección y reemplazo de outliers con R

$
0
0

Daremos un repaso rápido a algunos conceptos básicos de estadística para centrarnos en la detección y reemplazo de outliers con R. Por el camino aprenderemos a manejar fechas y horas, a convertir una variable numérica en categórica, los diagramas de caja y bigotes, y a crear nuestras propias funciones.

Índice de contenidos

1. Introducción

Continuando con la serie de tutoriales sobre R y RStudio, daremos un repaso a algunos conceptos básicos de estadística. Como ejemplo vehicular vamos a usar el proyecto Geria-TIC.

Se trata de un proyecto que mediante un wearable (una pulsera) registra una serie de parámetros de los ancianos como la actividad física, y los periodos de sueño y así poder establecer la relación con las caídas, problemas de insomnio e incontinencia urinaria.

El estudio sigue la filosofía de OpenData, en este caso promovida por en Red.es, y que se enmarca dentro de la Normativa RISP (Reutilización de la Información del Sector Público). En el siguiente enlace se puede consultar la información OpenData del proyecto Geria-TIC así como la documentación de sus ficheros y campos.

2. Leyendo el CSV de la actividad física del usuario

En un artículo anterior ya vimos cómo leer un archivo CSV. En este ejemplo nos vamos a centrar en el fichero que recoge la actividad física del usuario: user_activity.csv

Los campos que están en este fichero son:

  • userId: idenfiticador del usuario
  • registerStartDate: fecha de inicio del registro
  • registerFinalDate: fecha final del registro
  • steps: pasos caminados
  • distance: distancia recorrida en metros
  • runDistance: distancia recorrida en metros (corriendo)
  • calories: calorias gastadas
  • deepSleepTime: tiempo de sueño profundo (en minutos)
  • shallowSleepTime: tiempo de sueño superficial (en minutos)
  • wakeTime: tiempo para despertar (en minutos)
  • sleepStartDate: hora a la que comienza el sueño
  • sleepStopDate: hora a la que finaliza la fase de sueño

Como siempre, lo primero será definir nuestro directorio de trabajo:

#seteamos el directorio de trabajo
setwd("/lab/adictosaltrabajo/rstudio/04")

# Vamos a leer un CSV del proyecto GeriaTIC
user_activity <- read.csv("./user_activity.csv")

View(user_activity)

Vemos que nos ha recuperado las fechas como factores, pero eso no es lo que queremos.

3. Manejando fechas y horas en R

Lo primero que haremos será leer el CSV pero indicando que no queremos los strings como factores. Luego procedemos a la conversión de tipos a

Date
para fechas, y a
POSIXlt
para fecha-hora.
user_activity <- read.csv("./user_activity.csv", stringsAsFactors = F)

# para convertir string a fechas
user_activity$registerStartDate 
    <- as.Date(user_activity$registerStartDate, format="%Y/%m/%d")
user_activity$registerFinalDate 
    <- as.Date(user_activity$registerFinalDate, format="%Y/%m/%d")

# para convertir strings a fecha-hora
user_activity$sleepStartDate 
    <- strptime(user_activity$sleepStartDate, format="%Y/%m/%d %H:%M:%S", tz = "UTC")
user_activity$sleepStopDate 
    <- strptime(user_activity$sleepStopDate, format="%Y/%m/%d %H:%M:%S", tz = "UTC")

#nos quedamos con una version simplificada
activity <- user_activity[,c("userId","registerStartDate","steps","distance","calories")]

Fijémonos que con la función

as.Date()
cambiamos en tipo y con el parámetro
format
le indicamos el patrón del formato en que debe leer. La función
strptime()
funciona de forma similar, pero además tiene otro parámetro opcional
tz
que nos permite indicar el
TimeZone
.

Finalmente nos quedamos con un dataFrame que es una versión simplificada con sólo 5 columnas, pues nos vamos a centrar en la parte de la actividad física, y no tanto en los parámetros de sueño.

4. Un poquito de estadística

Es inevitable, y en algún momento teníamos que recordar ciertos conceptos elementales de estadística. Pero tranquilo, que empezaremos por los que estudiaste en el colegio:

  • Media: también conocida como media aritmética o promedio. En una muestra es la suma de todos los valores obtenidos dividido entre el número de repeticiones.
  • Mediana: si ordenamos todos los valores obtenidos, se trata del valor que deja a un lado la mitad de resultados y al otro la otra mitad.
  • Moda: es el valor que más se repite en el muestreo.
  • Cuartiles: Al igual que la mediana, los cuartiles dejan resultados a un lado y al otro. Pero dividiendo el muestre en cuatro partes. Así, el 25% de los resultados son menores que el primer cuartil (Q1), el 50% son menores que Q2 (es la mediana), el 75% menosres que el tercer cuartil (Q3)
  • Percentiles: Igual pero con tanto por cieto. El percentil 95 el valor en que el 95% de los resultados son menores que él

Depende lo que se estudie, la media es un valor representativo de la población estudiada. Pero hay casos en que no.

Por ejempplo, los sueldos. Imaginemos un equipo de 5 personas y el jefe, donde los sueldos son (20.000, 22.000, 30.000, 30.000, 50.000, 100.000).

La media sale: 42.000 sin embargo sólo un empleado y el jefe superan esa cifra.

En este caso es más representativa la mediana: si ordenamos de menos a mayor y nos quedamos con la cifra de en medio, la mediana es 30:000.

Cuando el sueldo del que más gana en una empresa puede ser varias veces el del que menos gana, la media puede distorsionar la información.

5. Convertir una variable numérica en categórica

Volviendo a nuestro dataFrame de actividad de los ancianos, vamos a fijarnos en el número de pasos que dan. Ya vimos cómo obtener un histograma con R que nos aporte información de en qué rangos se mueve esta variable con un simple vistazo.

# Histograma numero de pasos
hist(activity$steps, main="Frecuencia de número de pasos")

Vamos a definir una categorización, que a priori nos parece razonable:

  • hasta 2000 pasos es bajo
  • de 2000 – 4000 pasos sería medio
  • de 4000 – 6000 pasos estaría alto
  • mayor de 6000 es muy alto

# Convertir la variable numerica "pasos" en categorica
# para ello definimos los puntos de corte
breakPoints <- c(0, 2000, 4000, 6000, Inf)
categories <- c("Low", "Medium", "High", "Very High")

# y cortamos la variable número de pasos segun esta categorizacion
activity$steps.F <- cut(activity$steps, breaks = breakPoints, labels = categories)

En la imagen vemos que hay una nueva columna

steps.F
de tipo Factor, que puede tener 4 posibles valores. Pero si nos movemos un poco por el dataFrame vemos que se repite mucho el valor «Low». A lo mejor no hemos definido bien los puntos de corte. Vamos a obtener una serie de medidas estadísticas sobre la variable de pasos:
summary(activity$steps)

Y por consola nos salen los valores:

> summary(activity$steps)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
    0.0   353.0   878.5  1687.3  1947.5 33218.0

Vemos que el valor máximo es 33.218 pasos, mientras que la media son 1.687 pasos. Pero el 50% de las veces se han andado 878 pasos o menos. Esta mediana está tan distante de la media (casi el doble), que ya nos indica que pasa algo raro.

Tenemos unos pocos valores muy altos que nos están tergiversando el estudio.

6. Valores atípicos (outliers)

En el ejemplo que nos traemos entre manos, vemos que Q1 está en 353 pasos y que Q3 está en 1947 pasos. Entre Q1 y Q3 sabemos que están el 50% de los valores obtenidos en el estudio. A esta distancia se le llama rango intercuantílico (IQR: InterQuantile Range).

IQR = Q3 – Q1 = 1947.5 – 353.0 = 1594.5 pasos

Se define como valor atípico leve aquel que dista 1,5 veces el rango intercuantílico por debajo de Q1 o por encima de Q3

q < Q1 – 1,5 · IQR o bien q > Q3 + 1,5 · IQR

y valor atípico extremo aquel que dista 3 veces el rango intercantílico por debajo de Q1 o por encima de Q3

q < Q1 – 3 · IQR o bien q > Q2 + 3 · IQR

De hecho, si sospechamos que tenemos outliers por arriba, vamos a calcular cual sería el umbral:

Q1 + 1,5 · IQR    # 1947.5 + 1,5 · 1594.5 = 4339.25

Todos los valores del muestro que superen 4339.25 son outliers. De ahí para arriba, está muy por encima y se consideran valores atípicos.

¿Y por abajo? ¿Tendremos outliers por debajo?

Vamos a ver cual es el umbral inferior:

Q1 - 1,5 · IQR    # 353.0 - 1,5 · 1594.5 = -1241.5

El umbral inferior sería una cifra negativa, y el valor más bajo que tenemos es 0, el mínimo. Así que no tenemos ningún valor inferior al umbral. En este caso no hay valores atípicos por debajo.

Podemos quitar los outliers de una forma muy fácil

# quitamos los outliers superiores (inferiores no hay)
steps <- activity[activity$steps < 4339.25, c("steps")]

# y al hacer un summary de la variable vemos que los valores han cambiado
> summary(steps)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
    0.0   327.0   778.5  1126.2  1682.0  4337.0

Es normal que la media, mediana y cuartiles hayan cambiado, pues los outliers tenían mucho peso y distorsionaban los datos. Aún así, este muestreo que es un subconjunto del original, tiene pinta que no está bien balanceado y tendrá sus propios outliers. No hay más que ver la distancia que hay por encima del Q3.

7. Diagrama de caja y bigotes

Una forma fácil de detectar outliers es con un diagrama de caja y bigotes. Para ilustrar este tipo de gráficos nos vamos a fijar en los pasos que da un anciano en concreto (userID = 10039)

steps_10039 <- activity[activity$userId == 10039, c("steps")]

Nos fijamos que empieza recorriendo pocos pasos, pero que va en aument.

> summary(activity_10039$steps)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
      0    2057    2646    2624    3188    5842

Vamos a dibujar este diagrama de caja y bigotes.

boxplot(steps_10039)

En el diagrama vemos la caja, cuyo borde superior es Q3 y el inferior es Q1. Entre medias están el 50% de las ocurrencias. La altura de la caja es el rango intercuartílico. Y el bigote, la línea gruesa, es la mediana. Por encima y debajo se ven dos límites, que son los umbrales para los valores atípicos.

El superior es:
Q3 + 1,5 · IQR = 4884,5 pasos.
Y todo lo que esté por encima es un outlier.

Pero ¿y el límite inferior? Según lo que sabemos, debería ser: Q1 – 1,5 · IQR = 360,5 pasos. Sin embargo la función

boxplot()
nos pinta esa línea cercana a los 1.000 pasos. ¿Por qué? Muy sencillo. El día que menos andó fueron 1.045 pasos. De los 306 días registrados, 12 no andó nada (cero pasos). Así que el algoritmo que usa esta función considera esos días con cero pasos son valores atípicos, y dibuja la línea inferior en los 1.045 pasos y unos circulitos en el 0, para los outliers.

Podemos guardar los outliers en una variable a la vez que dibujamos el diagrama de caja y bigotes.

outliers_10039 <- boxplot(steps_10039)$out

> outliers_10039
 [1] 5344 4958 5017 5842 5139    0    0    0    0    0    0    0    0    0    0    0    0

Volviendo al resto de usuarios y ojeando el dataFrame, me doy cuenta, que casi todos los valores entre 20.000 y 30.000 pasos los da el usuario 10041. Este usuario me está distorsionando el estudio comparándolo con todos los demás ancianos. Es el andarín del grupo. Lo voy a sacar del dataFrame.

activity <- activity[activity$userId != 10041,]

Lo interesante sería poder dibujar este gráfico de caja y bigotes para cada usuario. Se puede hacer poniendo la variable «pasos» en función de la variable «usuario».

boxplot(activity$steps ~ activity$userId)

Cuando hacemos referencia a varias variables del mismo dataFrame es preferible indicarlo y nos ahorramos la sintasix del $.

# este boxplot sería equivalente al anterior
boxplot(steps ~ userId, 
        data = activity,
        main = "Pasos por usuario")

8. Reemplazo de Outliers con R

Ya hemos visto como aprender a detectar outliers usando la función boxplot(), pero a lo mejor queremos definir nosotros nuestros propios umbrales de qué valores se quedan fuera, y reemplazarlos por la media o la mediana.

Nos definimos nuestra propia función que recibe como parámetros el conjunto de datos, el umbral inferior y el umbral superior. Y en la función decidimos reemplazar por la media aquellos valores atípicos que estén por debajo del umbral inferior, y por la mediana aquellos que estén por encima del umbral superior.

# reemplazo de outliers con R
outliersReplace <- function(data, lowLimit, highLimit){
  data[data < lowLimit] <- mean(data)
  data[data > highLimit] <- median(data)
  data     #devolvemos el dato       
}

steps_10039_2 <- outliersReplace(steps_10039, 1045, 4884.5)

summary(steps_10039)
summary(steps_10039_2)

Y por consola veríamos:

> summary(steps_10039)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
      0    2057    2646    2624    3188    5842 
> summary(steps_10039_2)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
   1045    2117    2645    2684    3147    4704

Podemos pintar un gráfico al lado del otro

par(mfrow = c(1,2))

boxplot(steps_10039, main = "Sin reemplazo de outliers con R")
boxplot(steps_10039_2, main = "Con reemplazo de outliers con R")

Bueno, hemos definido nuestra primera función. Todo analista de datos tiene su buena colección de funciones a la que recurre habitualmente para tratar casuísticas habituales. R no deja de ser como cualquier otro lenguaje: cualquier tarea que se haga más de una vez es susceptible de ser programada.

Si volvemos al dataframe de activity, cada usuario tenía unos valores atípicos distintos. Nos interesará hacer una función que dado un dataFrame, particione los datos por un ID, y para cada ID encuentre los outliers de una de las columnas, reemplazando su valor por la mediana. Pero para no destruir los datos originales, podemos duplicar la columna con los datos a estudiar llamándola igual acabada en «.R» para indicar que tiene reemplazos.

Aunque tengamos en mente el dataFrame de activity y pensemos en los pasos por usuario, vamos a intentar hacer esta función genérica, que nos sirva para hacer el reemplazo de outliers con R sea cual sea el dataFrame, la columna por la que particionar los datos y la columna numérica a tratar.

# df es el dataFrame que recibimos (ej. activity)
# colNameData es la columna de los datos (ej. "steps")
# colNameBy es la columna por la que trocearemos (ej. "userId")
outliersReplace <- function(df, colNameData, colNameBy){
  # creamos una nueva columna llamada igual que colNameData pero con .R
  colNameData.R <- paste(colNameData, "R", sep=".")
  df[colNameData.R] <- df[colNameData]
  
  # obtenemos los IDs por los que partir el dataframe
  IDs <- unique(df[,c(colNameBy)])
  for (id in IDs){
    data <- df[df[colNameBy] == id, c(colNameData) ]
    
    Q  <- quantile(data)
    minimo <- Q[1]    # valor minimo
    Q1     <- Q[2]    # primer cuartil
    Me     <- Q[3]    # mediana
    Q3     <- Q[4]    # tercer cuartil
    maximo <- Q[5]    # valor maximo
    IQR    <- Q3 - Q1
    
    lowLimit  <- max(minimo, Q1 - 1.5*IQR)
    highLimit <- min(maximo, Q3 + 1.5*IQR)
    
    # todos los valores donde colNameBy es igual a id
    # y el valor de colNameData es > Q3 + 1.5 * IQR
    # lo reemplazamos por la mediana
    df[df[colNameBy] == id & df[colNameData] > highLimit, c(colNameData.R)] <- Me
    
    # lo mismo para el umbral inferior
    df[df[colNameBy] == id & df[colNameData] < lowLimit, c(colNameData.R)] <- Me
    
    cat(paste("El", colNameBy, id, "la mediana(", colNameData, ") ==", Me, "\n", sep=" " ))
    
  }
  df   # devolvemos el valor del dataFrame
}

Y si ahora llamamos a la función, obtenemos por consola:

> activity <- outliersReplace(activity,"steps","userId")
El userId 10036 la mediana( steps ) == 354 
El userId 10037 la mediana( steps ) == 4345 
El userId 10038 la mediana( steps ) == 0 
El userId 10039 la mediana( steps ) == 2646.5 
El userId 10043 la mediana( steps ) == 92 
El userId 10044 la mediana( steps ) == 1141.5 
El userId 10045 la mediana( steps ) == 450 
El userId 10047 la mediana( steps ) == 155.5 
El userId 10048 la mediana( steps ) == 1783 
El userId 10056 la mediana( steps ) == 13.5 
El userId 10057 la mediana( steps ) == 525 
El userId 10058 la mediana( steps ) == 1135 
El userId 10059 la mediana( steps ) == 633 
El userId 10060 la mediana( steps ) == 939 
El userId 10062 la mediana( steps ) == 1312

Y si vemos el dataFrame se observa la nueva columna que ha reemplazado los valores atípicos por la mediana de ese usuario.

reemplazo de outliers con R

Ahora podemos pintar los diagramas de caja y bigotes del número de pasos sin reemplazo y con reemplazo de outliers con R por la mediana de cada usuario.

par(mfrow = c(2,1))    # para ponerlos uno encima de otro

boxplot(steps   ~ userId, data = activity, main = "Sin reemplazo")
boxplot(steps.R ~ userId, data = activity, main = "Con reemplazo")

BoxPlot con y sin reemplazo de outliers con R

9. Conclusiones

Lo principal que hemos aprendido en este tutorial es a detectar y a hacer reemplazo de outliers con R, pero también:

  • hemos repasado algunos conceptos básicos de estadística
  • aprendimos a manejar fechas y horas con R y a convertir tipos
  • a convertir una variable numérica en factores (variables categóricas)
  • vimos un nuevo tipo de gráfico: diagrama de caja y bigotes
  • detectamos los valores atípicos o outliers
  • y aprendimos a hacer el reemplazo de outliers con R por la media o mediana
  • se vio cómo poner dos gráficos, uno al lado del otro, o uno encima de otro
  • y creamos nuestras propias funciones para trabajar en bloque sobre el dataFrame

Enlaces y Referencias

La entrada Detección y reemplazo de outliers con R se publicó primero en Adictos al trabajo.

Framer X, una potente herramienta de prototipado

$
0
0

Framer X es un software para realizar diseños y prototipar pero que además tiene la peculiaridad de poder vitaminar nuestros diseños con código basado en React.js.

Índice

Introducción

Llevo varias semanas indagando con otras herramientas de diseño para prototipar que sean diferentes a las que suelo usar para trabajar. Vengo de Sketch y hasta ahora era la herramienta por excelencia, pero desde los últimos años la competencia se ha puesto las pilas y no veo muy lejano el destrono de Sketch de la cúspide de herramientas para diseño y prototipado de interfaces.

Recientemente me he topado con Framer X, ¿qué es Framer X?.

Bien, Framer X es un software para realizar diseños y prototipar, pero que además tiene la peculiaridad de poder vitaminar nuestros diseños con código basado en React.js.

Framer X es la evolución mejorada de Framer Studio. Al principio Framer Studio nació para traer a los diseñadores la oportunidad de poder crear interacciones y animaciones avanzadas. Ahora Framer X va mucho más allá, no hace falta saber código, puedes prototipar sin necesidad de programar. Se ha añadido un editor gráfico más potente, nada que envidiar a las herramientas ya conocidas (Sketch, Adobe XD, etc), pudiendo crear ilustraciones en formato SVG y exportarlas.También dispone de una pestaña llamada «Components», lo que vienen a ser los «Símbolos» de Sketch, un conjunto de bloques creados para ahorrarnos trabajo.
Han añadido una tienda de paquetes en los que la comunidad de Frame sube componentes que pueden ser reutilizados ahorrándonos muchísimo trabajo.

Como podéis ver, en cuanto a herramienta de diseño es similar a sus competidores, pero lo que le hace distinto es la posibilidad de interactuar con el código y crear componentes muy avanzados gracias a React.js.

Interfaz

La interfaz de Frame X:

Frame

  1. Menú
  2. Selector de propiedades del Menú
  3. Canvas
  4. Panel de propiedades o Inspector

Dentro del Menú encontramos:

Tools

tools  Aquí encontramos todo lo relacionado con las herramientas  para crear la interfaz.

tools2

Podemos diferenciar entre Layout, Interactive, Drawing y Canvas.

  • Layout
  1. Frame: Son Artboards con tamaños preestablecidos ( Watch, Phone, Computer, TV), también puedes crear formas rectangulares y cuadradas.
  2. Round: Puedes crear círculos.
  3. Text: Puedes añadir Texto.
  4. Stack: Los stacks son pilas de elementos cuya distribución puedes alinear de manera sencilla, muy útil cuando tienes que cambiar de orden el diseño de tus cajas. Para que funcione los elementos tienen que ser hijos de un stack padre.

Stack GIF - Find & Share on GIPHY

Funcionamiento del stack

 

  • Interactive:

Las interacciones son justo eso mismo, interacciones que puedes realizar dentro de tu diseño sin necesidad de código. Dentro de ellas distinguimos entre, link scroll y page.

  • Drawing:

Nos proporciona herramientas como la pluma, rectángulo, círculo, polígono y estrella para realizar figuras y formas. Estas formas luego pueden ser exportados en SVG.

  • Canvas:

Accesos  visuales para la navegación por la interfaz. Si no hemos utilizado nunca una herramienta de prototipado, antes nos vendrá bien para saber manejar los comandos.

Layers

Aquí encontramos las capas de las formas y frames que estamos creando.

Components:

En la pestaña Components encontraremos los componentes de diseño creados por nosotros, muy útiles para ahorrarnos trabajo a nivel visual. También encontraremos componentes de código, que son componentes que obtenemos de la tienda de paquetes, creados con react.js, que poseen diferentes tipos de configuraciones que son modificables desde el panel de inspector.

 

Code

 

Aquí viene la parte interesante gracias a React.js hay un gran abanico en cuanto a posibilidades de animación.

Voy a aprovechar este post para explicaros cómo funciona un poco la parte de código.

En el Canvas, creamos una forma ( cuadrado, círculo, pentágono…). Yo he creado un pentágono.

Tools > Drawing > Pentagono

Como el pentágono entra dentro de la pestaña Drawing en el Menú de herramientas, necesitamos de un Frame para que podamos hacer de nexo con el código.

Botón derecho sobre la capa del pentágono y le damos a Add Frame.

Con el Frame que hemos creado seleccionado, nos vamos al Inspector y pinchamos sobre Override y en File le damos a New File. Se nos creará un archivo llamado App.tsx. Este archivo se encuentra en la pestaña Code.

 

Os dejo el código que he creado yo. Aquí cada uno puede investigar por su cuenta y cacharrear. (En este paso he cambiado el nombre de App por Framer)

import * as React from "react"
import { Override, Data, animate, motionValue } from "framer"

// Learn more: https://framer.com/docs/overrides/
const data = Data({
    PentagonoRotation: motionValue(0),
})

export const Pentagono: Override = () => {
    return {
        rotate: data.PentagonoRotation,
        onHoverStart() {
            animate(data.PentagonoRotation, 160)
        },
        onHoverEnd() {
            animate(data.PentagonoRotation, 0)
        },
    }
}

Vamos a Layers y tenemos que conectar el código con la parte visual, muy sencillo.

En file el nombre del archivo frame.tsx y en Override el nombre de la variable creada

Con todo ya enlazado pinchamos sobre el frame y le damos al botón de play que está situado arriba a la izquierda. Nos saldrá el reproductor y podremos interactuar.

 

Hemos creado un pentágono que cuando hacer HOVER se mueve sobre sí mismo.

Animated GIF - Find & Share on GIPHY

 

Como comento esto puede dar un sin fin de posibilidades.

Package:

Por último tenemos la pestaña de packages, donde podemos encontrar muchos componentes ya creados por otros usuarios de Frame  para agilizar nuestro trabajo.

Precio

Algo más caro que Sketch, y también ofrece otras posibilidades, dependiendo para qué proyecto o qué papel desarrollas en tu empresa te interesara en mayor o menor medida esta herramienta.

Recordad que disponemos de la versión de 14 días gratuita para probar la herramienta.

 

Enlaces y Referencias

 

Conclusiones:

A falta de poder testarlo más, ya que he dispuesto de la versión trial de 14 días que Framer X ofrece gratis, pienso que Frame X ha venido para quedarse, es una herramienta muy potente que puede acortar la fisura que existe entre los desarrolladores y diseñadores.

La entrada Framer X, una potente herramienta de prototipado se publicó primero en Adictos al trabajo.

Viewing all 990 articles
Browse latest View live