There are three stages in the reporting process that Ruport can handle - collecting data, manipulating data and formatting. All three can be used independently, and in this article we’ll focusing exclusively on formatting. As a contrived example, we’ll run through the definition and use of a report listing book sales.
Generally, each report is made up of two or more classes – one for the definition (the renderer), and one for each desired output format (formatters). The renderer defines the stages and the data required to build the report – it is important to keep this data as format independent as possible. For our sales report, something like this might be appropriate:
require 'rubygems'
require 'ruport'
class SalesReport < Ruport::Renderer
required_option :titles
option :report_title
stage :document_header, :document_body, :document_footer
finalize :document
end
This defines the following facts about our sales report:
This class should be a descendant of Ruport::Renderer, which defines a number of
methods to help you create your definition. Data needed to build your report can
be specified using the option or required_option methods. The stages
involved in building your report can be specified using the prepare, stage,
and finalize methods. As we’ll see, these define hooks that can be used in
your format definitions.
On its own, the renderer 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:
class SalesReportText < Ruport::Formatter
renders :text, :for => SalesReport
opt_reader :titles, :report_title
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_row(items, pads)
items.each_with_index do |item, i|
output << pad(item, pads[i]) << "|"
end
output.chop!
output << "\n"
end
def build_document_header
if report_title
output << "".ljust(75,"*") << "\n"
output << " #{report_title}\n"
output << "".ljust(75,"*") << "\n"
output << "\n"
end
end
def build_document_body
# table heading
build_row(['isbn', 'title', 'author', 'sales'], [15, 30, 15, 10])
output << "".ljust(75,"#") << "\n"
# table data
titles.each do |title|
build_row([title["isbn"], title["title"], title["author"],
title["sales"].to_s], [15, 30, 15, 10])
end
output << "".ljust(75,"#") << "\n"
end
end
The first line of this class registers this output format with our renderer -
this allows us to define as many different output formats for each renderer as
we wish. The second line, the call to opt_reader, gives us attribute-like
access to our options.
The pad function is a simple formatting function to simplify our work with
strings and the build_row function is a helper to construct each row of the
output.
The next two functions, build_document_header and build_document_body, 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 stages defined by our
renderer. They will be called in the order we specified in the renderer. The
finalize_document function is also named to match the “finalize” line in the
renderer.
Note that we don’t have to include all of the functions specified in the
definition. In this case, we haven’t defined build_document_footer or
finalize_document and if we don’t define a particular function, it simply
won’t be called while building the report.
Now that our report is defined with at least one output format, we can use it in our application. One important thing to point out is that although Ruport contains its own Array-like class that makes managing your data easier, we haven’t used it in this example. Ruport’s Table class would be perfect for storing our book sales data, however we wanted to focus on building your report.
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]
report = SalesReport.render_text do |e|
e.report_title = "December Sales Figures"
e.titles = books
end
File.open("dec_sales.txt", "w") { |f| f.write report }
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 only requires a few simple lines, hiding all formatting complexity.
The output for this report is shown below
*************************************************************************** December Sales Figures *************************************************************************** isbn |title |author |sales ########################################################################### 978111111111 |Book Number One |me |10 978222222222 |Two is better than one |you |267 978333333333 |Three Blind Mice |John Howard |1 978444444444 |The number 4 |George Bush |1829 ###########################################################################
Sure text is fine in many situations (e.g. 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.
class SalesReportPDF < Ruport::Formatter::PDF
renders :pdf, :for => SalesReport
opt_reader :titles, :report_title
def add_title( title )
rounded_text_box("<b>#{title}</b>") do |o|
o.fill_color = Color::RGB::Gray80
o.radius = 5
o.width = options.header_width || 200
o.height = options.header_height || 20
o.font_size = options.header_font_size || 12
o.x = pdf_writer.absolute_right_margin - o.width
o.y = pdf_writer.absolute_top_margin
end
end
def build_document_header
pad_bottom(50) { add_title(report_title) } if report_title
end
def build_document_body
draw_table Table(:column_names => %w[isbn title author sales],
:data => titles), :maximum_width => 500
end
def finalize_document
render_pdf
end
end
The structure of this is basically the same as the one that defined the text version, with two critical differences:
Ruport’s built-in formatting class does offer some methods to help you with your PDF formatting and we used a couple of them in our example above. The available methods include:
add_text – adds text to your outputcenter_image_in_box – takes an image path and box coordinates and centers within the boxrounded_text_box – draw text surrounded by a rounded-corner boxwatermark – places a centered watermark on each page of your reportmove_cursor – moves cursor specified number of units along the y-axismove_cursor_to – moves cursor to a specified location on the y-axispad – adds a specified amount of space above and below some outputpad_top – adds a specified amount of space above some outputpad_bottom – adds a specified amount of space below some outputdraw_table – uses PDF::SimpleTable to draw a table to outputhorizontal_line – draw a horizontal linevertical_line – draw a vertical lineleft_boundary – get the left boundary of the pageright_boundary – get the right boundary of the pagetop_boundary – get the top boundary of the pagebottom_boundary – get the bottom boundary of the pagecursor – get the current location of the cursor on the y-axisdraw_text – places text at a specified position on the pageSo 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:
report = SalesReport.render_pdf do |e|
e.report_title = "December Sales Figures"
e.titles = books
end
File.open("dec_sales.pdf", "w") { |f| f.write report }
You can take a look at the pretty output here
Switching output formats within your app according to user preference or whatever is a piece of cake.