Joe Woodward Joe Woodward - 4 months ago 24
Ruby Question

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

Answer

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

Comments