homeASCIIcasts

230: Recursos heredados 

(view original Railscast)

Other translations: En It

Other formats:

Written by Juan Lupión

En este episodio se presenta una gema desarrollada por José Valim llamada Inherited Resources. Esta gema extrae la funcionalidad más habitual de los controladores REST para evitar la duplicación del código. No se trata de la primera gema de Rails que trata de ayudarnos con esto, en el episodio 92 vimos una gema llamada make_resourceful y hay otros plugins disponibles. Cada uno tiene un enfoque distinto, y vale la pena echarles un vistazo antes de decidirnos por uno u otro. Nosotros hemos escogido Inherited Resources porque funciona bien con Rails 3.0 y parece estar un poco más actualizado.

La aplicación Rails sobre la que vamos a trabajar en este episodio es una sencilla tienda electrónica. La aplicación tiene un listado de productos y cada producto tiene varias reseñas.

La página de listado de productos.

Con Inherited Resources vamos a reorganizar el código de la aplicación, y veremos que podemos eliminar una gran cantidad de código de los controladores sin afectar a la funcionalidad.

Instalación de Inherited Resources

El código de ProductController tiene el siguiente aspecto:

/app/controllers/products_controller.rb

class ProductsController < ApplicationController
  def index
    @products = Product.all
  end

  def show
    @product = Product.find(params[:id])
  end

  def new
    @product = Product.new
  end
  
  def create
    @product = Product.new(params[:product])
    if @product.save
      flash[:notice] = "Successfully created product."
      redirect_to @product
    else
      render :action => 'new'
    end
  end
  
  def edit
    @product = Product.find(params[:id])
  end
  
  def update
    @product = Product.find(params[:id])
    if @product.update_attributes(params[:product])
      flash[:notice] = "Successfully updated product."
      redirect_to @product
    else
      render :action => 'edit'
    end
  end
  
  def destroy
    @product = Product.find(params[:id])
    @product.destroy
    flash[:notice] = "Successfully destroyed product."
    redirect_to products_url
  end

end

Estaremos acostumbrados a escribir código como este si en nuestras aplicaciones solemos usar controladores REST, que es la situación en la que Inherited Resources nos resultará más útil. Si por el contrario solemos personalizar mucho nuestros controladores, una abstracción como Inherited Resources no se adaptará a nuestras necesidades. El código de arriba sigue el patrón REST con bastante fidelidad por lo que nos servirá para ver qué nos puede dar Inherited Resources.

Nuestra aplicación de comercio electrónico está escrita con Rails 3 por lo que añadiremos Inherited Resources a nuestra aplicación incluyendo la gema en el GemFile.

/Gemfile

source 'http://rubygems.org'

gem 'rails', '3.0.0'

gem 'sqlite3-ruby', :require => 'sqlite3'
gem 'nifty-generators'
gem 'inherited_resources'

Y la instalaremos, como es habitual, con

$ bundle install

Este comando instalará la gema junto a un par de dependencias: has_scope y responders.

Con las gemas instaladas ya podemos actualizar ProductsController para utilizar Inherited Resources. Para ello hacemos que el controlador herede de InheritedResources::Base en ApplicationController. InheritedResources::Base hereda de ApplicationController por lo que tiene toda su funcionalidad.

Dado que el controlador ProductsController es un controlador REST normal podemos reemplazar sus métodos con el código heredado de Inherited Resources, con lo que el controlador queda mucho más escueto.

/app/controllers/products_controller.rb

class ProductsController < InheritedResources::Base

end

Tendremos que reiniciar el servidor para que se carguen las nuevas gemas, una vez hecho esto veremos que las páginas relacionadas con productos funcionan exactamente igual que antes. Podemos incluso crear un nuevo producto y veremos que al hacerlo se muestra el mensaje flash correspondiente

Creación de un producto.

Personalización de acciones

Cuando creamos antes un nuevo producto fuimos redirigidos a la página show de dicho producto, ¿cómo hacer que en lugar de eso la página nos lleve a la acción index? Con Inherited Resources podemos puentear cualquiera de las acciones por defecto simplemente redefiniendo el método correspondiente del controlador, por lo que podemos escribir un método create en ProductsController que cree el nuevo producto y luego haga la redirección a la acción index.

No tenemos, sin embargo, que reescribir por completo la acción create sólo para cambiar la redirección. Podemos incluir el comportamiento de Inherited Resources simplemente llamando a create! pero pasándole un bloque. Como cambiar la URL a la que se realizar la redirección tras crear un nuevo objeto es algo tan común, podemos simplemente devolver la URL que queramos en el bloque.

/app/controllers/products_controller.rb

class ProductsController < InheritedResources::Base
  def create
    create! { products_path }
  end
end

En el bloque podemos hacer más cosas que podemos consultar en la documentación.

<o>Cuando ahora creemos un nuevo producto seremos llevados a la acción index tal y como queríamos.</o>
Al añadir un producto nos redirigen al listado.

Soporte de múltiples formatos

Es fácil hacer que nuestro controlador sea capaz de responder a diferentes formatos (por ejemplo para trabajar con XML además de HTML). Lo único que hay que hacer es añadir respond_to como haríamos con cualquier otro controlador de Rails 3.

/app/controllers/products_controller.rb

class ProductsController < InheritedResources::Base
  respond_to :html, :xml

  def create
    create! { products_path }
  end
end

Esto funciona exactamente igual que en el episodio 224 [verlo, leerlo], si visitamos /products.xml recibiremos el listado de productos en formato XML.

Los productos en XML.

Recursos anidados

Ahora que ProductsController ha quedado más organizado pasemos a ReviewsController. Las reseñas están anidadas dentro de los productos, lo que quiere decir que las reseñas de un producto con id igual a 1 estarán en la URL /products/1/review, que se corresponderá con la acción index de ReviewsController. De la misma manera, si queremos añadir una reseña, ésta deberá estar anidada dentro de un producto.

Las reseñas se anidan bajo los productos.

El código de ReviewsController es así:

/app/controllers/reviews_controller.rb

class ReviewsController < ApplicationController
  def index
    @product = Product.find(params[:product_id])
    @reviews = @product.reviews
  end

  def new
    @product = Product.find(params[:product_id])
    @review = Review.new
  end
  
  def create
    @product = Product.find(params[:product_id])
    @review = @product.reviews.build(params[:review])
    if @review.save
      flash[:notice] = "Successfully created review."
      redirect_to product_reviews_path(@product)
    else
      render :action => 'new'
    end
  end

end

La diferencia obvia entre este controlador y ProductsController es que en este sólo tenemos tres en lugar de las siete acciones REST estándar. La otra diferencia es que, como tiene que gestionar el anidamiento, cada acción recibe el producto a partir de un parámetro de la URL.

Aunque estemos tratando con un recurso anidado aquí el comportamiento de Inherited Resources es fundamentalmente el mismo que en ProductsController. Podemos eliminar el código existente en el controlador y cambiar la clase para que herede de InheritedResources::Base. Lo único que hay que hacer para controlar el anidamiento es utilizar belongs_to, que es un método proporcionado por Inherited Resources para definir relaciones entre los controladores de la misma manera que se definen entre modelos con lo que Inherited Resources gestionará por nosotros la recuperación del producto correcto sobre el que se definen las reseñas.

/app/controllers/reviews_controller.rb

class ReviewsController < InheritedResources::Base
  belongs_to :product
end

Tal y como está el controlador ReviewsController tendrá las siete famosas acciones REST dado que se trata del comportamiento por defecto de Inherited Resources pero queremos que el controlador tan sólo responda a las acciones index, new y create. Podemos utilizar el método actions para restringir las acciones disponibles:

/app/controllers/reviews_controller.rb

class ReviewsController < InheritedResources::Base
  belongs_to :product
  actions :index, :new, :create
end

Al igual que hicimos en ProducsController queremos cambiar la URL a la que se nos redirige tras crear una nueva reseña. Dado que en nuestro caso se trata de un recurso anidado podremos usar uno de los métodos de ayuda que ofrece Inherited Resources para generar URLs de recursos. En este caso escogeremos un método llamado collection_url que nos redirigirá a la acción index y gestionará el anidamiento por nosotros.

/app/controllers/reviews_controller.rb

class ReviewsController < InheritedResources::Base
  belongs_to :product
  actions :index, :new, :create
  
  def create
    create! { collection_url }
  end
end

Podemos hacer la prueba y añadir una reseña.

Adding a review.

Tras el envío de la nueva reseña seremos llevados a la página de reseñas del producto, tal y como queríamos.

Tras añadir una reseña vamos a la página de reseñas del producto.

Ámbitos públicos

Otra característica útil de Inherited Resources se llama has_scope. Para usarla tan sólo tenemos que añadir una referencia a su gema en el Gemfile y luego ejecutar bundle install.

/Gemfile

source 'http://rubygems.org'

gem 'rails', '3.0.0'

gem 'sqlite3-ruby', :require => 'sqlite3'
gem 'nifty-generators'
gem 'inherited_resources'
gem 'has_scope'

Con esto instalado podemos llamar a has_scope en cualquiera de nuestros controladores y pasar el nombre de un ámbito sobre el modelo relacionado. En nuestro ejemplo añadiremos el ámbito limit, que Rails 3 proporciona en todos los modelos, a ProductsController.

/app/controllers/products_controller.rb

class ProductsController < InheritedResources::Base
  respond_to :html, :xml
  
  has_scope :limit

  def create
    create! { products_path }
  end
end

Con esto podemos añadir ámbitos como párametros a la URL por lo que si pasamos el parámetro limit se invocará dicho ámbito y se restringirá el número de productos mostrados.

Uso de un ámbito en la cadena de la petición.

Si queremos que siempre se aplique un ámbito sin necesidad de hacer referencia en la URL de la petición, podemos pasar un valor por defecto.

/app/controllers/products_controller.rb

class ProductsController < InheritedResources::Base
  respond_to :html, :xml
  
  has_scope :limit, :default => 3

  def create
    create! { products_path }
  end
end

Si ahora no pasamos el parámetro limit se utilizará el valor por defecto y veremos tres productos.

Si no pasamos un límite se utiliza el valor por defecto.

Por supuesto esto también funciona con el resto de ámbitos de la aplicación. Añadamos un ámbito al modelo Review para filtrar reseñas según su valoración.

/app/models/review.rb

class Review < ActiveRecord::Base
  belongs_to :product
  scope :rating, proc { |rating| where(:rating => rating) }
end

Ahora haremos que dicho ámbito sea público añadiéndolo a ReviewsController.

/app/controllers/reviews_controller.rb

class ReviewsController < InheritedResources::Base
  belongs_to :product
  actions :index, :new, :create
  has_scope :rating
  
  def create
    create! { collection_url }
  end
end

Con esto ya podemos usar un parámetro de valoración en la URL para restringir a las reseñas que tengan esa valoración.

Filtro de reseñas por valoración.

La gema has_scope también se puede usar de forma independiente de Inherited Resources utilizando el método apply_scopes en la acción index. Hay más detalles en la documentación en Github.

Personalización del mensaje flash

Terminaremos este episodio viendo cómo personalizar los mensajes flash. Cuando se crea una nueva reseña el mensaje por defecto es “Review was successfully created.” pero podemos cambiarlo a lo que queramos modificando los ficheros de internacionalización. Estos archivos son un excelente lugar para almacenar cadenas que se mostrarán en la interfaz de usuario aunque nuestra aplicación no esté dando soporte a múltiples lenguajes. Todas las aplicaciones Rails 3 vienen con un fichero de internacionalización en inglés en /config/locales/en.yml.

Para modificar los mensajes flash por defecto de Inherited Resources crearemos una clave flash: bajo la que tendremos otra clave que contendrá el nombre del controlador (en nuestro caso reviews) y en la que anidaremos una clave para la acción y dentro de esta otra para el nombre del mensaje de flash. Para nuestro controlador de reseñas el fichero de configuración quedará así:

/config/locales/en.yml

# Sample localization file for English. Add more files in this directory for other locales.
# See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.

en:
  flash:
    reviews:
      create:
        notice: "Your review has been created!"

Si no queremos tener que configurar esto en todos los controladores de nuestra aplicación podemos cambiar el nombre del controlador por actions: para aplicar dichos mensajes a todos los controladores, teniendo en cuenta que la cadena resource_name será reemplazada por el nombre del modelo.

/config/locales/en.yml

# Sample localization file for English. Add more files in this directory for other locales.
# See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.

en:
  flash:
    actions:
      create:
        notice: "Your {{resource_name}} has been created!"

Para probarlo crearemos una nueva reseña. Al enviarla se mostrará el mensaje flash personalizado.

Al crear la reseña se muestra el mensaje flash personalizado.

Con esto concluimos el episodio. Si acabamos escribiendo una y otra vez el mismo código en nuestros controladores nos interesará considerar el uso de Inherited Resources. El fichero README es bastante completo y cubre casos que no hemos cubierto aquí. La página del wiki también es interesante.