sisè / segona

Soy Víctor Rodríguez, programador web freelance con más de 10 años de experiencia

Render la última página vista en Rails

En Rails existe una manera muy sencilla de redireccionar a la última acción visitada por el usuario de nuestra aplicación. Es tan sencillo como llamar a esta función dentro de cualquier acción:

redirect_back(fallback_location, **args)

Opcionalmente se le puede pasar el parámetro fallback_location (con valor a una url o path por defecto) para asegurarnos que siempre llegará a algún sitio. Imagina que no ha habido ninguna petición anterior a esta llamada… 404?

Bueno, esto es a partir de la versión 5 de Rails, para versiones anteriores sería:

redirect_to :back

Pero no pasa lo mismo si lo que queremos es simplemente renderizar la última página vista.

Os pongo un ejemplo: tenemos un modelo que se puede editar/actualizar desde 2 páginas distintas en nuestra aplicación, pero como lo que estamos haciendo es actualizar los datos del modelo siempre acabamos llamando a la misma acción ‘update’ del controller del modelo. Algo así para el modelo User

def update
  @user = User.find(params[:id])
  if @user.update user_params
    redirect_to @user, notice: 'Usuario actualizado de manera correcta.'
  else
    render :edit
  end
end

Si las validaciones fallan a la hora de actualizar los datos, es habitual renderizar la pantalla anterior y mostrar los errores que se han producido. La clave es esta línea:

render :edit

El problema con esto es que siempre nos llevará a la misma pantalla, a la de edición, y a lo mejor queremos que la pantalla que vea nuestro usuario sea la de la vista de otro modelo (donde hemos editado los datos).

Bueno, pues aquí viene nuestro amigo referrer a la ayuda! En el objeto ActionDispatch::Request contiene la url previa visitada. Podemos acceder a dicha url mediante el header ‘Referer’ o directamente por el método request.referrer. Con esta información podemos traducir la url por un controlador y una acción gracias al router de la aplicación. La llamada sería la siguiente:

# con request.referrer = 'http://localhost:3000/user/1'
Rails.application.routes.recognize_path(request.referrer)
# Devolería: { :controller => 'users', :action => 'show', :id => '1' }

De esta manera, podemos acceder al action y llamar a render sin más en nuestro controller:

prev = Rails.application.routes.recognize_path(request.referrer) 
render prev[:action].to_sym

Si lo queremos hacer extensible a toda nuestra aplicación podemos crear el siguiente método dentro del ApplicationController:

def render_back(fallback)
  if request.referrer
    prev = Rails.application.routes.recognize_path(request.referrer)
    render prev[:action].to_sym
  else
    render fallback
  end
end

Y llamarlo siempre que necesitemos así:

render_back(fallback: :edit)

Ale, eso es todo. Bueno, un problema que veo en esta implementación es que no tenemos en cuenta el controller, por lo que sólo funciona de manera correcta siempre y cuando la edición/actualización del modelo sea desde dos acciones del mismo controller.

:P

Lanzar una Rails Task dentro de una migración

Para uno de los proyecto en los que estoy trabajando he creado una migración y una task para migrar unos datos. En concreto he usado, no se si llamarle pattern, counter_cache en una asociación entre modelos para evitar el problema de N+1 queries. Hasta ahí todo normal.

Lo curioso es que a la hora de publicar a producción la nueva versión he encontrado una manera de lanzar la task dentro de la migración. Así con una simple llamada de capistrano se ejecuta todo y no tengo que acceder al server a ejecutar la task manualmente. Os pongo el ejemplo:

class AddCommentsCountToArticle < ActiveRecord::Migration[5.0]
  def change
    add_column :articles, :comments_count, :integer, default: 0

    reversible do |dir|
      dir.up do
        Rake::Task['model:reset_counters'].invoke
      end
    end
  end
end

La clave está en llamar (invocar) la task dentro de este código:

reversible do |dir|
  dir.up do
    Rake::Task['namespace:taskname'].invoke
  end
end

Donde namespace es le namespace de la task y taskname el nombre que le hayas dado a tu task. Así de fácil.

:P

Sobre rake tasks en Rails 4

Ayer aprendí un par de cosillas sobre la creación de tareas en Rails 4, y para no olvidar… Apuntado queda.

Acceder al modelo de la aplicación

Para acceder al modelo de la aplicación sólo debemos hacer que la tarea sea dependiente del entorno de ejecución:

task task_name: :environment do |t|
  # You can use your model from here
end

La parte interesante aquí es :environment. Este pequeño código hace toda la magia necesaria para poder acceder a nuestro modelo, sin esto no funciona. Haz la prueba.

Una explicación un poquito más correcta es que a las tareas les puedes pasar un array de dependencias. Es lo que hacemos al añadir :environment detrás del nombre de la tarea en formato clave: :valor (vaya, un hash).

Pasar parámetros

Hay dos maneras de pasar parámetros (que yo conozca). La oficial, según la guía de Rails es la siguiente:

task :task_name, [:arg_1] do |t, args|
  # You can use args from here
end

Para ejecutar esta tarea debes hacerlo así:

$ bin/rake "task_name[value 1]" # entire argument string should be quoted

Encomillar el nombre de la tarea y pasar los parámetros entre corchetes ([]) no es muy bonito, estética pura claro. Así que busqué otra forma y la encontré. Se le pueden pasar los parámetros como variables de entorno. Así:

task task_name: :environment do |t|
  arg_1 = ENV['arg_1']
  # You can use arg_1 from here
end

Ten en cuenta que debes hacerla dependiente del entorno de ejecución, si no lo haces, cagada pastoret… Ahora la llamda cambia de la siguiente manera:

$ bin/rake task_name arg_1=Value

Es simple y un poco más claro de leer.

gem install Nokogiri on OS X Mavericks

Por si le sirve a alguien.

Al intentar actualizar las librerias en Rails con el comando bundle update, si falla la instalación de Nokogiri (versión 1.6.6.2 en el momento de la publicación de esta entrada) intentad esto:

$ xcode-select --install

Una vez instalado el paquete podéis probar:

$ gem install nokogiri

A mi me ha funcionado! O_o

Modulo de paginación con Ruby on Rails 4

Hoy os voy a mostrar cómo añadir una sencilla paginación a vuestros interminables listados en Rails. Sí, no uso ninguna librería como will_paginate u otras que debes de conocer. La idea es aprender haciendo las cosas uno mismo, si no, nos volvemos memos.

Para empezar, ¿qué necesitamos?

Lo primero y principal, un listado de alguno de nuestros modelos. Pongamos que tenemos un listado de usuarios, así que nuestro modelo se llamará ¡’User’! (te lo has currado…)

Ya, claro, pero hasta ahí todos lo sabemos hacer, solo hay que hacer un scaffolding y Rails lo tiene hecho, pintado y funcionando.

Vale, no me peguéis todos a la vez…

Las páginas

Una paginación necesita varios datos para funcionar de manera correcta:

  • Número de página
  • Número de usuarios por página
  • Número de páginas totales

Con estos datos podemos montar la query que necesitamos para sacar los usuarios que tocan en cada página.

A montar la página

Para montar la página debemos hacer, como en cualquier otro caso, definir la ruta o url a la que llamar. Sin esto no tenemos manera de crear los bonitos links que la hagan funcionar. Muy bien, vayamos a nuestro fichero routes.rb y añadimos la ruta:

get 'users/page/:page_id', to: 'users#index', as: 'users_pagination'

Lo que hay aquí es la ruta a la acción index del controlador UsersController con un parámetro llamado page_id que será la página que hay que mostrar. Y el método usado es GET ya que se trata de una consulta o lectura del modelo, no estamos grabando/borrando nada que necesite otro método más apropiado.

Ya tenemos el primero de los 3 datos que nos hacen falta. Para el segundo, Número de usuarios por página, lo podemos definir directamente en la acción (al final de todos os enseño a limpiar el código con un módulo, vayamos paso a paso).

Y el tercer dato, viene dado por las queries que tenemos que montar para realizar la paginación.

El código quedaría algo así:

class UsersController < ApplicationController
  def index
    # Número de usuarios por página
    users_per_page = 30 
    @page = params[:page_id].to_i
    @page = 1 unless @page > 0
    # Primer registro que debemos sacar según la pagina
    offset = (@page - 1) * users_per_page
    # Número de paginas totales
    @pages_count = (User.all.count.to_d / users_per_page).ceil
    @users = User.all.limit(users_per_page).offset(offset)
  end
  ... #resto de acciones te las puedes imaginar
end

¿Qué estamos haciendo?

Primero definimos el número de usuarios por página, en este caso 30.

Luego calculamos la página que tenemos que mostrar. O bien la sacamos del parámetro o lo inicializamos si este no existe. Como es casi seguro que tienes una línea en tu fichero routes.rb muy parecida esta:

resources :users

Puede ser que lleguemos a la acción index del UsersController sin parámetro alguno ;)

Luego calculamos el primer registro que debemos sacar, es decir el offset de la query. Montamos una query para sacar el número de páginas totales (es necesario para tener los límites de la paginación en la vista, podría funcionar sin, pero entonces llegaríamos a tener infinidad de páginas vacías de registros… queda poco cuidado)

Y para acabar, sacamos los registros de usuarios de la página que toca con la última línea de código.

Si te has fijado, hay 3 variables que son de instancia (pista, empiezan por @). @page, @pages_count y @users son las variables que necesitamos para la vista, la tabla en views/users/index.html.haml

La de @users es fácil saber por qué, sin la lista de usuarios no hay lista.

@page y @pages_count nos ayudarán a montar los enlaces a la página siguiente y anterior, siempre y cuando sean necesarios.

El código para montar los enlaces es el siguiente:

%ul.pager
  %li{ class: pagination_previous_class(@page) }
    =link_to 'Previous', users_pagination_path(@page - 1)
  %li{ class: pagination_next_class(@page, @pages_count) }
    =link_to 'Next', users_pagination_path(@page + 1)

Explicación:

Montamos una lista sin numerar con 2 enlaces a la página anterior y a la página siguiente. (Por cierto uso HAML, por si no te has dado cuenta hasta ahora).

Determinamos la clase que va a tener el list item (li) mediante 2 helpers que podemos guardar en el fichero application_helper.rb

module ApplicationHelper
  def pagination_previous_class(page)
    "disabled" if page <= 1
  end
  
  def pagination_next_class(page, pages_count)
    "disabled" unless page < pages_count
  end
end

Lo que hacen es añadir una clase 'disabled' si estamos en la primera página (para el enlace a la página anterior) o si estamos en la última página (para el enlace a la página siguiente, YA NO HAY MÁS PÁGINAS! :P) Esto es solo el maquillaje, para deshabilitarlos debéis usar Javascript del bueno. Os lo dejo de ejercicio.

¿Ya está? ¿Eso es todo?

NOOOO! os he dicho que limpiaríamos un poco el código, ¿no?, pues allá vamos...

Módulo ModelPaginator

Crea un fichero llamado model_paginator.rb dentro de la carpeta lib. Y añade el código de la paginación sin la query específica para nuestro modelo (User.all). De esta manera podemos reusar el módulo con otros modelos. Quedará algo así:

module ModelPaginator
  def paginate(resource)
    items_per_page = 30
    @page = params[:page_id].to_i
    @page = 1 unless @page > 0
    offset = (@page - 1) * items_per_page
    @pages_count = (resource.count.to_d / items_per_page).ceil
    resource.limit(items_per_page).offset(offset)
  end
end

Hemos cambiado User.all por resource, y lo hemos pasado como parámetro. Finalmente nuestro UsersController debemos modificarlo de esta manera:

class UsersController < ApplicationController
  include ModelPaginator

  def index
    @users = paginate(User.all)
  end
  ... #resto de acciones te las puedes imaginar
end

Hemos incluido el módulo y hemos limpiazo el código de la acción index. Mucho más fácil de saber qué estamos haciendo ahora que antes, ¿no?

Una cosita más, para que el módulo lo podamos incluir en los controladores y funcione, tenemos que decirle a Rails que lo añada al lanzar la aplicación de esta manera en el fichero config/application.rb

config.autoload_paths << Rails.root.join('lib')

Así, todos los módulos o clases que añadas a la carpeta lib Rails los 'autocargará' an inicializar la aplicación.

Ahora sí, ya está. Si quieres ampliar funcionalidades, tu mismo. ¿Puedes tener un fichero externo de configuración con el número de items por página? ¿En formato YAML por ejemplo?