04: CONTROLLERS: Trabajando con controladores - LPC-Ltda/Ruby-on-Rails GitHub Wiki

Remueva toda la lógica de negocios desde su controlador y póngala en el modelo. [Mis] instrucciones son precisas, pero seguirlas requiere intuición y un razonamiento sutil. -Nick Kallen

Como cualquier programa computacional, su aplicación Rails envuelve un flujo desde una parte de su código a otra. El flujo del control del programa se hace complejo en las aplicaciones Rails. Hay muchas piezas en el framework, muchas de las cuales ejecutan a otra. Y parte de la tarea del framework es averiguar, al vuelo, cual de sus archivos es llamado y que hay dentro de ellos, lo cual, por supuesto, varía desde una aplicación a otra.

El corazón de todo esto, aunque, es muy fácil de identificar. Es el controlador. Cuando alguien se conecta a su aplicación, lo que básicamente hace es pedirle a la aplicación que ejecute una acción del controlador. Por supuesto, hay muchas formas diferentes de que esto pueda ocurrir y casos extremos donde esto no ocurre exactamente del todo. Pero si usted conoce como los controladores calzan dentro del ciclo de vida de la aplicación, usted puede sujetar todo al rededor de ese conocimiento. Esa es la razón por la cual cubrimos los controladores antes que el resto de las APIs de Rails.

Los controladores son la C dentro de MVC. Ellos son el primer puerto de una llamada, después del despachador (dispatcher), para el requerimiento entrante. Ellos están a cargo del flujo del programa: Ellos se encuentran con la información y la hacen disponible para las vistas.

Los controladores están también muy relacionados a las vistas, más de lo que están con los modelos. Es posible escribir el nivel completo de modelos de una aplicación antes de crear un controlador o tener diferentes personas trabajando en el controlador y el en el modelo sin haber conversado nunca entre ellos. Sin embargo, vistas y controladores están más herméticamente emparejados el uno al otro. Ellos comparten mucha información y los nombres que usted elige para sus variables en el controlador tendrán un efecto en lo que usted hace en las vistas.

En este capítulo, veremos que pasa camino a que la acción de un controlador sea ejecutada y que pasa como resultado. En el medio, veremos en extenso el cómo las clases controller son seteadas, particularmente en lo que se refiere a diferentes formas en que podemos renderear vistas. Envolveremos el capítulo con un par de tópicos adicionales relacionados con controladores: callbacks de acciones y streaming.

4.1 Rack

Rack es una interfaz modular para el manejo de request web, escrita en Ruby, con soporte para muchos servidores web diferentes. Abstrae el manejo de las solicitudes y respuestas HTTP en un único método call simple que puede ser utilizado por cualquier cosa, desde un simple script de Ruby hasta Rails.

Listado 2.1 HelloWorld como una aplicación Rack

class HelloWorld
  def call(env)
    [200, {"Content-Type" => "text/plain"}, ["Hello world!"]]
  end
end

Un requerimiento HTTP invoca el método call y pasa un hash con variables de ambiente, similar a la forma en que trabaja CGI. El método call debe devolver una arreglo de tres elementos que consiste en el status, un hash de encabezados de request y, finalmente, el cuerpo de la request.

A partir de Rails 2.3, el manejo de requests se movió al Rack y el concepto de middleware fue introducido. Las clases que satisfacen la interface call del Rack pueden ser encadenadas juntas como filtros. El Rack mismo incluye un número de clases de filtros útiles que hacen cosas como logging y manejo de excepciones.

Rails 3 llegó un paso más allá y fue rediseñado desde cero para aprovechar al máximo los filtros de rack de forma modular y extensible. Una completa explicación del rediseño de Rack de Rails está fuera del alcance de este libro, especialmente porque Rack no juega realmente en el día a día del desarrollo de una aplicación. Sin embargo es conocimiento esencial de Rails entender que mucho del Action Controller es implementado como módulos middleware de Rack. Si desea ver cuales filtros Rack están habilitados para su aplicación Rails? Hay una tarea rake para ello!

$ rake middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIP
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
run Example::Application.routes

¿Qué tiene que ver la verificación de las migraciones de Active Record pendientes con la solicitud de requests?

module ActiveRecord
  class Migration
    class CheckPending
      ...
        def call(env)
          ActiveRecord::Base.logger.silence do
            ActiveRecord::Migration.check_pending!
          end
          @app.call(env)
        end
    end
  end
end

Ahh, esto no quiere decir que las migraciones de Active Record tengan algo específico que hacer con el servicio de request, Es que Rails está diseñado en tal forma que diferentes aspectos de su comportamiento son introducidos dentro de la cadena de llamados a request como componentes individuales del Rack middleware o filtros.

4.1.1 Configurando su Middleware Stack

Su objeto aplicación le permite acceder y manipular el stack Rack middleware durante la inicialización vía config.middleware así:

# config/application.rb

module Example
  class Application < Rails::Application
    ...
    # Rack::ShowStatus catches all empty responses the app it wrap and
    # replaces them with a site explaining the error.
    config.middleware.use Rack::ShowStatus
  end
end

Como he encontrado al experimentar con el divertido nombre Rack::Lobster, su clase Rack middleware personalizada necesita tener un método initializer explícito, incluso si no requiere argumentos runtime.

Los métodos de config.middleware le dan un control fino sobre el orden en el cual su stack middleware es configurado. El parámetro args es un hash de atributos opcional que se pasa al método initializer de su filtro Rack.

  • config.middleware.insert_after(existing_middleware, new_middleware, args)

Agrega un nuevo middleware después de un middleware específico existente en el stack de middlewares.

  • config.middleware.insert_before(existing_middleware, new_middleware, args)

Agrega un nuevo middleware antes de un middleware específico existente en el stack de middlewares.

  • config.middleware.delete(middleware)

Remueve un middleware específico del stack

  • config.middleware.swap(existing_middleware, new_middleware, args)

Intercambia un middleware específico del stack con una nueva clase.

  • config.middleware.use(new_middleware, args)

Toma la referencia de clase como su parámetro y sólo agrega el middleware deseado al final del stack middleware.

4.2 Action Dispatch: Donde todo comienza

El código del controlador y vistas en Rails siempre ha sido parte de su framework "Action Pack". A partir de Rails 3, el despacho de request fue sacado dentro de su propio subcomponente de Action Pack llamado Action Dispatch. Este contiene clases que hacen interface entre resto del sistema del controller y el Rack.

4.2.1 Manejo de request

El punto de entrada para un requerimiento es una instancia de ActionDispatch::Routing::RouteSet, el objeto sobre el cual usted puede llamar draw al comienzo del archivo config/routes.rb.

La ruta seteada elige la regla que coincide y llama su Rack endpoint. Así una ruta como:

get 'foo', to: 'foo#index'

tiene una instancia de dispatcher asociada a ella, cuyo método call se termina ejecutando

FooController.action(:index).call

Antes de Rails 4, el RouteSet convertiría sus datos de ruta en una matriz de expresiones regulares. Cuando era necesario enviar una solicitud, se iteraba a través de la matriz e intentaba hacer coincidir las URL proporcionadas una por una. Funcionó, pero fue relativamente lento. Hoy en día, el módulo de enrutamiento central en ActionDispatch se llama Journey. Utiliza técnicas informáticas avanzadas para hacer sus cosas, como un gráfico de transición generalizado (GTG) y autómatas finitos no determinísticos (NFA). Si profundiza en su código base, ¡encontrará un archivo de gramática Yacc para analizar las definiciones de ruta!

Como cubrimos en la sección "Rutas como Rack endpoints" en el Capítulo 2, "Ruteo", el conjunto de rutas puede llamar cualquier tipo de Rack endpoint, como una app Sinatra, una macro de redireccionamento, o un lambda puro. En estos casos, no se involucra el dispatcher.

Todo esto ocurre rápido detrás de la escena. Es poco probable que usted necesite hurgar en el código del ActionDispatch; es ese tipo de cosas que usted puede dar por garantizadas y sólo trabajar. Sin embargo para realmente comprender el estilo Rails, es importante saber que pasa con el dispatcher. En particular, es importante recordar que las distintas partes de su aplicación son sólo pedazos (a veces grandes) de código Ruby que serán cargados dentro del interprete Ruby que corre.

4.2.2 Intimemos con el Dispatcher

Sólo con el propósito de aprender, gatillemos el mecanismo de despacho de Rails manualmente. Haremos este pequeño ejercicio desde el comienzo, partiendo con una nueva aplicación Rails

$ rails new dispatch_me --skip-turbolinks --skip-spring --skip-action-cable

Ahora creamos un controlador único demo con una acción index (note que Haml es seteado como nuestro lenguaje de template):

cd dispatch_me
rails generate controller demo index
  create app/controllers/demo_controller.rb
    route get "demo/index"
  invoke haml
  create    app/views/demo/
  create    app/views/demo/index.html.haml
  invoke test_unit
  create    test/controllers/demo_controller_test.rb
  invoke helper
  create    app/helpers/demo_helper.rb
  invoke    test_unit
  create      test/helpers/demo_helper_test.rb
  invoke assets
  invoke    coffee
  create        app/assets/javascripts/demo.js.coffee
  invoke    scss
  create        app/assets/stylesheets/demo.css.scss

Si usted mira el archivo app/controllers/demo_controller.rb verá que tiene una acción index.

class DemoController < ApplicationController
  def index
  end
end

También hay un archivo template de vista, app/views/demo/index.html.haml, con algo de lenguaje placeholder (marcador de posición). Sólo para ver las cosas más claramente, reemplacémoslo con algo que reconozcamos definitivamente cuando lo volvamos a ver. Reemplace el contenido de index.html.haml con lo siguiente:

Hello!

No es un gran logro del diseño, pero hará el truco.

Ahora que tenemos un conjunto de dominós en fila, solo debemos empujar el primero: el dispatcher. Para hacer esto, empezaremos con ejecutar la consola Rails desde el directorio de la aplicación.

$ rails console
Loadding development environment (Rails 5.0.0.1)
>>

Hay algunas variables del servidor web que Rack espera usar para el proceso de request. Ya que vamos a invocar el dispatcher manualmente, debemos setear estas variables de esta forma en la consola (sin las líneas de salida para abreviar).

>> env = {}
>> env['REMOTE_ADDR'] = '127.0.0.1'
>> env['REQUEST_METHOD'] = 'GET'
>> env['PATH_INFO'] = '/demo/index'
>> env['rack.input'] = StringIO.new

Ahora que hemos replicado un ambiente HTTP, estamos listos para engañar al dispatcher y hacerlo pensar que se está enviando un requerimiento. Realmente, esto es enviar un requerimiento. Tan sólo que proviene de la consola y no de un servidor web.

>> rack_body_proxy = DispatchMe::Application.call(env).last
=> #<Rack::BodyProxy:0x007f8163bb8be0 @
         body=#<Rack::BodyProxy:0x007f8163bb8c30 @
         body=#<Rack::BodyProxy:0x007f8163bb8e88 @
         body=#<Rack::BodyProxy:0x007f816b911330 @
         body=#<Rack::BodyProxy:0x007f816b912aa0 @
         body=["<!DOCTYPE html>...
>> rack_body_proxy.last
=> "<!DOCTYPE html>
    <html>
      <head>...</head>
      <body><h1>Demo#index</h1>
        <p>Find me in app/views/demo/index.html.erb<(p>
      </body>
    </html>

Si usted desea ver todo lo contenido en el objeto ActionDispatch::Response retornado desde call, intente el siguiente código:

>> y DispatchMe::Application.call(env)

El método manual y formatea sus argumentos como un string YAML, haciéndolo más fácil de entender. No reproducimos aquí la salida porque es enorme, pero esta incluye cosas como encabezados HTTP.

Volviendo al proceso de despacho, hasta ahora hemos ejecutado el método call de nuestra aplicación Rails y, como resultado, la acción de index se ejecutó y la plantilla index (tal como está) fueron rendereados y los resultados del render fueron envieltos en algunos encabezados HTTP y devueltos.

Sólo piense, si usted fuera un servidor web en lugar de un humano y hubiera hecho la misma cosa, usted podría ahora retornar el encabezado del documento, "Hello!", y todo al cliente.

Usted puede seguir el rastro de migas de pan incluso más allá explorando el código fuente de Rails, pero para comprender la cadena de eventos en un request Rails y el rol del controlador, el vistazo bajo el capó que hemos hecho es suficiente.

Note que si usted le proporciona al Rack una ruta que resuelve a un archivo estático, este será servido directamente desde el servidor web sin involucrar al stack de Rails. Como resultado, el objeto retornado por el dispatcher para un archivo estático es diferente que el que usted puede esperar.

4.3 Render hacia la vista...

El objetivo de una acción de controlador típica es renderear un template de vista -esto es, llenar el template y manipular el resultado, usualmente un documento HTML, de vuelta al servidor para ser enviado al browser. Extrañamente -al menos puede parecerle un poco extraño, aunque no ilógico- usted no necesitará realmente definir una acción de controlador, ya que usted tiene un template que coincide con el nombre de la acción.

Usted puede intentar esto en un modo "bajo el capó". Vaya a app/controller/demo_controller.rb y borre la acción index el archivo lucirá vacío así:

class DemoController < ApplicationController
end

No borre el archivo app/views/demo/index.html.haml, y entonces pruebe un ejercicio de consola -DispatchMe::Application.call(env) y todo eso- nuevamente. Usted verá el mismo resultado

A propósito, asegúrese de volver a cargar la consola cuando haga cambios -no reacciona a los cambios en el código fuente automáticamente. La forma más simple de volver a cargar es simplemente tipear reload!. Pero tenga cuidado que no exista alguna instancia de un objeto Active Record que usted tenga que necesite ser vuelto a cargar (usando su método individual reload). Algunas veces es más simple salir de la consola y volver a levantarla nuevamente.

4.3.1 Cuando dude, render

La gente ama decir que Rails es mágico. Este es el tipo de cosa de la que hablan. Rails sabe que cuando obtiene un request para la acción index del controlador demo, lo que realmente importa es mandar algo de vuelta al servidor. Así que si no hay acción index en el archivo controlador, Rails se encoje de hombros y dice, "Bien, sólo asumamos que hay una acción index, estaría vacía de todas formas, y sólo renderea index.html.haml. Así que eso es lo que yo haré.

Usted puede aprender algo de una acción vacía del controlador. Así que vamos de vuelta a esta versión del controlador demo:

class DemoController < ApplicationController
  def index
  end
end

Lo que usted aprende del mirar una acción vacía es que, al final de toda acción de controlador, si ninguna otra cosa ha sido especificada, el comportamiento por defecto es renderear el template cuyo nombre coincide con el controlador y acción, la cual en este caso es app/views/demo/index.html.haml.

En otras palabras, cada acción de controlador tiene un comando render implícito en él.

Y render es un método real. Usted puede escribir el ejemplo anterior de la siguiente forma:

def index
  render "demo/index"
end

Usted no tiene que hacerlo, porque esto asume que es lo que usted desea, y eso es parte de lo que la gente de Rails dicen cuando discuten las convention over configuration. No fuerce al desarrollador a agregar código para lograr algo que puede asumirlo como cierto.

El comando render, sin embargo, hace más que sólo proveer una forma de contarle a Rails que haga algo que va a hacer de todas formas.

4.3.2 Rendereo explícito

Renderear un template es como ponerse una camisa: Si no le gusta la primera que usted encuentra es su closet -la por defecto, digamos- usted puede alcanzar otra para ponérsela a cambio.

Si una acción de controlador no desea renderear su template por defecto, puede renderear uno diferente llamando al método render explícitamente. Cuaquier archivo template en el arbol de directorio app/views esta disponible. (Realmente esto no es exáctamente cierto, cualquier template en el sistema completo está disponible!) Pero por qué desearía la acción de su controlador renderear un template diferente al que tiene por defecto? Hay varias razones, y viendo algunas de ellas podremos cubrir todas las características del método render del controlador.

Si desea capturar una llamada a render sin enviarla automáticamente al navegador, puede llamar a render_to_string en su lugar. Toma exactamente las mismas opciones que render, pero devuelve una string en lugar de desencadenar una respuesta.

4.3.3 Rendereando otro template de la acción

Una razón común para renderear un template completamente diferente es redesplegar un formulario cuando ha sido enviado con data inválida y necesita corrección. En tales circunstancias, la estrategia web usual es desplegar el formulario con la data enviada y gatillar simultáneamente el despliegue de alguna información de error para que el usuario pueda corregir el formulario y volver a enviar.

La razón que este proceso involucra el rendereo de otro template es que la acción que procesa el formulario y la acción que despliega el formulario pueden ser -y frecuentemente lo son- diferentes entre sí. Por lo tanto, la acción que procesa el formulario necesita una forma para volver a desplegar el template (del formulario) original, en lugar de tratar el envío del formulario como exitoso y moverse a lo que quiera que venga.

Wow, eso fue un bocado de una explicación. Aquí hay un ejemplo práctico:

class EventController < ActionController::Base
  def new
    # This (empty) action renders the new.html.haml template, which
    # contains the form for inputting information about the new
    # event record and is not actually needed.
  end

  def create
    # This method processes the form input. The input is available via
    # the params hash, in the nested hash keyed to :event.
    @event = Event.new(params[:event])
    if @event.save
      # Ignore the next line for now.
      redirect_to dashboard_path, notice: "Event created!"
    else
      render action: ´new'  # doesn't execute the new method!
    end
  end
end

En una falla, esto es, si @event.save no retorna true, rendereamos el template "new". Asumimos que new.html.haml ha sido escrito correctamente, esto incluirá automáticamente el despliegue de información de error en el nuevo (pero aún no grabado) objeto Event.

Note que el template mismo no "sabe" que ha sido rendereado por la acción create en lugar de por la acción new. Sólo hace su trabajo: Llena y expande e interpola, basado en las instrucciones que contiene y la data (en este caso @event) que el controlador le ha pasado.

4.3.4 Rendereo de un template completamente diferente

En una forma similar, si usted esta rendereando un template para una acción diferente, es posible renderear cualquier template en su aplicación llamando a render con un string apuntando al archivo template deseado. El método render es muy robusto en su habilidad de interpretar que template está usted tratando de referenciar.

render template: '/products/index.html.haml'

Un par de notas: No es necesario pasar un hash con :template porque es la opción por defecto. También, en nuestra prueba, todas las siguientes permutaciones trabajan igual cuando se llaman desde ProductsController:

render '/products/index.html.haml'
render 'products/index.html.haml'
render 'products/index.html'
render 'products/index'
render 'index'
render :index

La opción :template sólo trabaja con una ruta relativa a la raíz del template (app/views, a menos que usted lo haya cambiado, lo que es muy inusual).

Use sólo lo suficiente para eliminar la ambigüedad. El tipo de contenido predeterminado es el del request. y si tiene dos plantillas que difieren solo por el lenguaje de la plantilla, lo está haciendo mal.

En raras ocasiones, puede usar el método render para acceder a plantillas que están completamente fuera de su aplicación usando la opción :file.

render file: "/u/apps/warehouse_app/current/app/views/products/show"

Proporciona una ruta absoluta del sistema de archivos a la plantilla deseada. Se renderizará utilizando el diseño actual de su controlador.

Usando la opción :file en combinación con el input del usuario puede llevarnos a problemas de seguridad ya que un atacante puede usar esta información para llegar a archivos sensibles.

4.3.5 Rendereando un template partial

Otra opción es renderear un template partial (usualmente referenciado sólo como un partial). El uso de templates partial le permite organizar el código de sus templates en archivos pequeños. Los partials pueden también ayudarlo a eliminar el desorden y lo alientan a romper el código de su template en módulos reusables.

Hay algunas formas de gatillar el rendereo de un partial. El primero -y más obvio- es usar la opción :partial para especificar explícitamente un template partial. Rails tiene la convención de prefijar con un underscore los archivos de templates partial, pero usted nunca debe incluir el underscore cuando se refiera a los partials.

render partial: 'product'  # renders app/views/products/_product.html.haml

Dejar el underscore fuera del nombre del partial aplica incluso si usted se refiere a un partial en un directorio diferente al del controlador donde usted está actualmente!.

render partial: 'shared/product'
# renders app/views/shared/_product.html.haml

La segunda forma de gatillar un rendereo de un partial depende de una convención. Si usted le pasa a render :partial un objeto, Rails usa el nombre de su clase para encontrar un partial que renderear. Usted puede incluso omitir la opción :partial, como en el siguiente ejemplo.

render partial: @product
render @product
render 'product'

Estas tres líneas renderean el template app/views/products/_product.html.haml.

El rendereo partial desde un controlador es mayormente usado en conjunto con las llamadas AJAX que necesitan actualizar dinámicamente segmentos de una página ya desplegada. La técnica, sobre el uso genérico de partials en las vistas, es cubierto en gran detalle en el Capítulo 10, "Action View".

4.3.6 Rendereando HTML

Es una práctica pobre, pero usted puede enviar un string HTML de vuelta al browser usando la opción :html.

render html: "<strong>Not Found</strong>".html_safe

Asegúrese de marcarlo como seguro, o Rails se quejará de una posible vulnerabilidad de seguridad.

4.3.7 Rendereando código template inline

Ocasionalmente, usted necesita enviar al browser un retazo de HTML generado usando código template que sea demasiado pequeño como para ameritar su propio partial. Esta práctica es contenciosa, porque es una violación fragante a la separación de temas entre los niveles MVC.

Rails trata el código inline exáctamente como si fuera un template de view. El tipo por defecto para el proceso de templates de views es ERb, pero al pasarle una opción adicional :type nos permite elegir Haml.

render inline: "%span.foo #{@foo.name}", type: "haml"

Si usted fuera uno de mis empleados, lo retaría por usar código de vistas en el controlador, incluso si fuera sólo una línea. Mantenga su código de vistas en las vistas!

4.3.8 Accesando helpers en el controlador

Si ha decidido utilizar el código template inline en sus controladores a pesar de que nuestro consejo indique lo contrario, es posible que también tenga acceso a los asistentes de visualización. Rails le da acceso vía el método helpers de la clase controller base. Devuelve un módulo que contiene todos los métodos helper disponibles para la vista.

module UsersHelper
  def full_name(user)
    user.first_name + user.last_name
  end
end

class UsersController < ApplicationController def update @user = User.find params[:id] if @user.update(user_params) notice = "#{helpers.full_name(@user) is successfully updated}" redirect_to user_path(@user), notice: notice else render :edit end end end

Como se muestra en el ejemplo anterior, usar helpers dentro de su controlador es ocasionalmente muy útil para generar mensajes flash dinámicos.

Rendereando JavaScript

Rails puede ejecutar expresiones JavaScript arbitrarias en su browser.

render js: "alert('Hello world!')"

El string suministrado será enviado al browser con un tipo MIME text/javascript.

4.3.10 Rendereando texto

Que pasa si usted sólo necesita enviar texto plano de vuelta al browser, particularmente cuando responde a XHR y ciertos tipos de requerimientos de servicios web?

render text: 'Submission accepted'

4.3.11 Rendereando salida Raw Body

Puede enviar un contenido sin procesar al navegador, sin configurar ningún tipo de contenido, usando la opción :body.

render body:

Esta opción solo debe usarse si no le importa el tipo de contenido de la respuesta. Usar :plain o :html es usualmente más apropiado. A menos que se sobrescribe, la respuesta devuelta de esta opción de representación será text/html.

4.3.12 Rendereando otros tipos de data estructurada

El comando render también acepta una serie de opciones (convenientes) para retornar data estructurada como JSON o XML. El content-type de la respuesta debe ser seteado y las opciones adicionales aplicarán. (Yehuda ha escrito una excelente descripción de cómo registrar diferentes opciones de rendereo en http://blog.engineyard.com/2010/render-options-in-rails-3)

4.3.12.1 :json

JSON (http://www.json.org/) es un subconjunto pequeño de JavaScript seleccionado por su utilidad como formato liviano de intercambio de datos. Es en su mayoría usado como una forma de enviar data hacia el código JavaScript que se ejecuta en una aplicación web enriquecida vía llamadas Ajax. Active Record soporta la conversión JSON, lo cual hace de Rails una plataforma ideal para servir data JSON, como en el siguiente ejemplo:

render json: @record

En la medida que el parámetro responde a to_json, Rails lo llamará por usted, lo cual significa que usted no tendrá que llamarlo en los objetos Active Record.

Cualquier opción adicional pasada a render :json será también incluida en la invocación a to_json.

render json: @projects, include: :tasks

Adicionalmente, si usted está haciendo JSONP (JSON con padding), usted puede proporcionar el nombre de la función de respuesta a ser invocada en el browser cuando tenga su respuesta. Sólo agregue una opción :callback con el nombre de un método JavaScript válido.

render json: @record, callback: 'updateRecordsDisplay'

4.3.12.2 :xml

Active Record también tiene soporte incluido para las convenciones a XML, como en el siguiente ejemplo:

render xml: @record

En la medida que el parámetro responda a to_xml, Rails lo llamará por usted, lo cual significa que no tendrá que llamarlo usted con objetos Active Record.

Cualquier opción adicional pasada a render :xml serán también incluidas en la invocación de to_xml.

render xml: @projects, include: :tasks

4.3.13 Políticas de Rendereo por defecto

La búsqueda de templates para la representación tiene en cuenta el nombre de la acción, las configuraciones regionales, el formato, la variante, los manejadores de plantillas, etc. Discutiremos cómo estos parámetros se especifican momentáneamente.

Si existen templates para la acción del controlador, pero no en el formato correcto (o variante, etc.), se genera un ActionController::UnknownFormat, que puede dar como resultado que 204 No Content sea devuelto al cliente. Esta es una desviación significativa del comportamiento de Rails anterior de simplemente elegir un template predeterminado para renderizar. El motivo del cambio es que se supone que la lista de templates disponibles es una enumeración completa de todos los posibles formatos (o variantes) que desea el desarrollador. En otras palabras, tener solo plantillas JSON definidas es su forma de indicar que la acción del controlador, por ejemplo, no tiene la intención de manejar la solicitud de HTML o XML.

Ahora, si está en modo de desarrollo, y el request actual es una request de navegador "interactivo", lo que significa que llegó allí ingresando la URL en la barra de direcciones, enviando un formulario, haciendo clic en un enlace, etc. como parte de un XHR o una solicitud de API que no sea del navegador, entonces el aumento de ActionView::UnknownFormat debería dar como resultado un mensaje de error útil que se muestra en el navegador.

Muchas veces, la razón por la que Rails no encuentra una plantilla adecuada para renderear su acción es porque usted olvidó terminar su método con una llamada a redirect_to.

4.3.14 Rendereando nothing

Si usted realmente no necesita renderear nada, no sólo omitir su template de acción. Explícitamente indique su intención usando el método head. Este toma un símbolo correspondiente al código de status deseado.

head :ok

Un uso común de esta técnica es bloquear el acceso no autorizado.

head :unauthorized

El método head le permite retornar una respuesta sin contenido y un código de estado específico. Usted puede obtener el mismo resultado que el código previo llamando a render nothing: true y explícitamente proveer un estado.

render nothing: true, status: 401

El método head también acepta un hash de opciones que es interpretado como nombres del encabezado y valores que deben ser incluidos con la respuesta. Para ilustrar, considere el siguiente ejemplo que retorna una respuesta vacía con un estado de 201 y también setea el encabezado Location:

head :created, location: auction_path(@auction)

4.3.15 Opciones de rendereo

La mayoría de las llamadas al método render aceptan opciones adicionales. Aquí están en orden alfabético.

4.3.15.1 :content_type

Todo el contenido que vuela en la red esta asociado con un tipo MIME. (http://en.wikipedia.org/wiki/MIME). Por ejemplo, el contenido HTML es etiquetado con un tipo de contenido text/html. Sin embargo, hay ocasiones donde usted desea enviar al cliente algo diferente a HTML. Rails no valida el formato del identificador MIME que usted pasa a la opción :content_type, así que asegúrese de que sea valido.

4.3.15.2 :layout

Por defecto, Rails tiene convenciones con respecto al tamplate layout que elige para envolver su respuesta, y esas convenciones son cubiertas en detalle en el capítulo 10, "Action View". La opción :layout le permite especificar si usted desea que el template layout sea rendereado si usted pasa un valor booleano o, el nombre de un template layout, si usted desea desviarse del comportamiento por defecto.

render layout: false    # disable layout template
render layout: 'login'   # a template app/views/layout es asumido

**4.3.15.3 :status

El protocolo HTTP incluye muchos códigos de estado estándar (http://www.w3.org/Protocols/rfc2616/rfc2616.sec10.html) que indican una variedad de condiciones en la respuesta a un requerimiento de cliente. Rails utilizará el estado apropiado para la mayoría de los casos comunes, como un 200 OK para requerimientos exitosos.

La teoría y técnicas involucradas en el uso apropiado del todos los códigos de estado HTTP podrían requerir un capítulo dedicado -o incluso un libro entero. Para su conveniencia, la Tabla 4.1 muestra algunos códigos que encuentro útiles en mi día a día de programador Rails.

Tabla 4.1 Ejemplos de códigos de estado

200 OK

Todo está bien y aquí está su contenido.

201 Created

Un nuevo recurso ha sido creado y su ubicación puede ser encontrada en el encabezado de la respuesta Location HTTP.

307 Temporary Redirect

El recurso requerido reside temporalmente bajo una URI diferente.

Ocasionalmente, usted necesita re-direccionar temporalmente el usuario a una acción diferente, quizá mientras algún proceso largo está ocurriendo o mientras la cuenta de un dueño de recurso particular esta suspendida. Este código de estado particular dicta que un encabezado de respuesta HTTP llamado Location contiene la URI del recurso al que se redirecciona el cliente. Ya que el método render no toma un hash de campos de respuesta de encabezado, usted tiene que setearlos manualmente antes de invocar el render. Afortunadamente, el hash response esta al alcance de los métodos del controlador, como en el siguiente ejemplo:

def paid_resource
  if current_user.account_expired?
    response.headers['Location'] = account_url(current_user)
    render text: "Account expired"
    status: 307
  end
end

401 Unanthorized

Algunas veces un usuario no provee credenciales para ver un recurso restringido o autenticación, y/o la autorización puede fallar. Asumiendo que usted puede estar usando un esquemas de autenticación básico o Digest HTTP, cuando esto sucede usted debe probablemente retornar un 401.

403 Forbbiden El servidor comprende el requerimiento pero esta reusando cumplirlo.

A mí me gusta usar el 403 en conjunto con un mensaje corto render :texten situaciones donde el cliente requerido un recurso que no está normalmente disponible vía la interface de la aplicación web. En otras palabras, el requerimiento parece ocurrir por medios artificiales. Un humano o un robot, por razones inocentes o culpables (eso no importa), está tratando de engañar al servidor para hacer algo que se supone no debe hacer. Por ejemplo, mi aplicación Rails actual es pública y es visitada por el GoogleBot a diario. Probablemente debido a un bug existente en algún punto, la URL /favorites es indexada. Desafortunadamente, /favorites se supone que sólo debe estar disponible para usuarios logeados. Sin embargo, una vez que Google conoce una URL, volverá en el futuro. Esta es la forma en que le digo que se detenga:

def index
  return render nothing: true
  status: 403 unless logged_in?
  @favorites = current_user.favorites.all
end

404 Not Found El servidor no puede encontrar el recurso requerido

Usted puede elegir usar 404 cuando un recurso de un id especifico no existe en su base de datos (Ya sea debido a que es un id inválido o debido a que el recurso fue borrado). Por ejemplo, GET /people/2349594934896107 no existe en su base de datos, qué desplegaremos? Renderearemos una vista show con un mensaje flash que dice que ninguna persona con ese id existe? No en nuestro mundo REST. Un 404 será mejor. Más aún, si estamos usando algo como paranoia y sabemos que el recurso existía en el pasado,podemos responder con 410 Gone.

500 Internal Server Error

El servidor encontró una condición inesperada que lo previene de completar el requerimiento. Usted probablemente sabe por ahora que este es el código de estado que Rails levanta cuando usted tiene un error en su código.

503 Service Unavailable El servidor está temporalmente no disponible

El código 503 ocurre cuando un sitio está abajo por mantenimiento, particularmente cuando hacemos upgrade de servicios web REST.

4.4 Opciones de layout adicionales

Usted puede especificar opciones de layout al nivel de la clase del controlador si usted desea volver a usar layout por múltiples acciones.

class EventController < ActionController::Base
  layout "events", only: [:index, :new]
  layout "global", except: [:index, :new]
end

El método layout puede aceptar Strings, Symbol o booleanos, con un hash de argumentos después.

String Determina el nombre de template a usar. Symbol Llama el método con este nombre, el cual se espera retorne un string con el nombre del template. true Levanta un error de argumento *false No usa un layout

Los argumentos opcionales son :only o :except y esperan un array de nombres de acciones a las cuales deben aplicar o no aplicar el layout que se especifica.

4.5 Redireccionar

El ciclo de vida de una aplicación Rails esta dividida en requests. Renderear un template, ya sea el por defecto o uno alternativo -o, para el caso, renderear un partial, algún texto o cualquier cosa- es el paso final en el manejo de un requerimiento. Redireccionar, sin embargo significa terminar el requerimiento actual e iniciar uno nuevo.

Miremos de nuevo el ejemplo del método de manejo del formulario create:

def create
  if @event.save
    flash[:notice] = "Event created!"
    redirect_to :index
  else
    render :new
  end

Si la operación save tiene éxito, almacenamos un mensaje en la hash flash y redirect_to a una acción completamente nueva. En este caso es la acción index. La lógica aquí es que si el nuevo registro Event fue guardado, la siguiente regla de negocio es llevar al usuario al la vista del nivel superior.

La razón principal para redireccionar en lugar de sólo renderear un template después de crear o editar un recurso (realmente una acción POST) tiene que ver con el comportamiento de recarga del browser. Si usted no redireccionó, se le puede pedir al usuario volver a enviar el formulario si presiona el botón back o recargar.

Por que redireccionar es la opción correcta? Cuando usa el método Rails redirect_to, usted le cuenta al agente del usuario (es decir, el browser) que realice un nuevo requerimiento para una URL diferente. Esa respuesta puede significar diferentes cosas, y es el porque el HTTP moderno tiene cuatro diferentes códigos de estado para la redirección. El antiguo HTTP tenía dos códigos: 301, también conocido como Moved Permanently, y 302 o Moved Temporarily. Un redireccionamiento permanente significa que el agente del usuario debe olvidar la antigua URL y usar la nueva de ahora en adelante, actualizando cualquier referencia que pudiera tener (por ejemplo, un bookmark o, en el caso de Google, su base de datos de búsqueda). Un redireccionamiento temporal fue un asunto de solo una vez. La URL original aún es válida, pero para este requerimiento particular, el agente del usuario debe obtener un nuevo recurso desde la URL de redireccionamiento. Pero hay un problema, si el requerimiento original ha sido un POST, que método debe ser usado para el requerimiento de redirección? Para redireccionamientos permanentes, fue seguro asumir que el requerimeinto nuevo debía ser un GET. ya que este fue el caso en todos los escenario de uso. Pero los redireccionamiento temporales son usados tanto para redireccionar a una vista de un recurso que ha sido modificada en el requerimiento POST original (el cual sucede es el más común patrón de uso) y también para redireccionar el requerimiento POST original completo a una nueva URL que se encargará de él. HTTP 1.1 resuelve este problema con la introducción de dos nuevos códigos de estado: 303, que significa See Older, y 307: que significa Temporary Redirect. Una redirección 303 le contaría al agente de usuario que realice un requerimiento GET, sin importar que fue el verbo original, mientras que un 307 siempre usará el mismo método usado en el requerimiento original. En estos días, muchos browsers manejan redirecciones 302 en la misma forma que las 303 -con un requerimiento GET, el cual es el argumento usado por el equipo del núcleo de Rails para usar 302 en redirect_to. Un estado 303 sería la mejor alternativa, porque no deja espacio para interpretaciones (o confusión), pero supongo que nadie lo ha encontrado suficientemente molesto como para un parche. Si usted alguna vez necesita una redirección 307 -digamos, para seguir procesando un requerimeinto POST en una acción diferente- usted puede siempre lograr su redireccionamiento personalizado asignando una ruta a response.header["Location"] y entonces renderear con render status: 307.

4.5.1 El método redirect_to

El método redirect_to tiene dos parámetros:

redirect_to(target, response_status = {})

El parámetro target toma una de muchas formas.

*Hash La URL será generada al llamar a url_for con el argumento provisto.

redirect_to action: "show", id: 5

Objeto Active Record La URL será generada llamando a url_for con el objeto provisto, lo cual debe generar una URL nombrada para ese objeto.

redirect_to post

String que comienza con un protocolo como http:// Usada directamente como una URL objetivo para el redireccionameinto.

redirect_to "http://www.rubyonrails.org"
redirect_to articles_url

String que no contiene un protocolo El protocolo y host actual se usan como argumento para ser usados en el redireccionamiento.

redirect_to "/"
redirect_to articles_path

La redirección pasa como un encabezado "302 Moved" a menos que otra cosa sea especificado. El parámetro response_status toma un hash de argumentos. El código puede ser especificado por un nombre o número, como en los siguientes ejemplos:

redirect_to post_url(@post), status: :found
redirect_to :atom, status: :moved_permanently
redirect_to post_url(@post), status: 301
redirect_to :atom, status: 302

También es posible asignarle un mensaje flash como parte de la redirección. Hay dos accesos especiales para los nombres flash comúnmente usados alert y notice, así como un cubo flash de propósito general.

redirect_to post_url(@post), alert: "Watch it, mister!"
redirect_to post_url(@post), status: found, notice: "Pay attention to the road"
redirect_to post_url(@post), status: 301, flash: {updated_post_id: @post.id}
redirect_to :atom, alert: "Something serious happened."

Como en Rails 4, usted puede registrar sus propios tipos de accessor flash usando el método estilo macro ActionController::Flash.add_flash_types.

class ApplicationController
  ...
  add_flash_types :error
end

Cuando un tipo de flash es registrado, un acceso similar a alert y notice se hace disponible para ser usado con redirect_to.

redirect_to post_url(@post), error: "Something went really wrong!"

Recuerde que las instrucciones redirect y render no detienen máginamente la ejecución de su método de acción del controlador. Para prevenir un DoubleRenderError, considere llamar explícitamente return después de redirect_to o render así como sigue:

def show
  @user = User.find(params[:id])
  if @user.activated?
    render :activated and return
  end
end

El método redirect_back

Puede usar redirect_back para devolver al usuario a la página de la que acaba de llegar, una técnica muy útil en las aplicaciones web tradicionales. La ubicación para "volver a" se extrae del encabezado HTTP_REFERER. Si no se garantiza que el navegador lo establezca, debe proporcionar un parámetro fallback_location.

redirect_back fallback_location: root_path

Si usted viene de versiones previas de Rails, esta técnica funcionaba con el símbolo mágico :back.

redirect_to :back # doesn't work in Rails 5

4.6 Comunicación Controller/View

Cuando un template de vista es rendereado, éste generalmente hace uso de la data que el controlador a sacado desde la base de datos, En otras palabras, el controlador obtiene lo que necesita del nivel modelo y lo entrega a la vista.

La forma en que Rails implementa la entrega de data desde el controlador a la vista es a través de variables de instancia. Tipicamente, una acción del controlador inicializa una o más variables de instancia. Esas variables de instancia pueden ser entonces ser usadas por la vista.

Hay un poco de ironía (y posiblemente confusión para los principiantes) en la elección de variables de instancia para compartir data entre controladores y vistas. La razón principal para que las variables de instancia existen es para que objetos (ya sea objetos Controller, objetos String, etc) puedan contener la data que no comparten con otros objetos. Cuando la acción de su controlador es ejecutada, todo ocurre en el contexto de un objeto controlador -una instancia de digamos, DemoController o EventController. El Contexto incluye el hecho que cada variable de instancia en el código pertenece a la instancia controlador.

Cuando un template de vista es rendereado, el contexto es el de un objeto diferente, una instancia de ActionView::Base. Esta instancia tiene sus propias variables de instancia y no tiene acceso a las del objeto controller.

Así las variables de instancia, en virtud de esto, son la peor opción para la forma en que dos objetos compartan data. Sin embargo es posible hacer que esto pase -o hacer que aparentemente pase. Lo que Rails hace es iterar a través de las variables de objeto del controlador y, para cada una, crear una variable de instancia para el objeto vista de con el mismo nombre y conteniendo la misma data.

Este tipo de labor intensa para el framework: Es como copiar la lista del almacén a mano. Pero el resultado final es que las cosas son más fáciles para usted, el programador. Si usted es un purista de Ruby, puede hacer una pequeña mueca de dolor al pensar en variables de instancia sirviendo para conectar objetos más que para separarlos. Por otro lado, ser un purista de Ruby también incluye comprender el hecho que usted puede hacer muchas diferentes cosas en Ruby -tales como copiar variables de instancia en una iteración. Así no hay nada anti-Ruby en ello. Y provee una conexión sin costura, desde la perspectiva del programador, entre el controlador y el template que está rendereando.

Yo soy un viejo irritable y molesto, Rails está mal, mal, mal. Usar variables de instancia para compartir data con las vistas apesta. Si usted desea ver cómo mi librería "Decent Exposure" ayuda a eliminar esta horrible práctica, salte hasta la sección "Decent Exposure" en Capítulo 10, "Action View".

4.7 Callbacks de acción

Las callback de acciones permiten que el controlador ejecute código compartido de pre y post proceso para sus acciones. Estos callbacks pueden ser usados para hacer autenticación, caching, o auditar antes de que la acción destinada se realice. Las declaraciones de callbacks son métodos de clase estilo macros -esto es, ellas aparecen en la cima de su método controlador, dentro del contexto de la clase, antes de la definición de métodos. También eliminamos los paréntesis en torno a los argumentos del método, para enfatizar su naturaleza declarativa, como este:

before_action :require_authentication

Como con muchos métodos estilo macro en Rails, usted puede pasar tantos símbolos como desee al método callback:

before_action :security_scan, :audit, :compress

O puede dividirlas en líneas separadas como estas:

before_action :security_scan
before_action :audit
before_action :compress

Usted debe hacer que su método callback de acciones sea protected o private; de otra forma, ella será llamable como acción pública en su controlador (vía la ruta por defecto).

Adicionalmente a protected} y private, uno puede declarar que un método nunca debiera ser enviado con alguna intención revelada (?) hide_action`

Importante, los callbacks de acciones tiene acceso a request, response y todas las variables de instancia seteadas por otros callbacks en la cadena o por la acción (en el caso del callback after) Los callbacks de accione pueden setear variables de instancias para ser usadas por la acción requerida y frecuentemente hacen eso también.

4.7.1 Herecia de los callbacks de acciones

Las jerarquías de la herencia del controlador comparten los callbacks de acciones downward. Su aplicación Rails promedio tienen un ApplicationController desde el cual todos los otros controladores heredan, así que si usted desea agregar callbacks de acciones que estén siempre corriendo, sin importar que, este es el lugar para hacerlo también.

class ApplicationController < ActionController::Base
  after_action :compress

Las subclases pueden también agregar y/o saltar callback de acciones ya definidos sin afectar la superclase. Por ejemplo, considere las dos clases relacionadas del Listado 4.1 y cómo ellas interactúan.

Listado 4.1 Una pareja de callback before cooperantes

class BankController < ActionController::Base
  before_action :audit

  protected

  def audit
    # Record this controller's actions and parameters in an audit log.
  end
end

class VaultController < BankController
  before_action :verify_credentials

  protected

  def verify_credentials
    # Make sure the user is allowed into the vault.
  end
end

Cualquier acción realizada sobre BankController (o cualquiera de sus subclases) causará que el método audit sea llamado antes de que la acción requerida sea ejecutada. Sobre el VaultController, primero es llamado el método audit, seguido por verify_credentials. Porque ese es el orden en que los callbacks han sido especificados. (Los callbacks son ejecutados en el contexto de clase donde ellos son declarados, y el BankController tiene que ser cargado antes que VaultController, ya que es su clase padre.) Si ocurre que el método audit llama a render o a redirect_to por alguna razón, verify_credentials y la acción requerida no serán llamadas nunca. Esto es llamado detener la cadena de callbacks de acciones.

4.7.2 Tipos de callback de acciones

Un callback de acción puede tomar una de tres formas: referencia del método (symbol), clase externa, o block. La primera es por lejos la más común y trabaja referenciando un método protegido en alguna parte de la jerarquía de herencia del controlador. En el ejemplo del Listado 4.1, tanto BankController como VaultController usan esta forma.

4.7.2.1 Clases de callback de acciones

Usar una clase externa hace que sea más simple la reutilización de callbacks genéricos, cómo la compresión de salida (output). Las clases de callback externas son implementadas partiendo de un metodo callback estático sobre cualquier clase y luego pasar esta clase al método callback de acción, como en el listado 4.2. El nombre del método de la clase debe coincidir con el tipo de callback deseado (por ejemplo, before, after, around).

Listado 4.2 Un callback de accion de compresión de salida (output)

class OutputCompressionActionCallback
  def self.after(controller)
    controller.response.body = compress(controller.response.body)
  end
end

class NewspaperController < ActionController::Base
  after_action OutputCompressionActionCallback
end

El método de la clase action callback es pasado a la instancia del controlador para que corra dentro. Tiene acceso completo al controlador y puede manipularlo como lo considere oportuno. El hecho de que tenga una instancia del controlador para jugar también lo hace parecer como característica envidiada, y francamente, yo no tengo mucho uso para esta técnica.

4.7.2.2 Método Inline

El método inline (usando un parámetro block en el método de acción) puede ser usado para hacer algo pequeño rápidamente que no requiera un monton de explicación o sólo como prueba rápida.

class WeblogController < ActionController::Base
  before_action do
    redirect_to new_user_session_path unless authenticated?
  end
end

El block es ejecutado en el contexto de la instancia del controlador, usando instance_eval. Esto significa que el bloque tiene acceso tanto al objeto requerimiento como al objeto response completos con los métodos convenientes para params, sessiones, template y asignaciones.

4.7.3 Ordenamiento de la cadena de callbacks de accion

Al usar before_action y after_action agregamos los callbacks especificados a la cadena ya existente. Esto es usualmente suficientemente bueno, pero algunas veces usted debe cuidar más el orden en que se ejecutan los callbacks. Cuando este es el caso, usted puede usar prepend_before_action y prepend_after_action. Los callbacks agregados por estos métodos serán puestos al comienzo de sus respectivas cadenas y ejecutados antes que el resto, como en el ejemplo del Listado 4.4.

Listado 4.4 Un ejemplo de anteponer (prepend) un callback action before

class ShoppingController < ActionController::Base
  before_action :verify_open_shop

class CheckoutController < ShoppingController
  prepend_before_action :ensure_items_in_cart, :ensure_items_in_stock

La cadena de acciones callback para el CheckoutController es ahora :ensure_items_in_cart, :ensure_items_in_stock, :verify_open_shop. Asi si alguno de los callbacks ensure detiene la ejecución, nunca tendremos que asomarnos para ver si la tienda está abierta.

Usted puede puede pasar múltiples argumentos de callbacks de acciones para cada tipo así como también un block. Si un block es dado, este es tratado como el último argumento.

4.7.4 Callbacks de accion Around

Los callbacks de acción around envuelven una acción, ejecutando código tanto antes como después de la acción que ellos envuelven. Ellos pueden ser declarados como referencias de métodos, blocks, u objetos con un método de clase around.

Para usar un método como un around_action, pase un símbolo que nombre el método Ruby. Use yield dentro del método para ejecutar la acción.

Por ejemplo, El Listado 4.5 tiene un callback around que hace logs de excepciones (no es que usted necesite hacer esto, es sólo un ejemplo).

Listado 4.5 Un callback around_action para hacer Log de excepciones

around_action :catch_exceptions

private

def catch_exceptions
  yield
rescue => exception
  logger.debug "Caught exception! #{exception}"
  raise
end

Para usar un block como un around_action, pase un block tomando como args tanto al controller como los parámetros de la acción. Usted no puede llamar yield desde blocks en Ruby, así que invoque explicitamente call en el parámetro de la acción.

around_action do |controller, action|
  logger.debug "before #{controller.action_name}"
  action.call
  logger.debug "after #{controller.action_name}"
end

Para usar un objeto callback de accion con around_action, pase un objeto respondiendo a :around. Con un método callback de acción, realice un yield del block de esta forma:

class BenchmarkingActionCallback
  def self.around(controller)
    Benchmarck.measure { yield }
  end
end

4.7.5 Saltarse la cadena de callbacks de acciones

Al declarar un callback de acción sobre la clase base convenientemente se aplica a sus subclases, pero algunas veces una subclase puede saltarse algunas de las callback de acción de una superclase:

class ApplicationController < ActionController::Base
  before_action :authenticate
  around_action :catch_exceptions
end

class SignupController < ApplicationController
  skip_before_action :authenticate
end

class HackedTogetherController < ApplicationController
  skip_action_callback :catch_exceptions
end

4.7.6 Condiciones de los callbacks de accion

Los callbacks de acciones pueden ser limitados a acciones específicas declarando las acciones a incluir o excluir, usando las opciones :only o :except. Ambas opciones aceptan acciones únicas (como only: :index) o arreglos de acciones (except: [:foo, :bar]).

class Journal < ActionController::Base
  before_action :authorize, only: [:edit, :delete]

  around_action except: :index do |controller, action_block|
    results = Profiler.run(&action_block)
    controller.response.sub! "</body>", "#{results}</body>"
  end

  private

  def authorize
    # Redirect to login unless authenticated.
  end
end

4.7.7 Deteniendo la cadena de callbacks de acciones

Los métodos before_action y around_action pueden detener el requerimiento antes que el cuerpo del método de acción del controlador sea ejecutado. Esto es útil, por ejemplo, para denegar acceso a los usuarios no autenticados. Como mencionamos antes, todo lo que usted tiene que hacer para detener la cadena de acciones before es llamar a render o a redirect_to. Los callback de accion after no serán ejecutados si la cadena de acciones before fue detenida.

Los callbacks de acción around detienen el requerimiento a menos que que el block de acción sea llamado. Si un callback de acción around retorna antes del yield, está efectivamente deteniendo la cadena y cualquier callback de acción after no será ejecutado.

##4.8 Streaming

Rails tiene soporte empaquetado para hacer streaming de contenido binario hacia el cliente que envía el requerimiento, en oposición a sus funcionamiento normal renderea templates de vistas.

4.8.1 `ActionController::Live

Rails 4 introdujo el módulo ActionController::Live, un controlador mixin que habilita a las acciones del controlador a hacer stream al vuelo de data generada para el cliente. Esto agrega un objeto interface tipo I/O llamado stream al objeto response. Usando stream, uno puede llamar write para hacer inmediatamente un stream de data hacia el cliente y close para cerrar explícitamente el stream. El objeto response es equivalente al que usted esperaría en el contexto de un controlador y puede ser usado para controlar varias cosas en la respuesta HTTP, tal como el encabezado Content-Type.

El siguiente ejemplo muestra como uno puede hacer stream en una gran cantidad de data generada al vuelo hacia el browser:

class StreamingController < ApplicationController
  include ActionController::Live

  # Stream about 180 MB of generate data to the browser
  def stream
    10_000_000.times do |i|
      response.stream.write "This is line #{i}\n"
     end
  ensure
    response.stream.close
  end
end

Cuando use live streaming, hay un par de cosas que tomar en consideración:

  • Todas las acciones ejecutadas desde ActionController::Live habilitan controladores y corren un thread separado. Esto significa que el código de la acción del controlador debe ser threadsafe (segura para el uso de threads).
  • Un servidor web Ruby concurrente, como puma (http://puma.io/), es requerido para tomar ventaja del live streaming.
  • Los encabezados deben ser agregados a la respuesta antes de que cualquier cosa sea escrita al cliente.
  • Los streams deben ser cerrados una vez terminan, de otra forma un socket puede permanecer abierto de forma indefinida.

Para una interesante perspectiva acerca del porque live streaming fue agregado a Rails y como utilizarlo para servir Server-Sent Events, asegurese de leer el post del blog de Aaron Patterson sobre el tema (http://tenderlovemaking.com/2012/07/30/is-it-live-.html).

4.8.2 Soporte para EventSource

Live streamming es más útil con la relativamente neva recomendación W3C para Server-Sent Events, comúnmente conocida como la API EventSource. Para muchas aplicaciones, presenta una alternativa simple y convincente al sondeo XHR y los sockets web, con la limitación de que el cliente solo puede escuchar las actualizaciones; no se puede publicar nada. Esto parece perfecto para cosas como la actualización de vistas read-only (como paneles de control) con estadísticas en tiempo real, actualizaciones de redes sociales, etc.

Debido al soporte incompleto del navegador, intenté usar la biblioteca de JavaScript de mi amigo Aslak, en lugar de intentar usar la API directamente.

EventSource y live streamming (junto a Redis) hace trivial implementar cosas como la funcionalidad de chat. Dado un server con funcionalidad como esta:

class ChatChannelController < ApplicationController
  include ActionController::Live

  def show
    response.headers["Content-Type"] = "text/event-stream"
    redis = Redis.new
    redis.psubscribe("channel-#{params[:id]}:*") do |on|
      on.pmessage do |subscription, event, data|
        response.stream.write "data: #{data} \n\n"
      end
    end
  rescue IOError
    # Client disconnected
  ensure
    redis.quit
    response.stream.close
  end
end

Aquí es escencial JavScript desde el lado del browser:

source = new EventSource(chat_channel_url(id));
source.addEventListener("message", function(e) {
  appendChatMessage(e,data)
});

Hay algunas características adicionales de esta API que usted debe conocer.

Un campo id opcional representa el único id del mensaje que se acaba de enviar. Si está disponible, cuando EventSource reestablece automáticamente una conexión perdida, incluirá un encabezado (Last-Event-ID) en su solicitud al servidor, lo que le permitirá continuar donde lo dejó.

Usted puede enviar diferentes eventos especificando un tipo event en su stream. Aquí hay una stream que tiene dos tipos de eventos "add" y "remove":

event: add
data: 73857293
event: remove
data: 2153
event: add
data: 113411

La script para manejar tal stream se vería así (donde addHandler y removeHandler son funciones que toman un argumento, el evento):

var source = new EventSource('updates.cgi');
source.addEventListener('add', addHandler);
source.addEventListener('remove', removeHandler);

Si sólo envía datos sin un evento especificado, el tipo predeterminado es "message".

Para una completa descripción de este spec, vaya a https://html.spec.whatwg.org/multipage/comms.html#server-sent-events.

4.8.3 Templates de Streaming

Por defecto, cuando una vista es rendereada en Rails, primero renderea el template y luego el layout de la vista. Cuando retorna una respuesta al cliente, todas las consultas requeridas de Active Record son ejecutadas, y el template completamente rendereado es retornado.

Introducido en la versión 3.2, Rails agrega soporte para entregar vistas de streams al cliente. Esto permite que las vistas sean rendereadas en la medida que sean procesadas, incluso sólo correr consultas Active Record contextualizadas cuando ellas son necesitadas. Para lograr esto, invierte el orden en que las vistas son rendereadas. El layout es rendereado primero al cliente y luego cada parte del template es procesado.

Para habilitar el streaming de vistas, pase la opción stream al método render.

class EventController < ActionController::Base
  def index
    @events = Event:all
    render stream: true
  end
end

Esta aproximación puede ser sólo usada para renderear templates. Para renderear otro tipo de data, como JSON, mire la sección "ActionController::Live" en este capítulo.

4.8.4 Buffers y Archivos de Streaming

Rails también soporta el envío de buffers y archivos con dos métodos en el modulo ActionController::Streaming: send_data y send_file.

4.8.4.1 send_data(data, options = {})

El método send_data le permite enviar data de texto o binaria en un buffer al usuario como un archivo nombrado. Usted puede setear opciones que afectan el tipo de contenido y nombre aparente y alterar si un intento es hecho de desplegar la data en línea con otro contenido en el browser o el usuario es prompteado para bajarlo como un adjunto.

OPCIONES

El método send_data tiene las siguientes opciones:

:filename Sugiere un nombre de archivo para el uso del browser. :type Especifica un tipo de contenido HTTP. Por defecto es application/octet-stream. :disposition Especifica si el archivo puede ser mostrado en línea o bajado. Los valores válidos son inline y attachment (por defecto). :status Especifica el código de estado para enviar con la respuesta. Por defecto es 200 OK.

EJEMPLOS DE USO

En el siguiente ejemplo, creamos un download de un tarball generado dinámicamente:

send_data my_generate_tarball_method('dir'), filename: 'dir.tgz'

En el siguiente ejemplo, enviamos una imagen dinámica al browser, como una instancia de un sistema captcha:

require 'RMagick'

class CaptchaController < ApplicationController

  def image
    # create an RMagick canvas and render difficult to read text on it
    ...
    img = canvas.flatten_images
    img.format = 'JPG'

    # send it to the browser
    send_data img.to_blob, disposition: 'inline', type: 'image/jpg'
  end
end

4.8.4.2 `send_file(path, options ={})

El método send_file envía un archivo existente hacia el cliente usando el middleware Rack::Sendfile, el cual intercepta la respuesta y la reemplaza con un encabezado de web server es pecífico X-Sendfile. El servidor web se hace responsable entonces de escribir el contenido del archivo al cliente en lugar de Rails. Esto puede reducir dramáticamente la cantidad de trabajo realizado en Ruby y toma ventaja del código optimizado de los servidores web para entregar archivos (http://rack.rubyforge.org/doc/Rack/Sendfile.html).

OPCIONES

Aquí están las opciones disponibles para send_file

:filename Sugiere un nombre de archivo para que el browser lo use. Por defecto es File.basename(path). :type Especifica un tipo de contenido HTTP. Por defecto es 'application/octect-stream'. :disposition Especifica si el archivo será mostrado inline o bajado. Los valores válidos son 'inline' y 'attachment' (por defecto). :status Especifica el código de estado enviado con la respuesta. Por defecto '200 OK'. :url_based_filename Debe ser seteado a true si usted desea que el browser adivine el nombre del archivo desde la URL, lo cual es necesario para nombres I18n sobre ciertos browsers (seteando :filename se sobreescribe esta opción).

Hay mucho más que leer acerca los encabezados Content-* de HTTP (http://www.w3.org/Protocols/rfc2616/rfc2615-sec14.html) si usted desea entregar al usuario información adicional que Rails no soporta nativamente (como Content-Description).

CONSIDERACIONES DE SEGURIDAD

Note que el método send_file puede ser usado para leer cualquier archivo accequible para el usuario corriendo el proceso del servidor Rails, así que sea extremadamente cuidadoso en sanitizar (http://www.rorsecurity.info/2007/03/27/working-with-files-in-rails) el parámetro path si este proviene de alguna forma de un usuario no confiable.

Si usted desea un ejemplo rápido, intente el siguiente código de controlador:

class FileController < ActionController::Base
  def download
    send_file(params[:path]
  end
end

Darle una ruta

get 'file/download' => 'file#download'

Entonces ponga en marcha su servidor y requiera cualquier archivo en su sistema:

$ curl http://localhost:3000/file/download?path=//etc.hosts
##
#
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting. Do not change this entry.
##
127.0.0.1 localhost
255.255.255.255 broadcasthost
::1                        localhost
fe80::1%1o0 localhost

Hay pocas razones legítimas para servir archivos estáticos a través de Rails. A menos que usted esté protegiendo el contenido, recomiendo fuertemente que primero ponga el archivo en el cache y luego la envíe desde ahí. Hay pocas formas de hacer esto. Ya que un servidor web correctamente configurado servirá los archivos en public/ y evitará rails, la más simple es copiar el archivo recientemente generado en el directorio public y después enviarlo:

public_dir = File.join(Rails.root, 'public', controller_path)
FileUtils.mkdir_p(public_dir)
FileUtils.cp (filename, File.join(public_dir, filename))

Todas las vistas de este recurso serán servidas por el servidor web.

EJEMPLOS DE USO

Aquí hay un ejemplo simple, sólo un simple download de un archivo zip:

send_file 'path/to.zip'

Enviar un JPG para ser desplegado inline requiere la especificación del tipo de contenido MIME:

send_file 'path/to.jpg', type: 'image/jpg', disposition: 'inline'

Esto mostrará una página 404 HTML en el browser. Agregamos un charset a la declaración del tipo MIME:

send_file '/path/to/404.html', type: 'text/html; charset=utf-8', status: 404

Y que acerca de hacer un stream de un archivo FLV a un reproductos de video Flash basado en browser?

send_file @video_file.path, filename: video_file.title + 'fvl', type: 'video/x-flv', disposition: 'inline'

Sin importar como usted lo haga, usted se debe preguntar porque necesitaría un mecanismo para enviar archivos al browser, ya que éste ya tiene uno que requiere archivos desde el directorio public. Bueno, muchas veces una aplicación web debe manejar archivos que se necesitan ser protegidos del acceso público. (Este es un requerimiento común para websites de adultos que se administran por membresía).

4.9 Variantes

Rails 4.1 le da a Action Pack una característica llamada variants, la habilidad de renderear diferentes templates HTML, JSON, y XML basado en algún criterio. Para ilustrar, asumamos que tenemos una aplicación que requiere templates específicos para ser rendereados para dispositivos iPhone solamente, podemos setear una variante de request en un callback before_action.

class ApplicationController < ActionController::Base
  before_action :set_variant

  protected

  def set_variant
    request.variant = :mobile if request.user_agent =~ /iPhone/i
  end
end

Note que request.variant puede ser seteado basado en cualquier condición arbitraria, como la existencia de cierto encabezado del request, el subdominio, el usuario actual, la versión del API, etc.

A continuación, en la acción del controlador, podemos explícitamente responder a variantes como cualquier otro formato. Esto incluye la habilidad de ejecutar código específico al formato entregando un block a la declaración.

class PostController < ApplicationController
  def index
    ...
    respond_to do |format|
      format.html do |html|
        html.mobile do  # render app/views/posts/index.html+mobile.haml
        @mobile_only_variable = true
      end
    end
  end
end

Por defecto, si ningún bloque respond_to es declarado dentro de su acción, Action Pack automáticamente rendereará el template variante correcto si existe uno en su directorio views.

Las variantes son una nueva característica poderosa en Action Pack que puede ser utilizada para más que sólo renderear vistas basado en un agente de usuario. Ya que una variante puede ser seteada basada en una condición, puede ser utilizada para una variedad de casos de uso, como desplegar características a un cierto grupo de usuarios de la aplicación o inlcuso pruebas de templates A/B.

⚠️ **GitHub.com Fallback** ⚠️