Monday, November 30, 2009

Web: Redirecting the User Back After Some Workflow

Here's a common Web application workflow. A user gets to a page. He doesn't have access to the page because he isn't logged in. He gets redirected to the login page. He logs in. Perhaps he even has to create an account first. Afterwards, he gets redirected to the page he was trying to go to in the first place. Users find this sort of behavior helpful.

Here's another. The user is trying to buy something. He gets redirected to Google checkout (disclaimer: I've never used Google checkout). After he finishes checking out, he gets redirected back to the origin site. Perhaps he now has access to download the thing he just bought.

Thanks to the authlogic tutorial, I already had code to take care of the login workflow. However, I now find myself needing to do more workflows like it. The user tries to go to a page. However, he must do this other thing. Afterwards, he can go to the original page. What's the best way to handle the problem generically?

Here are the constraints that come to mind. I don't want two workflows stepping on each others toes. (I'm not even sure workflows have toes!) Hence, if one workflow leads to a page, and then the user has to do another workflow, he should get redirected at the end of the inner workflow and then get redirected at the end of the outer workflow. However, I don't expect that the user will need to recurse. That is, I don't expect one workflow to lead to itself again in a recursive sort of way.

With all that in mind, I took what authlogic gave me and expanded on it. In my base class, ApplicationController, I have:
  protected

def store_location(session_key_suffix, url=request.request_uri)
session[session_key_for_return_to(session_key_suffix)] = url
end

def redirect_back_or_default(session_key_suffix, default)
key = session_key_for_return_to(session_key_suffix)
redirect_to(session[key] || default)
session[key] = nil
end

def location_stored?(session_key_suffix)
session.has_key?(session_key_for_return_to(session_key_suffix))
end

private

def session_key_for_return_to(session_key_suffix)
"return_to_#{session_key_suffix.to_s}".to_sym
end
Here's how to use it to handle the login case:
  def require_user
unless current_user
store_location(:after_login)
flash[:error_message] = "You must be logged in to access this page"
redirect_to new_user_session_url
return false
end
end
After the user logs in, I call:
  redirect_back_or_default(:after_login, root_url)
Based on my tests, the above works. It can handle nested workflows. It can even handle a user working on two different workflows in two different tabs. I can envision some esoteric ways to mess the above code up, but I'm going to give it a couple weeks and see how it turns out in practice.

5 comments:

Tomasz Paczkowski said...

Storing such URLs in session has one major flaw: if you work in two tabs, than you share session and might end redirecting user in the wrong tab.

Shannon -jj Behrens said...

I know. The web is a pain. At least if they're working on two different workflows, the above code will work.

Shannon -jj Behrens said...

And if you're hinting at the idea of storing the URL to return to in the URL, the main reason I'm against this approach is that it's very painful to do once you have workflows that take several pages, and you have multiple of them happening at the same time. Not impossible, but very hard.

Jim said...

I had a similar problem and am using a hidden form input.

So call "login/?return_to=comment" and get username and password from the user but pass in "comment" as the place we want to come back to.

I'm new to programming but that seems to work pretty well.

Shannon -jj Behrens said...

cool