Search code examples
ruby-on-railslocaleactionmailerdelayed-jobrails-activejob

Storing current optional route scope before calling deliver_later on rails mailer


Rails 4.2 Ruby 2.3

I have two optional routing scopes relating to locale information. They are set in a before_action in the application_controller which configures the default_url_options method. i.e.

# app/controllers/application_controller
# simplified version, usually has two locale values, 
# locale_lang and locale_city


before_action :redirect_to_locale_unless_present

private

# If params[:locale] is not set then
# redirect to the correct locale base on request data    
def redirect_to_locale_unless_present
  unless params[:locale].present?
    redirect_to url_for(locale: request.location.country_code)
  end
end

def default_url_options(options = {}
  { locale_lang: params[:locale_lang] }.merge(options)
end

The scopes are locale_lang and locale_city which end up looking something like http://localhost:3000/fr/ or http://localhost:3000/en/

This all works as intended in the browser, however I would like to utilize ActionMailer::DeliveryJob to send emails in background processes. The obvious issue to this is that ActionMailer::DeliveryJob doesn't store the value of params[:locale].

I would like to be able to call SomeMailer.generic_email(options).deliver_later and have this send the current default_url_options to the ActionMailer::DeliveryJob which would then pass those along the chain and use them when actually processing the mail. I could of course define default_url_options as a parameter for each Mailer method but I would much rather set up the app so it was automatically included.

Have you ever encountered this issue or have any suggestions on how to approach the task. Keep in mind that it should also be thread safe.

My currently failing approach is to save the current request in Thread.current and then retrieve those when enqueue_delivery is called via .deliver_later. I then wanted to override ActionMailer::DeliveryJob's perform method to accept the url_options and use class_eval to define the default_url_options method within the current mailer class. However, perform doesn't seem to even be called when using deliver_later any ideas?

class ApplicationController
  before_action :store_request

  private

  def store_request
    Thread.current['actiondispatch.request'] = request
  end
end

module DeliverLaterWithLocale
  module MessageDeliveryOverrides
    def enqueue_delivery(delivery_method, options={})
      args = [
        @mailer.name,
        @mail_method.to_s,
        delivery_method.to_s,
        url_options,
        *@args
      ]
      ActionMailer::DeliveryJob.set(options).perform_later(*args)
    end

    private

    def url_options
      options = {}
      request  = Thread.current["actiondispatch.request"]
      if request
        host     = request.host
        port     = request.port
        protocol = request.protocol
        lang = request.params[:locale_lang]
        city = request.params[:locale_city]
        standard_port = request.standard_port
        options[:protocol] = protocol
        options[:host]     = host
        options[:port]     = port if port != standard_port
        options[:locale_lang] = lang
        options[:locale_city] = city
      end
      ActionMailer::Base.default_url_options.merge(options)
    end
  end

  module DeliveryJobOverrides
    def perform(mailer, mail_method, delivery_method, url_options, *args)
      mailer = mailer.constantize.public_send(mail_method, *args)
      Kernel.binding.pry
      mailer.class_eval <<-RUBY, __FILE__, __LINE__ + 1
        def default_url_options_with_options(*args)
          default_url_options_without_current_request(*args).merge(url_options)
        end
        alias_method_chain :default_url_options, :options
      RUBY
      mailer.send(delivery_method)
    end
  end
end

Solution

  • Incase anyone else wants to do this. I fixed it by adding

    class ApplicationController
      before_action :store_request
    
      private
    
      def store_request
        Thread.current['actiondispatch.request'] = request
      end
    end
    
    module DeliverLaterWithLocale
      module MessageDeliveryOverrides
        def enqueue_delivery(delivery_method, options={})
          args = [
            @mailer.name,
            @mail_method.to_s,
            delivery_method.to_s,
            url_options,
            *@args
          ]
          ActionMailer::DeliveryJob.set(options).perform_later(*args)
        end
    
        private
    
        def url_options
          options = {}
          request  = Thread.current["actiondispatch.request"]
          if request
            host     = request.host
            port     = request.port
            protocol = request.protocol
            lang = request.params[:locale_lang]
            city = request.params[:locale_city]
            standard_port = request.standard_port
            options[:protocol] = protocol
            options[:host]     = host
            options[:port]     = port if port != standard_port
            options[:locale_lang] = lang
            options[:locale_city] = city
          end
          ActionMailer::Base.default_url_options.merge(options)
        end
      end
    
      module DeliveryJobOverrides
        def perform(mailer, mail_method, delivery_method, url_options, *args)
          mailer = mailer.constantize
          mailer.default_url_options = url_options
          mailer.public_send(mail_method, *args).send(delivery_method)
        end
      end
    end
    

    And then prepend these to the respective classes in an initializer