Monday, 28 April 2008

redirect_to POST

Our current site uses the acts_as_authenticated plugin. When a user's session has timed out, we redirect them to the login page, and save the URL they were trying to reach. If they successfully login, we do a redirect to that URL. This all comes as standard in the plugin.

The problem is that redirect_to doesn't seem to preserve the HTTP method (ie GET or POST etc). Most of the time, a user will have clicked on a link, or have a link saved in their bookmarks. The URL will be something like /users/42/orders which would be a a GET-based request that is supposed to show the users current orders. However, sometimes a user will have left their browser session open while they went off to lunch, displaying their set of orders... along with a set of buttons that, say, delete or clone an order.

The problem comes in when this timed-out user tries to click on one of those buttons. A timed-out user that clicks on, say, the "Clone this order" button gets the following error: no route found to match "/user/42/orders/23/clone" with {:method=>:get}

Buttons (which are really a form-post) generally have an HTTP POST action - even the ones with Rails-faked-up PUT or DELETE actions are really POST under the covers. Our nicely RESTful application is set up to not accept GET-based requests for dangerous (non-idempotent) actions such as "clone".

Now, acts_as_authenticated uses two methods for storing/redirecting to URLs. The first is called store_location. The code here grabs the URI that the user requested, and stuffs it into the session. The second is called redirect_back_or_default - it tries to redirect back to the uri stored in the session - or to a given default.

The method: redirect_to is the Rails-standard way of sending you on to another URL via an HTTP redirect. Unfortunately, it seems that this method only tries to use GET-based URIs, so when we tried redirecting to the POST-only URI for the "clone" request, we got the routing error.

At first I thought all that was needed was to save the request's HTTP method, and pass that into redirect_to. Saving the method is easily done, and I stuffed it into the session along with the URI. Then I dug around in vain trying to find out how to pass in the HTTP method as a parameter.

Reading all the doco yielded no joy, nor did digging into the rails core code to see how it deals with uris, sessions, requests or redirection. It wasn't until I delved deep into the HTTP spec itself, and asked a few questions on RoR Oceania that I finally confirmed that it simply isn't "done" to try to redirect a user to anything other than a GET-based URI.

This is somewhat disappointing, as I don't see why a GET has to be treated like something special by HTTP - the use-case for allowing redirection to any other verb is there. :(

Still, there's no getting around this limitation, and the system must fail gracefully (which the routing spew doesn't). So the next best thing is to capture an attempt to access a non-GET request and try something different.

I figured that most of the time when a user tries to get to a non-GET request, it'll be from a button from another page on the site. eg, they'll have left their brower open on the "My orders" page, and have clicked a button from there. It's far less likely they'll somehow have hooked up a bookmark to a POST-based URI, and just as unlikely that some other site will have a button to our site. Therefore, I figued the best option is to try sending the person back to the page they started on (ie the "My orders" page), rather than the page they are asking for (ie whatever button they clicked on).

This can be solved fairly easily. If the user asked for a GET-based request we have no problem, just store it as usual. But if they asked for something else, then we need to save the referer-URI instead. I've also added a flash error to tell the user what happened, and why they have to click the button again.

So the usual acts_as_authenticated code becomes:

    # Store the URI of the current request in the session.
    #
    # We can return to this location by calling #redirect_back_or_default.
    def store_location
      # if the user has asked for a non-get request (eg posted a form). We
      # can't redirect to that - so try getting the referrer (probably the
      # index page they came from). We will post them back to their previous
      # page and warn them about what is going on
      session[:return_get] = request.get?
      session[:return_to] = request.get? ? request.request_uri : request.env["HTTP_REFERER"]
    end
    
    # Redirect to the URI stored by the most recent store_location call or
    # to the passed default.
    def redirect_back_or_default(default)
      # if the user had asked for a non-get request (eg posted a form). We
      # can't redirect to that - so we must warn the user that they will not
      # be going where they were expecting
      flash[:error] = "You clicked on a button or submitted a form, but we cannot redirect you back to that. Please try submitting again." unless session[:return_get]
      redirect_to(session[:return_to] || default)
      session[:return_to] = session[:return_get] = nil
    end

Tuesday, 22 April 2008

RailsEnvy - Rails vs...

RailsEnvy have put out a string of fantastic parodies of the famous "Mac vs PC" ads... featuring Rails vs a range of alternative frameworks...

If you haven't seen them yet, they're worth a few minutes for a giggle.

Monday, 21 April 2008

icon buttons

We're all pretty used to the CRUD-based scaffold-ful of actions on a resource index page - such things as "show", "edit" and "delete". But adding a few columns and a few extra actions, causes a swiftly increasing footprint which gets real ugly, real fast.

Buttons stack on top of one another, or they wrap in random ways, or they take up half the width of the screen and squish up all the other data.

The soluion, of course, is to make nifty little icon-buttons. But how do we make this RESTful and DRY?

fixed_button_to

Standard button_to is a wonder - it does practically everything a button needs, and you can override anything...

Well, almost - as I discovered when I first tried to make an image-based button. The "type" is set to submit every time. The following line of code is the culprit:

html_options.merge!("type" => "submit", "value" => name)

So even from the get-go, we need to monkey-patch button_to

You can override the method, or rename it to something else - whatever you like... but sadly you'll need to actually copy the whole button_to code and replace it. So I just created a newly named method. Drop the following into environment.rb (or similar) for buttony joy:

module ActionView::Helpers::UrlHelper
  def fixed_button_to(name, options = {}, html_options = {})
    html_options = html_options.stringify_keys
    convert_boolean_attributes!(html_options, %w( disabled ))

    method_tag = ''
    if (method = html_options.delete('method')) && %w{put delete}.include?(method.to_s)
      method_tag = tag('input', :type => 'hidden', :name => '_method', :value => method.to_s)
    end

    form_method = method.to_s == 'get' ? 'get' : 'post'
   
    request_token_tag = ''
    if form_method == 'post' && respond_to?(:protect_against_forgery?) && protect_against_forgery? 
      request_token_tag = tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, 
:value => form_authenticity_token)
    end
    
    if confirm = html_options.delete("confirm")
      html_options["onclick"] = "return #{confirm_javascript_function(confirm)};"
    end

    url = options.is_a?(String) ? options : self.url_for(options)
    name ||= url

    html_options = {"type" => "submit", "value" => name}.merge(html_options)

    "<form method=\"#{form_method}\" action=\"#{escape_once url}\" class=\"button-to\"><div>" +      
method_tag + tag("input", html_options) + request_token_tag + "</div></form>"
  end
end

Those with an unhealthy familiarity with the minutiae of the Rails source will also notice that I added a respond_to?(:protect_against_forgery?) check in there too. My site doesn't use that, and it kept breaking without the check (there must be some magic place its set in a normal Controller/Helper, but it doesn't get in there from here).

icon_button_to

Next up is to write our nifty icon_button_to helper. I use the following:

  # quick and  dodgy icon path-generator
  def get_icon_path(icon_type = 'default')
    icon_name = ACTION_ICONS.include?(icon_type.to_s) ? icon_type : 'default'
    "/images/icons/#{icon_name}.png"
  end

  # generates appropriate image-buttons, given an "icon type" and the usual
  # button options.
  def icon_button_to(icon_type, icon_label, form_path, button_opts = {})
    button_opts.merge!('type' => 'image', :src => get_icon_path(icon_type), 
                       :alt => icon_label, :title => icon_label)
    content_tag 'div', fixed_button_to(icon_label, form_path, button_opts), 
      :class => 'icon_button'
  end

Note that each icon is confined within a div classed icon_button. This allows us to neatly style each button thus:

.icon_button {
  float: left;
  padding: 0 5px 0 0; /* each icon needs a small gap between */
  margin: 0;
}

Iconography

Using the above is simple. It's the same as a normal button_to., but you add the icon_type to the front... and make sure you actually have an icon named after that type sitting in the appropriate image directory. Examples follow:


<%= icon_button_to(:edit, 'Edit order', order_path(order.id), :method => :get) -%>
<%= icon_button_to(:show, 'See order', order_path(order), :method => :get) -%>
<%= icon_button_to(:submit, 'Submit order for processing', submit_order_path(order), 
  :method => :put,  :confirm => 'This will submit the order for processing. Are you sure?') if order.valid_to_submit? -%>
<%= icon_button_to(:delete, 'Delete this order', order_path(order),
  :method => :delete,  :confirm => 'This will delete this order permanently! Are you sure?') unless order.submitted? -%>

Friday, 18 April 2008

Google Geek Girl Dinner

Getting together with a whole bunch of geeky women and talking about interesting developments in IT in a toy-filled environment. That's a great way to spend the evening! Especially with speakers such as Rob Pike, Lindsay Ratcliffe and Stephanie Hannon.

Google provided the venue and catered the event. Google's Sydney office is right in the centre of town. It's a friendly, open environment full of geek toys (eg a Guitar Hero setup in the cafe) as well as wonderful and interesting people to chat with. We had a brief tour of the office, including being shown the Google rule of thumb that you should never be more than 100m away from chocolate at any time.

Then Rob spoke about the future tech trends, and a few things google is working on. Apparently there are lots of other applications in the works that he couldn't talk about, but promised they'd be amazing.

Lindsay followed with an entertaining overview of the process of bulding Customer Experience. Especially in having to translate between the very different world-views of the technical developers, the creative designers and the problem-focussed business people.

Finally, Stephanie gave a great overview of the GeoWeb - showing all the various ways in which people use mapping technology to share geographically-based information. Including such programs as Google Outreach's Crisis in Darfur project which overlays the sites of burned-out villages on satellite imagery to underscore the extent of the genocide occurring there.

I was really enthused to meet such a variety of interesting women from a wide range of technical backgrounds. Geek women are almost always a minority in the IT world. It's rare to have more than one in a development team, and so the chance to meet up to share both technical and cultural experience is a great opportunity. I suddenly feel that I'm not alone anymore. I even met one lady who shares my interest of someday going into space

GirlGeek Dinners has been instituted to support women in the IT community - to provide opportunities for women to create strong networks with others in the field, and to attract more young women into the the industry. There is A GirlGeek blog to let you know when events are scheduled, and a Facebook group if you wish to keep in touch with the others.

I enjoyed the evening and look forward to many more to come.

Friday, 11 April 2008

Button-up your actions

Google-safing dangerous actions is second-nature by now. Any action that will change the state of a resource gets hidden behind a button. Anything else is safe as a link. But a mix of links and buttons looks ugly to the user. We want a consistent-looking interface so the user knows that "clicking on one these does something to my Order". The user doesn't need to be confused by some wishy-washy interface representation of of the level of dangerousness of their click. That's one piece of information too many for somebody that just wants to Get Things Done. So our choice is:

  1. Expose our unsafe operations - and just wait for the day when a crawler comes in and deletes all our users' orders. or
  2. Hide the safe actions behind buttons - and fiddle a bit to match the Rails RESTful interface.

We're unlikely to get crawlers come in past our login screen and our users are exceptionally unlikely to crawl for a copy of the site... but un-buttoning our dangerous actions will leave us with our pants down - and that's surely gonna bite us where it hurts someday. It's not something I'm comfortable with doing. So we've opted for the latter approach.

That leaves us with making the buttons play nice with Rails' RESTful interface. Normally this is an easy operation. Just like you have a delete button that requires you to pass in the DELETE method, you can pass in the GET method for any get-oriented actions thus:

  <%= button_to('Edit', edit_user_order_path(@user, @order), :method => :get) -%>

Magical disappearing parameters

For a long while the above approach worked just fine... but then we came across an instance where we needed to add in extra parameters. We wanted to pass along a flag that tells the controller we're using the wizard interface, rather than the standard one. It lets us rediect the user to the next step in the process, rather than the usual confirmation screen. So I tried to create the buttons looking something like below:

  <% button_opts = {:user_id => @user.id, :id => @order.id, :wizard => true} -%>

  <%= button_to('Edit', edit_user_order_path(button_opts), :method => :get) -%>
  <%= button_to('Delete', user_order_path(button_opts), :method => :delete) -%>

For the POST and DELETE buttons this parameter came through fine. For the GET-buttons, however, the flag was conspicuously absent from the params hash. :(

Checking the source code gave me something like below (note: cleaned up for clarity):

<form method="post" action="/users/23/orders/42?wizard=true">
  <input name="_method" type="hidden" value="delete" />
  <input  type="submit" value="Delete" />
</form>
<form method="get" action="/users/23/orders/42/edit?wizard=true">
  <input type="submit" value="Edit" />
</form>

As you can see - they both carry the wizard flag in the URL - so there's nothing wrong with how the parameters are getting passed in at the front. But when you click on the Edit button - the flag is missing from the URL in my browser, and from the Parameters hash (displayed in the console)... so it's somehow getting dropped along the way.

Gazing deeper into the source code, you can see that the delete form still has method="post". It passes the DELETE via the hidden "_method" field. By comparison, the edit form has replaced the POST method with GET.

This seems to be the major difference. Somehow, the standard GET form isn't behaving nicely. I don't know why this bug is occurring - it could be a browser-bug, or Rails - and it really doesn't matter. I need to make buttons that am certain will work.

Luckily, we can use our knowledge of the hidden "_method" field to our advantage. The following code forces the get into the hidden field rather than the form. It works fine and the flag shows up happily on the other side.

  <% button_opts = {:user_id => @user.id, :id => @order.id, :wizard => true} -%>

  <%= button_to('Edit', edit_user_order_path(button_opts.merge('_method' => 'get'))) -%>
  <%= button_to('Delete', user_order_path(button_opts), :method => :delete) -%>

Thursday, 10 April 2008

Array.item_after(this)

There are just so many really useful functions in Ruby that every time I look I find something new and funky. So it always surprises me when I look to find something I'd expect to see - that just isn't there.

I was hoping to find an array function that returns "the next item in the list from the given one". I have a set of steps in a workflow all stored in a list. Given the current step, I want to know what the next one is going to be... so that I can send the user on to the next action from the current one.

Luckily Ruby is really easy to extend. ;)

These functions will return the item before/after the given item... or an item of any given offset from the current one. They return nil if the item can't be found in the given list or if the offset would put the index outside the bounds of the array.

class Array
  def item_after(item)
    offset_from(item, 1)
  end
  def item_before(item)
    offset_from(item, -1)
  end
  def offset_from(match, offset = 1)
    return nil unless idx = self.index(match)
    self[idx + offset]
  end
end

Usage:

>> arr = ['a','b','c','d']
=> ["a", "b", "c", "d"]
>> arr.item_after('a')
=> "b"
>> arr.item_before('d')
=> "c"
>> arr.offset_from('a',2)
=> "c"
>> arr.offset_from('d',-3)
=> "a"
>> arr.offset_from('d',10)
=> nil
>> arr.item_after('purple')
=> nil

Monday, 7 April 2008

2.0 update or not 2.0 update: Is Rails 2.0 ready yet?

I had a play with converting one of my smaller playthings... er projects over to Rails 2.0 the other day. So far I see no convincing reason to convert yet. There were some small improvements, but I came across a couple of glaring bugs that make rake test incredibly painful to use.

When I find the specific ticket-numbers again I'll include them here, but to briefly describe the issue: The bug caused the setup method not to be called (ever) in unit tests. This meant that tests didn't get their fixtures fresh after every test. The several attempts to fix this issue somehow interfered with another bug that caused the controller not to be loaded in the functional tests.

Both of these situations are so impossible to work with that I'd recommend steering clear of Rails 2.0 for a short while.

I can see it being worthwhile using Rails 2.0 for new/greenfields projects - and putting up with the bugs until the Rails Core Team figure out how to fix them. But I would seriously recommend against it for any pre-existing project - especially one that is currently in a commercial/production environment. The pain of having to convert a complete test-suite over to work around the bugs (or to convert it all over to RSpec) isn't worth the effort just yet.

Conclusion: Yes, but not yet.