Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Report professionali per Ruby e Rails con Ruport

Creare report in PDF, ODF, CSV e altri formati
Creare report in PDF, ODF, CSV e altri formati
Link copiato negli appunti

Quando si ha a che fare con una banca dati, a prescindere dalla natura delle informazioni, che essa sia rappresentata da un database o da un grosso file, ci si deve confrontare con la necessità di confezionare delle visualizzazioni, dei report.

Questo per avere risultati utili e semplici da analizzare e manipolare, con tabelle, immagini e grafici, soprattutto se prodotti nei formati più usati come PDF o Excel (ma anche Word, CSV etc.).

In questo articolo ci occupiamo della creazione di report con Ruport, dividendo il discorso in due parti. Nella prima parleremo dei concetti di base della libreria:

  • strutture dati
  • manipolazione dati
  • controller
  • formatter

Nella seconda, invece, estenderemo alcuni concetti e approfondiremo temi quali la gestione dei template per la formattazione, la generazione di grafici, le estensioni per la generazione di file Excel e l'integrazione con Ruby on Rails.

Inoltre ci serviremo di esempi che aiuteranno a capire ed apprezzare funzionalità e potenzialità di Ruport.

Ruport: cosa è e cosa offre?

Ruport arrivato alla versione 1.6.1, sviluppato da Gregory Brown, Dudley Flanders, James Healy, Dinko Mehinovic e Michael Milner, fornisce un insieme di tool che aiutano lo sviluppatore ad aggiungere capacità reportistiche alle proprie applicazioni.

Offre una serie di facilitazioni per l'estrazione, il raggruppamento e la manipolazione di dati, oltre alla capacità di renderli disponibili in diversi formati di output grazie ad un sistema flessibile ed estensibile per la formattazione ed il rendering dei report.

Prepariamo il campo di lavoro

Prima di iniziare ad analizzare le potenzialità di Ruport, provvediamo all'installazione tramite l'ormai noto RubyGems.

gem install ruport

che provvederà ad installare automaticamente le dipendenze (fastercsv,
pdf-writer
).

Le strutture dati di Ruport

Ruport usa quattro strutture dati di base: record, table, group e grouping, tutte contenute nel modulo Ruport::Data. Il nucleo delle strutture dati è la tabella. I record formano le tabelle, i group ed i grouping servono per raggruppare i dati in tabella.

Iniziamo a crearne qualcuna. Partiamo dai record.

I Record rappresentano la struttura dati più semplice. Corrispondono ad una riga di dati proveniente da database o da altre sorgenti di dati.

Listato 1. Creare record

%w[rubygems ruport].each {|l| require l}

  column_headers = ["artist", "album", "year", "tracks"]

  first_item  = Ruport::Data::Record.new(["King Crimson", "In the Court of Crimson King", "1969", "5"], :attributes => column_headers) => #<Ruport::Data::Record:0x2d1b864 @data={"artist"=>"King Crimson", "year"=>"1969", "tracks"=>"5", "album"=>"In the Court of Crimson King"}, @attributes=["artist", "album", "year", "tracks"]>
  second_item = Ruport::Data::Record.new(["King Crimson", "In the Court of Crimson King", "1969", "5"], :attributes => column_headers)
  third_item  = Ruport::Data::Record.new(["Genesis", "Nursey Crime", "1972", "7"], :attributes => column_headers)
  fourth_item = Ruport::Data::Record.new(["Frank Zappa","We're Only In It For The Money", "1968", "19"], :attributes => column_headers)

L'accesso ai dati del record può avvenire usando una notazione array-like o hash-like, rispettivamente first_item[1] e first_item["album"].

Le Tabelle sono collezioni di Record e presentano i metodi necessari per lavorare con i dati in esse conte

Listato 2. Creare e visualizzare una tabella

table = Ruport::Data::Table.new => #<Ruport::Data::Table:0x2d2fb48 @record_class="Ruport::Data::Record", @data=[], @column_names=[]>

table.column_names = column_headers
table << first_item << second_item << third_item << fourth_item

puts table
+---------------------------------------------------------------+
|artist        | album                          | year | tracks |
+---------------------------------------------------------------+
| King Crimson | In the Court of Crimson King   | 1969 | 5      |
| King Crimson | In the Court of Crimson King   | 1969 | 5      |
| Genesis      | Nursey Crime                   | 1972 | 7      |
| Frank Zappa  | We're Only In It For The Money | 1968 | 19     |
+---------------------------------------------------------------+

Come accennato precedentemente, i metodi disponibili sulla struttura dati Table sono molti. Vediamone alcuni:

  • column_names
  • remove_column("album") e per più colonne remove_columns("album", "year")
  • rename_column('old_name', 'new_name'), rename_columns ('old_names', 'new_names')
  • swap_column('a', 'b')
  • replace_column('old_col', 'new_col')
  • add_column('c', :before => 'd')
  • sub_table(%w[album year])

Per un elenco completo fare riferimento alla documentazione ufficiale.

I dati possono essere raggruppati secondo criteri a nostra scelta. Ruport fornisce due strutture dati per questo:

  • I Group, Ruport::Data::Group, che raggruppa la tabella con un nome. È di fatto una classe ereditata da Ruport::Data::Table a cui aggiunge il metodo name
  • I Grouping rapprensentano una collezione di gruppi. È possibile usare il costruttore della classe Ruport::Data::Grouping.newoppure il Kernel method Grouping

Listato 3. Creazione di un Grouping

artist_grouping = Grouping(table, :by => "artist")
year_grouping = Ruport::Data::Grouping.new(table, :by => ["artist", "year"])
 
puts artist_grouping
puts year_grouping

Manipolazione dei dati

Con poche linee di codice, spesso ne basta solo una, è possibile eseguire manipolazioni dei dati presenti nelle strutture prima descritte. Questa funzionalità, insieme al fatto che i dati possono provenire da diverse sorgenti ed essere nativamente pronti per la visualizzazione in vari formati di output, rendono Ruport adatto alle esigenze di ogni sviluppatore.

Prendiamo ora in considerazione operazioni quali l'ordinamento, la ricerca, le operazioni su colonna, il calcolo della somma e della media dei dati in colonna.

Ordinare le tabelle

puts table.sort_rows_by("year", :order => :ascending)

Ordinare i Grouping

Nel più semplice dei casi possiamo ordinare un Grouping dal nome del gruppo impostando l'opzione :order al valore del :name del gruppo.

g = Grouping(table, :by => "tracks", :order => :name)
puts g.to_a.map {|name,group| name}

oppure ordinandolo in base ad uno specifico blocco:

g = Grouping(table, :by => "artist", :order => lambda {|g| g.size })
puts  g.to_a.map {|name,group| name }

o ancora, grazie al metodo sort_grouping_by dopo che è stato creato:

g = Grouping(table, :by => "year")
puts g.sort_grouping_by {|g| g.size }

Cercare righe in una tabella

Ruport mette a disposizione del tipo Table il metodo rows_with, perfetto per le semplici operazioni di ricerca:

table.rows_with("artist") { |a| 
  if a.size < 11
    puts a
  end
}

Se ad esempio volessimo conoscere il numero totale di tracce presenti nella nostra collezione di Cd e la media di tracce per cd, potremmo usare il metodo Table#sigma o il suo alias Table#sum

# Totale
puts table.sigma("tracks")

# Media
puts table.sum("tracks") / table.length

Filtrare e trasformare i dati

Ruport::Data::Feeder fornisce un semplice proxy object che ci permette di filtrare i dati che vogliamo aggregare. È principalmente usato per creare una tabella wrapper con vincoli. Può essere usato anche con strutture dati astratte.

# Esempio tratto dalla documentazione ufficiale
# http://api.rubyreports.org/classes/Ruport/Data/Feeder.html

t = Table(%w[a b c]) do |feeder|
  feeder.filter { |r| r.a < 5 }
  feeder.transform { |r| r.b = "B: #{r.b}"}
  feeder << [1,2,3]
  feeder << [7,1,2]
  feeder << { "a" => 3, "b" => 6, "c" => 7 }
end

t.length #=> 2
t.column("b") #=> ["B: 2","B: 6"]

Creazione dell'output

Tutte le strutture dati di base di Ruport possono produrre nativamente output in diversi formati (PDF, Text, HTML, CSV). Per farlo basta semplicemente richiamare il metodo corrispondente:

Listato 4. Formati di output per i tipi dati nativi di Ruport

# TXT
puts table.to_text #equivalente a puts table

# CSV
puts table.to_csv

# HTML
puts first_item.to_html

# PDF
File.open("test.pdf", "w") {|f| f << table.to_pdf }

Per situazioni semplici o per i test, le capacità di output native delle strutture dati rappresentano davvero un aiuto.

Il sistema di formattazione di Ruport ci consente di personalizzare la formattazione ed il rendering dei nostri dati. È costituito da due componenti:

  • Il Controller che definisce gli step necessari alla creazione dell'output
  • Il Formatter che definisce l'implementazione degli step per i formati di output

In Ruport gli step definiti nel Controller prendono il nome di stage. Definendo gli stage, il Controller sa che dovrà fare riferimento alle rispettive implementazioni presenti nel Formatter a lui associato al fine di produrre l'output.

Per la creazione dei metodi del Formatter corrispondenti agli stage del Controller, Ruport segue una convenzione interna. Fa uso del metodo di classe build con il nome dello stage e gli associa un block (il corpo del metodo). Nel caso non si volesse seguire la convenzione, nulla ci vieta di definire il metodo build_nomestage.

Esempio finale

In questo esempio mettiamo insieme i concetti finora esposti al fine di fornire un esempio di gestione e manipolazione dati, nonché di creazione di controller e formatter per i nostri report.

Listato 5. Esempio finale

# Definiamo il Controller
class CdCollectionController < Ruport::Controller
  stage :header, :table

  required_option :info
end

# Formatter per l'output testuale
class Text < Ruport::Formatter
  renders :text, :for => CdCollectionController

  build :header do
    output << options.info[:title] << "n"
  end

  build :table do
    output << data.to_text
  end
end

# Formatter per l'output HTML
# con template ERB
class HtmlFormatter < Ruport::Formatter::HTML
  renders :html, :for => CdCollectionController

  build :header do
    output << "#{options.info[:title]}"
  end

  build :table do
    output << erb("music.html.erb")
  end
end

myoptions = Hash.new
myoptions[:title] = "My Cd Collection"

puts CdCollectionController.render(:text, :data => table, :info => myoptions)
puts CdCollectionController.render(:html, :data => table, :info => myoptions)

Abbiamo accennato ai concetti ed alle funzionalità di base di Ruport. Nella seconda parte dell'articolo descriveremo le funzionalità permettono di creare grafici (SVG o su PDF), personalizzare il layout e interagire con RubyonRails.

Nella prima parte dell'articolo ci siamo occupati degli aspetti di base della libreria, di come si creano e visualizzano tabelle, delle manipolazioni semplici dei dati e di come creare output personalizzato tramite la definizione di controller e formatter. Ora discuteremo di come:

  • integrare i report nelle nostra applicazioni Rails
  • creare dei grafici
  • personalizzare i template
  • esportare i report in formati OpenDocument

ed infine accenneremo all'uso di rope.

Aggiungiamo il necessario al campo di lavoro

Con RubyGems provvediamo all'installazione delle gem necessarie (acts_as_reportable, ruport_util, documatic, pdf_writer_proxy).

  • ruport-util include una serie di utilities per il benchmark, la creazione di grafici, metodi helpers per i PDF, la conversione da csv ad ODF, etc.
  • documatic è un template-driven formatter per la generazione di file OpenDocument. Il codice Ruby per la gestione dei dati viene embeddato in template creati con OpenOffice.
  • Per la generazione di output in formato PDF, Ruport fa uso di pdf-writer. Spesso capita di avere la necessità di una maggiore flessibilità per la generazione del PDF, oppure abbiamo a disposizione del codice scritto in precedenza con PDF::Writer e lo si vuole integrare con codice scritto usando Ruport. Per venire incontro a queste esigenze c'è pdf_writer_proxy.
gem install ruport-util documatic acts_as_reportable
gem install pdf_writer_proxy --source http://gems.rubyreport.org

Ruport e RubyOnRails

Per integrare dati di applicazioni RubyOnRails all'interno dei nostri report usiamo il modulo acts_as_reportable che usa i risultati di un ActiveRecord::Base.find() per preparare una tabella di Ruport (Ruport::Data::Table). Integrandosi con ActiveRecord è naturalmente utilizzabile in modo indipendente da Rails ed in tutte le applicazioni che usano ActiveRecord in modo standalone.

Aggiungiamo require 'ruport' nel model ActiveRecord oppure direttamente nel file di Rails environment.rb che provvederà a caricare Ruport automaticamente.

Il modo più semplice per iniziare ad usare i risultati di una ricerca in DB è aggiungere il metodo acts_as_reportable all'interno della definizione della model class di ActiveRecord. In questo modo si avrà già a disposizione il metodo report_table.

Listato 6: acts_as_reportable

class Album < ActiveRecord::Base 
      acts_as_reportable
      belongs_to :artist
  
  def artist_name
    artist.name
  end
end
    
class Artist < Active::Record::Base
      acts_as_reportable
      has_many :albums
end
    
puts Album.report_table(:all, :except => :artist_id)

Riportiamo alcune opzioni disponibili con il metodo report_table:

  1. :only - seleziona le colonne desiderate
  2. :except - esclude alcune colonne
  3. :methods - in tabella saranno visualizzati anche i risultati della chiamata ad un metodo
  4. :include - include anche i risultati dei modelli associati
  5. :filters - filtra i risultati in base a criteri a nostro piaciemento
  6. :transforms - trasforma i risultati in tabella

Poiché acts_as_reportable usa il metodo find di ActiveRecord, tutte le opzioni che non vengono automaticamente riconosciute sono passate al metodo find. Questo significa che è possibile usare tutte le opzioni riconosciute da ActiveRecord::Base.find.

puts Album.report_table(
        :all, 
        :only => :title,
        :include => { :artist    => { :only => :name } }
        :filters => lambda {|r| r["id"] > 1 })

Equivalente a:

puts album.report_table(:all, :only => [:title], :methods => [:artist_name], :filters => lambda {|r| r["id"] > 1 })

Il metodo di acts_as_reportable prende tutte le opzioni del metodo report_table, permettendo così di impostare le opzioni di default per i nostri report:

acts_as_reportable :except => [:id, :artist_id]

Analogamente al metodo ActiveRecord::Base.find_by_sql è presente il metodo report_table_by_sql.

Un'altra modalità messa a disposizione da Ruport per collezionare i dati è quella di usare la classe Ruport::Query, che fornisce il supporto all'uso della libreria RubyDBI

Listato 7. Query

require 'rubygems'
require 'ruport'
require 'active_record'
require 'ruport/acts_as_reportableì
    
Ruport::Query.add_source(
    :default, 
    :user => "testuser",
    :passwrod => "testpwd"
    :dsn => "dbi:mysql:cdArchive_db")
        
query = Ruport::Query.new("SELECT * FROM albums")
puts query.result

Inserire grafici ed immagini

La creazione di grafici a partire dai dati presenti nelle strutture base di Ruport richiede l'installazione di ruport-util e non molto effort. Ruport fa da wrapper per alcune porzioni della libreria Gruff per i formati JPEG e PNG e di Scruffy per l'SVG. Risulta quindi necessario provvedere all'installazione di queste librerie, ricordandoci della dipendenza di Gruff da RMagick.

È presente una struttura dati apposita per i grafici, nello specifico, Ruport::Data::Graph, figlia di Ruport::Data::Table, con l'aggiunta di metodi specifici per la preparazione di grafici, come series che permette di aggiungere linee ai grafici. A livello di Kernel è presente lo shurtcut Graph.

Come detto in precedenza, porzioni delle librerie Gruff e Scruffy sono presenti direttamente in Ruport. Ad esempio usando Gruff è possibile creare soltanto grafici a linee. Questa non è affatto una limitazione. Sfruttando il concetto di classi aperte di Ruby, basta semplicemente aggiungere il tipo di grafico desiderato (pie, bar, etc.) a quello già presente. Vediamo come aggiungere il tipo grafico pie (torta) e bar.

Listato 8. Estensione del modulo Graph

%w[rubygems ruport ruport/util ruport/extensions].each {|lib| require lib}

class Ruport::Formatter

  module Graph
    class Gruff < Ruport::Formatter 
      renders [:png,:jpg], :for => Ruport::Controller::Graph
      
      def build_graph
        height = "#{options.height || 600}"
        width = "#{options.width || 800}"
        dimensions = "#{width}x#{height}"
        type = options.graph_type || :line
        
        graph = case type
          when :line
            ::Gruff::Line.new(dimensions)
          
          # inizio aggiunta
          when :bar
            ::Gruff::Bar.new(dimensions)
          when :pie
            ::Gruff::Pie.new(dimensions)
          # fine aggiunta
        end
        
        graph.title = options.title if options.title
        graph.labels = options.labels if options.labels
        
        data.each do |r|
          graph.data(r.gid,r.to_a)
        end
        
        graph.maximum_value = options.max if options.max
        graph.minimum_value = options.min if options.min
        
        output << graph.to_blob(format.to_s)
      end
    end
  end
end

Listato 9. Applicare l'estensione al controller

class ReportController < Ruport::Controller
  stage :long_report
  required_option :file

  ##
  # L'hook method setup viene richiamato dopo che le opzioni
  # sono processate. Questo significa che possiamo manipolare
  # in modo semplice gli attributi dei dati così come ogni
  # opzione passata al controller.
  ##
  def setup
    # do someting
  end
  
  class PDFReport < Ruport::Formatter::PDF
    renders :pdf, :for => ReportController
    
    proxy_to_pdf_writer
    
    build :long_report do
      prepare_long_report
      render_pdf
    end
    
    build :graph do
      x = 150
      width = 300
      height = 225
      y = 150
      g = Ruport::Data::Graph(%w[Artist Year])
      
      test = Array.new
      test << data.rows_with("artist" => "King Crimson").length
      test << data.rows_with("artist" => "Genesis").length
      test << data.rows_with("artist" => "Frank Zappa").length
      
      g.series(test, "gid")
      g.series([3, 4], "second")
      
      draw_graph(g, :title => "Grafico di prova", :x => x, :y => y, 
                 :width => width, :height => height, :graph_type => :pie)
    end
    
    def prepare_long_report
      start_page_numbering(300, 87, 7, :center)
      draw_table(data.sort_rows_by("artist"))
      build_graph
      apply_long_report_page_footer
    end
    
    def apply_long_report_page_footer
      pdf_writer.bottom_margin = 75
    end 
  end  # PDFReport
end # ReportController

# Dummy Test
ReportController.render_pdf({
  :file => "/tmp/output.pdf", 
  :data => table
})

Personalizzare i template

Con la definizione di template propri è possibile specificare delle opzioni per la formattazione dei nostri report. La creazione di un nuovo template avviene usando il metodo Ruport::Formatter::Template.create mentre, per la scelta del template da usare in fase di rendering, si specifica l'opzione :template quando si crea l'output. È anche possibile estendere un template figlio a partire dal padre.

Facciamo un esempio di quanto appena detto, definiamo un template che chiamiamo simple.

Listato 10. Definire un template

Ruport::Formatter::Template.create(:simple) do |format|

  format.page = { :layout => :landscape, :size => "LETTER" }      
  format.text = { :font_size => 12, :justification => :center}
end

Ora vediamo come estedere il template simple

Listato 11. Estendere un template

Ruport::Formatter::Template.create(:derived, :base => :simple) do |format|
  
  format.table = { :font_size => 10, :heading_font_size => 10, 
                   :maximum_width => 720, :width => 720 }      
  format.grouping = { :style => :separated }
  format.column = { :alignment => :right }
  format.heading = { :alignment => :right, :bold => true }
end

class Ruport::Formatter::Text
  def apply_template
    options.note = template.note
  end
end

...
puts AlbumController.render_text(:data => data, :template => :simple)
puts AlbumController.render_text(:data => data, :template => :derived)

Documatic - report in OpenOffice

Si tratta di un progetto indipendente che fornisce un'utile estensione OpenDocument a Ruport. Come si legge dalla home page del progetto, la versione attuale (0.2.0) supporta i formati testo (odt) e foglioelettronico (ods).

L'utilizzo di base è molto semplice. Basta creare un template con OpenOffice come quello in figura, preso dalla sezione tutorial del sito del progetto

Figura 1. Esempio di template OpenDocument
Esempio di template OpenDocument

ed uno script ruby che si occupa di creare i dati, richiamare il template e passargli i dati.

Listato 5. Popolare il template con i dati

require 'rubygems'
require 'documatic'

# definizione della tabella
....

# utilizziamo Documatic
# per generare un OpenDocument in formato testo
table.to_odt_template(:template_file => 'template/cd_collection.odt',
                      :output_file   => 'output/cd_collection.odt')
                      
# ed un foglio elettronico
table.to_ods_template(:template_file => 'template/cd_collection.ods',
                      :output_file   => 'output/cd_collection.ods')

Rope

Rope è un tool che offre una serie di semplici utilities per la generazione di progetti di reportistica. Genera la struttura di base di una applicazione Ruport, fornendo un valido aiuto agli sviluppatori. Viene installato automaticamente con l'installazione di ruport-util.

Per iniziare con un nuovo progetto basta lanciare il comando $rope testprj (pressoché l'equivalente di quanto fa il comando rails a livello di struttura del progetto).

$ rope testprj

  creating directories..
      testprj/test
      testprj/config
      testprj/output
      testprj/data
      testprj/data/models
      testprj/lib
      testprj/lib/reports
      testprj/lib/controllers
      testprj/sql
      testprj/util
  creating files..
      testprj/lib/reports.rb
      testprj/lib/helpers.rb
      testprj/lib/controllers.rb
      testprj/lib/templates.rb
      testprj/lib/init.rb
      testprj/config/environment.rb
      testprj/util/build
      testprj/util/sql_exec
      testprj/Rakefile
      testprj/README
  • Utilities
    • build : un tool per la generazione di report e delle estensioni per il sistema di formatting
    • sql_exec: un semplice tool per ottenere risultati da un file SQL (possibilmente con ERB)
    • Rakefile: script noto per l'automatizzazione di task che riguardano il progetto.
  • Directories
    • test : gli unit test sono salvati qui
    • config : contiene i file di configurazione
    • reports : i report auto-generati vengono salvati qui
    • controllers : le estensioni del sistema di formating risiedono qui
    • models : contiene i modelli ActiveRecord auto-generati
    • sql : i file SQL possono essere salvati qui (sono preprocessati da ERB)
    • util : contiene tool in relazione con rope

Il file config/environment.rb contiene le configurazioni per il progetto generato con rope (es. database, mailer).

Generazione di un modello con rope

$ util/build model album
model file: data/models/album.rb
class name: Album
    
$ cat data/models/album.rb
class Album < ActiveRecord::Base
  acts_as_reportable
end

Conclusioni

L'utilizzo di Ruport e delle sue estensioni rende la generazione di report particolarmente semplice. In definitiva si tratta di un tool ben fatto, estendibile e ben documentato.

Ti consigliamo anche