In my current Rails projects (Gradesheet & MynaStuff) I have the need to create reports as PDFs. Prawn is a relatively new library that does just what I need in pure Ruby. There are a couple of really good tutorials on how to integrate Prawn into your Rails project on the web but I wanted to do something a little different. There's even a plugin (PrawnTo) that makes Prawn look like a native Rails construct and allows you build PDFs without leaving your views. Neat!

However, this is not really how I wanted to implement reporting in my applications. So I created a directory in lib and filled it with self-contained reports. In my view, this makes reports easier to build and maintain. Let's get started. First, you need a Rails project.


strutter$ rails HelloPrawn
create
create app/controllers
...
create log/server.log
create log/production.log
create log/development.log
create log/test.log

Now, we need to get the Prawn & Prawn-layout plugins. Since they are pretty new and constantly changing I like to get the latest version straight from GitHub. Just clone them in the usual way. Note that you have to get a couple of subcollections when you get Prawn this way.

$cd vendor/plugins
$git clone git://github.com/sandal/prawn.git
Initialized empty Git repository in /Rails/HelloPrawn/vendor/plugins/prawn/.git/
remote: Counting objects: 6716, done.
remote: Compressing objects: 100% (2762/2762), done.
Receiving objects: 21% (1459/6716), 6.31 MiB | 220 KiB/s
...
$git clone git://github.com/sandal/prawn-layout.git
Initialized empty Git repository in /Rails/HelloPrawn/vendor/plugins/prawn-layout/.git/
remote: Counting objects: 271, done.
remote: Compressing objects: 100% (232/232), done.
remote: Total 271 (delta 111), reused 0 (delta 0)
...

Since we are running Prawn from Git we have to do a couple of extra things.

$cd vendor/plugins/prawn
$git submodule init
Submodule 'vendor/pdf-inspector' (git://github.com/sandal/pdf-inspector.git) registered for path 'vendor/pdf-inspector'
Submodule 'vendor/ttfunk' (git://github.com/sandal/ttfunk.git) registered for path 'vendor/ttfunk'
$git submodule update
Initialized empty Git repository in /Users/richardhurt/Rails/HelloPrawn/vendor/plugins/prawn/vendor/pdf-inspector/.git/
remote: Counting objects: 16, done.
remote: Compressing objects: 100% (14/14), done.
...

Now we have to tell Rails to load Prawn-layout as well as Prawn. I do this with a simple initializer.

# config/initializers/prawn.rb
require "prawn/core"
require "prawn/layout"

Next we'll create a report. First build a new directory to hold all your reports.

mkdir lib/reports

Then add this new path to your Rails load_path. Look for the following section in you config/environment.rb file and make it look similar to this.

# Add additional load paths for your own custom dirs config.load_paths += %W({RAILS_ROOT}/extras )
config.load_paths += %W( #{RAILS_ROOT}/lib/reports )

and add a new PDF mime type.

#config/initializers/mime_types.rb
Mime::Type.register_alias "application/pdf", :pdf

Now that most of the setup is out of the way we're ready to get to work. Let's create a simple report and then show the controller that goes along with it. This report doesn't do much but it shows the basics.


# lib/reports/hello_prawn.rb
class HelloPrawn

# Build the parameter screen for this report.
def self.get_params()
# This report has no parameters
end

# Build the report in PDF form and sent it to the users browser
def self.draw(params)
# Create a new document
pdf = Prawn::Document.new(:page => "LETTER")

# Make it so we don't have to use pdf. everywhere. :)
pdf.instance_eval do
# Print something
text "Hello Prawn", :align => :center, :size => 20

# Render the page and send it back to the controller
render
end

end
end

Finally we'll build a controller to either build the parameter screen or render the PDF itself.

class ReportsController < ApplicationController

def show
# Since we are building the report object on the fly we need to make sure
# that it is a valid report before we try to build/show it.
begin
# Try to make the report into an 'object'
@report = params[:id].classify.constantize
rescue NameError
# This is not a valid report or some other error happened
flash[:error] = "Unknown Report '#{params[:id]}'"
redirect_to :action => :index
else

respond_to do |format|
format.html do
# display the parameters screen
end

format.pdf do
# Send the PDF back to the browser for viewing
send_data @report.draw(params),
:filename => params[:id].titleize + '.pdf',
:type => 'application/pdf',
:disposition => 'inline'
end
end
end
end
end

Lets test it out. Start up your Rails server and go to http://localhost:3000/reports/hello_prawn.pdf and you should get an inline PDF report in return. The cool thing about this is that each report can generate its own parameters screen on demand. See that self.get_params() function up in the report? That's where you can put your ERB stuff and have it render when you hit http://localhost:3000/reports/hello_prawn. Neato!

def self.get_params()
# Get the homeroom information
homerooms = Student.find_homerooms()

# Build the parameter screen
params = <<-EOS



Student Roster









EOS