Ahí va una rápida que me ha costado trabajo encontrar (gracias a kithpom en #rubyonrails): cuando tenemos varias versiones de la gema de Rails instaladas en nuestro sistema (por ejemplo, la 2.0.2 y la 1.2.6), si ejecutamos rails en la consola se creará un esqueleto de aplicación 2.0 ¿Qué pasa si, por desdicha, necesitamos crear una aplicación 1.2.x?

rails _1.2.6_ myapp

La explicación, en cat `which rails`

Como ya ha anunciado Sergio, el próximo jueves comienza la II Conferencia Rails Hispana en Madrid. Este año, al igual que el anterior, estaré por allí y haré una presentación.

En este caso se trata de una introducción a Joyent Slingshot, la plataforma para integrar aplicaciones Rails en el escritorio. ¡Nos vemos allí!

Corriendo aplicaciones Rails en JBoss

El objetivo de esta anotación es mostrar los pasos necesarios para lanzar una aplicación Rails dentro de un servidor de aplicaciones Java, en este caso el popular JBoss La intención última es comprobar los progresos está llevando a cabo el equipo de JRuby. Quién nos lo iba a decir hace un año, así que… ¿quién nos dirá qué va a ocurrir para el año que viene?

Instalación de JRuby

El primer paso será la instalación de JRuby , por supuesto. Damos por ya instalado Java en nuestra máquina. Para Mac OS X, la instalación por defecto de Java será suficiente (lo cual es raro)

1
2
3
  cd ~/jrubydemo/
  wget http://dist.codehaus.org/jruby/jruby-bin-1.0.1.tar.gz
  tar zxvf jruby-bin-1.0.1.tar.gz

Y se creará el directorio jruby-1.0.1. A continuación, añadiremos las siguientes variables al entorno de ejecución actual, una forma de hacerlo es la siguiente

1
2
3
Tamarindo:~/jrubydemo/ juan$ cat > entorno
export JRUBY_HOME=`pwd`/jruby-1.0.1
export PATH=$JRUBY_HOME/bin:$PATH

Cada vez que cambiemos este fichero tendremos que acordarnos de ejecutar el nuevo entorno en todas las sesiones que tengamos abiertas, hasta que añadamos estas variables al entorno por defecto.


. entorno

Ya podemos comprobar que las variables de entorno están presentes. Posteriormente podremos añadir la carga de este entorno a nuestro profile, pero de momento nos bastará con recordar que hay que evaluar este archivo de entorno para poder trabajar con JRuby. Ya podemos hacer nuestra primera, emocionante prueba:

1
2
Tamarindo:~ juan$ jruby -v
ruby 1.8.5 (2007-08-23 rev 4201) [i386-jruby1.0.1]
1
2
Tamarindo:~ juan$ which gem
/Users/juan/jrubydemo/jruby-1.0.1/bin/gem

JRuby incluye su propia versión de gem, y todas las gemas que instalemos con el entorno de JRuby quedarán instaladas en $JRUBY_HOME/lib/gems, de manera que no romperemos nuestra instalación estándar de Rails y otra gemas.

Instalación de Rails sobre JRuby

Merced a la flexibilidad del sistema de paquetes de Ruby, este paso será muy sencillo. Para instalar a los sospechosos habituales bastará con:

1
2
gem install rails -y --no-rdoc
gem install activerecord-jdbc --no-rdoc --no-ri

Esto instalará las gemas habituales: ActionMailer, ActionPack, ActiveSupport, ActiveRecord, Rails, Rake, RSpec, etc. asi como el adaptador JDBC para ActiveRecord. En este punto es cuando cualquier tutorial de Rails que se precie creará una aplicación de prueba (por ejemplo, un blog, un CMS, un mashup o cualquier otra cosa) en menos de 5 minutos. Nosotros, que somos algo más vagos, optaremos por instalar una aplicación que ya tenemos lista en su repositorio de Subversion:

1
2
3
4
cd ~/jbossdemo
svn checkout  http://mi-repositorio.com/myapp/trunk myapp
cd myapp
jruby script/server

Y, si la base de datos está arrancada y ’’config/database.yml’’ está bien configurado, la aplicación funcionará sin más dilación. El mismo código de nuestra aplicación funcionará tanto sobre la implementación de Ruby en C que sobre la implementación en Java. No es poco.

Más difícil todavía: JBoss

Descargaremos JBoss de Sourceforge. El paquete zip es algo más pequeño:

1
2
3
4
http://labs.jboss.com/jbossas/downloads/   (4.2.1GA)
cd ~/jrubydemo/
wget http://downloads.sourceforge.net/jboss/jboss-4.2.1.GA.zip?modtime=1184616808&big_mirror=1
unzip jboss-4.2.1.GA.zip  (la versión zip es más pequeña)

Y por supuesto a nuestro fichero de entorno añadiremos:

1
2
3
4
cd ~/jbossdemo
export JRUBY_HOME=`pwd`/jruby-1.0.1
export JBOSS_HOME=`pwd`/jboss-4.2.1.GA
export PATH=$JRUBY_HOME/bin:$PATH

Configurando el acceso a bases de datos en JBoss

Este paso es lógicamente específico de JBoss, pero es necesario para correr nuestra aplicación. Dado que en el mundo Java no está aún muy extendido lo de la convención sobre configuración, tendremos que explayarnos un poco en el acceso a la base de datos, creando el fichero ’’jboss-4.2.1.GA/server/default/deploy/mysql-ds.xml’‘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<datasources>
  <local-tx-datasource>
    <jndi-name>MySqlDS</jndi-name>
    <connection-url>jdbc:mysql://mysql-hostname:3306/myapp</connection-url>
    <driver-class>com.mysql.jdbc.Driver</driver-class>
    <user-name>usuario</user-name>
    <password>clave</password>
    <exception-sorter-class-name>org.jboss.resource.adapter.jdbc.vendor.MySQLExceptionSorter</exception-sorter-class-name>

    <metadata>
       <type-mapping>mySQL</type-mapping>
    </metadata>
  </local-tx-datasource>
</datasources>

Con esta configuración le hemos dicho a JBoss que acceda a MySQL usando JDBC para acceder a la base de datos. Pero aún necesitamos instalar el correspondiente driver JDBC para Java, lo que MySQL denomina conector . De esta página podremos descargar el conector en forma de archivo .zip y la instalación es tan sencilla como copiar un archivo ’’.jar’’ al directorio lib del directorio de nuestro servidor.

1
2
3
4
cd ~/jbossdemo
wget http://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-java-5.1.3-rc.zip/from/http://mysql.rediris.es/
unzip mysql-connector-java-5.1.3-rc.zip
cp mysql-connector-java-5.1.3-rc/mysql-connector-java-5.1.3-rc-bin.jar  jboss-4.2.1.GA/server/default/lib/

Con todo esto, ya podremos probar si JBoss goza de buena salud.

cd $JBOSS_HOME
bin/run.sh

Tras varios mensajes esotéricos, JBoss cobra vida:

INFO  [Server] JBoss (MX MicroKernel) [4.2.1.GA (build: SVNTag=JBoss_4_2_1_GA date=200707131605)] Started in 16s:863ms

También nos interesa el mensaje

[ConnectionFactoryBindingService] Bound ConnectionManager 'jboss.jca:service=DataSourceBinding,name=MySqlDS' to JNDI name 'java:MySqlDS'

que indica que JBoss ha creado el gestor de conexiones MySQL. Es hora de mudar nuestra aplicación Rails.

Creando un archivo .war

La manera estándar de desplegar aplicaciones dentro de un contenedor Java es mediante el uso de archivos war” los cuales no son más que ficheros zip que contienen toda una jerarquía de directorios con clases y recursos estáticos (HTML, imágenes, etc) aderezados con ciertos metadatos. Cuando uno de estos ficheros aparece en el directorio adecuado del servidor, éste lo abre, analiza los metadatos y toma las acciones pertinentes para activar la aplicación.

Así, nos hace falta crear un fichero war que contenga nuestra aplicación Rails. Afortunadamente, no tenemos que preocuparnos mucho del asunto porque ya existe un plugin Rails que se encarga de ello, creado por Charles Nutter. Tenemos toda la información necesaria en este wiki

cd ~/jbossdemo/myapp
jruby script/plugin install svn://rubyforge.org/var/svn/jruby-extras/trunk/rails-integration/plugins/goldspike

Para configurar los contenidos de nuestro war no todo van a ser convenciones, tenemos que editar el archivo config/war.rb para referenciar el conector.

1
2
  
maven_library 'mysql', 'mysql-connector-java', '5.0.4'

Teniendo en cuenta que por defecto Goldspike ejecutará la aplicación Rails en modo producción, tendremos que configurar adecuadamente ’’config/database.yml’’ Por último:


rake war:standalone:create

Tras un rato de funcionamiento la tarea de Rake finalizará dejando un bonito myapp.war en nuestro directorio. Obsérvese que el fichero .war pesa la friolera de 18M: no en vano incluye todo JRuby, Rails y las gemas que tuviéramos instaladas. Si no hemos parado la anterior instancia de JBoss podemos copiar en caliente el war:

cp myapp.war /Users/juan/jbossdemo/jboss-4.2.1.GA/server/default/deploy/

Veremos que en JBoss comienzan a pasar cosas y nuestra aplicación cobra vida.

23:29:10,762 INFO  [TomcatDeployer] deploy, ctxPath=/myapp, warUrl=.../tmp/deploy/tmp22213myapp-exp.war/
23:29:11,322 INFO  [[/myapp]] Ruby is running in standalone mode
23:29:14,472 INFO  [[/myapp]] JRuby init time: 2082ms
23:29:26,946 INFO  [[/myapp]] Rails init time: 9818ms
23:29:26,947 INFO  [[/myapp]] Runtime 0 loaded
23:29:26,947 INFO  [[/myapp]] Requests can now be processed
23:29:35,874 INFO  [[/myapp]] Runtime 1 loaded

Básicamente, JBoss descomprime el war en un directorio temporal y lanza dos instancias de Rails, lo que sería equivalente a dos mongrels tradicionales (por supuesto todo esto en configurable, según se explca en el wiki de Headius)

Tras esto, y si no ha habido errores (es de suponer que no) ¡es el momento de probar nuestra aplicación! Sólo debemos tener en cuenta que la URL raíz es, por la configuración de JBoss por defecto, algo distinta: http://127.0.0.1:8080/myapp, por lo que las URLs absolutas de nuestra aplicación (si las hubiera, que no debería) dejarán de funcionar.

Hace unos meses, los chicos de b-simple publicaron el tutorial en PDF RESTful Rails Development con licencia CC. Desde hoy, está disponible también la versión en castellano traducida por mí.

Mingle es una aplicación escrita con Ruby on Rails. Hasta aquí, nada de extrardinario.

Lo interesante llega cuando nos cuentan algunas de sus características: será una aplicación de escritorio para entornos corporativos y que deberá correr sobre Windows, Linux y Mac.

Y más interesante aún es la plataforma sobre la que corre esta aplicación Rails: usa como base de datos Derby y Jetty como servidor web. Nada que ver con los habituales mongrels y MySQL.

A estas alturas ya no nos sorprende saber que Mingle también se distribuirá como un fichero .war que se puede desplegar en cuaqluier contenedor J2EE.

Todo esto es posible gracias a JRuby

Desde que trabajo con Ruby on Rails de manera intensiva cada día, una de las cosas que más me incordia es abandonar el cómodo entorno de script/console para lanzar la (al menos para mí) menos cómoda interfaz de mysql La razón por la que me veo obligado a hacer esto con cierta frecuencia es para encontrar los ids de los objetos ActiveRecord que me interesan para investigar o depurar alguna funcionalidad. Cuando la aplicación tiene cientos de registros en las tablas, viene muy bien poder hacer consultas por el nombre aproximado con la construcción LIKE de SQL. ActiveRecord nos permite escribir nuestro propio SQL pero como soy un vago me resulta bastante tedioso escribir la condición completa con la sintaxis que pide Rails, así que normalmente ando saliendo de la consola, escribiendo una sentencia SELECT ... LIKE en mysql y volviendo a la consola con el id apuntado.

Hasta hoy, que me he terminado de cansar y he escrito un plugin que permite hacer sentencias LIKE desde ActiveRecord de manera dinámica.

Finders dinámicos en ActiveRecord

Los finders dinámicos de ActiveRecord son una de las primeras perlas de Ruby con las que uno se encuentra cuando investiga las tripas de Rails. Supongo que serán conocidos, pero no está de más repasarlos un poco.

Supongamos que tenemos una clase ActiveRecord mapeada sobre una tabla MyTable con los atributos atributo1, atributo2 y atributo3, sabemos que podemos escribir:


Mytable.find_by_id(87)

Y ActiveRecord tratará de encontrar el registro identificado con el id 87. No parece nada del otro mundo, porque entre otras convenciones ActiveRecord asume que la clave primaria en nuestras tablas será una columna llamada id, por lo que podría ser que existiese un método find_by_id en ActiveRecord::Base

Pero resulta algo más sorprendente toparse con que sentencias como:


Mytable.find_by_atributo1('Valor')

también funcionan. Aquí comienza a saborearse el dinamismo de Ruby, y podemos asumir que ActiveRecord, de manera astuta, construye tantas funciones Mytable::find_by_... como atributos tengamos en la tabla, que lanzarán la correspondiente consulta SQL a la base de datos.

Pero.. ¡un momento!

1
2
Mytable.find_by_atributo1_and_atributo3('Valor', 58)
Mytable.find_by_atributo1_and_atributo2_and_atributo3('Valor',58,'Valor3')

¡También! funcionan. Y sería ridículo pensar que ActiveRecord construye funciones de manera dinámica para todas las combinaciones posibles de los atributos (y, además, en cualquier orden que queramos)

Nuestro propósito es añadir más finders dinámicos, que en lugar de búsquedas exactas hagan búsquedas aproximadas, invocándose de la siguiente manera:

Mytable.find_like_atributo1('al')

Para hacerlo, tenemos que entender bien qué hace ActiveRecord con estas misteriosas funciones dinámicas…

La magia de method_missing

A estas alturas ya nos imaginamos que no se añaden métodos para cada atributo, sino que hay algún otro mecanismo actuando en este caso. Se trata de method_missing que es el método que ejecuta una clase Ruby cuando se le invoca un método que no tiene implementado. Aquí podeis ver otro uso muy creativo de method_missing

ActiveRecord se aprovecha de method_missing para articular los finders dinámicos. Si abrís el fichero lib/active_record/base.rb vereis que la clase ActiveRecord::Base define un method_missing muy especial (si no estais viendo ese código, hacedlo ahora: leer código del core de Rails es siempre un ejercicio inspirador). Este método se activa cuando llamamos a Mytable.find_by_attributo('valor'), y lo primero que hace es comprobar con una expresión regular si el método invocado tenía la pinta find_by o find_all_by, en cuyo caso pasa a construir una sentencia SQL de búsqueda según los atributos y parámetros recibidos. Parece evidente que nosotros tendremos que puentear este método y hacer algo parecido por nuestra cuenta pero construyendo sentencias SQL con el modificador LIKE.

Cómo construir nuestro propio finder

Lo primero es preparar un plugin, lo cual es tan fácil como crear un directorio find_like en vendor/plugins, y ahí escribiremos un fichero init.rb con el siguiente contenido:


require 'find_like'

A continuación, creamos el directorio vendor/plugins/find_like/lib y ahí pondremos el código de nuestro plugin:

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
29
30
31
32
33
module ActiveRecord

class Base
  class << self
  
  private
      
  def construct_conditions_from_like_arguments(attribute_names, arguments)
    conditions = []
    attribute_names.each_with_index{ |name, idx| 
      conditions <<= "#{table_name}.#{connection.quote_column_name(name)} LIKE '%%" << arguments[idx] << "%%'"}
    [conditions.join(" AND "), *arguments[0..attribute_names.length]]
  end
      
  alias_method :previous_method_missing, :method_missing

  def method_missing(method_id, *arguments)
    if match = /find_(all_like|like)_([_a-zA-Z]\w*)/.match(method_id.to_s)
      finder = (match.captures.first == 'all_like' ? :find_every : :find_initial)
          
      attribute_names = extract_attribute_names_from_match(match)
      super unless all_attributes_exists?(attribute_names)
      conditions = construct_conditions_from_like_arguments (attribute_names, arguments)
      options = {:conditions => conditions}
      set_readonly_option!(options)
      send(finder, options)
    else
      previous_method_missing(method_id, *arguments)
    end
  end
end
end
end

El código más o menos es fácil de seguir: usamos class << self para abrir la clase ActiveRecord::Base y modificar sus propias tripas. Dado que estamos volviendo a escribir el método method_missing no querremos perder la funcionalidad ofrecida por el method_missing originalmente incluido con ActiveRecord::Base (el que veíamos antes en base.rb) así que simplemente haremos un alias a este método y lo llamaremos desde nuestro method_missing si detectamos que el método invocado no es uno de los que nosotros queremos controlar (find_like o find_all_like)

Tras esto, podemos lanzar una consola y comprobar que, por arte de birlibirloque, ya tenemos nuestros finders operativos:

1
2
3
4
5
6
7
8
9
Tamarindo:~/src/repos/dconrails juan$ script/console development
Loading development environment.

>> Algoritmo.find_all_like_nombre("oyal")

=> [#<Algoritmo:0x245c908 @attributes={"status"=>"BEGIN", "nombre"=>"RoyalRoadDemo"}>, #<Algoritmo:0x245c82c @attributes={"status"=>"RUN", "nombre"=>"RoyalRoad2">]

>> Algoritmo.find_all_like_nombre_and_status("oyal", "UN")
=> [#<Algoritmo:0x243dabc @attributes={"status"=>"RUN", "nombre"=>"RoyalRoad2"}>]

Son conocidos los screencasts de Peepcode, obra del magno Topfunky. Diría que se tratan de un recurso imprescindible: por un puñado de dólares (algo menos de euros) uno recibe una densa hora de conceptos y consejos de alta calidad.

Pues bien, hoy he descubierto Railscasts, donde Ryan Bates publica screencasts breves con trucos y consejos de Rails. Uno puede, además, suscribirse en iTunes, lo que lo hace todo aún más fácil.

Y, sí, Peepcode también tiene algo que ver con esto.

Cómo paginar, ordenar y hacer búsquedas en una tabla con Ajax y Rails

Introducción y observaciones

(Nota: este documento es una traducción del tutorial How to paginate, sort and search a table with Ajax and Rails, escrito por Julien Barnier, y publicado en su web http://nozav.org)

En este tutorial intentaremos utilizar Ajax con Rails para mostrar una tabla de elementos con diversas funcionalidades:

  1. paginación: separar la tabla en páginas distintas, si el contenido no puede mostrarse de una sola vez
  2. ordenación: ordenar la tabla por alguna de las columnas, en orden ascendente o descendente
  3. búsqueda: escoger qué elementos van a ser mostrados con una consulta

Esta es una tarea muy común en el desarrollo de aplicaciones web. El interés de usar Ajax para esto es proporcionar una interfaz dinámica que no hace necesario recargar toda la página cuando la tabla cambia. El interés de usar Rails es, bueno, si estás leyendo esto ya deberías saberlo, pero Ajax y Rails se llevan bastante bien, de forma que es muy fácil implementar funcionalidades Ajax con nuestro framework favorito. Además, el código de este tutorial debería funcionar también de la manera tradicional (recargando la página entera) si JAvascript no está disponible en la parte del cliente. Esto es muy importante para la accesibilidad.

El código que sigue a continuación esta muy inspirado en diferentes páginas del wiki de Rails, en especial “How to make a real-time search box with the Ajax helpers” y “How to paginate with Ajax”. Simplemente puse las cosas juntas y adapté el código ligeramente.

Ahora, las observaciones. No soy un gurú de Rails, de hecho estoy bien lejos de ser un experto en Ajax. Soy un principiante en ambas áreas, así que tómense este documento como una introducción para principiantes escrita por un principiante: el código podría ser más claro, las explicaciones más acertadas y algunas cosas podrían haberse diseñado de manera más eficaz y elegante. Además, como el inglés no es mi lengua nativa, es posible que se encuentren muchos errores e inexactitudes durante el texto. Les pido excusas por anticipado (N. del T: ¡esto va también por mí!)

En todo caso no duden en enviarme sus comentarios a la siguiente dirección:

julien (at) nozav (dot) (org)

Instalación y configuración de la aplicación

Lo primero que hemos de hacer es instalar y configurar la aplicación. Si ya lo has hecho, te puedes saltar lo que viene a continuación.

Requisitos

En este tutorial se supone que tienes una versión reciente e Rails instalado (al menos la versión 1.0) y un sistema de bases de datos funcional. Aquí usaremos SQLite, pero se puede reemplazar por cualquier otro sin el menor problema.

Archivos

Primero, tenemos que crear el esqueleto de la aplicación en un directorio de nuestra elección

$ rails ajaxtable
$ cd ajaxtable

Como se trata de una aplicación muy básica solamente utilizaremos un modelo que representará a los elementos de nuestra tabla, así como un controlador para estos elementos. Generamos todo el código correspondiente usando Rails:

$ ruby script/generate model Item
$ ruby script/generate controller Item

La base de datos

Una vez que tengamos el código, debemos configurar una fuente de datos. En aras de la simplicidad utilizaremos SQLite junto a un esquema Rails. Esto significa que tendremos que describir nuestra base de datos dentro de Rails y dejar que sea el framework quien gestione la creación de la base de datos.

Tenemos que modificar la fuente de datos para el entorno de desarrollo en config/database.yml:

1
2
3
development:
        adapter: sqlite3
        database: db/development.db

A continuación, crearemos la base de datos utilizando las herramientas de Rails

$ rake db_schema_dump

Esto debería crear un archivo llamado db/schema.rb en el que definiremos nuestro esquema de base de datos de la siguiente manera:

1
2
3
4
5
6
7
8
ActiveRecord::Schema.define() do

        create_table "items" do |t|
                t.column "name", :string, :limit => 30
                t.column "quantity", :integer, :null => false, :default => 0
                t.column "price", :integer, :null => false, :default => 0
        end
end

Esto crea una tabla llamada items con tres columnas: un campo de cadena llamado nombre, y dos columnas numéricas llamadas cantidad y precio (no es nada demasiado original, ciertamente)

Luego tan sólo hemos de hacer

$ rake db_schema_import

Y ya deberíamos tener un fichero development.db en nuestro directorio db que es la base de datos creada por Rails con la tabla items en su interior.

Después podemos introducir algunos ítems en la tabla para tener algo que mirar mientras desarrollamos el código. Se puede hacer manualmente o escribiendo las siguientes instrucciones SQL en un fichero db/dump.sql:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BEGIN TRANSACTION;
INSERT INTO "items" VALUES(1, 'caja', 3, 10);
INSERT INTO "items" VALUES(2, 'carretilla', 2, 60);
INSERT INTO "items" VALUES(3, 'tenazas', 15, 3);
INSERT INTO "items" VALUES(4, 'batman', 1, 3000);
INSERT INTO "items" VALUES(5, 'morcilla de pescado', 2, 8);
INSERT INTO "items" VALUES(6, 'sauerkraut', 9, 9);
INSERT INTO "items" VALUES(7, 'regadera', 4, 13);
INSERT INTO "items" VALUES(8, 'dandiliones', 78, 1);
INSERT INTO "items" VALUES(9, 'nevera', 12, 250);
INSERT INTO "items" VALUES(10, 'cerillas voladoras', 8, 145);
INSERT INTO "items" VALUES(11, 'acordeón roto', 1, 18);
INSERT INTO "items" VALUES(12, 'silbido salvaje', 5, 7);
INSERT INTO "items" VALUES(13, 'caracol histérico', 8, 13);
COMMIT;

Podremos insertar todas estas tablas en la base de datos ejecutando

$ sqlite3 db/development.db < db/dump.sql

(Nota, si se utiliza una base de datos MySQL, la sintaxis adecuada es @INSERT INTO items VALUES (1, ‘hoe’, 3, 10);@, etc)

Creación del modelo

Como seguramente sabrán, una aplicación Rails se divide en tres tipos de componentes: modelos, vistas y controladores. Los iremos creando uno por uno.

El modelo de nuestra aplicación será muy sencillo. De hecho, como no tenemos ninguna consulta compleja que hacer a la base de datos, será exactamente el mismo que ha generado Rails para nostros en nuestra instalación de la aplicación. Vamos, que irá vacío. Así que no tocaremos el fichero app/models/item.rb

¡Espero que este paso no haya sido demasiado difícil!

Creación de la vista

La vista de nuestra aplicación estará dividida en dos componentes: un layout, una vista y un parcial.

Layout

El layout es una plantilla de página que se utilizará para interpretar diferentes vistas. En él se pondrán los elementos que no varían, tales como los encabezamientos y pies de página en HTML, menús, elementos de diseño, etc. La utilidad de un layout en nuestro ejemplo es muy limitada, porque sólo tendremos una página. Pero a fin de cuentas esto no es más que un tutorial…

En nuestro caso guardaremos el layout en app/views/layouts/item.rhtml, que contendrá algo parecido a

1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<head>
  <title>Ajax table manipulation attempt</title>
  <%= stylesheet_link_tag "style" %>
  <%= javascript_include_tag :defaults %>
</head>
<body>

<div id="content">
<%= @content_for_layout %>
</div>

</body> </html>

Es muy importante no olvidar la sentencia javascript_include_tag, que será reemplazada por las librerías que utiliza Rails para proporcionar toda la funcionalidad Ajax: sin esto no funcionará. También es de señalar la instrucción content_for_layout, que será reemplazada por el contenido generado convenientemente por…

La vista

El componente de la vista se utiliza para mostrar acciones particulares de nuestros controladores. Como es una lista de nuestro controlador de items, lógicamente se hallará en app/views/item/list.rhtml

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
29
30
31
<h1>Welcome to my wonderful items list</h1>

<p>This list is updated real-time from the world largest database of
items by using bleeding-edge technology associated to full-featured
Web 2.0 eye candy goodies.</p>

<p>But be careful, there are plenty of bugs.</p>

<h2>And here is the list...</h2>

<p>
<form name="sform" action="" style="display:inline;">
<label for="item_name">Filter on item name  : </label>
<%= text_field_tag("query", @params['query'], :size => 10 ) %>
</form>

<%= image_tag("spinner.gif",
              :align => 'absmiddle',
              :border=> 0,
              :id => "spinner",
              :style=>"display: none;" ) %>
</p>

<%= observe_field 'query',  :frequency => 2,
         :update => 'table',
         :before => "Element.show('spinner')",
         :success => "Element.hide('spinner')",
         :url => {:action => 'list'},
         :with => 'query' %>

<div id="table"> <%= render :partial => "items_list" %> </div> 

En principo no hay nada muy complicado. Un texto estúpido de presentación y un formulario de búsqueda que utilizaremos para filtrar los items por su nombre.

Después, tendremos una imagen cuyo id es spinner y que está oculto por defecto. Esta imagen se mostrará brevemente durante las operaciones Ajax en la página, y luego se ocultará otra vez cuando todo haya acabado. Se pueden conseguir algunas imágenes de domino público de este estilo en http://mentalized.net/activity-indicators/

Hay que poner la imagen escogdia en el directorio public/images de nuestra aplicación.

La instrucción observe_field es algo más especial. Lo que hace esta instrucción es añadir un nuevo observer Ajax al campo de consulta. Este observador periódicamente comprobará (aquí es cada dos segundos, pero hay un parámero que permite establecer la frecuencia) los contenidos de este campo y se activará si cambian sus contenidos. La acción a tomar viene descrita por los parámetros,

  • update determina el id del elemento de la página que será actualizado. En este caso es la tabla que rodea a nuestra llamada al parcial (la tabla, por supuesto, tendrá el id ‘table’).
  • url determina la acción que mostrará el nuevo contenido HTML a insertar. Aquí cada petición será tratada por la acción list de nuestro controlador
  • with indica la manera en al que el campo del contenido será pasadao a la URL de la acción. Aquí añadiremos un parámetro query a nuestra petición Ajax con un valor igual a lo que se haya introducido en el campo observado.
  • before y success indican, respectivamente, una acción a realizar durante el tiempo que se trata la acción Ajax, y una vez que haya sido completada satisfactoriamente.

El efecto concreto de todo esto es muy sencillo. Cuando el usuario escribe algo en el campo de consulta, el observador detectará los nuevos contenidos del campo y a continuación generará una petición Ajax al servidor con la URL y los parámetros especificados. En cuanto que se envíe la petición, la acción :before cambia la visibilidad del elmento spinner, y el usuario puede ver la imagen animada. Cuando se recibe la respuesta, el elemento de la tabla XHTML se actualiza y la acción :success oculta la imagen otra vez.

Por curiosidad, aquí está el código que devuelve la función observe_field con los parámetros mostrados:

1
2
3
4
5
6
7
8
9
<script type="text/javascript">
//<![CDATA[
new Form.Element.Observer('query', 2, function(element, value) {Element.show('spinner'); 
new Ajax.Updater('table', '/item/list', {
        asynchronous:true, 
        evalScripts:true, 
        onSuccess:function(request){Element.hide('spinner')}, parameters:'query=' + value})})
//]]>
</script>

Hemos detallado las diferentes opciones de los métodos Ajax porque volveremos a verlas en casi todos los métodos Ajax que veamos en este tutorial.

Para concluir con esta descripción de la vista, tenemos una llamada a un parcial llamado items_list. Describiremos este concepto en detalle después del controlador.

Creando el controlador

El controlador gestionará diferentes tipos de peticiones para actualizar la vista, llamando al modelo según convenga dependiendo de la petición y sus parámetros. Nuestro controlador Item será muy sencillo, y tan sólo incluirá una acción llamada list. No implementaremos ninguna otra acción CRUD (Create, Read, Update, Delete) en el tutorial.

Aquí están los contenidos de app/controllers/item_controller.rb

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
29
class ItemController < ApplicationController

  def list

    items_per_page = 10

    sort = case @params['sort']
           when "name"  then "name"
           when "qty"   then "quantity"
           when "price" then "price"
           when "name_reverse"  then "name DESC"
           when "qty_reverse"   then "quantity DESC"
           when "price_reverse" then "price DESC"
           end

    conditions = ["name LIKE ?", "%#{@params[:query]}%"] unless @params[:query].nil?

    @total = Item.count(:conditions => conditions)
    @items_pages, @items = paginate :items, :order => sort, 
             :conditions => conditions, 
             :per_page => items_per_page

    if request.xml_http_request?
      render :partial => "items_list", :layout => false
    end

  end

end 

Nuestro controlador sólo define una acción, llamada list, que gestionará todas las peticiones. Primero, definimos una variable llamada items_per_page que representará el número de líneas que nuestra tabla mostrará en cada página.

A continuación, definiremos otra variable llamada sort, que depende del parámetro de la petición que tiene el mismo nombre. Con la sentencia case construimos la sentencia a enviar a la base de datos para ordenar los elementos de la tabla. La cadena reverse es el parámetro que indica que la ordenación debe hacerse por orden inverso (en el helper veremos cómo se envía este parámetro al controlador).

Luego se construye una variable conditions si viene un parámetro de consulta, armando una sentencia SQL que filtrará los resultados.

Tras esto, asignamos el número total de elementos en nuestra base de datos que se corresponden con las condiciones en la variable total

Y, por fin, encontramos una llamada a la función paginate de Rails. Le damos a esta instrucción el modelo al que deberia asociarse (:items), un campo de ordenación, las condiciones para aplicar a la consulta y el número delementos que queremos en cada página. Y automágicamente devuelve un objeto paginador llamado items_pages y los items para la página actual (este número de pagina se pasa como un parámetro en la petición de página, que veremos más adelante) El objeto paginador se usará después para mostrar enlaces de paginación.

Todo lo que hemos visto para el controlador hasta ahora era aplicable a cada petición que se pasa a la aplicación, sea cual sea su tipo. Los tipos de peticiones HTTP más frecuentes son los tradicionales GET y POST, pero Ajax y Rails pueden hacer uso de un tercer tipo, cuyo nombre es *XmlHttpRequest* (en realidad, la peticón XmlHttpRequest no es otro tipo de petición HTTP, es tan sólo una petición GET o POST tradicional que es enviada y tratada por el intérprete Javascript de manera asíncrona)

Como hemos dicho, este tipo de petición es invocada desde Javascript, que la enviará en segundo plano por HTTP hacia el sevidor. Un uso posible (y que es el que usaremos) de este tipo de petición es el de obtener un fragmento de una página XHTML para actualizar parte del contenido que muestra el navegador sin volver a cargar la página entera, lo cual proporciona una experiencia de usuario más rápida y agil.

De esto es de lo que va la parte final de nuestro controlador: comprueba si la petición que tiene es de tipo xml_http_request (también podría haberse escrito abreviadamente xhr). En este caso no mostrará la lista completa sino tan sólo un fragmento de ella el parcial cuyo nombre es items_list

Así que, como puede verse, esta comprobación xml_http_request es el único código “realmente Ajax” con que nos topamos en el controlador. Esto es así porque en Rails los asuntos de Ajax están enlazados al interfaz de nuestra aplicación (es decir, a la vista) y más específicamente a la parte de ella que será gestionada por Ajax (es decir, el parcial).

Así que, ¿adivinan qué es lo que vamos a hacer ahora?

Creación del parcial

Un componente parcial de la vista se utiliza para mostrar una parte de la página. Es muy útil separar diferentes partes de la vista para poder reutilizarlas y seguir el principio Rails de _No te Repitas_ (DRY, Don’t Repeat Yourself) Pero además también es muy útil con Ajax.

De hecho el efecto de nuestras acciones Ajax en este tutorial siempre consistirá en volver a dibujar la tabla de elementos, ya cambie la página, el filtro por nombre o el orden de búsqueda. Así que tendremos que refrescar la tabla pero no toda la página: por eso separaremos todos los elementos que tendrán que ser actualizados en un parcial. (Esto podríamos haberlo hecho de alguna otra manera. Por ejemplo, creando otra acción de nombre ajax_list que gestionaría las peticiones xml_http_request, con una vista asociada pero sin ningún parcial. El problema que tiene este enfoque es que habríamos duplicado buena parte del código en las acciones list y ajax_list, así que.. ¡DRY!)

Los nombres de fichero de los parciales siempre empiezan con un subrayado. Así que nuestro parcial será app/views/item/_items_list.rhtml

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<% if @total == 0 %>

<p>No items found...</p>

<% else %>

<p>Number of items found : <b><%= @total %></b></p>

<p>
<% if @items_pages.page_count > 1 %>
Page&nbsp;:
<%= pagination_links_remote @items_pages %>
<% end %>
</p>


<table>
  <thead>
    <tr>
      <td <%= sort_td_class_helper "name" %>>
        <%= sort_link_helper "Name", "name" %>
      </td>
      <td <%= sort_td_class_helper "qty" %>>
        <%= sort_link_helper "Quantity", "qty" %>
      </td>
      <td <%= sort_td_class_helper "price" %>>
        <%= sort_link_helper "Price", "price" %>
      </td>
    </tr>
  </thead>
  <tbody>
    <% @items.each do |i| %>
    <tr class="<%= cycle("even","odd") %>">
      <td><%= i.name %></td>
      <td><%= i.quantity %></td>
      <td><%= i.price %></td>
    </tr>
    <% end %>
  </tbody>
</table>

<% end %>

El parcial contiene la paginación de la tabla y la gestión de la ordenación. Vamos a verlo con más detalle.

Helpers de paginación

Al principio tenemos una comprobación para ver si el número total de elementos que se ha encontrado es mayor que cero. En este caso, mostramos este número y después un párrafo que estará vacío si sólo hay una página en nuestro objeto de paginación.

Si tenemos más de una página de resultados está claro que tendremos que mostrar los enlaces de paginación. Rails por supuesto ya tiene métodos de ayuda pero tendremos que personalizarlos un poco. Para hacer esto, crearemos un helper. Un helper es una función Ruby que se utiliza para ayudar a generar la vista. La idea es separar el código en estas funciones de la propia vista, favoreciendo la reusabilidad.

Nuestros helpers estarán todos en app/helpers/item_helper.rb. Cada uno de los métodos de este archivo estarán disponibles para usarse en el código de nuestra vista. Opcionalmente, y si quisiéramos que estuviera disponible en todas las vistas de la aplicación, lo añadiríamos a application_helper.rb

Bien, ya basta de charla, he aquí el código de nuestro helper,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def pagination_links_remote(paginator)
  page_options = {:window_size => 1}
  pagination_links_each(paginator, page_options) do |n|
    options = {
      :url => {:action => 'list', :params => @params.merge({:page => n})},
      :update => 'table',
      :before => "Element.show('spinner')",
      :success => "Element.hide('spinner')"
    }
    html_options = {:href => url_for(:action => 'list', 
                    :params => @params.merge({:page => n}))}
    link_to_remote(n.to_s, options, html_options)
  end
end

A este método le pasaremos un objeto paginador como argumento. El paginador, como vimos en el controlador, es un objeto de Rails que guarda información sobre el estado de paginación (número de páginas, la página que estamos viendo, etc.)

Después definiremos un hash llamado page_options que contiene tan sólo un item llamado window_size Este será un parámetro que le dice a Rails cuántas páginas tiene que mostrar alrededor de la actual en la línea a los enlaces de paginación. Por ejemplo, si window_size vale uno, tendremos algo como

1 ... 5 6 7 ... 13

Mientras que si window_size es 2:

1 … 4 5 6 7 8 … 13

Podríamos entonces hacer una llamada a la función pagination_links, que generaría el código XHTML necesario para dibujar nuestros enlaces. El problema es que esto generaría enlaces XHTML “clásicos”, que no realizarían llamadas Ajax. Así que tendremos que generar por nuestra cuenta el código de los enlaces. Esto lo haremos con el método pagination_links_each Este método de Rails se recorre las páginas a mostrar y después aplica el bloque que se le pasa como argumento.

Nuestro bloque en primer lugar define dos tipos de hashes de opciones

  • options se define para la generación del enlace Ajax. Son muy similares a las que se definieron en el elemento observe_field. La única novedad es la invocación de params.merge, que añadirá los parámetros de la petición al enlace reemplazando cualquier parámetro que ya hubiera en la página por el número del bloque .
  • html_options simplemente se definen para generar el enlace “clásico” XHTML, para que funcione la paginación si Javascript no está disponible o el navegador no lo soporta.

Después hay una llamada a la función link_to_remote que generará el código XHTML para nuestro enlace, incluyendo el Javascript que hará la invocación Ajax y la parte “clásica” con su href.

Por ejemplo, esto es lo que devuelve el helper si hay dos páginas, y estamos ya mostrando la primera:

1
2
3
4
5
6
7
8
1 <a href="/item/list?page=2" 
     onclick="Element.show('spinner'); 
              new Ajax.Updater('table', '/item/list?page=2', {
                                asynchronous:true, 
                                evalScripts:true, 
                                onSuccess:function(request){Element.hide('spinner')}
                           }); 
                          return false;">2</a>

Helpers de ordenación

Volvamos a nuestro parcial. Después de los enlaces de paginación empezamos a definir la tabla propiamente dicha. La definición de la cabecera es algo complicada porque ahí es donde vamos a definir los enlaces que ordenarán los datos por una u otra columna. Cada celda del encabezamiento de la tabla hace uso de dos helpers.

El primer helper no es del todo necesario. Se llama sort_td_class_helper, y su único objetivo es el de añadir una clase “sortup” si la columna que va a dibujar es la que se está usando para ordenar la tabla, o la clase “sortdown” si se utiliza para ordenar en orden inverso. La única utilidad de esta función es que, usando CSS, podremos indicar al usuario que función se está empleando como campo de ordenación

El código no es precisamente interesante:

1
2
3
4
5
def sort_td_class_helper(param)
  result = 'class="sortup"' if @params[:sort] == param
  result = 'class="sortdown"' if @params[:sort] == param + "_reverse"
  return result
end

Luego tenemos el segundo helper, llamado sort_link_helper.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def sort_link_helper(text, param)
  key = param
  key += "_reverse" if @params[:sort] == param
  options = {
      :url => {:action => 'list', 
               :params => @params.merge({:sort => key, :page => nil})},
      :update => 'table',
      :before => "Element.show('spinner')",
      :success => "Element.hide('spinner')"
  }
  html_options = {
    :title => "Sort by this field",
    :href => url_for(
        :action => 'list', 
        :params => @params.merge({:sort => key, :page => nil}))
  }
  link_to_remote(text, options, html_options)
end

Este helper, de hecho, es muy similar al que hemos visto más arriba, pagination_links_remote, y recoge dos argumentos:

  • text es tan sólo el texto a mostrar como cabecera de la columna y el enlace para ordenar * param es el nombre del parámetro de la petición asociado con la columna que luego el controlador usará para generar la consulta SQL

Las primeras dos líneas definen una nueva variable, llamada key, que recibe el valor del argumento param, es decir, la clave de búsqueda. Se le añadirá el sufijo _reverse si el parámetro ya está siendo para ordenar. Esto se utiliza para implementar la búsqueda tanto con criterio ascendente como descendente: si el usuario pulsa un enlace de ordenación, se ordenará por orden ascendente y si escoge otra vez el mismo enlace ordenará por criterio descendiente y así sucesivamente. Viendo el código del controlador probablemente todo esto quedará más claro.

El resto del helper define las opciones para la llamada final a la función link_to_remote, que son muy similares a los de pagination_links_remote

  • options como ya sabemos se utiliza para el enlace Ajax y contiene la URL a invocar para generar la nueva tabla, el id del elemento a actualizar y las acciones before y success para mostrar y ocultar el spinner.
  • html_options se utiliza para el enlace HTML. Genera el contenido del atributo href con la función url_for de Rails.

Esto es lo que devuelve sort_link_helper para una llamada con las cadenas Quantity y qty como texto y parámetro, respectivamente:

1
2
3
4
5
6
7
8
<a href="/item/list?sort=qty" 
        onclick="Element.show('spinner'); 
            new Ajax.Updater('table', '/item/list?sort=qty', {
                                asynchronous:true, 
                                evalScripts:true, 
                                onSuccess:function(request){Element.hide('spinner')}});
                         return false;" 
        title="Sort by this field">Quantity</a>

Cuerpo de la tabla

Por último, el cuerpo de la tabla simplemente muestra el contenido de la misma: un elemento en cada fila. Lo único relevante aquí es el uso de la función cycle de Rails, que automática y alternativamente añadirá la clase odd o even al estilo de nuestras filas de la tabla, lo que puede resultar útil a la hora de mostrarlas de forma más vistosa.

¡Y eso es todo amigos!

Ya hemos visto todos los trozos de nuestra aplicación con más menos detalle. En teoría, uno podría ver los resultados arrancando del servidor WEBrick y entrando a la dirección

http://localhost:3000/item/list

Espero que este documento les haya sido útil. Repito que pueden enviarme cualquier comentario, etc. a la direccón de correo dada en la introducción.

Acerca de este documento.

Este documento se publica bajo licencia Creative Commons Attribution.

Agradecimientos a Nicolas St-Laurent y Rachel McConnel por sus observaciones.

Nota: Este artículo es una traducción (con permiso) de You never know when the brakes might fail, escrito por Damien Tanner y publicado en New Bamboo

Por si fallan los frenos

Con Rails podemos ir a toda máquina a la hora de desarrollar nuestras aplicaciones, pero con esta velocidad sufrimos un mayor riesgo de descarrilar (no puedo resisitrme a hacer comparaciones ferroviarias). Afortunadamente para nosotros Rails (con todo el cariño de Ruby) tiene un arsenal de pruebas soberbio. Muchos de los que conozco aún no han adoptado la mentalidad del desarrollo orientado a pruebas, lo cual es comprensible porque es una manera totalmente nueva de pensar que lleva cierto tiempo para aprenderla y adaptarse a ella. A principio puede parecer complicado apreciar los méritos de la automatización de pruebas, porque puede significar que tardas el doble de tiempo en construir tu aplicación. Pero algún dia una de tus pruebas fallará, investigarás y descubrirás la causa del fallo y te imaginarás qué habría pasado si ese fallo no hubiera sido descubierto a tiempo. A partir de entonces te darás cuentas, de forma definitiva, de la potencia de las pruebas automáticas.

El uso de pruebas por codigo también te da libertad. Por ejemplo, en un eciente proyecto nuestro hicimos hace un par de semanas ciertos cambios estructurales importantes en la base de datos, y con unos 50 modelos hubiera sido un infierno hacer todo lo que hacía falta sin emplear pruebas unitarias. Una batería completa de tests nos permitió hacer los cambios en la base de datos y ver, inmediatamente, qué modelos de la aplicación necesitaban ser modificados y si las modficiaciones que habíamos hecho eran correctas. En última instancia, las baterias de pruebas me permiten dormir por las noches.

Sorprendentemente, todavía hay pocos tutoriales para ayudarnos a la hora de empezar a escribir nuestras pruebas. Sin embargo, las cosas están cambiando deprisa, por suerte. Esperamos que este artículo te ayuda a familiarizarte con los diferentes tipos de pruebas que se usan con más frecuencia.

Pruebas unitarias

Probar los modelos es vital para que la aplicación funcione. Una batería completa de pruebas untiarias tambien nos da la lbiertad de cambiar los esquemas de la base de datos y la lógica de negocio sin miedo a que se nos cuelen los problemas inadvertidamente. Una vez que le pilles el truco, te preguntarás cómo fué que antes podías vivir sin escribir pruebas unitarias.

Hay diferentes patrones comunes que utilizo a la hora de escribir pruebas unitarias.

Prueba de relación

Es facil cometer errores en las relaciones, especialmente si el esquema de la base de datos evoluciona durante el desarrollo. Cuando uno se encuentra inmerso en un proyecto de envergadura se necesita una bateria completa de pruebas para dormir bien por las noches y por suerte son los mas fáciles de escribir.

Supongamos que en nuestros modelos un Autor tiene muchos Articulos. Esto podría probarse de esta manera:

1
2
3
def test_autor
  assert_equal articulos(:primer_articulo).autor, autores(:damien)
end

Necesitariamos cubrir con pruebas todas las relaciones belongs_to,has_one, has_many y has_and_belongs_to_many en nuestro modelo.

Prueba de creación

En una batería completa de pruebas, nos hará falta tener pruebas de creación, lectura, actualización y borrado. La creación es el sitio donde es mas probable que las cosas vayan mal.

Para comprobar si un registro ha sido creado correctamente, antes nos hacia falta contar el número actual de registros, almacenarlo, y luego volver a contar y comparar después de la invocación del método create. Sin embargo, ahora tenemos un método llamado assert_difference y su contrapartida assert_no_diference. Cuando vi estos metodos por primera vez utilizados en una prueba no me pareció que tuvieran mucho sentido (supongo que era porque por defecto, estos metodos comprueban una diferencia de 1 sin que haga falta que se lo especifiques explícitamente), sin embargo una lectura rápida del código me ayudó a entender el funcionamiento:

1
2
3
4
5
6
7
8
9
10
def assert_difference(object, method = nil, difference = 1)
  initial_value = object.send(method)
  yield
  assert_equal initial_value + difference,
    object.send(method)
end

def assert_no_difference(object, method, &block)
  assert_difference object, method, 0, &block
end

Así, ahora podemos escribir pruebas de creación de objetos de esta guisa:

1
2
3
4
5
def test_create
  assert_difference Articulo, :count do
    autores(:damien).articulos.create(:titulo => 'Segundo articulo', :cuerpo => '...')
  end
end

Lo cual es lo mismo que hacer:

1
2
3
4
5
def test_create
  cuenta_inicial = Articulo.count
  autores(:damien).articles.create(:titulo => 'Segundo articulo', :cuerpo => '...')
  assert_equal cuenta_inicial + 1, Articulo.count
end

Prueba de validación

Después de que ya tengamos nuestras pruebas de creación, querremos extender las validaciones en el modelo. En lugar de montar todos los parámetros a create() en cada prueba, nos será útil añadir un metodo para crear el registro y permitir sobreescribir los parámetros:

1
2
3
4
5
6
7
protected

def create_articulo(opciones = {})
  attrs = { :titulo => 'Mi segundo articulo', :contenido => '...', 
            :contenido_extendido=> 'mas texto' }.merge(opciones)
  Articulo.create(attrs)
end

Así, para validar la presencia o no de atributos de obligada presencia, podriamos hacer:

1
2
3
4
5
6
def test_validates_presence_of_titulo
  assert_no_difference Articulo, :count do 
    nuevo_articulo = create_article({ :titulo => nil })
    assert new_articulo.errors.on(:titulo)
  end
end

Obsérvese que ademas de comprobar que no hay diferencia en el número de articulos en la base de datos (lo cual nos dice que el articulo mal creado no ha sido almacenado en la base de datos), también nos hemos asegurado que se ha asignado un error al atributo titulo. Tambien se podría hacer esta comprobación fuera del bloque assert_difference, pero creo que queda mejor dentro.

Pruebas de acceso

Es posible saltarse las pruebas de lectura de registors, pero si hemos definidos métodos personalizados para leer atributos es posible que queramos añadir pruebas para ellos.

Por ejemplo, en nuestro modelo Articulo hemos añadido un método resumen que devuelve una copia truncada de los contenidos del articulo. Para probarlo, haríamos así:

1
2
3
def test_excerpt
  assert_equal articulos(:mi_primer_post).resumen.length, 100
end

Este es un ejemplo muy simple, pero una vez que empecemos a tratar con la lógica de negocio (y más aun con el dinero de la gente) estaremos obligados a probar los métodos de un modelo.

Prueba de borrado

He aquí mi método favorito para probar la destruccion de registros:

1
2
3
4
5
def test_delete
  assert_difference Articulo, :count, -1 do
    Articulo.destroy(articulos(:mi_primer_post))
  end
end

Pruebas funcionales

Como desarrollador, todos sabemos lo importantes que son nuestros modelos, pero poca gente realmente llega a verlos. A lo que realmente prestan atencino nuestors usuarios es a los controladores y las vistas. Si no estamos escribiendo pruebas funcionales probablement nuestros clientes o usuarios están acostumbrados a ver de vez en cuando los errores de tipo ApplicationError. Sin embargo hay esperanza. Si escribimos una bateria completa de pruebas funcionales, estos errores de aplicación serán cosa del pasado.

Prueba de éxito

La prueba mas básica que nos ahorrara sufrir errores de aplicación será la prueba de éxito. Lo que hacemos es requerir una acción del controlador y asegurarnos que la respuesta HTTP es OK. Y ya que estamos podemos asegurarnos que de que se muestra la vista correcta y que están bien construidas las variables que hacen falta en la vista:

1
2
3
4
5
6
def test_show
  get :show, :id => 1
  assert_response :success
  assert_template 'show'
  assert assigns(:articulo)
end

Con esta prueba nos aseguramos que visitar /articulos/show/1 mostrará la página utilizando la plantilla articulos/show.rhtml y que la variable @@articulo tendrá un valor que podrá ser usado por la vista.

Prueba de redirección

Las acciones que se invoquen en el área de usuarios o de administración de nuestra aplicación deberían redirigirnos a otra página si no estamos registrados. Para comprobar que lo hacemos bien podremos emplear assert_redirected_to

1
2
3
4
5
def test_destroy_requires_admin_priv
  get :destroy, :id => 1
  assert_equal 'Solo administradores', flash[:notice]
  assert_redirected_to '/admin/login'
end

En este caso tambien nos hemos asegurado de que mostramos el mensaje adecuado.

Las pruebas CRUD

Igual que con las pruebas unitarias de los modelos, querremos comprobar las acciones de creación, acceso, actualización y borrado (CRUD, de Create, Read, Update, Delete). De nuevo utilizaremos assert_difference:

1
2
3
4
5
6
def test_destroy
  assert_difference Articulo, :count do
    login_as_admin
    get :destroy, :id => 1
  end
end

Tambien querremos comprobar el envio de formularios como cuando se crean articulos nuevos en un blog:

1
2
3
4
5
6
def test_create
  assert_difference Articulo, :count do
    login_as_admin
    post :new, { :titulo => 'Post de prueba', :cuerpo => 'Texto de artículo' }
  end
end

Pruebas de etiquetas

Si pudiésemos probar las etiquetas de marcado HTML que mostraremos al usuario, estaremos probando exactamente lo que el usuario verá. Siempre se pueden probar los títulos importantes y los enlaces entre páginas.

Uno de los metodos más útiles en las pruebas funcionales es assert_tag, que buscará una etiqueta específica en el marcado HTML devuelto por la aplicación. Han aparecido recientmente dos alternativas a este metodo: Hpricot y sus métodos de pruebas (también hay disponible un plugin) y assert_select que también permite usar selectores CSS para buscar etiquetas concretas. No cabe duda de que estos dos plugins prometen mucho, peor por ahora considero que assert_tag es adecuado en la mayoría de las ocasiones.

En este ejemplo, nos aseguraremos que la página de listado de todos los articulos tiene los títulos correctos con enlaces al pagína de mostrar cada artículo:

1
2
3
4
5
6
7
8
def test_titulos_listado
  get :list
  articulos.each do |articulo|
    assert_tag :tag => 'a',
               :attributes => { :href => "/articles/show/#{articulo.id}" }, 
               :child => { :tag => 'h2', :content => articulo.titulo }
  end
end

Una lectura rápida de la documentación de assert_tag nos ayudará a entender mejor su uso.

Aún no las tengo todas conmigo

Uno de los libros mas útiles que tengo es Practices of an Agile Developer. Si alguna vez tengo mis dudas o vacilo en algún proyecto particular, abro el libro y leo algunos apartados. Todas las practicas son concisas y le levantarán el ánimo incluso al desarrollador mas ignorante. Algunas de las prácticas hacen referencias al desarrollo guiado por pruebas y las herramientas asociadas.

por Curt Hibbs (traducción de J. Lupión)

Enlace al artículo original en OnLamp (en inglés)

Bienvenidos de nuevo.

En la primera parte del tutorial apenas empezamos a vislumbrar todas las cosas que pueden hacerse con Ruby on Rails. Por ejemplo, no hablamos sobre la validación de datos, las transacciones en la base de datos, las callbacks, el subsistema de pruebas unitarias o la caché. No hicimos mención alguna de los helpers que ofrece Rails para hacernos la vida más fácil. Aunque en realidad es imposible hacer justicia a ninguno de estos temas en el espacio limitado de este artículo sí que trataremos con cierto detalle alguno de ellos y del resto presentaremos un breve resumen, con enlaces a páginas con información más detallada.

Tampoco entramos con profundidad en el lenguaje de programación Ruby (a propósito). Si te interesa leer un breve tratado sobre los porqués que hay detrás de Ruby y del código Rails que vimos en la primera parte, te recomiendo que visites el blog de Amy Hoy, concretamente la historia Really Getting Started in Rails.

Pero antes de pasar a cubrir todo este material, quiero completar los ejercicios de tareas que dejamos pendientes al final de la primera parte:

  • Ya no hay forma de eliminar una receta. Añádele un botón -o enlace- de borrado a la plantilla de edición.
  • En la página principal de las recetas no hay ningún enlace a las páginas que permiten editar categorías. Arréglalo.
  • Estaría bien poder visualizar de alguna forma sólo las recetas que pertenecen a una categoría particular. Por ejemplo, tal vez me gustaría ver una lista de todas los snacks o una lista de todas las bebidas. En la página que lista las recetas, haz que el nombre de la categoría sea un enlace a una página que visualice todas las recetas de esa categoría.

Un astuto lector nos ha señalado que después de añadir categorías a nuestra aplicación ya no era posible crear nuevas recetas porque la acción new del controlador Receta, tal y como la proporciona el andamiaje no tiene manera de asignar una categoría, y esto hace que la acción list en la lista de recetas provoque un error. Por supuesto esto hay que arreglarlo.

¿Estás listo? ¡Empezamos!

Actualización de Ruby on Rails

[N. del T: aunque las versiones de Rails y de los componentes que has instalado si has seguido los pasos de este artículo son superiores a las que describe Curt y por tanto no es necesario actualizarse (o al menos no tan perentorio), el procedimiento que él describe para actualizar nuestro paquete Rails sí que sigue siendo válido, asi que también lo incluiré en la versión traducida.]

Cuando escribí la primera parte, la última versión de Rails era la 0.9.3, mientras que en el momento de escribir esta segunda parte, Rails ya va por la versión 0.10.0 y tiene algunas caracerísticas nuevas. Utilizaremos Rails 0.10.0 en este artículo, así que describiremos la actualización. Ten en cuenta que si te instalaste Rails después del 24 de Febrero de 2005 ya tienes la 0.10.0 instalada.

La figura 1 muestra cómo ver qué Gems de Ruby tenemos instaladas, así como sus números de versión. Como en la primera parte, estoy trabajando en un sistema Windows, así que tendrás que traducir un poco todo esto si usas una plataforma diferente.

Listado de Gems instaladas
Figura 1. Listado de Gems instaladas

Abre una ventana de terminal y ejecuta lo siguiente:

gem list --local

Truco: el comando gem list --remote mostrará todas las gems disponibles en el servidor de rubyforge.org.

Si no tienes instalado Rails 0.10.0 (o posterior), para actualizar a la última versión sólo hace falta volver a ejecutar el comando:

gem install rails

Actualización de seguridad de MySQL

En la primera parte, te recomendé que dejases la clave de administrador de MySQL vacía porque (en el momento de escribir el artículo) Rails aún no soportaba el nuevo protocolo de claves. Muchos de vosotros no estabais conformes con esta situación y, para empeorarlo todo, ahora existe un virus que explota vulnerabilidades de clave en MySQL sobre Windows. Afortunadamente, a partir de la versión 0.9.4, Rails soporta el nuevo protocolo de claves.

Nueva funcionalidad del scaffold

Rails ha incoporado una nueva funcionalidad en el andamiaje, que no exploraremos aquí en profundidad pero que es tan interesante que no quiero dejar pasar la oportunidad de contarla. Es mejor ilustrarla con un ejemplo.

En la primera parte se vio como crear un modelo y un controlador para la clase Receta con los siguientes comandos:

ruby script\generate model Receta
ruby script\generate controller Receta

Y luego instanciamos el andamiaje insertando scaffold :receta en la clase RecetaController. Todo lo generado por este sistema, es decir, el controlador con métodos CRUD (creación, acceso, actualización y borrado) así como las plantillas resultantes, eran creados al vuelo en tiempo de ejecución y no era posible inspeccionarlos.

Esta técnica sigue funcionando, pero ahora tenemos otra opción. Podemos ejecutar el siguiente comando,

ruby script\generate scaffold Receta

que generará tanto el modelo como el controlador y además crea el código y vistas del andamiaje para todas las operaciones CRUD, lo cual nos permite ver este código generado y modificarlo para adaptarlo a nuestras necesidades. Conviene tener cuidado porque si ya se han creado modelos, controladores o plantillas sobreescribirá cualquier fichero existente al ir generando los contenidos.

Acabar la aplicación Recetario

Ha llegado la hora de pulir un poco la aplicación de la lista de recetas. Después presentaré otras prestaciones de rails que estoy seguro que resultarán interesantes.

Recuerda: en la primera parte, creamos la aplicación del recetario en el directorio c:\rails\recetario; y todas las rutas de directorio que se usan en este artículo asumirán este directorio base. Si escoges un lugar distinto asegúrate de hacer los cambios apropiados en los ejemplos de este artículo.

También te puedes descargar el codigo fuente de la versión en inglés de este tutorial en un único archivo zip. Funcionará bajo Rails 0.13 y posteriores, así que si aún utilizas una versión anterior de Rails, te sugiero que sigas las insrucciones de actualización descritas anteriormente.

Ah, y para todos los que estáis haciendo trampas (ellos saben quiénes son) y teneis pensado descargar el código fuente sin pasar primero por la primera parte, también os hará falta crearos una base de datos llamada recetario en MySQL y rellenarla.

Creación de una nueva receta con categorías

Dado que el código sigue usando el andamiaje para crear nuevas recetas, no hay manera de asignar una categoría a una receta. Esto no sería tan grave salvo por el detalle de que la página que hemos creado para mostrar todas las recetas asume que cada receta tendrá asignada una categoría y Rails generará un error si esto no se cumple. Esto significa que, en el estado que dejamos las cosas en la primera parte, tras añadir una nueva receta tendremos un error al intentar ver la lista de recetas.

El arreglo consiste en capturar la acción new del andamiaje como hicimos con la acción edit. Abre c:\rails\recetario\app\controllers\receta_controller.rb y añade el método new como en la figura 2.

El nuevo código del método new del
controlador Receta
Figura 2. El nuevo código del método new del controlador Receta.

La sentencia @receta = Receta.new crea un nuevo objeto receta vacío y se lo asigna a la variable de instacia @receta. Recuerda que una instancia de la clase Receta representa una fila en la tabla recetas de nuestra base de datos. Así pues cuando creamos una nueva instancia, la clase Receta puede asignar valores por defecto para que los utilice la plantilla de la vista.

Ahora mismo la clase del modelo Receta no pone ningún valor por defecto, pero la plantilla que voy a mostrar usará cualquier cosa que esté en el objeto @receta para inicializar el formulario del display. Más adelante, puedes añadir los valores por defecto en la clase Receta que se mostrarán cuando crees una nueva receta.

Como con la acción edit, este código también recupera una colección de todas las categorías de forma que pueda visualizar una lista desplegable con las categorías de entre las cuales el usuario puede escoger. La variable de instancia @categorias es la que lleva esta lista.

En el directorio c:\rails\recetario\app\views\receta, crea un fichero denominado new.rhtml que contenga la plantilla que se muestra debajo. Como es habitual, se trata en su mayor parte de HTML con algo de código para mostrar las etiquetas select y option para el desplegable con la lista de categorías:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
 <head>
 <title>Nueva Receta</title>
 </head>
  
 <body>
 <h1>Nueva Receta</h1>
 
 <form action="/receta/create" method="post">
 
   <p>
   <b>Titulo</b><br/>
   <input id="receta_titulo" name="receta[titulo]" size="30" type="text" value=""/>
   </p>
   
   <p>
   <b>Descripcion</b><br/>
   <input id="receta_descripcion" name="receta[descripcion]" size="30" type="text" value=""/>
   </p>
   
   <p>
   <b>Categoria:</b><br/>
   <select name="receta[categoria_id]">
 
   <% @categorias.each do |categoria| %>
        <option value="<%= categoria.id %>"> 
        <%= categoria.nombre %>
        </option>
   <% end %>
   
   </select>
   
   </p>
   <p>
 
   <b>Instrucciones</b><br/>
   <textarea cols="40" id="receta_instrucciones" name="receta[instrucciones]" 
      rows="20" wrap="virtual">
   </textarea>
   </p>
   
   <input type="submit" value="Create"/>
 </form>
  
 <a href="/receta/list">Back</a> 
 </body>
</html>

Como puede verse no hay mucha diferencia con respecto a la plantilla del método edit que ya vimos en la primera parte. Hemos omitido la fecha de la receta porque la ajustaremos por código cuando el usuario envíe el formulario a la aplicación, para estar seguros de que la fecha de la receta siempre será su fecha exacta de creación.

Si observas la etiqueta del formulario verás que se envía a la acción create del controlador receta. Edita c:\rails\recetario\app\controllers\receta_controller.rb y añade el método create, que recibirá los datos introducidos en el formulario anterior:

1
2
3
4
5
6
7
8
9
def create
    @receta = Receta.new(@params['receta'])
    @receta.fecha = Date.today
    if @receta.save
        redirect_to :action => 'list'
    else
        render_action 'new'
    end
end

Este método en primer lugar crea un nuevo objeto para la receta y lo inicializa con los parámetros introducidos en new.rhtml. Luego le ajusta la fecha a la receta con la fecha de hoy y le indica al objeto receta que se guarde en la base de datos. Si la operación tiene éxito, nos redirige a la acción listado que muestra todas las recetas, mientras que si no hemos podido grabar los datos en la base de datos, volveremos a la acción new de forma que el usuario lo pueda volver a intentar.

Vamos a probarlo. Arranca el servidor web abriendo una ventana de terminal, cambiando al directorio c:\rails\recetario y ejecutando el comando ruby script\server. Luego abre con el navegador http://127.0.0.1:3000/receta/new y añade una nueva receta como en la figura 3.

Nueva receta con categoría
Figura 3. Nueva receta con categoria

Tras crear la nueva receta debería verse algo parecido a la figura 4.

listado con todas las recetas
Figura 4. Listado con todas las recetas

Borrado de recetas

Si recuerdas la primera parte del artículo, una vez que escribimos nuestra propia acción para el listado de recetas (sustituyendo la del scaffold) dejamos de poder borrar una receta. Para implementar esto en la acción del listado, añadiré un enlace para el borrado después del nombre de cada receta en la página principal, de forma que al seguir ese enlace se borre su receta asociada. Esto es fácil.

Primero, edita c:\rails\recetario\app\views\receta\list.rhtml y añadele el enlace al borrado de la siguiente manera:

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
29
30
31
 <head>
   <title>Todas las Recetas</title>
 </head>
 <body> 
   <h1>Recetario Online - Todas las Recetas</h1>
   <table border="1">
    <tr>
      <td width="40%"><p align="center"><i><b>Receta</b></i></td>
      <td width="20%"><p align="center"><i><b>Categoria</b></i></td>
      <td width="20%"><p align="center"><i><b>Fecha</b></i></td>
    </tr>
    <% @recetas.each do |receta| %>
      <tr>
        <td>
        <%= link_to receta.titulo, 
                    :action => "show", 
                    :id => receta.id %>
        <font size=-1>
        &nbsp;&nbsp;&nbsp;
        <%= link_to "(delete)", 
                    {:action => "delete", :id => receta.id},
                    :confirm => "¿Seguro que quieres borrar #{receta.titulo}?" %>
        </font>
        </td>
        <td><%= receta.categoria.nombre %></td>
        <td><%= receta.fecha %></td>
      </tr>
    <% end %>
   </table>
 <p><%= link_to "Nueva receta", :action => "new" %></p> 
 </body>

El cambio principal es que hemos añadido este enlace:

1
2