I just spent some time whipping footodo into shape for production. (When you’re just giving away the code, it’s a bit easier to forget about some things.) Thankfully, I have you, the ever-brilliant reader, to keep me honest. In that spirit, I’d like to share a couple of my favorite fixes with you.
From hideous to beautiful
If you took a look at the TodosController before today, you probably saw this offensive method, along with its comment of condemnation:
# This code needs to be rewritten or destroyed. Couldn't the update method be used instead?
def toggle_complete
@todo = Todo.find(params[:id])
if params[:complete]
@todo.complete = false
@todolist = @todo.todolist
respond_to do |format|
if @todo.save
flash[:notice] = 'Todo was successfully updated.'
format.js { render :action => 'uncompleted' }
format.html { redirect_to todo_url(@todo) }
format.xml { head :ok }
else
format.js { render :action => 'error' }
format.html { render :action => "edit" }
format.xml { render :xml => @todo.errors.to_xml }
end
end
else
@todo.complete = true
@todolist = @todo.todolist
respond_to do |format|
if @todo.save
flash[:notice] = 'Todo was successfully updated.'
format.js { render :action => 'completed' }
format.html { redirect_to todo_url(@todo) }
format.xml { head :ok }
else
format.js { render :action => 'error' }
format.html { render :action => "edit" }
format.xml { render :xml => @todo.errors.to_xml }
end
end
end
end
Tsk tsk, the new version is DRY-er, and a heck of a lot more readable. Interestingly enough, it’s not a dramatic decrease in code. It is, however, a dramatic increase in flexibility. By making the conditional responses their own methods, I’ve pulled a little bit of strategy pattern action by encapsulating what varies in the method, while leaving the static parts intact.
# Refactored the kilobytes out of this one. The complete attribute passed in
# in the params is the former state of the @todo's complete attribute.
def toggle_complete
@todolist = @todo.todolist
params[:complete] ? uncomplete! : complete!
respond_to do |format|
if @todo.save
format.js { render :action => @rjs }
format.html { redirect_to todolist_url(@todo.todolist) }
format.xml { head :ok }
else
format.js { render :action => 'error' }
format.html { redirect_to todolist_url(@todo.todolist) }
format.xml { render :xml => @todo.errors.to_xml }
end
end
end
def complete!
flash[:notice] = 'Todo was successfully completed.'
@todo.complete = true
@rjs = 'completed'
end
def uncomplete!
flash[:notice] = 'Todo was successfully uncompleted.'
@todo.complete = false
@rjs = 'uncompleted'
end
Lesson learned: Never trust your users
One of my oversights with this app was the fact that any User was able to access the :show action of the TodosController. An obvious problem in hindsight, but at the time, I thought the fact that I wasn’t linking to that action was enough to keep them private. (Again, I didn’t design this application with production in mind.) Once I was made aware of the problem by an eagle-eyed reader, I set to work fixing it.
Here’s how I’m keeping your to-dos private.
# I plopped this before_filter directly after the :login_required filter.
before_filter :ensure_ownership, :only => [:show, :edit, :update, :destroy, :toggle_complete]
# The before_filter calls this method, which checks for a to-do item that corresponds
# to the :id passed in the parameters. If it does, it calls the ensure_ownership method
# that makes sure the current_user is authorized to view the item.
def ensure_validity
!Todo.find(:first, :conditions => { :id => params[:id] }).nil? ? ensure_ownership : redirect_to(todolists_path)
end
def ensure_ownership
@todo = Todo.find params[:id]
unless @todo.todolist.user === current_user
flash[:notice] = "That doesn't belong to you!"
redirect_to todolists_path
end
end
# And of course, I have tests.
# This one goes out to Jamie (http://jamie.ideasasylum.com)
def test_should_not_show_if_not_owner
login_as('aaron')
get :show, :id => 1
assert_response :redirect
assert_redirected_to todolists_path
end
def test_should_not_get_edit_if_not_owner
login_as('aaron')
get :edit, :id => 1
assert_response :redirect
assert_redirected_to todolists_path
end
def test_should_not_update_if_not_owner
login_as('aaron')
put :update, :id => 1
assert_response :redirect
assert_redirected_to todolists_path
end
def test_should_not_delete_if_not_owner
login_as('aaron')
delete :destroy, :id => 1
assert_response :redirect
assert_redirected_to todolists_path
end
Hopefully this post will give you some insight into the refactoring process. If any of you ever see something else that needs changing, leave a comment. I’ll all ears.