Dagi3d v4

Generar documentos pdf con Rails y Flying Saucer

Aviso: 

Este post está obsoleto. Escribí un plugin que permite generar los pdfs de manera mucho más sencilla. Más información ­aquí

---

En la aplicación que estaba haciendo en RoR necesitaba generar documentos en formato pdf a partir de los formularios rellenados por el usuario. Había comenzado a hacerlos usando la librería para ruby PDF::Writer y aparentemente funcionaba bastante bien pero resultaba un infierno el tener que maquetar toda la presentación desde código, aparte de que no me gustaba mucho la idea de hacerlo así de cara a posibles cambios de la plantilla.
De casualidad, a través de un enlace en Devzone di con un árticulo sobre la generación de pdf's en java usando las librerías Flying Saucer e iText.
Flying Saucer es una librería para renderizar documentos xhtml y css 2.1 y que ahora trabaja de manera conjunta a iText, que sirve para generar documentos pdf.
Así que la opción de tratar de integrar esta librería en la aplicación para así convertir la vista generada desde Rails en el pdf, era más que tentadora.

Para hacerlo, lo primero era descargar la librería Flyin Saucer(que ya viene con iText incluida) y generar la aplicación en Java que se encargase de convertir el documento xhtml en pdf(está copiada casi tal cual de un ejemplo del artículo):

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

import org.xhtmlrenderer.pdf.ITextRenderer;
import com.lowagie.text.DocumentException;


public class Xhtml2Pdf {

    /**
     * @param args
     */
    public static void main(String[] args) throws IOException, DocumentException {
       
        if (args.length != 2) return;
       
        String inputFile = args[0];
        String url = new File(inputFile).toURI().toURL().toString();
       
        String outputFile = args[1];
       
        OutputStream os = new FileOutputStream(outputFile);

        ITextRenderer renderer = new ITextRenderer();
        renderer.setDocument(url);
        renderer.layout();
        renderer.createPDF(os);

        os.close();
    }
}

Luego escribí una sencilla función en ruby que se encargase de ejecutar la clase hecha en java pasándole como parámetros el archivo de origen con el documento xhtml y el archivo de destino donde se generaría el pdf(lo único que tiene así de 'chicha' es que genera el classpath de manera dinámica):

def xhtml2pdf(input_file, output_file)
  
  java_dir = File.join(File.expand_path(File.dirname(__FILE__)), "java")
  jar_dir = File.join(java_dir, "jar")
  
  class_path = ".:#{java_dir}"
  
  Dir.foreach(jar_dir) do |jar|
    class_path << ":#{jar_dir}/#{jar}" if jar.match(/\.jar/)  
  end
  
  command = "java -cp #{class_path} Xhtml2Pdf #{input_file} #{output_file}"
  system(command)
end

Para poder utilizar la librería desde el controlador, tan sólo quedaba copiar el fichero en ruby dentro la carpeta lib/ del proyecto Rails, la clase en java en lib/java y los jar's necesarios para ésta, en lib/java/jar:

Y en el controlador tan sólo tenía que generar la vista y guardarla en un fichero, convertirlo en pdf y mandarlo al cliente:

require 'xhtml2pdf'

class FooController < ApplicationController
 
  def generate_pdf
        
    @document = Document.find(params[:id])
    
    xhtml = "/tmp/foo.xhtml"
    pdf = "/tmp/foo.pdf"
    
    File.open(xhtml, "w") do |file|
      file << render_to_string(:template => "pdf/document", :layout => "../pdf/pdf")
    end

    xhtml2pdf(xhtml, pdf)
    
    send_file pdf,
      :filename => "document.pdf",
      :type => "application/pdf"
  end
end

Y si quisieramos ir viendo en el navegador cómo va a quedar sin necesidad de generar el pdf, bastaría con comentar el contenido de esta función(o hacer una nueva) y generar la vista como hariamos normalmente:

render :template => "pdf/document", :layout => "../pdf/pdf"

Por último, comentar que para que Flying Saucer pueda acceder a las imágenes y hojas de estilo del documento xhtml, las rutas de estos elementos deben estar de manera absoluta apuntando a un recurso local(file://<rutadelfichero>) o bien de manera relativa a dónde se encuentre físicamente el xhtml, por lo que igual habría que hacer un helper que modificase la ruta de los recursos dependiendo de si se está previsualizando desde el navegador o se está volcando en disco.

Aunque PDF::Writer parecía funcionar bastante bien e iba más rápido ya que escribe directamente el pdf sin necesidad de parsear un documento xhtml y sus hojas de estilo, creo que en este caso compensa tener un pequeño híbrido en rails y java por el tiempo ahorrado en maquetar el pdf directamente desde ruby.

­

Bantha Studio abre sus puertas

Bueno, pues parece que al final el señor King George se ha animado a dotar de presencia en internet al proyecto bajo el que estaba firmando sus últimos trabajos: Bantha Studio.

De momento tiene abierto un blog donde hablará principalmente sobre diseño, cómics y toys y aunque su portafolio todavía no está visible, ya se pueden ver algunas de sus ilustraciones dentro del propio blog, con una estética bastante influenciada por todos los tebeos que se debió de mamar de pequeño.

Esperemos que a partir de ahora le salgan proyectos interesantes. ¡Larga vida al rey!

www.banthastudio.com

 

Ocultando objetos en Ruby On Rails: acts_as_invisible

Durante el desarrollo de la aplicación que mencioné en el último post, se me planteó un nuevo 'problema' y es que necesitaba que desde el propio programa se pudieran borrar registros pero que realmente no se eliminaran de la base de datos, si no que simplemente desaparecieran de cara a la aplicación. En principio esto era tan sencillo de solucionar como añadir un nuevo atributo a la clase a modo de flag y añadir una nueva condición a la hora de buscar los objetos dentro del controlador:

Document.find(:all, :conditions => ["visible = ?", true])
Document.find(id, :conditions => ["visible = ?", true])

# "borrando" el objeto
document.visible = false
document.save

Esto funcionaba, pero me parecía un tanto engorroso tener que añadir una condición extra a la hora de hacer las búsquedas y borrar en todas las clases que quería que fuesen 'ocultables'. Así que por aquello de seguir con la filosofía del DRY e indagar un poco en el core de Rails, hice un sencillo plugin que añadía esta funcionalidad de manera transparente y sin necesidad de tocar el código ya existente del controlador. Así, tan sólo habría que marcar la clase del modelo con el método 'acts_as_invisible' (Se da por hecho que existe una columna de tipo booleano llamada 'visible' en la tabla asociada a la clase):

class Document < ActiveRecord::Base

acts_as_invisible

end

Y a la hora de trabajar con el modelo, se haría de la siguiente manera:

# recuperar sólo los documentos visibles
Document.find(:all)

# recuperar sólo los documentos visibles cuyo título empiece por 'presupuesto'
Document.find(:all, :conditions => "title like 'presupuesto%'")

# recuperar todos los documentos
Document.find(:all, :show_hidden => true)

# ocultar un documento
Document.destroy(id)

# si se tratase de recuperar el documento ocultado
# previamente, saltaría una excepción
Document.find(id)

# volver a hacer visible un documento
Document.show(id)

# borrar el documento de la base de datos
Document.confirm_destroy(id)

Por si a alguien le interesa, puede bajarse el plugin de http://svn.dagi3d.net/rails/acts_as_invisible/tags/acts_as_invisible/ y si tengo algo de tiempo ya le añadiré alguna funcionalidad más como que el nombre del campo utilizado sea configurable.

Parche para acl_system

Ahora estoy desarrollando un programa de gestión con Ruby On Rails donde necesitaba usar algún sistema de control de privilegios dentro de la aplicación. Tras investigar un poco opté por usar acl_system, que parecía bastante sencillo de usar y era más que suficiente para lo que necesitaba, ya que permite restringir el acceso a los métodos del controlador y delimitar partes de la vista para usuarios que tengan un determinado 'rol' .
El único inconveniente era que si dentro del controlador quería preguntar por un rol en concreto para hacer cosas más específicas, tenía que hacer algo así:
if current_user.roles.map { |role| role.title }.include?("admin")
...
end
Aunque funcionaba perfectamente, pensaba que quedaría mejor si le añadía un pequeño parche al plugin acl_system que permitiese hacer algo del tipo
if current_user.has_admin_role?
...
end
Así que aprovechando el potencial de ruby, en unas pocas líneas de código conseguí dar con la solución:
module Dagi3d

  module RoleModel
    def self.included(mod)
      mod.extend(ClassMethods)
    end
    
    module ClassMethods
      
      def has_roles
        include InstanceMethods
      end
      
    end # ClassMethods 
    
    module InstanceMethods
      
      def method_missing(method, *args)
      
        if method.to_s =~ /has_(\w+)_role\?/
          self.roles.map {|role| role.title}.include? $1
        else
          super
        end
      end #method_missing
      
    end #InstanceMethods
  end
end
Para hacerlo andar, bastaría con añadir el módulo en la carpeta del plugin e incluirlo en el fichero init.rb:
require 'caboose/logic_parser'
require 'caboose/role_handler'
require 'caboose/access_control'
require 'caboose/role_model'
 
ActionController::Base.send :include, Caboose
ActionController::Base.send :include, Caboose::AccessControl
ActiveRecord::Base.send :include, Dagi3d::RoleModel

Y ya por último, llamar en nuestro modelo al método de clase 'has_roles':
class User < ActiveRecord::Base
  
  has_roles
  ...
end

Accesibilidad con Ajax y Ruby On Rails

Estaba desarrollando una página web con Ruby On Rails donde necesariamente tenía que utilizar Ajax para mostrar el contenido de las distintas secciones ya que ésta tenía un reproductor de audio hecho en Flash, por lo que el hecho de recargar la página entera suponía cortar la música cada vez que se cambiase de sección (o eso o utilizar iframes, así que la cosa estaba clara)

El 'problema' que me encontré con la función 'link_to_remote' es que por defecto no añade ninguna url en el atributo 'href' del enlace, lo que no sólo supone un problema para aquellos usuarios que no tengan javascript, si no a la hora de que los buscadores indexen el contenido, ya que en teoría no siguen los enlaces dentro de código javascript.

Para solucionarlo, tan sólo tuve que escribir una sencilla función en un helper que añadiese automáticamente al atributo 'href' la misma url a la que se iba a hacer la petición con Ajax:
module PublicHelper

def ajax_link(label, url = {}, options = {})

options.merge!({:href => url_for(url)})

link_to_remote label,
{
:url => url,
:update => "section_content",
:loading => "Element.hide('section'); Element.show('loading'); ",
:complete => "Element.hide('loading');"
},
options
end
end
Así, al llamar a la función ajax_link("enlace", :controller => "foo", :action => "bar"), se genería el código html <a href="/foo/bar" onclick=".." />

El siguiente incoveniente era distinguir en el controlador cuándo se estaba haciendo una petición con Ajax o se estaba pidiendo directamente la url para devolver sólo el trozo html con el contenido, o bien la página entera. Por defecto, Prototype, la librería utilizada en Rails para usar Ajax, realiza la petición a través del método POST, así que para decidir si se debía utilizar el layout tan sólo había que tener en cuenta el tipo de petición que se estaba haciendo:
class FooController < ApplicationController

layout "foo"

def bar
render(:action => "foobar", :layout => request.get?)
end
end
Otra opción hubiese sido mandar un parámetro adicional en la petición y luego comprobar en el controlador si éste existia y así saber si se trataba de una petición con Ajax o no.

Igual sería interesante hacer esto en forma de plugin e incluir alguna funcionalidad más como añadir el código javascript a los enlaces de manera no intrusiva.