Ruport
06 Jan 2007NOTE: THIS GUIDE IS FOR RUPORT 0.7. AND IT IS NOW OUT OF DATE.
There is an updated version of this article available on the official ruport website.
Ruport is a lightweight reporting framework for Ruby. It’s focus is on providing a structure for your report definitions, not on providing a high level language to build the layout of your reports. This means that there are no methods for adding a heading, table or graph to the your report. The developers feel it is important for users of the framework to have flexibility in choosing an output format (pdf, text, html, jpg, svg, smoke signals), and wrapping all those formats with higher level functions is a significant challenge that hasn’t yet been solved.
There are 3 stages to report that Ruport can handle - collecting data, manipulating data and formatting reports. All three steps can be used independently, and in this article I’m focusing exclusively on the formatting reports - I’ll leave working with data for another day.
Ruport 0.7 was released on 25th December 2006, and with it came a brand new system of defining output formats. This article is intended to be an introduction to using this new system.
As a contrived example, I will run through the definition and use of a report listing book sales for the month of December. All code and sample output is available to download at the end of the article. If at the end you have any questions, feel free to contact me.
Installing
Ruport is available as a gem, and assuming you have the rubygems library installed, can be installed using this command:
gem install ruport
If prompted, accept any extra required dependencies.
Report Definition
Generally, each report is made up of at 2 or more classes - 1 for the definition, and 1 for each desired output format. I recommend all these classes are kept in the same file for manageability.
The first class defines the report and the data required to build it - it is important that keep this data as format independent as possible. For our sales report, something like this might be appropriate:
require 'rubygems'
require 'ruport'
module MyReports
class SalesReport < Ruport::Renderer
include Ruport::Renderer::Helpers
def run
build [:header,:body,:footer],:document
finalize :document
end
def report_title=(t)
options.report_title=(t)
end
def titles=(t)
options.titles=(t)
end
end
end
This defines the following facts about our sales report:
- There are 4 stages required to build the report: a header, a body, a footer and finalizing (rendering).
- There are 2 pieces of data required to build the report: a list of titles and a report title. You can define as many options like this as you wish.
Format Definition
On it’s own, the report definition listed above won’t do much - we need to tell ruport how to render it into the required format.
To begin with, we will create a text version of the report. Something like the following placed immediately after the first class should work nicely:
module MyReports
class SalesReportText < Ruport::Format::Plugin
SalesReport.add_format self, :txt
def pad(str, len)
return "".ljust(len) if str.nil?
str = str.slice(0, len) # truncate long strings
str.ljust(len) # pad with whitespace
end
def build_document_header
unless options.report_title.nil?
output << "".ljust(75,"*") << "\n"
output << " #{options.report_title}\n"
output << "".ljust(75,"*") << "\n"
output << "\n"
end
end
def build_document_body
# table heading
output << pad("isbn", 15) << "|" << pad("title",30) << "|"
output << pad("author", 15) << "|" << pad("sales", 10) << "\n"
output << "".ljust(75,"#") << "\n"
# table data
options.titles.each do |title|
output << pad(title["isbn"],15) << "|"
output << pad(title["title"],30) << "|"
output << pad(title["author"],15) << "|"
output << pad(title["sales"].to_s,10) << "\n"
end
output << "".ljust(75,"#") << "\n"
end
def build_document_footer
# do nothing, we won't bother with a footer at this stage
end
def finalize_document
# text doesn't need any special rendering, so just return the output
output
end
end
end
The first line of this class registers this output format with our report definition - this allows us to define as many different output formats for each report as we wish.
The pad function is a simple formatting function to simplify our work with strings.
The next 3 functions are called to build the report. Notice the function names follow a particular style - these names are important and are a direct result of the “build” line in our first class and will be called in the order we specified in the definition.
The finalize function is also named to match the “finalize” line in the report definition, and returns the entire report.
Using it
Now that our report is defined with at least 1 output format, we can use it in our application. One important thing to point out is that although Ruport contains it’s own Array-like class that makes managing your data easier, I haven’t used it in this example. Ruport’s Table class would be perfect for storing our book sales data, however I wanted to focus on building your report. Maybe next time.
Assuming the report definition is in a file called salesreport.rb, the following code should be placed in app.rb in the same directory:
require "salesreport"
book1 = {"isbn" => "978111111111", "title" => "Book Number One", "author" => "me", "sales" => 10}
book2 = {"isbn" => "978222222222", "title" => "Two is better than one", "author" => "you", "sales" => 267}
book3 = {"isbn" => "978333333333", "title" => "Three Blind Mice", "author" => "John Howard", "sales" => 1}
book4 = {"isbn" => "978444444444", "title" => "The number 4", "author" => "George Bush", "sales" => 1829}
books = [book1, book2, book3, book4]
text = MyReports::SalesReport.render_txt do |e|
e.report_title = "December 2006 Sales Figures"
e.titles = books
end
File.open("dec_sales.txt", "w") { |f| f.write text}
Once the sample data has been built, the report itself is generated with a single block. Using this approach, building the report within your app is limited to a few simple lines, hiding all formatting complexity.
Adding PDF
Sure text is fine in many situations (ie. emailing the report to a co-worker), but these days PDF is becoming the format of choice for many people. How do we add it as an option for our sales report?
As mentioned earlier, Ruport won’t try to abstract any of the complexities of formatting your report. The default library for generating PDFs in Ruport is PDF:Writer, and you will need to get your hands dirty with the foibles of this library to make your PDF. The following code placed inside salesreport.rb should get you started.
module MyReports
class SalesReportPDF < Ruport::Format::PDF
SalesReport.add_format self, :pdf
def add_title( title )
width = 200
height = 20
top_left_x = pdf_writer.absolute_right_margin - width
top_left_y = pdf_writer.absolute_top_margin
radius = 5
font_size = 12
loop = true
title = "<b>#{title}</b>"
while ( loop == true )
sz = pdf_writer.text_width( title, font_size )
if ( (top_left_x + sz) > top_left_x+width )
font_size -= 1
else
loop = false
end
end
pdf_writer.fill_color(Color::RGB::Gray80)
pdf_writer.rounded_rectangle(top_left_x, top_left_y, width, height, radius).fill_stroke
pdf_writer.fill_color(Color::RGB::Black)
pdf_writer.stroke_color(Color::RGB::Black)
pdf_writer.text( title, :absolute_left => top_left_x,
:absolute_right => top_left_x + width,
:justification => :center,
:font_size => font_size)
end
def build_document_header
add_title( options.report_title ) unless options.report_title.nil?
move_cursor_to(pdf_writer.y - 50) # padding
end
def build_document_body
::PDF::SimpleTable.new do |table|
table.maximum_width = 500
table.orientation = :center
table.data = options.titles
table.column_order = %w[isbn title author sales]
table.render_on(pdf_writer)
end
end
def build_document_footer
# do nothing, we won't bother with a footer at this stage
end
def finalize_document
output << pdf_writer.render
end
end
end
The structure of this basically the same as the one that defined the text version, with 2 critical differences:
- It extends Ruport PDF formatting class, which provides and instance of PDF:Writer as pdf_writer
- The class registers itself to our SalesRreport class with a different format (:pdf)
So what changes do we have to make to our application to generate the pdf instead? Leave the sample data definition the same, just modify the remaining lines like so:
pdf = MyReports::SalesReport.render_pdf do |e|
e.report_title = "December 2006 Sales Figues"
e.titles = books
end
File.open("dec_sales.pdf", "w") { |f| f.write pdf}
- call render_pdf instead of render_txt
- modify the text variable name for readability
- save the file with a pdf extension.
Switching output format’s within your app according to user preference or whatever would be a piece of cake.