Dagi3d v4

Actualización de Acts As Flying Saucer

Hace poco me enviaron un correo con un par de bugs que habían encontrado en el plugin que escribí para generar pdfs desde Rails y y­a están solucionados:

­Antes se añadía por defecto la opción x_sendfile a la hora de enviar el pdf al cliente, pero en según qué entornos puede generar un fichero vacio(por ejemplo cuando estemos en desarrollo con mongrel), por lo que ahora se ha eliminado, pero se puede pasar como parámetro de send_file cuando sea necesario:

class FooController < ApplicationController
  
  acts_as_flying_saucer
  
  def index
    render_pdf :send_file => { :filename => 'bar.pdf', :x_sendfile => true}
  end
end­

La librería Flying Saucer necesita que las llamadas a los recursos(imágenes y hojas de estilo) sean llamadas de manera absoluta. Antes el plugin convertía las llamadas en rutas locales pero entonces no se podían usar los assets hosts, por lo que ahora se convierten en llamadas remotas y en caso de no especificar ningún asset host, se utiliza el mismo desde el que se este solicitando el documento:


# HTML 
<%= stylesheet_link_tag("styles.css") %>
<link href="/stylesheets/styles.css?1228586784" media="screen" rel="stylesheet" type="text/css">

<%= image_tag("rails.png") %>
<img alt="Rails" src="/images/rails.png?1228433051">

# PDF
<%= stylesheet_link_tag("styles.css") %>
<link href="http://localhost:3000/stylesheets/styles.css" media="print" rel="stylesheet" type="text/css">

<%= image_tag("rails.png") %>
<img alt="Rails" src="http://localhost:3000/images/rails.png">

Commit

Gracias a Max Williams por el feedback

Cambiando la locale de una clase en Globalize2

En el último proyecto que estuve realizando con RoR necesitaba ofrecer soporte multiidioma para los modelos así que opté por utilizar Globalize2 ya que ya había estado trasteando con esta librería y me parecía muy cómoda de usar(aunque todavía le quedan algunas cosas por pulir).

El caso es que desde hace relativamente poco permite cambiar la locale de una clase en concreto sin necesidad de cambiar el idioma de toda la aplicación. Así se podría tener nuestra aplicación en un idioma determinado y trabajar con una instancia de un modelo en otro totalmente distinto.

El problema era que con la implementación actual, si se cambiaba la locale de una clase en concreto, se cambiaba automáticamente en todas las demás clases que tuvieran campos traducibles con Globalize2:­ 

­I18n.locale = 'es'
Post.locale = 'en'
Post.locale         # en
Category.locale     # en <- WTF?!

Esto sucede por la utilización de variables de clase y la implementación de éstas en Ruby­. Con Ruby, si se modifica el valor de una variable de clase que ha heredado de otra, cambiará en todas las demás clases que hereden de la misma:


class Polygon
  @@sides = 0

  def self.sides
    @@sides
  end
end

class Triangle < Polygon
  @@sides = 3
end

class Square < Polygon
­  @@sides = 4
end

Triangle.sides # 4 <- WTF?!­
Rails provee un mecanismo para solucionar esto a través de los métodos write_inheritable_attribute y read_inheritable_attribute, así que escribí un mini parche para este plugin de Rails. Aquí el commit del fork en Github

Probando Globalize2

Vi que se había liberado hace poco una nueva versión de Globalize, el plugin para Rails que permite tener traducciones de nuestros modelos, así que me animé a probarlo. Ahora es compatible con Rails 2.2 y hace uso de la nueva api para la internacionalización de nuestras aplicaciones. Además, ahora en lugar de tener una única tabla con todas las traducciones, hay que crear una tabla adicional para cada modelo con los campos que queramos que sean traducibles(tal como hace el plugin translate_columns de Samuel Lown). Por ahora hay que escribir la migración a mano, pero se planea hacer un generator para automatizar este paso(si saco un momentillo igual me animo a escribirlo yo, que nunca hice uno).
De momento parece que funciona bastante bien, salvo un detalle que me tuvo entretenido un buen rato y que comento por si alguien se encuentra en la misma situación.

En todos los ejemplos de la documentación aparece que podemos indicar la nueva locale usando un símbolo, pero resulta que si lo hacemos así, no se puede recuperar luego el campo traducido(aunque sí que se guarda correctamente en la base de datos) y lo que hay que hacer es utilizar cadenas en su lugar.

Supongamos que tenemos la clase Post con la siguiente migración:

# Post
class Post < ActiveRecord::Base
  translates :title
end­
­­
# CreatePosts
class CreatePosts < ActiveRecord::Migration
  def self.up
    create_table :posts do |t|
      t.timestamps
    end
    create_table :post_translations, :force => true do |t|
      t.references :post
      t.string :locale
      t.string :title
      t.timestamps
    end
  end

  def self.down
    drop_table :post_translations
    drop_table :posts
  end
end

Podemos ver que si usa­­mos un símbolo para nuestra locale, el campo nos devuelve un nil, pero si usamos una cadena, devuelve el valor correcto:

gambitero:rails-test dagi3d$ ./script/console 
Loading development environment (Rails 2.2.0)
>> I18n.locale = :es
=> :es
>> post = Post.new(:title => 'titulo')
=> #
>> post.title
=> nil
>> I18n.locale = "es"
=> "es"
>> post.title = "titulo"
­=> "titulo"
>> post.title
=> "titulo"
>> I18n.locale = "en"
=> "en"
>> post.title = "title"
=> "title"
>> I18n.locale = "es"
=> "es"
>> post.title
=> "titulo"

También­ comentar que si tenemos traducido un campo en la locale por defecto(inicialmente ésta es 'en-US') e intentamos acceder a un atributo de una que todavía no tiene ningún valor asignado, se devolverá el valor de la locale por defecto. Si queremos evitar este comportamiento basta con comentar la línea 26 del fichero vendor/plugins/globalize2/lib/globalize/locale/fallbacks.rb para que no añada ésta a la lista de fallbacks(otra opción sería redefinir el constructor de la clase Globalize::Locale::FallBacks)

Por último decir que de momento la asociación que se crea entre nuestra clase y la generada por el plugin con las traducciones no se carga con 'eager loading' pero es una cosa que tienen prevista hacer.

Validando las asociaciones de ActiveRecord con RSpec

Después de estar un tiempo sin tocar Rails para nada, me puse el otro día a cacharrerar un poco con RSpec y se me ocurrió escribir un matcher para validar las asociaciones de los modelos, ya que consideraba que un simple @objeto.should respond_to(:metodo) realmente tampoco garantiza nada.
Lo único 'interesante' que puede aportar este código es que a la hora de escribir nuestros specs, basta con poner directamente el nombre de la relación, ya que la clase asociada se obtiene de manera automática(bendita sea la convención sobre la configuración face-smile.png), al contrario que el resto de ejemplos que pude encontrar por ahí, donde se indica la clase y si se desea, se indica aparte el nombre de la relación(y creo que así se aporta algo de legibilidad a los specs) :

@record.should have_many(:songs) # utiliza la clase Song

@record.should belong_to(:artist) # utiliza la clase Artist­

@record.should have_one(:cover) # utiliza la clase Cover

Si fuese necesario también se puede indicar de manera manual la clase del modelo relacionado:

@record.should have_many(:favorite_songs).from_class(Song)
module ARAssociationsMatchers
  
  # ARAssociationMatcher
  #
  class ARAssociationMatcher
  
    def initialize(expected, macro)
      @expected_association = expected
      @expected_macro = macro
    end
    
    def matches?(target)
      @target = target
      
      unless @expected_class.nil?
        expected_class = @expected_class
      else
        expected_class_name = @expected_association.to_s.singularize.camelize
        expected_class = Kernel.const_get(expected_class_name)
      end
      
      reflection = target.class.reflect_on_association(@expected_association)
      
      !reflection.nil? && (reflection.macro == @expected_macro) && (reflection.klass == expected_class)
    end
    
    def from_class(expected_class)
      @expected_class = expected_class
      self
    end
    
    def failure_message
      "expected #{@target.inspect} to #{@expected_macro} #{@expected_association.inspect}, but it didn't"
    end
    
    def negative_failure_message
      "expected #{@target.inspect} not to #{@expected_macro} #{@expected_association.inspect}, but it didn't"
    end
    
  end
  
  # matchers functions
  #
  def have_many(expected)
    ARAssociationMatcher.new(expected, :has_many)
  end

  def have_one(expected)
    ARAssociationMatcher.new(expected, :has_one)
  end
  
  def belong_to(expected)
    ARAssociationMatcher.new(expected, :belongs_to)
  end
  
end
La idea inicial está tomada de ­este enlace

Back to the world

Bueno, después de bastante tiempo sin actualizar voy a ver si vuelvo a retomar el tema del blog. Últimamente han cambiado muchas cosas, tanto en el aspecto laboral/escolar como en el personal(eso sí, todas para bien), así que espero volver a darle caña al tema de Rails que lo tenía bastante abandonado. Me acaban de llegar de Amazon los libros The Ruby Way, The Rails Way(que ya había catado en su versión en pdf y me pareció muy bueno) y Agile Software Development with Scrum, así que ahora toca procrastinar a tope face-smile.png