Próxima reunión de Argentina on Rails

El próximo sábado (1 de noviembre) nos estamos reuniendo para compartir, aprender y divertirnos un rato con ruby. El que quiera aparecer, que aparezca y si quiere avisar que va, mejor así calculo bien la cantidad de mates que tenemos que tener :).

La cita es en la facultad de ingeniería de la UBA, aula 402 (4to piso) a partir de las 11hs. Más detalles de como se va a organizar la cosa acá (incluye un mapa para aquellos que no conozcan la facultad).

Anuncios

Usando Rails.cache en 2.1.x

Para uno de los sitios que tenemos montados llegó la hora de optimizar algunas partes para descargar la DB un poco. Lo primero (y por ahora único :P) que se hizo fue cachear la instancia que representa a la Etapa actual del juego en memoria, para así no cargarla en cada request.

El código original era :

class Stage  ["start_at = ?", t, t])
  end
end

y lo cambiamos para usar el nuevo sistema de cache de Rails 2.1.x que realmente fue simplificado. Hay varios sitio donde hablan sobre el cache en 2.1, les recomiendo mirar el screencast de RailsCasts y este y este post.

El código modificado quedó como se muestra a continuación.

# config.environment.rb
config.cache_store = :mem_cache_store

# app/models/stage.rb
class Stage  ["start_at = ?", t, t])
      Rails.cache.write('stage_current', @current)
    end
    @current
  end

La condición del if es necesaria por dos motivos : la primera por si el cache fue limpiado y la segunda para invalidar el valor guardado actualmente si la etapa eterminó, para que la nueva etapa pase a ocupar el cache.

En este caso estamos usando MemCache como CacheStore ya que se comparte entre más de un webserver y simplifica expirar el cache.

Lo único que resta es expirar el cache en caso de que cambie el modelo, que además de la fecha de finalización contiene textos que son usados en la web (como las bases y condiciones, premios, etc). Para eso creamos un Observer que se encargue de eso cuando una etapa es guardada y además es la etapa actual (si no, no tiene sentido hacer nada) :

# app/models/stage_observer.rb
class StageObserver < ActiveRecord::Observer
  def after_save(stage)
    Rails.cache.delete('stage_current') if stage.is_current?
  end
end

# config/environment.rb
config.active_record.observers = :stage_observer

Con esto bajó bastante el uso de la DB (prácticamente no se puede hacer nada en el sitio sin consultar la etapa actual). Ahora tengo que ver que le pasa a la DB que a veces respondes después de 3 segs :S, pero eso ya escapa a programar :).

Refactoring de “Fat Methods” – Episodio 2

Continuando con esta serie de ejemplos de como refactorizar métodos de controladores vamos a seguir ahora con uno que cuando lo volví a ver apestaba feo feo.

El método en cuestión tiene por objetivo que el usuario actual se suscriba (es decir, anote) para jugar un partido determinado. Para eso el siguiente código es el que está siendo ejecutado actualmente :

  # app/controllers/matches_controllers.rb
  def subscribe
    flash[:notice] = "Ya formás parte del equipo!" and 
        redirect_to match_path(@match) and 
        return if @match.has_player? current_user

    flash[:error] = "El equipo está completo." 
      and redirect_to match_path(@match) and ]
      return if @match.available_places(params[:team].to_i)  current_user, :team => params[:team]

    redirect_to match_path(@match)
  end

Lo feo del problema se puede ver desde dos perspectivas : una que es una excusa y otra que es la razón de la desprolijidad. La excusa para este código tan horrible es que en caso de no poder agregar el jugador al equipo elegido tengo que mostrar un mensaje de error y redireccionar. Pero el redirect_to lo que hace es setear un header nada más, es decir que no hace un return auto-mágico, y de no hacerlo yo el segundo redirect_to (ubicado al final del código) haría que se lance una Exception por haber reenviado los headers.

El problema real es que estoy delegando en el controlador la responsabilidad de la clase Player para determinar si es válido crear una instancia para un partido y usuario dado.

Es por eso que lo primero que debemos hacer es usar los validators de Rails para esta tarea :

  # app/models/player.rb
  def validate
    errors.add_to_base("Ya formás parte del equipo!") if match.has_player? user
    errors.add_to_base("El equipo está completo.") if match.available_places(team) < = 0
  end

En este caso los errores no dependen de un atributo, por lo que usamos add_to_base para que los errores digan lo que queremos. Este método validate es llamado por Rails al crear o actualizar una instancia de Player, y si hay algún error nunca llega a la DB.

Habiendo quitado la validación del controlador ahora podemos escribirlo de una forma más prolija y entendible :

  # app/controllers/matches_controllers.rb
  def subscribe
    @player = @match.players.create :user => current_user, :team => params[:team].to_i

    flash[:notice] = @player.errors.full_messages.join(", ") if @player.errors.any?

    redirect_to match_path(@match)
  end

Lovely :). Se crea un Player para el Match, se prepara una alerta visual en caso de que haya algún error y luego se redirige a la página del partido. Si ven el antes y el después creo que nadie me va a negar de la mejora :).

Por hoy es todo, hasta la próxima entrega!.

Refactoring de “Fat Methods” – Episodio 1

Luego de escribir mucho código uno comienza a mirar a ver que hacen los de al lado, y al mirar se da cuenta de lo mal que uno escribe código cuando comienza con un nuevo lenguaje :). Algo de eso me pasa hoy en día con ¡Falta Uno!.

En el camino de emprolijar las cosas para liberar el código tuve la necesita de poner a refactorizar el código porque hay partes que no son nada bonito. Voy a tratar de plasmar en los siguientes posts algunas lecciones aprendidas junto con el paso a paso del refactoring del código.

Antes que nada y como buena práctica es ideal tener buenos test sobre los que se va a refactorizar ya que es muy fácil romper alguna funcionalidad que se suponía que andaba. Si bien para los ejemplos acá voy a ignorar esta buena práctica, no se los aconsejo :).

Vamos a empezar con el método create de controlador Matches, encargado de crear un nuevo partido :

  def create
    @match = current_user.matches.create(params[:match])
    if @match.errors.empty?
      Player.create :match => @match, :user => current_user

      @notifications = @match.owner.friends.select {|f| f.notify_new_matches? }.collect(&:email)
      Emailer.deliver_match_created(@match, @notifications) if @notifications.any?

      flash[:success] = "El partido fue creado."
      redirect_to matches_path
    else
      render :action => 'new'
    end
  rescue
    render :action => 'new'
  end

Asusta un poco, ¿no?. Para entender el problema, veamos primero como que involucra crear un nuevo partido :

  • Crear el partido
  • Anotar al usuario que creó el partido en uno de los equipos
  • Notificar a todos los amigos que se creó un nuevo partido

Dado esto, pasemos a analizar los problemas. La primer pregunta que habría que hacerse es : ¿Quién es el responsable de anotar al jugador?, ¿Tiene sentido que un usuario pueda crear un partido sin que esté anotado?. En el contexto nuestro, la respuesta a la última pregunta es “No”, solo porque así lo defino yo :). Entonces podemos responder a la primer pregunta : El Match debería ser el responsable de anotar al dueño del partido cuando es creado.

Para lograr esto último lo que debemos hacer es mover la lógica de creación del Player dentro de un callback en el modelo Match, quedando algo así :

class Match  self, :user => owner
  end
end

De esta forma nos aseguramos que se cumpla lo pedido sin correr el riesgo de que se puedan crear matches sin que el owner este anotado inicialmente. También ganaríamos en el caso de que exista alguna otra forma de crear un partido, por ejemplo si agregáramos la posibilidad de crear un partido enviando un email, no tendríamos la necesidad de repetir la lógica inicial en dos lugares diferentes.

Y ahora nuestro controlador queda algo cómo :

  def create
    @match = current_user.matches.create(params[:match])
    if @match.errors.empty?
      @notifications = @match.owner.friends.select {|f| f.notify_new_matches? }.collect(&:email)
      Emailer.deliver_match_created(@match, @notifications) if @notifications.any?

      flash[:success] = "El partido fue creado."
      redirect_to matches_path
    else
      render :action => 'new'
    end
  rescue
    render :action => 'new'
  end

Lo siguiente es limpiar un poco lo lógica del modelo. Una cosa que molesta a la vista son las dos llamadas a render, ¿son realmente necesarias? La respuesta en NO. Con el rescue lo que hacemos es que dada una Exception vuelva al formulario de creación, lo mismo que si el partido no es válido, por lo que simplemente podríamos haber usado el método create! que lanza una Exception si el modelo no es válido evitándonos así el molesto if. Quedando ahora :

  def create
    @match = current_user.matches.create!(params[:match])

    @notifications = @match.owner.friends.select {|f| f.notify_new_matches? }.collect(&:email)
    Emailer.deliver_match_created(@match, @notifications) if @notifications.any?

    flash[:success] = "El partido fue creado."
    redirect_to matches_path
  rescue
    render :action => 'new'
  end

Que si lo comparan con la versión inicial es mucho más legible. “Fat model, skinny controllers” es la clave a seguir. Todavía queda por mejorar la forma en que se genera la lista de emails de los amigos que desean recibir la notificación, pero no es algo que me moleste de momento.

En otras entregas seguiré mostrando el resto del refactoring de otro métodos que son aún peores que este :).

Simple Forum plugin for Rails

La semana pasada tenía la necesidad de agregar un foro super simple para un cliente, basicamente querían que los usuarios del sitio puedan dejar comentarios de la campaña, hablen entre ellos y no mucho más. Ya tenía uno hecho todo mezclado en otra aplicación por lo que decidí que era hora de reescribir menos :).

Lo primero que se me pasó por la mente “algo ya debe haber hecho” y si bien es cierto, lo poco que encontré se apoya en Engines lo cual para mi no es aceptable. Simplemente no me gusta Engines :). La mejor opción tal vez es Beast pero integrarlo con otra aplicación no es simple y tener el foro de forma externa no era una opción.

Por lo que decidí empezar un “site project” a partir del código que ya tenía, que al haberlo escrito hace mucho era horrible por lo que el resultado fue casi casi que codeado desde 0.

La idea es que en 3 pasos uno pueda tener el foro andando integrado en el sitio, y creo que mas o menos se logra con algunos “problemas”. El principal tema es que al estar los controllers en el plugin, y estos no se recargan en development, es medio molesto usarlo porque depende de que se haga hay que reiniciar el server a cada rato (tira unos stack level too deph”).

Acá está el código para el que quiera probarlo, testearlo y recomendar alguna alternativa. Sobre todo si encuentran errores de gramática/sintaxis en la poca documentación que hice se agradecería :).

Multiupload de imágenes con Prototype

Desde hace unos días que estoy haciendo un widget que soporte upload de múltiples archivos para una aplicación web. No fue fácil el comienzo pero despues de varias horas (unas 8 hasta este momento) ya va tomando forma.

Para poder trackear el upload de cada archivo utilizo apache_mod_upload_progress, un genio “Drogomir” :). Para compilarlo en OSX tuve algunos problemas ya que apache2 esta compilado en x86_64 y el default del apsx es x86 pero googleando se encuentra fácil como pasarle el parámetro al gcc. Lo otro que necesitamos tener instalado es mod_rails y apache 2.2. Cuando termine el código y lo publique estará todo explicado en detalle :).

El segundo problema grande fue el formulario. Para hacer el upload lo que hago es crear un iframe oculto y cambiar el target del formulario a ese frame (de esta menera si no tenemos javascript la aplicación degrada automáticamente al upload de imagenes individuales y el usuario no se entera), pero claro, necesitaba tener múltiples input:file, uno por cada archivo a subir. De ponerlos todos juntos tendríamos un POST super gigante que no era lo que se buscaba ya que no podría trackear cada upload por separado.

La solución fue, cada vez que se selecciona un archivo sacar el INPUT del form y guardarlo en un array. El espacio vacío se reemplaza con un nuevo INPUT y como todo es tán rápido, uno no se da cuenta. El problema llegó cuando terminaba el primer archivo, tenía que volver a agregar el siguiente file al formulario y hacer otra vez el submit. Pero si uno llama $(form).submit() desde javascript, el callback onSubmit no es ejecutado (defecto de las implementaciones de todos los navegadores que probé y parece que no va a cambiar) por lo que no era útil.

La solución finalmente fue simular el click enel botón enviar con un simple $(submit_button).click() que resuelve el problema anterior. Les dejo el video para que lo disfruten :).

httpv://www.youtube.com/watch?v=7PqZg_1Pi1w

Jornadas Regionales 2008 – Día 1

Día agotador, como hace mucho que no sufría :D.

Como todos los años arrancaron las Jornadas Regionales cuyo organizador en esta oportunidad es Cafelug. Los chicos arrancaron a pleno, super organizados, aunque como siempre sobre el final se olvidan de algunos detalles en la marea de gente consultando cosas.

Yo arranqué temprano dando una charla de Software Libre para Windows orientado a home/office user, sin tocar siquiera temas más de coding. No es una charla que me super guste pero fue “por encargo” y no me costaba nada sumar.

A la tarde fui a dar la segunda (y espero nunca repetir el error de dar dos charlas el mismo día, a mi garganta no le hace gracia :P) : SocialApps con Ruby On Rails. Fue un poco rara esta, porque iba a ser un taller pero no pude al final, así que hice un lindo cookbook y creo que a la gente le gustó. Estoy filmado en dos ángulos distintos, después tengo que conseguir los videos, y obviamente, destruirlos :D.

Como asistenmte estuve en muy pocas la verdad, más que nada porque me colgaba charlando con gente que hace mucho que no veo.

Vi un poco de Introduction to building RPM packages, a un amigo que debutaba en las jornadas con Desarrollo de aplicaciones Sociales. y la mitad de Mozilla and Browsers over the last 10 years. porque me tenía que ir a dar clases.

Este año el regalo por dar charlas me vino barbaro : un mate!. Si, el mío ya tenía que jubilarse por lo que me viene al pelo. Es lindo ver como estos eventos crecen año a año.