Activation Emails with Restful Authentication and Acts_As_State_Machine
Update: December 18, 2007: less code, same result
Summary
If you use the Restful Authentication plugin with the –include-activation and –stateful options (see Acts_As_State_Machine and the post by Jonathan Linowes which united the two plugins), you’ll need to make a couple modifications to the code after you run the generator in order to keep the user activation email from being sent simultaneously with the user signup notification email.
Problem
The UserObserver code sends an activation email during the after_save callback if the user’s state is “pending.” Without activation the state transition goes from pending to active and we can leave the code alone. With activation, however, we introduce another state: the “state of being notified that our account has been created.”
If we change the UserObserver to watch for an “active” state instead of “pending,” the user will receive an activation email every time we save the user’s record and that puts the user into the state of “I’m about to adios this annoying website.”
Suggested Solution
We add a temporary state “notified” between pending and active that allows us to send the activation email at the right time with little disruption to the original restful authentication code.
Just before we “activate!” the user in the UserController, we “notify!” them, putting them in the “notified” state, save the user, which calls the after_save callback, which sends the activation email, and then return control to the UserController, which calls “activate!,” putting the user in the “active” state.
The Code
Changes to User.rb
Update: I removed the :do_notify code which was redundant (state transitions automatically save the record)
state :notified event :notify do transitions :from => :pending, :to => :notified end event :activate do transitions :from => :notified, :to => :active end event :suspend do transitions :from => [:passive, :pending, :notified, :active], :to => :suspended end event :delete do transitions :from => [:passive, :pending, :notified, :active, :suspended], :to => :deleted end
Changes to User_Observer.rb
def after_save(user) UserMailer.deliver_activation(user) if user.notified? end
Changes to User_Controller.rb
# In the activate method, # insert the notify! line right before the activate! line current_user.notify! current_user.activate!
If I’ve left something out, or if you have a better solution, please say so below. Cheers!
9 Comments
-
Jamie spoke thusly:
Hi Harry,
Can one not simply change …
def after_save(user)
UserMailer.deliver_activation(user) if user.pending?
endto …
def after_save(user)
UserMailer.deliver_activation(user) if user.active?
endI’m having another problem, which is that my activation_code isn’t making its way into the activation e-mail nor the database, even though it’s set correctly within User#make_activation_code. I’ve just started to try to figure it out though…
Good luck!
Jamie -
Harry spoke thusly:
Jamie,
Unfortunately, no. If you change the code to user.active, you get a confirmation email every time you save the user, even if the user is just changing his password.
-
Juha Syrjälä spoke thusly:
Hi,
I was just wrestling with this same problem. Your solution works just fine. It makes an extra update to database when user is activated, but that is not a problem for me. Thanks!
Juha
-
emil tin spoke thusly:
in my view, the problem is that emails should be triggered by changes in the state, not bu saves. that’s the purpose of acts_as_state_machine. so using an observer, and checking for states is just causing confusion. my solution (which seems toworks) is to get rid of the observer, and send the emails from the state change methods in User. (does anyone know if this might have any bad side effects?)
an improvement could be to create an new state change observer class that could be attached and would listen for state changes, instead of activerecord callbacks.
another problem with the code was empty activation codes we. here the problem is that the if you set an initial state, the corresponding state change method is called only AFTER the initial saving of the object, (which is somewhat counterintuitive), so the activation code was generated after the save and was lost. the solution is to start the User as passive, and do an pending! right after the initial save. this way there’s no need for the temporary notified state. -
Harry spoke thusly:
I like the idea of using the state instead of the callback method which polls every time but may not be used. Whereas, when you transition to the state which requires an email to be sent, you know the email should be sent at that time.
Do you have code you can share, Emil?
-
emil tin spoke thusly:
Sure. Here’s what so far seems to work for me. Use at your own risk! If you see any problems, I would be interested in knowing about it. I’ve also had to do number of other changes to how the activation works.
Everything to do with UserObserver has been removed. Remember to remove the reference in environment.rb too.User.rb:
acts_as_state_machine :initial => :passivestate :passive
state :pending, :enter => :do_pending
state :active, :enter => :do_activate
state :suspended
state :deleted, :enter => :do_delete[....]
def do_pending
make_activation_code
UserMailer.deliver_signup_notification(self)
enddef do_activate
self.activated_at = Time.now.utc
self.deleted_at = self.activation_code = nil
UserMailer.deliver_activation(self) #this email could be left out
enduser_controller.rb:
def create
cookies.delete :auth_token
# protects against session fixation attacks, wreaks havoc with
# request forgery protection.
# uncomment at your own risk
# reset_session
@user = User.new(params[:user])
@user.save! #save first
@user.register! #then change state
current_user = @user
#at this point, the user is registered and logged in but not authorized,
#because the account is not yet activated.
#redirect to activation page
redirect_to :controller => :account, :action => ‘activate’
rescue ActiveRecord::RecordInvalid
render :action => ‘new’
end -
Ches spoke thusly:
I followed emil’s suggestion to eliminate the Observer and send emails upon state change callbacks (much cleaner, thanks emil!). I did so in essentially the same way as the code he shared.
I also added a ‘forgetful’ state for handling password resetting with emails. I’ll get around to blogging that code sometime soon :-)
Just wanted to share though that, as emil alluded to above, the differences in the way callbacks work for new vs. existing model objects is the tricky part in working with acts_as_state_machine. The section on Callbacks at this link has been the most useful summary reference I’ve found:
http://rails.aizatto.com/category/plugins/acts_as_state_machine/
-
Harry Love spoke thusly:
Thanks for the suggestions and the link, Ches. You reminded me that I still need to handle the forgetful state. Being able to eliminate the observers is a nice way to go.
-
Saurav spoke thusly:
Hi,
I want to send the welcome email only after the user logs in for the first time. Can anybody give me some suggestion on how to do it. Rite now Im using
def after_save(user)UserMailer.deliver_activation(user) if user.pending?
end
and this method sends the welcome email only when the user activates himself.
