sábado, 2 de abril de 2016

Ruby on Rails: migraciones

Una migración (migration) es la evolución del modelo de datos. Cada migración produce una versión del esquema de base de datos. Con este sistema de migraciones podríamos ir hacia adelante o hacia atrás en la versión del esquema. Ir hacia adelante es crear una nueva migración con cualquier operación DDL: añadir una nueva tabla, añadir o suprimir una tabla o realizar modificaciones en la estructura del registro como añadir un nuevo campo o alterar o suprimir un campo. Ten en cuenta que ir hacia atrás (ver rollback más abajo) puede potencialmente eliminar tablas y en definitiva destruir datos simplemente por el hecho de que no estaban en la versión anterior. CUIDADO.

Aquí os dejo este chuletario con los comandos más recurrentes en relación con las migraciones. Si no estás familiarizado con la expresión "bundle exec" que antepongo a los comandos, te recomiendo que revisites este post.

Primero, un par de ejemplos de migración. Primero Añadimos la tabla "nuevatabla". A continuación añadimos el campo "column" a la tabla Table. Esto que tenemos aquí son 2 MIGRACIONES, porque hemos modificado 2 veces sucesivas el esquema de base de datos.
bundle exec rails generate model nuevatabla campo1:string campo2:string
bundle exec rails generate migration AddColumnToTable column:tipo
Ejecuta la migración del entorno indicado en RAILS_ENV. Por defecto será en development.
bundle exec rake db:migrate

Ejecuta la migración del entorno indicado en RAILS_ENV. Como se observa es en production. Todos los comandos siguientes pueden incorporar también la variable RAILS_ENV aunque no lo indiquemos.
bundle exec rake db:migrate RAILS_ENV=production

Para deshacer la última migración empleamos la fórmula siguiente:
bundle exec rake db:rollback
Si quereoms deshacer una cantidad de migraciones que conocemos perfectamente podemos usar STEP. En el ejemplo desharíamos 2 migraciones.
bundle exec rake db:rollback STEP=2
Si por el contrario queremos ir a una versión concreta del esquema de base de datos, sea la cantidad de pasos hacia adelante o hacia atrás que sean, indicamos la versión con la variable VERSION.
bundle exec rake db:migrate:down VERSION=20160506210540
El numerito de la versión es una marca de tiempo con la que se etiqueta la migración. También es el nombre del fichero donde se guarda. Los tenemos todos en "db/migrate" de tu directorio de proyecto rails. Si ordenas por fecha dejando abajo los más recientes puedes hacerte una idea de qué deshacer con rollback.

martes, 15 de marzo de 2016

Importar un XML en ruby

Para ello usaremos la gema "nokogiri".

XML

Vamos a jugar con el ejemplo de w3schools que se encuentra en http://www.w3schools.com/xml/xml_attributes.asp
<messages>
  <note id="501">
    <to>Tove</to>
    <from>Jani</from>
    <heading>Reminder</heading>
    <body>Don't forget me this weekend!</body>
  </note>
  <note id="502">
    <to>Jani</to>
    <from>Tove</from>
    <heading>Re: Reminder</heading>
    <body>I will not</body>
  </note>
</messages>
Lo vamos a guardar en un fichero llamado "example.xml".

Importación

La fase de importación y parsing la hace la propia librearía. Lo que no está tan bien explicado en la documentación es cómo ir consiguiendo los registros sucesivamente y el valor de los campos de los mismos.

Vamos a probar un script como el siguiente

require 'nokogiri'

fichero='./example.xml'

xmlfeed = Nokogiri::XML(open(fichero))
all_items = xmlfeed.xpath("//messages/note")

fields = [ 'to','from','heading','body' ]
all_items.each do |item|
    fields.each do |f|
        entrada = item.at(f)
        valor = entrada.text
        puts "#{f} = #{valor}"
    end
end
#Fin del script
Vamos por partes.

La importación se hace con el bloque:

xmlfeed = Nokogiri::XML(open(fichero))
all_items = xmlfeed.xpath("//messages/note")

Para el fichero podríamos tirar de URL también.
Hasta ahora nosotros no hemos dicho nada de la estructura del XML... ¿ O si ? Si. La librería ha importado todo el XML, pero le hemos dicho que los regustros son eso que está dentro de la estructura de documento en el segundo nivel, esto es "//messages/note". Esto es una de las cosas que debemos hacer bien porque si no el desglose en registros no se hace correctamente.

Los campos que vamos a estudiar deben ser conocidos. Responden a una estructura predeterminada.
fields = [ 'to','from','heading','body' ]
Por supuesto que podríamos obtenerlos pero no complicaremos más el ejemplo.

Finalmente para ilustrar el ejemplo, imprimimos los campos uno a uno por consola.

miércoles, 9 de marzo de 2016

Ruby sin rails: parser de opciones en el script

El siguiente es un script de ejemplo de cómo se usa un parser de opciones en nuestros scripts de ruby. Un poco más abajo explicamos las cuestiones más relevantes. Usamos la gema optparse. Si tienes dudas sobre cómo instalar la gema para usarla en un script, puede serte útil el post de uso de gemas sin Rails.

require 'optparse'                                                                                             

options = {}
options[:fichero] = ""
options[:lineas] = -1

OptionParser.new { |opts|
    opts.banner = "Usage: #{File.basename($0)} -l  -f "

    opts.on( '-l', '--lineas N', 'lineas del fichero') do |arg|
        options[:lineas] = arg.to_i
    end

    opts.on( '-f', '--file FILENAME', 'fichero a tratar') do |arg|
        options[:fichero] = arg
    end

}.parse!

puts "Opciones: fichero = #{options[:fichero]}, lineas=#{options[:lineas]}"

# Resto del script
# ...
#Fin del script



Uso normal del script

$ ruby test_parse.rb -f prueba.txt
Opciones: fichero = prueba.txt, lineas=-1
Queda a juicio del programador considerar si el script debe o no seguir en base a los valores que han quedado en las variables. Este comentario me lo puedo ahorrar, pero lo destaco para distinguir entre una opción incorrecta y un valor incorrecto de una opción que si existe.

Ayuda del script (-h)

El parse hace cosas automáticamente. Suelen ser el tipo de cosas que todos deberíamos gestionar cuando creamos nuestros scripts. La principal es la ayuda. Observa la linea del script

opts.banner = "Usage: #{File.basename($0)} -l  -f "


Indicando el "usage" en la forma anterior, el resto lo hace el parser. Lo comprobamos indicando con "-h".

$ ruby test_parse.rb -h                                                      
Usage: test_parse.rb -l -f                                                                   
   -l, --lineas  N                  líneas del fichero  
   -f, --file FILENAME              fichero a leer



Errores del parser

Cuando la opción no está incluida entre las declaradas obtenemos un error que nos informa de ello. Además observa que el script no continúa y que la variable "$?" informa del error.

$ ruby test_parse.rb -i 10
test_parse.rb:23:in `
': invalid option: -i (OptionParser::InvalidOption) $ echo $? 1


martes, 1 de marzo de 2016

Ruby on Rails: despliegue en Apache

Para poner una aplicación Ruby on Rails en linea podemos usar el WEBrick y hacer algún escarceo pero para dejarlo desatendidamente en linea lo suyo es poner un Apache o un nginx. Como bien sabes tanto Apache como nginx son servidores web y solo llegan hasta la capa HTTP. Para usar páginas dinámicas producidas con cualquier aplicación se necesita un módulo que la ejecute. Esto es idéntico para PHP, Java, python y por supuesto Ruby.

El módulo que más se usa para Ruby on Rails es Passenger. También es posible usar Passenger para lenguajes como node.js o python.

Los pasos que vamos a dar son los siguientes:
1.- Instalar passenger
2.- Compilar para nuestro apache y sistema
3.- Configurar Apache para nuestra aplicación



INSTALACIÓN DE PASSENGER

Se instala como una gema. Comprueba si lo tienes instalado en tu sistema:
$ gem list passenger

*** LOCAL GEMS ***
passenger (5.0.24, 5.0.23

Si no es el caso, instalas la gema:
$ gem install passenger


COMPILACIÓN PARA APACHE

Este paso deberías hacerlo como root. Para la versión 5.0.24, en mi caso lo tengo en "/var/lib/gems/2.2.0/gems/passenger-5.0.24". Es un proceso guiado de varios pasos. Ejecúta lo siguiente sin más:
# /var/lib/gems/2.2.0/gems/passenger-5.0.24/bin/passenger-install-apache2-module


Es posible que el proceso falle por la falta de alguna librería. Esto es muy específico de cada equipo. Lee atentamente los mensajes de error e instala los paquetes necesarios para la compilación en tu sistema.

Configuración del Passenger

El siguiente es un ejemplo de fichero para usar en Apache2. Lo habitual es colocarlo en el /etc/apache2/sites-available y usar a2ensite/a2dissite (hace un enlace simbólico a /etc/apache2/sites-enabled). Una vez creado este fichero, es necesario reiniciar Apache.

Seguramente estarás familiarizado con la configuración de apache. Si no es tu caso te diré que hemos supuesto:
  • Suponemos que el servicio estará en la máquina con nombre DNS www.servidorpruebaruby.com y escuchará en el puerto 8080
  • Vamos a usar la versión 2.2 de Ruby
  • Suponemos que la aplicación está ubicada en /var/www/rails/RoRapp
  • Queremos poner producción en linea. Cambiando "RailsEnv" puedes especificar otro entorno como development o test.


Aquí tienes el fichero de configuración para Apache concordante con lo que hemos comentado en el post:

LoadModule passenger_module /var/lib/gems/2.2.0/gems/passenger-5.0.24/buildout/apache2/mod_passenger.so
<ifmodule mod_passenger.c="">
 PassengerRoot /var/lib/gems/2.2.0/gems/passenger-5.0.24
 PassengerDefaultRuby /usr/bin/ruby2.2
</ifmodule>

Listen www.servidorpruebaruby.com:8080
NameVirtualHost  www.servidorpruebaruby.com:8080

<virtualhost www.servidorpruebaruby.com:8080="">
   ServerName www.servidorpruebaruby.com
   DocumentRoot /var/www/rails/RoRapp/public
   RailsEnv production
   # RailsEnv development

   ErrorLog /var/log/apache2/RoRapp_error_log
   TransferLog /var/log/apache2/RoRapp_access_log
   LogFormat "%v %h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" 
   LogLevel warn

   AllowOverride all              
</virtualhost>


lunes, 29 de febrero de 2016

Bundle exec

Verás que a veces aparece la secuencia "bundle exec" delante de los comandos que usamos para gestionar aspectos de una aplicación Ruby on Rails. Esto es debido a que queremos que todo ocurra en el contexto de las gems y las respectivas versiones que nos interesan. Estas son las declaradas en el Gemfile.

La localización del fichero Gemfile, puede venir determinada de muchas maneras. Te recomiendo que visites los enlaces siguientes:


Si no quieres leer los enlaces que te sugiero, te propongo una simplificación: considera que el fichero Gemfile está en el directorio desde el que estás trabajando o donde apunta la variable de entorno BUNDLE_GEMFILE.

Al hilo de esto, quiero recordarte que debes tener en cuenta que:

  • En un sistema pueden haber instaladas varias versiones de una misma gema. Usando bundle exec delante de la invocación a tu script te aseguras de que se usan las versiones que en el Gemfile se indican
  • Es posible que en algunos casos te funcionen las aplicaciones con cualquier versión de la gema que tengas instalada, pero el mantenimiento y despliegue rigurosos de la aplicación requieren que hagas un esfuerzo por delimitar con qué versión de gemas estás trabajando bien y declararlas en el Gemfile.
  • "bundle install" se encargará de recrear, instalando lo necesario, el entorno de tu aplicación.
  • También sirve para aplicaciones o scripts Ruby sin Rails, de manera que tiene sentido lanzar un script ruby con "bundle exec" y se usarán las gemas previstas en el "Gemfile".

sábado, 27 de febrero de 2016

Ruby sin rails: importar datos en sqlite3

El ejemplo propuesto en este post permite importar un fichero de texto, donde suponemos que cada linea se inserta directamente en la tabla. Los id consecutivos se calculan manualmente por lo que al principio se obtiene el mayor de la tabla.

Observa que:
- Se mete en un bloque que captura posibles excepciones
- En cuanto a la sentencia ten en cuenta que lo correcto es usarla parametrizada para evitar inyección de SQL y para favorecer la reutilización que algunos motores de base de datos hacen de sentencias precompiladas.
- Las sentencias que hacen "FETCH" de datos como las SELECT deben ser cerradas.
#!/bin/ruby
require 'sqlite3'

begin
    db = SQLite3::Database.open "/var/www/rails/muytest/db/development.sqlite3"
    puts db.get_first_value 'SELECT SQLITE_VERSION()'

    stm = db.prepare "SELECT max(id) as id FROM items"
    rs = stm.execute
    row = rs.next
    id = row[0].to_i
    stm.close

    
    sentencia = "INSERT INTO items(id, data) VALUES(?,?);"

    File.open('fichero.txt', 'r') do |f1|
       while linea = f1.gets
           id = id+1
           stm = db.prepare sentencia
           stm.bind_params(id,linea)
       end
    end
end


rescue SQLite3::Exception => e
    puts "Exception occurred"
    puts e
ensure


viernes, 26 de febrero de 2016

Ruby sin rails: uso de gemas


Las gemas son los componentes con los que ruby extiende la funcionalidad. Los podéis concebir como clases, paquetes, extensiones, etc.. La cosa es añadir componentes sobre los que construir nuestras aplicaciones.

Hay gente que en sus primeros pasos queda impresionada por la potencia de Rails y piensa que las "gems" le son propias, pero desconocen que esas gemas pueden ser usadas también cuando hacemos un programa o script, es decir, fuera de Rails.


¿ Dónde están las gemas ?

Preguntamos por el environment del comando "gem" y nos indica la ubícación del depósito de gemas:
$ gem env
RubyGems Environment:
- RUBYGEMS VERSION: 1.8.23
- RUBY VERSION: 1.9.3 (2013-11-22 patchlevel 484) [x86_64-linux]
- INSTALLATION DIRECTORY: /var/lib/gems/1.9.1
- RUBY EXECUTABLE: /usr/bin/ruby1.9.1
- EXECUTABLE DIRECTORY: /usr/local/bin
- RUBYGEMS PLATFORMS:
   - ruby
   - x86_64-linux
- GEM PATHS:
   - /var/lib/gems/1.9.1
   - /home/user/.gem/ruby/1.9.1
   - /usr/share/rubygems-integration/1.9.1
   - /usr/share/rubygems-integration/all
- GEM CONFIGURATION:
   - :update_sources => true
   - :verbose => true
   - :benchmark => false
   - :backtrace => false
   - :bulk_threshold => 1000
- REMOTE SOURCES:
   - http://rubygems.org/

El directorio /var/lib/gems/1.9.1 de forma genérica y el resto de los GEM PATHS pueden albergarlas. Las gemas estarán accesibles desde cualquiera de esas rutas.

Instalación de gemas
$ gem install mysql2
Si queremos instalar una versión concreta tenemos el modificador "-v"
$ gem install mysql2 -v 0.3.14

La ubicación será una de las anteriores. Depende de si eres el usuario root o no. El listado completo de gemas que un usuario tiene accesibles se puede obtener con el siguiente comando:
$ gem list --local

*** LOCAL GEMS ***

mysql2 (0.4.3, 0.3.16, 0.3.14)
actionmailer (4.0.3, 4.0.2)
actionpack (4.0.3, 4.0.2)
[...]
Si queremos eliminar una versión concreta de una gema
$ gem uninstall mysql2 -v 0.3.14

Uso de una gema en un script
El uso de la gema es tan sencillo como incluir "require" y el nombre de la gema.
#!/usr/bin/env ruby

require 'mysql2'
# Uso de la gema...

jueves, 25 de febrero de 2016

Gestión de rutas en Rails

Seguro que te has preguntado por los automatismos que Ruby on Rails tiene por todos los rincones del framework. Una de las primeras que llaman la atención son los nombres abreviados de las rutas tipo "new_user" o "edit_item". Cuando se está empezando con Rails, al poco de modificar lo que viene de serie con los scaffold, nos solemos encontrar con un batiburrillo de links con nombres de ruta como los anteriores y otros más manuales e improvisados o bien con una cadena fija o bien indicando :controller, :action, :id, etc... Vamos a intentar arrojar algo de luz a todo a la forma en que funcionan las rutas y los alias de cada una de ellas. 

Lo primero es que debemos conocer el contenido del fichero config/routes.rb. En este fichero se declaran las rutas o más bien el esquema de rutas que sigue Ruby on Rails para saber a qué método y de qué controller hay que redirigir la petición. Un error típico de una ruta no contemplada es el siguiente:
ActionController::RoutingError (No route matches [GET] "/items/0/unkownroute"):
Si miras tu config/routes.rb, un contenido típico sería del tipo:


Myrubyapp::Application.routes.draw do
    resources :items
    resources :users

    match '/users/:id/logout', :to => "users#logout" , :via => 'get', :as => 'user_logout'
    match "/users/:id/login", :to => "users#login" , :via => 'get'
    match "/users/:id/login", :to => "users#login" , :via => 'post'
    match "/users/:id/change_pass", :to => "users#change_pass"  , :via => 'post'
    
    #...

    get '/items/:id/preview', to: 'items#preview', as: 'preview_item'

    #...

    root :to => "items#index"

end
Primero lo primero 
¿ Adónde se encamina a un visitante que no sabe a donde va ? A donde indique la linea que comienza con "root". Cuando se crea una aplicación el primer controller puede que sea el destinatario de la URL root pero podemos cambiar facilmente este comportamiento editando esta linea. 

Sintaxis
Vemos 2 tipos de sintaxis: las lineas que empiezan por match y las que empiezan por "GET", "POST", etc... Son equivalentes. Sirva como ejemplo de la equivalencia el que las 2 siguientes lineas darían el mismo resultado:
match "/items/:id/preview", :to => "items#preview"  , :via => 'get', as: 'preview_item'
get '/items/:id/preview', to: 'items#preview', as: 'preview_item'

La parte mágica
Ya habrás adivinado que esos nombres mágicos ("edit_item", "new_user", etc..) los podemos crear con el atributo :as.

IMPORTANTE: el estandar HTTP
Un programador web debe ser consciente de los distintos métodos posibles de interacción entre el cliente y el servicio web. Se suele frivolizar con ello y se usa en demasía el GET para todo. Tampoco quiero ser purista aquí, aunque hay que serlo si se puede, pero qué menos que no usar GET cuando está estrictamente desaconsejado e incluso prohibido. 

Todo el mundo sabe como funcionan los buscadores. Van visitando enlaces y navagando de aquí para allá para realizar una reconstrucción de los contenidos de la web. No olvides qué usan el método "GET"!!!!. ¿ Qué pasa si pones una operación de escritura en tu sistema a tiro de GET ? Exacto, te van a bondardear y sin querer van a modificar el estado de tu sistema. El tema es importante y da para otra entrada en el blog. Me conformo de momento con que sepas que ninguna operación que suponga cambios del tipo que sea debe atender a una invocación con GET.
Me remito al fichero config/routes.rb donde verás que hay urls que se sirven indistintamente del método HTTP de invocación GET y POST
    match "/users/:id/login", :to => "users#login" , :via => 'get'
    match "/users/:id/login", :to => "users#login" , :via => 'post'
    match "/users/:id/change_pass", :to => "users#change_pass"  , :via => 'post'



otros solo para GET
get '/items/:id/preview', to: 'items#preview', as: 'preview_item'

y otro solo para POST

    match "/users/:id/change_pass", :to => "users#change_pass"  , :via => 'post'


¿ Cómo queda el resultado ?
El resultado ya te lo puedes imaginar, ¿ no ?. Ah, que no te lo imaginas. No pasa nada para eso está el comando siguiente:
$ bundle exec rake routes
          Prefix Verb   URI Pattern                                   Controller#Action
            items GET    /items(.:format)                              items#index
                  POST   /items(.:format)                              items#create
         new_item GET    /items/new(.:format)                          items#new
        edit_item GET    /items/:id/edit(.:format)                     items#edit
             item GET    /items/:id(.:format)                          items#show
                  PATCH  /items/:id(.:format)                          items#update
                  PUT    /items/:id(.:format)                          items#update
                  DELETE /items/:id(.:format)                          items#destroy
[...]

Creo que para una primera aproximación a las rutas en Rails no está mal.

miércoles, 24 de febrero de 2016

Generar un docx desde Rails

Una de las bondades de tener bases de datos y aplicaciones web que los gestionen es que aquellos pueden ser presentados de infinidad de formas. Pensemos por ejemplo en la generación de un informe que enviar a un cliente, presupuesto, etc... Los formatos estrella son PDF y documento de texto tipo DOC/DOCX (para Word o Libreoffice). En este post quiero dar unas directrices sencillas para que cualquiera pueda generar un documento tipo DOC o DOCX desde su aplicación Rails. Básicamente vamos a generar el documento y enviarlo al cliente web.


Para ello usaremos la gema "Caracal". La documentación oficial la puedes encontrar a partir de la URL de github https://github.com/trade-informatics/caracal.

Como es habitual en Rails, la incluimos en el Gemfile y ejecutamos el bundle install.


Generación de documento word

En la documentación de la gema hay ejemplos suficientes. Aquí muestro un ejemplo adaptado que podéis incluir tal cual en vuestro controller

def genera_word
  @items = Item.all
  Caracal::Document.save 'example.docx' do |docx|

    #Tamaño de página 
    docx.page_size do
          width   12240     # page width in twips.
          height  15840     # page height in twips.
    end

    #Fijamos los márgenes
    docx.page_margins do
      left    720     # left margin in twips.
      right   720     # right margin in twips.
      top     1440    # top margin in twips.
      bottom  1440    # bottom margin in twips.
    end

    #Colocamos el numero de página
    docx.page_numbers true do
      align :right    # accepts :left, :center, and :right.
    end
  
    #Cambiamos la fuente por defecto
    docx.font do
      name 'Droid Serif'
    end
  
    docx.h1 'Listado de items'
    cuenta = 0
    @items.order(:orden).each do |i|
      cuenta +=1
      docx.page if (cuenta%20==0)  #Página cada 20 lineas 
      docx."Item #{i.name}"
      docx.p  #Linea en blanco
    end
  end #Caracal
end #genera_word

Si has usado el nombre de función que te he propuesto acuérdate de incluir la ruta en config/routes.rb.

Descarga en el cliente

Tan sencillo como usar send_file. El método invocado en el controller contendrá algo como lo siguiente que envía el fichero al cliente.

def get_info_word
  #....

  send_file("example.docx" ,
     :filename      =>  "example.docx",
     :type          =>  'application/msword')
end
Ni qué decir tiene que send_file tiene diferentes opciones para optimizar la descarga. Aquí hemos mostrado la formulación básica.

Muchos lo tenéis claro, pero por si acaso, me comprometo mostrar en otro post como hacer esto mismo desde un script RUBY obviando el framework Ruby on Rails.