Suppose I have a mailer that sends different emails, but is expected to be called with the same parameters. I want to process those parameters for all mailer actions. So, calling a before_action that would read the parameters sent to the mailer method
/mailers/my_mailer.rb
class MyMailer < ApplicationMailer
before_filter do |c|
# c.prepare_mail # Will fail, because I need to pass `same_param` arguments
# # I want to send the original arguments
# c.prepare_mail(same_param) # How do I get `same_param` here ?
end
def action1(same_param)
# email view is going to use #to, #from, #context
method_only_specific_to_action1
end
def action2(same_param)
# email view is going to use #to, #from, #context
method_only_specific_to_action2
end
private
def prepare_mail(same_params)
#to = same_params.recipient
#from = same_params.initiator
#context = same_params.context
end
end
Then in my controller/service I do somewhere
MyMailer.actionx(*mailer_params).deliver_now
How can I access the same_param arguments list inside the before_action block ?
EDIT :
I want to refactor from
/mailers/my_mailer.rb
class MyMailer < ApplicationMailer
def action1(same_param)
#to = same_params.recipient
#from = same_params.initiator
#context = same_params.context
method_only_specific_to_action1
end
def action2(same_param)
#to = same_params.recipient
#from = same_params.initiator
#context = same_params.context
method_only_specific_to_action2
end
def actionx
...
end
end
And this refactoring
/mailers/my_mailer.rb
class MyMailer < ApplicationMailer
def action1(same_param)
prepare_mail(same_params)
method_only_specific_to_action1
end
def action2(same_param)
prepare_mail(same_params)
method_only_specific_to_action2
end
def actionx
...
end
private
def prepare_mail(same_params)
#to = same_params.recipient
#from = same_params.initiator
#context = same_params.context
end
end
Feels non-optimal (prepare_mail(same_params) duplicated in every action)
Hence what was suggested above
ActionMailer uses the AbstractController::Callbacks module. I tried it and it seems to work for me.
The code
class MyMailer < ApplicationMailer
def process_action(*args)
# process the args here
puts args
super
end
def some_mail(*args)
end
end
MyMailer.some_mail(1, 2) #=> prints ['some_mail', 1, 2]
The documentation
UPDATE
If you're using Rails 5.1, you can have a look at ActionMailer::Parameterized
Solution1:
I would suggest you use this if you do not care about the format
MyMailer.generic("actionx", *mailer_params).deliver_now
def generic(actiontype, *mailer_params)
# custom logic goes here to construct the to, from, etc.,
# new_options from custom logic
self.send(actiontype, new_options)
end
alternative solution below using method_missing from the parent controller
Its not right to put your logic there, but if you still want to do it, you can use the method_missing to put your logic there and skip the action1 and action2 methods.
Original method_missing from action_mailer which can be used as a reference:
def method_missing(method_name, *args)
if action_methods.include?(method_name.to_s)
MessageDelivery.new(self, method_name, *args)
else
super
end
end
https://github.com/rails/rails/blob/c8a18aaf44a84f1ef0df007aa3f8446752dc327d/actionmailer/lib/action_mailer/base.rb#L561-L567
Based on Sairam's answer I though of the following but that feels a bit weird, can it not be done with before_action callback ??
class MyMailer < ApplicationMailer
# Simulation of before_action callback that would support passing the *args to the callback method
def self.method_missing(method_name, *args)
method_name = :"#{method_name.to_s}_headers_prefilled"
if action_methods.include?(method_name)
mailer = MyMailer.generic(*args) # The before_action callback that passes *args
mailer.send(method_name, *args) # The action itself
else
super
end
end
def generic(*mailer_params)
# custom logic goes here to construct the headers to, from, etc.,
end
def action1_headers_prefilled(mailer_params)
# Logic only relevant for action1
end
Also I lose all the cool stuff from before_action (passing an only or except array, etc.)
Related
ActionMailer::Base provides call backs like ActionController::Base does which allows for common code to be called before a mailer function/action. It seems this is not supported for ActionMailer::Preview classes but could be useful. Is there a module I can include that would give me this functionality so I can call something like:
class MailerPreview < ActionMailer::Preview
include SomeCallBackModule
before_action :create_records
def mailer_a
# Don't need this anymore because create_records gets called
# #user = FactoryBot.create(:some_factory)
# #widget = FactoryBot.create(:another_factory, user: #user)
Mailer.mailer_a(#user, #widget)
end
def mailer_b
# Don't need this anymore because create_records gets called
# #user = FactoryBot.create(:some_factory)
# #widget = FactoryBot.create(:another_factory, user: #user)
Mailer.mailer_b(#user, #widget)
def
private
def create_records
#user = FactoryBot.create(:some_factory)
#widget = FactoryBot.create(:another_factory, user: #user)
end
end
ActionMailer::Preview indeed doesn't support callbacks out of the box.
In your example, I would suggest calling a simple private method directly instead of with a callback:
class MailerPreview < ActionMailer::Preview
def mailer_a
Mailer.mailer_a(user)
end
def mailer_b
Mailer.mailer_b(user)
def
private
def user
#user ||= User.first
end
end
In Rails we can define a class like:
class Test < ActiveRecord::Base
before_initialize :method
end
and when calling Test.new, method() will be called on the instance. I'm trying to learn more about Ruby and class methods like this, but I'm having trouble trying to implement this in plain Ruby.
Here's what I have so far:
class LameAR
def self.before_initialize(*args, &block)
# somehow store the symbols or block to be called on init
end
def new(*args)
## Call methods/blocks here
super(*args)
end
end
class Tester < LameAR
before_initialize :do_stuff
def do_stuff
puts "DOING STUFF!!"
end
end
I'm trying to figure out where to store the blocks in self.before_initialize. I originally tried an instance variable like #before_init_methods, but that instance variable wouldn't exist in memory at that point, so I couldn't store or retrieve from it. I'm not sure how/where could I store these blocks/procs/symbols during the class definition, to later be called inside of new.
How could I implement this? (Either having before_initialize take a block/proc/list of symbols, I don't mind at this point, just trying to understand the concept)
For a comprehensive description, you can always check the Rails source; it is itself implemented in 'plain Ruby', after all. (But it handles lots of edge cases, so it's not great for getting a quick overview.)
The quick version is:
module MyCallbacks
def self.included(klass)
klass.extend(ClassMethods) # we don't have ActiveSupport::Concern either
end
module ClassMethods
def initialize_callbacks
#callbacks ||= []
end
def before_initialize(&block)
initialize_callbacks << block
end
end
def initialize(*)
self.class.initialize_callbacks.each do |callback|
instance_eval(&callback)
end
super
end
end
class Tester
include MyCallbacks
before_initialize { puts "hello world" }
end
Tester.new
Left to the reader:
arguments
calling methods by name
inheritance
callbacks aborting a call and supplying the return value
"around" callbacks that wrap the original invocation
conditional callbacks (:if / :unless)
subclasses selectively overriding/skipping callbacks
inserting new callbacks elsewhere in the sequence
... but eliding all of those is what [hopefully] makes this implementation more approachable.
One way would be by overriding Class#new:
class LameAR
def self.before_initialize(*symbols_or_callables, &block)
#before_init_methods ||= []
#before_init_methods.concat(symbols_or_callables)
#before_init_methods << block if block
nil
end
def self.new(*args, &block)
obj = allocate
#before_init_methods.each do |symbol_or_callable|
if symbol_or_callable.is_a?(Symbol)
obj.public_send(symbol_or_callable)
else
symbol_or_callable.(obj)
end
end
obj.__send__(:initialize, *args, &block)
end
end
class Tester < LameAR
before_initialize :do_stuff
def do_stuff
puts "DOING STUFF!!"
end
end
I have a class that look like this (simplified)
class Timereg < ActiveRecord::Base
def initialize(hour_id)
super()
self.hour_id = hour_id
self.status = -2
self.slug = SecureRandom.uuid.to_s
end
end
When using it like this
Timereg.new(1)
all is good.
How can I use it with this
Timereg.create!
I can't figure out the syntax. I get:
ArgumentError: wrong number of arguments (2 for 1)
It is a bad idea to override create or initialize methods.
You should use callbacks. You can use after_initialize hook to assign the values.
You need to add the block parameter:
def initialize(hour_id, &block)
super(nil, &block)
..
end
This is what ActiveRecord's create! looks like:
# File activerecord/lib/active_record/persistence.rb, line 47
def create!(attributes = nil, &block)
if attributes.is_a?(Array)
attributes.collect { |attr| create!(attr, &block) }
else
object = new(attributes, &block)
object.save!
object
end
end
Because your .new does not take two parameters, the create! fails.
But as #kartikey-tanna said, it may not be a good idea to modify the initializer in the first place.
Consider something like:
def self.create_for_hour_id!(hour_id, attributes = nil)
timereg = new(attributes)
timereg.hour_id = hour_id
yield timereg if block_given?
timereg.save!
timereg
end
And use the callbacks to set up the slugs, status, etc:
before_validation :generate_slug
private
def generate_slug
self.slug ||= SecureRandom.uuid.to_s
end
i want to make a method inside a module (for grouping reason) that can be called as a module.method, something like this:
helpers do
module UserSession
def logged_in?
not session[:email].nil?
end
def logout!
session[:email] = nil
end
end
end
but when i try to call it using UserSession.logged_in? it said that logged_in is not UserSession's method
undefined method `logged_in?' for UserSession:Module
when i move the method as UserSession's method:
helpers do
module UserSession
def self.logged_in?
not session[:email].nil? # error
end
def self.logout!
session[:email] = nil
end
end
end
it gives an error, that i could not access the session variable
undefined local variable or method `session' for UserSession:Module
what is the best solution for this problem?
You can use a different convention for the helpers method.
module UserSession
def logged_in?
not session[:email].nil?
end
def logout!
session[:email] = nil
end
end
helpers UserSession
get '/foo' do
if logged_in?
'Hello you!'
else
'Do I know you?'
end
end
The module definition can of course be in another (required) file.
Behind the scenes, helpers <Module> is doing an include, but not simply into the Sinatra application sub-class you are using for your app. The include needs to be made compatible with how get, post etc work their magic, and helpers does that for you.
nevermind, i found the answer, i have tried define_method('UserSession.logged_in?') also, but no luck
last thing i've tried is:
# outside helpers
class UserSession
##session = nil
def initialize session
##session ||= session
end
def self.check
throw('must be initialized first') if ##session.nil?
end
def self.logged_in?
self.check
not ##session[:email].nil?
end
def self.logout
self.check
##session.delete :email
end
end
but something must be called first
before // do
UserSession.new session
end
then it can be used as desired:
get '/' do
if UserSession.logged_in?
# do something here
end
end
class A
def a
puts 'in #a'
end
end
class B < A
def a
b()
end
def b
# here i want to call A#a.
end
end
class B < A
alias :super_a :a
def a
b()
end
def b
super_a()
end
end
There's no nice way to do it, but you can do A.instance_method(:a).bind(self).call, which will work, but is ugly.
You could even define your own method in Object to act like super in java:
class SuperProxy
def initialize(obj)
#obj = obj
end
def method_missing(meth, *args, &blk)
#obj.class.superclass.instance_method(meth).bind(#obj).call(*args, &blk)
end
end
class Object
private
def sup
SuperProxy.new(self)
end
end
class A
def a
puts "In A#a"
end
end
class B<A
def a
end
def b
sup.a
end
end
B.new.b # Prints in A#a
If you don't explicitly need to call A#a from B#b, but rather need to call A#a from B#a, which is effectively what you're doing by way of B#b (unless you're example isn't complete enough to demonstrate why you're calling from B#b, you can just call super from within B#a, just like is sometimes done in initialize methods. I know this is kind of obvious, I just wanted to clarify for any Ruby new-comers that you don't have to alias (specifically this is sometimes called an "around alias") in every case.
class A
def a
# do stuff for A
end
end
class B < A
def a
# do some stuff specific to B
super
# or use super() if you don't want super to pass on any args that method a might have had
# super/super() can also be called first
# it should be noted that some design patterns call for avoiding this construct
# as it creates a tight coupling between the classes. If you control both
# classes, it's not as big a deal, but if the superclass is outside your control
# it could change, w/o you knowing. This is pretty much composition vs inheritance
end
end