Skinny user activation
February 01, 2007 00:27 (Sydney Australia), updated 68 days later
Update: Nicolas pointed out the User.find_and_activate! method had a serious bug where the user could pass a nil activation_code param and it would find the first user with a nil activation code and activate them. I’ve updated the model method to raise an ArgumentError if a nil activation code is passed in. Thanks Nic!
Jamis Buck has been blogging like a bat out of hell coming out of christmas, totalling 26 posts in all. If you’re a Rails dev and haven’t checked out his articles you should do so right away. I just wish I lived in his vincinity so I could pick his brains at Rails meetups.
Jamis recently blogged about skinny controllers and fat models, which I had also been thinking about and agree it feels like a great way to go. As an aside though, I don’t like the idea of doing all your HTTP parameter parsing in the model, but maybe that’s ok in your model until you feel the need to refactor it.
Getting out of the controller making business decisions mindset is tough. Most of the books written, and most of the apps written until now, have made controllers the starting point for determining application behaviour and they treat ActiveRecord models as simple data stores with a few extra methods and overrides. For example, here’s some of my code that handles user activation:
class UsersController < ActionController::Base
def activate
unless user = User.find_by_activation_code(params[:activation_code])
flash[:notice] = 'Activation code not found.'
redirect_to must_activate_path and return
end
if user.activated?
flash[:notice] = 'Your account has already been activated. You can log in below.'
redirect_to login_path and return
end
user.activate
self.current_user = user # log in user
flash[:notice] = "Your account has been activated!"
redirect_back_or_default('/')
end
end
and the corresponding User model method:
class User < ActiveRecord::Base
def activate
@activated = true
self.attributes = {:activated_at => Time.now.utc, :activation_code => nil}
save(false)
end
end
So what’s wrong with the above? Firstly, the controller’s main purpose isn’t really obvious, it begins 12 lines down at user.activate.
The UsersController#activate specification, if it had one, would read:
Activate the user with the corresponding activation code, log them in and redirect them to the home page.
If the activation code is invalid, display some error message.
If the account is already activated, display some error message.
Does the implementation read similarly to the spec? Nope.
So if we forget for the moment about handling wrong activation codes and reactivations, we could refactor the controller to read more like the spec:
class UsersController < ActionController::Base
def activate
self.current_user = User.find_and_activate!(params[:activation_code])
flash[:notice] = "Your account has been activated!"
redirect_back_or_default('/')
end
end
Much simpler, and we even removed a temporary variable. So how about handling those two edge cases?
We could use if statements and have the controller check the model and make decisions in a similar fashion to our first implementation, but that’d ruin the way our action reads, and its making the controller more intelligent about the activation process than it needs to be.
Every if statement makes a method’s behaviour less obvious. Refactoring away nasty nested ifs is a great past time of mine.
Instead of explicitly handling the edge cases with ifs we’ll just make the meat of the method assume things went well, and then handle exceptions (aha!) after the fact.
class UsersController < ActionController::Base
def activate
self.current_user = User.find_and_activate!(params[:activation_code])
flash[:notice] = "Your account has been activated!"
redirect_back_or_default('/')
rescue User::ActivationCodeNotFound
flash[:notice] = 'Activation code not found.'
redirect_to must_activate_path
rescue User::AlreadyActivated
flash[:notice] = 'Your account has already been activated. You can log in below.'
redirect_to login_path
end
end
Compare that to the first implementation of the activate action.
# The old UsersController#activate implementation
class UsersController < ActionController::Base
def activate
unless user = User.find_by_activation_code(params[:activation_code])
flash[:notice] = 'Activation code not found.'
redirect_to must_activate_path and return
end
if user.activated?
flash[:notice] = 'Your account has already been activated. You can log in below.'
redirect_to login_path and return
end
user.activate
self.current_user = user # log in user
flash[:notice] = "Your account has been activated!"
redirect_back_or_default('/')
end
end
What does the model implementation look like now?
class User < ActiveRecord::Base
class ActivationCodeNotFound < StandardError; end
class AlreadyActivated < StandardError
attr_reader :user, :message;
def initialize(user, message=nil)
@message, @user = message, user
end
end
# Finds the user with the corresponding activation code, activates their account and returns the user.
#
# Raises:
# +User::ActivationCodeNotFound+ if there is no user with the corresponding activation code
# +User::AlreadyActivated+ if the user with the corresponding activation code has already activated their account
def self.find_and_activate!(activation_code)
raise ArgumentError if activation_code.nil?
user = find_by_activation_code(activation_code)
raise ActivationCodeNotFound if !user
raise AlreadyActivated.new(user) if user.activated?
user.send(:activate!)
user
end
private
def activate!
@activated = true
self.update_attribute(:activated_at, Time.now.utc)
end
end
There’s a few neat things worth noting in the above snippet; nested classes for neatly tucking away the Exceptions, making User.find_and_activate the only public way to activate a user, and a custom intialiser for the AlreadyActivated exception in case the controller wants to know which user we’re talking about when we raise the error.
We’ve ended up with a much cleaner and intelligent model, we formalised the edge cases as actual Exceptions (which means the edge cases can be nicely documented via RDoc) and cleaned up the controller.
I think it simply comes down to:
Models want to be smart about everything business specific.
Now for the fun part: refactoring all your old apps for all these new techniques. Doesn’t it seem like we’re needing to do this every 6 months? I think this is great though because:ActionControllers want to be smart about HTTP and request control-flow.
- We’re still learning.
- The community actually care about this stuff (one of the things I love most about this community).
- Rails is flexible enough in its opinions that we can still change the way we’re approaching problems.
- The community (and Jamis in particular) is open to admitting they don’t know all the answers and are sharing knowledge instead of getting on high horses or climbing ivory towers.
Time for the ActionController weight-watchers plan.

Comments
topfunky
Well written.
I started a new app in December and wrote most of it in this fashion. It also makes the code much more reusable since models are available everywhere but controller methods are only available in one place.
Andi
Nice article. Basically you’re using “Exceptions” to propagate Domain errors. If this is a good thing or not is up to the programmer. In Java it’s not considered very good style. Then again, Ruby isn’t Java ;)
Tim Lucas
@topfunky: thanks. Yeah, I can imagine it’d be handy when adding APIs and in other scripts.
@Andi: The ruby/java distinction shouldn’t really make any difference. I understand the sentiment, but how should this code then be written, whilst making sure the model is enforcing consistency?
Eadz
I haven’t written my user activation code yet, but it’s next on the list so perfect timing with this great post Tim :)
Shane
A quick question. Why did you decided to use user.send(:activate!) instead of user.activate!
Thanks
Tim Lucas
@shane:
User#activate!is a private method, and asUser.find_and_activate!is a class method it can’t directly call it. I don’t mind this slight hack as it’s contained within the same class. It does proabably deserve a comment in the code though.Max
@Andi: on the contrary, that’s how checked Exceptions in Java should be used. Forcing developers to catch TheDataBaseIsOnFireSoWhatchaGoingToDoAboutItExceptions is poor design. On the other hand, any error condition that the application can do something about is almost certainly part of the business domain.
Tim Lucas
Max’s point makes more sense to me than “[you shouldn’t use] Exceptions to propagate Domain errors”
nicolas
but you still have the problem pointed out by Barry Hess in http://technoweenie.stikipad.com/plugins/show/User+Activation (about the need to check for empty params[:activation_code] due to the danger of getting reconfirmed and logged in as an other unreconfirmed user). but beside this.. thank you for this eye-opener (can i use this phrase in english? ;-) )
Tim Lucas
Thanks nicolas. I’ve updated the article above to handle this, though I think nil argument should still be handled correctly within
User.find_and_activatelevel rather than the controller.Geoffroy
This is just brilliant! I am java developer for a living and a Rails developer as part time occupation and this is the way I work in java but couldn’t figure out how to adapt this to Ruby. Thanks for the pointers.
steve
Great post! I am wondering why find_and_activate! and activate! has to be separate? The code in the latter could easily be inserted into the end of the former.
Patrick Crowley
I, too, am digging this code.
It would be nice to see a followup post, with a complete, working code example. Activation affects login and signup, but I don’t see that fully documented here.
The acts_as_authenticated docs on activation are kind of half-assed, so I’m sure a more complete activation example would be very welcome.
Tim Lucas
Alright Patrick… and I’ve been meaning to post some mods to AAA with some other fixes too. I’ll keep you posted.
Jamie Hill
Doesn’t the activate! method need to set the activation_code to nil as well as setting activated_at, otherwise activated? will never return true.
Jamie Hill
Apologies for the double post. Is there any reason why the activate! method can’t be public (avoiding the send hack)?
Tim Lucas
Jamie: ahh I left out the implemention of the
User#activated?method in the final example. An implementation ofdef activated?; activated_at? endshould work.No reason to keep it private except for that your making it public. For my intent and purpose it was to be only called from the class method.
Alex Fetcher
Great site!
76c775a94bc118dab1609035c7ad354b
To comment on this article you must have javascript enabled.