Galería Photography Prize de Google

Ilmari Heikkinen

Sitio web de Google Photography Prize

Recientemente, lanzamos la sección Galería en el sitio de Google Photography Prize. La galería muestra una lista de desplazamiento infinito de fotos recuperadas de Google+. Esta obtiene la lista de fotos de una app de AppEngine que usamos para moderar la lista de fotos de la galería. También lanzamos la app de galería como un proyecto de código abierto en Google Code.

Página de la galería

El backend de la galería es una app de App Engine que usa la API de Google+ para buscar publicaciones que contengan uno de los hashtags del premio Google Photography Prize (p.ej., #megpp y #travelgpp). Luego, la app agrega esas publicaciones a su lista de fotos no moderadas. Una vez por semana, nuestro equipo de contenido revisa la lista de fotos no moderadas y marca las que infringen nuestros lineamientos de contenido. Después de presionar el botón Moderar, las fotos no marcadas se agregan a la lista de fotos mostradas en la página de la galería.

El backend de moderación

El frontend de Galería se compila con la biblioteca de Google Closure. El widget de la Galería en sí es un componente de Cierre. En la parte superior del archivo fuente, le indicamos a Closure que este archivo proporciona un componente llamado photographyPrize.Gallery y requiere las partes de la biblioteca de Closure que usa la app:

goog.provide('photographyPrize.Gallery');

goog.require('goog.debug.Logger');
goog.require('goog.dom');
goog.require('goog.dom.classes');
goog.require('goog.events');
goog.require('goog.net.Jsonp');
goog.require('goog.style');

La página de la galería tiene algo de JavaScript que usa JSONP para recuperar la lista de fotos de la app de App Engine. JSONP es un truco simple de JavaScript de origen cruzado que inserta una secuencia de comandos similar a jsonpcallback("responseValue"). Para controlar el contenido de JSONP, usamos el componente goog.net.Jsonp en la biblioteca de Closure.

La secuencia de comandos de la galería recorre la lista de fotos y genera elementos HTML para que las muestre en la página de la galería. El desplazamiento infinito funciona cuando se conecta al evento de desplazamiento de la ventana y se carga un nuevo lote de fotos cuando el desplazamiento de la ventana está cerca de la parte inferior de la página. Después de cargar el nuevo segmento de la lista de fotos, la secuencia de comandos de la galería crea elementos para las fotos y los agrega al elemento de la galería para mostrarlas.

Muestra la lista de imágenes

El método de visualización de lista de imágenes es bastante básico. Recorre la lista de imágenes y genera elementos HTML y botones de +1. El siguiente paso es agregar el segmento de lista generado al elemento de galería principal de la galería. Puedes ver algunas convenciones del compilador de Closure en el siguiente código, observa las definiciones de tipos en el comentario de JSDoc y la visibilidad de @private. Los métodos privados tienen un guion bajo (_) después de su nombre.

/**
 * Displays images in imageList by putting them inside the section element.
 * Edits image urls to scale them down to imageSize x imageSize bounding
 * box.
 *
 * @param {Array.<Object>} imageList List of image objects to show. Retrieved
 *                                   by loadImages.
 * @return {Element} The generated image list container element.
 * @private
 */
photographyPrize.Gallery.prototype.displayImages_ = function(imageList) {
  
  // find the images and albums from the image list
  for (var j = 0; j < imageList.length; j++) {
    // change image urls to scale them to photographyPrize.Gallery.MAX_IMAGE_SIZE
  }

  // Go through the image list and create a gallery photo element for each image.
  // This uses the Closure library DOM helper, goog.dom.createDom:
  // element = goog.dom.createDom(tagName, className, var_childNodes);

  var category = goog.dom.createDom('div', 'category');
  for (var k = 0; k < items.length; k++) {
    var plusone = goog.dom.createDom('g:plusone');
    plusone.setAttribute('href', photoPageUrl);
    plusone.setAttribute('size', 'standard');
    plusone.setAttribute('annotation', 'none');

    var photo = goog.dom.createDom('div', {className: 'gallery-photo'}, ...)
    photo.appendChild(plusone);

    category.appendChild(photo);
  }
  this.galleryElement_.appendChild(category);
  return category;
};

Cómo controlar los eventos de desplazamiento

Para ver cuando el visitante se desplazó por la página hasta el final y necesitamos cargar imágenes nuevas, la galería se conecta al evento de desplazamiento del objeto window. Para ocultar las diferencias en las implementaciones de navegadores, usamos algunas funciones de utilidad útiles de la biblioteca de Closure: goog.dom.getDocumentScroll() muestra un objeto {x, y} con la posición actual de desplazamiento del documento, goog.dom.getViewportSize() muestra el tamaño de la ventana y goog.dom.getDocumentHeight() la altura del documento HTML.

/**
 * Handle window scroll events by loading new images when the scroll reaches
 * the last screenful of the page.
 *
 * @param {goog.events.BrowserEvent} ev The scroll event.
 * @private
 */
photographyPrize.Gallery.prototype.handleScroll_ = function(ev) {
  var scrollY = goog.dom.getDocumentScroll().y;
  var height = goog.dom.getViewportSize().height;
  var documentHeight = goog.dom.getDocumentHeight();
  if (scrollY + height >= documentHeight - height / 2) {
    this.tryLoadingNextImages_();
  }
};

/**
 * Try loading the next batch of images objects from the server.
 * Only fires if we have already loaded the previous batch.
 *
 * @private
 */
photographyPrize.Gallery.prototype.tryLoadingNextImages_ = function() {
  // ...
};

Imágenes de carga

Para cargar las imágenes del servidor, usamos el componente goog.net.Jsonp. Se necesita un goog.Uri para realizar la consulta. Una vez creada, puedes enviar una consulta al proveedor de JSONp con un objeto de parámetro de consulta y una función de devolución de llamada exitosa.

/**
 * Loads image list from the App Engine page and sets the callback function
 * for the image list load completion.
 *
 * @param {string} tag Fetch images tagged with this.
 * @param {number} limit How many images to fetch.
 * @param {number} offset Offset for the image list.
 * @param {function(Array.<Object>=)} callback Function to call
 *        with the loaded image list.
 * @private
 */
photographyPrize.Gallery.prototype.loadImages_ = function(tag, limit, offset, callback) {
  var jsonp = new goog.net.Jsonp(
      new goog.Uri(photographyPrize.Gallery.IMAGE_LIST_URL));
  jsonp.send({'tag': tag, 'limit': limit, 'offset': offset}, callback);
};

Como se mencionó anteriormente, la secuencia de comandos de la galería usa el compilador de Closure para compilar y reducir el código. El compilador de Closure también es útil para aplicar la escritura correcta (usas la notación JSDoc @type foo en tus comentarios para configurar el tipo de propiedad) y también te avisa cuando no tienes comentarios para un método.

Pruebas de unidades

También necesitábamos pruebas de unidades para la secuencia de comandos de la galería, por lo que es útil que la biblioteca de Closure tenga un framework de prueba de unidades integrado. Sigue las convenciones de jsUnit, por lo que es fácil comenzar a usarla.

Para ayudarme a escribir las pruebas de unidades, escribí una pequeña secuencia de comandos de Ruby que analiza el archivo JavaScript y genera una prueba de unidades fallida para cada método y propiedad del componente de la galería. Un {i>script<i} como:

Foo = function() {}
Foo.prototype.bar = function() {}
Foo.prototype.baz = "hello";

El generador de pruebas genera una prueba vacía para cada una de las propiedades:

function testFoo() {
  fail();
  Foo();
}

function testFooPrototypeBar = function() {
  fail();
  instanceFoo.bar();
}

function testFooPrototypeBaz = function() {
  fail();
  instanceFoo.baz;
}

Estas pruebas autogeneradas me ayudaron a comenzar fácilmente a escribir pruebas para el código, y todos los métodos y propiedades se abordaron de forma predeterminada. Las pruebas fallidas crean un efecto psicológico agradable en el que tenía que pasar por las pruebas una por una y escribir pruebas adecuadas. Junto con un medidor de cobertura de código, es un juego divertido hacer que las pruebas y la cobertura sean de color verde.

Resumen

Galería+ es un proyecto de código abierto que muestra una lista moderada de fotos de Google+ que coinciden con un #hashtag. Se construyó con Go y la biblioteca de Closure. El backend se ejecuta en App Engine. Galería+ se usa en el sitio web del Premio de fotografía de Google para mostrar la galería de postulaciones. En este artículo, revisamos las partes interesantes de la secuencia de comandos del frontend. Mi colega Johan Euphrosine, del equipo de Relaciones con Desarrolladores de App Engine, escribe un segundo artículo sobre la app de backend. El backend está escrito en Go, el nuevo lenguaje del servidor de Google. Si te interesa ver un ejemplo de producción del código de Go, mantente alerta.

Referencias