Sunday, September 13, 2009

Alternate Preparations Helper

‹prev | My Chain | next›

Today, I continue working on the alternate recipe preparations. After a small spike yesterday, I know how I would like to access the lists of alternate preparations for any given recipe (via a recipes/alternatives view) and how the results should look:
cstrom@jaynestown:~/repos/eee-code$ curl \
http://localhost:5984/eee/_design/recipes/_view/alternatives\?key=\"2004-02-04-squid\"
{"total_rows":8,"offset":5,"rows":[
{"id":"d16f491629b78a5bb13adb49ec6bee00",
"key":"2004-02-04-squid",
"value":["2002-08-12-sauce",
"2003-01-17-sausage_pasta",
"2003-02-13-pasta",
"2003-02-16-sauce",
"2003-04-07-cavatelli",
"2004-04-26-sauce",
"2006-02-14-sauce"]
}
I have been dumping code into my Sinatra helpers for some time now and it is beginning to show. I have reached 300 lines of code in helpers.rb. Making matters worse, I have code that is not directly used in the Sinatra app or the Haml templates. I may need to refactor for better cohesion. Before getting it right, I will get it done.

First up, I write an RSpec example describing that a helper method should ask CouchDB for alternate preparations:
describe "couch_alternatives" do 
it "should ask CouchDB for alternates" do
RestClient.
should_receive(:get).
with(/alternatives.+key=.+2000-09-13-recipe/).
and_return('{"rows": [] }')
couch_recipe_updated_by('2000-09-13-recipe')
end
end
After adding code to resolve failures for no such method, wrong number of arguments and not receiving the expected arguments, I end up with this helper:
    def couch_alternatives(permalink)
url = "#{_db}/_design/recipes/_view/alternatives?key=%22#{permalink}%22"
RestClient.get url
end
That will return JSON, which is going to be too helpful. I would rather return the results of the "rows" attribute above:
  it "should be a list of IDs" do
RestClient.
stub!(:get).
and_return('{"rows": [{"value": ["2000-09-13-recipe"]}] }')

couch_alternatives('2009-09-12-recipe').
should == ['2000-09-13-recipe']
end
As expected, that fails because the function still returns JSON:
cstrom@jaynestown:~/repos/eee-code$ spec ./spec/eee_helpers_spec.rb 
................................................................................F

1)
'couch_alternatives should be a list of IDs' FAILED
expected: ["2000-09-13-recipe"],
got: "{\"rows\": [{\"value\": [\"2000-09-13-recipe\"]}] }" (using ==)
./spec/eee_helpers_spec.rb:568:

Finished in 0.113826 seconds

81 examples, 1 failure
To get that passing, I store the JSON in a local variable and parse it:
    def couch_alternatives(permalink)
url = "#{_db}/_design/recipes/_view/alternatives?key=%22#{permalink}%22"
data = RestClient.get url
results = JSON.parse(data)['rows']
results.first && results.first['value']
end
Next up is the actual helper that will be used in the Haml templates, which I will call alternate_preparations. The ultimate goal of this helper is a comma separated list of recipe links, when alternate preparations are present:
describe "alternative_preparations" do
it "should return nothing if there are no alternate preparations"
it "should return a comma separated list of links to recipes"
end
First I need to use the couch_alternates helper to pull back the IDs:
  it "should retrieve IDs" do
should_receive(:couch_alternatives).and_return([])
alternative_preparations('2009-09-13')
end
After implementing that, I need to post JSON keys to CouchDB to pull back a list of recipe titles:
  it "should retrieve titles for each alternate ID" do
RestClient.
should_receive(:post).
with(/titles/, '{"keys":["2000-09-13-recipe","2001-09-13-recipe"]}').
and_return('{"rows": [] }')
alternative_preparations('2009-09-13')
end
I make that pass with:
    def alternative_preparations(permalink)
ids = couch_alternatives(permalink)
recipes = RestClient.post "#{_db}/_design/recipes/_view/titles",
%Q|{"keys":[#{ids.map{|id| "\"#{id}\""}.join(',')}]}|

end
It took a while to get that example passing because the example is so tied to implementation of the JSON output (no spaces, double quotes, etc.). I stop there for the day. I will pick back up completing the alternative_preparations helper tomorrow and, hopefully, working my way back out to the Cucumber scenario describing this feature.

Saturday, September 12, 2009

Prototyping Alternatives

‹prev | My Chain | next›

I spend a good deal of time today messing about with the CouchDB view for alternate recipe preparations. I will have documents describing these alternate preparations of the form:
{
"_id": "d16f491629b78a5bb13adb49ec6bee00",
"_rev": "2-4002233925",
"name": "tomato_sauce",
"type": "Alternative",
"recipes": [
"2002-08-12-sauce",
"2003-01-17-sausage_pasta",
"2003-02-13-pasta",
"2003-02-16-sauce",
"2003-04-07-cavatelli",
"2004-02-04-squid",
"2004-04-26-sauce",
"2006-02-14-sauce"
]
}
That is a real document. My wife's family is Italian and they have many, many different types of sauce. I am a little surprised that there is not one with a potato in it—something to add to the cookbook another day.

At any rate, I would like a view for alternate preparations such that I can lookup a recipe by ID and see a list of all alternate preparations for that recipe, excluding itself. I eventually work out this view:
function(doc) {
if (doc['type'] == 'Alternative') {
for (var i=0; i < doc['recipes'].length; i++) {
var alternatives = [];
for (var j=0; j < doc['recipes'].length; j++) {
if (i != j) {
alternatives.push(doc['recipes'][j]);
}
}
emit(doc['recipes'][i], alternatives);
}
}
}
That is not too complex a Javascript function, my main difficulty came when I omitted a closing parenthesis and got this unhelpful error:
cstrom@jaynestown:~/repos/eee-code$ curl http://localhost:5984/eee/_design/recipes/_view/alternatives\?key=\"2004-02-04-squid\"
{"error":"case_clause","reason":"{{bad_return_value,{os_process_error,{exit_status,0}}},\n
{gen_server,call,\n
[<0.12199.3>,\n
{prompt,[<<\"add_fun\">>,\n
<<\"function(doc) {\\n if (doc['type'] == 'Alternative') {\\n for (var i=0; i < doc['recipes'].length; i++) {\\n var alternatives = [];\\n for (var j=0; j < doc['recipes'].length; j++) {\\n if (i != j) {\\n alternatives.push(doc['recipes'][j];\\n }\\n }\\n emit(doc['recipes'][i], alternatives);\\n }\\n }\\n}\\n\">>]},\n
infinity]}}"}
"infinity"? Really?

The lesson that I take away from this is that, as easy as the couch_docs gem makes it to upload design documents, it has no Javascript error checking. I either need to add it (and a javascript testing framework while I am at it) or do my initial view building in the futon web interface.

With working view in hand, I can try it out from the command line:
cstrom@jaynestown:~/repos/eee-code$ curl \
http://localhost:5984/eee/_design/recipes/_view/alternatives\?key=\"2004-02-04-squid\"
{"total_rows":8,"offset":5,"rows":[
{"id":"d16f491629b78a5bb13adb49ec6bee00",
"key":"2004-02-04-squid",
"value":["2002-08-12-sauce",
"2003-01-17-sausage_pasta",
"2003-02-13-pasta",
"2003-02-16-sauce",
"2003-04-07-cavatelli",
"2004-04-26-sauce",
"2006-02-14-sauce"]
}
]}
As desired, I have all of the recipes that I wanted in the "value" attribute of the results.

Rather than diving into code, I finish off my work tonight by prototyping how I might pull back those recipes for ultimately displaying them in the recipes. I will need the title of the recipes in addition to the IDs. For that, I create another, simple recipe view:
function(doc) {
if (doc['type'] == 'Recipe') {
emit(doc['_id'], doc['title']);
}
}
Given a document ID, such as "2002-08-12-sauce", this "title" view will return the recipe's title. In the case of our tomato sauce recipes, that will amount to 7 separate calls to the CouchDB server. I would just as soon avoid that if I can. And I can using a somewhat obscure feature of CouchDB views: POSTing multiple keys to get an array result. In this case, I take the results of the "alternatives" view for the tomato sauce recipes and POST it to the title view to find:
cstrom@jaynestown:~/repos/eee-code$ curl -X POST \
http://localhost:5984/eee/_design/recipes/_view/titles \
-d '{"keys": ["2002-08-12-sauce","2003-01-17-sausage_pasta","2003-02-13-pasta","2003-02-16-sauce","2003-04-07-cavatelli","2004-04-26-sauce","2006-02-14-sauce"]}'
{"total_rows":578,"offset":175,"rows":[
{"id":"2002-08-12-sauce","key":"2002-08-12-sauce","value":"Tomato Sauce"},
{"id":"2003-01-17-sausage_pasta","key":"2003-01-17-sausage_pasta","value":"Farfalle with Spicy Sausage Sauce"},
{"id":"2003-02-13-pasta","key":"2003-02-13-pasta","value":"Penne with Vodka Sauce"},
{"id":"2003-02-16-sauce","key":"2003-02-16-sauce","value":"Sausage Cream Sauce"},
{"id":"2003-04-07-cavatelli","key":"2003-04-07-cavatelli","value":"Cavatelli with Mushroom Tomato Sauce"},
{"id":"2004-04-26-sauce","key":"2004-04-26-sauce","value":"Quick Tomato Sauce"},
{"id":"2006-02-14-sauce","key":"2006-02-14-sauce","value":"Thick Tomato Sauce"}
]}
That is a good stopping point for today. With some successful prototyping completed, I will pick up with implementation tomorrow.

Friday, September 11, 2009

Holy Crap

‹prev | My Chain | next›

With the recipe update feature complete (or at least effectively pushed off for completion another day), I am extremely close to being feature complete:
cstrom@jaynestown:~/repos/eee-code$ cucumber features/
...
37 scenarios (2 undefined, 1 pending, 34 passed)
334 steps (9 skipped, 7 undefined, 1 pending, 317 passed)
0m39.874s
To be sure, there is much code clean-up that can be done and some optimization (caching would be a very good thing), but wow, only 2 undefined scenarios?

Maybe I can cut that to one tonight...

The last remaining feature is alternate recipe preparation (Robin's family has many ways to make tomato sauce). The feature in Cucumber format:
Feature: Alternate preparations for recipes

As a user curious about a recipe
I want to see a list of similar recipes
So that I can find a recipe that matches my tastes or ingredients on hand

Scenario: No alternate preparation

Given a "pancake" recipe with "buttermilk"
And no other recipes
When I view the recipe
Then I should see no alternate preparations

Scenario: Alternate preparation

Given a "Hearty Pancake" recipe with "wheat germ"
And a "Buttermilk Pancake" recipe with "buttermilk"
And a "Pancake" recipe with "chocolate chips"
When the three pancake recipes are alternate preparations of each other
And I visit the "Hearty Pancake" recipe
Then I should see a link to the "Buttermilk Pancake" recipe
And I should see a link to the "Pancake" recipe
And I should not see a link to the "Hearty Pancake" recipe
When I click the "Buttermilk Pancake" link
Then I should see a link to the "Hearty Pancake" recipe
And I should see a link to the "Pancake" recipe
And I should not see a link to the "Buttermilk Pancake" recipe
Sweet! That first scenario is a happy path description of how recipes behave normally (when no alternates exist). Even better, the second step is a no-op and can be defined as:
Given /^no other recipes$/ do
# nop
end
Thanks to previously defined steps, there is but one step remaining:



On the legacy site, the alternate preparations look like:



So the absence of an alternate preparation can be defined as:
Then /^I should see no alternate preparations$/ do
response.should_not contain('Alternate Preparations')
end
Since I have not created the feature yet, this passes right away:



That may seem like a useless scenario. The point is to ensure that nothing changes for non-alternate preparation recipes when I implement the feature.

Just like that, I am down to one remaining scenario. At first glance, the step definitions in that last scenario are almost done:



I say "at first glance" because there is a little too much bold in the blue steps. I want to verify that the text inside the quotes of those step are links on the pages. The extra bold is telling me that the step is going to look for everything bold.

I cannot simply define another, more specific step definition. This is Cucumber, not CSS. Instead I need to change the old step definition to no longer match:
Then /^I should see a link to (.+)$/ do |date|
response.should have_selector("a", :content => date)
end
So, instead of the match anything (.) RexExp, I match anything but a quote:
Then /^I should see a link to ([^\"]+)$/ do |date|
response.should have_selector("a", :content => date)
end
Since that step no longer matches the steps in the current scenario (because they have quotes), I can define those steps as:
Then /^I should see a link to the "([^\"]*)" recipe$/ do |title|
response.should have_selector("a", :content => title)
end
With that, I have good bold:



I do not normally define steps before doing inside work (preferring to do them afterwards to verify the inside work), but those steps were obviously wrong. While I am on a roll, I might as well finish the last two steps off. Making use of the instance variable lookup from the updated recipes features, I can define the visit the recipe step as:
When /^I visit the "Hearty Pancake" recipe$/ do
visit "/recipes/#{@permalink_identified_by['wheat germ']}"
end
The last undefined cucumber step in my entire application(!) describes the alternate preparations document. On the really old, XML document based legacy site (the one that pre-dates the Rails legacy site) the alternates preparation documents were a simple list of the recipes:
<recipes>
<recipe year="2002" month="08" day="12" label="sauce"/>
<recipe year="2003" month="01" day="17" label="sausage_pasta"/>
<recipe year="2003" month="02" day="13" label="pasta"/>
<recipe year="2003" month="02" day="16" label="sauce"/>
<recipe year="2003" month="04" day="07" label="cavatelli"/>
<recipe year="2004" month="02" day="04" label="squid"/>
<recipe year="2004" month="04" day="26" label="sauce"/>
<recipe year="2006" month="02" day="14" label="sauce"/>
</recipes>
To do the same on the CouchDB based site, I can define the alternate preparations document as:
Given /^the three pancake recipes are alternate preparations of each other$/ do
alternates = {
:type => "Alternate",
:name => "pancakes",
:recipes => [
@permalink_identified_by['wheat germ'],
@permalink_identified_by['buttermilk'],
@permalink_identified_by['chocolate chips']
]
}

RestClient.put "#{@@db}/pancake_alternates",
alternates.to_json,
:content_type => 'application/json'
end
With that I have all of my steps defined:
cstrom@jaynestown:~/repos/eee-code$ cucumber   features/recipe_alternate_preparations.feature:14
Sinatra::Test is deprecated; use Rack::Test instead.
Feature: Alternate preparations for recipes

As a user curious about a recipe
I want to see a list of similar recipes
So that I can find a recipe that matches my tastes or ingredients on hand

Scenario: Alternate preparation
Given a "Hearty Pancake" recipe with "wheat germ"
And a "Buttermilk Pancake" recipe with "buttermilk"
And a "Pancake" recipe with "chocolate chips"
When the three pancake recipes are alternate preparations of each other
And I visit the "Hearty Pancake" recipe
Then I should see a link to the "Buttermilk Pancake" recipe
expected following output to contain a <a>Buttermilk Pancake</a> tag:

...
And I should see a link to the "Pancake" recipe
And I should not see a link to the "Hearty Pancake" recipe
When I click the "Buttermilk Pancake" link
Then I should see a link to the "Hearty Pancake" recipe
And I should see a link to the "Pancake" recipe
And I should not see a link to the "Buttermilk Pancake" recipe

Failing Scenarios:
cucumber features/recipe_alternate_preparations.feature:14 # Scenario: Alternate preparation

1 scenario (1 failed)
12 steps (1 failed, 6 skipped, 5 passed)
0m0.675s
The steps are defined, but not passing because I still need to actually write the code to implement the feature. I will pick that up tomorrow at which point, the site should be (dare I say it?) feature complete!

Thursday, September 10, 2009

Excluding couchdb-lucene Documents (or trying to)

‹prev | My Chain | next›

Tonight is one of those days that I make a small offering to the gods of my chain. I spent most of the night at Kevin Smith's presentation to the DC Erlang group, limiting the time that I can devote to the chain.

I have moved onto the next Cucumber scenario of the recipe update feature. This scenario describes how recipes that have been updated are suppressed in couchdb-lucene search results.

The scenario currently reads:
   Scenario: Searching for a recipe with an update
Given a "Buttermilk Pancake" recipe with "buttermilk" in it
And a "Buttermilk Pancake" recipe on another day with "lowfat milk" in it
When the "buttermilk" recipe is marked as update of the "lowfat milk" recipe
And I search for "pancake"
Then I should see the "buttermilk" recipe in the results
And I should not see the "lowfat milk" recipe in the results
The first few steps are already defined, though I do need to update the given-recipe steps to ensure that they get indexed—the transform function requires that recipe documents are of type "Recipe" and have been published:
Given /^a "([^\"]*)" recipe (.*)with "([^\"]*)" in it$/ do |title, on, ingredient|
date = (on == "") ? Date.new(2009, 9, 5) : Date.new(2000, 9, 5)
permalink = date.to_s + "-" + title.downcase.gsub(/\W/, '-')

@permalink_identified_by ||= { }
@permalink_identified_by[ingredient] = permalink

recipe = {
:title => title,
:type => 'Recipe',
:published => true,

:date => date,
:preparations => [{'ingredient' => {'name' => ingredient}}]
}

RestClient.put "#{@@db}/#{permalink}",
recipe.to_json,
:content_type => 'application/json'
end
I have found it wise to give couchdb-lucene a half second to index these documents so I add another given step to allow that to happen:
   Scenario: Searching for a recipe with an update
Given a "Buttermilk Pancake" recipe with "buttermilk" in it
And a "Buttermilk Pancake" recipe on another day with "lowfat milk" in it
And a 0.5 second wait to allow the search index to be updated
...
I end up rewriting the text of the then clauses so that I avoid clashing with predefined steps:
    Then I should see the recipe with "buttermilk" in the search results
And I should not see the recipe with "lowfat milk" in the search results
I can then define those two steps as:
Then /^I should see the recipe with "([^\"]*)" in the search results$/ do |ingredient|
response.should have_selector(".ingredients",
:content => ingredient)
end

Then /^I should not see the recipe with "([^\"]*)" in the search results$/ do |ingredient|
response.should_not have_selector(".ingredients",
:content => ingredient)
end
When I run that scenario, it fails because I have yet to exclude the updated recipe from the index:
cstrom@jaynestown:~/repos/eee-code$ cucumber features/recipe_replacement.feature:26
Sinatra::Test is deprecated; use Rack::Test instead.
Feature: Updating recipes in our cookbook

As an author
I want to mark recipes as replacing old one
So that I can record improvements and retain previous attempts for reference

Scenario: Searching for a recipe with an update # features/recipe_replacement.feature:26
Given a "Buttermilk Pancake" recipe with "buttermilk" in it # features/step_definitions/recipe_replacement.rb:1
And a "Buttermilk Pancake" recipe on another day with "lowfat milk" in it # features/step_definitions/recipe_replacement.rb:1
And a 0.5 second wait to allow the search index to be updated # features/step_definitions/recipe_search.rb:212
When the "buttermilk" recipe is marked as update of the "lowfat milk" recipe # features/step_definitions/recipe_replacement.rb:21
And I search for "pancake" # features/step_definitions/recipe_search.rb:216
Then I should see the recipe with "buttermilk" in the search results # features/step_definitions/recipe_replacement.rb:58
And I should not see the recipe with "lowfat milk" in the search results # features/step_definitions/recipe_replacement.rb:63
expected following output to omit a <.ingredients>lowfat milk</.ingredients>:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html>
<head>
<title>EEE Cooks</title>
<link href="/stylesheets/style.css" rel="stylesheet" type="text/css">
</head>
<html><body>
..
<tr class="row0">
<td>
<a href="/recipes/2009-09-05-buttermilk-pancake">Buttermilk Pancake</a>
</td>
<td>
<span class="date">2009-09-05</span>
</td>
<td class="numeric">
<span class="prep">0</span>
</td>
<td>
<span class="ingredients">buttermilk</span>
</td>
</tr>
<tr class="row1">
<td>
<a href="/recipes/2000-09-05-buttermilk-pancake">Buttermilk Pancake</a>
</td>
<td>
<span class="date">2000-09-05</span>
</td>
<td class="numeric">
<span class="prep">0</span>
</td>
<td>
<span class="ingredients">lowfat milk</span>
</td>
</tr>
</table>
<div class="pagination">
<span class="inactive">« Previous</span><span class="current">1</span><span class="inactive">Next »</span>
</div>
<div id="footer"></div>
</body></html>
</html>
(Spec::Expectations::ExpectationNotMetError)
features/recipe_replacement.feature:33:in `And I should not see the recipe with "lowfat milk" in the search results'

Failing Scenarios:
cucumber features/recipe_replacement.feature:26 # Scenario: Searching for a recipe with an update

1 scenario (1 failed)
7 steps (1 failed, 6 passed)
0m1.155s
Unfortunately, I am stuck at this point. The lucene Document object, which is exposed as a Rhino "scriptable" object by couchdb-lucene has no concept of the IndexReader, which could be used to modify existing documents. Thus I have no way to "Update" documents to remove old recipes from the index.

I have no immediate answer for this, so I mark the example as pending for now:
cstrom@jaynestown:~/repos/eee-code$ cucumber features/recipe_replacement.feature:26
Sinatra::Test is deprecated; use Rack::Test instead.
Feature: Updating recipes in our cookbook

As an author
I want to mark recipes as replacing old one
So that I can record improvements and retain previous attempts for reference

Scenario: Searching for a recipe with an update # features/recipe_replacement.feature:26
Given a "Buttermilk Pancake" recipe with "buttermilk" in it # features/step_definitions/recipe_replacement.rb:1
And a "Buttermilk Pancake" recipe on another day with "lowfat milk" in it # features/step_definitions/recipe_replacement.rb:1
And a 0.5 second wait to allow the search index to be updated # features/step_definitions/recipe_search.rb:212
When the "buttermilk" recipe is marked as update of the "lowfat milk" recipe # features/step_definitions/recipe_replacement.rb:21
And I search for "pancake" # features/step_definitions/recipe_search.rb:216
Then I should see the recipe with "buttermilk" in the search results # features/step_definitions/recipe_replacement.rb:58
And I should not see the recipe with "lowfat milk" in the search results # features/step_definitions/recipe_replacement.rb:63
figure out how to exclude documents, based on other documents (Cucumber::Pending)
features/recipe_replacement.feature:33:in `And I should not see the recipe with "lowfat milk" in the search results'

1 scenario (1 pending)
7 steps (1 pending, 6 passed)
0m1.817s
This may just be exhaustion on my part. I will likely play with this again tomorrow before moving onto another feature.

Wednesday, September 9, 2009

Enjoying Cucumber

‹prev | My Chain | next›

This is the fun part.

After several days of working deep in the code—including a couple of CouchDB views—I get to work my way out to the Cucumber scenario to mark steps as complete. If I am lucky/good I can verify that each step is complete without another code change.

I finished up yesterday by putting my new Sinatra helper methods, which determine if the current recipe is an update to a previous recipe or has itself been updated, into the recipe Haml template:
#recipe-meta
.recipe-stats
...
%div
= recipe_updated_by(@recipe['_id'])
%div
= recipe_update_of(@recipe['_id'])


- if @recipe['preparations']
%ul.preparations
...
The <div> tags around the helpers will have no children if the helpers return nil (e.g. the recipes have not been updated/do not replace old recipes). Empty <div> tags never hurt anyone.

The next Cucumber step in need of definition is that which describes marking one recipe as an update of another. Recalling that an update document has an updates attribute listing versions of a particular recipe, the most recent last, this definition does the trick:
When /^the "([^\"]*)" recipe is marked as update of the "([^\"]*)" recipe$/ do |arg1, arg2|
update = {
:type => "Update",
:name => "buttermilk pancakes",
:updates => [
{ :id => @permalink_identified_by[arg2] },
{ :id => @permalink_identified_by[arg1] }
]

}

RestClient.put "#{@@db}/buttermilk_pancake_updates",
update.to_json,
:content_type => 'application/json'
end
Indeed, that step now passes. The next step, visiting the recipe with a particular ingredient in it, can be defined as:
When /^I visit the recipe with "([^\"]*)" in it$/ do |ingredient|
visit "/recipes/#{@permalink_identified_by[ingredient]}"
end
The @permalink_identified_by hash was defined in a previous step to make lookup easy.

At this point all steps in this scenario are defined, so does it pass? Sadly, no:
cstrom@jaynestown:~/repos/eee-code$ cucumber -s \
features/recipe_replacement.feature:14
Sinatra::Test is deprecated; use Rack::Test instead.
Feature: Updating recipes in our cookbook

As an author
I want to mark recipes as replacing old one
So that I can record improvements and retain previous attempts for reference

Scenario: A previous version of the recipe
Given a "Buttermilk Pancake" recipe with "buttermilk" in it
And a "Buttermilk Pancake" recipe on another day with "lowfat milk" in it
When the "buttermilk" recipe is marked as update of the "lowfat milk" recipe
And I visit the recipe with "buttermilk" in it
Then I should see a link to the previous recipe with "lowfat milk" in it
expected following output to contain a <a>the previous recipe with "lowfat milk" in it</a> tag:
...
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html>
<head>
<title>EEE Cooks</title>
<link href="/stylesheets/style.css" rel="stylesheet" type="text/css">
</head>
<html><body>
...
<div>

</div>
<div>
<span class="update-of">This is an update of a previous recipe: <a href="/recipes/2000-09-05-buttermilk-pancake">September 5, 2000</a></span>
</div>
<ul class="preparations">
...
Ah, the step definition is coming from a previous feature, which is slurping up too much too much of the text ("the previous recipe with "lowfat milk" in it"—everything after "Then I should see a link to"). Rather than attempting to resolve the conflict, I will rewrite the step to read:
    Then I should see that the recipe is an update to the recipe with "lowfat milk" in it
Making use again of the handy @permalink_identified_by lookup, this step can be defined as:
Then /^I should see that the recipe is an update to the recipe with "([^\"]*)" in it$/ do |ingredient|
response.should have_selector(".update-of a",
:href => "/recipes/#{@permalink_identified_by[ingredient]}")
end
After defining a similar step for the converse (the recipe that has an update), I have the entire scenario passing:



Although this scenario is done, the feature is not quite complete. Up tomorrow, I will start work on the last scenario: suppressing old recipes from couchdb-lucene search results.

Tuesday, September 8, 2009

Recipe Update Grits

‹prev | My Chain | next›

Yesterday, I created Sinatra helper methods that queried CouchDB views for updated versions of recipes as well as recipes that have been replaced by a more recent recipe in the cookbook. Those helper methods were close to the CouchDB metal. Today I would like to build additional helper methods that can be used directly inside Haml templates.

The first thing that I do is query-replace the previous method names (recipe_update_of, recipes_updated_by) for more CouchDB-centric names (couch_recipe_update_of, couch_recipes_updated_by). This a cue for my future self that these methods are closely related to the underlying CouchDB view. It could also be interpreted as a cue to my current self that these methods might be more properly organized in their own class or module. I will table that latter thought for the time being.

Right now, I want to drive a helper that will indicate that the current recipe is an update to a previous recipe. On the legacy site, that looked like:



Putting on my BDD hat, I write the following RSpec
example:
describe "recipe_update_of" do
it "should include links to previous recipes" do
stub!(:couch_recipe_update_of).and_return(['2000-09-07-recipe'])
recipe_update_of('2009-09-07-recipe').
should have_selector('span.update-of a')
end
end
The first time I run this example, I see a failure due to the lack of a helper method named recipe_update_of:
cstrom@jaynestown:~/repos/eee-code$ spec ./spec/eee_helpers_spec.rb 
.......................................................................F..

1)
NoMethodError in 'recipe_update_of should include links to previous recipes'
undefined method `recipe_update_of' for #<Test::Unit::TestCase::Subclass_13:0xb6b25d5c>
./spec/eee_helpers_spec.rb:501:

Finished in 0.104805 seconds

74 examples, 1 failure
So I define that helper method:
    def recipe_update_of
end
When the examples are run now, I receive a failure for the incorrect arity of the helper method:
cstrom@jaynestown:~/repos/eee-code$ spec ./spec/eee_helpers_spec.rb 
.......................................................................F..

1)
ArgumentError in 'recipe_update_of should include links to previous recipes'
wrong number of arguments (1 for 0)
./spec/eee_helpers_spec.rb:501:in `recipe_update_of'
./spec/eee_helpers_spec.rb:501:

Finished in 0.112535 seconds

74 examples, 1 failure
I add a single argument to the helper method as desired by the example:
    def recipe_update_of(permalink)
end
Now I find no errors, but the expectation is not met:
cstrom@jaynestown:~/repos/eee-code$ spec ./spec/eee_helpers_spec.rb 
.......................................................................F..

1)
'recipe_update_of should include links to previous recipes' FAILED
expected following output to contain a <span.update-of a href='/recipes/2000-09-07-recipe'/> tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">

./spec/eee_helpers_spec.rb:501:

Finished in 0.118594 seconds

74 examples, 1 failure
I work methodically through the change-the-message or make-it-pass cycle to ensure that I check my assumptions with each step. With assumptions successfully checked, I am ready to make it pass. That is a fairly in-depth example—I need the links returned by the couch_recipe_update_of helper method to be embedded in an update <span>:
    def recipe_update_of(permalink)
previous = couch_recipe_update_of(permalink)
links = previous.map do |update_permalink|
%Q|<a href="/recipes/#{update_permalink}"></a>|
end

%Q|<span class="update-of">| +
%Q|This is an update of a previous recipe: | +
links.join(", ") +
%Q|</span>|
end
With that, I have all of my examples passing:
cstrom@jaynestown:~/repos/eee-code$ spec ./spec/eee_helpers_spec.rb 
..........................................................................

Finished in 0.172632 seconds

74 examples, 0 failures
The links are in place, but there is no content. To drive the link text, I use this example:
  it "should link to a pretty formatted date" do
stub!(:couch_recipe_update_of).and_return(['2000-09-07-recipe'])
recipe_update_of('2009-09-07-recipe').
should have_selector('span.update-of a',
:content => "September 7, 2000")
end
I get that passing by calculating the date string:
    def recipe_update_of(permalink)
previous = couch_recipe_update_of(permalink)
links = previous.map do |update_permalink|
date_str = Date.parse(update_permalink).strftime("%B %e, %Y")
%Q|<a href="/recipes/#{update_permalink}">#{date_str}</a>|
end

%Q|<span class="update-of">| +
%Q|This is an update of a previous recipe: | +
links.join(", ") +
%Q|</span>|
end
Last up, I write an example that describes the case in which the recipe is not an update to a previous recipe:
  it "should do nothing if this is not an update" do
stub!(:couch_recipe_update_of)
recipe_update_of('2009-09-07-recipe').
should be_nil
end
I can get that passing with a conditional:
    def recipe_update_of(permalink)
previous = couch_recipe_update_of(permalink)
if previous
links = previous.map do |update_permalink|
date_str = Date.parse(update_permalink).strftime("%B %e, %Y")
%Q|<a href="/recipes/#{update_permalink}">#{date_str}</a>|
end

%Q|<span class="update-of">| +
%Q|This is an update of a previous recipe: | +
links.join(", ") +
%Q|</span>|
end
end
That gets all of my specs passing:
cstrom@jaynestown:~/repos/eee-code$ spec ./spec/eee_helpers_spec.rb 
............................................................................

Finished in 0.109127 seconds

76 examples, 0 failures
I follow a similar procedure to drive the this-recipe-has-been-updated feature:



With helper methods in hand, I update my Haml templates to use them:
...
%div
= recipe_updated_by(@recipe['id'])
%div
= recipe_update_of(@recipe['id'])
...
That should just about do it for the "inside" code work. Tomorrow I will work my way back out to the Cucumber scenario to verify (hopefully) that I have done it well.

Monday, September 7, 2009

Pizza Dough is Tough to Get Right

‹prev | My Chain | next›

I would like to store recipe document updates in a document similar to:
{
"_id": "pizza_dough_updates",
"_rev": "2-1463622047",
"type": "Update",
"updates": [
{
"id": "2001-12-30-pizza"
},
{
"id": "2002-01-28-pizza_crust"
},
{
"id": "2002-03-28-pizza_dough"
}
]
}
The order of the "updates" attribute is significant—the last entry is the most up-to-date version of the recipe and the others are considered deprecated. The updates are hashes so that we can add a description of why we felt the need to replace an earlier incarnation (e.g. the old photo was not of sufficient quality or the ingredient list needed to be tweaked).

Last night, I played around with CouchDB's Futon interface for building map-reduce views. I eventually got a view that given a recipe ID, produces a list of the previous versions of the same recipe:
function(doc) {
if (doc['type'] == 'Update') {
var num = doc['updates'].length;
var old = [];
for (var i=0; i<num-1; i++) {
old[i] = doc['updates'][i]['id'];
}
emit(doc['updates'][num-1]['id'], old);
}
}
For the pizza dough update document, that view produces:
{"total_rows":2,"offset":0,"rows":[
{"id":"pizza_dough_updates",
"key":"2002-03-28-pizza_dough",
"value":["2001-12-30-pizza","2002-01-28-pizza_crust"]}
,
{"id":"roasted_potato_updates",
"key":"2003-08-17-potatoes",
"value":["2001-09-02-potatoes"]}
]}
Today, I continue to play with Futon to accomplish the opposite—given a recipe ID, lookup if it has been updated and, if so, by which recipe. The "updated by" map view is:
function(doc) {
if (doc['type'] == 'Update') {
var num = doc['updates'].length;
for (var i=0; i<num-1; i++) {
emit(doc['updates'][i]['id'], doc['updates'][num-1]['id']);
}
}
}
For every update in an update document, the view should contain the ID of the update and the ID of the last update. For the pizza dough update document, I get:
{"total_rows":3,"offset":0,"rows":[
{"id":"roasted_potato_updates","key":"2001-09-02-potatoes","value":"2003-08-17-potatoes"},
{"id":"pizza_dough_updates","key":"2001-12-30-pizza","value":"2002-03-28-pizza_dough"},
{"id":"pizza_dough_updates","key":"2002-01-28-pizza_crust","value":"2002-03-28-pizza_dough"}

]}
This is an example of CouchDB's ability to emit more than one result per-document. The pizza dough update doc emitted two results—one for each of tried and not-so-true versions of the recipe.

I am reasonably happy with my "update_of" and "updated_by" recipe views. I may not end up with the same views, but the output seems solid. Soo... it's time to move back into driving code by example.

Describing the recipe_update_of helper function with RSpec:
describe "recipe_update_of" do
it "should ask CouchDB" do
RestClient.
should_receive(:get).
with(/update_of.+key=.+2009-09-07/).
and_return('{"rows": [] }')
recipe_update_of('2009-09-07')
end
end
That fails at first because the method is not defined:
1)
NoMethodError in 'update_of should ask CouchDB'
undefined method `recipe_update_of' for #<Test::Unit::TestCase::Subclass_12:0xb6aa1a34>
./spec/eee_helpers_spec.rb:487:

Finished in 0.106194 seconds

70 examples, 1 failure
After defining the method, then giving it the proper arity, then making the required RestClient call, I finally end up with this:
    def recipe_update_of(permalink)
url = "#{_db}/_design/recipes/_view/update_of?key=%22#{permalink}%22"
data = RestClient.get url
end
Since I need real data, not JSON returned from this helper, I add a second example to drive the desired return value of the helper:
  it "should return the value from the JSON" do
RestClient.
stub!(:get).
and_return('{"rows": [{"value": ["2000-09-07-recipe"]}] }')
recipe_update_of('2009-09-07-recipe').
should == ['2000-09-07-recipe']
end
I get that example passing with:
    def recipe_update_of(permalink)
url = "#{_db}/_design/recipes/_view/update_of?key=%22#{permalink}%22"
data = RestClient.get url
results = JSON.parse(data)['rows']
results.first && results.first['value']
end
After driving the recipe_updated_by helper in a similar fashion, I am ready to call it a night. I will pick things up tomorrow by using these helpers in the Haml view for recipes and ultimately working my way back out to the Cucumber scenario to mark it as complete.