Better Redirects in Rails

Saturday, March 22nd, 2008

Its been a very long time since I posted to this blog, but I thought I might share some tricks I’ve developed for handling a few special types of redirects a bit more gracefully in Ruby on Rails.

Note: all of the code in this post, except for one view helper method, should be placed in your ApplicationController (app/controllers/application.rb) from which all other controllers inherit.

Most people who have read any book on Rails will probably have run into the redirect_to_index method, which goes something like this:

def redirect_to_index(msg = nil)
  flash[:notice] = msg if msg
  redirect_to :action => 'index'
end

This method is mainly useful when you the user has just edited an object, and you now want to display a message in the flash like “User updated successfully”, and then redirect them back to the index action. But what about if you don’t want to redirect the user back to the index action, or if you want to send them to another controller? Say hello to the flash_redirect method:

def flash_redirect(msg, *params)
  flash[:notice] = msg
  redirect_to(*params)
end

flash_redirect accepts both a message to be put in the flash, along with the params for redirect_to, so that you can flash a message and then send the user anywhere you please.

These two methods by themselves are pretty useful, but what about the common situation where a user attempts to go somewhere they don’t have permission to, or that they need to login before accessing? In the Rails book, this is handled by storing the original url in the session, and then redirecting to the other action, which then has to do special session processing when it returns the user to the original action. Encapsulating all this code into some easily reusable methods would look like this:

# redirect somewhere that will eventually return back to here
def redirect_away(*params)
  session[:original_uri] = request.request_uri
  redirect_to(*params)
end

# returns the person to either the original url from a redirect_away or to a default url
def redirect_back(*params)
  uri = session[:original_uri]
  session[:original_uri] = nil
  if uri
    redirect_to uri
  else
    redirect_to(*params)
  end
end

redirect_away handles sending the user away to another action, and redirect_back will send them back to either the action they were redirected away from, or to a default action. One possible use for these could be to require authorization before accessing an action:

class AdminController < ApplicationController
  before_filter :require_admin, :except => 'login'

  def index 
    # unauthorized people shouldn't be able to access this
  end

  def login
    # handle login
    if User.authorize(params[:username], params[:password])
      session[:admin] = true
      redirect_back(:action => 'index')
    end
  end

  private

  def require_admin
    unless session[:admin]
      flash[:notice] = "You must be logged in"
      redirect_away(:action => 'login')
      return false
    end
  end
end

So now we can redirect a user away from an action, and then send them back to it later on. But what if we want to let a user click a link to go somewhere, and then send them back where they came from later? With a simple helper method and a before filter, we can accomplish just that:

# app/helpers/application_helper.rb
def link_away(name, options = {}, html_options = nil)
  link_to(name, { :return_uri => url_for(:only_path => true) }.update(options.symbolize_keys), html_options)
end

# app/controllers/application.rb
before_filter :link_return

private

# handles storing return links in the session
def link_return
  if params[:return_uri]
    session[:original_uri] = params[:return_uri]
  end
end

Now in the view, you can write:

<%= link_away "Edit post", :controller => '/admin/posts', :action => 'edit', :id => post %>

And as long as the controller uses redirect_back, the user can click the link, and when they’re done editing, they will come right back to where they clicked the link. This trick is probably the most useful from a usability standpoint, given that nothing annoys a user more than having to manually navigate back to where they were after each change.

I hope you find these techniques useful for writing more user friendly and concise code!

9 Responses

  1. DHH - March 23rd, 2008 at 9:17 am

    Those are some neat patterns. You should wrap them up in a plugin.

  2. Dan - March 26th, 2008 at 3:22 pm

    Very handy ideas!

  3. Anthony - March 26th, 2008 at 4:34 pm

    Very nice. Simple, basic, almost obvious. But really helps to tighten up recurring crap in controller code.

    Thanks for sharing!

  4. Alderete - April 10th, 2008 at 3:11 am

    I notice that this isn’t quite a drop-in replacement for the link_to code that is generated by the Rails 2.0 scaffolding generator, specifically, the URL helpers generated by using map.resources in routes.rb, e.g.:

    <%= link_away 'Edit', edit_person_path(@person) %>
    

    Will fail with the error “undefined method `symbolize_keys’ for “/people/1/edit”:String”

    def link_away(name, options = {}, html_options = nil)
        link = case options
            when String
                link_to(name, options + "?return_uri=" + url_for(:only_path => true), html_options)
            else
                link_to(name, { :return_uri => url_for(:only_path => true) }.update(options.symbolize_keys), html_options)
            end
    end
    

    But that’s fairly unsatisfying, if for no reason than it will add an extra “?” to the URL if there are already parameters in it. (My Ruby-fu is weak.)

  5. Alderete - April 10th, 2008 at 3:30 am

    On further testing, neither the original nor my altered version of the helper handle the case of a generated show link, which simply passes in an ActiveRecord object:

    <%= link_away h(person.display_name), person %>
    

    (person also gets passed for generated delete links.)

    Not really sure what the answer is, except to only use link_away for edit links.

  6. Adam - April 11th, 2008 at 8:11 am

    We do something similar by marking certain points in an application as return points.

    before_filter :mark_return_point :only =>[x,z]
    

    Which does exactly the same thing and marks a return parameter in the session, we just have a

    return_to_last_point 
    

    then whenever an action like an update completes redirects to the last point someone went through.

    Not sure which I prefer, our current way keeps the logic of the flow inside the controller

  7. David Cotter - July 5th, 2008 at 7:57 am

    @Alderete for it to work here’s how I did it:

    
        def link_away(name, options = {}, html_options = nil)
          if (options.is_a?(String)) 
            options = ApplicationHelper.append_url_parameters(options, {:returnuri => url_for(:onlypath => true)})
          else
            options = { :return_uri => urlfor(:only_path => true) }.update(options.symbolizekeys)
          end
          link_to(name, options, htmloptions)
        end

    To append a param to the URL I use this (forgive me if there’s already a function to do this which I can’t find)

    def self.append_url_parameters(url, urlparams) 
      new_params_query_string = ''
      first = true
      urlparams.each_pair do |param, value|
        new_params_query_string << '&`' unless first
        new_params_query_string << "#{param}=#{CGI.escape(value.to_s)}"
        first = false
      end
      uri, query = url.split('?')
      if (query.blank?) 
        uri + "?" + new_params_query_string
      else   
        uri + "?" + query + "&" + new_params_query_string
      end    
    end
    
  8. Nathan Levitt - August 8th, 2008 at 6:30 pm

    Thanks for the awesome article!

    @Adam – I love your suggestions on this. I do the same thing and it seems to work great!

  9. İzzet Emre Kutlu - May 14th, 2009 at 6:51 am

    another option for link_away method : def link_away(*args, &block) original_uri = {:original_uri => request.request_uri} if block_given? options = args.first || {} html_options = args.second options.merge!(original_uri) link_to(options, html_options) do capture(&block) end else name = args.first options = args.second || {} html_options = args.third options.merge!(original_uri) link_to(name, options, html_options) end end