O'Reilly    
 Published on O'Reilly (http://oreilly.com/)
 See this if you're having trouble printing code examples


Ruport: Business Reporting for Ruby

by Gregory Brown and Michael Milner
04/08/2008

Many of us want to work in Ruby, with or without Rails, because it is fun. For many tasks, this is true. However, when you think of business reporting, what feelings arise? If you're like us, you might feel a bit of dread and misery. Reporting tasks tend to involve malleting data from any number of places into any number of formats, as quickly as possible. The response to all your hard work usually comes in the form of "OK, that's a decent start, but can you make this text bold and blue, and oh… we need to be able to download the report to Excel, too."

Though we won't claim that Ruby Reports (Ruport) will make your reporting tasks fun, we think it manages to make them a whole lot more tolerable. Ruport doesn't try to write your reports for you, it just provides you a solid foundation to start with. Using Ruport as a basis for your reporting applications will help you keep your code clean and organized, and keep you from going postal the next time someone asks you for a printable version of your finely crafted in-browser report.

This article is meant to give you a taste of what Ruport can do for you. We've put together the beginnings of a simple application called Bibliophile that manages book lists. By following through the examples here and playing with the source code for Bibliophile, you should be able to get a feel for what it's like to work with Ruport.

Before we dive into report generation, we will cover how to get Ruport up and running on Rails. Although it's a little more involved than a simple plugin install, it is still a very easy process.

Installation and Configuration

The first thing you need to do is install Ruport. The easiest way to do that is to install the gem.

gem install ruport

You'll also want to install Ruport's acts_as_reportable module, since it provides a connection with ActiveRecord for data collection.

gem install acts_as_reportable

That should provide everything you need to start working with Ruport. There are some other packages that provide additional functionality outside the core of Ruport, but you won't need them to get started.

Once you have everything installed, you can begin to use Ruport in your Rails projects. We're going to create a Rails project called Bibliophile that will allow us to store information about a book collection. After you create the application structure using the standard rails bibliophile command, you'll need to load Ruport in the environment.rb file to make it available. The most reliable way to do so, and be sure that Ruport is loaded at the proper time, is to add the code to require Ruport to a config.after_initialize block. The relevant section of the configuration file is shown below.

  Rails::Initializer.run do |config|
    config.after_initialize do 
      require "ruport" 
    end
  end

Now that Ruport is loaded, it will be available from within your Rails project. Note that although we also want to use the acts_as_reportable module, we don't need to specifically require it since loading Ruport will automatically attempt to load acts_as_reportable also.

When we begin to create reports, we'll need to decide on a location to put the files that will contain the reports' code. Rails, of course, establishes conventions for the directory structure and file locations for all of the files it uses, but not for the files that Ruport will need. Although it's ultimately personal preference, the Ruport community generally uses an app/reports directory to hold all reporting code. So, if you're following along, you should create that directory now, since we'll be using it later.

The final configuration step is to make the newly created app/reports directory visible to the Rails project. To do so, we add a directive to the configuration section of the environment.rb file, adding the new directory to the load path.

  config.load_paths += %W( #{RAILS_ROOT}/app/reports )

That's all there is to installing Ruport and hooking it up to a Rails project. Next, we'll examine how Ruport obtains the data that will be presented in the reports.

Data Collection Using acts_as_reportable

Before we get to generating a full report, we should take a look at data collection using Ruport. While it's possible to add free text to a report without needing any kind of data collection mechanism, in most cases you're probably going to want to present some type of tabular data as part of your finished report. Given this need, Ruport provides a set of data structures that allow you to work with tabular data easily and efficiently.

The core data structure is the Ruport::Data::Table. For the simplest case, you need to populate a Table with the data you want to present in your report. The purpose of the acts_as_reportable module is to provide an easy way to populate a Ruport Table from the data represented by an ActiveRecord Model.

For now, we're going to use the Rails console to demonstrate how to use acts_as_reportable for data collection, and we're going to use some of the built-in mechanisms provided by Ruport to output the data in a text format. Later, we'll demonstrate how to use Ruport's formatting system to customize the output.

First, we're going to need some models. Since our application is called Bibliophile, it seems apparent that we're going to need models for books and authors. The following are the migrations to create the books and authors tables.

  class CreateBooks < ActiveRecord::Migration
    def self.up
      create_table :books do |t|
        t.string :name
        t.string :description
        t.string :isbn
        t.string :status
        t.integer :author_id
        t.integer :pages
        t.integer :genre_id
      end
    end

    def self.down
      drop_table :books
    end
  end

  class CreateAuthors < ActiveRecord::Migration
    def self.up
      create_table :authors do |t|
        t.string :name
        t.timestamps
      end
    end

    def self.down
      drop_table :authors
    end
  end

In order to hook up the models to Ruport, you just add the acts_as_reportable line to each of the model definitions.

  class Book < ActiveRecord::Base
    acts_as_reportable
    belongs_to :author  
  end

  class Author < ActiveRecord::Base
    acts_as_reportable
    has_many :books
  end

This will provide each of the models with a class method called report_table that you can use to directly create a Ruport Table from the model. From the Rails console, let's create some data, so we can see how this works.

  >> Author.create(:name => 'Umberto Eco')
  >> Author.create(:name => 'William Gaddis')
  >> Author.create(:name => 'Thomas Hardy')
  >> Author.create(:name => 'Ben Okri')

Now that we have a few authors, we can see how easy it is to create a Ruport table. You just call the report_table method and the table is created for you. In order to see the structure of the resulting table, we'll use the default text output provided by Ruport.

>> puts Author.report_table
+----------------------------------------------------------------------------->>
|      name      |           updated_at           | id |           created_at >>

+----------------------------------------------------------------------------->>
| Umberto Eco    | Mon Mar 31 23:05:39 -0400 2008 |  1 | Mon Mar 31 23:05:39 ->>
| William Gaddis | Mon Mar 31 23:05:58 -0400 2008 |  2 | Mon Mar 31 23:05:58 ->>
| Thomas Hardy   | Mon Mar 31 23:06:07 -0400 2008 |  3 | Mon Mar 31 23:06:07 ->>
| Ben Okri       | Mon Mar 31 23:06:22 -0400 2008 |  4 | Mon Mar 31 23:06:22 ->>
+----------------------------------------------------------------------------->>

This is very easy, but in a real report, you might not want to show all of the columns. In fact, in the authors table, you might only care to show the authors' names. You can customize the output by supplying various options to the report_table method. You should think of this method as if it were an ActiveRecord.find method, with a few extra options used by acts_as_reportable. When you include any options, you need to specify, as the first parameter, whether you want :all or :first (:all is the default if you don't supply any parameters).

Some of the options allow you to specify which columns to include in the resulting table. The :only option specifies that only the columns specified should be included and the :except option specifies that all columns except those listed should be included. In either case, you supply the name of a column or any array of column names.

>> puts Author.report_table(:all, :only => 'name')
+----------------+
|      name      |
+----------------+
| Umberto Eco    |
| William Gaddis |
| Thomas Hardy   |
| Ben Okri       |
+----------------+

Notice also, that if you supply an array of column names to the :only option, the columns will be ordered according to their order in the array.

>> puts Author.report_table(:all, :only => ['id','name'])
+---------------------+
| id |      name      |
+---------------------+
|  1 | Umberto Eco    |
|  2 | William Gaddis |
|  3 | Thomas Hardy   |
|  4 | Ben Okri       |
+---------------------+

You can also use all of the options available in a normal ActiveRecord.find.

>> puts Author.report_table(:all, :only => ['id','name'], :order => 'authors.name')
+---------------------+
| id |      name      |
+---------------------+
|  4 | Ben Okri       |
|  3 | Thomas Hardy   |
|  1 | Umberto Eco    |
|  2 | William Gaddis |
+---------------------+

If you want to combine the output from multiple associated models, you can use the :include option. You can also nest options to the included models using hashes, allowing you to customize all of the data in the table. We'll first need to create some books, so we can see how to combine related models into one table.

>> Book.create(:name => 'Baudolino', :author_id => 1, :pages => 521)
>> Book.create(:name => 'The Famished Road', :author_id => 4, :pages => 500)

>> Book.create(:name => 'The Recognitions', :author_id => 2, :pages => 956)
>> Book.create(:name => 'The Return of the Native', :author_id => 3, :pages => 418)

The simplest use of the :include option is to just name the model to be included.

>> puts Book.report_table(:all, :only => 'name', :include => :author)
+----------------------------------------------------------------------------->>
|           name           | author.id |       author.created_at        |     >>
+----------------------------------------------------------------------------->>

| Baudolino                |         1 | Mon Mar 31 23:05:39 -0400 2008 | Mon >>
| The Famished Road        |         4 | Mon Mar 31 23:06:22 -0400 2008 | Mon >>
| The Recognitions         |         2 | Mon Mar 31 23:05:58 -0400 2008 | Mon >>
| The Return of the Native |         3 | Mon Mar 31 23:06:07 -0400 2008 | Mon >>
+----------------------------------------------------------------------------->>

In order to fully customize the output, you may need to supply options to the included models as well. Note that the names of the columns returned from the included models are qualified with the name of the model.

>> puts Book.report_table(:all, :only => 'name', :include => { :author => { :only => 'name' } })
+-------------------------------------------+
|           name           |  author.name   |
+-------------------------------------------+
| Baudolino                | Umberto Eco    |
| The Famished Road        | Ben Okri       |
| The Recognitions         | William Gaddis |
| The Return of the Native | Thomas Hardy   |
+-------------------------------------------+

You should now have a basic idea of how to create a Ruport table from an ActiveRecord model (or multiple models). There are some other advanced instructions that acts_as_reportable understands, which you can explore on your own. For now, we'll move on to show you how to use the data you've just collected to create a formatted report.

Report Formatting

Ruport takes a very organized approach to formatting your reports. Rather than mixing your data manipulation code with your format specific code, we maintain a clean separation that is quite similar to the MVC pattern in Rails.

Like Rails, Ruport has controllers that act as the go-between for your data and the code that will ultimately format it. The best way to see how this works is by example, so let's build upon what we've gone over in the last section and produce a simple Book list. We'll add other formats later, but for now, we can start with HTML.

The following code implements a simple report that displays the title, author, and number of pages for all the books in the Bibliophile application.

app/reports/book_report.rb

class BookReport < Ruport::Controller

  stage :list

  def setup
    self.data = Book.report_table(:all, :include => { :author => { :only => ["name"] } },
                                         :only => ["name", "author.name", "pages"],
                                         :order => "books.name")
    data.rename_columns("name" => "Title", "author.name" => "Author")
  end

  formatter :html do
    build :list do
      output << textile("h3. Book List")
      output << data.to_html
    end
  end

end

Using script/console, we can actually run this report:

>> puts BookReport.render_html
<h3>Book List</h3>      <table>
                <tr>

                        <th>Title</th>
                        <th>Author</th>
                        <th>pages</th>
                </tr>

                <tr>
                        <td>Baudolino</td>
                        <td>Umberto Eco</td>
                        <td>521</td>

                </tr>
                <tr>
                        <td>The Famished Road</td>
                        <td>Ben Okri</td>
                        <td>500</td>

                </tr>
                <tr>
                        <td>The Recognitions</td>
                        <td>William Gaddis</td>
                        <td>956</td>

                </tr>
                <tr>
                        <td>The Return of the Native</td>
                        <td>Thomas Hardy</td>
                        <td>418</td>

                </tr>
        </table>

The resulting HTML is quite vanilla, and shouldn't come as a surprise to anyone. A little later on, we'll show you how to wire up your Rails controllers and views to display this report, but for now, let's take a closer look at what Ruport is doing here.

Ruport controllers work by processing reports in stages, which are defined by your formatters. In this very simple report, there is only a single stage our formatters can build, the list. The code that defines this is shown below:

  stage :list

With this in mind, the formatter code is probably a little clearer:

  formatter :html do
    build :list do
      output << textile("h3. Book List")
      output << data.to_html
    end
  end

It's clear that the HTML formatter gets the code inside the build block executed when BookReport.render_html, but it might be a little tougher to figure out where things like the output and textile methods are coming from.

To simplify things a bit, we can unravel Ruport's syntactic sugar and take a look at how this comes together in plain old Ruby objects. This report could easily be re-written like this:

class BookReport < Ruport::Controller

  stage :list

  def setup
    self.data = Book.report_table(:all, :include => { :author => { :only => ["name"] } },
                                         :only => ["name", "author.name", "pages"],
                                         :order => "books.name")
    data.rename_columns("name" => "Title", "author.name" => "Author")
  end

  class HTML < Ruport::Formatter::HTML

    renders :html, :for => BookReport

    def build_list
      output << textile("h3. Book List")
      output << data.to_html
    end
  end

end

When we write it this way, it becomes clear that Formatter objects in Ruport are actually separate entities from the controllers, sharing only the details they need to. Specifically, the only two bits of data shared between a Controller and a Formatter are the data and options attributes.

The code that allows this to happen is shown below:

  renders :html, :for => BookReport

This line tells the BookReport controller that our subclass of Ruport's HTML formatter will handle the HTML output for that controller. This tells the controller that when BookReport.render_html is called, this object will be the one it executes its stages on.

In this code, we find that the build method we used before with a block is nothing more than syntactic sugar that produces vanilla Ruby methods.

  def build_list
    output << textile("h3. Book List")
    output << data.to_html
  end

At the bottom of the chain here, we find that there is little magic to be found and we can come up with a nice summary of how Ruport's formatting system works in the context of this report.

When BookReport.render_html is called, the following steps are taken, in order:

  1. BookReport looks for the Formatter registered as :html.
  2. The setup method is run, allowing data and options to be tweaked as needed.
  3. Stages are run in the order they are defined.
  4. The output of the formatter is returned.

Though this process is a bit more complex for advanced uses of the formatting system, these steps form the core of what happens when you render a report in Ruport.

Now that we've covered how the system actually works, let's take a quick look at adding CSV and PDF support to this report.

  formatter :pdf do
    build :list do
      pad(10) { add_text "Book List" }
      draw_table data
    end
  end

  formatter :csv do
    build :list do
      output << data.to_csv
    end
  end

The CSV format is generic as they come, as you can see, Ruport's tables have built in formatting support, and for the CSV, we just want a simple dump of the data.

Taking a look via script/console, you'll see that's exactly what we get.

>> puts BookReport.render_csv 
Title,Author,pages
Baudolino,Umberto Eco,521
The Famished Road,Ben Okri,500
The Recognitions,William Gaddis,956
The Return of the Native,Thomas Hardy,418

Our PDF output is only slightly more interesting. The first line adds some text to the document with a padded margin at the top and bottom:

  pad(10) { add_text "Book List" }

The second line may be a bit surprising, because you may have expected something like output << data.to_pdf. However, PDF is not a streaming data format like HTML, text, or CSV, so we need to use a special helper to draw the table on the PDF canvas.

  draw_table data

Other than that, the code is very basic. Ruport's controllers support rendering to a file, so we can do that to get our PDF back:

>> BookReport.render_pdf(:file => "books.pdf")

The output looks something like this:

figure

The output looks fairly generic, and it'd probably be nicer to have the table heading centered over the table and in a larger font. Though this is possible to do with the low level PDF formatter helpers, we'll instead look at a higher level system in Ruport known as formatter templates.

Formatter Templates

Templates are meant to provide some abstraction by separating default formatting values from the individual formatters. Though formatters in Ruport ultimately can choose what, if any, template options to implement, the general goal of templates it to provide a normalized interface to your formatting options.

Templates are simple to create, so we'll take a quick look at how to use them to manipulate the current PDF report. The following template centers and increases the size of text drawn by add_text.

app/reports/templates.rb

Ruport::Formatter::Template.create(:default) do |format|
  format.text = {
    :font_size => 16,
    :justification => :center
  }
end

To get this to work, we'll need to add an explicit require in our environment.rb, since it won't be possible to use class lookups to autoload the file.

  require "app/reports/templates"

Once this is set, without any changes to the underlying code, you can regenerate a PDF that looks like this:

figure

It's worth noting that this template will be used by all your controllers by default. If you find that's not what you want, you can give your template a name, and then refer to it by name when rendering your output.

# in your templates file
Ruport::Formatter::Template.create(:book_list) do |format|
  # ...
end

# when you render output
BookReport.render_pdf(:template => :book_list)

You can even derive a template from another template:

Ruport::Formatter::Template.create(:small_centered, :base => :default) do |format|
  format.text.merge!(:font_size => 10)
end

Finally, when working with templates, if you want to be sure that your controller ignores all templates, you can do so:

BookReport.render_pdf(:template => false)

Ruport's formatters support a wide range of templating options, which you can find by browsing the API documentation.

This gives you just a taste of templates, but they're worth knowing about. You can build your own custom formatters in Ruby, and by implementing hooks that understand some templating options, you can make things quite flexible.

We'll stick to the basics for now though, so let's move on to getting this report working in your Rails application.

Wiring Up Your Rails Controllers and Views

Now that we have a basic report, we can see how you might generate it from a Rails controller. Perhaps the easiest of the formats to integrate with your Rails project is HTML. Since the report will produce HTML output, you can just insert it into one of the views wherever you want it.

Let's create an index action in our Rails controller and just generate the HTML report, saving it to an instance variable.

  class BooksController < ApplicationController
    def index
      @book_report = BookReport.render_html
    end
  end

Then, the corresponding view can be as simple as:

  <%= @book_report %>

Our view in Bibliophile is only slightly more complicated, mostly due to styling. You can see that it centers around the same basic report though:

figure

Generating the other formats isn't much harder, but you need to consider how you'll return the requested data. You won't be able to just render it directly, so you need to use the Rails send_data method to stream the results to the browser. The report itself, however, is generated in the same way, just substituting the appropriate format.

Let's take a look at how you might generate printable PDF output. We can add another method to the controller in order to create the PDF.

  class BooksController < ApplicationController
    def printable_list
      pdf = BookReport.render_pdf
      send_data pdf, :type => "application/pdf",
                     :filename => "books.pdf" 
    end
  end

You can see that we render the PDF report in the same manner as we did the HTML report. However, in this case, we save the results to a variable and then supply those results as the data for the send_data method. We also specify the content type and filename. Other than that, you just need to add a link to this method to be able to generate a PDF version of the report.

Just to be thorough, here is the controller action to generate the CSV output from the report. It follows the same pattern as the others.

  class BooksController < ApplicationController
    def csv_list
      csv = BookReport.render_csv
      send_data csv, :type => "text/csv",
                     :filename => "books.csv" 
    end
  end

Integrating report generation into Rails controllers is as simple as that and in many cases, that will be all you need.

Filtering Report Data

If you looked closely at the screenshot of Bibliophile, you might have noticed that the report could be filtered by author names. This kind of functionality is an extremely common need with Rails based reporting, ranging from something as simple as filtering based on a single field to building full blown query generators to narrow down your reports.

Though this particular example represents the most simple case, the general pattern can be built upon to implement arbitrarily complex filtering systems.

The feature requires changes to both the Rails controller and the BookReport. Let's take a look at the Ruport code first, since it's the interesting part:

class BookReport < Ruport::Controller

  stage :list

  def setup
    conditions = ["authors.id = ?", options.author] unless options.author.blank?
    self.data = Book.report_table(:all, :include => { :author => { :only => ["name"] } },
                                         :only => ["name", "author.name", "pages"],
                                         :order => "books.name",
                                         :conditions => conditions)
    data.rename_columns("name" => "Title", "author.name" => "Author")
  end

  formatter :html do
    build :list do
      output << textile("h3. Book List")
      output << data.to_html
    end
  end

  formatter :pdf do
    build :list do
      pad(10) { add_text "Book List" }
      draw_table data
    end
  end

  formatter :csv do
    build :list do
      output << data.to_csv
    end
  end
end

Looking at the modifications to the Ruport, you can see very little has changed. Since the formatters all use the data provided by the BookReport controller, they have not changed. The only new code is in setup(), which is simply creating some conditions that will be passed back to the underlying ActiveRecord#find call. Perhaps the only surprising thing is that the report is now referencing an options.author attribute.

A quick dance with script/console should shed light on this:

>> Author.find(1).name
=> "Umberto Eco" 
>> puts BookReport.render_csv(:author => 1)
Title,Author,pages
Baudolino,Umberto Eco,521

As you can see from the example above, Ruport takes any option passed in at rendering time and assigns them to an options object. The only exceptions are the few special keywords, such as :file, :data, and :template.

This turns out to be extremely useful, because it allows arbitrary options to be passed to your controllers and formatters. In case you were curious, it's worth noting that these values can also be accessed in a hash like manner, such as options[:author].

We already have a working filtering mechanism, so all that remains is to get it working within the context of Rails:

class BooksController < ApplicationController
  def index
    session[:author] = params[:author]
    @book_report = render_book_list_as :html
    @authors = Author.find(:all)
  end

  def printable_list
    pdf = render_book_list_as :pdf
    send_data pdf, :type => "application/pdf",
                   :filename => "books.pdf" 
  end

  def csv_list
    csv = render_book_list_as :csv
    send_data csv, :type => "text/csv",
                   :filename => "books.csv" 
  end

  protected

  def render_book_list_as(format)
    BookReport.render(format, :author => session[:author])
  end

end

As you can see, the change here is nothing fancy. The main index page which shows the HTML report persists the selected author in the dropdown menu in the session. This value is then passed on when any of the HTML, CSV, or PDF formats are rendered. We have created a simple helper method to avoid needless duplication, but the code is otherwise the same as before.

Filtering obviously can get more complex than this. We won't cover it here, as it tends to be more Rails code than Ruport, but it's worth mentioning that acts_as_reportable supports some additional options that might be useful for implementing data filters. If you're working on this kind of task, be sure to look at the documentation report_table's :filters and :transforms options.

Just the Tip of the Iceberg

In the interest of keeping things simple and easy to approach, we've not gone into many of Ruport's advanced features in this article. We have definitely shown all of the major components of the system, but have glossed over most of the advanced features, especially those that are a little bit specialized in purpose.

We hope that the simple examples here have offered a taste of what Ruport can do for you, and given you a starting point for continuing to explore its possibilities. If you found this interesting, definitely consider browsing Ruport's API documentation or taking a look through the Ruport Book, which has a free HTML version available for online browsing.

Finally, don't hesitate to get involved! Our mailing list is one of the best resources for learning the software, and we welcome users to come join our community and help us make Ruport better.

Please enjoy working with Ruport, and Happy Hacking!

Gregory Brown is a New Haven, CT based Rubyist who spends most of his time on free software projects in Ruby. He is the original author of Ruby Reports.

Michael Milner is an active member of the Ruby community. He is currently the lead developer for the Ruport project and also maintains the Ruport/Rails plugin that provides Rails integration for Ruport. He works professionally using Ruby and has developed large web applications using Rails


Return to O'Reilly Ruby.

Copyright © 2009 O'Reilly Media, Inc.