sisè / segona

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

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?

Definir página de inicio por rol de usuario con Ruby on Rails 4

Para una pequeña aplicación que estoy desarrollando en Ruby on Rails me ha surgido la necesidad o más bien las ganas de crear una página de inicio según el rol del usuario que esté identificado en el sistema. La idea es montar un dashboard en el futuro, y claro, lo que necesita un administrador no es lo que necesita un cliente del sistema. Así que había dos opciones, creas un controlador con una única acción que determine qué mostrar al usuario según su rol, o algo más elegante, le dejas decidir a Rails qué acción y controlador debe llamar de inicio. De esta manera, puedes separar mucho mejor la lógica de negocio de un usuario administrador y la de un usuario cuyo rol sea el de cliente.

Premisa: ¿qué queremos conseguir?

Algo tan sencillo como que la aplicación muestre una u otra información dependiendo del tipo de usuario de sesión. Si el usuario que se identifica en el sistema es un administrador, que pueda ver los nuevos pedidos que han creado los clientes del sistema. Y que si el usuario que se identifica es un cliente, que pueda ver el estado en el que están los últimos pedidos que ha realizado en el sistema. Por poner un ejemplo.

¿Cómo lo conseguimos?

Como sabrás, para definir la página de inicio o root en nuestra aplicación Ruby on Rails necesitas algo así dentro del fichero routes.rb

root to: "home#index"

Donde ‘home’ hace referencia al controlador HomeController e ‘index’ a la acción o función index que hay en él. Hasta aquí todo muy fàcil. ¿Pero si queremos distinguir entre varios tipos de usuarios como montamos más de una página root?

Aquí entran en juego las llamadas constraints (traducción: restricciones) que se pueden añadir a cualquier ruta definida en Rails.

Hay varias maneras de definir las restricciones con Rails, las podéis encontrar en la guía (siempre visitad la guía de Rails) a partir de aquí, en la sección de Rails Routing from Outside In de las guias.

La que nos interesa es la apuntada en el apartado 3.10. Advanced Constraints.

En concreto, determinaremos vía lambda si el usuario tiene el rol de administrador o de cliente en la restricción y Rails hará el resto del trabajo por nosotros. Parece mágia… Pero bueno, pongamos el código y lo explicamos después.

root to: 'home#index', constraints: lambda { |request| !request.env['warden'].user }

root to: 'admin/dashboard#index', as: 'admin_root',
  constraints: lambda { |request| request.env['warden'].user.administrator? }

root to: 'customer/dashboard#index', as: 'customer_root',
  constraints: lambda { |request| request.env['warden'].user.customer? }

Expliquemos un poco el código.

¿Qué ha pasado aquí?

La primera ruta, mira si en el sistema no existe un usuario en sesión, y llama a la acción index del HomeController. Necesaria cuando no hay nadie identificado, lógico.

La segunda, mira si el usuario es adminsitrador. En caso afirmativo, llama a la acción index del controlador DashboardController dentro del módulo admin.

'admin/dashboard#index'

Y la tercera, lo mismo pero para usuarios cliente. En este caso llama a la acción index del controlador DashboardController pero dentro del módulo customer.

'customer/dashboard#index'

Un poco más en detalle, vemos que la lambda recibe como parámtetro el objeto ActionDispatch::Request. Es en este objeto donde miramos si existe o no el usuario en sesión. Para ello, debes saber que estoy usando la librería Devise, construida sobre una librería llamada Warden.

Como vemos en su documentación, los usuarios se guardan dentro del entorno ‘warden’. Tal que así:

env['warden'].user

Esta simple línea nos da acceso al usuario en sesión. Ahora solo falta preguntarle si es administrador o cliente.

Lo podríamos haber hecho de la siguiente manera, accediendo directamente al atributo role de la clase User y comparándolo con los posibles valores, ‘administrator’ o ‘customer’.

env['warden'].user.role == 'administrator'
env['warden'].user.role == 'customer'

Pero eso requier saber demasiadas cosas sobre la clase usuarios fuera de ella, así que mejor implementar un par de funciones dentro de la clase User que nos respondan más directamente. El código queda mucho más limpio y fácil de mantener.

class User < ActiveRecord::Base
  
  def administrator?
    self.role == 'administrator'    
  end
  
  def customer?
    self.role == 'customer'
  end

end

Eso es todo, hace falta meterse a investigar las entrañas de Devise y Warden para llegar a esta solución, pero es realmente efectiva cuando la conoces.

:P