Class methods defined as instance methods - ruby-on-rails

This is a working code from the Michael Hartle book. This is the mailer code in app/mailers/user_mailer.rb to create an activation mail for a user account:
class UserMailer < ActionMailer::Base
def account_activation(user)
#user = user
mail to: user.email, subject: "Account activation"
end
end
The preview of the mail is generated using the ruby file in test/mailers/previews/user_mailer_preview.rb:
class UserMailerPreview < ActionMailer::Preview
def account_activation
user = User.first
user.activation_token = User.new_token
UserMailer.account_activation(user)
end
end
The account_activation method is defined as an instance method in user_mailer.rb. But it is used in preview generator as a class method. Did I misunderstand the code or is there something else going on here?
To anyone who have the same doubt
That's how ActionMailer works. Emails are defined as instance method in a class that extends ActionMailer::Base , but you access them as class methods.

That's how ActionMailer works. Emails are defined as instance method in a class that extends ActionMailer::Base, but you access them as class methods.
class MyMailer < ActionMailer::Base
def my_email
end
end
MyMailer.my_email
# and not MyMailer.new.my_email
This is a shortcut that will instantiate an instance of the ActionMailer class, invoke the corresponding email method and return an email message. This is the code that handles the call.

The Magic is behind deliver/deliver_now/deliver_later (or any other deliver method):
def deliver_now
processed_mailer.handle_exceptions do
message.deliver
end
end
Looks like processed_mailer is the key method that we were looking for:
def processed_mailer
#processed_mailer ||= #mailer_class.new.tap do |mailer|
mailer.process #action, *#args
end
end
This method creates the instance of the mailer, calls process method with #action argument (which is the name of the instance method) and with #args, which are the arguments passed to the class method and in the end it returns the created instance of the mailer.
So thats the reason that mailer methods are being declared like instance ones but get called like class ones.
Happy Learning :)

Related

Pass instance variable from Devise controller to custom mailer?

I use Devise for authentication in my Rails app.
In my registrations_controller I set an instance variable like this:
class RegistrationsController < Devise::RegistrationsController
def create
#foo = "bar"
super
end
end
In my customized mailer I then try to access the #foo instance variable, but it just returns nil:
class CustomMailer < Devise::Mailer
helper :application
include Devise::Controllers::UrlHelpers
def confirmation_instructions(record, token, opts={})
Rails.logger.error #foo.inspect # => nil
super
end
end
Anyone who could help?
I have looked through the posts How do I add instance variables to Devise email templates?, How to pass additional data to devise mailer?, How to pass instance variable to devise custom mailer in Rails?. But none of them seem to deal with this exact problem.
First let me explain you have instance variables work in ruby classes.
In ruby, instance variable(in your case #foo) in a class(in your case RegistrationsController) do not pass down to other classes(in your case CustomMailer) even if inheritance is ON. For Example consider:
class Abc
#first = "First"
puts "Abc: #{#first}"
end
class Def < Abc
puts "Def: #{#first}"
end
# => Abc: First instance variable
# => Def:
As you can see the class Def cannot have access to #first instance variable. Thus, instance variables don't get passed down to other classes automatically.
If you want these variables to pass down the class, you should consider using Class Instance Variables which starts with ##. Ex:
class Abc
##first = "First instance variable"
puts "Abc: #{##first}"
end
class Def < Abc
puts "Def: #{##first}"
end
# => Abc: First instance variable
# => Def: First instance variable
Now ##first will pass down to inherited class automatically.
Now, relate the above senario to your question. So you're creating a instance variable #foo in RegistrationsController and it will not be passed to other inherited classes. Under the hood both Devise::RegistrationsController and DeviseMailer are inherited from Devise.parent_controller.
So, better way to work with it is to send the #foo as a parameter, for example:
class RegistrationsController < Devise::RegistrationsController
def create
#foo = "bar"
CustomMailer.confirmation_instructions(user, token, opts={foo: #foo})
...
end
end
Then you can access this in your CustomMailer:
def confirmation_instructions(record, token, opts={})
puts opts[:foo]
super
end

Include application_helper.rb in ActionMailer

I'm trying to get access to some of my application_helper methods within my mailer, but nothing seems to be working from these SO posts:
View Helpers in Mailers
Access Helpers from mailer
In app/helpers/application_helper.rb I have the following:
module ApplicationHelper
def get_network_hosts
.. stuff get get a #network_hosts object
end
end
In my mailer at app/mailers/user_notifier.rb I have the following:
class UserMailer < ActionMailer::Base
default from: "Support <support#me.co>"
add_template_helper(ApplicationHelper)
def trial_notifier(user)
get_network_hosts
#user = user
#total = user.company.subscription.per_device_charge * #network_hosts.count
if #total < 501
#message = "You'd pay #{#total}/month if you converted to full-access now!"
else
#message = "You'd pay #{#total}/month if you converted to full-access now, but we have a better deal for you!"
end
#url = edit_user_registration_url
mail(to: #user.email, subject: 'What's up?')
end
end
In my mailer I've tried all of the suggestions in the above SO posts, but I'm still getting this error:
`NameError: undefined local variable or method `get_network_hosts' for #<UserMailer:0x007fe756c67c18`>
I'm currently using Rails 4.1.7.
So what do I have to actually do to be able to use my application_helper methods within a mailer?
You can try to do this as following:
In your mailer at app/mailers/user_notifier.rb:
class UserMailer < ActionMailer::Base
default from: "Support <support#me.co>"
helper :application
or you can try this:
helper ApplicationHelper

pass instance variable while using observer pattern in rails

I'm using observer pattern in rails. The "delivered_email" method in TestObserver will be called after notification email been sent out. How could I pass the instance variable in "notification" to the "delivered_email"? I could add it either in the header or subject. But it could pose security issue since user who received email could also see the variable. Is there any better way to solve it?
class GeneralMailer < ActionMailer::Base
def notification(data)
#emails = data[:emails]
subject = "#{#sender.to_s}"
mail(:to => #emails, :subject => subject)
end
end
class TestObserver
def self.delivered_email(message)
begin
# do something here
puts #emails
rescue => ex
# do something here
end
end
ActionMailer::Base.register_observer(TestObserver)
If you do want to send extended information to a mail observer – as I myself did – it can be done by monkey-patching your mailer a bit to use a custom class extending from Mail::Message.
Note that this requires setting an internal, undocumented attribute. In a future Rails version, it might not continue to work.
class ExtendedMessage < Mail::Message
attr_accessor :extra_args
end
class ApplicationMailer
before_action :patch_message
def patch_message
# Patch the internal attribute #_message to use our overridden class
# that you can store extra attributes inside.
# NOTE: If a future Rails version decides to change the internals, this could break.
#_message = ExtendedMessage.new
end
end
class MailObserver
def self.delivered_email(mail)
do_something_with mail.extra_args
end
end

ActionMailer are the methods static?

class UserMailer < ActionMailer::Base
default from: 'notifications#example.com'
def welcome_email(user)
#user = user
#url = 'http://example.com/login'
mail(to: #user.email, subject: 'Welcome to My Awesome Site')
end
end
To send the email I write UserMailer.welcome_email(#user).deliver so my question is: are the methods declared in the Mailer Controller static? Becausei I call welcome_email on a class, so I am cunfused
Not really, but in practice it works as if they were. You have the answer here: How can you call class methods on mailers when they're not defined as such? .
Basically, the Mailer has a method_missing defined that if the method called doesn't exist, it will create an instance of the mailer and call the method on it.

Rails3: access request.host from within a Mailer

I'm trying to access request.host (well, ideally host_with_port) from a Mailer in Rails. The actually call to request.host is in a Helper:
#/app/helpers/confirmations_helper
module ConfirmationsHelper
def email_confirm_url(token)
"http://#{request.host_with_port}/confirm/#{token}" # failure: undefined method
end
end
#/app/mailers/user_mailer
class UserMailer < ActionMailer::Base
default from: "email#domain.com"
add_template_helper(ConfirmationsHelper) #get access to helpers/confirmations_helper.rb
def email_confirmation(user)
#user = user
#url = "http://www.domain.com/"
mail(to: user.email, subject: "Email Confirmation")
end
end
#config/environments/development.rb
...
config.action_mailer.default_url_options = { :host => "localhost:3000" }
Error I'm getting is:
ActionView::Template::Error:
undefined method `host' for nil:NilClass
Use
ActionMailer::Base.default_url_options[:host]
in your Mailer to access the configured host in in config/environments/
I believe, this is the easiest way.
ActionView::Template::Error:
undefined method `host' for nil:NilClass
This is telling you that request is nil. This is because outside of the scope of your controller (ie. in a class extending ActionMailer::Base) request doesn't exist.
You need to pass the request object or just the part you need (request.host_with_port) to the mailer like you do other data like user in your email_confirmation.
So you have a create method with something like this
def create
#user = User.new
#user.assign_attributes(params[:user])
#user.save
#user.send_email_confirmation
end
Inside your User model you have a send_email_confirmation method like this
class User < ActiveRecord::Base
def send_email_confirmation
UserMailer.email_confirmation(self).deliver
end
Your mailer's email_confirmation looks like
def email_confirmation(user)
#user = user
#url = "http://www.domain.com/"
mail(to: user.email, subject: "Email Confirmation")
end
Making the request to the mailer from your model is not the best idea; you should keep a cleaner separation of concerns. This is part of your problem and why you are finding unwanted complexity when trying to pass something like request from your controller action into the mailer template.
What I might suggest is creating a worker class. Here I explain how to setup classes in lib/ - the same concept can be applied to something like a lib/your_app/workers/user.rb.
You could have the following in this class
module YourApp
module Workers
module User
extend self
def create!(params, options{})
options.reverse_merge! host: ""
user = User.new
user.assign_attributes(params)
user.save
UserMailer.email_confirmation(user, host).deliver
user
end
end
end
end
Your controller action could then simply be
def create
#user = ::YourApp::Worker::User.create!(params[:user], host: request.host_with_port)
end
Your mailer method can now look like
def email_confirmation(user, host)
#user = user
token = "" # define token somehow
#url = "#{host}/confirm/#{token}"
mail(to: user.email, subject: "Email Confirmation")
end
Finally, you can remove send_email_confirmation from your model as well as the email_confirm_url method from your helper since they're no longer used. Two things to note
my example above doesn't include anything in the way of validations/error-checks
my example makes an assumtion about where token is being defined and used
As you can see, by introducing this 'worker' class, there is a clean separation of functionality without duplication.

Resources