I'm trying to fully understand a line of code in the action mailer that is shown in every documentation but not really explained.
def welcome_email(user)
#user = user #don't understand exactly which user this is
mail(to: #user.email, subject: 'Do you have any spam?')
end
I thought you had to define that variable like User.find(params[:id]) or User.first or something else that retrieves a specific user. What does plain 'user' mean in this context?
Thanks for your help with this beginner level question.
#user = user
This line is setting the value of #user instance variable to user which is being passed as a variable to the welcome_email method.
#user instance variable can be accessed in the views linked to this mailer.
Where-ever the welcome_email method is called, it is likely to have set the value of user using user = User.find(params[:id]) or user = User.first or something similar, and that user is passed in as a parameter with welcome_email(user) call.
Assuming the mailer is called Notifier, and the welcome email has to be sent when the user signs up, the following code is likely to be in the app/controllers/users_controller.rb file:
class UsersController < ApplicationController
...
def create
...
#user = ...
Notifier.welcome_email(#user).deliver_now
...
end
...
end
Related
I have a before action in a user mailer file, which is supposed to stop mailers sending if a column on user is set to true or false. However current user is currently unavailable. I understand why, but was wondering if there was a way to do this.
I want to avoid adding the check_if_users_can_receive_mailers at the top of each mailer method.
before_action :check_if_users_can_receive_mailers
#methods that send mailers
private
def check_if_users_can_receive_mailers
current_user.send_mailers?
end
You have to make the current user available as a attribute or class variable. The most straight forward method is something like this:
class MailerBase < ActionMailer::Base
before_action :check_if_users_can_receive_mailers
attr_accessor :user
def initialize(user)
#user = user
end
private
def check_if_users_can_receive_mailers
user.send_mailers?
end
end
class SomeMailerClass < MailerBase
end
In Rails only your controller and views are request aware. Mailers and models and other classes in your application are not and they cannot get the current user since they can't access the session nor the method current_user which is a helper method mixed into your controller (and the view context).
If your mailers need to know about the current user the most logical approach is to pass that information into the mailer:
class UserMailer < ApplicationMailer
def intialize(user)
#user = user
end
end
However a mailer should only have one job - to send emails and it shouldn't be questioning if it should do the job or not. Determining if you should send an email to the user should be done elsewhere. You can place this logic in the controller or even better in a service object:
# app/notifiers/user_notifier.rb
class UserNotifier
def initialize(user, event:)
#user = user
#event = event
end
def notify
if #user.wants_email?
spam_user!
end
send_in_app_notification
end
def self.notify(user, event:)
new(user, event:)
end
private
def spam_user!
# ...
end
def send_in_app_notification
# ...
end
end
class ThingsController
def create
#thing = Thing.new
if #thing.save
UserNotifier.notify(current_user, event: :thing_created)
redirect_to #thing
else
render :new
end
end
end
This question already has an answer here:
Ruby : Difference between Instance and Local Variables in Ruby [duplicate]
(1 answer)
Closed 5 years ago.
I would like to know what is the difference between #user and user, like in:
#user = User.new
and
user = User.new
Some people say that #user with # is an instance used at views, but I already see this instance at Unit tests.
Is there any difference between then? In purpose or semantic or anything?
The instance #user at unit tests is not the same instance #user at controller, but they're used for similar reasons.
# signify instance variables, which are available in all other methods of the instance object.
If in a controller you have
def new
#user = User.new
apply_another_value_to_user
end
def apply_another_value_to_user
#user.nickname = 'Zippy'
end
That works.
If instead you do...
def new
user = User.new
apply_another_value_to_user
end
def apply_another_value_to_user
user.nickname = 'Zippy'
end
You will get an error "undefined local variable or method 'user'" because user is only defined for use within the new method.
unit tests use #user to ensure a user object can be shared by different methods in the test instance. Controllers use #user to ensure a user object can be shared by different methods (and views) in the controller instance. It may be that during a test a controller instance is initialized and both #user instance variables happen to be created but they are not the same variable.
This is why in a test, to access a controller instance variable, you can't use #user directly, you have to use assigns(:user) which references the controller instance variable.
expects(assigns(:user).not_to be_nil
user is defined as local variable. You can reuse user with in the same method but cannot use it in other methods.
#user is instance variable. You can use it in other methods as well.
class SomeClass
def first_method
user = User.new
#user = User.new
end
def second_method
# ..
# You can use #user here.
# But user is undefined in this method.
end
end
Let's say I have a website where people can get a free ebook if they will sign up for a newsletter - after they've done it, I will create a User model and I will show them Edit Form to add some extra details about them.
I don't want to force them to add a password or any other details on the first page because it would decrease conversions and I don't require the additional information either. Also, I don't want them to have forever access to the Edit page so I solved it by assigned a session to them and recognize it through it on the Edit page. This is my controller:
class UsersController < ApplicationController
def create
user = User.new(user_params)
if user.save
session[:user_id] = user.id
UserWorker.perform_in(5.minutes, 'new_user', user.id)
redirect to edit form...
end
end
def edit
#user = User.find(session[:user_id])
end
def update
#user = User.find(session[:user_id])
#user.update!(user_edit_params)
redirect_to user_thank_you_path
end
end
But if they won't add extra information within 10 mins, I will send them an email via ActiveMailer with a link to the Edit form and ask them to do so.
Th question is how could I identify the user through the session and show them the form - how could I do User.find(session[:user_id] via ActionMailer)? Is it actually a correct way or would you recommend a different approach?
One way could be to set a background job to run in 10 minutes.
Inside that job, you would check if they're still "unregistered". You deliver the email if they've yet to complete the registration.
Something like this;
class UsersController < ApplicationController
def create
user = User.new(user_params)
if user.save
session[:user_id] = user.id
RegistrationCompletionReminderWorker.perform_in(10.minutes, user.id)
# redirect to edit form...
end
end
end
class RegistrationCompletionReminderWorker
def perform(user_id)
user = User.find(user_id)
if user.password.nil? # or whatever your logic for registration completion is
UserMailer.registration_reminder(user_id).deliver_now
end
end
end
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.
I'm on railcasts just practicing some rails and have come across something I'm trying to understand.
I didn't get what the "self" on the authenticate method was doing. So I deleted it and tested the login of my app to see if it would show an error and it did:
error:
**NoMethodError in SessionsController#create
undefined method `authenticate' for #<Class:0x00000102cb9000**>
I would really appreciate if someone could explain exactly what that "Self" is doing. I was trying to figure out exactly what was going on but can't get my head around it.
Method is defined in model and called in sessions_controller..
I've been continuously deleting my app and starting from scratch to get the hang of it and many things make sense to me each time i start again but I'm stuck at "self".
I'm just the type of person who likes to understand why something works.
controller:
def create
user = User.authenticate(params[:email], params[:password])
if user
session[:user_id] = user.id
redirect_to root_path, :notice => "Logged In"
else
flash.now.alert = "Invalid credentials"
render "new"
end
end
model:
def self.authenticate(email, password)
user = find_by_email(email)
if user && user.password_hash == BCrypt::Engine.hash_secret(password, user.password_salt)
user
else
nil
end
end
This is a basic ruby question. In this case, self is used to define a class method.
class MyClass
def instance_method
puts "instance method"
end
def self.class_method
puts "class method"
end
end
Which are used like this:
instance = MyClass.new
instance.instance_method
Or:
MyClass.class_method
Hope that clears things up a little bit. Also refer to: http://railstips.org/blog/archives/2009/05/11/class-and-instance-methods-in-ruby/
self defines a method of the class instead of the instance of the class. So with def self.authenticate you can do the following:
u = User.authenticate('email#domain.com','p#ss')
Instead of doing…
u = User.new
u.authenticate('email#domain.com','p#ss')
That way, you don't have to create an instance of user to authenticate one.
For the sake of completion and to thwart future headaches, I'd like to also point out that the two are equivalent:
class User
def self.authenticate
end
end
class User
def User.authenticate
end
end
Matter of preference.
class User
def self.xxx
end
end
is one way of defining class method while
class User
def xxx
end
end
will define an instance method.
If you remove the self. from the def, you will get a method not found error when you do
User.authenticate
because you are trying to call a method on a class rather than an instance of the class. To use an instance method, you need an instance of a class.