sisè / segona

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

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

UPDATE: dejo aquí un enlace con un artículo relacionado Role based routing in Rails

Desarrollar Java interpretado??

Hola!!

Sí, sí, existe un plugin de la rehostia (con perdón) que permite tener un tomcat levantado e ir desarrollando como si fuera php sobre un apache. Bueno, no me enrollo mucho que lo estoy comenzando a probar y todavía no os he dicho el nombre.

Se llama JRebel y lo podéis encontrar aquí http://zeroturnaround.com/software/jrebel/. La pega: 365$ por la licencia anual, es decir, 1$/día. Totalmente amortizable…

Simplemente compráis la licencia (tienen una de prueba de 2 semanas) y os instaláis el plugin en vuestro IDE favorito.

Un saludo!

El 5% de lo que he aprendido jugando a las empresas

El 5% de lo que he aprendido jugando a las empresas es que nunca, nunca, aceptes entrar en una empresa con menos acciones que las que representen el 5% del total.

Durante los últimos 7 años he trabajado en una empresa muy pequeña (hasta el pasado Noviembre). Una startup (que molón queda) de la que sigo siendo socio con un 2% de las acciones. De lo único que sirve tener esa participación es de tener el deber moral de seguir luchando por que tire adelante. A la hora de pedir algo, no sirve de nada.

¿Te gustaría saber cómo van los números de la empresa? No puedes exigirlos, ¿te gustaría pedir una auditoría contable? No puedes. ¿Te gustaría…? A mi también me gustaría… Simplemente no puedes.

Por eso, es muy importante que cuando se crea una nueva empresa, y se te ofrece la posibilidad de entrar, que sea como mínimo con el 5% de las acciones, aunque te pueda costar más que a los demás.

Es algo que me ha costado aprender más de lo que me gustaría admitir. Y lo que voy a admitir es el doble de lo que mereces. O algo así :P

Nueva versión de rodriguezortega.com

Esta mañana he publicado una nueva versión de mi página web personal, rodriguezortega.com. Esta vez he priorizado explicar qué hago o hacemos juntos mi hermano David y yo a qué hemos hecho hasta la fecha. Creo que es más facil llegar al cliente potencial si explicamos qué servicios ofrecemos que un portfolio medio mal montado.

Os dejo con la web, para que la visitéis. rodriguezortega.com. Ha sido diseñada y programada para ser vista en PCs, Tablets y Smartphones siguiendo los estándares HTML5 y CSS3.