Sudoku on Rails, v6
Para esta revisión de nuestra aplicación nos hemos planteado los siguientes objetivos
<ul>
<li>Hacer realmente <em>algo</em> con el modelo del usuario. Ya tenemos la posibilidad de dar de alta nuevos usuarios y enviarles una clave de activación por correo… pero no utilizamos esta información de usuario para nada. La idea es guardar algo así como una tabla de puntuaciones con los nombres de los usuarios que han sido capaces de resolver el rompecabezas en menos tiempo y movimientos.</li>
<li>Una sugerencia de un lector: incluir la posibilidad de recuperar una cuenta una vez que hemos olvidado la clave (gracias, <strong>Neko</strong>), ampliando el código generado por el plugin <code>acts_as_authenticated</code></li>
<li>Traducir los mensajes de validación que nos devuelve <code>ActiveRecord</code> cuando tratamos de dar de alta un usuario.</li>
<li>Por último, aprovecharemos para hacer un pequeño rediseño de la web. Este último aspecto, siendo más o menos ajeno a Rails, no lo vamos a tratar aquí, pero lo podeis ver directamente en <a href="http://v6.sudokuonrails.com">la aplicación</a></li>
</ul>
<p>Como siempre, podeis descargar el código de la aplicación <a href="http://pantulis.sobrerailes.com/src/sor-6.0.tar.gz">aquí</a>.</p>
<!--more-->
<h4>Traducción de los mensajes de validación de <code>ActiveRecord</code></h4>
<p>Pretendemos que aparezcan en castellano los mensajes con errores de validación que muestra nuestra aplicación cuando tratamos dar de alta un nuevo registro en la base de datos (tales como que el campo clave de usuario no coincide, que el usuario o el correo electrónico ya existen, etcétera) Dichos mensajes son mostrados desde nuestra vista cuando invocamos el <em>helper</em> <code>error_messages_for</code></p>
<p>Inspirándonos <a href="http://barclay.textdrive.com/pipermail/ror-es/2005-November/000467.html">en este hilo de la lista ror-es</a>, vamos a acometer la tarea en varias etapas.</p>
<p>La primera es redefinir el <em>array</em> que usa <code>ActiveRecord</code> para construir los mensajes de error, para ello crearemos un fichero <code>lib/traduccion_validaciones.rb</code> y de momento le añadiremos esto:</p>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
module ActiveRecord class Errors begin @@default_error_messages = { :inclusion => "Es obligatorio", :exclusion => "Es campo reservado", :invalid => "No es válido", :confirmation => "No coincide la confirmación", :accepted => "Debe ser aceptado", :empty => "No puede estar vacío", :too_long => "Es demasiado largo (%d caracteres como máximo)", :too_short => "Es demasiado corto (%d caracteres como mínimo)", :wrong_length => "Debe tener %d caracteres", :taken => "Ya existe", :not_a_number => "No es un número" } end end end |
A continuación, nos crearemos nuestra propia versión de error_messages_for La versión por defecto nos indica que no ha podido grabarse el registro y nos muestra todos los mensajes de error, uno por uno, en una lista sin numerar. Nosotros preferimos simplemente generar un mensaje que diga que no se ha podido almacenar y luego mostraremos los mensajes de error para cada campo en la vista usando error_message_on (el cual usará el array default_error_messages que ya hemos traducido)
En lib/traduccion_validaciones.rb:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
module ActionView module Helpers module ActiveRecordHelper def mensajes_error_para (object_name, options = {}) options = options.symbolize_keys object = instance_variable_get("@#{object_name}") unless object.errors.empty? content_tag("div", content_tag( options[:header_tag] || "h2", "El #{object_name.to_s.gsub("_", " ")} no puede crearse")) end end end end end |
Ahora sólo nos falta invocar este código al cargar nuestra aplicación. El mejor lugar que he encontrado para hacerlo es al principio de config/environment.rb , justo antes de invocar a Rails::Initializer
1 2 3 4 5 6 7 |
# Bootstrap the Rails environment, frameworks, and default configuration require File.join(File.dirname(__FILE__), 'boot') require 'traduccion_validaciones' Rails::Initializer.run do |config| # Settings in config/environments/* take precedence those specified here ... |
A continuación veremos cómo hemos usado todo esto en el formulario de alta de usuario (que es el único que tenemos)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<%= mensajes_error_para 'usuario' %> <%= start_form_tag %> <p><%= error_message_on(:usuario, :login) %><label for="login">Usuario</label><br/> <%= text_field 'usuario', 'login' %></p> <p><%= error_message_on(:usuario, :email) %><label for="email">Email</label><br/> <%= text_field 'usuario', 'email' %></p> <p><%= error_message_on(:usuario, :password) %><label for="password">Clave</label><br/> <%= password_field 'usuario', 'password' %></p> <p><%= error_message_on(:usuario, :password_confirmation) %><label for="password_confirmation">Confirmar clave</label><br/> <%= password_field 'usuario', 'password_confirmation' %></p> <p><%= submit_tag 'Alta de usuario' %></p> <%= end_form_tag %> |
Lo cual, combinado con las hojas de estilo termina dando el siguiente aspecto cuando hay algún problema al validar los datos del formulario.

Listado con las mejores puntuaciones
En primer lugar, tendremos que crear una tabla en la base de datos donde almacenar las puntuaciones de las patidas de aquellos jugadores qe estén registrados. Para ello usaremos, como ya vimos en la revisión anterior, las migraciones:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class GeneraTablaPuntuaciones < ActiveRecord::Migration def self.up create_table :puntuaciones do |t| t.column :tiempo, :integer t.column :fecha, :datetime t.column :usuario_id, :integer t.column :movimientos, :integer end end def self.down drop_table :puntuaciones end end |
Al correr esta migración se creará la correspondiente tabla puntuaciones. Como vemos almacenaremos tanto las jugadas empleadas en terminar de completar el tablero como el tiempo (recordemos que ya llevamos cuenta de ellos en nuestro modelo tablero.rb). Como siempre, para aplicar estos cambios sólo tenemos que hacer
rake migrate
A continuación modificaremos el controlador del tablero (app/controllers/sudoku_controller.rb) Cuando la partida acaba (lo cual se comprueba tras cada movimiento) redirigiremos al usuario al método fin_partida del controlador de la tabla de puntuaciones.
1 2 3 |
def acabar redirect_to(:controller => 'records', :action => 'fin_partida') end |
La acción fin_partida comprueba, obviamente, que el tablero esté resuelto (para evitar que algún usuario astuto pueda querer hacer trampas invocando directamente este método) y a continuación almacena la nueva puntuación en nuestra tabla de puntuaciones
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
def fin_partida if tablero.resuelto? == false redirect_to(:controller=>'sudoku', :action=>'list') end movimientos = tablero.movimientos segundos = tablero.dame_segundos session[:tablero] = nil if logged_in? then puntuacion = Puntuacion.new puntuacion.tiempo = segundos puntuacion.movimientos = movimientos puntuacion.usuario = current_usuario puntuacion.fecha = DateTime.now if puntuacion.save flash[:notice]="Has terminado la partida en #{segundos} segundos y #{movimientos} movimientos" else flash[:notice]="¡No he podido guardar tu puntuacion, lo siento!" end else flash[:notice]="No estás registrado así que no guardaré tu marca personal. Tu tiempo ha sido #{segundos} segundos, y has hecho #{movimientos} movimientos" end redirect_to(:action=>'index') end |
En cualquier caso, se redirige a la acción index del controlador de récords cambiando el mensaje que se muestra al usuario. La acción index sencillamente muestra la lista de récords usando para ello un objeto de paginación, que nos permitirá ver las puntuaciones de diez en diez.
1 2 3 |
def index @paginador_puntuaciones, @puntuaciones = paginate (:puntuacion, :order_by => 'tiempo ASC, movimientos ASC') end |
Y en index.rhtml pintamos la tabla de puntuaciones así obtenida
<%= stylesheet_link_tag("tabla_records") %>
Listado de mejores partidas
Usuario
Fecha
Tiempo
Movimientos
<% for puntuacion in @puntuaciones
usuario=puntuacion.usuario
-%>
">
<%= h(usuario.login) %>
<%= puntuacion.fecha.strftime("%d-%m-%y") if puntuacion.fecha %>
<%= pinta_hms(puntuacion.tiempo) if puntuacion.tiempo %>
<%= puntuacion.movimientos%>
<% end -%>
<%= pagination_link (@paginador_puntuaciones) %>
<p>Hemos usado una función de ayuda <code>damehms</code> para mostrar el tiempo empleado en acabar la partida de una manera más cómoda de ver: transformamos un número en segundos en una cadena de texto que dice las horas, minutos y segundos (hasta los días, de ser necesario, aunque creo que los sudokus que generamos no son tan difíciles :-) Esta función la hemos definido en <code>app/helpers/records_helper.rb</code> de la siguiente manera</p>
module RecordsHelper
def damehms (tiempo)
segundos = tiempo % 60
tiempo /= 60
minutos = tiempo % 60
tiempo /= 60
horas = tiempo % 24
tiempo /= 24
{:dias => tiempo,
:horas => horas,
:minutos => minutos,
:segundos => segundos}
end
def pinta_hms (tiempo)
cadena = ""
if tiempo
t=damehms(tiempo)
cadena="#{t[:segundos]} segs"
if t[:minutos] != 0
tmp="#{t[:minutos]} min, #{cadena}"
cadena=tmp
end
if t[:horas] != 0
tmp="#{t[:horas]} hr, #{cadena}"
cadena=tmp
end
if t[:dias] != 0
tmp="#{t[:dias]} dias, #{cadena}"
cadena=tmp
end
end
cadena
end
end
Recuperación de claves
La estrategia será la siguiente: permitir que el usuario recupere su clave enviándole un mensaje a su cuenta de correo con un enlace que le permitirá entrar en la aplicacion sin autenticarse, tras lo cual podrá modificar su clave (recordemos que no le podemos enviar la clave porque ésta se almacena cifrada y el proceso no es reversible) Para ello, primero será necesario permitir que un usuario registrado pueda modificar su clave. Lo haremos en una acción modificar
1 2 3 4 5 6 7 8 9 10 11 |
def modificar return unless request.post? and current_usuario current_usuario.password=params[:usuario][:password ] current_usuario.password_confirmation=params[:usuario][:password_confirmation] if current_usuario.save redirect_back_or_default(:controller => '/cuenta', :action => 'index') flash[:notice] = "Datos del usuario modificados" end end |
y en modificar.rhtml
<% if current_usuario then
@usuario=current_usuario
%>
Configuración del usuario <%= @usuario.login %>. Aquí puedes cambiar tu clave personal.
<%= mensajes_error_para (:usuario) %>
<%= start_form_tag ({ :action => :modificar }) %>
<%= error_message_on(:usuario, :password) %>
<%= password_field 'usuario', 'password' %>
<%= error_message_on(:usuario, :password_confirmation) %>
<%= password_field 'usuario', 'password_confirmation' %>
<%= submit_tag 'Modificar cuenta' %>
<%= end_form_tag %>
<% else %>
<% end %>
No hay ningún misterio aquí: cuando se invoca la acción modificar siguiendo el correspondiente enlace de la página principal del usuario (método GET) se mostrará el formulario. Cuando el usuario rellene los datos se invocará otra vez el método pero esta vez request.post? devolverá true y modificaremos el objeto del usuario, cambiando los seudocampos password y password_confirmation (recordemos que estos no se guardan en la base de datos) Al invocar el método save, ActiveRecord comprobará que los campos de clave coinciden (porque en nuestro modelo de usuario lo especificamos así con validates_confirmation_of :password) y se generará la clave encriptada para guardarla en la base de datos. Ahora sólo necesitamos que un usuario que haya olvidado su clave pueda acceder a esta página mediante un enlace en su correo. Veamos el método recuperar
1 2 3 4 5 6 7 8 9 10 |
def recuperar @usuario = Usuario.find_by_email(params[:email]) return unless @usuario @usuario.genera_codigo_activacion if @usuario.save UsuarioNotifier.deliver_clave_perdida(@usuario) flash[:notice] = "Se ha enviado un correo con instrucciones a la direccion especificada" end end |
El método find_by_email es sintético: no hemos tenido que definirlo en nuestra clase Usuario, sino que ActiveRecord lo genera para nosotros. En general, para cualquier clase que herede de ActiveRecord estarán definidos los método find_by_@XXX siendo XX cualquier columna de la tabla de la base de datos (el mecanismo que usa @ActiveRecord para ello consiste en capturar la excepción de método no definido y ver si el método invocado es de la forma find_by , en cuyo caso realiza la búsqueda por el atributo pertinente y lo devuelve como resultado del método. ¡No intenteis hacer esto en vuestro lenguaje compilado favorito!)
Tras esto, no queda mucho más por hacer, sólo genera el código de activación invocando para ello la acción que ya estaba definida en usuario.rb pero que es privada a la clase, por tanto no la podemos invocar desde el controlador, así que creamos el siguiente método público
1 2 3 |
def genera_codigo_activacion make_activation_code end |
Por último, el Notifier de clave_perdida es similar que el ya existente para enviar la clave de activación cuando se produce el alta de usuario. Tras recibir este mensaje, el usuario tendrá un nuevo código de activación y podrá entrar en su cuenta sin introducir la clave para poder crear una nueva.
Conclusión
Aunque pueda parecer lo contrario por la extensión del post, en esta revisión hemos conseguido bastante funcionalidad nueva a cambio de muy poco código real: probablemente lo que más trabajo nos ha costado ha sido el diseño CSS de las tablas de puntuaciones y el nuevo layout de la aplicación.
¿Has encontrado algún fallo? ¿Crees que se podía mejorar algo? ¡Deja tu comentario!
