advertisement

Print

Ruport: Business Reporting for Ruby
Pages: 1, 2, 3

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.