Monday, July 30, 2012

Using Prawn and Rails to email a pdf

In MicroHealth we needed to email a pdf for some users.
The document that is attached can be downloaded as well, and I wanted to reuse as much code as possible.

For the views we use prawn and prawnto gems. The recipe consists of 3 steps:
- generate and save the pdf to disk
- email (attach the pdf)
- delete the pdf

The 'view' used to generate the pdf document is located in /app/views/my_controller/show.pdf.prawn and is shared for both emailing and downloading actions.

Generate a pdf file AND save it to disk

The key is to inherit from Prawn::Document, and to provide the @ instance variables that the view expects:

 class MyGenerator  < Prawn::Document
 

  def initialize(options)
    options && options.merge!({:inline=>true})
    create_instance_variables(options.delete(:variables))
    super(options)
  end

  def render_template(template)
    pdf = self
    pdf.instance_eval do
      eval(template) #this evaluates the template with your variables
    end
    ensure_path
    pdf.render_file(File.join(output_path,filename))
  end

  private

  def create_instance_variables(vars)
    return if vars.blank?
    vars.each_pair do |k,v|
        instance_variable_set("@#{k}", v)
    end
  end

  def output_path
    @output_path ||= File.join(Rails.root,'tmp','documents')
  end

  def ensure_path
    FileUtils.mkdir_p(output_path)
  end

  def filename
    @output_file ||= "#{Process.pid}::#{Thread.current.object_id}.pdf"
  end

end



For example my view expects some variables as @start, @end and @records, and that the pdf document variable is named 'pdf'

template = File.read("#{Rails.root}/app/views/my_controller/show.pdf.prawn")
writter = HemoPdfReport.new(:page_size => 'A4', :page_layout  => :landscape, :variables => {:start => params[:start], :end => params[:end], :records => @results})
attachment = writter.render_template(template)
begin
#send
  MyPdfMailer.attachment_email(
   :user => @user,
   :destination => @email,
   :message => @text,
   :attachment => attachment.path).deliver
              @report.save!
ensure
   FileUtils.rm_f(attachment.path)

end

Things to note:
  • The create_instance_variables method copies the 'variables'  parameter used in the initialization, to instance variables.
  • As our generator is a PrawnDocument, we can pass it as the 'pdf' variable that the view expects (note the line pdf = self before the eval)
  • We automatically provide a filename to the output. We could have used a timestamp but we use one based on the current thread. The key here is to avoid using an static name (we can have several processes / threads generating documents concurrently)

Emailing PDF

Emailing a file is really simple, just follow the http://guides.rubyonrails.org/action_mailer_basics.html
 
An example of my mailer
  def attachment_email(options)
    attachment = options[:attachment]
    @user = options[:user]
    @destination = options[:destination]
    @text = options[:message]
    attachment.present? && attachments['report.pdf'] = {
      :mime_type => 'application/pdf',
      :content => File.read(attachment)
    }
    subject = "Your Pdf Document"
    mail(:to => @destination, :subject => subject)
  end


Delete file

Once the file is emailed, dont forget to delete it. The ensure block is meant for this.

Profit!