Search code examples
ruby-on-railssidekiqrails-activejob

Using view_context inside the Mailer


I'm using ActiveJob to send mails:

Using deliver_now method:

invoices_controller.rb

def send_invoice
  #other stuff
  Members::InvoicesMailer.send_invoice(@invoice.id, view_context).deliver_now
end

invoices_mailer.rb

require 'open-uri'
class Members::InvoicesMailer < ApplicationMailer
  def send_invoice(invoice_id, view_context)
    @invoice = Invoice.find(invoice_id)
    attachments["#{@invoice.identifier}.pdf"] = InvoicePdf.new(@invoice, view_context).render

    mail :to => @invoice.client.email, :subject => "Invoice"
  end
end

Notice here that I'm sending the view_context from the controller to the mailer, that will again pass it to the InvoicePdf class to generate the invoice.

Result: Email sent correctly

Using deliver_later method:

invoices_controller.rb

def send_invoice
  #other stuff
  Members::InvoicesMailer.send_invoice(@invoice.id, view_context).deliver_later
end

Result: ActiveJob::SerializationError in Members::InvoicesController#send_invoice Unsupported argument type: view_context.

How to inject the view_context inside the InvoicePdf, either loading it from inside InvoicePdf, or InvoiceMailer?

Edit: This is what the InvoicePdf looks like

invoice_pdf.rb

class InvoicePdf < Prawn::Document
  def initialize(invoice, view_context)
    @invoice, @view_context = invoice, view_context
    generate_pdf
  end

  def generate_pdf
    # calling some active_support helpers:
      # @view_context.number_to_currency(//)
    # calling some helpers I created
  end
end

Solution

  • The problem with passing an object like the view context and then using deliver_later is that the parameters you give it are serialized to some backend (redis, MySQL), and another ruby background process picks it up later.

    Objects like a view context are not really things you can serialize. It's not really data.

    You can just use ActionView::Base.new, for example from rails console:

    # New ActionView::Base instance
    vagrant :002 > view = ActionView::Base.new
    
    # Include some helper classes
    vagrant :003 > view.class_eval { include ApplicationHelper }
    vagrant :004 > view.class_eval { include Rails.application.routes.url_helpers }
    
    # Now you can run helpers from `ApplicationHelper`
    vagrant :005 > view.page_title 'Test' 
    "Test"
    
    # And from url_helpers
    vagrant :006 > view.link_to 'Title', [:admin, :organisations]
     => "<a href=\"/admin/organisations\">Title</a>" 
    

    Here's what I do in my PdfMaker class, which is probably similar to your InvoicePdf class.

        def action_view
            @action_view ||= begin
                view = ActionView::Base.new ActionController::Base.view_paths
    
                view.class_eval do
                    include Rails.application.routes.url_helpers
                    include ApplicationHelper
                    include FontAwesome::Rails::IconHelper
                    include Pundit
    
                    def self.helper_method *name; end
                    def view_context; self; end
                    def self.before_action f; end
                    def protect_against_forgery?; end
                end
    
                view
            end
        end