When I am developing page actions, I quite often find myself approaching a standard get/post action in the same way. This is particularly because I want to validate a number of ActiveRecord models, only progressing if they are ALL valid, and showing all errors at once
Of course, that means that I need to have a standard way of doing this, and a standard way of reporting it. It also implies the use of transactions, so that all saves are rolled back on failure. I have
So to begin with, an (hypothetical) example of my approach is:
def edit_user
@user = User.find(session[:user_id])
@address = @user.address
if request.post?
begin
# note that I do not want to reference the objects in the
# method call - so that the changes are available to render
@user.transaction do
results = []
results << @user.save
results << @address.save
raise "Validation failed" if results.include?(false)
end
redirect_to :action => :show_user and return
rescue Exception
logger.warn{"Transaction terminated : #{e.message}"}
logger.warn{"@user : #{(@user.errors.full_messages.join('; ') rescue nil)}"}
logger.warn{"@address : #{(@address.errors.full_messages.join('; ') rescue nil)}"}
logger.debug{e.backtrace}
end
end
end
The first thing that is useful about this is that because all objects are saved together and the transaction is not terminated unless ONE of them is invalid, we will not redirect and the edit_user template will be rendered. Also, the validation errors are written to the log “just in case”
Also, note that I am using the log4r syntax of using braces instead of brackets. In Log4r, if the logger is not logging at the level that is specified, the code inside the braces will not be run. In the example above, if we are in production mode and we are not logging DEBUG, the e.backtrace method is actually not executed.
From here, lets DRY it up.
First, there’s those validation messages. There’s quite a lot of code here. I’ve approached this from 2 angles. First, in environment.rb, I put this code in
class ActiveRecord::Base
def full_messages
self.errors.full_messages.join('; ') rescue nil
end
end
and in application.rb
def validation_message(*objects)
str = "Validation error :"
objects.each{ |obj| str << "\n#{obj.class.name} => #{obj.full_messages}" }
str
end
This means that the validation message can now be handled by simply doing this in the rescue block:
logger.warn{validation_message(@user, @address)}
Next, because I use this structure regularly, I created a page_errorhandler method to contain the exception block that does this:
def page_errorhandler(*objects, &block)
begin
yield
rescue Exception => e
logger.warn{"Transaction terminated : #{e.message}"}
logger.warn{validation_message(*objects)}
logger.debug{e.backtrace}
end
end
Combining these with our method above:
def edit_user
@user = User.find(session[:user_id])
@address = @user.address
if request.post?
page_errorhandler(@user, @address) do
# note that I do not want to reference the objects in the
# method call - so that the changes are available to render
@user.transaction do
results = []
results << @user.save
results << @address.save
raise "Validation failed" if results.include?(false)
end
redirect_to :action => :show_user and return
end
end
end
Neatens things up nicely!