Setting instance variables in ActionMailer - ruby

I have around 30 mailer methods where I'm passing the user as an argument. Since I need access to the #user variable in the view, I'm having to set this instance variable in every mailer method, for example, send_x_email(user).
Normally this would be done in an initialize method, but I've read that mailers act a bit differently. Additionally, some of the methods take a different number of arguments (one just takes user, the other takes user and message).
I've investigated before_action callbacks and looked at this post
Setting instance variables in Action Mailer?
...but I'm still stuck.
I would appreciate any thoughts on how to simplify things and remove #user = user from the 30 or so methods in the mailer class. Cheers!
class ReminderSender < ActionMailer::Base
def send_commands_email(user)
#user = user
mail(to: #user.email,
subject: "All Commands",
from: "<commands##{ENV['DOMAIN']}>")
end
def send_attachment_warning(user, message)
#user = user
#message = message
mail(to: #user.email,
subject: "Attachment Warning",
from: "<attachments##{ENV['DOMAIN']}>")
end
end

Try defining a 'mail' method in your class and declaring an instance variable there e.g.
class YouMailer
def send_email(user, message)
subject = 'something'
body = message
mail(user, {subject: subject, body: body}})
end
def mail(user, options={})
#user = user
mail_options = {to: #user.email}.merge(options)
super(mail_options)
end
end
But you might need to specify the 'template_path' and 'template_name' options with that strategy.
My suggestion would be to keep things as they are. Having "#user = user" in all of your mailer methods out of necessity isn't bad.

Related

Ruby - devise : confirmations_controller stop registrations_controller

I have a problem with devise I can't find the solution.
When a user sign_up, I need to call several services to make his profile. So here is the registrations_controller.rb.
require_relative '../../../app/services/affinities'
require_relative '../../../app/services/astroprofil'
require_relative '../../../app/services/geocode'
class Users::RegistrationsController < Devise::RegistrationsController
ASTROPROFIL = Astroprofil.new
AFFINITIES = Affinities.new
GEOCODE = Geocode.new
after_action :create_astroprofil, only: %i[new create]
after_action :create_affinities, only: %i[new create]
private
def create_astroprofil
return unless user_signed_in?
ASTROPROFIL.profil(current_user)
end
def create_affinities
return unless user_signed_in?
affinities(current_user, ten_mates)
end
def affinities(user, mates)
AFFINITIES.partner_report(user, mates)
AFFINITIES.sign_report(user, mates)
AFFINITIES.match_percentage(user, mates)
end
def ten_mates
mates_by_gender = User.where(gender: current_user.looking_for).where.not(id: current_user.id)
return mates_by_gender.sample(10)
end
end
When I sign up everything works perfectly, a new user is entirely created.
But as soon as I try to add a confirmation per mail with devise, the mails are sent but it stops the 'create_astroprofil' and the 'create_affinities' methods.
Do you have any idea about what's happening ?
I would say it's coming from this line
registrations_controller.rb#L28
Since you cannot login without having confirmed your email, I'm pretty sure create_astroprofil and create_affinities are called but their first line is return unless user_signed_in?.
2 options here:
Astroprofil.new and Affinities.new can be called for an unconfirmed user
Called create_astroprofil and create_affinities from ConfirmationController#show

Mailer unable to access reset_token in User model

faced with an issue where #user.reset_token returns nil.
app/views/user_mailer/password_reset.html.erb
<%= link_to "Reset password", edit_password_reset_url(#user.reset_token, email: #user.email) %>
Reset_token is declared in User model, whereby this problem happens when I try to use a sidekiq worker. Refer to code below.
app/models/user.rb
class User < ActiveRecord::Base
attr_accessor :reset_token
def User.new_token
SecureRandom.urlsafe_base64
end
def send_password_reset_email
PasswordResetWorker.perform_async(self.id)
end
private
def create_reset_digest
self.reset_token = User.new_token
update_attribute(:reset_digest, User.digest(reset_token))
update_attribute(:reset_sent_at, Time.zone.now)
end
app/workers/password_reset_worker.rb
class PasswordResetWorker
include Sidekiq::Worker
sidekiq_options retry: false
def perform(user_id)
user = User.find(user_id)
UserMailer.password_reset(user).deliver
end
end
app/mailers/user_mailer.rb
class UserMailer < ActionMailer::Base
default from: "noreply#example.com"
def password_reset(user)
#user = user
mail to: user.email, subject: "Password Reset"
end
end
This problem DOES NOT happen when I do not use workers
app/models/user.rb
def send_password_reset_email
UserMailer.password_reset(self).deliver
end
Would like to know what can I replace "#user.reset_token" with? Let me know if you need more info. Thanks in advance.
You're not storing the reset_token in the database - you're storing the reset_digest.
When you don't use workers, you're storing the reset_token in the User instance, then passing that same User instance to your mailer - hence the reset_token is still available.
When you use workers, your worker only has the User's ID, so it's reloading the User instance from the database. Because the reset_token isn't being stored in the database, it's coming back nil.
Either you should be saving the reset_token in the database, or your password email should be using reset_digest in the URL.

Padrino controller abstraction

I've been trying Padrino framework in one of my project, and there is one thing that really annoys me. I want to implement just for instance a user registration process using OmniAuth and want to break my request handler (controller's action) to separate methods, like this:
get ":provider/callback" do
#user = find_the_user_by_oauth(request)
create_user unless #user
store_user_in_session
end
def find_the_user_by_oauth(request)
#...
end
def store_user_in_session
session[:user_id] = #user.id
end
I know it would be nicer to push the logic to the model layer, but my question is, how could I break a controller logic to separated methods and share information among them (like using instance variables). In Rails I created these methods in the private scope of my controller, but here I should extend the Application class because it throws Undefined method exception for the previous code. I tried Helpers, but helpers don't know the instance variables, so you should pass the variables every time.
What is the good way to make my controller actions clean in Padrino?
To define a method inside an Padrino Controller you can use define_method instead of def.
For your example, do something like this:
Admin.controllers :dummy do
define_method :find_the_user_by_oauth do |request|
request.params["username"]
# ...
end
define_method :store_user_in_session do
session[:user_id] = #user
end
get :test do
#user = find_the_user_by_oauth(request)
create_user unless #user
store_user_in_session()
session.inspect
end
end
Padrino runs the block sent to Admin.controllers using instance_eval.
See this answer for the differences https://stackoverflow.com/a/3171649 between define_method and def
possible offtopic, but would you consider to use Espresso Framework instead.
then you'll can solve your issue as simple as:
class App < E
def index provider, action = 'callback'
#user = find_the_user_by_oauth
create_user unless #user
store_user_in_session
end
private
def find_the_user_by_oauth
# provider, action are accessed via `action_params`
# action_params[:provider]
# action_params[:action]
end
def store_user_in_session
session[:user_id] = #user.id
end
end

in UsersController#create, User.new(params[:user]) return an empty User (params looks good)

I'm kind of new to Rails 3.1. and I'm facing an issue only in my production env with my Signup form (actually, it's more about the controller).
Here is the code in User
class UsersController < ApplicationController
[...]
def create
#user = User.new(params[:user])
logger.info "value of login in param : #{params[:user][:login]}" #-> log the actual login
logger.info "value of login : #{#user.login}" #-> log empty
#user.admin = false
if #user.save
flash[:notice] = t('flash.notice.user.create.valid')
redirect_back_or_default root_path
else
flash[:notice] = t('flash.notice.user.create.invalid')
render :action => :new
end
end
end
Also, the controller logs show that the params hash is good
Parameters: {"utf8"=>"✓",
"authenticity_token"=>"QwOqmp0CT/d4mmC1yiLT4uZjP9bNDhbUXHanCQy5ZrA=",
"user"=>{"login"=>"myLogin",
"email"=>"t.r#gmail.com",
"password"=>"[FILTERED]",
"password_confirmation"=>"[FILTERED]"}}
My login form works as expected (already created users are able to sign in)
Again, this only happens in production.
EDIT: Here is my User Model
class User < ActiveRecord::Base
acts_as_authentic
#== Callbacks
before_create :set_defaults
attr_accessible :avatar ##### EDIT: TO FIX THE ISSUE, ADD THE OTHER FIELDS AS WELL
protected
def set_defaults
self.total_1 = self.total_2 = self.total_3 = 0
end
end
Just to memorialize the answer from the comments above:
Normally you can use mass assignment to set fields on a model, but when you use attr_accessible, you are then limited to only mass assigning those fields. So stuff like User.new(params[:user]) won't work; instead, you'd have to do:
#user = User.new
#user.login = params[:user][:login]
# ...etc.
#user.save
Simple add your fields to the attr_accessible list and you can go back to mass assignment.

How can I send emails in Rails 3 using the recipient's locale?

How can I send mails in a mailer using the recipient's locale. I have the preferred locale for each user in the database. Notice this is different from the current locale (I18n.locale), as long as the current user doesn't have to be the recipient. So the difficult thing is to use the mailer in a different locale without changing I18n.locale:
def new_follower(user, follower)
#follower = follower
#user = user
mail :to=>#user.email
end
Using I18n.locale = #user.profile.locale before mail :to=>... would solve the mailer issue, but would change the behaviour in the rest of the thread.
I believe the best way to do this is with the great method I18n.with_locale, it allows you to temporarily change the I18n.locale inside a block, you can use it like this:
def new_follower(user, follower)
#follower = follower
#user = user
I18n.with_locale(#user.profile.locale) do
mail to: #user.email
end
end
And it'll change the locale just to send the email, immediately changing back after the block ends.
Source: http://www.rubydoc.info/docs/rails/2.3.8/I18n.with_locale
This answer was a dirty hack that ignored I18n's with_locale method, which is in another answer. The original answer (which works but you shouldn't use it) is below.
Quick and dirty:
class SystemMailer < ActionMailer::Base
def new_follower(user, follower)
#follower = follower
#user = user
using_locale(#user.profile.locale){mail(:to=>#user.email)}
end
protected
def using_locale(locale, &block)
original_locale = I18n.locale
I18n.locale = locale
return_value = yield
I18n.locale = original_locale
return_value
end
end
in the most resent version of rails at this time it's sufficient to use
"I18n.locale = account.locale"
in the controller and make multiple views with the following naming strategy
welcome.html.erb,
welcome.it.html.erb and e.g.
welcome.fr.html.erb
None of the above is really working since the version 3 to translate both subject and content and be sure that the locale is reseted back to the original one... so I did the following (all mailer inherit from that class:
class ResourceMailer < ActionMailer::Base
def mail(headers={}, &block)
I18n.locale = mail_locale
super
ensure
reset_locale
end
def i18n_subject(options = {})
I18n.locale = mail_locale
mailer_scope = self.class.mailer_name.gsub('/', '.')
I18n.t(:subject, options.merge(:scope => [mailer_scope, action_name], :default => action_name.humanize))
ensure
reset_locale
end
def set_locale(locale)
#mail_locale = locale
end
protected
def mail_locale
#mail_locale || I18n.locale
end
def reset_locale
I18n.locale = I18n.default_locale
end
end
You just need to set the locale before you call the mail() method:
set_locale #user.locale
You can use the i18n_subject method which scope the current path so everything is structured:
mail(:subject => i18n_subject(:name => #user.name)
This simple plugin was developed for rails 2 but seems to work in rails 3 too.
http://github.com/Bertg/i18n_action_mailer
With it you can do the following:
def new_follower(user, follower)
#follower = follower
#user = user
set_locale user.locale
mail :to => #user.email, :subject => t(:new_follower_subject)
end
The subject and mail templates are then translated using the user's locale.
Here's an updated version that also supports the '.key' short-hand notation, so you don't have to spell out each key in its entirety.
http://github.com/larspind/i18n_action_mailer
The problem with the mentioned plugins are that they don't work in all situations, for example doing User.human_name or User.human_attribute_name(...) will not translate correctly. The following is the easiest and guaranteed method to work:
stick this somewhere (in initializers or a plugin):
module I18nActionMailer
def self.included(base)
base.class_eval do
include InstanceMethods
alias_method_chain :create!, :locale
end
end
module InstanceMethods
def create_with_locale!(method_name, *parameters)
original_locale = I18n.locale
begin
create_without_locale!(method_name, *parameters)
ensure
I18n.locale = original_locale
end
end
end
end
ActionMailer::Base.send(:include, I18nActionMailer)
and then in your mailer class start your method by setting the desired locale, for example:
def welcome(user)
I18n.locale = user.locale
# etc.
end

Resources