Mariano Guerra: Creemos en la Web: Datos por favor, pero después

En el capítulo anterior vimos como pedirle datos al usuario, pero en el momento de validar los datos, lo hicimos simple con lógica fija en el código.

En una aplicación real, le preguntaríamos a un servicio que tiene registradas las cuentas si el usuario y la contraseña son validas.

Este tipo de servicios suelen llamarse APIs (interfaz de programación de aplicaciones en ingles).

Las APIs son como funciones pero que corren en otras computadoras, razón por la cual al llamarlas no devuelven instantáneamente si no que demoran un buen rato, así que tenemos que aprender a esperar y manejar distintos casos.

Por ejemplo cuando la solicitud funciona correctamente y cuando falla, ya sea por problemas de red, porque mandamos parámetros inválidos, porque el servicio no esta funcionando correctamente o una gran variedad de causas normalmente resumidas con "se cayo el sistema".

Empecemos cargando datos y mostrandolos, arrancamos con todo el HTML ya que no hay nada nuevo:

<!doctype html>
<html>
  <head>
    <!-- para que muestre bien tildes y caracteres especiales -->
    <meta charset="utf-8">

    <!-- para que se vea bien en Internet Explorer -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge">

    <!-- para que se vea bien en telefonos mobiles -->
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Cargando Datos</title>

    <!-- importamos vue para poder crear aplicaciones con javascript https://vuejs.org/ -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.21/vue.min.js"></script>
    <!-- importamos las hojas de estilo base bootstrap https://getbootstrap.com/ -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">

    <!-- importamos nuestro estilo especifico -->
    <link rel="stylesheet" href="/style.css">

    <!-- importamos nuestro javascript -->
    <script src="/client.js" defer></script>
  </head>
  <body class="m-3">

    <!-- la raíz de nuestra aplicación -->
    <div id="app">
      <div v-if="cargando">
        <div class="alert alert-info">
          Cargando
        </div>
      </div>
      <div v-else="">
        <table class="table table-bordered table-stripped table-hover">
          <tbody>
            <tr><td>Fecha</td><td>{{datos.fecha}}</td></tr>
            <tr><td>Numero</td><td>{{datos.numero}}</td></tr>
            <tr><td>Nombre</td><td>{{datos.nombre}}</td></tr>
          </tbody>
        </table>

        <div class="text-center">
          <div @click="recargar()" class="btn btn-primary">
            Recargar
          </div>
        </div>
      </div>
    </div>

  </body>
</html>

Notaras que hice copy/paste del capitulo anterior, lo único que cambio es lo que esta dentro del div app, pero tampoco es algo nuevo, veamoslo en detalle:

El primer div solo se muestra si el atributo cargando es true, mostrando el mensaje "Cargando".

<div v-if="cargando">
  <div class="alert alert-info">
    Cargando
  </div>
</div>

Sino (con v-else que es como el else del if en javascript) significa que ya cargamos los datos, osea que los podemos mostrar, asumimos que los datos tienen al menos 3 campos: fecha, numero y nombre.

<div v-else="">
  <table class="table table-bordered table-stripped table-hover">
    <tbody>
      <tr><td>Fecha</td><td>{{datos.fecha}}</td></tr>
      <tr><td>Numero</td><td>{{datos.numero}}</td></tr>
      <tr><td>Nombre</td><td>{{datos.nombre}}</td></tr>
    </tbody>
  </table>

Finalmente ponemos un botón que permite recargar los datos.

<div class="text-center">
  <div @click="recargar()" class="btn btn-primary">
    Recargar
  </div>
</div>

La lógica de nuestra aplicación esta en el archivo client.js, empecemos con una base que ya conocemos:

/*globals Vue*/

function main() {
  let app = new Vue({
    el: '#app',
    methods: {
      recargar: function () {
        // cargar aca
      }
    },
    data: {
      cargando: true,
      datos: {
      }
    }
  });

  app.recargar();
}

window.addEventListener('load', main);

El código no hace nada nuevo, y de hecho no esta terminado, pero repasemos.

window.addEventListener('load', main);

Cuando la pagina termine de cargar llamamos a la función main.

La función main crea un nuevo objeto Vue, que va a funcionar en el tag con id "app", la aplicacion tiene dos atributos:

cargando
indica si la aplicación esta cargando datos
datos
los datos cargados, inicialmente vacíos

Nuestra aplicación también tiene un método, recargar, que por ahora no hace nada, osea que si probas esto la pagina se va a quedar en "Cargando" por siempre.

En el método recargar podríamos hacer algo como:

recargar: function () {
  this.datos = {nombre: "bob", numero: 42, fecha: "una fecha"};
  this.cargando = false;
}

Pero eso setearia datos instantáneamente, nunca veríamos el mensaje "Cargando", si solo hubiera una forma de pedirle al navegador que llame a una función "después"...

Obviamente dicha funcionalidad existe y suele ser muy útil, por ejemplo para mostrar o esconder mensajes después de un tiempo prudente.

Esta funcionalidad esta disponible en el objeto window y se llama setTimeout, el método recibe dos argumentos, el primero es la función a llamar, el segundo es cuanto tiene que pasar entre el momento en el que llamamos al método y cuando se va a llamar a nuestra función, en milisegundos (1 segundo son 1000 milisegundos).

recargar: function () {
  let app = this;

  function cargarFalso() {
      app.datos = {nombre: "bob", numero: 42, fecha: "una fecha"};
      app.cargando = false;
  }

  app.cargando = true;

  window.setTimeout(cargarFalso, 3000);
}

Hay un par de detalles importantes en ese código, vamos por partes:

Primero creamos una variable llamada app que tiene el mismo valor que this, porque haríamos eso? lo vamos a ver en un par de lineas.

let app = this;

Luego creamos una función llamada cargarFalso, que se llama así porque va a simular que estamos cargando los datos de un servicio esperando un poco y seteando los datos.

Lo primero para notar es que podemos crear una función dentro de otra función o método, esto lo hacemos acá por dos razones.

La primera razón es porque a esta función solo la necesitamos en el método recargar, así que hacerla "visible" al resto del código es innecesario y puede llegar a hacernos pensar después que es una función que tiene algún tipo de utilidad general cuando en realidad no la tiene fuera del método recargar.

La segunda es que queremos tener acceso a la variable app que esta disponible en los métodos de nuestra aplicación vue, las funciones "ven" las variables declaradas en su "cuerpo" (body en ingles, lo que esta entre llaves), entonces al declararla dentro del método recargar tenemos acceso a la variable sin necesidad de hacer cosas raras, ya que window.setTimeout llama a una función pero no le pasa ningún argumento.

En el cuerpo de la función cargarFalso podemos ver que seteamos el atributo datos de la variable app y aquí revelamos el misterio de la linea let app = this;, como vimos en el capitulo anterior, cada variable tiene un this implícito, su valor depende del "valor antes del punto" al llamarla, osea que cada función puede tener un valor distinto de this o ninguno si llamamos a la función directamente y no como un método.

Para asegurarnos de setear datos en el objeto adecuado, asignamos el valor de this en el método de nuestra aplicación a otra variable para que no se confunda con el this de la función cargarFalso.

No te preocupes, esta es una de las cosas mas confusas de javascript y te puede llevar una buena cantidad de tropiezos terminar de entenderlo por completo.

function cargarFalso() {
    app.datos = {nombre: "bob", numero: 42, fecha: "una fecha"};
    app.cargando = false;
}

Seteamos el atributo cargando de nuestra aplicación a true así la pagina cambia y muestra el mensaje "Cargando" hasta que la función cargarFalso sea llamada.

app.cargando = true;

Finalmente llamamos al método setTimeout del objeto window y le pasamos nuestra función a llamar y cuando la tiene que llamar (en 3000 milisegundos, es decir 3 segundos).

window.setTimeout(cargarFalso, 3000);

Si probas esto vas a ver que la pagina va a cargar, mostrar el mensaje "Cargando" y 3 segundos después va a mostrar los datos.

Si apretamos el botón "Recargar" va a mostrar el mensaje y 3 segundos después los datos.

Hagamos una pequeña modificación para que el cambio se note al hacer click en "Recargar" hagamos que el valor del campo fecha cambie.

Para obtener la fecha y hora actual existe un tipo de objeto llamado "Date" (fecha en ingles).

Cuando creamos un nuevo objeto del tipo Date el objeto que nos devuelve contiene la fecha y hora actual según el reloj de tu computadora, probemos un poco:

Creamos un nuevo objeto de tipo Date usando new:

let date1 = new Date();

Llamamos al método toString que devuelve una representación de la fecha como texto:

date1.toString();
< 'Tue Jan 01 2019 17:41:01 GMT-0300 (-03)'

Llamamos al método toLocaleString que devuelve una representación de la fecha como texto adaptada al idioma y configuración de tu computadora, en tu caso probablemente aparezca distinto al mio, que tengo un lio de configuraciones de ingles, español y alemán:

date1.toLocaleString();
< '2019-1-1 17:41:01'

El método toLocaleDateString devuelve una representación de la fecha, sin la parte del tiempo, como texto adaptada al idioma y configuración de tu computadora:

date1.toLocaleDateString();
< '2019-1-1'

El método toLocaleTimeString devuelve una representación de la fecha, solo la parte del tiempo, como texto adaptada al idioma y configuración de tu computadora:

date1.toLocaleTimeString();
< '17:41:01'

El método getDate Devuelve el día del mes.

No confundirlo con getDay que devuelve el día de la semana.

date1.getDate();
< 1

El método getMonth devuelve el numero de mes, para complicarnos la existencia enero es 0 en lugar de 1:

date1.getMonth();
< 0

El método getFullYear devuelve el año.

No confundirlo con getYear que devuelve el año menos 1900, osea que 2019 es 119`, si, no tiene sentido, como muchas cosas que vamos a aprender.

date1.getFullYear();
< 2019

El método getHours devuelve la hora.

date1.getHours();
< 17

El método getMinutes devuelve los minutos.

date1.getMinutes();
< 41

El método getSeconds devuelve los segundos.

date1.getSeconds();
< 1

Esos son los principales métodos, y luego de este breve tour alternativo volvemos a lo que nos compete, usar new Date() para mostrar la hora del momento en el que cargaron los datos:

Cambiamos cargarFalso para que en el campo fecha tenga la fecha actual en texto:

function cargarFalso() {
    let fecha = new Date(),
        fechaTexto = fecha.toLocaleString();

    app.datos = {nombre: "bob", numero: 42, fecha: fechaTexto};
    app.cargando = false;
}

Ahora al apretar "Recargar" la fecha debería cambiar indicandonos que cargo de nuevo.

Ya aprendimos mucho, como "llamar funciones después", como declarar funciones dentro de otras para tener acceso a las variables del cuerpo de la función, como no confundir el this de distintas funciones creando variables y sobre el tipo de dato Date y alguno de sus métodos, pero lo mejor esta por llegar, en la próxima vamos a usar todo esto que aprendimos para cargar los datos de un servicio/servidor/API.

El resultado esta en https://marianoguerra.github.io/creemos-en-la-web/paginas/datos-despues/2/

Podes explorar el código con las herramientas de desarrollo.

Mariano Guerra: Creemos en la Web: Datos por favor, pero validos

Ya sabemos suficiente HTML, CSS y Javascript para hacer una aplicación completa, así que empecemos y aprendamos lo que falta a medida que lo vayamos necesitando.

Gran parte de las aplicaciones implica mostrar y solicitar datos, así que probemos solicitar datos, mas conocidos como "formularios" o "forms" para los amigos.

El primero que nos vamos a encontrar en cualquier aplicación es el de autenticación, donde pedimos usuario y contraseña y lo validamos, si es valido pasamos a la siguiente pagina, si no, mostramos un mensaje de error.

Empecemos con un "esqueleto de aplicación vue", es decir, los componentes necesarios pero sin que haga algo en particular.

Para eso necesitamos un archivo html, llamemoslo index.html:

<!doctype html>
<html>
  <head>
    <!-- para que muestre bien tildes y caracteres especiales -->
    <meta charset="utf-8">

    <!-- para que se vea bien en Internet Explorer -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge">

    <!-- para que se vea bien en telefonos mobiles -->
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Login</title>

    <!-- importamos vue para poder crear aplicaciones con javascript https://vuejs.org/ -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.21/vue.min.js"></script>
    <!-- importamos las hojas de estilo base bootstrap https://getbootstrap.com/ -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">

    <!-- importamos nuestro estilo especifico -->
    <link rel="stylesheet" href="/style.css">

    <!-- importamos nuestro javascript -->
    <script src="/script.js" defer></script>
  </head>
  <body class="m-3">

    <!-- la raíz de nuestra aplicación -->
    <div id="app">
      <h1>Soy una aplicación vue.js basica</h1>
    </div>

  </body>
</html>

Si no te acordás algunos de los elementos podes repasarlos de capítulos anteriores o enterarte que yo no los escribí de memoria sino que los fui a buscar a un capitulo anterior y lo copie acá ;)

Para darle "vida" a esta aplicación vamos a necesitar un poco de Javascript, indicado por la presencia del tag <script src="/script.js"></script> en el html.

Ahora creemos un archivo llamado script.js:

/*globals Vue*/

function main(event) {
  let app = new Vue({
    el: '#app',
    data: {}
  });
}

window.addEventListener('load', main);

El código define una función llamada main (principal en ingles) que va a ser el "punto de entrada" de nuestra aplicación.

Pero cuando la llamamos?

Necesitamos llamarla cuando la pagina termine de cargar, para saber eso le podemos decir al objeto window que cuando termine de cargar llame a la función main.

En HTML/Javascript distintos elementos pueden emitir eventos para notificar de cosas que suceden, uno de ellos es la ventana que nos puede avisar cuando termino de cargar.

Para poder reaccionar a estos eventos, tenemos que "escuchar" (listen en ingles).

Los objetos que emiten eventos tienen un método llamado addEventListener (recordemos que un método es una función asociada a un objeto que podemos llamar con objeto.metodo()).

El método addEventListener ("añadir oyente de evento" en ingles) recibe dos argumentos, el primero es el nombre del evento que queremos escuchar, en este caso es el evento 'load'. El segundo argumento es la función a llamar cuando ese evento suceda. La función va a ser llamada con un argumento que contiene información sobre el evento que acaba de suceder, en nuestro caso no nos hace falta.

Cuando la pagina cargue, el objeto window (ventana) va a emitir el evento 'load' y va a buscar todas las funciones que escuchan ese evento y las va a llamar pasandoles un objeto evento con la información del evento.

En ese momento nuestra función main sera llamada, la cual va a crear un nuevo objeto de tipo Vue pasandole opciones de inicialización en un objeto, las opciones de inicialización por ahora son:

el
id del tag donde la aplicación va a correr, en nuestro caso es el id app, para indicarle que es un id le ponemos el # adelante (después vamos a ver otras formas de indicarle el tag raiz.
data
los datos iniciales de nuestra aplicación, por ahora un objeto vacio.

Por si te dio curiosidad que es el /*globals Vue*/, es para indicarle al programa que chequea errores que la variable Vue existe porque otro script la definió, así no nos dice que estamos refiriéndonos a una variable inexistente.

Acá tenes el proyecto base en glitch.com, hace click en Remix to Edit arriba a la derecha para hacerle cambios, hace click en Edit abajo a la derecha para abrir este ejemplo en una pagina completa para editarlo.

Abajo a la izquierda podes cambiar entre ver el código (Code), la aplicación (App) o ambas (Both).

Muy lindo todo pero esta aplicación no hace nada, empecemos por agregar los campos de usuario y contraseña, para eso los definimos en el estado inicial de nuestra aplicación (el campo data):

/*globals Vue*/

function main(event) {
  let app = new Vue({
    el: '#app',
    data: {
        username: '',
        password: ''
    }
  });
}

window.addEventListener('load', main);

Luego, como personas eficientes que somos, vamos a un ejemplo de bootstrap de pagina de login https://getbootstrap.com/docs/4.1/examples/sign-in/ apretamos el botón derecho sobre la pagina y elegimos la opción que dice algo así como "Ver Código Fuente" o "View Page Source" (puede variar en tu navegador), copiamos la parte del HTML que nos interesa, la pegamos en nuestro proyecto y la adaptamos a nuestras necesidades, a mi me quedo así:

<div id="app">
    <h1 class="h3 mb-3 font-weight-normal">Login</h1>

    <label for="inputUser">Usuario</label>
    <input v-model="username" type="text" id="inputUser" class="form-control" required autofocus>

    <label for="inputPassword" class="mt-3">Password</label>
    <input v-model="password" type="password" id="inputPassword" class="form-control" required>

    <button class="btn btn-primary btn-block mt-3" type="submit">Enviar</button>
</div>

Usando el atributo v-model en los tags input conectamos los valores de nuestra aplicación al HTML.

Ahora necesitamos que cuando se haga click en el botón comprobemos los valores, para eso vamos a usar el atributo @click en el tag button:

<button @click="login()" class="btn btn-primary btn-block mt-3" type="submit">Enviar</button>

Pero a que función o método login llama? al método login de nuestra aplicación, razón por la cual tenemos que definirlo en el atributo methods:

/*globals Vue*/

function main(event) {
  let app = new Vue({
    el: '#app',
    methods: {
      login: function () {
        if (this.username === 'bob' && this.password === 'secreto') {
          alert("Éxito");
        } else {
          alert("Error");
        }
      }
    },
    data: {
      username: '',
      password: ''
    }
  });
}

window.addEventListener('load', main);

Lo que login hace por ahora es comprobar si el usuario es 'bob' y la contraseña es 'secreto', si es así muestra un dialogo con el mensaje "Éxito", sino muestra un dialogo con el mensaje "Error".

Fijate que para acceder a los campos username y password dentro del método login de nuestro objeto app, usamos this.username y this.password.

La variable this es una variable que esta disponible en métodos (osea, funciones de objetos), y su valor es el objeto al que el método pertenece, this significa "este" en ingles, por lo que podemos acceder a los atributos y otros métodos del objeto actual usando this.

Dicho de otra forma, si tenemos dos aplicaciones vue app1 y app2 y llamamos a login en ambas, this se va a referir a la aplicación correcta.

app1.login(); // this es app1
app2.login(); // this es app2

Dicho de otra otra forma, this es el valor a la izquierda del punto cuando llamamos a un método.

Proba login con distintos usuarios y contraseñas.

Lo que podemos hacer ahora es, si el usuario y contraseña son correctos, ir a otra pagina.

Para eso necesitamos saber en que pagina estamos, si en login o la pagina principal y según eso mostrar una pagina o la otra. Para eso vamos a agregar un atributo mas a nuestra aplicación, llamado view (vista e ingles), si view es 'login', mostramos lo que veníamos mostrando hasta ahora, si view es 'main' (principal en ingles), mostramos una pagina donde le damos la bienvenida.

Ya que estamos, vamos a mostrar un mensaje mas agradable si el login es invalido, para eso vamos a agregar otro atributo, llamado error, que si no es vacio va a mostrar un mensaje de error en la pantalla:

function main(event) {
  let app = new Vue({
    el: '#app',
    methods: {
      login: function () {
        if (this.username === 'bob' && this.password === 'secreto') {
          this.view = 'main';
        } else {
          this.error = 'Usuario o contraseña incorrecta';
        }
      }
    },
    data: {
      view: 'login',
      error: '',
      username: '',
      password: ''
    }
  });
}

El HTML queda así:

<div id="app">

  <div v-if="view === 'login'">
    <h1 class="h3 mb-3 font-weight-normal">Login</h1>
    <label for="inputUser">Usuario</label>
    <input v-model="username" type="text" id="inputUser" class="form-control" required autofocus>
    <label for="inputPassword" class="mt-3">Password</label>
    <input v-model="password" type="password" id="inputPassword" class="form-control" required>

    <div v-if="error !== ''" class="alert alert-danger mt-3">
      {{error}}
    </div>

    <button @click="login()" class="btn btn-primary btn-block mt-3">Enviar</button>
  </div>

  <div v-if="view === 'main'">
    <h1>Hola {{username}}!</h1>
  </div>

</div>

Fijate que la estructura base es:

<div id="app">

  <div v-if="view === 'login'">
    <!-- pagina de login acá -->
  </div>

  <div v-if="view === 'main'">
    <!-- pagina principal acá -->
  </div>

</div>

Usamos v-if para incluir o no distintos tags (y sus descendientes) según el valor del atributo view, lo mismo hacemos con el mensaje de error:

<div v-if="error !== ''" class="alert alert-danger mt-3">
  {{error}}
</div>

Para cerrar el ciclo, vamos a permitir volver a la pagina de login, osea que vamos a agregar un botón "logout" que permita ir de la pagina principal al login:

Cambiamos la sección principal para que quede:

<div v-if="view === 'main'">
    <h1>Hola {{username}}!</h1>
    <a href="#" @click="logout()" class="float-right">logout</a>
</div>

Y agregamos el método logout a la lista de métodos de la aplicación, donde seteamos view a 'login' y por las dudas limpiamos el mensaje de error, usuario y password.

function main(event) {
  let app = new Vue({
    el: '#app',
    methods: {
      login: function () {
        if (this.username === 'bob' && this.password === 'secreto') {
          this.view = 'main';
        } else {
          this.error = 'Usuario o contraseña incorrecta';
        }
      },
      logout: function () {
        this.view = 'login';
        this.error = '';
        this.username = '';
        this.password = '';
      }
    },
    data: {
      view: 'login',
      error: '',
      username: '',
      password: ''
    }
  });
}

El resultado final para explorarlo:

Mariano Guerra: Creemos en la Web: No nos llames, nosotros te llamamos

En el capítulo anterior vimos como acceder a elementos y atributos individuales de una colección o a todos ellos iterando con el ciclo for.

en este capítulo vamos a ver como usar funciones para evitar repetir código y evitar errores comunes.

Las funciones también son valores

Hasta ahora declaramos funciones con la palabra clave function y las llamamos usando su nombre, paréntesis y una lista de argumentos.

Pero quizás notaste que el nombre de las funciones es un identificador como cualquier otra variable, que pasa si intentamos usarla como cualquier otra variable?

Probemos con una función simple:

function incrementar(valor) {
    return valor + 1;
}

Probemosla:

incrementar(10);
< 11

Que pasa si solo la nombramos?:

incrementar;
< [Function: incrementar]

nada muy útil, pero nos indica que las funciones son valores como cualquier otra cosa y como cualquier otra cosa en javascript podemos pasarla como parámetro a funciones y devolverla de funciones.

Probemos pasarla como parámetro a una función, pero, para que la usaríamos? lo único que sabemos hacer con una función es llamarla, así que hagamos eso:

function llamar(fn, valor) {
    return fn(valor);
}

Probemos:

llamar(incrementar, 25);
< 26

Funciona, pero no es muy útil que digamos, que mas podríamos hacer?

Como mencione antes, una buena practica en programación es no repetirse mucho, porque al repetirse podemos introducir errores en algunas partes, también hace mas difícil hacer cambios ya que tenemos el mismo código repetido en muchos lugares. También es útil escribir código complicado una sola vez en un solo lugar para evitar errores y hacer mas simple el resto del código.

No se vos, pero escribir todo un for cada vez es un poco molesto, repetitivo y si en lugar de < por error escribimos <= tenemos un error que va a ser difícil de detectar, probemos escribir una función que nos evite escribir el for cada vez:

function porCadaElemento(lista, fn) {
    for (let i = 0, len = lista.length; i < len; i += 1) {
        let elemento = lista[i];
        fn(elemento, i);
    }
}

Lo que la función porCadaElemento hace es recibir una lista y una función como parámetros y llamar a la función porCadaElemento de la lista pasandole dos parámetros, el elemento y el indice del elemento en la lista.

Para probarlo vamos a necesitar una función, empecemos imprimiendo los elementos de la lista:

function imprimirElemento(elemento, i) {
    console.log('elemento', i, 'de lista es', elemento);
}

Ahora llamemos a porCadaElemento con una lista y esta función:

porCadaElemento([10, 20, 30], imprimirElemento);

El resultado es:

elemento 0 de lista es 10
elemento 1 de lista es 20
elemento 2 de lista es 30

Funciona, pero que pasa si queremos hacer algo en cada elemento y acumularlo? creemos una función para eso:

function aCadaElemento(lista, fn) {
    let resultado = [];

    for (let i = 0, len = lista.length; i < len; i += 1) {
        let elemento = lista[i],
            nuevoElemento = fn(elemento, i);

        resultado.push(nuevoElemento);
    }

    return resultado;
}

aCadaElemento se le aplica la función que pasamos por parámetro y el resultado que devuelva se agrega a una nueva lista que devolvemos cuando llamamos a la función para todos los elementos de la lista de entrada, probemos un caso para entenderlo mejor:

aCadaElemento([10, 20, 30], incrementar);
< [ 11, 21, 31 ]

A cada elemento de la lista [10, 20, 30] le "aplicamos" la función incrementar y acumulamos el resultado en una nueva lista que devolvemos.

Otra cosa que podemos hacer es "filtrar" elementos de una lista, con una función que recibe una lista y una función que le indica si cada elemento debe ser descartado de la lista resultado o no.

function filtrar(lista, fn) {
    let resultado = [];

    for (let i = 0, len = lista.length; i < len; i += 1) {
            let elemento = lista[i],
                incluir = fn(elemento, i);

            if (incluir) {
                resultado.push(elemento);
            }
    }

    return resultado;
}

Para probar esta función vamos a necesitar una función nueva que reciba un valor y devuelva true si la condición es verdadera y false si es falsa. Este tipo de funciones se suelen llamar "predicados".

Creemos una que nos diga si un numero es par:

function esPar(n) {
    // si el resto de la división es cero, entonces el numero es par
    return n % 2 === 0;
}

Probemos nuestro "predicado" esPar:

esPar(0);
< true
esPar(1);
< false
esPar(2);
< true
aCadaElemento([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], esPar);
< [ true, false, true, false, true, false, true, false, true, false ]

Ahora probemosla con nuestra función filtrar:

filtrar([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], esPar);
< [ 0, 2, 4, 6, 8 ]

Funciona y la intención del código queda mas clara que si tuviéramos todo el for mezclado con la lógica de lo que queremos hacer.

Una cosa que podemos hacer si la lógica que queremos aplicar es solo útil en esa llamada es no declarar la función de antemano sino declararla directamente en el lugar donde la llamamos, así queda todo junto y no se nos llena el código de funciones que se usan una sola vez, como podemos pasar parámetros de valores directamente sin declarar variables, podemos pasar funciones directamente sin declararlas de antemano:

filtrar([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], function (elemento, i) {
    return elemento % 2 === 0;
});
< [ 0, 2, 4, 6, 8 ]

Estas funciones sin declarar se llaman "funciones anónimas" o "lambda" ya que no tienen nombre, pero es buena practica aun cuando nunca la vamos a llamar por nombre, de darle un nombre para poder entender mas fácil que es lo que hace:

filtrar([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], function esPar(elemento, i) {
    return elemento % 2 === 0;
});
< [ 0, 2, 4, 6, 8 ]

Pero si estas cosas son tan comunes, seguro alguien ya las hizo por nosotros no?

Así es, estas funciones son tan comunes que tienen nombres estándar y vienen incluidas en javascript.

  • porCadaElemento se llama forEach ("porCada" en ingles)
  • aCadaElemento se llama map ("mapear" o "aplicar" en ingles)
  • filtrar se llama filter ("filtrar" en ingles)

Estas funciones están disponibles como atributos en las listas, un atributo que es una función suele llamarse método.

Probemos con los métodos de las listas lo que ya hicimos:

let lista = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
lista.forEach(imprimirElemento);

Imprime:

elemento 0 de lista es 0
elemento 1 de lista es 1
elemento 2 de lista es 2
elemento 3 de lista es 3
elemento 4 de lista es 4
elemento 5 de lista es 5
elemento 6 de lista es 6
elemento 7 de lista es 7
elemento 8 de lista es 8
elemento 9 de lista es 9
lista.map(incrementar);
< [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
lista.filter(esPar);
< [ 0, 2, 4, 6, 8 ]

Este capitulo es uno de los mas densos de todos hasta ahora, pero con lo aprendido hasta acá ya podes programar cualquier cosa :) solo queda un concepto importante por aprender pero no es necesario, sino que es útil para evitar que nuestro código no se vuelva muy complicado.

Mariano Guerra: Creemos en la Web: Uno, dos, muchos

En capítulos anteriores vimos tipos de datos "compuestos", también llamados colecciones, como listas y objetos, también vimos, solo por necesidad, como mostrar elementos de una lista.

En este capitulo vamos a ver en detalle como hacer algo con cada elemento de una lista o un objeto.

Repasando

Recordemos un poco los tipos de datos compuestos/colecciones, escribí cada linea en la consola del navegador (F12 y selecciona el tab Consola o similar) para ver el resultado.

Lista (o Array)

Una lista es una secuencia de cero o mas elementos de cualquier tipo, cada elemento se accede por su posición en la lista, empezando desde la posición cero.

Una lista vacía (sin elementos):

let listaVacia = [];

Una lista con un elemento:

let lista1 = [42];

Una lista con números:

let listaNumeros = [1, 2, 42, 100, 9001];

Una lista con valores de distinto tipo:

let listaTipos = [42, null, undefined, true, "hola", []];

Objeto

Un objeto es un conjunto de cero o mas elementos de cualquier tipo, cada elemento se accede por su clave, que es el nombre que le damos a ese campo, también llamado atributo del objeto.

Un objeto vacío:

let objetoVacio = {};

Un objeto con un campo:

let objetoCampo = {nombre: 'bob'};
// es lo mismo que lo siguiente, siempre y cuando la clave (key)
// sea un identificador valido, sino siempre hay que usar comillas
let objetoCampo1 = {'nombre': 'bob'};

Un objeto con valores de distinto tipo:

let objetoTipos = {numero: 42, null: null, undefined: undefined, booleano: true, texto: "hola", lista: [10, 20, 30], objeto: {clave: 'valor'}, 'clave rara!': 12.5, 100: 'cien'};

Vale aclarar que las claves de los objetos son siempre de tipo texto (strings), por mas que no le pongamos comillas, javascript las va a convertir en texto, por lo que las claves 'null', 'undefined' o si ponemos un numero, no son de ese tipo sino que se convierten en texto antes de guardarlas.

Accediendo a los valores a mano

Como accedemos a los valores almacenados en colecciones? hay dos formas dependiendo la colección, que queremos acceder dentro de esa colección y la clave.

Accediendo elementos de una lista

Como ya dijimos, las listas son una secuencia de elementos de cualquier tipo, donde cada elemento se accede por su posición en la lista, empezando desde la posición cero, probemos con las variables que declaramos arriba, si no las escribiste en la consola, este es un buen momento para hacerlo.

Si ya las escribiste y las intentas declarar de nuevo, puede que obtengas un error de que la variable ya fue declarada, proba con otro nombre o cerra y abrí el tab para poder declararla de nuevo.

Siempre me gusta empezar tratando de romper todo, así que veamos que pasa cuando intentamos acceder a un elemento que no esta definido, por ejemplo, el primer elemento de la lista vacía, que seria el elemento con indice 0, para acceder a un elemento de una colección usamos los corchetes después del nombre de la colección y entre corchetes ponemos el identificador del valor que queremos acceder:

listaVacia[0];

El resultado me da:

< undefined

Es decir que cuando le pedimos un elemento que no tiene nos devuelve undefined.

Pero como sabemos cuantos elementos tiene para saber hasta donde le podemos pedir?

En Javascript todos los valores son "objetos", es decir que tienen atributos y funciones asociadas para operar sobre sus valores, esto quiere decir que le podemos preguntar a una lista cuantos elementos tiene accediendo a su atributo length (que es "longitud" o "largo" en ingles).

Probemos:

listaVacia.length;
< 0

Nos dice que tiene 0 elementos, osea que es una lista vacía.

Como habrás visto usamos otra forma de acceder a un atributo de la lista, esta vez con un punto, esta forma se puede usar cuando el valor del atributo es un identificador valido, esto no quita que podamos hacer:

listaVacia['length'];
< 0

Lo que no es muy útil si el atributo al que queremos acceder es "fijo", pero nos va a ser útil para cuando queramos acceder a atributos que no conocemos cuando estamos escribiendo el programa, para darte una idea de lo que seria, imaginemos que tenemos una función que recibe dos parámetros, un valor y un nombre de un atributo de ese valor y devuelve el valor de ese atributo:

function devolverValor(valor, atributo) {
    return valor[atributo];
}

La función no sabe cual es el valor de atributo, ya que es una variable, puede ser cualquier cosa, probemos con listaVacia y 'length':

devolverValor(listaVacia, 'length');
< 0

Probemos con un indice de la lista:

devolverValor(listaVacia, 0);
< undefined

Probemos con lista1:

devolverValor(lista1, 'length');
< 1
devolverValor(lista1, 0);
< 42

Accediendo elementos de una objeto

Buenas noticias, ya sabes como acceder a elementos de un objeto, las formas son las mismas, con corchetes si la clave no es un identificador valido y con punto si lo es, empecemos rompiendo, tratemos de acceder a un atributo que no esta definido en el objeto:

objetoVacio.yoNoExisto;
< undefined

Como la lista, si no esta definido devuelve undefined. Ahora probemos con uno que si exista:

objetoCampo.nombre;
< 'bob'

Como ya vimos, se puede acceder con corchetes, cosa que no es necesaria en este caso pero probemos:

objetoCampo['nombre'];
< 'bob'

En donde si la vamos a necesitar es con nuestra 'clave rara!', que no podemos acceder con punto ya que no es un identificador valido:

objetoTipos['clave rara!'];
< 12.5

Como habíamos dicho, las claves de los objetos son siempre de tipo texto (string), probemos que es cierto:

objetoTipos[100];
< 'cien'

Eso no demuestra nada... pero esto si:

objetoTipos['100'];
< 'cien'

Como veras también funciona, porque si le pasamos el numero, javascript lo convierte en string, si le pasamos un string lo usa directamente, por eso ambas funcionan.

Iterando (Listas)

Hasta acá podemos acceder elementos de una colección de a uno, sabiendo la clave o indice que queremos acceder de antemano o cuando tenemos la clave/indice en una variable.

Pero muchas veces necesitamos acceder a todos los elementos/atributos de una colección sin saber de antemano cuales son.

Para esto vamos a aprender a iterar sobre los elementos de una colección, hay varias formas de hacerlo, vamos a empezar con una de las mas flexibles y estándares, aunque un poco complicada, así que no te preocupes si no la entendés la primera vez, yo programe por un año antes de entender bien como funcionaba.

Antes de mostrar la sintaxis pensemos que es lo que queremos hacer, para el caso de una lista:

Inicialización:

  • Preguntarle a la lista cuantos elementos tiene y guardarlo en una variable, llamemosle len
  • Declarar una variable a cero, llamemosle i (de indice)

Ciclo:

  • Si el indice es menor que el largo
    • Obtener el valor de la lista en ese indice
    • Hacer algo
    • Incrementar la variable "indice"
    • Ejecutar Ciclo de nuevo

Como veras, la lógica tiene dos partes, una que se ejecuta una sola vez al principio (Inicialización) y una que se ejecuta por cada elemento de la lista, hasta que una condición deje de cumplirse (que el indice este dentro del largo de la lista).

Esto se suele llamar "ciclo for", veamos la sintaxis a grandes rasgos, no es la sintaxis valida pero para darte una idea:

for (inicializarVariables (1); condicionDeCorte (2); siguiente (3)) {
    // lógica por cada paso (4)
}

Veamoslo con código valido, no hace falta que lo entiendas entero, es para que te des una idea:

for (let i = 0, len = lista.length; i < len; i += 1) {
    let elemento = lista[i];
    console.log('elemento', i, 'de lista es ', elemento);
}

Inicialización:

la sección inicializarVariables (1) se ejecuta una sola vez al principio.

Ciclo:

Luego se evalúa la condicionDeCorte (2) para ver si la lógica (4) se va a ejecutar o no.

Si la condición de corte evalúa a false el ciclo termina, si evalúa a true se ejecuta la lógica (4) con el valor de i actual. Luego se evalúa siguiente (3), que normalmente incrementa el valor de i, pero puede hacer otras cosas.

Luego de evaluar siguiente (3), se repite el ciclo.

Probemoslo con varias listas de largos distintos, para eso pongamos nuestro ciclo for en una función:

function mostrarElementos(lista) {
    console.log('antes del ciclo for para', lista);

    for (let i = 0, len = lista.length; i < len; i += 1) {
        let elemento = lista[i];
        console.log('elemento', i, 'de lista es ', elemento);
    }

    console.log('después del ciclo for');
}

Probemoslo con una lista vacía:

mostrarElementos(listaVacia);

Imprime lo siguiente:

antes del ciclo for para []
después del ciclo for

Como podemos ver, el paso (4) nunca se ejecuto, porque la condición de corte (2) evaluó a false la primera vez.

La secuencia completa para listaVacia es:

// Inicialización
// (1)
i = 0
len = 0

// Ciclo
// (2)
i < len // false

// Fin

Probemos con una lista de un elemento:

mostrarElementos(lista1);

Imprime:

antes del ciclo for para [ 42 ]
elemento 0 de lista es  42
después del ciclo for

Ahora el paso (4) y (3) se ejecutaron 1 vez, veamos la secuencia:

// Inicialización
// (1)
i = 0
len = 1

// Ciclo
// (2)
i < len // true

// (4)
elemento = lista[0]; // 42

// (3)
i += 1 // 1

// (2)
i < len // false

// Fin

Ultima, con dos elementos:

mostrarElementos([10, 20]);

Imprime:

antes del ciclo for para [ 10, 20 ]
elemento 0 de lista es  10
elemento 1 de lista es  20
después del ciclo for
// Inicialización
// (1)
i = 0
len = 2

// Ciclo
// (2)
i < len // true

// (4)
elemento = lista[0]; // 10

// (3)
i += 1 // 1

// Ciclo
// (2)
i < len // true

// (4)
elemento = lista[1]; // 20

// (3)
i += 1 // 2

// (2)
i < len // false

// Fin

Iterando (Objetos)

Iterar listas es lo mas común, pero algunas veces necesitamos iterar por todas las claves de un objeto, para eso usamos una versión distinta (y por suerte mas simple) del ciclo for, veamoslo directamente en una función:

function mostrarElementosObjeto(objeto) {
    console.log('antes del ciclo for para', objeto);

    for (let key in objeto) {
        let elemento = objeto[key];
        console.log('elemento', key, 'de objeto es ', elemento);
    }

    console.log('después del ciclo for');
}

Esta versión es mas simple, asigna a key cada clave en objeto y ejecuta la lógica.

Probemos con un objeto vacío:

mostrarElementosObjeto({});

Imprime:

antes del ciclo for para {}
después del ciclo for

Con varios elementos:

mostrarElementosObjeto(objetoTipos);

Imprime:

antes del ciclo for para { '100': 'cien',
  numero: 42,
  null: null,
  undefined: undefined,
  booleano: true,
  texto: 'hola',
  lista: [ 10, 20, 30 ],
  objeto: { clave: 'valor' },
  'clave rara!': 12.5 }

elemento 100 de objeto es  cien
elemento numero de objeto es  42
elemento null de objeto es  null
elemento undefined de objeto es  undefined
elemento booleano de objeto es  true
elemento texto de objeto es  hola
elemento lista de objeto es  [ 10, 20, 30 ]
elemento objeto de objeto es  { clave: 'valor' }
elemento clave rara! de objeto es  12.5

después del ciclo for

Para saber cuantos atributos tiene un objeto, podemos usar una función llamada Object.keys que nos devuelve una lista de las claves de un objeto, probemosla:

Object.keys(objetoVacio);
< []
Object.keys(objetoCampo);
< [ 'nombre' ]
Object.keys(objetoTipos);
< [ '100',
  'numero',
  'null',
  'undefined',
  'booleano',
  'texto',
  'lista',
  'objeto',
  'clave rara!' ]

Veamos como podríamos iterar sobre las claves de un objeto usando Object.keys y lo que aprendimos sobre listas:

function mostrarElementosObjeto1(objeto) {
    console.log('antes del ciclo for para', objeto);

    let keys = Object.keys(objeto);
    for (let i = 0, len = keys.length; i < len; i += 1) {
        let key = keys[i],
            elemento = objeto[key];
        console.log('elemento', key, 'de objeto es ', elemento);
    }

    console.log('después del ciclo for');
}

Probemoslo:

mostrarElementosObjeto1({});

Imprime:

antes del ciclo for para {}
después del ciclo for

Con varios elementos:

mostrarElementosObjeto1(objetoTipos);

Imprime:

antes del ciclo for para { '100': 'cien',
  numero: 42,
  null: null,
  undefined: undefined,
  booleano: true,
  texto: 'hola',
  lista: [ 10, 20, 30 ],
  objeto: { clave: 'valor' },
  'clave rara!': 12.5 }

elemento 100 de objeto es  cien
elemento numero de objeto es  42
elemento null de objeto es  null
elemento undefined de objeto es  undefined
elemento booleano de objeto es  true
elemento texto de objeto es  hola
elemento lista de objeto es  [ 10, 20, 30 ]
elemento objeto de objeto es  { clave: 'valor' }
elemento clave rara! de objeto es  12.5

después del ciclo for

Como podemos ver, funciona igual que mostrarElementosObjeto.

Facundo Batista: Pasó la PyCon Argentina 2018

¡La décima! Todo un hito. Tuvimos 1080 asistentes, lo cual es más de lo que esperábamos, :)

El evento en si salió muy bien, todes les organizadores estamos muy contentes (y muy cansades). Pero valió la pena.

Arranqué más o menos prolijo......terminé bastante roto :p

Le pusimos mucho amor y creo que se notó. Increíble el equipo que se formó de trabajo, el laburazo que se pegaron es indescriptible. Estoy seguro que ese tipo de compromiso no se logra pagándoles. Creo que fue todo mucho mejor que si se los hubiese contratado para que trabajen (onda "organizadores profesionales de conferencias").

Sí, lleva mucho laburo, pero nos gusta más así: una conferencia hecha por todes nosotres, con nuestra impronta, no por un organizador de eventos con saco y corbata.

De la comunidad, para la comunidad.

Organizadores, ayudantes de sala, ayudantes de registración, etc...

La conferencia se sucedió durante tres días. El jueves fueron los talleres: tres tracks en paralelo de talleres de tres horas, más un cuarto track con el Django Girls, el único taller que tenía registración previa (y cupo, tuvimos que cortar en los 100 asistentes).

Todo el equipo del taller de Django Girls con asistentes y todo

Viernes y sábado fue el formato de conferencias clásicas: tres tracks de charlas en paralelo, la mayoría de 25 minutos, algunas de 50. También tuvimos espacios abiertos, speed interviews, lightning talks, e incluso un Panel de Diversidad y Género.

Y dos invitados internacionales: Carol Willing, core developer de Jupyter y ex directora de la PSF, y Brandon Rhodes, programador Python en Dropbox y organizador de la PyCon USA en Portland 2016 y 2017.

El que suscribe, con Brandon y Carol luego de darles regalitos

Incluso tuvimos un evento puramente social al aire libre, el domingo: un Pycnic totalmente abierto al que quisiera venir.

Pero más allá del desglose de actividades, quiero resaltar que logramos armar una conferencia de nivel internacional, sin nada que envidiarles a las que se hacen en otros países, pero completamente libre y gratuita. Zaffaroni dijo una vez que "El cambio social profundo, inclusivo, la revolución del siglo XXI, se hace apoderándose del conocimiento... conocimiento que la elite se empeña en monopolizar". Y en verdad, en este momento macrista donde nuestro país ya no tiene ni Ministerio de Ciencia y Tecnología, distribuir información gratis es más valioso que nunca.

Vino mucha gente: un venue más chico no nos hubiese alcanzado

De la misma manera, en este momento con tanta xenofobia, homofobia y "diversofobia", tenemos que ser más inclusivos que nunca. Y ser explícitos en darle la bienvenida a todo el mundo.

Por eso armamos este maravilloso texto sobre Diversidad, del cual incluimos un resumen en el librito de la conferencia.

Un panel de lujo, una charla super interesante

Durante el cierre tiré un par de anuncios para el año que viene. El primero es que tenemos el PyCamp 2019 confirmado, se va a hacer en San Rafael, Mendoza, del 2 al 5 de Marzo (estamos esperando que nos pasen un par de datos para confirmar el precio y salir a juntar el dinero).

El otro anuncio es que la PyCon 2019 se va a hacer también en Buenos Aires... la idea es empezar a hacer dos conferencias por cada sede (2018 y 2019 en Buenos Aires, 2020 y 2021 en otro lugar, etc), porque de esta manera se baja muchísimo el costo para los organizadores de hacer el evento. Sí, el primer año es un quilombo, pero el segundo año se repiten tantas cosas que es mucho más fácil.

La grupal (socavada notablemente por el partido "importante" :/ )

Facundo Batista: Décima Edición de la Conferencia Nacional de Python Argentina

Suena fuerte, ¡diez ediciones! Cuánta agua abajo del puente pasó desde que organicé la PyCon 2009, también en la Ciudad de Buenos Aires. Desde ese arranque gracias a otres organizadores pudimos disfrutar el evento en Córdoba, Junín, Quilmes, Rosario, Rafaela, Mendoza, Bahía Blanca, nuevamente Córdoba.

Y ahora volvemos al origen. Cerramos un círculo, pero no para terminar, sino para subir la apuesta y salir con más ímpetu a una nueva ronda. No sólo tener una conferencia con invitades internacionales, talleres y charlas relámpagos, sino también otras actividades como espacios abiertos, coaching para la preparación de charlas, entrevistas rápidas, y profundizar acciones sobre un tema que tratamos desde hace años en la comunidad: nuestra diversidad.

Claro, los tiempos no son los mismos. Incluso Python, como lenguaje, cambió notablemente. En esa época Python 3 era un futuro difuso, ahora Python 2 está en sus últimos estertores. Y no hay especialidad profesional relacionado con la programación que no use Python.

La situación del lenguaje en el país también cambió muchísimo. En esa época la problemática era encontrar trabajos relacionados con Python, hoy en día el desafío es encontrar suficientes trabajadores en nuestro querido lenguaje para poder satisfacer las necesidades de empresas, cooperativas y el Estado mismo.

Considerando que difundir el lenguaje era un objetivo del grupo de Python Argentina, entendemos que también esta comunidad haya cambiado, siendo un hito notable la creación de la Asociación Civil hace ya un par de años.

Y es con la ayuda de la Asociación Civil que pudimos hacer las últimas conferencias, incluyendo esta por supuesto. Y también con la colaboración y soporte del Centro Cultural General San Martín, de los sponsors que pueden visitar durante la conferencia y conocer en este mismo libro, y especialmente de todes aquelles que colaboraron en la organización, es que logramos hacer una nueva conferencia de calidad internacional, totalmente gratuita e inclusiva.

Una nueva PyCon Argentina. De la comunidad, para la comunidad. Que la disfruten.

Facundo Batista: Inminente gran PyCon gran

¡¡¡La PyCon arranca el jueves que viene!!! AAAAAAAAAAAAAHHHHHHHHH

respira

AAAAAAAAAAAAAAAAAAAAHHHHHHHHHHHHHHHHHHHHHHHH

Sabrán disculpar que no posteo mucho por acá...

A recontra full

Ya está publicado el cronograma de actividades, con todes les talleres y charlas que se van a dar.

La conferencia es libre y gratuita, para venir a aprender lo único que necesitás hacer es registrarte en este formulario y listo, ya podés acceder a todo lo que ofrece la conferencia (excepto al Django Girls, que era con cupo limitado y la registración al mismo ya cerró hace varias semanas). Si por algún motivo necesitás un certificado de asistencia, pedilo acá.

Entre todas las actividades que podés acceder están las Speed Interviews. Una speed interview es un modo de conocer empresas de la misma forma que una entrevista tradicional, solo que las entrevistas duran 5 minutos, y en una hora se pueden conocer a varias empresas, interiorizarse en lo que hacen, y generar un contacto para luego profundizar el conocimiento mutuo. Es ideal para aquelles que tienen mediana, poca o incluso ninguna experiencia laboral. No hay que tener miedo de aprovechar la oportunidad, ¡es un servicio gratuito de la conferencia! Sólo hay que reservar la posición anotándose acá.

Por último, acordate de reservar tu remera! No te cuelgues! También las vas a poder comprar los días de la conferencia, pero tené en cuenta que la forma de pago es la misma que ahora (no se puede manejar efectivo en el Centro Cultural)... metele que algunos talles se están acabando...

¡Nos vemos en unos días!

Marcos Dione: pefan

A few weeks ago I needed to do some line based manipulation that kinda went further of what you can easyly do with awk. My old-SysAdmin brain kicked in and the first though was, if you're going to use awk and sed, you might as well use perl. Thing is, I really can't remember when was the last time I wrote even a oneliner in perl, maybe 2011, in my last SysAdmin-like position.

Since then I've been using python for almost anything, so why not? Well, the python interpreter does not have an equivalent of perl's -n switch; and while we're at it, -a, -F, -p are also interesting for this.

So I wrote a little program for that. Based on those switch names, I called it pefan. As python does not have perl's special variables, and in particuar, $_ and @_, the wrapper sets the line variable for each line of the input, and if you use the -a or -F switches, the variable data with the list that's the result of splitting the line.

Meanwhile, while reading the perlrun manpage to write this post, I found out that -i and even -s sound useful, so I'll be adding support for those in the future. I'm also thinking of adding support for curly-brace-based block definitions, to make oneliners easier to write. Yes, it's a travesty, but it's all in line with my push to make python more SysAdmin friendly.

In the meantime, I added a couple of switches I find useful too. See the whole usage:

usage: pefan.py [-h] [-a] -e SCRIPT [-F SPLIT_CHAR] [-i] [-M MODULE_SPEC]
                [-m MODULE_SPEC] [-N] [-n] [-p] [--no-print] [-r RANDOM]
                [-s SETUP] [-t [FORMAT]] ...

Tries to emulate Perl's (Yikes!) -peFan switches.

positional arguments:
FILE                  Files to process. If ommited or file name is '-',
                      stdin is used. Notice you can use '-' at any point in
                      the list; f.i. "foo bar - baz".

optional arguments:
-h, --help            show this help message and exit
-a, --split           Turns on autosplit, so the line is split in elements.
                      The list of e lements go in the 'data' variable.
-e SCRIPT, --script SCRIPT
                      The script to run inside the loop.
-F SPLIT_CHAR, --split-char SPLIT_CHAR
                      The field delimiter. This implies [-a|--split].
-i, --ignore-empty    Do not print empty lines.
-M MODULE_SPEC, --import MODULE_SPEC
                      Import modules before runing any code. MODULE_SPEC can
                      be MODULE or MODULE,NAME,... The latter uses the 'from
                      MODULE import NAME, ...' variant. MODULE or NAMEs can
                      have a :AS_NAME suffix.
-m MODULE_SPEC        Same as [-M|--import]
-N, --enumerate-lines
                      Prepend each line with its line number, like less -N
                      does.
-n, --iterate         Iterate over all the lines of inputs. Each line is
                      assigned in the 'line' variable. This is the default.
-p, --print           Print the resulting line. This is the default.
--no-print            Don't automatically print the resulting line, the
                      script knows what to do with it
-r RANDOM, --random RANDOM
                      Print only a fraction of the output lines.
-s SETUP, --setup SETUP
                      Code to be run as setup. Run only once after importing
                      modules and before iterating over input.
-t [FORMAT], --timestamp [FORMAT]
                      Prepend a timestamp using FORMAT. By default prints it
                      in ISO-8601.

FORMAT can use Python's strftime()'s codes (see
https://docs.python.org/3/library/datetime.html#strftime-and-strptime-
behavior).

Go get it here.


python pefan sysadmin

Marcos Dione: ansible-in-a-box-or-iso

At my $NEWJOB I'm in the team that installs the products we sell on the client's machines. Most of the time the client has bought appliances from us, so they come with the installer and some tools for setting them up. Because of the type of product we sell, the customer might have bought between 3 to 12 or more nodes that will form a cluster, and some times they're spread over several data centers.

The system needs a frontend network and a backend one, and the nodes come with two high speed NICs (typically 10Gib), two low speed (1Gib), and a BMC/IPMI interface. The typical use is to bond both high speed NICs and then build two VLANs on top. The atypical use might be whatever the client came up with. One client has bonded each of the high speed NICs with one of the low speed in primary/backup mode, and has two physical networks. Another one does everything through a single interface with no VLANs. This should give you an idea of how disparate the networking setups can be, so the networking has to be custom made for each client.

Our first step is to connect to the nodes and configure networking. The only preconfigured interface is the BMC/IPMI one, which asks for an IPv4 via DHCP. So we connect to the BMC interface. This involves connecting via HTTP to a web interface that is run within the IPMI subsystem, then download a Java application that gives us a virtual KVM so we can use the computer as if we just had connected a keyboard and a monitor to it.

For those who don't know (I didn't before I started this new position), the IPMI/BMC system is a mini computer fully independent of the main system, which boots once the node has power connected to it, but not necessarily powered on. You can turn on/off the machine, divert KVM as I mentioned before, and more, as you'll see. If you're surprised to find out you have more than one computer in your computer, just read this.

Once connected to the node, we run a setup script, to which we feed all the networking info, including static IPs, gateways, DNS servers, timezone, etc. All this for each node. By hand. Slow, error prone, boring.

Let's automate this. The simplest tool I can think of is Ansible. In fact, I also think it's perfect for this. But there's a catch: there's no Ansible installed on the node, there is no way Ansible will be able to talk KVMimplementedasajavaapp-ese, and again, there's no networking yet, so no ssh or any other remote access. But most modern IPMI systems have an extra feature: virtual devices. You can upload iso images and IPMI will present them as a USB cd reader with media inside.

So today's trick involves in creating an iso image with ansible on it that can run on the target system. It's suprisingly easy to do. In fact, it would be as easy as creating a virtualenv, install ansible, add the playbooks and stuff, etc, and create an iso image from that, if it were not for the fact that the image has to be at least less than 50MiB (we have seen this limit on Lenovo systems). Ansible alone is 25MiB of source code, and compiled into .pyc files doubles that. So the most difficult part is to trim it down to size.

Of course, we fisrt get rid of all the .py source code. But not all. Modules and module tools in ansible are loaded from the .py files, so we have to keep those. I can also get rid of pip, setuptools and wheel, as I won't be able to install new stuff for two reasons: one, this is going to be a read only iso image, and two, remember, networking is not setup yet :) Also, ansbile is going to be run locally (--connection local), so paramiko is gone too. Next come all those modules I won't be using (cloud, clustering, database, etc). There a couple more details, so let's just have the script we currently use:

#! /bin/bash
# this is trim.sh

set -e

while [ $# -gt 0 ]; do
    case "$1" in
      -a|--all)
        # get rid of some python packages
        for module in pip setuptools pkg_resources wheel; do
            rm -rfv "lib/python2.7/site-packages/$module"
        done

        shift
        ;;
    esac
done

# cleanup
find lib -name '*.py' -o -name '*.dist-info' | egrep -v 'module|plugins' | xargs rm -rfv

for module in paramiko pycparser; do
    rm -rfv "lib/python2.7/site-packages/$module"
done

ansible_prefix="lib/python2.7/site-packages/ansible"

# trim down modules
for module in cloud clustering database network net_tools notification remote_management source_control web_infrastructure windows; do
    rm -rfv $ansible_prefix/modules/$module
done

# picking some by hand
find $ansible_prefix/module_utils | \
    egrep -v 'module_utils$|__init__|facts|parsing|six|_text|api|basic|connection|crypto|ismount|json|known_hosts|network|pycompat|redhat|service|splitter|urls' | \
    xargs -r rm -rfv
find $ansible_prefix/module_utils/network -type d | egrep -v 'network$|common' | xargs -r rm -rfv
find $ansible_prefix/modules/packaging -type f | \
    egrep -v '__init__|package|redhat|rhn|rhsm|rpm|yum' | xargs -r rm -v
find $ansible_prefix/modules/system    -type f | \
    egrep -v '__init__|authorized_key|cron|filesystem|hostname|known_hosts|lvg|lvol|modprobe|mount|parted|service|setup|sysctl|systemd|timezone' | \
    xargs -r rm -v

Notice that if I was even more space constrained (and it could be possble, if we find another IPMI implementation with smaller staging space) I could go further and make the venv use the Python installed in the system and not the one copied in the venv.

Now, the next step is to fix the venv to be runnable from any place. The first step is to make it relocatable. This fixes all the binaries in bin to use /usr/bin/env python2 instead of the hardcoded path to the python binary copied into the venv. One thing I never understood is why it didn't went a step further and also declared the VIRTUAL_ENV as relative to the path were bin/activate resides. In any case, I do an extra fix with sed and I'm done.

Last step is just to create the iso image. It's been ages since I last generated one by hand, and the resulting command line (which I simply stole from running k3b) resulted more complext than I expected (what happened to sensible defaults?). Here are the interesting parts:

#! /bin/bash

set -eu

ansible-playbook --syntax-check --inventory-file inventory.ini playbook.yaml

./check_config.py

./trim.sh --all

# make relative
/usr/bin/python2 -m virtualenv --relocatable .

# try harder
# this doesn't cover all the possibilities were bin/activate might be sourced
# from, but in our case we have a wrapper script that makes sure we're in a sane place
sed -i -e 's/VIRTUAL_ENV=".*"/VIRTUAL_ENV="$(pwd)"/' bin/activate

genisoimage -sysid LINUX -rational-rock -joliet -joliet-long \
    -no-cache-inodes -full-iso9660-filenames -disable-deep-relocation -iso-level 3 \
    -input-charset utf-8 \
    -o foo.iso .

We threw in some checks on the syntax and contents of the playbook (it's annoying to find a bug when running on the target machine, come back, generate a new iso, upload it, mount, etc). It is possible that you would also like to exclude more stuff in your working directory, so just create a build dir, copy over your files (maybe with rsync --archive --update --delete) and run genisoimage there.

This method produces an iso image 26MiB big that works both in virtual machines, with which I developed this solution, and on some IPMI systems, like the Lenovo I mentioned before. Unluckily I couldn't get my hands on many different systems that have IPMI and not being used for anything else.

One final note about sizing. If you run du on your working/staging directory to see how far are from the limit, use --apparent-sizes, as the iso format packs files better than generic filesystems (in my case I see 26MiB apparent vs 46MiB 'real'; this is due to block sizes and internal fragmentation).


ansible ipmi bmc sysadmin

Mariano Guerra: Creemos en la Web: Ver o no ver, también es una cuestión

En el capítulo anterior vimos como calcular o no según el resultado de una condición, ahora vamos a ver como cambiar que mostramos en HTML según el valor de condiciones.

Vamos a hacerlo dándole una cara a nuestro despertador.

Empecemos con la versión mas simple, inicializando un despertador con una sola alarma y mostrando si esta prendido o no y a que hora esta configurada la alarma.

<div id="despertador-app-1">
  <p class="alert alert-info">Despertador Prendido? <strong>{{despertadorPrendido}}</strong></p>
  <p class="alert alert-info">Hora Despertador: {{horaDespertador}}</p>
</div>
<script>
  new Vue({
    el: '#despertador-app-1',
    data: {
      despertadorPrendido: false,
      horaDespertador: 8
    }
  })
</script>

El resultado:

Despertador Prendido? {{despertadorPrendido}}

Hora Despertador: {{horaDespertador}}

Repasando un poco vue.js, tenemos un div raíz de nuestra aplicación, el cual tiene que tener un atributo id así le podemos indicar a vue.

Dentro del div tenemos dos párrafos, el primero muestra el valor de la variable despertadorPrendido usando el formato de vue {{variable}} para indicar que queremos que ahí ponga el valor de variable, el segundo muestra el valor de la variable horaDespertador.

Luego en un tag script inicializamos nuestra aplicación con new Vue(...) donde en ... indicamos usando un objeto javascript dos atributos:

el
Id del tag raíz de esta aplicación, para indicarle que es un id tenemos que prefijarlo con un #.
data
Los datos de nuestra aplicación, por ahora las dos variables que usamos en el HTML arriba y que usamos en el capitulo anterior.

Con esto ya mostramos el estado actual de nuestro simple despertador, pero no podemos prenderlo ni cambiar la hora, veamos como hacer eso:

<div id="despertador-app-2">
  <p v-if ="despertadorPrendido" class="alert alert-success">Despertador Prendido</p>
  <p v-else class="alert alert-danger">Despertador Apagado</p>

  <p class="alert alert-info">Hora Despertador: {{horaDespertador}}</p>

  <input v-model.number="horaDespertador" type="number" class="form-control" placeholder="Hora Despertador">

  <div class="mt-3 text-center">
    <button v-if="despertadorPrendido" v-on:click="despertadorPrendido = false" class="btn btn-danger">Apagar</button>
    <button v-else v-on:click="despertadorPrendido = true" class="btn btn-success">Prender</button>
  </div>
</div>

<script>
  new Vue({
    el: '#despertador-app-2',
    data: {
      despertadorPrendido: false,
      horaDespertador: 8
    }
  })
</script>

El resultado, proba cambiando la hora y haciendo click en el botón:

Despertador Prendido

Despertador Apagado

Hora Despertador: {{horaDespertador}}

Algunos cambios con respecto a la versión anterior, notaras que vue tiene su propio if como javascript, en este caso se escribe v-if="condicion" lo usamos para decidir que párrafo mostramos, si despertadorPrendido es true, mostramos:

<p v-if="despertadorPrendido" class="alert alert-success">Despertador Prendido</p>

Sino:

<p v-else class="alert alert-danger">Despertador Apagado</p>

v-else tiene que estar después de un tag con un atributo v-if o v-else-if.

A la hora la seguimos mostrando de la misma forma pero ahora tenemos un campo de texto para poder cambiarlo:

<input v-model.number="horaDespertador" type="number" class="form-control" placeholder="Hora Despertador">

Con v-model le indicamos a vue que queremos que el contenido de este input refleje el valor del campo horaDespertador, es decir que va a mostrar su valor y cuando lo cambiemos va a actualizar su valor.

Como el campo es un numero y no queremos tener un valor de tipo texto con un numero dentro, le indicamos a vue que nos lo convierta a numero con v-model.number.

El resto es HTML estándar.

Finalmente usamos v-if y v-else de nuevo para mostrar el botón Prender o Apagar según el valor de despertadorPrendido:

<button v-if="despertadorPrendido"
    v-on:click="despertadorPrendido = false"
    class="btn btn-danger">Apagar</button>

<button v-else
    v-on:click="despertadorPrendido = true"
    class="btn btn-success">Prender</button>

En cada botón le indicamos a vue con v-on:click que cuando el botón sea clickeado queremos cambiar el valor de despertadorPrendido.

Con esto ya tenemos una aplicación para prender/apagar un despertador y cambiar la hora de la alarma, pero en el capítulo anterior teníamos una alarma con día, veamos como replicar eso en HTML.

Lo primero que vamos a tener que hacer es tener por cada día de la semana un campo para despertadorPrendido y uno para horaDespertador.

Podemos elegir mostrar todos los días o podemos hacerlo mas simple con un selector del día que queremos ver y manipular.

El resto debería ser como hasta ahora.

Veamos como seria eso:

<div id="despertador-app-3">
  <p v-if="dias[diaSeleccionado].despertadorPrendido"
     class="alert alert-success">Despertador Prendido</p>
  <p v-else
     class="alert alert-danger">Despertador Apagado</p>

  <p class="alert alert-info">Hora Despertador:
    {{dias[diaSeleccionado].horaDespertador}}</p>

  <input v-model.number="dias[diaSeleccionado].horaDespertador"
     type="number" class="form-control" placeholder="Hora Despertador">

  <select v-model="diaSeleccionado" class="custom-select mt-3">
    <option value="lunes">Lunes</option>
    <option value="martes">Martes</option>
    <option value="miercoles">Miércoles</option>
    <option value="jueves">Jueves</option>
    <option value="viernes">Viernes</option>
    <option value="sabado">Sábado</option>
    <option value="domingo">Domingo</option>
  </select>

  <div class="mt-3 text-center">
    <button v-if="despertadorPrendido"
        v-on:click="despertadorPrendido = false"
        class="btn btn-danger">Apagar</button>

    <button v-else
        v-on:click="despertadorPrendido = true"
        class="btn btn-success">Prender</button>
  </div>
</div>

<script>
  new Vue({
    el: '#despertador-app-3',
    data: {
      diaSeleccionado: 'lunes',
      dias: {
        lunes: {
          despertadorPrendido: true,
          horaDespertador: 8
        },
        martes: {
          despertadorPrendido: true,
          horaDespertador: 7
        },
        miercoles: {
          despertadorPrendido: true,
          horaDespertador: 8
        },
        jueves: {
          despertadorPrendido: false,
          horaDespertador: 8
        },
        viernes: {
          despertadorPrendido: true,
          horaDespertador: 7
        },
        sabado: {
          despertadorPrendido: false,
          horaDespertador: 8
        },
        domingo: {
          despertadorPrendido: false,
          horaDespertador: 8
        },
      }
    }
  })
</script>

Resultado:

Despertador Prendido

Despertador Apagado

Hora Despertador: {{dias[diaSeleccionado].horaDespertador}}

El HTML es bastante similar, solo que donde antes teníamos despertadorPrendido ahora tenemos dias[diaSeleccionado].despertadorPrendido y donde antes teníamos horaDespertador ahora tenemos dias[diaSeleccionado].horaDespertador, ya que tenemos que mostrar y cambiar los valores del día seleccionado.

También tenemos un tag select con v-model seteado a diaSeleccionado así muestra el día seleccionado y si elegimos otro valor en el select el valor de diaSeleccionado es actualizado.

Si no te gusta repetir mucho las cosas como yo, notaras que dias[diaSeleccionado] esta por todos lados, es molesto escribirlo, podemos cometer un error si lo escribimos mal y si llegamos a renombrar algo vamos a tener que ir a todos los lugares a actualizarlo.

Seria mas fácil si pudiéramos "nombrar" este pedazo de código a algo mas claro y simple.

Otra cosa que podrás haber notado si tenes una tendencia a intentar romper todo lo que te dan es que se puede poner horas incoherentes, por ejemplo 42 en el campo de horaDespertador, estaría bueno poder evitar eso.

Veamos como podemos solucionar estos problemas:

<div id="despertador-app-4">
  <p v-if ="diaActual.despertadorPrendido"
     class="alert alert-success">Despertador Prendido</p>
  <p v-else
     class="alert alert-danger">Despertador Apagado</p>

  <p class="alert alert-info">Hora Despertador:
    {{diaActual.horaDespertador}}</p>

  <div class="input-group mt-3">
    <div class="input-group-prepend">
      <label class="input-group-text" for="inputGroupSelect01">Cambiar Hora</label>
    </div>
  <input v-model.number="diaActual.horaDespertador"
     type="range" min="0" max="23" class="form-control" placeholder="Hora Despertador">
  </div>

  <div class="input-group mt-3">
    <div class="input-group-prepend">
      <label class="input-group-text" for="inputGroupSelect01">Día</label>
    </div>
    <select v-model="diaSeleccionado" class="custom-select">
      <option value="lunes">Lunes</option>
      <option value="martes">Martes</option>
      <option value="miercoles">Miércoles</option>
      <option value="jueves">Jueves</option>
      <option value="viernes">Viernes</option>
      <option value="sabado">Sábado</option>
      <option value="domingo">Domingo</option>
    </select>
  </div>

  <div class="mt-3 text-center">
    <button v-if="diaActual.despertadorPrendido"
            v-on:click="diaActual.despertadorPrendido = false"
            class="btn btn-danger">Apagar</button>
    <button v-else
            v-on:click="diaActual.despertadorPrendido = true"
            class="btn btn-success">Prender</button>
  </div>
</div>

<script>
  new Vue({
    el: '#despertador-app-4',
    computed: {
      diaActual: function () {
        return this.dias[this.diaSeleccionado];
      }
    },
    data: {
      diaSeleccionado: 'lunes',
      dias: {
        lunes: {
          despertadorPrendido: true,
          horaDespertador: 8
        },
        martes: {
          despertadorPrendido: true,
          horaDespertador: 7
        },
        miercoles: {
          despertadorPrendido: true,
          horaDespertador: 8
        },
        jueves: {
          despertadorPrendido: false,
          horaDespertador: 8
        },
        viernes: {
          despertadorPrendido: true,
          horaDespertador: 7
        },
        sabado: {
          despertadorPrendido: false,
          horaDespertador: 8
        },
        domingo: {
          despertadorPrendido: false,
          horaDespertador: 8
        },
      }
    }
  })
</script>

Resulta en:

Despertador Prendido

Despertador Apagado

Hora Despertador: {{diaActual.horaDespertador}}

Notaras que dias[diaSeleccionado] cambio a diaActual, pero como diaActual no es un valor, sino un valor "calculado" (computed en ingles) en base a otros dos valores dias y diaSeleccionado, no podemos ponerlo en el campo data, para estos datos calculados, vue nos permite especificarlos en el atributo computed, donde cada campo es un nombre y una función que devuelve su valor.

En nuestro caso:

computed: {
  diaActual: function () {
    return this.dias[this.diaSeleccionado];
  }

Ahora podemos decir diaActual en nuestro HTML y vue lo va a reemplazar por el valor de dias[diaSeleccionado].

Notaras que a diferencia del código en v-on:click en el HTML acá cada variable empieza con this., esto es porque en el HTML de vue las únicas variables que podemos acceder son las que están en el campo data y computed de nuestra aplicación, por eso vue nos hace el favor de ponerle this. adelante a cada nombre, cuando pasamos ese código a javascript, el navegador no sabe a que nos estamos refiriendo, puede ser una variable en la función actual, una variable global o un atributo de nuestra aplicación vue, para especificar que nos referimos a un nombre dentro de nuestra aplicación, tenemos que prefijar los nombres con this (que en ingles se traduce a "esto/este" y siempre se refiere al objeto al que pertenece la función que estamos ejecutando.

El otro cambio, mas allá de algunas mejoras estéticas usando clases de bootstrap, es que le cambiamos el tipo al input de number a range y le especificamos dos atributos nuevos min y max, de esta manera el navegador en lugar de mostrar un campo de texto donde se puede escribir cualquier cosa, muestra un "slider" donde solo se puede especificar un valor en el rango valido.

Con eso ya tenemos nuestro despertador con soporte para alarmas por día, en el camino repasamos vuejs y aprendimos sobre campos calculados y el tipo range del tag input.