Weblogs Código

Blog Bitix

Comando para convertir imágenes a WebP con menor tamaño desde JPEG y PNG

diciembre 08, 2019 01:00

Las velocidades de conexión actuales de 100 Mbps y de 600 Mbps en los hogares permiten no darle tanta importancia al tamaño de una imagen y aunque los dispositivos móviles también tienen unas velocidades de conexión rápidas reducir el tamaño de las imágenes puede significar que una página web cargue algo más rápido. El ahorro está en un 30% y 60% lo que en colecciones grandes de imágenes el ahorro es considerable. WebP proporciona un ahorro de tamaño en las imágenes con una calidad similar que jpg y png.

GNU
Linux

Para que las imágenes ocupen menos espacio se utilizan formatos como jpg para fotos y png utilizado en imágenes como capturas de pantalla de ordenador. En las imágenes fotográficas los píxeles adyacentes suelen tener el mismo color y formatos como jpg se basan en esto para dividir la imagen en pequeñas baldosas en las que pueda almacenar la información de menos píxeles, jpg es un formato con pérdida de calidad que sin ser excesiva es aceptable y se ve compensada con un ahorro importante en el tamaño de la imagen. El formato png es un formato sin pérdida de calidad pero con igualmente con reducido tamaño en imágenes de captura de pantalla de ordenador.

Para comprimir aún más el tamaño de las imágenes con la misma calidad apreciable Google ha desarrollado el formato de imágenes WebP. Webp soporta imágenes con pérdida de calidad para ser una alternativa a jpg, sin pérdida de calidad y imágenes con animaciones como alternativa a imágenes png con animación. La reducción en tamaño de WebP sobre jpg y png está en un 30% o 60% dependiendo de la imagen un ahorro de tamaño significativo que es útil para que por ejemplo las páginas web tengan un menor tamaño de descarga y con ello se carguen más rápido si tiene numerosas fotos e imágenes.

Los navegadores modernos como Google Chrome, Firefox, Microsoft Edge y versiones para Android ya soportan WebP en los formatos con pérdida, sin pérdida y con animaciones.

Una imagen que usé para crear una imagen reducida o vista previa tiene un tamaño de 1600 píxeles de ancho y 1067 de alto ocupando 1018 KiB, la imagen en formato png tiene un tamaño de 1426 píxeles por 947 ocupando 78 KiB.

Imágenes en formato jpg y PNG

Las mismas imágenes comprimidas con WebP ocupan 826 KiB, un 18% menos, para la original en formato jpg y 26 KiB, un 66% menos, para la original en formato png. Las reducciones de tamaño son importantes y teniendo en cuenta que es sin pérdida de calidad apreciable sobre las originales utilizar WebP como formato de imagen permite ahorrar un tamaño importante de descarga en una página web o en espacio de almacenamiento en colecciones grandes de fotografías e imágenes.

Mismas imágenes en formato WebP

ImageMagick permite convertir las imágenes entre estos formatos. Con este comando se convierten todas las imágenes jpg y png de un directorio formato WebP.

1
2
$ for f in *.png; do convert -define webp:lossless=true "$f" "${f%.*}.webp"; done;
$ for f in *.jpg; do convert -define webp:lossless=false "$f" "${f%.*}.webp"; done;

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

Picando Código

Piques Emacs: Contar líneas, palabras y caracteres de una región

diciembre 05, 2019 01:00

En Emacs al seleccionar una región, Alt = nos muestra información de las líneas, palabras y caracteres de la selección:

 

El atajo de teclado llama a la función count-words-region. Ésta función sólo cuenta los caracteres dentro de la región. Así que si no hay una región seleccionada, nos va a dar 0 como resultado. Otra función que podemos usar para contar el buffer entero es count-words, que funciona tanto para una región como para un buffer sin regiones. Como resulta más práctica, terminé reemplazando count-words-region con count-words en mi archivo de keybindings:

(global-set-key (kbd "M-=")'count-words)

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

Blog Bitix

Nube privada para documentos personales con Nextcloud y OnlyOffice

diciembre 05, 2019 11:00

Empresas como Google ofrecen servicios gratuitos que los usuarios pueden utilizar, algunos de ellos a cambio de entregrarles documentos con información personal con la consiguiente potencial pérdida de privacidad. Algunos de estos servicios son sustituibles con alternativas como Nextcloud y OnlyOffice que permiten a sus usuarios ser dueños de su información ya sea utilizando una Raspberry Pi como servidor que debe ser administrada o incluso en la nube con servicios como DigitalOcean o AWS.

Nextcloud
OnlyOffice

Una cuenta de Google da acceso a múltiples servicios gratuitos, útiles y con un servicio más que correcto. Algunas de estos servicios está el de correo electrónico de GMail, la sincronización y unidad de documentos de Google Drive, calendario para apuntar citas y recordatorios con Google Calendar, el acceso a la suite ofimática colaborativa Google Docs o fotos con Google Photos. Estos son los servicios que uso de Google por su comodidad.

Los servicios en la nube permiten guardar los documentos e información fuera del dispositivo donde se usen, esto permite tener sincronizados y compartir todos los documentos entre varios dispositivos como el ordenador personal en casa, tener disponibles los documentos en el ordenador del trabajo y en un dispositivo móvil como un smartphone en cualquier lugar. El problema de los servicios en la nube es que no somos realmente propietarios de nuestra información y documentos, son entregados a esos servicios como los de Google, esto genera una pérdida de privacidad sin ser conscientes de los usos que le pueda dar Google. Para proteger nuestra privacidad hay alternativas para disponer de nuestra propia nube que proporcione la mayoría de estos servicios.

Nextcloud es un software que permite alojar en nuestro propio servidor nuestros documentos sustituyendo a varios de los servicios de Google. Nextcloud permite almacenar archivos y documentos ofimáticos, fotos, música, calendarios, un visor de PDF, editor de markdown, gestor de tareas o nuestros contactos. Permite complementos con los que añadir las funcionalidad que necesitemos como un reproductor de música o un paquete ofimático con OnlyOffice que sustituya a Google Docs.

Una Raspberry Pi 4 o una de sus 7 computadoras alternativas similares es una buena opción como servidor por su pequeño tamaño, bajo consumo, totalmente silencioso con un recomendable disipador pasivo y suficiente para ejecutar con normalidad Nexcloud con los 4 GiB de memoria del modelo con más capacidad. Hay otras placas similares o incluso se puede utilizar un Intel NUC que permiten más cantidad de memoria y sus procesadores son más capaces. Para una nube privada una Raspberry Pi es interesante por su pequeño tamaño y bajo consumo eléctrico dado que su funcionamiento sería constante.

Usando Docker y el repositorio de GitHub es sencillo iniciar el servidor de Nextcloud realizando los siguientes pasos.

  • Instalar Docker.
  • Descargar o clonar el repositorio de GitHub.
  • Iniciar con Docker Compose los contenedores de Nextcloud y OnlyOffice.
  • Acceder http://localhost y realizar la configuración incial, introducir el usuario y contraseña de administrador.
  • Ejecutar _bash setconfiguration.sh.
  • Añadir el complemento de OnlyOffice.
  • Acceder a http://localhost.

Este archivo de Docker Compose incluye Nextcloud con OnlyOffice sin usar una base de datos externa.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
version: '3'
services:
 app:
 container_name: app-server
 image: nextcloud:fpm
 stdin_open: true
 tty: true
 restart: always
 expose:
 - '80'
 - '9000'
 volumes:
 - app_data:/var/www/html
 onlyoffice-document-server:
 container_name: onlyoffice-document-server
 image: onlyoffice/documentserver:latest
 stdin_open: true
 tty: true
 restart: always
 expose:
 - '80'
 - '443'
 volumes:
 - document_data:/var/www/onlyoffice/Data
 - document_log:/var/log/onlyoffice
 nginx:
 container_name: nginx-server
 image: nginx
 stdin_open: true
 tty: true
 restart: always
 ports:
 - 80:80
 - 443:443
 volumes:
 - ./nginx.conf:/etc/nginx/nginx.conf
 - app_data:/var/www/html
volumes:
 document_data:
 document_log:
 app_data:
 mysql_data:
1
$ docker-compose up
Configuración y archivos en Nextcloud

OnlyOffice es un paquete ofimático alternativa a Microsoft Office que ofrece un editor de documentos de texto, una hoja de cálculo y una aplicación para realizar presentaciones integrables en Nextcloud. Son aplicaciones con menos opciones que las ofrecidas por Microsoft Office pero suficientes para un uso sencillo, también dispone de una versión como aplicaciones de escritorio.

Ofimática con OnlyOffice y Nextcloud alternativa a Google Docs

Otras utilidades es un reproductor de música, calendario o galería de fotos, hay un complemento para añadir estas funcionalidades.

Aplicaciones y complementos

Con WebDAV los documentos son accesibles como si fuese una unidad local proporcionando la misma funcionalidad de Google Drive. En GNOME con el explorador de archivos Nautilus es posible conectarse a dispositivo WebDAV, en el caso de Nextcloud la dirección es dav://localhost/remote.php/dav/files/admin.

Archivos en el explorador de archivos Nautilus con WebDAV y opciones de administración

Con las aplicaciones para smatphone los documentos quedan accesibles en cualquier lugar teniendo un dispositivo móvil, smartphone o tableta. Tener una nube propia que esté accesible en internet hace necesario tener un dominio propio, añadir seguridad para lo que es necesario configurar Nextcloud de modo que utilice el protocolo seguro que cifre las comunicaciones con TLS, esto requiere obtener un certificado autofirmado al menos o mejor obteniendolo de Let’s Encrypt, que proporciona certificados de forma automatizada y gratuita. Otra medida para aumentar la seguridad es utilizar un segundo factor de autenticación o 2FA.

Hay ejemplo de archivo de Docker Compose para tener Nexcloud con un certificado creado y renovado de forma automática con Let’s Encrypt a través del contenedor jrcs/letsencrypt-nginx-proxy-companion y configurando las variables de entorno LETSENCRYPT_HOST, LETSENCRYPT_EMAIL con el dominio propio para Nexcloud y un correo electrónico.

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

Blog Bitix

Nube privada de documentos personales con Nextcloud y OnlyOffice

diciembre 05, 2019 11:00

Empresas como Google ofrecen servicios gratuitos que los usuarios pueden utilizar, algunos de ellos a cambio de entregrarles documentos con información personal con la consiguiente potencial pérdida de privacidad. Algunos de estos servicios son sustituibles con alternativas como Nextcloud y OnlyOffice que permiten a sus usuarios ser dueños de su información ya sea utilizando una Raspberry Pi como servidor que debe ser administrada o incluso en la nube con servicios como DigitalOcean o AWS.

Nextcloud
OnlyOffice

Una cuenta de Google da acceso a múltiples servicios gratuitos, útiles y con un servicio más que correcto. Algunas de estos servicios está el de correo electrónico de GMail, la sincronización y unidad de documentos de Google Drive, calendario para apuntar citas y recordatorios con Google Calendar, el acceso a la suite ofimática colaborativa Google Docs o fotos con Google Photos. Estos son los servicios que uso de Google por su comodidad.

Los servicios en la nube permiten guardar los documentos e información fuera del dispositivo donde se usen, esto permite tener sincronizados y compartir todos los documentos entre varios dispositivos como el ordenador personal en casa, tener disponibles los documentos en el ordenador del trabajo y en un dispositivo móvil como un smartphone en cualquier lugar. El problema de los servicios en la nube es que no somos realmente propietarios de nuestra información y documentos, son entregados a esos servicios como los de Google, esto genera una pérdida de privacidad sin ser conscientes de los usos que le pueda dar Google. Para proteger nuestra privacidad hay alternativas para disponer de nuestra propia nube que proporcione la mayoría de estos servicios.

Nextcloud es un software que permite alojar en nuestro propio servidor nuestros documentos sustituyendo a varios de los servicios de Google. Nextcloud permite almacenar archivos y documentos ofimáticos, fotos, música, calendarios, un visor de PDF, editor de markdown, gestor de tareas o nuestros contactos. Permite complementos con los que añadir las funcionalidad que necesitemos como un reproductor de música o un paquete ofimático con OnlyOffice que sustituya a Google Docs.

Una Raspberry Pi 4 o una de sus 7 computadoras alternativas similares es una buena opción como servidor por su pequeño tamaño, bajo consumo, totalmente silencioso con un recomendable disipador pasivo y suficiente para ejecutar con normalidad Nexcloud con los 4 GiB de memoria del modelo con más capacidad. Hay otras placas similares o incluso se puede utilizar un Intel NUC que permiten más cantidad de memoria y sus procesadores son más capaces. Para una nube privada una Raspberry Pi es interesante por su pequeño tamaño y bajo consumo eléctrico dado que su funcionamiento sería constante.

Usando Docker y el repositorio de GitHub es sencillo iniciar el servidor de Nextcloud realizando los siguientes pasos.

  • Instalar Docker.
  • Descargar o clonar el repositorio de GitHub.
  • Iniciar con Docker Compose los contenedores de Nextcloud y OnlyOffice.
  • Acceder http://localhost y realizar la configuración incial, introducir el usuario y contraseña de administrador.
  • Ejecutar _bash setconfiguration.sh.
  • Añadir el complemento de OnlyOffice.
  • Acceder a http://localhost.

Este archivo de Docker Compose incluye Nextcloud con OnlyOffice sin usar una base de datos externa.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
version: '3'
services:
 app:
 container_name: app-server
 image: nextcloud:fpm
 stdin_open: true
 tty: true
 restart: always
 expose:
 - '80'
 - '9000'
 volumes:
 - app_data:/var/www/html
 onlyoffice-document-server:
 container_name: onlyoffice-document-server
 image: onlyoffice/documentserver:latest
 stdin_open: true
 tty: true
 restart: always
 expose:
 - '80'
 - '443'
 volumes:
 - document_data:/var/www/onlyoffice/Data
 - document_log:/var/log/onlyoffice
 nginx:
 container_name: nginx-server
 image: nginx
 stdin_open: true
 tty: true
 restart: always
 ports:
 - 80:80
 - 443:443
 volumes:
 - ./nginx.conf:/etc/nginx/nginx.conf
 - app_data:/var/www/html
volumes:
 document_data:
 document_log:
 app_data:
 mysql_data:
1
$ docker-compose up
Configuración y archivos en Nextcloud

OnlyOffice es un paquete ofimático alternativa a Microsoft Office que ofrece un editor de documentos de texto, una hoja de cálculo y una aplicación para realizar presentaciones integrables en Nextcloud. Son aplicaciones con menos opciones que las ofrecidas por Microsoft Office pero suficientes para un uso sencillo, también dispone de una versión como aplicaciones de escritorio.

Ofimática con OnlyOffice y Nextcloud alternativa a Google Docs

Otras utilidades es un reproductor de música, calendario o galería de fotos, hay un complemento para añadir estas funcionalidades.

Aplicaciones y complementos

Con WebDAV los documentos son accesibles como si fuese una unidad local proporcionando la misma funcionalidad de Google Drive. En GNOME con el explorador de archivos Nautilus es posible conectarse a dispositivo WebDAV, en el caso de Nextcloud la dirección es dav://localhost/remote.php/dav/files/admin.

Archivos en el explorador de archivos Nautilus con WebDAV y opciones de administración

Con las aplicaciones para smatphone los documentos quedan accesibles en cualquier lugar teniendo un dispositivo móvil, smartphone o tableta. Tener una nube propia que esté accesible en internet hace necesario tener un dominio propio, añadir seguridad para lo que es necesario configurar Nextcloud de modo que utilice el protocolo seguro que cifre las comunicaciones con TLS, esto requiere obtener un certificado autofirmado al menos o mejor obteniendolo de Let’s Encrypt, que proporciona certificados de forma automatizada y gratuita. Otra medida para aumentar la seguridad es utilizar un segundo factor de autenticación o 2FA.

Hay ejemplo de archivo de Docker Compose para tener Nexcloud con un certificado creado y renovado de forma automática con Let’s Encrypt a través del contenedor jrcs/letsencrypt-nginx-proxy-companion y configurando las variables de entorno LETSENCRYPT_HOST, LETSENCRYPT_EMAIL con el dominio propio para Nexcloud y un correo electrónico.

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

Coding Potions

Cómo crear una web de tareas usando Vue (todo app)

diciembre 05, 2019 12:00

Introducción

Hoy vengo con un caso práctico para afianzar los conocimientos que hemos ido viendo a lo largo de estos artículos.

Vamos a crear la típica página web para crear tareas que se suele hacer cuando empiezas con un framework. Siempre recomiendo ir creando ejemplos de este tipo según aprendes cualquier lenguaje porque muchas cosas no las entiendes del todo hasta que no las pones en práctica.

🗺️ Hoja de ruta

  • [ ] 🏗️ Crear el proyecto de Vue
  • [ ] ✨ Crear el formulario para crear tareas
  • [ ] ✨ Mostrar las tareas
  • [ ] ✨ Hacer que al pulsar las tareas se marquen como completadas
  • [ ] 💄 Crear estilos para las tareas

Creando el proyecto de Vue

Como vimos anteriormente, la manera más sencilla de crear un proyecto de Vue es utilizando su asistente de línea de comandos llamado Vue CLI. Si todavía no lo tienes instalado en tu equipo ejecuta:

npm install -g @vue/cli

Ahora ejecuta el comando para crear el proyecto con el nombre que quieras, en mi caso “tareas”.

vue create tareas

Te preguntará si quieres crear el proyecto directamente o si quieres elegir las opciones manualmente. Yo voy a crear el proyecto mediante el método default pero tú puedes seleccionar cada cosa por separado si lo prefieres, te dejo este artículo donde explico cada opción:

Tutorial Vue CLI

Listo, proyecto creado y primera tarea completada.

[X] 🏗️ Crear el proyecto de Vue

El comando habrá creado una carpeta en el directorio en el que te encontraras en la terminal al realizar ejecutar el comando. Para acceder simplemente ejecuta cd tareas

Te recomiendo que cada vez que termines una tarea, hagas commit con los cambios para tenerlo guardado por si quieres verlos o volver a un punto anterior.

Ahora ejecuta npm run serve dentro del proyecto para comprobar que el proyecto se ha creado correctamente.

Creando el formulario con Vue

Vamos con la siguiente tarea, la de crear el formulario y para ello vamos a seguir un ciclo iterativo, es decir, poco a poco añadimos funcionalidad y por el momento no nos preocupamos por cómo se vea o si es feo o bonito.

De lo que se trata es de que funcione y más adelante mejoraremos el apartado visual.

Lo primero que voy a hacer es borrar completamente el componente HelloWorld.vue que viene por defecto al crear el proyecto.

Dentro de la carpeta /src/components voy a crear un archivo llamado TodoList.vue con la estructura básica de un componente:

<template>
</template>

<script>
export default {

}
</script>

<style>
</style>

Para que todo sea más sencillo en este componente irá toda la lógica de la aplicación.

En posteriores capítulos veremos cómo separar funcionalidad y cómo pasar información de un componente a otro pero por el momento vamos a hacer las cosas sencillas para aprender.

Ante de seguir tenemos que sustituir dentro del componente App.vue el componente de HelloWorld por el que acabamos de crear. Quedaría así:

<template>
  <div id="app">
    <todo-list />
  </div>
</template>

<script>
import TodoList from './components/TodoList.vue'

export default {
  name: 'app',
  components: {
    TodoList
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Recuerda que vue automáticamente hace el cambio de PascalCase a kebab-case en la vista. Es decir al poner el nombre TodoList, para usarlo en el template de otro componente tendrás que usar todo-list.

Hora de crear el formulario. El formulario va a constar de un elemento <form> y dentro un input para escribir la tarea y un botón submit.

Recogeremos el valor del input y lo guardaremos en una variable. Cuando el usuario haga clic sobre el input almacenaremos la tarea en el array de tareas.

Dentro del archivo TodoList que hemos creado antes por el momento he puesto esto:

<template>
  <form id="app">
    <label class="label" for="task">Nueva tarea: </label>
    <input type="text" v-model="newTask" id="task">
      
    <input type="submit" value="Crear tarea">
  </form>
</template>

<script>
export default {
  data: () => ({
    newTask: ""
  })
};
</script>

<style>
</style>

Dentro del data he creado la variable newTask en la que se almacenará el valor que meta el usuario en el input para crear tareas.

Encima del botón de submit he puesto la variable de forma temporal para que veas que se actualiza en tiempo real su valor al escribir sin tener que hacer nada.

Lo que queda para terminar esta tarea es poder almacenar la tarea que escriba el usuario.

Para ello vamos a crear en el data una variable para almacenar todas las tareas (por defecto se crea vacío) y un método conectado el submit para que al darle al botón se guarde en el array. Vamos a ello:

<template>
  <form id="app" @submit.prevent="createTask">
    <label class="label" for="task">Nueva tarea: </label>
    <input type="text" v-model="newTask" id="task">
    <input type="submit" value="Crear tarea">
  </form>
</template>

<script>
export default {
  data: () => ({
    newTask: "",
    tasks: []
  }),
  methods: {
    createTask() {
      let task = {
        text: this.newTask,
        completed: false
      };
      this.tasks.push(task);
      this.newTask = "";
      console.log(this.tasks);
    }
  }
};
</script>

<style>
</style>

¿Recuerdas que cuando vimos los métodos hablamos de un evento @click que sirve para capturar eventos de clic en elementos del html? Si no lo recuerdas echa un vistazo a este artículo sobre ḿetodos y computadas en Vue

Bueno pues hay otro evento especial para los formularios para capturar cuando el usuario hace submit en un formulario.

Como en todos los eventos, para recoger el valor se hace con el @ delante y llamando al método que queremos que se ejecute.

Este evento tiene una particularidad y es que puedes añadir el .prevent detrás para evitar que la página se recargue cuando hacemos submit.

Dentro del método que llamamos de createTask lo que hago es crear una variable local llamada task en la que defino la estructura que tendrán todas las tasks.

Cada tarea será un objeto con dos parámetros, “texto” que será un string con la descripción de la tarea (lo que el usuario mete en el input) y “completed”, un booleano para saber si está completada (por defecto a false).

Como las propiedades del data son reactivas lo único que tenemos que hacer es meter dentro del parámetro text de la task el valor de la variable newTask que tendrá el valor introducido por el usuario.

Luego lo que se hace es meter la tarea dentro del array de tareas definido en la sección data y limpiar el valor de newTask para que cada vez que se cree una el input se limpie.

Pues listo, otra tarea completada, hora de hacer commit y pasar a la siguiente.

[X] ✨ Crear el formulario para crear tareas

Mostrar las tareas creadas

Mostrar las tareas no debería que ser muy complicado, solo tenemos que mostrar el array con la tareas (usando un bucle v-for en la vista) y definir una clase dependiendo de si la tarea se ha completado o no. Vamos con ello.

Para pintar las tareas lo hacemos con un v-for:

<template>
  <form id="app" @submit.prevent="createTask">
    <label class="label" for="task">Nueva tarea:</label>
    <input type="text" v-model="newTask" id="task" />
    <input type="submit" value="Crear tarea" />
    <ul>
      <li v-for="(task, i) in tasks" :key="'task' + i"></li>
    </ul>
  </form>
</template>

Recuerda añadir el parámetro único key para que vue pueda identificar cada uno de los elementos del bucle.

Por último dentro del bucle accedemos al texto de la tarea accediendo a un propiedad text (o el que hayas definido al crear tareas).

Vamos ahora con los estilos para que el usuario sepa si la tarea está completada o no.

Si recuerdas capítulos anteriores, puedes definir clases de en los elementos HTML de forma dinámica dependiendo de si una variable está a true o false.

<template>
  <form id="app" @submit.prevent="createTask">
    <label class="label" for="task">Nueva tarea:</label>
    <input type="text" v-model="newTask" id="task" />
    <input type="submit" value="Crear tarea" />
  </form>
    <ul>
      <li v-for="(task, i) in tasks" :key="'task' + i" :class="{completed: task.completed}"></li>
    </ul>
</template>

<script>
export default {
  data: () => ({
    newTask: "",
    tasks: []
  }),
  methods: {
    createTask() {
      let task = {
        text: this.newTask,
        completed: true
      };
      this.tasks.push(task);
      this.newTask = "";
      console.log(this.tasks);
    }
  }
};
</script>
<style scoped>
.completed {
  text-decoration: line-through;
  color: grey;
}
</style>

La clase completed se añadirá dependiendo de si el objeto task que está pintando el bucle tiene la propiedad completed a true o false.

Para que veas que funciona he puesto en el método de crear tareas que por defecto las ponga con el parámetro completed a true para que todas las tareas se creen ya completadas.

Por último he añadido estilos a la clase completed. En mi caso he hecho que se pinte el texto tachado y las letras de color gris más claro para que se diferencie más, pero tu puedes poner los estilos que quieras.

Tarea completada.

[X] ✨ Mostrar las tareas

Completando tareas

Toca hacer que las tareas se completen. En mi caso he decidido que se completen al pulsar sobre ellas.

Para este ejemplo lo voy hacer con el evento de click que ya conocemos pero lo correcto y accesible sería crear un botón al lado de cada tarea.

<template>
  <form id="app" @submit.prevent="createTask">
    <label class="label" for="task">Nueva tarea:</label>
    <input type="text" v-model="newTask" id="task" />
    <input type="submit" value="Crear tarea" />
  </form>
  <ul>
    <li
      v-for="(task, i) in tasks"
      :key="'task' + i"
      :class="{completed: task.completed}"
      @click="completeTask(task.text)"
    ></li>
  </ul>
</template>

<script>
export default {
  data: () => ({
    newTask: "",
    tasks: []
  }),
  methods: {
    createTask() {
      let task = {
        text: this.newTask,
        completed: true
      };
      this.tasks.push(task);
      this.newTask = "";
      console.log(this.tasks);
    },
    completeTask(taskText) {
      for (let i = 0; i < this.tasks.length; i++) {
        let task = this.tasks[i];
        if (taskText === task.text) {
          task.completed = !task.completed;
        }
      }
    }
  }
};
</script>
<style scoped>
.completed {
  text-decoration: line-through;
  color: grey;
}
</style>

Veamos lo que he cambiado.

En primer lugar, he llamado al evento @click dentro de cada tarea en la vista. El evento de click llama a la función completeTask pasando el texto de la tarea (lo mejor hubiera sido asignar a cada tarea un id y pasar a este función el id de la tarea a completar).

Ya en la función, simplemente lo que hago es recorrer todas las tareas en busca de la tarea que ha pulsado el usuario. Si se encuentra la tarea hace que su parámetro completed se invierta, de tal forma que si no se ha completado se complete y viceversa.

DISCLAIMER. Sí, se que esta solución no es ni de lejos la más óptima y correcta, pero he optado por esto para que sea sencillo de entender. Por ejemplo no hace falta recorrer toda la lista para encontrar una tarea, Puedes añadir un break cuando la encuentre para que el bucle no continúe o puedes usar la función find de javascript.

Por cierto, IMPORTANTE, para actualizar los valores de un array de objetos como en este caso, Vue recomiendo utilizar su método (importando Vue en el componente):

Vue.$set(array, indexOfItem, newValue)

array” es el array a editar, “indexOfItem”“ el índice del array a editar y newValue el nuevo valor.

Esto es así porque Vue tiene una limitación con los arrays al actualizar valores de forma reactivas. Esto se cambiará en futuras versiones.

Si quieres más información de esto o no te queda claro consulta la documentación de Vue:

Documentación vue sobre arrays

Otra tarea más completada, falta una.

[X] ✨ Hacer que al pulsar las tareas se marquen como completadas

Estilos

Ya solo queda añadir los estilos CSS que quieras, a tu gusto.

Aquí no hay mucho que explicar, yo he optado por añadir estos. Te dejo como ha quedado todo el componente:

<template>
  <div class="task-list">
    <h1> Tasks</h1>
    <form class="form" @submit.prevent="createTask">
      <label class="label" for="task">Nueva tarea:</label>
      <input class="input" type="text" v-model="newTask" id="task" />
      <input class="button" type="submit" value="Crear tarea" />
    </form>
    <ul class="list">
      <li
        class="task"
        v-for="(task, i) in tasks"
        :key="'task' + i"
        :class="{completed: task.completed}"
        @click="completeTask(task.text)"
      ></li>
    </ul>
  </div>
</template>

<script>
export default {
  data: () => ({
    newTask: "",
    tasks: []
  }),
  methods: {
    createTask() {
      let task = {
        text: this.newTask,
        completed: false
      };
      this.tasks.push(task);
      this.newTask = "";
      console.log(this.tasks);
    },
    completeTask(taskText) {
      for (let i = 0; i < this.tasks.length; i++) {
        let task = this.tasks[i];
        if (taskText === task.text) {
          task.completed = !task.completed;
        }
      }
    }
  }
};
</script>
<style scoped>
.task-list {
  width: 800px;
  max-width: 100%;
  margin: 0px auto;
}
.form {
  background: white;
  border-radius: 12px;
  padding: 30px;
  box-shadow: 0px 10px 22px -1px rgba(0,0,0,0.25);
  margin-top: 10px;
}
.label {
  display: block;
  margin-bottom: 10px;
}
.input {
  height: 35px;
}
.button {
  margin-left: 20px;
  height: 35px;
  border: none;
  border-radius: 5px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, .2);
  background-color: #2ecc71;
  color: #ecf0f1;
  cursor: pointer
}
.list {
  margin-top: 40px;
}
.task {
  cursor: pointer;
  margin: 10px 0;
}
.completed {
  text-decoration: line-through;
  color: lightgrey;
}
</style>

Así quedaría todo:

El diseño lo he hecho en muy rápido en 2 minutos, no me lo tomes en cuenta.

Pues todo listo, te dejo el proyecto subido a Codesanbox para que puedas jugar con él:

Proyecto completo en codesabox

Te dejo un par de ejercicios como deberes por si quieres experimentar por tu cuenta:

  • [ ] 🗑️ Botón dentro de cada tarea para poder eliminarlas completamente.
  • [ ] ✏️ Botón encima de la lista de tareas para poder marcar o desmarcar todas como completadas

Conclusiones

Espero de verdad que te haya gustado este ejemplo. Yo creo que este tipo de ejercicios vienen muy bien cuando estás aprendiendo porque no es solo teoría y se ven las cosas de otra forma.

No te quedes solo con este ejemplo, con lo que ya sabes deberías ser ya capaz de hacer cosas bastantes interesantes. Te animo a que intentes crear otros proyectos con lo que ya sabes para probar tus conocimientos.

En los siguientes episodios seguiremos investigando más funcionalidades de Vue para que puedas hacer cosas más increíbles.

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

Picando Código

Video promocional de Intellivision de Mattel Electronics – 1978

diciembre 04, 2019 11:30

Ayer se cumplieron 40 años de la salida de la consola de videojuegos Intellivision. Fue lanzada por Mattel Electronics el 3 de diciembre de 1979. Se desarrollaron juegos desde 1978 hasta 1990 cuando fue discontinuada. La marca Intellivision se niega a morir, y en 2020 verá la luz una nueva consola de la marca: Intellivision Amico.

Intellivision

Como parte del aniversario, la empresa actual decidió subir a internet un video promocional nunca antes visto por el público en general. El video es de 1978, filmado un año antes del lanzamiento oficial de la computadora y cuenta con información que cambió antes del lanzamiento. Pero resulta sumamente interesante.

“El sistema basado en computadoras que transforma la televisión en un centro interactivo para juegos, entretenimiento, autoeducación, mejora personal y procesamiento de información individual y familiar.”

“Intellivision es una serie de componentes de hardware modulares y software”, en principio una consola de videojuegos (master component) con dos controles de mando y lector de cartuchos con juegos y aplicativos de software. Cuando hablan del componente “teclado” agregado, lo describen como un teclado completo estilo máquina de escribir con 64 teclas. ¡Estilo máquina de escribir! Algo que en el momento ayudaría a venderlo al público general porque ya estarían familiarizados con máquinas de escribir. También describen el lector de cassettes (cintas con dos pistas digitales y 2 pistas de audio) y micrófono para dar comandos de voz.

Las aplicaciones educativas y de bienestar están pre-programadas para explicarte el material, y el usuario debe responder por el micrófono o teclado “en simple inglés. No hay absolutamente ninguna necesidad de tener conocimiento de lenguaje informático”. Mencionan gestión financiera familiar, salud física, una aplicación para aprender francés, una para generar menúes y presupuestos de comida para la casa, y servicios financieros de gestión de presupuesto e impuestos. Todo en la privacidad de tu propio hogar. Algo que se va perdiendo de a poco 40 años después.

Me resulta sumamente interesante leer, ver y aprender sobre distintas épocas de la computación e imaginar un presente alternativo si otros jugadores hubieran dominado los mercados y los distintos eventos que nos trajeron a la situación actual. En fin, si les interesa, acá va el video:

YouTube Video

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

Picando Código

Datos y Cogollos – Encuentro sobre acceso a la información y transparencia en el sector agua

diciembre 03, 2019 01:20

El próximo martes, 10 de diciembre de 2019 a las de 19:00 a 21:00, la comunidad de datos abiertos de Uruguay se reune en la diaria Media Lab:

Datos y Cogollos

Detalles

Llega la cuarta edición de este evento para conocer y dar a conocer lo que se está haciendo con datos abiertos, ya cumpliendo un año desde la primera edición y ahora cómo evento paralelo del Encuentro sobre acceso a la información y transparencia en el sector agua, organizado por Fundación Avina y la diaria.

¡Pero tranquilos que siguen pudiendo hablar de cualquier tema, no sólo de agua! 😬

¿Qué es esto?

Es un evento para juntar a toda la comunidad trabajando alrededor (o cerca, o con interés) del tema Datos Abiertos, conocer los proyectos que hay en la vuelta, pero sobretodo conocer a las personas que estamos en eso y compartir una charla acompañada por alguna bebida para hacerlo más informal.

Es un formato que empezó en México en 2013, impulsado por SocialTIC bajo el nombre de “Datos y Mezcales” y que hace bastante rato que teníamos ganas de traer a Uruguay.

Está organizado por la comunidad de Datos Abiertos local, así que no le pertenece a ninguna organización en particular y está más que abierto a que se sigan sumando nuevas personas y organizaciones.

¿Qué va a pasar?

Ésta es la agenda que vamos a intentar seguir:
– Bienvenida y presentación de la iniciativa
– Presentaciones relámpago ⚡ (máx. 5 min.) de proyectos y organizaciones
– Brindis y convivencia (asado, chelas, cogollos, helado de dulce de leche, etc.)

¿Cómo me anoto para presentar mi proyecto?

Comentá en este Meetup y decinos el nombre de persona que presentaría, un correo de contacto y el nombre del proyecto/organización/presentación que quieren compartir. Vamos a ir anotando la lista de oradores/as acá, donde también podés anotarte directamente si así lo preferís.

Las presentaciones pueden ser con diapositivas/video/apoyo audiovisual y solicitamos que cada persona que presente la suba a la web (ej. Google Slides) y pegue el enlace a su presentación en el documento anterior. Buscamos presentaciones y presentadores/as lo más diversas posibles, empezando por apuntar a la paridad de género, así que por favor no se ofendan si preguntamos si es posible que presenten otras personas llegado el caso. ☺

Acá podés encontrar sobre las presentaciones del Primer Datos y Cogollos.

¿Por qué “cogollos”?

Básicamente porque podemos… El evento adapta su nombre a alguna bebida típica del país y así pasó por México (Datos y Mezcales), El Salvador (Datos y Cervezas), Bolivia (Datos y Singanis), Ecuador (Datos y Bielas), Colombia (Datos y Guaros), Costa Rica (Datos y Chiliguaros), Guatemala (Datos y Tragos) y España (Datos y Cañas). Cuando le toca a Uruguay, optamos por algo que ninguna de las organizaciones amigas de todos esos países podría poner legalmente (o sea, es un poco un chiste interno).

¿Es obligatorio presentar/saber de datos abiertos/tomar/fumar porro?

No, para nada. Cada uno viene y hace lo que quiere 🤗

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

Fixed Buffer

Novedades de C# 8: Índices y Rangos

diciembre 03, 2019 09:00

La imagen muestra el logotipo de c# 8.0

Ya son varias semanas hablando de C# 8, y hoy le toca el turno a los Índices y Rangos. En semanas anteriores hemos hablado sobre los Streams asíncronos o el nuevo pattern matching y su potencia, ahora es el momento de explorar que mejoras tenemos para operar colecciones.

Índices en C# 8

La primera de las dos opciones, los índices, nos va a permitir acceder al índice de una colección como su propio nombre indica. Vaya… ¿Esto es nuevo? Pues sí, la novedad es que se ha introducido una estructura «Index» dentro de «System.Index». Esta estructura nos va a permitir acceder a una posición desde el inicio (como toda la vida) o desde el final utilizando ‘^‘. Esta es precisamente la novedad con los índices. Imagina esta matriz:

var array = new string[]
{
    "Hola",
    "¿Cómo estás?",
    "Bien. ¿Y tú?",
    "Muy bien. Muchas gracias",
    "Adiós"
};

Hasta ahora, podíamos acceder a su índice simplemente haciendo algo como esto:

var primero = array[0];
var último = array[array.Lenght -1];

Utilizando índices, vamos a poder vamos a poder hacer algo como esto:

var indexPrimero = new Index(0);
var indexFin = new Index(1,true);
var primero = array[indexPrimero];
var último = array[indexFin];

Esto como tal es poco práctico, ya que estamos necesitando el doble de código, pero podemos colocar directamente el valor del índice:

var primero = array[0];
var último = array[^1];

Al trabajar con índices desde el final, hay que tener en cuenta que el símbolo ‘^’ es equivalente a la longitud del array, por lo que ‘^0’ provocará una excepción como lo haría acceder a ‘array.Lenght’

Rangos en C# 8

Hasta ahora los índices no nos están aportando especial utilidad, puesto que lo que conseguimos con ellos ya lo podíamos hacer con las herramientas que estaban disponibles. Donde van a tomar especial utilidad los índices de C# 8 es cuando los combinemos con los rangos.

Los rangos son una nueva estructura introducida en C# 8 que vamos a poder utilizar para extraer una parte de una colección. Para indicar que queremos usar un rango, vamos a poder utilizar la estructura «Range» dentro de «System.Range» o ‘..’. Si por ejemplo quisiéramos extraer las dos primeras posiciones del array, podemos hacer algo como esto:

var range = new Range(0, 2);
var sel = array[range];

var sel2 = array[0..2];

Es importante tener en cuenta que cuando definimos un rango con esta nueva característica de c# 8, el primer valor se incluye y el segundo se omite. Esto quiere decir que en el caso anterior, vamos a obtener las posiciones 0 y 1, omitiendo la dos.

Como realmente un rango esta compuesto por dos índices, vamos a poder utilizar un índice desde el final. Si por ejemplo queremos quedarnos con todos los valores menos el primero y el último, podemos hacer algo como esto:

var sel = array[1..^1];

Otra posibilidad que nos ofrecen los rangos es crear rangos abiertos:

var sel1 = array[..];       //Devuelve la colección completa
var sel2 = array[1..];      //Devuelve todos los valores desde el 1 hasta el final
var sel3 = array[..^1];    //Devuelve todos los valores desde el 0 hasta el penúltimo

¿Y dónde puedo aplicar esta nueva característica? La verdad es que no todas las colecciones implementan esta posibilidad, tienen que ser colecciones «contables» . Este tipo de colecciones contables engloban tipos como Array, List<T>, Span<T>,… y String (puedes verlos todos en el enlace anterior). Esto quiere decir que vamos a poder hacer un Substring.

var input = "Hola, buenos días";
var output = input[6..12]; //buenos

Conclusión

Esta característica de C# 8 para poder acceder a colecciones mediante índices y rangos puede aportar utilidad y legibilidad en el código, sobre todo a la hora de eliminar algunas operaciones con Linq que se limitan a obtener un fragmento secuencial de la colección. Es una herramienta built-in, por lo que vamos a tener mejor rendimiento que si usáramos Linq y es más compacto. ¿Qué te parece a ti esta característica?

**La entrada Novedades de C# 8: Índices y Rangos se publicó primero en Fixed Buffer.**

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

Variable not found

Ojo con la inicialización de propiedades usando expression bodied members

diciembre 03, 2019 07:05

.NET CoreHace poco estaba revisando un proyecto y me topé con un problema derivado de un uso extraño de los expression bodied members, y creo que es interesante comentarlo porque, aunque probablemente la mayoría no os vayáis a encontrar con esto, seguro que puede venir bien a alguien que no tenga del todo claro cómo y cuándo se debe utilizar esta sintaxis.

Pero comencemos desde el principio...

Evolución de la inicialización de propiedades

Como sabemos, el siguiente código es la forma "tradicional" de definir una propiedad de sólo lectura con backing field, inicializada a un valor concreto:
public class Example
{
private int _theNumber = 42;
public int TheNumber
{
get
{
return _theNumber;
}
}
}
C# 3 introdujo las propiedades automáticas, que eliminaban la necesidad de definir explícitamente un backing field. Esto simplificaba algo el código, pero su inicialización debíamos implementarla en el constructor:
public class Example
{
public int TheNumber { get; }
public Example()
{
TheNumber = 42;
}
}
Afortunadamente, C# 6 introdujo la posibilidad de inicializar directamente propiedades automáticas junto a su declaración, lo que permitía obtener el mismo resultado escribiendo un código más compacto:
public class Example
{
public int TheNumber { get; } = 42;
}
Y al mismo tiempo, C# 6 introdujo los expression bodied members, que luego fueron extendidos en C# 7 para permitir una sintaxis aún más concisa en determinados escenarios. Usando esta característica, el código anterior podríamos dejarlo en:
public class Example
{
public int TheNumber => 42;
}

¿Y cuál es el problema?

Ciertamente esta forma de inicializar propiedades usando expression bodied members es muy cómoda y compacta, pero puede confundir un poco si no sabemos exactamente qué estamos haciendo.

Imaginemos ahora el siguiente código, donde se utiliza => con el objetivo de inicializar una propiedad:
public class Example: IDisposable
{
public DatabaseConnection Connection => Database.OpenConnection(); // Mal!
public void AddOneHundredCustomers()
{
for(var i=1; i <= 100; i++)
{
Connection.AddCustomer(new Customer() { Id = i })
}
}
public void Dispose()
{
Connection.Dispose();
}
}
Aunque la intención en el uso de => era inicializar la propiedad, en realidad lo que estamos haciendo es instanciar un objeto DatabaseConnection cada vez que se accede a la propiedad Connection, tanto en el cuerpo de AddOneHundredCustomers() como en Dispose(). Es decir, durante la ejecución del siguiente código instanciaríamos 101 conexiones, y liberaríamos sólo la última de ellas:
using (var example = new Example())
{
example.AddOneHundredCustomers();
}
Lo importante aquí es entender que al usar esa sintaxis no estamos inicializando nada, sino indicando la expresión que retornará el valor de la propiedad cada vez que accedamos a ella.

Entonces, ¿cuál sería la forma correcta de hacerlo?

Pues como siempre ;) Por una parte declaramos la propiedad y luego la inicializamos en el constructor de la clase, o bien utilizamos los inicializadores de propiedades, como en el siguiente ejemplo:
public class Example : IDisposable
{
public DatabaseConnection Connection { get; } = Database.OpenConnection(); // Ok
...
}
En conclusión, cuando se introducen novedades al lenguaje, a los desarrolladores nos encanta comenzar a utilizarlas enseguida, a veces sin llegar a entender bien qué es lo que estamos haciendo.

Y la moraleja de esta historia es que cualquier novedad, por insignificante que parezca, requiere dedicar al menos unos minutos de estudio y asimilación, pues esto puede marcar la diferencia entre una aplicación que funciona y otra que no.

Publicado en Variable not found.

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

Variable not found

Enlaces interesantes 382

diciembre 02, 2019 07:46

Enlaces interesantesAhí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

ASP.NET Core / ASP.NET

Azure / Cloud

Conceptos / Patrones / Buenas prácticas

Data

Web / HTML / CSS / Javascript

Visual Studio / Complementos / Herramientas

Xamarin

Otros

Publicado en Variable not found.

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

Blog Bitix

Implementar un segundo factor de autenticación en una aplicación web Java con Spring

noviembre 29, 2019 05:00

El segundo factor de autenticación es una medida adicional en la autenticación que proporciona una notable mayor seguridad que utilizar solo un usuario y contraseña. Utilizando Spring y la aplicación para smatphone Google Authenticator se puede implementar en una aplicación Java el segundo factor de autenticación o 2FA con códigos temporales o TOTP.

Java
Spring

Comúnmente para realizar el proceso de autenticar a un usuario se ha realizado simplemente con el método de usuario y contraseña. Sin embargo, verificar la identidad mediante usuario y contraseña para algunos usuarios no es suficientemente seguro dado que los usuarios pueden elegir contraseñas débiles con pocos caracteres o sin usar una combinación que incluya letras, números y símbolos, pueden elegir contraseñas comunes muy utilizadas fáciles de adivinar con un ataque de diccionario, pueden usar la misma contraseña para varios servicios de modo que si las contraseñas de un servicio son descubiertas cualquier otro servicio que las utilice potencialmente corre un riesgo de seguridad. Usar solo usuario y contraseña no proporciona la suficiente seguridad para ciertos servicios que permiten realizar transacciones que involucra dinero, tratan datos sensibles o son servicios atractivos para ser atacados.

Para que las contraseñas sean seguras las aplicaciones en sus bases de datos guardar las contraseñas usando Salted Password Hashing, los usuarios por su parte deben utilizar un generador de contraseñas, utilizar una contraseña distinta para cada servicio y guardalas en una base de datos cifrada como KeePassXC para recordar cada una de ellas. Las contraseñas son algo que se conoce, cualquier persona que conozca la contraseña puede autenticarse, más recientemente una capa adicional de seguridad es requerir algo que se tiene, el segundo factor de autenticación o 2FA.

La aplicación Google Authenticator para dispositivos móviles Android permite utilizarse como segundo factor de autenticación, esta aplicación genera códigos con un tiempo corto de duración que son requeridos en un segundo paso de la autenticación después de introducir el usuario y contraseña. Con un segundo factor de autenticación se requiere algo que se sabe, el usuario y contraseña, y algo que se tiene, el dispositivo móvil que genera códigos con lo que aunque la contraseña quede comprometida no se podría realizar la autenticación sin poseer el segundo factor de autenticación.

Dado que los códigos de verificación tienen un tiempo de vida corto, habitualmente de 30 segundos, y acceder al generador del segundo factor de autenticación requiere acceso físico al dispositivo móvil la combinación de que las credenciales queden comprometidas es significativamente más difícil y por tanto la seguridad aumenta al mismo tiempo. Los principales servicios de internet como Google, Amazon, Twitter y otros servicios utilizados por millones de usuarios permiten ya utilizar 2FA, un fallo en su seguridad por la cantidad de usuarios e importante información que registran les supodría una muy mala imagen, pérdida de ingresos, costes, reputación, usuarios o dependiendo de la gravedad del fallo y los datos comprometidos multas millonarias.

A través de Spring Security y la librería aerogear-otp-java una aplicación Java puede implementar el segundo factor de autenticación, incluso posibilitar de que el requerimiento de solicitar segundo factor de autenticación sea opcional según la preferencia de un usuario o como forma de que los usuarios progresivamente habiliten el 2FA. El primer paso es proporcionar al usuario una clave secreta a través de un código QR que codifica una clave secreta que se utiliza para generar los códigos de verificación, el usuario debe escanearlo con la aplicación Google Authenticator con la cámara para que genere código de 6 dígitos con una validez de 30 segundos en el momento de autenticarse, este paso se realiza en el momento de registrarse o de activar el 2FA si es opcional. Con Google Authenticator el código en vez con la cámara también se puede introducir mediante el teclado si la aplicación se lo proporciona en forma de texto en vez de como imagen QR. La ventaja del código QR es que es más rápido y cómodo.

El primer paso de la autenticación utilizando 2FA es introducir el usuario y contraseña. El segundo paso es introducir el código del segundo factor de autenticación. Introducidos ambos el usuario es redirigido a la página de inicio.

Autenticación con segundo factor de autenticación
Aplicación Google Authenticator con varios generadores de códigos temporales

Validado el código del 2FA al usuario se le asignan los permisos que le corresponden en el sistema y que le otorgan permisos para realizar acciones, en este caso entrar a la página de inicio.

La implementación en código contiene las clases que representan una cuenta en el sistema, en InMemoryAccountRepository se crean dos usuarios admin y user con sus contraseñas en el ejemplo en texto plano y los roles que tiene asignados que les otorgarán permisos para realizar acciones en la aplicación.

1
2
3
4
5
6
package io.github.picodotdev.blogbitix.spring2fa.account;
public interface AccountRepository {
Account find(String username);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package io.github.picodotdev.blogbitix.spring2fa.account;
...
@Repository
public class InMemoryAccountRepository implements AccountRepository {
private static String ADMIN_SECRET = "6YFX5TVT76OHHNMS";
private List<Account> accounts;
public InMemoryAccountRepository() {
accounts = new ArrayList<Account>();
init();
}
private void init() {
Account admin = new Account();
admin.setUsername("admin");
admin.setPassword("{noop}password");
admin.setAuth2fa(true);
admin.setSecret(ADMIN_SECRET);
admin.setRoles(Arrays.asList("ROLE_USER"));
Account user = new Account();
user.setUsername("user");
user.setPassword("{noop}password");
user.setAuth2fa(false);
user.setRoles(Arrays.asList("ROLE_USER"));
accounts.add(admin);
accounts.add(user);
}
@Override
public Account find(String username) {
return accounts.stream().filter(account -> account.getUsername().equals(username)).findFirst().orElse(null);
}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package io.github.picodotdev.blogbitix.spring2fa.account;
...
public class Account {
private String username;
private String password;
private String secret;
private Boolean auth2fa;
private List<String> roles;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public Boolean isAuth2fa() {
return auth2fa;
}
public void setAuth2fa(Boolean auth2fa) {
this.auth2fa = auth2fa;
}
public List<String> getRoles() {
return roles;
}
public void setRoles(List<String> roles) {
this.roles = roles;
}
}

La configuración de seguridad en Spring Security indica para cada URL que permisos se requieren. Para acceder a la página de contenido /home de la aplicación se requiere el rol USER, a la página de inicio de sesión /login se permite acceder a los usuario no autenticados donde introducen sus credenciales de usuario y contraseña, una vez validado el usuario y contraseña el usuario autenticado tiene el rol PRE_AUTH_USER, dependiendo de si el usuario en su prefrencia usa 2FA o no en el manejador de autenticación exitosa SecondFactorAuthenticationSuccessHandler redirige al usuario a la página /home o la página /code para intorducir el código de verificación del segundo factor autenticación. Al usuario autenticado exitosamente de forma completa se le sustituye el permiso PRE_AUTH_USER por los que tenga asignado, en el ejemplo el rol USER.

La verificación del código del segundo paso de autenticación se realiza en la clase CodeController con la clase Totp a partir del código enviado y el código secreto con el cual se generó la imagen de código QR.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package io.github.picodotdev.blogbitix.spring2fa.spring;
...
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Bean
public PasswordEncoder encoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
return new SecondFactorAuthenticationSuccessHandler();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/static/**").permitAll()
.antMatchers("/code").hasRole("PRE_AUTH_USER")
.antMatchers("/home").hasRole("USER")
.anyRequest().authenticated();
http.formLogin()
.loginPage("/login")
.permitAll()
.successHandler(authenticationSuccessHandler);
http.logout()
.permitAll();
}
@Autowired
public void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package io.github.picodotdev.blogbitix.spring2fa.spring;
...
@EnableWebMvc
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
registry.addViewController("/code").setViewName("code");
registry.addViewController("/home").setViewName("home");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
}
@Bean
public ClassLoaderTemplateResolver templateResolver() {
ClassLoaderTemplateResolver result = new ClassLoaderTemplateResolver();
result.setPrefix("templates/");
result.setSuffix(".html");
result.setTemplateMode("HTML");
return result;
}
@Bean
public SpringTemplateEngine templateEngine(ClassLoaderTemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.addDialect(new LayoutDialect());
templateEngine.setTemplateResolver(templateResolver);
return templateEngine;
}
@Bean
public ThymeleafViewResolver viewResolver(SpringTemplateEngine engine) {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(engine);
viewResolver.setCache(false);
return viewResolver;
}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package io.github.picodotdev.blogbitix.spring2fa.spring;
...
public class SecondFactorAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
handle(request, response, authentication);
clearAuthenticationAttributes(request);
}
protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
String targetUrl = getTargetUrl(authentication);
if (response.isCommitted()) {
return;
}
redirectStrategy.sendRedirect(request, response, targetUrl);
}
protected String getTargetUrl(Authentication authentication) {
UserDetailsAdapter userDetailsAdapter = (UserDetailsAdapter) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (userDetailsAdapter.getAccount().isAuth2fa()) {
return "/code";
} else {
Utils.setAuthentication();
return "/home";
}
}
protected void clearAuthenticationAttributes(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return;
}
session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
}
public void setRedirectStrategy(RedirectStrategy redirectStrategy) {
this.redirectStrategy = redirectStrategy;
}
protected RedirectStrategy getRedirectStrategy() {
return redirectStrategy;
}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package io.github.picodotdev.blogbitix.spring2fa.spring;
...
public class Utils {
public static void setAuthentication() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
UserDetailsAdapter userDetailsAdapter = (UserDetailsAdapter) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(userDetailsAdapter.getAccount().getRoles().toArray(new String[0]));
Authentication newAuth = new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), authentication.getCredentials(), authorities);
SecurityContextHolder.getContext().setAuthentication(newAuth);
}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package io.github.picodotdev.blogbitix.spring2fa.spring;
...
public class UserDetailsAdapter implements UserDetails {
private Account account;
public UserDetailsAdapter(Account account) {
this.account = account;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return AuthorityUtils.createAuthorityList("ROLE_PRE_AUTH_USER");
}
@Override
public String getUsername() {
return account.getUsername();
}
@Override
public String getPassword() {
return account.getPassword();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
public Account getAccount() {
return account;
}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package io.github.picodotdev.blogbitix.spring2fa.spring;
...
@Component
@Primary
public class UserDetailsServiceAdapter implements UserDetailsService {
@Autowired
private AccountRepository accountRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account account = accountRepository.find(username);
if (account == null) {
throw new UsernameNotFoundException(username);
}
return new UserDetailsAdapter(account);
}
}

El código QR es una imagen generada a partir del código secreto y una información adicional que al usurio le permite identificar la cuenta, hay webs que permiten decodificar una imagen QR para analizar que información incorpora, en esta la información de la cuenta Spring2FA (admin) y el secreto 6YFX5TVT76OHHNMS utilizado para generar los códigos temporales. En el HTML devuelto se incluye una imagen con la información embebida en el enlace de la imagen, la imagen se genera por un servicio de Google.

Decodificador de imágenes código QR
1
2
3
4
5
6
7
8
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout" layout:decorate="~{layout}">
<body>
...
<p><img src="https://chart.googleapis.com/chart?chs=200x200&chld=M%7C0&cht=qr&chl=otpauth%3A%2F%2Ftotp%2FSpring2FA%20(admin)%3Fsecret%3D6YFX5TVT76OHHNMS" /></p>
...
</body>
</html>

Las dependencias de librerías son las siguientes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
...
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf:thymeleaf-spring5:3.0.11.RELEASE'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:2.4.1'
implementation 'org.jboss.aerogear:aerogear-otp-java:1.0.0'
}

Este ejemplo está hecho con la infraestructura que proporciona Spring pero el proceso de autenticación es igualmente implementable con cualquier otro framework o librería.

Muchos de los servicios populares en internet implementan 2FA como medida de proteger las cuentas de los usuarios y la información en esos servicios. Hay bancos que como contraseña de acceso solo tienen un número de seis dígitos con el riesgo que representa sus usuarios por la importancia que tiene la banca en línea de los datos que se trata.

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradlew run.

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

Picando Código

Rookie Series: Nuevo DLC gratuito disponible para Horizon Chase Turbo

noviembre 28, 2019 03:30

Me vengo divirtiendo mucho jugando Horizon Chase Turbo. El estilo visual del juego es genial, y la música excelente. Aquaris publicó una campaña nueva diseñada específicamente para principiantes, aprovechando su primer aniversario en Nintendo Switch y Xbox One. Rookie Series es un DLC gratuito disponible en Nintendo Switch, Playstation 4, Steam y XBox (en la PSN europea, el DLC estará disponible a partir del dia 2 de diciembre).

Horizon Chase Turbo: Rookie Series

La nueva campaña está enfocada a los jugadores jóvenes e inexpertos y me parece una excelente idea. Es una buena manera de atraer gente nueva, evitando la frustración de perder o bloquearse. Está orientada a que los jugadores puedan evolucionar a su propio ritmo. No hay tablas de puntuación, monedas coleccionables o gestión del combustible, el DLC se preocupa únicamente en que el jugador aprenda las mecánicasde la carrera. Y no tiene nada de malo pasear con tu auto de manera casual y sin preocupaciones, disfrutando de la música y los escenarios que nos ofrece Horizon Chase Turbo.

Características Principales:

  • Rookie, un vehículo totalmente nuevo.
  • 24 circuitos seleccionados del modo World Tour (2 carreras en cada localización).
  • 12 vehículos compitiendo en cada circuito con una IA menos agressiva (World Tour tiene 20 vehículos em cada pista).
  • Sin tablas de puntuación, monedas coleccionables o gestión de combustible.
  • Terminar una carrera en cualquier puesto desbloquea la siguiente.
  • Al terminar todas las carreras, el jugador recibe un Carnet de Conducir Dorado

Nuevo vehículo: RookieHorizon Chase Turbo: Rookie

Horizon Chase Turbo: Rookie Series presenta a Rookie, un auto nuevo perfecto para los jugadores que no tienen mucha experiencia con los juegos de carreras. Tiene un gran control y obtiene actualizaciones automáticas a medida que el jugador progresa en la campaña. Los jugadores también pueden elegir entre otros 4 vehículos clásicos en el juego base, desbloqueando nuevas carreras al final de cada circuito: puedes regresar a cada una libremente para entrenar y mejorar tus resultados.

Al completar las Rookie Series, los jugadores desbloquean una Licencia de Conducir Dorada, que muestra que están listos para el World Tour y otros modos de juego. Con el nuevo DLC de campaña y el modo multijugador local (¡que todavía no he tenido el gusto de probar!), Aquiris busca reunir a amigos y familiares para que puedan disfrutar de todo lo que ofrece Horizon Chase Turbo.

El juego tiene un montón de contenido de por sí, y se sigue agregando más con éstos DLC. En el World Tour voy por India, y me faltan completar Australia, China, Japón y Hawaii para terminarlo (hasta donde sé). Mientras tanto, también estoy jugando el modo torneo, habiendo completado el Amateur y corriendo el Profesional. Y en Playground siguen cambiando las campañas cada tanto. ¡Diversión para rato!

Si pudiera sugerir otro DLC, sería armar nuestro auto con un lanzamisilies y poder dispararlo a los autos de la IA cuando no nos dejan pasarlos…

Les dejo el tráiler del nuevo DLC:
YouTube Video

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

Picando Código

Piques Emacs: mantener el límite de caracteres por cada línea

noviembre 28, 2019 12:30

Dependiendo el contexto podemos querer un límite de 80, 100, 120 caracteres por línea. Por ejemplo en algunos proyectos se sugiere usar un límite de 50 caracteres para el mensaje de un commit en Git.

Emacs tiene un modo para mantener el límite de caracteres en cada línea que escribimos: auto-fill-mode. Al activarlo, Emacs se va a encargar automáticamente de mantener cada línea dentro del límite establecido:

 

El límite depende de la variable fill-column de Emacs. Para leer más al respecto podemos usar el comando de Emacs describe-variable e ingresar fill-column. Emacs nos cuenta que es una variable definida en el código fuente C, su valor es 80 (en mi caso), y su valor original era 70.

También nos cuenta que podemos cambiar el valor de la variable presionando Ctrl-x f. Ésto va a cambiar el valor para el buffer en el que estamos trabajando. Si queremos cambiar el valor permanentemente, podemos agregar la siguiente línea a nuestro archivo de configuración:

(setq-default fill-column 80)

Si queremos formatear un párrafo existente con el límite de caracteres por línea, debemos dirigir el cursor al párrafo y presionar Alt q (fill-paragraph). Si queremos hacer lo mismo con una región: Alt x e ingresamos fill-region en el mini-buffer.

Un paquete interesante que podemos usar relacionado al ancho de columna es fill-column-indicator, una indicación gráfica de dónde se encuentra el límite de caracteres. Si -como yo- usan Spacemacs, pueden ejecutarlo automáticamente con [SPC (evil-mode) | Alt M (holy-mode)] + t (de toggles) + f (de fill-column-indicator).

Emacs fill-column-indicator

Emacs fill-column-indicator

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

Arragonán

Sobre Sistemas de Diseño

noviembre 27, 2019 12:00

Estaba preparando otro post donde quería incluir una pequeña explicación de qué es un design system o sistema de diseño. Al ver que me iba extendiendo y se iba a diluir con el tema original, me he atrevido a intentar escribir sobre qué es y dar un poco mi visión sobre ello.


Hay multitud de compañías y organizaciones que han ido trabajando en sus sistemas de diseño, incluso hay muchos que han sido liberados como open source: El famosísimo Material de Google, Primer de Github, shibori3 y purple3 de Heroku, el de Atlassian, el del gobierno de UK… Así que es un tema que va surgiendo de forma recurrente hablando con gente dedicada o involucrada en la creación de productos digitales.

¿Qué es un sistema de diseño?

Por conversaciones en las que he participado y algunas lecturas sobre el tema, veo que hay interpretaciones algo distintas sobre qué es un sistema de diseño. Así que cuando tengo oportunidad de preguntarle a alguien sobre a qué se refiere con sistema de diseño encuentro que lo habitual es que se utilice como sustituto al de guía de estilo o al de librería de componentes UI. Incluso en algún caso aislado he encontrado que en realidad se referían a que tenían plantillas que alguien había hecho 😅.

Una guía de estilo o una librería de componentes son librerías de patrones perceptuales y/o funcionales, los artefactos más visibles de un sistema de diseño, pero no son el sistema de diseño en sí mismo.

Hasta donde yo sé no hay una definición canónica, yo uso como base la del libro Design Systems de Alla Kholmatova:

A set of connected patterns and shared practices, coherently organized to serve the purposes of a digital product.

Los patrones son una pata y las prácticas que se utilizan la otra, pero como en cualquier otro producto debemos marcar unas metas u objetivos además de tener una serie de principios de diseño que estén acordes, todo ello nos servirá como guía para la sistematización.

No darle un sentido claro a un sistema de diseño puede hacernos acabar con un batiburrillo de patrones que no tengan mucha consistencia entre sí, tener una variedad de patrones excesiva para resolver un mismo problema o incluso provocar conflictos entre personas por no tener claros los objetivos.

De cara a tener una comunicación efectiva entre los que construimos producto, también debemos trabajar en tener un lenguaje compartido para referirnos a los patrones del sistema de diseño para evitar confusiones.

Ya sean más descriptivos o algo más metafóricos, ese lenguaje compartido debería ser fácil de comprender por quienes construyen o utilizan el sistema de diseño para hacer producto. Deberíamos esforzarnos tratar de dar nombres que reflejen la finalidad de los patrones y no su aspecto.

Un ejemplo muy sencillito podría ser tener un patrón que se llame "call to action button" frente a "button orange big". Uno indica para qué está pensado mientras que el otro dice y está acoplado a cómo es, si cambiara algo de su aspecto su nombre perdería el sentido en el segundo caso.

Evidentemente todo esto termina en librerías de patrones para herramientas tipo Sketch, Figma, Adobe XD… enfocado a que quienes diseñen puedan reutilizarlos para poder centrarse más en la exploración de nuevos problemas que en resolver de nuevo cuestiones que ya se han resuelto anteriormente.

No tan habitual pero también necesario es tener artefactos como guías de componentes en html/css plano y librerías de componentes para el framework javascript o de la plataforma nativa de turno. Así se facilita el trabajo de quienes pasan a código frontend los diseños permitiendo entregar antes y ganando en mantenibilidad.

¿En qué nos beneficia todo esto?

  • Tener mayor consistencia tanto visual como de interacción. Al compartir principios de diseño facilitamos que los usuarios tengan una experiencia de usuario similar usando las distintas partes de un producto o productos que compartan y respeten el mismo sistema de diseño.
  • Mejorar la comunicación entre los miembros de un equipo de producto por tener un lenguaje compartido entre diseño, desarrollo, producto… En la documentación, UI kits, código, discusiones…
  • Gracias a las diferentes librerías con el tiempo permite ser más eficientes tanto al diseñar como al desarrollar. Esta eficiencia suele traducirse en permitir centrarse en resolver problemas nuevos y en entregar producto antes.

Suelo insistir en que a lo que desde luego no ayuda es a no contratar a alguien con skills de diseño, pretender que un sistema de diseño sea una bala de plata que cubra todo es bastante ingenuo. Por mucho sistema de diseño que haya, luego se ven interfaces que son auténticas aberraciones.

Además hay que tener en cuenta que un sistema de diseño debería estar en continua revisión, lo único constante es el cambio y haciendo producto digital el cambio es bastante rápido.

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

Picando Código

Firefox Lockwise – El gestor de contraseñas seguro de Mozilla

noviembre 26, 2019 12:30

Por fín empecé a usar un gestor de contraseñas. Es bueno tener contraseñas complejas y distintas para cada servicio que usemos. Así, si (cuando) uno de éstos servicios aparece en un nuevo escándalo de contraseñas filtradas al público, nuestra cuenta se ve comprometida únicamente en ese servicio.

Intenté por un tiempo vivir sin un gestor de contraseñas. De trabajos anteriores tuve experiencia con 1Password, pero no tenía extensión para Firefox, y lo usaba más que nada para el trabajo en sí. En la época de Cubox usábamos otro gestor online que no recuerdo el nombre, pero la interfaz gráfica se había quedado en los 90’s.

Finalmente Mozilla -una fundación en la que confío- lanzó un gestor de contraseñas software libre y con las características que necesito:

Firefox Lockwise

Firefox Lockwise provee un plugin para Firefox y aplicaciones móviles para Android y iOS. En el navegador web, se enchufa a la característica de “¿Desea guardar esta contraseña?” y mantiene un listado de los sitios web, usuarios y contraseñas. También nos advierte si nuestras contraseñas han sido vulneradas en un sitio en particular (gracias Pablo por señalarlo en los comentarios). Firefox cuenta también con el servicio Firefox Monitor que nos informa de sitios en los que cuentas asociadas a nuestro correo electrónico han sido vulneradas.

Para sincronizar esta información, debemos crear una cuenta gratuita Firefox Account:

Crear una cuenta Firefox te permite mantenerte seguro en Internet. La meta de Mozilla es darle poder a las personas en sus vidas conectadas. Al crear una cuenta Firefox, podemos subir archivos más grandes en Firefox Send (de manera segura y privada), informarnos si nuestras cuentas han sido vulneradas (mediante Firefox Monitor), mantener nuestras contraseñas bien guardadas y sincronizar sitios web y pestañas entre distintas instancias de Firefox (de una computadora a otra, del teléfono a la computadora, etc).

Firefox Lockwise

Otra característica interesante que tenemos en Firefox es “generar contraseña segura”. Cuando nos damos de alta en un sitio o cambiamos la contraseña de una cuenta existente, Firefox nos da la posibilidad de generar una contraseña aleatoria y segura, y guardarla ya en Lockwise. ¡Podemos tener una contraseña tan segura que ni siquiera la hemos visto!

Por otro lado están las aplicaciones móviles para Android y iOS. En ellas nos logueamos con nuestra cuenta Firefox, y las credenciales se sincronizan automáticamente, permitiéndonos acceder a las credenciales desde aplicaciones y sitios web en nuestro teléfono. Podemos configurar la aplicación para desbloquearla con nuestra huella dactilar, y también podemos habilitar el completado automático de contraseñas para que Firefox Lockwise ingrese las credenciales automáticamente en todas las aplicaciones. En el peor de los casos, podemos copiar las credenciales desde la aplicación y pegarlas en el campo de usuario y contraseña de cualquier aplicación o sitio web.

La verdad que me ha resultado bastante práctico y lo recomiendo. Recuerden siempre usar contraseñas seguras, contraseñas distintas en distintos servicios, y cuídense!

Visita Firefox Lockwise para empezar a usarlo.

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

Variable not found

La directiva @helper, ¿reencarnada en ASP.NET Core 3?

noviembre 26, 2019 07:48

ASP.NET CoreHace ya algún tiempo nos preguntábamos que dónde había ido a parar la directiva @helper de Razor en ASP.NET Core, y la respuesta era simple: había desaparecido.

Como recordaréis, esta directiva era bastante útil para simplificar el código de las vistas y mejorar su legibilidad, pues permitía crear funciones reutilizables que mezclaban HTML y código de servidor, como en el siguiente ejemplo:
@* File: Test.cshtml *

@Multiplication(2)
@Multiplication(3)

@helper Multiplication(int x)
{
<h2>Multiplication table of @x</h2>
<ul>
@for (var i = 1; i <= 10; i++)
{
<li>@x * @i = @(x * i)</li>
}
</ul>
}
Hasta la versión 2.2, teníamos que conformarnos con apaños como los descritos en aquél post si queríamos emular lo que llevábamos tantos años utilizando con MVC 5 y anteriores. Y también entonces comentamos que había ciertas posibilidades de que en algún momento volviera a la vida, y éstas se han materializado, por fin, en ASP.NET Core 3.0.

Aunque quizás más bien habría que hablar de reencarnación...

¿Cómo implementar funciones con código de marcado?

La nueva forma de implementar bloques de código reutilizables mezclando C# y HTML en el interior de una vista ya no se basa en la directiva @helper. Ahora, simplemente podemos introducir código de marcado en funciones declaradas localmente o en el interior de bloques @functions de Razor.

Observad el siguiente ejemplo, donde creamos una función Multiplication() que mezcla código HTML y C# para generar una tabla de multiplicación:
@{
Multiplication(2);
Multiplication(3);
}

@functions
{
void Multiplication(int x)
{
<h2>Multiplication table of @x</h2>
<ul>
@for (var i = 1; i <= 10; i++)
{
<li>@x * @i = @(x * i)</li>
}
</ul>
}
}
Con esto podríamos considerar que tenemos de vuelta la directiva @helper, aunque sea utilizando esta nueva sintaxis. Sin embargo, hay algunos aspectos adicionales a tener en cuenta.

Por ejemplo, si la función utilizara internamente asincronía, tendríamos que definirla e invocarla de forma diferente, como en el siguiente ejemplo:
@{
await SlowMultiplication(2);
await SlowMultiplication(3);
}

@functions
{
async Task SlowMultiplication(int x)
{
await Task.Delay(5000);
<h2>Multiplication table of @x</h2>
<ul>
@for (var i = 1; i <= 10; i++)
{
<li>@x * @i = @(x * i)</li>
}
</ul>
}
}
Otro aspecto sumamente importante es que, a diferencia de la clásica directiva @helper, estas funciones renderizan directamente el contenido y no retornan ningún valor, por lo que no se pueden utilizar como expresiones directas:
<h1>Current date: @DateTime.Now</h1>
<div>
@Multiplication(1) <!-- Error: Multiplication() retorna void -->
</div>
-->
Aunque si quisiéramos simular este comportamiento siempre podríamos forzar un retorno, aunque fuera nulo o vacío, de la siguiente forma:
<h1>Current date: @DateTime.Now</h1>
<div>
@Multiplication(1)
</div>

@functions
{
string Multiplication(int x)
{
<h2>Multiplication table of @x</h2>
<ul>
@for (var i = 1; i <= 10; i++)
{
<li>@x * @i = @(x * i)</li>
}
</ul>
return string.Empty;
}
}
Por último, es interesante destacar que también podemos introducir la mezcla de código HTML y C# en funciones locales de C# implementadas dentro de la propia página Razor, como en el siguiente ejemplo:
@{ 
void ShowCurrentDate()
{
<div class="date">@DateTime.Now</div>
}

ShowCurrentDate();
}

¿Y ese tipo de funciones son reutilizables?

Pues me temo que no, al menos de momento. Hasta donde he podido ver, no existe ninguna forma para reutilizar este tipo de funciones; su ámbito es local a la página donde han sido definidas con @functions. Ni siquiera definiéndolas en vistas especiales como _ViewImports.cshtml o _ViewStart.cshtml están accesibles desde el resto de vistas.

Pero que no cunda el pánico ;) Recordad que si queremos reutilizar ese tipo de código entre varias vistas, siempre podemos utilizar cualquiera de los mecanismos que ASP.NET Core MVC proporciona para ello: vistas parciales, tag helpers, helpers HTML o view components.

Publicado en: www.variablenotfound.com.

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

Variable not found

Enlaces interesantes 381

noviembre 25, 2019 08:11

Enlaces interesantesAhí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

ASP.NET Core / ASP.NET

Azure / Cloud

Conceptos / Patrones / Buenas prácticas

Data

Machine learning / IA / Bots

Web / HTML / CSS / Javascript

Visual Studio / Complementos / Herramientas

Xamarin

Otros

Publicado en Variable not found.

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

Coding Potions

Técnicas para mejorar tu nivel de programación

noviembre 24, 2019 12:00

Introducción

Da igual que seas programador desde hace varios años o que acabes de empezar con la programación porque siempre se puede mejorar eso es así. Si consideras que sabes todo de algo estás muy equivocado, siempre queda lugar para el aprendizaje.

Lo primero que tienes que tener claro es que la mejor forma de mejorar como programador es programando. Si, suena obvio, pero es que hay veces en las que nos centramos en intentar aprender todo a nivel teórico pero sin aplicarlo luego en cosas prácticas.

Con esto no quiero decir que no tengas que estudiar teoría o leer libros de programación, simplemente digo que para afianzar conocimientos lo mejor es ponerlos en práctica.

En ese artículo voy a contar técnicas que uso y recomiendo para prácticar programación y mejorar tu nivel como programador a nivel práctico.

Hackathons

Hackathon

Los hackathons son pruebas de programación a resolver en un tiempo determinado, por equipos normalmente, (de 2 a 6 personas aunque también los hay individuales) en los que se plantea un reto o problema a resolver mediante algoritmos de programación.

Usualmente duran unas horas pero también los hay que duran 24 horas o 48 horas incluso.

Los hackathons pueden ser presenciales u online. Encuentra un hackathon cerca de tu ciudad o participa en un evento online desde cualquier lugar mediante esta página: Hackathon

Lo bueno de los hackathons es que te enfrentas a un desafío de programación limitado por tiempo por lo que te sirve para aprender a trabajar bajo presión, a pensar rápido y a trabajar mejor con compañeros de equipo.

Además, te van a venir bien para ampliar el currículum ya que hoy en día en las empresas se valora mucho una actitud proactiva y con curiosidad en el mundo de la programación.

Lo bueno es que si el hackathon es presencial te va a servir para intercambiar conocimientos con los programadores de otros equipos y a conocer nuevas amistades.

Existen también hackathons organizados por empresas en los que suelen dar premios en metálico o becas a los equipos ganadores.

En mi caso me presente al hackathon de Google en mi universidad en el que me tuve que enfrentar a un problema sin solución (no existe solución pero hay que buscar un algoritmo que se acerque lo máximo posible). En cuento a resultados no se me dió muy bien pero me gustó la experiencia y volvería a repetir.

Ejercicios de programación online tipo codewars

Existen páginas en Internet que proponen pequeños retos o problemas de programación. Lo bueno de este método es que lo puedes hacer desde donde quieras y a tu ritmo.

En muchas de ellos te dejan incluso escoger el lenguaje de programación que prefieras así que no importa el lenguaje que uses. Además te permiten seleccionar la dificultad de los ejercicios para que sea progresivo y no te cueste mucho al principio.

Voy a enseñarte las páginas que recomiendo yo de ejercicios:

Codewars

Codewars

https://www.codewars.com/

  • Puedes elegir lenguaje de programación
  • Se acumula tu progreso y recibes puntos por los ejercicios
  • Puedes realizar problemas de otros usuarios
  • Tiene editor de código online y puedes ver el resultado en tiempo real

Coding game

Coding game

https://www.codingame.com/start

  • Muy ameno porque practicas jugando. Los ejercicios están basados en pequeños videojuegos por lo que es mucho más visual y entretenido
  • Competiciones entre usuarios. Son una especie de hackathons en las que se pone un problema y la gente compite para sacar más puntuación
  • Posibilidad de elegir entre varios lenguajes de programación y editor online.

Top Coder

Top coder

https://www.topcoder.com/challenges/

  • Más enfocada a la competición entre usuarios
  • Desafíos con premios en dinero
  • Problemas más complejos con enunciados más largos y algoritmos más sofisticados para los usuarios más exigentes

Side projects

Side projects

Para mí esta es una de las mejores técnicas. Se trata de hacer pequeños proyectos en tu tiempo libre con el fin de seguir aprendiendo. Te puede servir como excusa para probar lenguajes o frameworks que no has usado nunca.

Los proyectos te los pones tu mismo por lo que siempre intenta elegir proyectos sobre temas que te gusten o proyectos que necesites realizar para tu día a día. También puedes hacer proyectos chorra que no sirvan para nada, pero ya que te pones a hacerlo por lo menos búscale un uso.

Ponte que necesitas una app o web para tomar notas. Qué mejor forma de aprender que hacerlo tu mismo. Vas a poder crear la app o web justo con las cosas que necesitas, completamente a tu gusto y encima por el camino vas a aprender mucho de programación.

Lo que sería ideal sería que esos proyectos fueran open source para que la comunidad lo pueda usar o proponerte cambios que te van a venir bien para seguir mejorando tu nivel de programación.

Además de aprender y mejorar como programador, vas a poder aprender cómo gestionar proyectos, planificación de los mismos, publicación. etc.

Yo lo que recomiendo es que empieces con proyectos que te gusten pero que sean asequibles para que no te agobies y lo dejes todo a medias. Es mejor dedicar poco tiempo y varios días a los proyectos que intentar hacerlo todo de una.

Si todavía no sabes qué hacer te recomiendo empezar por crear tu propia página web a modo de currículum. Puedes diseñarla tu mismo o puedes coger un diseño de internet que te guste. Con tu página vas a poder empezar a mostrar tu talento a los demás y te va a venir bien para enseñar a las empresas.

Entrevistas de trabajo

Entrevistas de trabajo

La prueba definitiva. Normalmente en las entrevistas de trabajo te plantearán una serie de problemas o ejercicios relacionados con la programación para ver tu nivel y decidir si estás capacitado para el puesto.

Si has ido a hackathons y has estado haciendo ejercicios de las páginas anteriores, estarás más preparado que los otros candidatos y por lo tanto tendrás más probabilidades de entrar que los demás.

El nivel de la prueba dependerá del puesto al que presentes candidatura y de la empresa, incluso hay empresas que no hacen pruebas técnicas demasiado exigentes o incluso ni las hacen directamente.

Intenta no ponerte nervioso porque además de valorar tu nivel técnico de programación, van a estar pendientes de tu actitud, valores e incluso de tu curiosidad de aprendizaje. Se valora muy bien que tengas ganas de aprender.

Conclusiones

Como he dicho al principio la mejor forma de afianzar conocimientos es llevarlos a la práctica, pero tampoco te agobies, no es obligatorio que hagas todo y practiques todo y todos los días.

Aunque en este sector tenemos que estar en constante aprendizaje y evolución, no significa que tengas que destinar todo tu tiempo libre a aprender. Con que destines una pequeña parte de tus ratos libres cada cierto tiempo es suficiente, simplemente para no perder la práctica y no quedarnos oxidados.

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

Blog Bitix

Extraer información del navegador del usuario en el servidor de una aplicación web Java

noviembre 23, 2019 11:15

Java

En una aplicación web dependiendo de las características del cliente si es de escritorio, móvil u otro dispositivo y tamaño, lenguaje o dependiendo de la ubicación mediante su dirección IP puede servirse diferente contenido adaptado a las propiedades del cliente.

La comprobación desde el navegador se realiza con JavaScript, con la propiedad window.navigator.userAgent se obtienen propiedades básicas del agente del usuario como navegador, versión y plataforma. Con la librería JavaScript Modernizr se obtiene las características que soporta el navegador. Pero el código JavaScript se ejecuta en el cliente y la primera petición de un usuario llega al servidor cuando aún no se ha cargado ninguna página ni código JavaScript, sólo se dispone del agente de usuario, lenguaje preferido y dirección IP con lo que es necesario una librería en el lado de servidor que procese el agente del usuario u otra información que este envíe para actúa en consecuencia.

En la primera petición de un usuario a una aplicación Java con la librería browscap-java es posible procesar el agente del usuario y conocer su navegador y versión, tipo de dispositivo, plataforma y versión. Con esta información la aplicación es capaz adaptar el contenido al cliente, por ejemplo si se trata de una versión antigua de un navegador al que hay que seguir ofreciendo soporte porque algunos usuarios siguen usándolo y para el que hay que devolver un JavaScript especial por no soportar algunas características de navegadores más modernos.

En este ejemplo se hace uso de la librería browscap-java y se imprime en la salida de la aplicación la información que esta proporciona interpretando la cadena del agente del usuario. Se observa que en mi caso uso GNU/Linux, Firefox en la versión 70 de la versión de escritorio. Los navegadores envían una cabecera del protocolo HTTP, User-Agent, en la que incluyen información y la firma del navegador o agente del usuario. En el servidor para recuperar la cabecera basta con utilizar el método HttpServletRequest.getHeader() o usando Spring MVC con la anotación @RequestHeader.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package io.github.picotodtdev.blogbitix.useragent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.ResponseBody;
import com.blueconic.browscap.Capabilities;
import com.blueconic.browscap.UserAgentParser;
import com.blueconic.browscap.UserAgentService;
@Controller
public class IndexController {
@Autowired
private UserAgentParser userAgentParser;
@GetMapping("/")
@ResponseBody
public String index(@RequestHeader("User-Agent") String userAgent) {
Capabilities capabilities = userAgentParser.parse(userAgent);
String browser = capabilities.getBrowser();
String browserType = capabilities.getBrowserType();
String browserMajorVersion = capabilities.getBrowserMajorVersion();
String deviceType = capabilities.getDeviceType();
String platform = capabilities.getPlatform();
String platformVersion = capabilities.getPlatformVersion();
return String.format("User-Agent: %s\nBrowser: %s, Type: %s, Version: %s, Device: %s, Platform: %s, PlatformVersion: %s", userAgent, browser, browserType, browserMajorVersion, deviceType, platform, platformVersion);
}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
plugins {
id 'org.springframework.boot' version '2.2.1.RELEASE'
id 'io.spring.dependency-management' version '1.0.8.RELEASE'
id 'java'
id 'application'
}
group = 'io.github.picotodtdev.blogbitix.useragent'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
application {
mainClassName = 'io.github.picotodtdev.blogbitix.useragent.Main'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'com.blueconic:browscap-java:1.2.13'
}

Accediendo a la dirección del controlador http://localhost:8080 se obtienen los siguientes resultados dependiendo de con que navegador se acceda, en estos ejemplos con Firefox de escritorio, con Chrome de escritorio, y con Firefox de Android.

1
2
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:70.0) Gecko/20100101 Firefox/70.0
Browser: Firefox, Type: Browser, Version: 70, Device: Desktop, Platform: Linux, PlatformVersion: Unknown
1
2
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36
Browser: Chrome, Type: Browser, Version: 78, Device: Desktop, Platform: Linux, PlatformVersion: Unknown
1
2
User-Agent: Mozilla/5.0 (Android 7.0; Mobile; rv:68.0) Gecko/68.0 Firefox/68.0
Browser: Firefox, Type: Browser, Version: 68, Device: Mobile Phone, Platform: Android, PlatformVersion: 7.0

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradlew run.

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

Blog Bitix

Extraer información del navegador del usuario desde Java en una aplicación web

noviembre 23, 2019 11:15

Java

En una aplicación web dependiendo de las características del cliente si es de escritorio, móvil u otro dispositivo y tamaño, lenguaje o dependiendo de la ubicación mediante su dirección IP puede servirse diferente contenido adaptado a las propiedades del cliente.

La comprobación desde el navegador se realiza con JavaScript, con la propiedad window.navigator.userAgent se obtienen propiedades básicas del agente del usuario como navegador, versión y plataforma. Con la librería JavaScript Modernizr se obtiene las características que soporta el navegador. Pero el código JavaScript se ejecuta en el cliente y la primera petición de un usuario llega al servidor cuando aún no se ha cargado ninguna página ni código JavaScript, sólo se dispone del agente de usuario, lenguaje preferido y dirección IP con lo que es necesario una librería en el lado de servidor que procese el agente del usuario u otra información que este envíe para actúa en consecuencia.

En la primera petición de un usuario a una aplicación Java con la librería browscap-java es posible procesar el agente del usuario y conocer su navegador y versión, tipo de dispositivo, plataforma y versión. Con esta información la aplicación es capaz adaptar el contenido al cliente, por ejemplo si se trata de una versión antigua de un navegador al que hay que seguir ofreciendo soporte porque algunos usuarios siguen usándolo y para el que hay que devolver un JavaScript especial por no soportar algunas características de navegadores más modernos.

En este ejemplo se hace uso de la librería browscap-java y se imprime en la salida de la aplicación la información que esta proporciona interpretando la cadena del agente del usuario. Se observa que en mi caso uso GNU/Linux, Firefox en la versión 70 de la versión de escritorio. Los navegadores envían una cabecera del protocolo HTTP, User-Agent, en la que incluyen información y la firma del navegador o agente del usuario. En el servidor para recuperar la cabecera basta con utilizar el método HttpServletRequest.getHeader() o usando Spring MVC con la anotación @RequestHeader.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package io.github.picotodtdev.blogbitix.useragent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.ResponseBody;
import com.blueconic.browscap.Capabilities;
import com.blueconic.browscap.UserAgentParser;
import com.blueconic.browscap.UserAgentService;
@Controller
public class IndexController {
@Autowired
private UserAgentParser userAgentParser;
@GetMapping("/")
@ResponseBody
public String index(@RequestHeader("User-Agent") String userAgent) {
Capabilities capabilities = userAgentParser.parse(userAgent);
String browser = capabilities.getBrowser();
String browserType = capabilities.getBrowserType();
String browserMajorVersion = capabilities.getBrowserMajorVersion();
String deviceType = capabilities.getDeviceType();
String platform = capabilities.getPlatform();
String platformVersion = capabilities.getPlatformVersion();
return String.format("User-Agent: %s\nBrowser: %s, Type: %s, Version: %s, Device: %s, Platform: %s, PlatformVersion: %s", userAgent, browser, browserType, browserMajorVersion, deviceType, platform, platformVersion);
}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
plugins {
id 'org.springframework.boot' version '2.2.1.RELEASE'
id 'io.spring.dependency-management' version '1.0.8.RELEASE'
id 'java'
id 'application'
}
group = 'io.github.picotodtdev.blogbitix.useragent'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
application {
mainClassName = 'io.github.picotodtdev.blogbitix.useragent.Main'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'com.blueconic:browscap-java:1.2.13'
}

Accediendo a la dirección del controlador http://localhost:8080 se obtienen los siguientes resultados dependiendo de con que navegador se acceda, en estos ejemplos con Firefox de escritorio, con Chrome de escritorio, y con Firefox de Android.

1
2
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:70.0) Gecko/20100101 Firefox/70.0
Browser: Firefox, Type: Browser, Version: 70, Device: Desktop, Platform: Linux, PlatformVersion: Unknown
1
2
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36
Browser: Chrome, Type: Browser, Version: 78, Device: Desktop, Platform: Linux, PlatformVersion: Unknown
1
2
User-Agent: Mozilla/5.0 (Android 7.0; Mobile; rv:68.0) Gecko/68.0 Firefox/68.0
Browser: Firefox, Type: Browser, Version: 68, Device: Mobile Phone, Platform: Android, PlatformVersion: 7.0

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradlew run.

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

Fixed Buffer

Novedades de C# 8: Pattern Matching

noviembre 19, 2019 09:00

Imagen ornamental para la entrada de C#8 pattern matching

En la última entrada hablamos del nuevo IAsyncEnumerable, y hoy es el turno de ver entra novedad que nos trae c#8, el ‘pattern matching’. Esta es una característica muy interesante y muy útil que nos puede ahorrar complejidad a la hora de escribir el código, y un buen número de líneas dentro de él.

Por si aún no conoces cómo funciona el pattern matching en C# o no te suena de que estoy hablando, debes saber que es una característica que se introdujo en C# 7 y que nos permitía reconocer objetos tipados dentro de otro tipo y castear directamente al tipo si se cumple. Podríamos decir que es una especia de ‘is’ seguido de un ‘as’:

//C# 6
if (dato is string)
{
    var valor = dato as string;
    Console.WriteLine("Dato es un string y su valor es: " + valor);
}

//C#7
if (dato is string valor)
{
    Console.WriteLine("Dato es un string y su valor es: " + valor);
}

Pattern matching en C# 8

Con la llegada de C# 8, esto ha mejorado aun más, y ha añadido algunas opciones bastante interesantes:

  • Expresiones switch
  • Switch con propiedades
  • Switch con tuplas
  • Switch posicional

Vamos a verlos en profundidad:

Expresiones switch

Con los cambios de C# 7 podemos utilizar un pattern matching como el que hemos visto arriba en un ‘switch’, pero gracias a esta nueva característica de C# 8, vamos a poder crear expresiones para los switch. Las expresiones reemplazan el ‘case :’ y el ‘break’ por ‘=>’ y el ‘defaul :’ por ‘_’, además, el objeto sobre el que vamos a hacer el switch va delante de este (y no detrás como hasta ahora) para indicar que se trata de una expresión. Vamos a ver un ejemplo:

//Version en C# 7
public static char GetLetraInstruccion(int code)
{
    switch (code)
    {
        case 1:
            return 'a';
        case 2:
            return 'b';
        default:
            return '_';
    }
}
//Version en C# 8
public static char GetLetraExpresion(int code)
{
    return code switch
    {
        1 => 'a',
        2 => 'b',    
        _ => '_' // Default
    };
}

//Versión en C# 8 con arrow functions
public static char GetLetraExpresion2(int code) => code switch
{
    1 => 'a',
    2 => 'b',
    _ => '_' //Default
};

Como puedes comprobar, esta versión de C# y su pattern matching junto a las arrow fuctions nos deja un código mucho más limpio.

Switch con propiedades

Utilizando el pattern matching de C# 7, podíamos utilizar las propiedades del objeto para aplicar condiciones extra en los switch:

public class Direccion
{
    public string Pais { get; set; }
    public string Comunidad { get; set; }
}
//...
[Flags]
public enum Idiomas
{
    Castellano = 1,
    Gallego = 2,
    Euskera = 4,
    Catalan = 8,
}
//...
//Version en C# 7
public static Idiomas GetIdiomasC7(Direccion direccion)
{
    switch (direccion)
    {
        case Direccion dir when dir.Pais == "ES" && dir.Comunidad == "Galicia":
            return Idiomas.Castellano | Idiomas.Gallego;
        case Direccion dir when dir.Pais == "ES" && dir.Comunidad == "Pais Vasco":
            return Idiomas.Castellano | Idiomas.Euskera;
        case Direccion dir when dir.Pais == "ES" && dir.Comunidad == "Cataluña":
            return Idiomas.Castellano | Idiomas.Catalan;
        case Direccion dir when dir.Pais == "ES":
            return Idiomas.Castellano;
        default:
            return Idiomas.Ingles;
    }
}

Esto ha cambiado gracias a C# 8 y su pattern matching, gracias al reconocimiento de propiedades y a las expresiones switch, vamos a poder dejar un código más limpio accediendo directamente a sus propiedades:

public static Idiomas GetIdiomasC8(Direccion direccion)
{
    return direccion switch
    {
        { Pais: "ES", Comunidad: "Galicia" } => Idiomas.Castellano | Idiomas.Gallego,
        { Pais: "ES", Comunidad: "Pais Vasco" } => Idiomas.Castellano | Idiomas.Euskera,
        { Pais: "ES", Comunidad: "Cataluña" } => Idiomas.Castellano | Idiomas.Catalan,
        { Pais: "ES" } => Idiomas.Castellano,
        _ => Idiomas.Ingles //Default
    };
}

Con esto que hemos visto, el código queda mucho más limpio y claro ¿verdad? Hasta este momento solo hemos visto cambios que son azúcar sintáctico, son útiles, son bonitos, pero no aportan nada nuevo respecto a lo que ya teníamos disponible en la versión anterior del C#. Entonces, ¿qué trae nuevo el pattern matching de C# 8? ¿Hay algo que realmente sea nuevo? ¡Sí!, vamos con ello.

Switch con tuplas

Hasta ahora, un switch era un flujo de control que se operaba solo sobre una variable. Si necesitábamos conjugar dos o más variables solo nos quedaba utilizar un if, pero if else tiene un problema de rendimiento cuando aumenta mucho de tamaño ya que para llegar al último tiene que ir comprobando todos los anteriores, cosa que con un switch no pasa.

Como dato, aproximadamente a partir de 5 posibles caminos dentro de un if else, ya es más ‘barato’ en términos de CPU el utilizar un switch que siempre va a tardar lo mismo independientemente del número de posibles caminos y cual sea el elegido. Da igual que sea el primero o el último.

Precisamente esta nueva característica una de las dos que se han introducido en C# 8 y su pattern matching, permitiéndonos utilizar una tupla como objeto para el switch, y una tupla como condición, fíjate que limpito queda ahora un «Piedra papel o tijera, lagarto Spock«:

public static string PiedraPapelTijeraLagartoSpock(string playerA, string playerB)
{
    return (playerA, playerB) switch
    {
        ("piedra", "papel") => "Gana papel",
        ("piedra", "tijera") => "Gana piedra",
        ("piedra", "lagarto") => "Gana piedra",
        ("piedra", "spock") => "Gana Spock",
        ("papel", "piedra") => "Gana papel",
        ("papel", "tijera") => "Gana tijera",
        ("papel", "lagarto") => "Gana lagarto",
        ("papel", "spock") => "Gana papel",
        ("tijera", "piedra") => "Gana piedra.",
        ("tijera", "papel") => "Gana tijera",
        ("tijera", "lagarto") => "Gana tijera.",
        ("tijera", "spock") => "Gana Spock",
        ("lagarto", "piedra") => "Gana piedra",
        ("lagarto", "papel") => "Gana lagarto",
        ("lagarto", "tijera") => "Gana tijero",
        ("lagarto", "spock") => "Gana lagarto",
        ("spock", "piedra") => "Gana Spock.",
        ("spock", "papel") => "Gana papel",
        ("spock", "tijera") => "Gana Spock.",
        ("spock", "lagarto") => "Gana lagarto",
        (_, _) => "empate" //Default
    };
}

En el caso anterior, como en todos los que se usen expresiones switch, ‘_’ significa cualquier valor, por lo que puedes tener errores porque exista algún código que es inalcanzable si las pones en el orden erróneo (por ejemplo {_,_} el primero de todos).

Vale, ya hemos visto una utilidad que realmente es nueva, pero… ¿Quedan más? Por supuesto, vamos a por la última.

Switch posicional

Una característica de C# 7 era la deconstrucción de clases. Esta característica nos va a permitir si tenemos un método Deconstruct en la clase que sea accesible, obtener una tupla con los valores que generemos con la deconstrucción. Por ejemplo:

public class Posicion
{
    private readonly double _x;
    private readonly double _y;

    public Posicion(double x, double y)
    {
        _x = x;
        _y = y;
    }

    public void Deconstruct(out double x, out double y)
    {
        x= _x;
        y= _y;
    }
}

//
var posicion = new Posicion(10,20);
var (x, y) = posicion;

En el caso anterior veíamos que podemos trabajar con tuplas perfectamente, y esto es una tupla… ¿Dónde está lo nuevo?

Precisamente el caso anterior trabajábamos con diferentes casuísticas de tuplas, pero ahora vamos a poder trabajar con los valores de la tupla para hacer la condición de nuestro switch, por ejemplo:

public enum Cuadrantes
{
    SuperiorDerecho,
    SuperiorIzquierdo,
    InferiorIzquierdo,
    InferiorDerecho,
    Origen
}

public static Cuadrantes GetCuadrante(Posicion posicion)
{
    return posicion switch
    {
        var (x, y) when x > 0 && y > 0 => Cuadrantes.SuperiorDerecho,
        var (x, y) when x < 0 && y > 0 => Cuadrantes.SuperiorIzquierdo,
        var (x, y) when x < 0 && y < 0 => Cuadrantes.InferiorIzquierdo,
        var (x, y) when x > 0 && y < 0 => Cuadrantes.InferiorDerecho,
        _ => Cuadrantes.Origen
    };
}

Si te detienes en el código anterior, puedes comprobar que estamos haciendo un switch deconstruyendo el objeto, y después filtrándolo por sus diferentes propiedades deconstruidas.

En realidad, esta nueva característica del pattern matching de C# 8 es igual que al anterior ya que ambas trabajan con tuplas (de hecho, en el caso anterior también podemos utilizar variables y usar when como en ese), pero aportándonos algo de azúcar sintáctico para ahorrarnos tener que añadir un paso extra para obtener la tupla.

Puede que a simple vista parezca algo raro y poco útil, es la reacción habitual a los cambios. Una vez que te acostumbres a utilizar las expresiones switch y el resto de las características, no vas a querer dejar de usarlas ya que deja un código mucho más limpio y claro.

Si tienes cualquier duda o comentario, no te lo pienses y adelante!!

**La entrada Novedades de C# 8: Pattern Matching se publicó primero en Fixed Buffer.**

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

Variable not found

Novedades en la evaluación en cliente de EF Core 3.0

noviembre 19, 2019 07:05

Entity Framework CoreHace algunos meses hablábamos del peligro que suponía la la evaluación en cliente de Entity Framework Core, una característica que, por defecto, venía activada de serie. En el post explicábamos los problemas que podían dar, cómo podíamos detectar su uso e incluso impedirlo para evitar males mayores.

Pues bien, esto ha cambiado a partir de la versión 3.0, en mi opinión, hacia una dirección bastante más correcta. Veamos en qué consisten estos cambios.

El comportamiento antes de EF Core 3.0

Como sabemos, en versiones anteriores a la 3.0 de Entity Framework Core, una consulta como la siguiente funcionaba perfectamente:
// Consulta:
public List<Friend> GetAdults()
{
var adults = ctx.Friends.Where(f => IsAdult(f));
return adults.ToList();
}

private static bool IsAdult(Friend f) => f.Birthdate < DateTime.Now.AddYears(-18);
El problema que tenía ese código es que, dado que la expresión IsAdult() no tenía sentido para el proveedor de datos, ésta era evaluada en cliente para todas las filas obtenidas en la consulta, que en
este caso, dado que no hay otros criterios de filtrado, serían absolutamente todas.

El framework ejecutaría la consulta, recorrería todas las filas de la tabla, materializaría las entidades y descartaría las que no cumplieran las condiciones, pero el destrozo a nivel de rendimiento ya se habría hecho.

El comportamiento a partir de EF Core 3.0

A partir de Entity Framework Core 3.0, la ejecución de una consulta como la mostrada anteriormente resultará en una excepción InvalidOperationException como la mostrada en la siguiente captura de pantalla:

Excepción lanzada
System.InvalidOperationException: 'The LINQ expression 'Where<Friend>( source: DbSet<Friend>, predicate: (f) => Program.IsAdult(f))' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.'
El error indica que la expresión IsAdult() no puede ser traducida al "idioma" del almacén de datos (por ejemplo, no existe algo como IsAdult() en SQL), y nos invita a replantear la consulta de forma que el motor pueda entenderla, o bien mover esa condición al lado cliente para que pueda ser evaluada con éxito.

Hay muchos escenarios en los que no será posible aplicar la primera solución, aunque en nuestro caso podría ser muy sencillo porque podemos convertirlo a operaciones soportadas por el almacén de datos:
// Sí, no es muy exacto, pero ya me entendéis... ;)
var adults = ctx.Friends.Where(f => DateTime.Today.Year - f.Birthdate.Year >= 18);
O también podríamos utilizar las extensiones DbFunctions como se muestra a continuación:
var adults = ctx.Friends.Where(f =>
EF.Functions.DateDiffYear(f.Birthdate, DateTime.Now) >= 18
);
La segunda sugerencia es más violenta, pues implica la lectura de todas las entidades y su posterior cribado. Vaya, algo similar a lo que se hacía con versiones anterior a EF Core 3.0, lo que ocurre es que seríamos nosotros los que explícitamente se lo estaríamos indicando:
var adults = ctx.Friends
.AsEnumerable() // La consulta es ejecutada aquí
.Where(f => IsAdult(f)); // El filtro se aplica sobre las entidades materializadas
Entonces, ¿esto significa que en las consultas no podemos incluir ningún tipo de expresiones evaluadas en cliente? Pues no. Hay un escenario en el que sí pueden ser incluidas, y puede ser bastante útil: las expresiones en proyecciones finales.

Por ejemplo, imaginad que queremos obtener el nombre de nuestros amigos que cumplan una condición determinada, junto con un booleano indicando si es mayor de edad. La consulta podría ser como la siguiente:
public class FriendServices
{
public void ShowSomeFriends()
{
using var ctx = new FriendsContext();
var query = ctx.Friends
.Where(f => f.Name.StartsWith("J"))
.Select(f=> new { Name=f.Name, IsAdult = IsAdult(f) });

foreach (var friend in query)
{
Console.WriteLine(friend.Name + " " + friend.IsAdult);
}
}

private static bool IsAdult(Friend f) => f.Birthdate < DateTime.Now.AddYears(-18);
}
Aquí son importantes dos detalles que conviene remarcar.

En primer lugar, la expresión en cliente sólo podrá ser utilizada si se trata de la proyección final. Tiene bastante sentido: esta proyección se realiza en el momento de la materialización a objetos del CLR, por lo que ya nos encontramos en el lado cliente, y ya podemos utilizar expresiones .NET.

Sin embargo, dado que las proyecciones intermedias son resueltas en el almacén de datos, no sería posible utilizar expresiones evaluables únicamente en cliente. Por esta razón, el siguiente ejemplo fallaría en tiempo de ejecución:
var query = ctx.Friends
.Select(f => new {f.Name, f.Birthdate, IsAdult = IsAdult(f)})
.Where(f=>f.IsAdult)
...
Otro aspecto importante a tener en cuenta es que el árbol de expresión introducirá referencias hacia los objetos o métodos incluidos en la expresión cliente, y esto puede conducir a sutiles memory leaks. Por ejemplo, si en el caso anterior el método IsAdult() fuera de instancia, el árbol de expresión contendría una referencia a dicho método y, por tanto, a la instancia de FriendServices que lo aloja.

De hecho, EF Core detectará esta circunstancia y hará que la consulta rompa en ejecución por este motivo:

Excepción InvalidOperationException por posible memory leak
System.InvalidOperationException: 'Client projection contains reference to constant expression of type: MyConsoleApp.FriendServices. This could potentially cause memory leak.'
Publicado en Variable not found.

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

Variable not found

Enlaces interesantes 380

noviembre 18, 2019 07:27

Enlaces interesantesAhí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

ASP.NET Core / ASP.NET

Azure / Cloud

Conceptos / Patrones / Buenas prácticas

Data

Machine learning / IA / Bots

Web / HTML / CSS / Javascript

Visual Studio / Complementos / Herramientas

Xamarin

Otros

Publicado en Variable not found.

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

proyectos Ágiles

Debate sobre métodos ágiles vs métodos tradicionales

noviembre 12, 2019 05:00

Debate sobre métodos ágiles vs métodos tradicionales en La Salle Barcelona con motivo del 20 aniversario del MUDP.

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

Fixed Buffer

Novedades de C# 8: IAsyncEnumerable

noviembre 05, 2019 09:05

La imagen muestra el logotipo de c# 8.0 para la entrada sobre IAsyncEnumerable<T>

¿Y qué es eso de IAsyncEnumerable? Hace algo más de un mes que se libreró .Net Core 3.0 y ya hemos hablado sobre (abre en una nueva pestaña)» href=»https://www.fixedbuffer.com/index.php/worker-service-como-crear-un-servicio-net-core-3-multiplataforma/»>algunas de las novedades que nos trae (que no son pocas…). Pero sin duda, una de las principales novedades que se liberaron junto a .Net Core 3, fue C# 8.0.

C# 8 nos ofrece una interesante cantidad de novedades de las que vamos a ir hablando durante las próximas semanas:

  • Tipos nullables
  • Rangos e índices
  • Declaraciones using
  • Interfaces con implementaciones por defecto
  • Reconocimiento de patrones
  • Asignaciones de uso combinado

Pero para mí el más importante en cuanto al impacto que puede tener en el rendimiento de nuestra aplicación, sin duda alguna, es «IAsyncEnumerable«.

Hay otros cambios que afecta mucho más al rendimiento, como Span<T> por ejemplo, pero es más difícil que lo usemos en nuestro día a día que IAsyncEnumerable

Hasta ahora, no teníamos manera de iterar un objeto de manera asíncrona medida que se va generando. ¿Qué quiere decir esto? ¡Pues fácil! Imagina que tenemos un método que nos devuelve una colección de 100 objetos y que, por ejemplo, tarda en generar cada objeto 1 segundo. Una vez que tenemos la colección lista, la iteramos con un foreach que tarda otro segundo en realizar un ciclo. Algo así:

async Task<IEnumerable<int>> GetCountIEnumerableAsync()
{
    var list = new List<int>();
    for (int i = 0; i < 100; i++)
    {
        await Task.Delay(1000); 
        list.Add(i);
    }

    return list;
}

//....

foreach (var valor in await GetCountIEnumerableAsync())
{
    //Acción que tarda un segundo en ejecutarse
}

Con el código anterior, asumiendo 1 segundo para generar cada objeto de la colección y un segundo para procesarlo, vamos a tardar 200 segundos en ejecutar el foreach completo. Hasta ahora, no nos quedaba otra que gestionar nosotros la situación para obtener los datos por un lado y procesarlos por otro para poder darle salida nada más obtenerlos, obligándonos a implementar una lógica extra que hay que mantener y sincronizar.

IAsyncEnumerable<T> al rescate

Como decíamos antes, con C# 8 se han introducido los streams asíncronos a través de la interfaz IAsyncEnumerable<T>. Esto lo que nos va a permitir es no tener que esperar a que la colección se obtenga completa antes de empezar a iterarla. Volviendo al ejemplo anterior, si en obtener cada dato tardamos un segundo, pero mientras obtenemos un nuevo dato procesamos el anterior, en vez de 200 segundos, solo vamos a tardar 101 (aproximadamente). Vamos a verlo con un poco de código:

async IAsyncEnumerable<int> GetCountIAsyncEnumerableAsync()
{
    for (int i = 0; i < 100; i++)
    {
        await Task.Delay(1000); 
        yield return i;
    }
}

//....

await foreach (var valor in GetCountIAsyncEnumerableAsync())
{
    //Acción que tarda un segundo en ejecutarse
}

Este código va a ir despachando cada uno de los objetos a medida que se van generando nuestro método «GetCountIAsyncEnumerableAsync». Como puedes comprobar esto es infinitamente más fácil y más legible. Imagina si tuviésemos que gestionar dos hilos independientes, uno para adquirir datos y otro para procesarlos…

Si los datos provienen de algún servicio externo como una base de datos o un API Rest (o cualquier otro origen en general). Como resultado, vamos a obtener una mejora bastante importante gracias a esto. Ya sea por aumentar el rendimiento, o por facilitarnos el código. ¿Aun no tienes claro cómo funciona?

Vamos a ver un ejemplo claro

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace IAsyncEnumerable
{
    internal static class Program
    {
        private static async Task Main(string[] args)
        {
            Console.WriteLine("Ejecutando foreach con Task<IEnumerable<int>>");
            foreach (var valor in await GetCountIEnumerableAsync())
            {
                Console.WriteLine($"Procesado el valor {valor}");
            }

            Console.WriteLine("Ejecutando foreach con IAsyncEnumerable<int>");
            await foreach (var valor in GetCountIAsyncEnumerableAsync())
            {
                Console.WriteLine($"Procesado el valor {valor}");
            }

            Console.Read();
        }

        private static async IAsyncEnumerable<int> GetCountIAsyncEnumerableAsync()
        {
            for (var i = 0; i < 3; i++)
            {
                await Task.Delay(1000);
                Console.WriteLine($"Añadido valor {i}");
                yield return i;
            }
        }

        private static async Task<IEnumerable<int>> GetCountIEnumerableAsync()
        {
            var list = new List<int>();
            for (var i = 0; i < 3; i++)
            {
                await Task.Delay(1000);
                Console.WriteLine($"Añadido valor {i}");
                list.Add(i);
            }
            return list;
        }
    }
}

En el código anterior, simplemente hemos implementado ambos casos, uno con IAsyncEnumerable<int> y otro con Task<IEnumerable<int>>. Si ejecutamos el código la salida que obtenemos es algo como esto:

La imagen muestra la salida por consola del ejemplo anterior, con y sin IAsyncEnumerable

Con la imagen se ve más claro cuál es el cambio de funcionamiento que nos porta esta nueva característica, pero… ¡Si tienes alguna duda, puedes dejar un comentario!

Durante las próximas semanas vamos a seguir hablando de las interesantes novedades que nos aporta C# 8. No te lo pierdas, realmente es muy interesante

**La entrada Novedades de C# 8: IAsyncEnumerable<T> se publicó primero en Fixed Buffer.**

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

Meta-Info

¿Que es?

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

rss subscripción

Puedes utilizar las siguientes imagenes para enlazar PlanetaCodigo:
planetacodigo

planetacodigo

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

Idea: Juanjo Navarro

Diseño: Albin