Archive for septiembre, 2011

Ruby on Rails, primeras impresiones


Después de 4 años sin programar (profesionalmente), me he puesto a actualizarme. La “gente guay” está muy metida en el stack de Ruby on Rails y jScript. Llevo una semana en el ajo y estas son las primeras impresiones:

  • La curva de aprendizaje es pronunciada: No solamente hay que aprender Ruby. Encima de Ruby está Rails, un framework MVC, con un nivel de abstracción muy alto. Incluso con una aplicación sencilla, las herramientas automáticas crean muchos directorios, código y configuración que me han hecho perderme varias veces.
  • Ruby es un lenguaje denso: Estilo Perl, una línea de Ruby tiene mucho significado. La legibilidad del código se resiente inicialmente (aunque los Ruby-istas presumen de que es un lenguaje muy cercano al humano).
  • No todas las empresas de hosting soportan las ultimas versiones de Ruby: Había asumido, equivocadamente, que cualquier hosting soportaría RoR (Ruby on Rails). Para mi sorpresa, mi host actual, OVH, no soporta Rails 3 ni Passenger. Estoy buscando un host económico para poner mi aplicación.
  • Desarrollar en Ruby es rápido: En menos una semana ya tengo una aplicación que lee un XML, lo parsea y lo convierte en JSON.
    Aunque hay tutoriales y mucha información por la red, hay muchas maneras de hacer las cosas y todo el mundo compite diciendo “yo soy el mejor, usa mi programita/librería/herramienta”. Al final hay que hacer un collage para que las cosas funcionen.
  • “Solo se que no se nada, en comparación con lo que me queda por saber”: Cuanto más me meto, más cosas veo: Capistrano, Git, Passenger, Gems
  • -

Contando el número de “likes” en un blog WordPress

Desde que incluimos un botón “me gusta” en todas las páginas de Los Fogg, creamos una página de Los Fogg en Facebook, y enlazamos la cuenta de Twitter con el muro de Los Fogg, Facebook es nuestra fuente de tráfico número uno. No hay duda que tanto Facebook como Twitter se han convertido en el nuevo RSS, una tecnología que nunca llegó a conseguir seguidores entre los no-geeks (los infieles).

Hemos subido más de 600 fotos al blog desde agosto de 2010 y tenía curiosidad por saber cuales eran las que tenían más “me gusta” en Facebook. El método cro-magnon es mirarse todas las páginas y apuntar el número que sale en el botón “me gusta”. O refinando un poco, ponerlas en un Excel para luego ordenarlas automáticamente.

Oigo una voz, mi geek interno, el maestro Yoda, que me dice: “Joven Padawan, esto es escriptable” (si el verbo escriptar no está en la DRAE, que alguien les escriba un email). Vamos a ver lo que dice el SDK de Facebook:

“El SDK de Javascript pone a tu disposición un extenso conjunto de funcionalidades para acceder desde el cliente al API de los servidores de Facebook. Estas incluyen toda la funcionalidad del REST API, Graph API y Dialogs. También dota a los desarrolladores de un mecanismo para renderizar versiones XFBML de nuestros Plugins Sociales.

Tienes que tener un app id para inicializar el SDK, que se puede obtener de la Developer App.”

Vale. No entiendo nada. Esto de pasarme 4 años sin programar no es bueno, ya me lo decía mi abuela. A ver por donde le hinco a esto el diente:

  • Me tengo que sacar el “app id”. Fácil.
  • Tengo que investigar que es el XFBML
  • Tengo que investigar que es el Graph API

XFBML

Según he entendido en el parrafazo anterior, es una manera de renderizar los Plugins Sociales (botón “me gusta”, etc.). Tirando un poco del hilo, veo que es una extension de XHTML con tags propios para añadir versiones precocinadas de los Plugins Sociales a una página web (la otra opción es usar iframes). No voy a seguir leyendo esta tarjetita.

Graph API

Esto ya es más interesante. En la documentación, el Graph API está en la sección de conceptos fundamentales (core concepts):

“El Graph API presenta una vista sencilla y consistente del grafo social de Facebook, representando de manera unforme los objetos en el grafo (gente, fotos, eventos, páginas, etc.) y las conexiones entre ellos (relaciones entre amigos, contenido compartido, etiquetas de fotos, etc.).

Cada objeto en el grafo tiene un identificador único y se puede acceder en https://graph.facebook.com/identificador.”

Es decir, ¿que puedo acceder al nodo de mi página Facebook de Los Fogg en https://graph.facebook.com/losfogg

{
"id": "161137637259282",
"name": "Los Fogg",
"picture": "http://profile.ak.fbcdn.net/hprofile-ak-ash2/50254_161137637259282_2241030_s.jpg",
"link": "http://www.facebook.com/losfogg",
"likes": 185,
"category": "Personal blog",
"website": "http://losfogg.com",
"username": "losfogg",
}

Esto ha sido fácil. ¿Y si intento ver la cuenta de “likes” de una página en concreto? Vamos a probar con la URL https://graph.facebook.com/http://losfogg.com/2011/08/09/banya-la-sauna-rusa/

{
"id": "http://losfogg.com/2011/08/09/banya-la-sauna-rusa/",
"shares": 5
}

Bingo.

Listando las entradas en WordPress

Esto parece fácil: creo una página en WordPress que liste todas las URLs de las entradas (en mi caso, de la categoría “pics”) y luego le paso las URL al Javascript API y listo. Para hacer esto, me creo una plantilla de página de WordPress, y crear una nueva página que use esa plantilla (se puede cambiar en “atributos de la página”):

<span class="Apple-style-span" style="font-family: Consolas, Monaco, monospace; font-size: 12px; line-height: 18px; white-space: pre;">/* </span>
Template Name: Plantilla para contar likes de Facebook 
*/
?>
<html>
    <head><title>Facebook likes test</title></head>
    <body>
        <ol id="link_list">
        <?php
            global $post;
            $args = array( 'posts_per_page' => -1, 'category_name' => 'pics' );
            $myposts = get_posts( $args );
            foreach( $myposts as $post ) :	
                setup_postdata($post); ?>
                <li><?php the_permalink(); ?></li>
            <?php endforeach; ?>
        </ol>
  </body>
</html>

Llamando al Javascript API de Facebook

Siguiendo el manual del API, lo primero es cargar el Javascript SDK en nuestra plantilla de WordPress:

<!-- Cargamos el Javascript API de Facebook -->
<div id="fb-root"></div>
<script src="http://connect.facebook.net/en_US/all.js"></script>
<script>
  FB.init({
    appId  : 'PON_AQUI_TU_ID',
    status : true, // check login status
    cookie : true, // enable cookies to allow the server to access the session
    xfbml  : true, // parse XFBML
    oauth  : true // enable OAuth 2.0
  });
</script>

Y ahora solamente tenemos que consultar el grafo con el Javascript API. El método a usar es FB.api(), que como primer argumento toma la URL del grafo, y como segundo, una función callback… ojo… que estas son asíncronas, es decir que cuando el Javascript API haya terminado de consultar el valor a los sevidores de Facebook, entonces llamará a la función, no necesariamente siguiendo el órden en el que tenemos escritp el programita.

El pequeño lío aqui es que primero genera el HTML/javascript al ejecutar el PHP, y luego, dependiendo de las acciones en la página (onLoad, onClick, etc.), se ejecutará el Javascript. Para no liarme, uso el método “divide y vencerás”: primero vamos a llamar al Javascript API con un valor hardcoded (“a piñón fijo”) y luego vamos a ir añadiendo funcionalidades:

<script type="text/javascript">
  function alertame() {
      FB.api("/http://losfogg.com/2011/08/09/banya-la-sauna-rusa/", function(response) {
              alert(response.shares);
      });
  }
</script>

La función alertame() la llamamos en el evento onload del elementoy efectivamente, nos sale una ventanita con el número de likes. Vamos bien.

Mezclando el PHP con el Javascript: ¿DOM o no DOM?

Idealmente, en el PHP, cuando generamos nuestro

  • con el permalink, podríamos llamar al Javascript para que nos averiguara el número de likes, y así lo añadimos. Pero el problema es que el Javascript se ejecuta en el navegador, y el PHP en el servidor, así que tenemos que buscar otra manera.

Una opción sería usar el PHP API de Facebook que tan ricamente hemos ignorado desde el principio. Pero ya estamos tan cerca… ya casi saboreo la victoria (que inocente, si yo hubiera sabido…).

La siguiente opción que se me ocurre es, una vez generado el HTML, podemos recorrer el DOM, averiguar las URLs del grafo y hacer unas llamaditas al Javascript. Lo que pasa es que lo de manipular el DOM suele ser muy costoso computacionalmente y me da la impresión de que me voy a meter en camisa de once varas.

La opción que me parece más viable y fácil es hacer que el PHP me genere el Javascript directamente:

  • Declaramos un Array que contenga todas las URLs
  • Por cada URL, hacemos la llamada al API de Facebook y obtenemos el número de likes
  • Añadimos un elemento a la lista manipulando el DOM lo justo
<script type="text/javascript">
  function creaLista() {
       // Crea un array con los enlaces a las paginas
      var allLinks = new Array( <?php
      global $post;
      $args = array( 'posts_per_page' => -1, 'category_name' => 'pics' );
      $myposts = get_posts( $args );
      foreach( $myposts as $post ) :	
         setup_postdata($post); ?> "<?php the_permalink(); ?>", 
         <?php endforeach; ?> "popme");
      allLinks.pop(); // Truco sucio: he puesto un elemento de mas, 
      // popme, para no tener que borrar la ultima coma 

      // Itera el Array 
      // aqui ponemos los elementos, un poquito de DOM
      var linkListRef = document.getElementById("link_list"); 

      for (var i=0;i<allLinks.length;i++) {
          var oneLink = allLinks[i];
          var fbGraphNode = "/"+oneLink; 
          // preceder con slash, para que se haga un elemento del grafo

          // OJO, llamada asincrona 
          FB.api(fbGraphNode, function(response) {
                  // Crear el <li>
                  var anElement = document.createElement("li");
                  anElement.appendChild(document.createTextNode("("+response.shares+") "));
                  // elemento <a>
                  var linkElement = document.createElement("a"); 
                  // lo que va entre <a> y </a>
                  linkElement.appendChild(document.createTextNode(response.id)); 
                  linkElement.setAttribute("href",response.id); // el href
                  // ponemos el <a> dentro del <li>
                  anElement.appendChild(linkElement);
                  // ponemos el <li> dentro del <ol> (variable global)
                  linkListRef.appendChild(anElement); 
         });
      }
  }
</script>

En la parte de HTML pura, hay que darle un id al elemento en el que queremos crear la lista:

<ol id="link_list"></ol>

El script de arriba es bastante denso, pero he puesto comentarios que espero que os ayuden a clarificar un poco lo que voy haciendo.

Ordenando los resultados: problemas con los arrays asociativos en Javascript

Para rematar la faena, solamente tenemos que ordenar la lista por orden descendente de likes. Googleando un poco, veo que Javascript tiene una estructura de datos muy parecida a las tablas hash de PHP. Ponemos ahí los URLs como key y el número de likes como valor, ordenamos y ya ¿no?.

Para ahorraos el disgusto y la pérdida de tiempo, os cuento: los arrays asociativos de Javascript no se comportan como Arrays, sino simplemente como objetos a los que estamos añadiendo propiedades. Cuando añadimos myArray [key] = “value” estamos creando una propiedad “key” en el objeto myArray. El resultado es que al ponerse a iterar, me salían muchas más keys de las que había añadido y se liaba el tema al intentar llamar al API de Facebook.

Lo mejor es usar un Array de tuplas: myArray.push([key, value]), es decir, un array de arrays. En el script anterior, la llamada al API de Facebook quedaría:

    
FB.api(fbGraphNode, function(response) {
          linkArray.push([response.id, response.shares]);
      });

Ahora solamente nos queda ordenar ese Array, para lo cual podemos redefinir la función sort() del array, para que sepa como comparar nuestras tuplas con la URL y el número de likes.

linkArray.sort(function(a, b) { // Gracias http://bit.ly/pvzJIV                  
                  a = a[1];
                  b = b[1];

                  if(a == undefined) a = Number.MIN_VALUE;
                  if(b == undefined) b = Number.MIN_VALUE;

                  return a < b ? 1 : (a > b ? -1 : 0);
              });

Solamente una pega: No podemos hacer la iteración sobre linkArray directamente después de la llamada a FB.api, porque las llamadas son asíncronas. Si lo hacemos, iteraríamos sobre un array vacío, ya que el API no ha devuelto ningún valor aún, cuando nuestro hilo de ejecución hace la iteración.

La solución es solamente hacerlo cuando linkArray esté lleno, al volver de la última llamada al API. El PHP final se nos queda así:

<body onload="creaLista();">
<!-- Cargamos el Javascript API de Facebook -->
<div id="fb-root"></div>
<script src="http://connect.facebook.net/en_US/all.js"></script>
<script>
  FB.init({
    appId  : '185062228233114',
    status : true, // check login status
    cookie : true, // enable cookies to allow the server to access the session
    xfbml  : true, // parse XFBML
    oauth  : true // enable OAuth 2.0
  });
</script>

<script type="text/javascript">
<!--
    function creaLista() {
       // Crea un array con los enlaces a las paginas
      var allLinks = new Array( <?php
      global $post;
      $args = array( 'posts_per_page' => -1, 'category_name' => 'pics' );
      $myposts = get_posts( $args );
      foreach( $myposts as $post ) :	setup_postdata($post); ?> "<?php the_permalink(); ?>", <?php endforeach; ?> "popme");

      allLinks.pop(); // Truco sucio: he puesto un elemento de mas, popme, para no tener que borrar la ultima coma 

       // Itera el Array 
      var linkListRef = document.getElementById("link_list"); // aqui ponemos los elementos, un poquito de DOM
      var linkArray = []; // Aqui ponemos los elementos para ordenarlos. Es un array de tuplas.

      for (var i=0;i<allLinks.length;i++) {
          var oneLink = allLinks[i];
          var fbGraphNode = "/"+oneLink; // preceder con slash, para que se haga un elemento del grafo de FB

          // OJO, llamada asincrona 
          FB.api(fbGraphNode, function(response) {
              linkArray.push([response.id, response.shares]);
              if (linkArray.length == allLinks.length) { // El array esta lleno 
                  linkArray.sort(function(a, b) { // Thanks http://bit.ly/pvzJIV                  
                      a = a[1];
                      b = b[1];

                      if(a == undefined) a = Number.MIN_VALUE;
                      if(b == undefined) b = Number.MIN_VALUE;

                      return a < b ? 1 : (a > b ? -1 : 0);
                  });
                  for (var j=0;j<linkArray.length;j++) {
                      var anElement = document.createElement("li");
                      anElement.appendChild(document.createTextNode("("+linkArray[j][1]+")"));
                      var linkElement = document.createElement("a");
                      linkElement.appendChild(document.createTextNode(linkArray[j][0]));
                      linkElement.setAttribute("href",linkArray[j][0]);
                      anElement.appendChild(linkElement);
                      linkListRef.appendChild(anElement);
                  }
              }
          });
       } 
    }
-->
</script>

<ol id="link_list">

</ol>
</body></html>