« March 2008 | Main | May 2008 »

April 21, 2008

HTTP Ranges for Pagination?

Listen to this articleListen to this article

Would it be a gross perversion to use HTTP ranges for pagination?:

Client asks the server what range types it accepts for people:

HEAD /people HTTP/1.1

Server responds:

Status: 200
Accept-Ranges: pages; records

Client requests the first page of people:

GET /people HTTP/1.1
Range: pages=1-1

Server Responds:

Status: 206
Content-Range: pages 1-1/13

April 17, 2008

Finding the index of an item using a block

Listen to this articleListen to this article

Update: Ruby 1.8.7 also has this.

Ruby 1.9 has it but if you're not that bleeding edge, you can have it now:

class Array
  def index_with_block(*args)
    return index_without_block(*args) unless block_given?
    each_with_index do |entry, index|
      return index if yield(entry)
    end
    nil
  end
  alias_method :index_without_block, :index
  alias_method :index, :index_with_block

  def rindex_with_block(*args)
    return rindex_without_block(*args) unless block_given?
    
    index = size
    reverse_each do |entry|
      index -= 1
      return index if yield(entry)
    end
    nil
  end
  alias_method :rindex_without_block, :rindex
  alias_method :rindex, :rindex_with_block
end

If you're using Rails you can substitute the two calls each to alias_method with a single call to alias_method_chain.

April 15, 2008

It's OK for GET Requests to Update the Database

Listen to this articleListen to this article

We've all been indoctrinated into associating the HTTP request methods POST, GET, PUT and DELETE with the standard database (aka CRUD) operations Create (INSERT), Read (SELECT), Update and Delete respectively. For the most part the analogue holds. When we make a GET request, our intention is to read whatever the server hands back. When we POST some data, our intention is to update something.

The general view however, seems to be that the HTTP methods relate directly to database operations. In fact many developers seem to think that they are in fact one in the same thing: POST is for INSERTing data, GET for SELECTing, etc. The popularity of which seems to have strengthened with the growing interest in REST and the wide-spread adoption of Rails 21. In fact read of the HTTP/1.1 Method Definitions specification explicitly states that so-called "Safe Methods" such as GET:

... SHOULD NOT have the significance of taking an action other than retrieval.

But what happens when we have a site that tracks where you have visited, updates the "last read date" on each record retrieved, or remembers the last search criteria you used? Each of these features requires recording information into some kind of database--be it relational or simply a log file.

What people seem to overlook is the paragraph that follows in the same document:

Naturally, it is not possible to ensure that the server does not generate side-effects ... The important distinction here is that the user did not request the side-effects, so therefore cannot be held accountable for them.

The HTTP methods should be used to indicate the user's intention without regard to the underlying implementation. The web application is an abstraction so we need to model the interaction on that abstraction. If the user's intention is to make a change to something then go ahead and use a PUT but if they're only reading some data use a GET even if you know it involves some database writes.

It may seem somewhat esoteric but spending a bit of time thinking about what the user's intention is exactly has helped me better flesh out an application's API.

1 I don't mean to imply that Rails is the culprit here. Nothing in Rails explicitly makes these assertions. However the fact that the idiom is explicitly referred to as CRUD resources certainly doesn't help.

April 14, 2008

Getting Too Fancy with HTTP Response Codes

Listen to this articleListen to this article

As part of my adoption of REST and all its goodness, I've started using HTTP response codes more, responsibly ;-) So, for example, instead of always returning 200 (OK) for just about everything, I'm using 201 (Created) with a Location header set to the new URL after a POST. For PUT, I send back 204 (No Content), a 404 (Not Found) after a GET for a resource that no longer exists, and a good old 200 (OK) after a successful DELETE or GET.

Interestingly, in the system I'm developing at present, an update (PUT) might actually cause a resource to move due to the application of server-side business rules. In this case, the 204 response also sets the Location header so that the client knows where it can be found.

All this was working beautifully on my local machine using both Safari and Firefox so once I was happy with the result I deployed it into the remote test site and started playing in FireFox. So far so good. Everything checks out. Next let's try Safari...not so great.

Some bits of the application worked just fine but others seemed to have no effect. Then mysteriously things would start working again. Even stranger was the fact that hitting the browser's refresh button had no effect either.

At first I suspected that nesting Ajax calls might be to blame but as everything seemed to work perfectly in FireFox and a Google search turned up nothing, I decided to do some more investigation.

I logged in to the server box and tailed the logs for signs of life. Everything looked normal. All the expected requests and responses were there but still nothing client side. Using Safari's new Network Timeline I could see what the browser thought was going on. All the requests and responses were there but something was odd. In all but a few cases, the response code was 204 (No Content). I double checked the server logs but no, the server was definitely sending back the correct responses; a mixture of 204, 200 and 404 as appropriate.

On a hunch I went back and re-read the HTTP Status Codes document and in particular the definition for 204:

The server has fulfilled the request but does not need to return an entity-body ... the client SHOULD NOT change its document view from that which caused the request to be sent ...

That might actually explain it. If Safari received a 204 and interpreted that to mean "Don't change anything" then hitting refresh would indeed have no effect even if my code subsequently went on to perform more asynchronous requests as a result.

So, I dutifully changed all the 204s to 200s and voila! Safari started to behave just as expected and FireFox continued to work as had previously.

I've also noticed a difference in the way both browsers handle 303 (Redirect) from within an XML HTTP Request: Safari performs the redirect and keeps all the headers as per the original, whereas FireFox seems to essentially construct an entirely new request. The upshot is that you can't actually detect (server-side) an XML HTTP Request from FireFox if it is the result of a redirect.

I'm really not sure why the two browsers have such differing opinions of what the appropriate behaviour should be in either case but I hope this helps some other poor sod keep from pulling their hair out.

April 11, 2008

Drag & Drop Prioritizable Lists

Listen to this articleListen to this article

Yes, it's true, Scriptaculous already provides a Sortable that makes it almost trivial to enable drag'n'drop sorting of your HTML lists. Whenever an item is moved an onUpdate() event is called (if provided) allowing you to inspect the new order and presumably perform an AJAX request to record the change. In principle, this sounds great but I've never really liked it for a couple of reasons.

For a start, if you have any appreciable number of items updating each in the database just to re-order one seems somewhat unnecessary. Not withstanding the fact that we need to send all those ids to the server in the first place.

Secondly, if you're doing any kind of filtering, it's difficult at best to take the newly constructed ordering and apply that at the back-end; what happens to all the items that may be lurking in between that aren't presently displayed?

Enter Prioritizable (itself built on top of Sortable).

You use it in much the same way as Sortable with the major difference being that the onUpdate() event is called with three arguments: the item that was moved, the sibling relative to which it was moved, and the relative position ("higher" or "lower"). And, if like me, you're feeling a bit RESTful, it's pretty easy to turn these arguments into a nice semantic URL and parameters as shown:

Prioritizable.create($("chores"), {
  onUpdate: function(item, position, sibling) {
    id = item.substring(6);                           // "chore_17" => "17"
    sibling_id = to.substring(6);                     // "chore_2" => "2"
    url = "/chores/" + sibling_id + "/" + position;   // "/chores/2/higher"
    
    new Ajax.Request(url, {
      method: "post",
      parameters: { id: id }
    });
  }
});

When the onUpdate() event is called we POST the id of the item to be moved to a path constructed from the id of the sibling and the relative position. Assuming the the user moves chore_17 just above chore_2 we would POST "id=17" to /chores/2/higher.

In practice, I combine this client-side behaviour with some server-side code that provides move_higher_than() and move_lower_than() methods that efficiently handle all the necessary database updates.

All the pieces mentioned will eventually be available alongside Cogent's other Rails plugins but until then, here's enough of the Javascript side of things to get you going.

var Prioritizable = {
  create: function(element) {
    options = Object.extend(arguments[1] || {}, {
      onChange: Prioritizable.onChange,
      onUpdate: Prioritizable.onUpdate  
    });

    Sortable.create(element, options);
  },

  destroy: function(element) {
    Sortable.destroy(element);
  },

  onChange: function(item) {
    Sortable.options(item)._item = item;
  },

  onUpdate: function(element) {
    options = Sortable.options(element);
    item = options._item;
    options._item = null;

    sibling = item.previous();
    if (other) {
      position = "higher";
    } else {
      sibling = item.next();
      position = "lower"
    }
    
    options.onUpdate(item, position, sibling);
  }
};

Enjoy!

April 09, 2008

Getting Chronic to Parse Non-U.S. Dates

Listen to this articleListen to this article

In an attempt to push yet more behaviour from my Rails controllers into model classes, I was extracting some code into a yet-to-be-published plugin that allows date and time columns to be set using more human-readable values. For examples:

>> task.completed_at = "now"
>> task.completed_at
=> Wed Apr 09 14:39:12 +1000 2008

Although my actual requirement was to support "now" and "today" I figured it would be rather cool if I could support anything that Chronic does. (If you haven't used Chronic before, it's a natural language date/time parser written in pure Ruby that understands a vast array of expressions including ranges.)

Naturally (no pun intended), Chronic also supports explicit dates such as "1/2/08". As part of my testing however, I discovered that the date parsing is decidedly US-centric presuming, of course, that dates are specified as "month-day-year". So for anyone living in say, Australia, it can be pretty frustrating to have Chronic.parse("1/2/08") return "Wed Jan 02 12:00:00 +1100 2008" rather than the expected "Fri Feb 01 12:00:00 +1100 2008".

The good news is, there is a solution. The bad news is, the solution is far from elegant. But first some context.

Chronic is actually written very nicely and the code is fairly easy to follow. In essence it works as follows: the input string is tokenized; each token is inspected to see if it's a keyword such as a month name, day name, or a number, etc.; and finally tries to matches the sequence of tokens against a pattern such as "a day number followed by a month name and then a year." The problem arises because the pattern for matching "month-day-year" comes before the one for "day-month-year" meaning that unless the first number is greater than 12, Chronic will always consider it to be a month.

The less than elegant solution is almost trivial and involves switching the order in which the patterns are matched. Doing so returns the desired result:

>> Chronic.parse("1/2/08")
=> "Fri Feb 01 12:00:00 +1100 2008"

Which is all very well and good but now we have the reverse problem. What would be better is if we had a more general solution, one that allows us to specify the desired precedence when parsing:

>> Chronic.parse("1/2/08")                                    # Default to U.S. date formats
=> Wed Jan 02 12:00:00 +1100 2008

>> Chronic.parse("1/2/08", :explicit_date_format => :non_us)  # Prefer Non-U.S. formats
=> Fri Feb 01 12:00:00 +1100 2008

>> Chronic.parse("1/2/08", :explicit_date_format => :us)      # Prefer U.S. formats
=> Wed Jan 02 12:00:00 +1100 2008

Among other things, this allows us to have users in Australia enter dates with one format and users in the U.S. another.

For anyone interested, I've pasted a diff for each approach below.

A less than elegant solution:

--- a/chronic-0.2.3/lib/chronic/handlers.rb
+++ b/chronic-0.2.3/lib/chronic/handlers.rb
@@ -13,8 +13,8 @@ module Chronic
                  Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
                  Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
                  Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
-                 Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
                  Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy),
+                 Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
                  Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
                  Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)],

A more general solution:

--- a/chronic-0.2.3/lib/chronic/chronic.rb
+++ b/chronic-0.2.3/lib/chronic/chronic.rb
@@ -43,7 +43,8 @@ module Chronic
       default_options = {:context => :future,
                          :now => Time.now,
                          :guess => true,
-                         :ambiguous_time_range => 6}
+                         :ambiguous_time_range => 6,
+                         :explicit_date_format => :us}
       options = default_options.merge specified_options
             
       # ensure the specified options are valid
@@ -51,6 +52,7 @@ module Chronic
         default_options.keys.include?(key) || raise(InvalidArgumentException, "#{key} is not a valid option key.")
       end
       [:past, :future, :none].include?(options[:context]) || raise(InvalidArgumentException, "Invalid value ':#{options[:context]}' for :context specified. Valid values are :past and :future.")
+      [:us, :non_us].include?(options[:explicit_date_format]) || raise(InvalidArgumentException, "Invalid value ':#{options[:explicit_date_format]}' for :explicit_date_format specified. Valid values are :us and :non_us.")
       
       # store now for later =)
       @now = options[:now]
--- a/chronic-0.2.3/lib/chronic/handlers.rb
+++ b/chronic-0.2.3/lib/chronic/handlers.rb
@@ -3,41 +3,50 @@ module Chronic
 	class << self
 	  
 	  def definitions #:nodoc:
-	    @definitions ||= 
-      {:time => [Handler.new([:repeater_time, :repeater_day_portion?], nil)],
+	    if @definitions.nil?
+        us_date = [Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, :time_zone, :scalar_year], :handle_rdn_rmn_sd_t_tz_sy),
+                   Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
+                   Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
+                   Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
+                   Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
+                   Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
+                   Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
+                   Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
+                   Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy),
+                   Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
+                   Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)]
+
+        non_us_date = us_date.dup
+        non_us_date[7] = us_date[8]
+        non_us_date[8] = us_date[7]
+
+	      @definitions = 
+        {:time => [Handler.new([:repeater_time, :repeater_day_portion?], nil)],
         
-       :date => [Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, :time_zone, :scalar_year], :handle_rdn_rmn_sd_t_tz_sy),
-                 Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
-                 Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
-                 Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
-                 Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
-                 Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
-                 Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
-                 Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
-                 Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy),
-                 Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
-                 Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)],
+         :date => {:us => us_date, :non_us => non_us_date},
                  
-       # tonight at 7pm
-       :anchor => [Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
-                   Handler.new([:grabber?, :repeater, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
-                   Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)],
+         # tonight at 7pm
+         :anchor => [Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
+                     Handler.new([:grabber?, :repeater, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
+                     Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)],
                    
-       # 3 weeks from now, in 2 months
-       :arrow => [Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
-                  Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
-                  Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)],
+         # 3 weeks from now, in 2 months
+         :arrow => [Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
+                    Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
+                    Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)],
                   
-       # 3rd week in march
-       :narrow => [Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
-                   Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)]
-      }
+         # 3rd week in march
+         :narrow => [Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
+                     Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)]
+        }
+      end
+      @definitions
     end
     
     def tokens_to_span(tokens, options) #:nodoc:                   
       # maybe it's a specific date
       
-      self.definitions[:date].each do |handler|
+      self.definitions[:date][options[:explicit_date_format]].each do |handler|
         if handler.match(tokens, self.definitions)
           puts "-date" if Chronic.debug
           good_tokens = tokens.select { |o| !o.get_tag Separator }

April 02, 2008

Fixing lowpro form submission failures

Listen to this articleListen to this article

I finally worked out why my forms weren't submitting when the user hits the ENTER key. lowpro serializes the button that was clicked along with any other parmeters when submitting a form via AJAX. However, when the user hits enter under FireFox, there is no button and consequently the browser barfs. Safari on the other hand tries to be too helpful and triggers an onclick event for the first submit button (which is why I never noticed it).

So anyway, rather than try to be too clever myself, I simply changed the parameter serialization in Remote.Form.onsubmit to look like:

parameters : this.element.serialize({ submit: this._submitButton ? this._submitButton.name : null })

Problem solved.

Shameless Plugs

Recommend me on Working With Rails

Simian (Similarity Analyser): Rapidly identifies duplication in Java, C#, C, C++, COBOL, Ruby, JSP, ASP, HTML, XML, SQL, Visual Basic source code and even plain text files.

Beginning Algorithms: A good understanding of algorithms, and the knowledge of when to apply them, is crucial to producing software that not only works correctly, but also performs efficiently.

Blogroll

Creative Commons License
This weblog is licensed under a Creative Commons License.

Powered by
Movable Type 3.2