Archivo de la categoría ‘ruby on rails’

validates uniqueness y Shoulda

Miércoles, 31 de agosto de 2011

Generalmente utilizo Factory Girl y Shoulda para testear aplicaciones rails. El primero os lo recomiendo y sobre Shoulda, aunque siempre me da algún que otro dolor de cabeza, también me da muchas satisfacciones.

Hoy me encontraba en la tesitura de testear que un atributo tuviera un valor único.

class Example < ActiveRecord::Base
  validates :name, :uniqueness => true
end

Si incluimos el test correspondiente con shoulda nos da un error del tipo should require case sensitive unique value for … Para solucionarlo basta con incluir la primera línea que podéis leer en el test:

class ExampleTest < ActiveSupport::TestCase
  subject { Factory(:example) }
  should validate_uniqueness_of(:name)
end

should validate_uniqueness_of necesita tener un registro creado y con subject conseguimos tal objetivo. Podéis obtener más información sobre subject, que es bastante interesante, en la documentación de Shoulda.

Comenzar un proyecto desde 0 con Ruby 1.9.2, Rails 3.1 y Mongodb

Sábado, 27 de agosto de 2011

En lo último que ando metido en mis ratos libres es en aprender un poco sobre otros tipos de base de datos. Usualmente suelo trabajar con MySQL pero tenía ganas de probar una base de datos documental, en este caso, MondoDB.

Para trastear MongoDB he empezado un proyectillo simple donde aplicar todo lo que vaya aprendiendo. El proyecto será en Rails 3.1 y utilizaré Mongoid, un ODM (Object-Document-Mapper) muy amigable para los que ya estamos acostumbrados a ActiveRecord. En este primer post os voy a contar los pasos que he seguido para poner en pie el entorno de trabajo:

Antes que nada, tendremos en cuenta que vamos a utilizar Ruby 1.9.2. Para trabajar con diferentes entornos de desarrollo Ruby utilizo RVM. Una vez tengas instalado Ruby 1.9.2, cambia a ese entorno de desarrollo con rvm use 1.9.2.

Ahora a instalar MongoDB:

brew update
brew install mongodb

Con la primera línea actualizamos las formulas de homebrew, el instalador de paquetes con el que trabajo. Si no lo conoces, dale una oportunidad, merece la pena. Una vez instalado mongodb, sigue los pasos que te vayan indicando y, para comprobar que todo ha ido correctamente, prueba a ejecutar mongod y acceder a http://localhost:28017.

Lo siguiente es empezar el proyecto de rails. Lo importante es evitar que utilice ActiveRecord:

rails new myproject -O

Esto basicamente crea tu proyecto Rails con todo lo necesario para trabajar con MongoDB. Puedes ver qué hace realmente viendo la documentación de MongoDB, donde explican cómo trabajar con Rails.

Ahora vamos a hacer un alto en el camino. Es una buena práctica con rvm definir un gemset por proyecto, para evitar posibles conflictos. Lo explica muy bien Javi Baena en su blog. Accedemos al directorio de nuestro proyecto /myproject y creamos un fichero .rvmrc que incluya lo siguiente:

rvm --create use 1.9.2@myproject

Sal del directorio del proyecto y vuelve a entrar. Si no había ningún gemset con el nombre myproject lo creará y cambiará a él de manera automática. Como ves, esto es de mucha ayuda cuando tienes varios proyectos en diferentes entornos de desarrollo, no tienes que estar pensando qué entorno utiliza cada uno y demás. Muy útil.

Bien, ahora vamos con el último paso, Mongoid. Simplemente incluye en tu Gemfile:

gem "mongoid", "~> 2.1"
gem "bson_ext", "~> 1.3"

Y para terminar, en consola:

bundle install
rails g mongoid:config

Con la última línea creamos el fichero de configuración que necesita Mongoid (config/mongoid.yml). En su página oficial puedes las diferentes opciones que se pueden incluir en este fichero.

Como primera toma de contacto no está nada mal, ahora toca pelearse con MongoDB. Te recomendaría ver el railscast de Ryan Bates sobre Mongoid, donde te cuenta lo mismo que yo y mucho más.

Actualización: Si os encontráis en el momento de plantear cómo va a ser el esquema de la base de datos, os aconsejo echarle un vistazo a esta presentación de Kyle Banker. Intentaré plasmar en un post lo que saque en claro después de verla.

Metaprogramación: métodos y delegates

Jueves, 18 de agosto de 2011

En el proyecto que estoy trabajando ahora mismo necesitamos diferenciar en vista ciertos elementos en función del tipo al que pertenecen. Sin embargo, el tipo no está asociado directamente a ellos. Para entendernos:

class Kind < ActiveRecord::Base
  has_many :items
end

class Item < ActiveRecord::Base
  belongs_to :kind
end

class ItemData < ActiveRecord::Base
  belongs_to :item
end

Un tipo consta de nombre y label. Para saber si un Item es de un tipo concreto podemos solucionarlo facilmente con un poco de metaprogramación básica:

if ActiveRecord::Base.connection.tables.include?('kinds')
  Kind.all.map(&:label).each do |label|
    define_method "#{label}?" do
      self.kind.label == label
    end
  end
end

Ahora tenemos métodos como por ejemplo item.a?, item.b?, etc… Sin embargo, por manos del diablo, en vista no trabajamos directamente con un objeto de tipo Item sino con objetos de tipo ItemData. Si todo estuviera cerradito podríamos hacer cosas como:

class ItemData < ActiveRecord::Base
  belongs_to :item

  delegate :a?, :to => :item
end

Pero no es nuestro caso, no está cerrado. ¿Cómo lo hacemos? Le echamos un ojo a cómo está hecho el método delegate y tras poner aquí y quitar allá, nos queda algo como esto:

class ItemData < ActiveRecord::Base
  belongs_to :item
  if ActiveRecord::Base.connection.tables.include?('kinds')
    Kind.all.map(&:label).each do |label|
      method = label + '?'
      module_eval(<<-EOS, "(__DELEGATION__)", 1)
         def #{method}
           notice.__send__(#{method.inspect})
         end
       EOS
    end
  end
end

Y ya está. Ahora podemos hacer cosas como @item_data.a? y si el día de mañana se añaden tipos, tendremos su método delegate correspondiente.

Actualización: incluyo un condicional para saber si está disponible o no la tabla Kind, ya que de otro modo, rake ni siquiera nos deja correr migraciones para que exista.

Dragonfly y procesado de imágenes: problema con el uid

Martes, 26 de julio de 2011

Desde hace algún tiempo venimos trabajando en Flowers in Space en un nuevo producto propio que queremos lanzar a Internet. En él, para el tratamiento de imágenes, por diversos motivos que no vienen a cuento, nos decidimos por utilizar Dragonfly, un framework Rack para el tratamiento de imágenes.

Dragonfly es una maravilla. Nos abstraemos del post procesado de imágenes y cuando necesitamos que cierta imagen esté en un determinado tamaño basta hacer en vista algo tan simple como por ejemplo @album.cover_image.thumb(’400×200#’) y él nos sirve la imagen, la guarda donde le hayamos indicado, y nos olvidamos.

Sin embargo, en algunos casos concretos sabemos que no vamos a necesitar más de un tamaño de imagen. Para esos determinados podemos seguir utilizando Dragonfly y post procesar la imagen antes de guardarla. Mi problema era que estaba intentando hacerlo mediante un before_save de toda la vida y el uid de mi imagen no se generaba. Consecuencia: que en realidad no tenía imagen.

He aquí la solución. Imaginemos el caso anterior, un album que tiene una portada (el ejemplo de la documentación de Dragonfly, vaya). Pero sabemos de antemano que cuando mostremos nuestra portada en la web tendrá un tamaño de 50×50 (un poné). Pues al definir el image_accessor en nuestro modelo podemos hacer lo siguiente:

image_accessor :cover_image do
    after_assign{ |img| img.process!(:thumb, '50x50') }
end

Podéis encontrar más ejemplos y otros callbacks en la documentación de Dragonfly.

Error ActiveRecord::ReadOnlyRecord al actualizar atributos

Miércoles, 9 de marzo de 2011

Sigo con mi proyecto final de carrera y poco a poco vamos aprendiendo cosillas nuevas de Rails. Al mismo tiempo que voy picando código en el controlador, modelo y vistas voy testeando, dos frentes de aprendizaje pero que recomiendo a todos los que comiencen en Rails. Aunque al principio avanzas de forma más lenta y te das (como estoy haciendo yo) de bruces contra un muro una y otra vez, a la larga aprendes mucho tanto de tus múltiples errores como de tus aciertos.

Una vez tenía hecho el test de un update supuestamente correcto obtuve un error al actualizar los atributos de un objeto. En primera instancia pensé que había olvidado en el modelo alguna cosa, pero también estaba correcto. Tras una búsqueda en Google encontré la solución.

Extrayendo la explicación del artículo

Introduce read-only records. If you call object.readonly! then it will mark the object as read-only and raise ReadOnlyRecord if you call object.save. object.readonly? reports whether the object is read-only. Passing :readonly => true to any finder method will mark returned records as read-only. The :joins option now implies :readonly, so if you use this option, saving the same record will now fail. Use find_by_sql to work around.

O lo que es lo mismo, si tu objeto lo obtienes tirando de relaciones, por ejemplo, un has_many :through, por debajo está haciendo un INNER JOIN con el modelo intermedio, y ahi está el problema, que el objeto pasa a ser de solo lectura. Las soluciones podrían ser dos:

  1. Obligar al objeto a que no sea solo lectura indicando como parámetro del find :readonly => false
  2. Utilizar include en lugar de join, si es posible

Hasta aquí lo que venías buscando. Pero si quieres ver un ejercicio práctico sobre relaciones y cómo evitar el ActiveRecord::ReadOnlyRecord en él sigue leyendo.

Ejemplo práctico

Imaginemos que estamos en el WoW (oh dios, no puedo creer que esté escribiendo esto). Tenemos hermandades (guild) y jugadores (users). Las hermandades pueden tener varios jugadores, de los cuales varios pueden ser Maestre, algo así como los “admin” de la hermandad, y puede haber más de un Maestre por hermandad. Además los jugadores pueden estar en varias hermandades.

Seguro que se puede hacer de varias formas, pero nosotros vamos a hacerlo mediante un has_many :through, utilizando como modelo intermedio membership, donde tenemos un campo owner que nos indica si es o no maestre de la hermandad.

Empezamos con los modelos:

guild.rb

  has_many :memberships
  has_many :users, :through => :memberships, :uniq => true
  has_many :owners, :through => :memberships, :source => :user, :conditions => { "memberships.owner" => true }

  validates_presence_of :name

De esta manera si tenemos una hermandad en @guild podemos saber quienes son todos sus miembros haciendo @guild.users y sus maestres @guild.owners.

IMPORTANTE: Este ejemplo no está completo. Es decir, si hacemos @guild.owners << User.find(5) (por ejemplo) no crea adecuadamente el membership, ya que faltaría marcar el campo owner del membership creado a true. Eso lo dejo para otro post y no seguir complicando las cosas.

user.rb

  has_many :memberships
  has_many :guilds, :through => :memberships
  has_many :owned_guilds, :through => :memberships, :source => :owner, :conditions => { "memberships.owner" => true }

De esta manera podemos saber qué hermandades lidera un jugador haciendo @user.owned_guilds y a cuales pertenece (sea maestre o no) haciendo @user.guilds.

De nuevo, importante: Este ejemplo no está completo. Es decir, si hacemos @user.owned_guilds << Guild.find(7) (por ejemplo) no crea adecuadamente el membership, ya que faltaría marcar el campo owner del membership creado a true.

ownership.rb

belongs_to :user
belongs_to :owner, :class_name => "User"
belongs_to :guild

Hasta aquí el ejercicio práctico de relaciones, ahora vamos al lío. Imaginemos que queremos actualizar el nombre de nuestra hermandad. Para hacer esto debes ser maestre de la hermandad. Si tenemos en cuenta que no hemos metido aquí un sistema de permisos (rollo CanCan o similares) podemos solucionar esto de dos formas:

Buscar y modificar si es posible

guilds_controller.rb

def edit
  @guild = Guild.find(params[:id])
end

def update
  @guild = Guild.find(params[:id])
  if @guild.update_attributes(params[:guild])
    redirect_to guild_path(@guild), :success => "Guild actualizada"
  else
    render :edit
  end
end

Algo de refactoring básico:

guilds_controller.rb


before_filter :get_guild

def edit
end

def update
  if @guild.update_attributes(params[:guild])
    redirect_to guild_path(@guild), :success => "Guild actualizada"
  else
    render :edit
  end
end

protected

def get_guild
  @guild = Guild.find(params[:id])
end

Además vamos a tener aquello de que actualizar el nombre de la hermandad solo puede hacerlo el maestre.

guilds_controller.rb


before_filter :get_guild
before_filter :check_ownerhip

def edit
end

def update
  if @guild.update_attributes(params[:guild])
    redirect_to guild_path(@guild), :success => "Guild actualizada"
  else
    flash[:error] = "No ha sido posible actualizar los datos de la guild"
    render :edit
  end
end

protected

def get_guild
  @guild = Guild.find(params[:id])
end

def check_ownership
   redirect_to user_path(current_user), :error => "No tienes permisos" unless current_user.owned_guilds.include?(@guild)
end

En este primer ejemplo, no obtenemos un ActiveRecord::ReadOnlyRecord ya que obtenemos la @guild directamente (sin el uso del join de la has_many :through) y el objeto @guild no es de solo lectura. La consulta que se hace a base de datos sería:

SELECT `guilds`.* FROM `guilds` WHERE `guilds`.`id` = 1 LIMIT 1
Tirar de relaciones y 404

Esta es la segunda forma, donde no vamos a utilizar un before_filter para comprobar si el jugador es maestre de la hermandad. En su lugar vamos a aprovecharnos de que al buscar entre las owned_guilds de un jugador una de la que no sea maestre en Rails obtenemos un ActiveRecord::RecordNotFound, o lo que es lo mismo, un 404.

guilds_controller.rb

before_filter :get_guild

protected

def get_guild
  @guild = current_user.owned_guilds.find(params[:id])
end

El resto de acciones del controlador permanece intacto (exceptuando como dije, el before_filter :check_ownership que no estaría en este segundo ejemplo).

Y ahora… por fin, ¿y el ActiveRecord::ReadOnlyRecord donde anda en este segundo ejemplo? Pues al hacer @guild = current_user.owned_guilds.find(params[:id]), @guild es de solo lectura, al haber tirado de relaciones.

Suponiendo que el User con id igual a 1 quiere actualizar la Guild con id igual a 1. Lo primera lanzaría la siguiente consulta:

SELECT `guilds`.* FROM `guilds` INNER JOIN `ownerships` ON `guilds`.id = `ownerships`.guild_id WHERE `guilds`.`id` = 1 AND ((`ownerships`.user_id = 1)) LIMIT 1

Ahí podéis ver el JOIN al que antes hacía mención. Habiendo un JOIN de por medio, el objeto es de solo lectura. Y no podremos actualizarlo.

Tío, termina ya, so pesao

Pues la solución para mantener el segundo ejemplo tal cual y poder actualizar es tan simple como indicar que no es de solo lectura. ¿Cómo?

before_filter :get_guild

protected

def get_guild
  @guild = current_user.owned_guilds.find(params[:id], :readonly => false)
end

Vaya artículo largo. No tengo remedio. Al menos espero que le sirva a alguien. Y como siempre, si alguien tiene algo que aportar o corregir, dispone de los comentarios para ello, yo lo agradezco un montón.

has_many con múltiples foreign_keys

Viernes, 4 de marzo de 2011

Estos días estoy desconectado del mundo. No tengo tiempo para nada entre el proyecto final de carrera y el WoW al que José Galisteo aka Ceritium me enganchó muy gentilmente. Pero como estoy aprendiendo cositas de Rails, donde estoy todavía muy verde, y son sencillas, pues quería compartirlas.

Tenemos un modelo, por ejemplo, Person. Y tenemos otro Dish. Los platos (dishes) están relacionados de diferentes formas con las personas: alguien los cocina, alguien se los come, alguien los limpia (un poné, como se dice en mi tierra). Un plato tiene un cocinero, un comensal, un limpiador, pero todos son Personas (nen). Y una persona puede tener varios platos cocinados, varios platos degustados y haber limpiado varios platos. Veamos como solucionarlo ahora que está más claro:

User.rb

has_many :cooked_dishes, :class_name => "Dish", :foreign_key => "cooker_id"
has_many :tasted_dishes, :class_name => "Dish", :foreign_key => "taster_id"
has_many :cleaned_dishes, :class_name => "Dish", :foreign_key => "cleaner_id"

Dish.rb

belongs_to :cooker, :class_name => "User"
belongs_to :taster, :class_name => "User"
belongs_to :cleaner, :class_name => "User"

De esta manera, si en @user tenemos a un usuario, podemos hacer @user.cooked_dishes, @user.tasted_dishes o @user.cleaned_dishes. Y del mismo modo si tenemos un plato podríamos saber quién lo cocinó, quién lo degustó y quien lo limpió: @dish.cooker, @dish.taster, @dish.cleaner.

uninitialized constant Rails::Railtie, inherited_resources y responders

Viernes, 26 de noviembre de 2010
.rvm/gems/ruby-1.8.7-p302/gems/activesupport-2.3.5/lib/active_support/dependencies.rb:440:
in 'load_missing_constant': uninitialized constant Rails::Railtie (NameError)

…and again, and again, and again… El otro día me encontré con este mensaje intentando levantar el servidor Mongrel en un proyecto que hasta ese momento no había dado ningún problema. Ya me había peleado con un mensaje parecido e intuía que el problema era por algún conflicto entre gemas.

Me fui al servidor, vi qué gemas estaban instaladas, modifiqué el environment.rb de local indicando de qué versión debían tirar, pero no había forma, no daba con el foco. El dichoso mensaje seguía apareciendo. Afortunadamente Gumer me echó un cable y dimos con la solución.

El problema estaba en qué inherited_resources, una de las gemas que necesitaba el proyecto, requiere de responders y al no estar indicado en el environment.rb tiraba de la última versión que yo tenía instalada del mismo, la 0.6.2, ocasionando el conflicto. La versión adecuada del responders era la 0.4.7 así que la solución fue tan simple como añadir al principio del Initializer:

config.gem 'responders', :version => '0.4'

Y como la seda, hoygan. Solución simple pero menudo quebradero de cabeza. Espero que si a alguien le ocurre algo parecido lo solucione gracias a este post de manera más rápida que yo.

Usar jQuery en Rails 3

Viernes, 20 de agosto de 2010

Actualmente me encuentro desarrollando mi primera aplicación web (me hago mayor). Nada novedoso, nada impactante, pero me sirve para aprender. Aprovechando el momento de transición que se vive estoy realizándola en Rails 3 sobre Ruby 1.9.2, HTML5 y cómo no, jQuery. Si es tu caso, que te has tirado a la piscina a empezar a probar cosas y quieres utilizar jQuery, te cuento cómo:

  • Eliminar todos los ficheros dentro de public/javascripts.
  • Descargar la última versión de jQuery y cópiala dentro de la carpeta public/javasctipts de tu proyecto.
    Nota: existe actualmente un bug de un método de jQuery 1.4.2 (live) e Internet Explorer (qué raro que de problemas, ¿verdad? :P). Se recomienda a día de hoy utilizar la versión 1.4.1
  • Descargar el driver jquery ujs desde github. En la carpeta src encontrarás un archivo llamado rails.js. Cópialo dentro de la carpeta public/javasctipts de tu proyecto.
  • Ve al archivo config/application.rb de tu proyecto y busca la siguiente línea:
    
    # config.action_view.javascript_expansions[:defaults] = %w(jquery rails)
    
    

    Descoméntala y cambia jquery por el nombre del archivo de la versión de jquery que has descargado (en mi caso era jquery-1.4.1).

¡Y ya está! No tiene mayor misterio. Ahora a currar :)

Botones y links para compartir contenido

Martes, 25 de mayo de 2010

En un nuevo proyecto en el que estoy trabajando he tenido que incluir botones y enlaces para compartir los artículos que creará el administrador de la web. Gracias a estos enlaces o botones los visitantes de la web podrán enviar el artículo a Facebook, Twitter, Linkedin, Google Buzz, etc… os dejo con la recopilación de enlaces, esperando que os resulte de utilidad.

¿Los usamos todos? Deberíamos pararnos un poco a pensar si realmente nos serán necesarios. Si tu contenido es “pro” quizás te interese más Twitter y Linkedin. Si no lo es tanto y es un contenido más generalista, quizá todos menos Linkedin. Cuestión de probar, también. Total, vamos al lío, que me enrollo.

Twitter

No pude encontrar la documentación oficial de Twitter (si alguien tiene a bien pasar la url le estaré agradecido), pero integrar un botón para compartir en Twitter es bastante sencillo.


<a href="http://twitter.com/homestatus=mensaje_que_saldra_antes_del_enlace%20url_del_articulo" title="Comparte la noticia"/>Compartir en Twitter</>

Creo que se entiende bien. En lugar de “mensaje_que_saldra_antes_del_enlace” deberás poner cualquier cosa, por ejemplo “Quizás esto te interese: ” o “Lee esto que mola un cacho”.


Facebook

Gracias a un sencillo formulario podremos crear nuestro botón de “Me gusta” para integrarlo en nuestra web. Facebook ha tomado gran relevancia los últimos tiempos y se revela como un medio muy potente donde dar a conocer nuestros artículos. Lo que no me gusta es que el código generado es un iframe, y tratándose del gran caralibro, ya podrían habérselo currado un poco más.

¿necesitas más información?


Google Buzz

Sí, todavía existe. Podrás crear botones y enlaces de una forma sencilla y rápida. Te ofrece tres tipos de estilo: Botón y contador, sólo botón y enlace. Basta con añadir un enlace y un script a nuestra página. Poco más.

¿necesitas más información?


Linkedin

Éste fue el caso más curioso de todos. Cuando estamos en desarrollo, en mi caso, mi entorno de prueba apunta a http://localhost:3000. Al hacer click en el enlace una vez generado me mostraba un error y era porque Linkedin es listo y no te deja compartir enlaces a localhost. ¿Qué ocurre? Que el mensaje de error no era muy amigable y hasta que te das cuenta de qué está ocurriendo, pasa un buen rato.

http://www.linkedin.com/shareArticle?mini=true&url={articleUrl}&title={articleTitle}&summary={articleSummary}&source={articleSource}

¿necesitas más información?


Vale, todo esto está muy bien, pero…¿cómo ahorramos algo de tiempo y de ruido visual en Rails? Aquí entran en juego nuestros amigos los helpers.

Helpers para generar botones de compartir contenido

Aquí debo agradecer a Ceritium la ayuda, prácticamente los helpers que veréis aquí detallados los hizo él.

Bastaría colocar los helpers en el controlador que nos convenga. Imaginemos que queremos compartir noticias (news). ¿Donde queremos llevar al usuario al hacer click? Al show de la noticia en cuestión – news_url(news). ¿Donde colocamos el helper? Pues en news_helpers.


def share_on_twitter
  link_to content_tag(:span,'Twitter'), "http://twitter.com/homestatus=Te%20recomiendo%20que%20leas%20#{news_url(news)}", :title => "Comparte en Twitter"
end

def share_on_buzz
  link_to content_tag(:span,'Google Buzz'),"http://www.google.com/buzz/post", :title => "Comparte en Google Buzz", :class => "google-buzz-button", "data-button-style" => "link", "data-locale" => "es"
end

# Nota: headline (título de la noticia), summary (resumen de la noticia).
def share_on_linkedin(news)
  url = 'http://www.linkedin.com/shareArticle?mini=true'
  url << "&url=#{news_url(news)}"
  url << "&title=#{news.headline}"
  url << "&summary=#{truncate(news.summary, :length => 200)}"
  link_to content_tag(:span,'Linkedin'), url
end

En el caso de Facebook preferí meter el iframe directamente en el show, por ahorrarme tiempo. Pero os animo a crear un helper vosotros mismos :)