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 Listato 2. Creare e visualizzare una tabella Come accennato precedentemente, i metodi disponibili sulla struttura dati Table sono molti. Vediamone alcuni: 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: Listato 3. Creazione di un Grouping 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. Nel più semplice dei casi possiamo ordinare un Grouping dal nome del gruppo impostando l'opzione oppure ordinandolo in base ad uno specifico blocco: o ancora, grazie al metodo Ruport mette a disposizione del tipo 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 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 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: In Ruport gli step definiti nel Controller prendono il nome di stage. Definendo gli stage, il Per la creazione dei metodi del 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 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: ed infine accenneremo all'uso di rope. Con Per integrare dati di applicazioni RubyOnRails all'interno dei nostri report usiamo il modulo acts_as_reportable che usa i risultati di un Aggiungiamo Il modo più semplice per iniziare ad usare i risultati di una ricerca in DB è aggiungere il metodo Listato 6: acts_as_reportable Riportiamo alcune opzioni disponibili con il metodo report_table: Poiché Equivalente a: Il metodo di Analogamente al metodo Un'altra modalità messa a disposizione da Ruport per collezionare i dati è quella di usare la classe Listato 7. Query La creazione di grafici a partire dai dati presenti nelle strutture base di Ruport richiede l'installazione di È presente una struttura dati apposita per i grafici, nello specifico, Come detto in precedenza, porzioni delle librerie Listato 8. Estensione del modulo Graph Listato 9. Applicare l'estensione al controller 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 Facciamo un esempio di quanto appena detto, definiamo un template che chiamiamo Listato 10. Definire un template Ora vediamo come estedere il template Listato 11. Estendere un template 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 ( L'utilizzo di base è molto semplice. Basta creare un template con OpenOffice come quello in figura, preso dalla sezione tutorial del sito del progetto 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 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 Per iniziare con un nuovo progetto basta lanciare il comando Il file Generazione di un modello con rope 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.Record
e presentano i metodi necessari per lavorare con i dati in esse conte
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 |
+---------------------------------------------------------------+
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])
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
Ruport::Data::Grouping.new
oppure il Kernel method 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
Ordinare le tabelle
puts table.sort_rows_by("year", :order => :ascending)
Ordinare i Grouping
:order
al valore del :name
del gruppo.
g = Grouping(table, :by => "tracks", :order => :name)
puts g.to_a.map {|name,group| name}
g = Grouping(table, :by => "artist", :order => lambda {|g| g.size })
puts g.to_a.map {|name,group| name }
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
Table
il metodo rows_with
, perfetto per le semplici operazioni di ricerca:
table.rows_with("artist") { |a|
if a.size < 11
puts a
end
}
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
# 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 }
Controller
sa che dovrà fare riferimento alle rispettive implementazioni presenti nel Formatter
a lui associato al fine di produrre l'output.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
# 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)
Aggiungiamo il necessario al campo di lavoro
RubyGems
provvediamo all'installazione delle gem necessarie (acts_as_reportable
, ruport_util
, documatic
, pdf_writer_proxy
).
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
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.require 'ruport'
nel model ActiveRecord oppure direttamente nel file di Rails environment.rb
che provvederà a caricare Ruport automaticamente.acts_as_reportable
all'interno della definizione della model class di ActiveRecord. In questo modo si avrà già a disposizione il metodo report_table
.
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)
:only
- seleziona le colonne desiderate:except
- esclude alcune colonne:methods
- in tabella saranno visualizzati anche i risultati della chiamata ad un metodo:include
- include anche i risultati dei modelli associati:filters
- filtra i risultati in base a criteri a nostro piaciemento:transforms
- trasforma i risultati in tabellaacts_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 })
puts album.report_table(:all, :only => [:title], :methods => [:artist_name], :filters => lambda {|r| r["id"] > 1 })
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]
ActiveRecord::Base.find_by_sql
è presente il metodo report_table_by_sql
.Ruport::Query
, che fornisce il supporto all'uso della libreria RubyDBI
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
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
.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
.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.
%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
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
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.simple
.
Ruport::Formatter::Template.create(:simple) do |format|
format.page = { :layout => :landscape, :size => "LETTER" }
format.text = { :font_size => 12, :justification => :center}
end
simple
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
odt
) e foglioelettronico (ods
).
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
ruport-util
.$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
build
: un tool per la generazione di report e delle estensioni per il sistema di formattingsql_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.
test
: gli unit test sono salvati quiconfig
: contiene i file di configurazionereports
: i report auto-generati vengono salvati quicontrollers
: le estensioni del sistema di formating risiedono quimodels
: contiene i modelli ActiveRecord auto-generatisql
: i file SQL possono essere salvati qui (sono preprocessati da ERB)util
: contiene tool in relazione con ropeconfig/environment.rb
contiene le configurazioni per il progetto generato con rope (es. database
, mailer
).
$ 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