Thursday, April 30, 2009

Down the Sort Hole

‹prev | My Chain | next›

With an understanding between me and couchdb-lucene sorting, I start back with implementation. In the Sinatra application's spec for /recipes/search, I add:
    it "should sort" do
RestClient.should_receive(:get).
with(/sort=title/).
and_return('{"total_rows":30,"skip":0,"limit":20,"rows":[]}')

get "/recipes/search?q=title:egg&sort=title"
end
I make this example pass by simply passing the sort parameter through to couchdb-lucene:
data = RestClient.get "#{@@db}/_fti?limit=20&skip=#{skip}&q=#{params[:q]}&sort=#{params[:sort]}"
That spec may pass, but my cucumber scenario no longer does:
cstrom@jaynestown:~/repos/eee-code$ cucumber -n features \
-s "Sorting (name, date, preparation time, number of ingredients)"
Feature: Search for recipes

So that I can find one recipe among many
As a web user
I want to be able search recipes

Scenario: Sorting (name, date, preparation time, number of ingredients)
Given 50 "delicious" recipes with ascending names, dates, preparation times, and number of ingredients
And a 0.5 second wait to allow the search index to be updated
When I search for "delicious"
HTTP status code 400 (RestClient::RequestFailed)
/usr/lib/ruby/1.8/net/http.rb:543:in `start'
./features/support/../../eee.rb:30:in `GET /recipes/search'
(eval):7:in `get'
features/recipe_search.feature:79:in `When I search for "delicious"'
Then I should see 20 results
When I click the "Name" column header
...
The cucumber scenario is not even reaching the sorting steps—it is failing on the simple search-for-a-string step. The cause of the failure is couchdb-lucene's dislike of empty (or non-indexed) sort fields. I have to guard against empty sort parameters:
    it "should not sort when no sort field is supplied" do
RestClient.stub!(:get).
and_return('{"total_rows":30,"skip":0,"limit":20,"rows":[]}')

RestClient.should_not_receive(:get).with(/sort=/)

get "/recipes/search?q=title:egg&sort="
end
I can implement this example thusly:
get '/recipes/search' do
@query = params[:q]

page = params[:page].to_i
skip = (page < 2) ? 0 : ((page - 1) * 20) + 1

couchdb_url = "#{@@db}/_fti?limit=20" +
"&q=#{@query}" +
"&skip=#{skip}"

if params[:sort] =~ /\w/
couchdb_url += "&sort=#{params[:sort]}"
end

data = RestClient.get couchdb_url

@results = JSON.parse(data)

if @results['rows'].size == 0 && page > 1
redirect("/recipes/search?q=#{@query}")
return
end

haml :search
end
With that, my Cucumber scenarios are again passing and I am ready to proceed with the view / helper work.
(commit)

Shortly after starting work in the Haml template, the sort field gets unwieldy, which is a good indication that it ought to be a helper. I opt for the name of sort_link for the helper and build the following examples to describe how it should work:
describe "sort_link" do
it "should link the supplied text" do
sort_link("Foo", "sort_foo", "query").
should have_selector("a",
:content => "Foo")
end
it "should link to the query with the supplied sort field" do
sort_link("Foo", "sort_foo", "query").
should have_selector("a",
:href => "/recipes/search?q=query&sort=sort_foo")
end
end
I implement this code as:
    def sort_link(text, sort_on, query)
id = "sort-by-#{text.downcase}"
url = "/recipes/search?q=#{query}&sort=#{sort_on}"
%Q|#{text}|
end
There are no example for the link's id. That is semantic information, having nothing to do with behavior of the application. The only reason to include it is for styling and, more importantly, the Cucumber scenario.

Speaking of the Cucumber scenario, I am now ready to implement the next step, Then the results should be ordered by name in ascending order, which is aided by some CSS selector fanciness:
Then /^the results should be ordered by name in ascending order$/ do
response.should have_selector("tr:nth-child(2) a",
:content => "delicious recipe 1")
response.should have_selector("tr:nth-child(3) a",
:content => "delicious recipe 10")
end
The first child of the results table is the header, which is the reason the first selector is looking for the second child. The reason for the second test is that I want to ensure that sorting has taken place. The "delicious recipe 1" was the first recipe entered, so it may show up in the results list first for that reason alone. But "delicious recipe 10" will come before "delicious recipe 2" only if they have been sorted (because the "1" in "10" comes before "2" when performing text sorting).
(commit)

Up next: reversing the sort order.

No comments:

Post a Comment