Friday, July 31, 2009

Rolling Version 1.1 of the couch_design_docs Gem

‹prev | My Chain | next›

I left off yesterday having BDD'd my way to an updated version of the couch_design_docs gem.

As a considerate gem maintainer, I update my History.txt:
== 1.1.0 / 2009-07-31

* Add the ability to upload "normal" documents in addition to design
documents.
* CouchDesignDocs::upload_dir is aliased as
CouchDesignDocs::put_dir and is DEPRECATED.
* CouchDesignDocs::put_dir now puts both design documents
and "normal" documents onto the CouchDB database.
* CouchDesignDocs::put_design_dir has been added to put
design documents
* CouchDesignDocs::put_document_dir has been added to put
"normal" documents
Since I have changed the functionality slightly, I also update the README, specifically including modified usage instructions:
== SYNOPSIS:

DB_URL = "http://localhost:5984/db"
DIRECTORY = "/repos/db/couchdb/"

# /repos/db/couchdb/_design/lucene/transform.js
# /repos/db/couchdb/foo.json

CouchDesignDocs.put_dir(DB_URL, DIRECTORY)

# => lucene design document with a "transform" function containing
# the contents of transform.js
# - AND -
# a document named "foo" with the JSON contents from the foo.json
# file
I started with version 1.0—partly because I extracted working code from a functioning application and partly because bones defaults to 1.0 and I am lazy. Given that some new functionality was added, I bump the VERSION up to 1.1, recording this information in the couch_design_docs.rb:
  VERSION = '1.1.0'
Before pushing to Github, I would like to make sure that it still works with my existing application.

To install from my local repository, I use Bones's built-in rake gem:install:
cstrom@jaynestown:~/repos/couch_design_docs$ rake gem:install
(in /home/cstrom/repos/couch_design_docs)
rm -r pkg
mkdir -p pkg
mkdir -p pkg/couch_design_docs-1.1.0
...
cd pkg/couch_design_docs-1.1.0
Successfully built RubyGem
Name: couch_design_docs
Version: 1.1.0
File: couch_design_docs-1.1.0.gem
mv couch_design_docs-1.1.0.gem ../couch_design_docs-1.1.0.gem
cd -
sudo /usr/bin/ruby1.8 -S gem install --local pkg/couch_design_docs-1.1.0
[sudo] password for cstrom:^C
I Ctrl-C out of this because I prefer installing my gems locally:
cstrom@jaynestown:~/repos/couch_design_docs$ gem install --local pkg/couch_design_docs-1.1.0
WARNING: Installing to ~/.gem since /var/lib/gems/1.8 and
/var/lib/gems/1.8/bin aren't both writable.
Successfully installed couch_design_docs-1.1.0
1 gem installed
I have a rake task in my application that lets me quickly reload my design documents:
DB = "http://localhost:5984/eee"

namespace :couchdb do
# ...
require 'couch_design_docs'

desc "Load (replacing any existing) all design documents"
task :load_design_docs do
CouchDesignDocs.upload_dir(DB, "couch")
end
end
I verify that the couch_design_docs gem still works by deleting one of my design documents manually from the CouchDB database, running rake couchdb:load_design_docs, and then reloading the view in my browser to see that the design document is back and working:



To check the new functionality of being able to upload regular documents, I implement the document that started all of this—a lookup document containing the names of our kids and the nicknames that we display on the website (yah, we're that paranoid):
// couch/kids.json
{"foo":"bar"}
Before running the rake task, the kids document is the blank one that I manually created to allow the app to work:



Then I run the rake task:
cstrom@jaynestown:~/repos/eee-code$ rake couchdb:load_design_docs --trace
(in /home/cstrom/repos/eee-code)
** Invoke couchdb:load_design_docs (first_time)
** Execute couchdb:load_design_docs
rake aborted!
undefined method `put!' for #
/home/cstrom/.gem/ruby/1.8/gems/couch_design_docs-1.1.0/lib/couch_design_docs.rb:45:in `put_document_dir'
/home/cstrom/.gem/ruby/1.8/gems/couch_design_docs-1.1.0/lib/couch_design_docs/document_directory.rb:14:in `each_document'
/home/cstrom/.gem/ruby/1.8/gems/couch_design_docs-1.1.0/lib/couch_design_docs/document_directory.rb:12:in `each'
/home/cstrom/.gem/ruby/1.8/gems/couch_design_docs-1.1.0/lib/couch_design_docs/document_directory.rb:12:in `each_document'
/home/cstrom/.gem/ruby/1.8/gems/couch_design_docs-1.1.0/lib/couch_design_docs.rb:44:in `put_document_dir'
/home/cstrom/.gem/ruby/1.8/gems/couch_design_docs-1.1.0/lib/couch_design_docs.rb:21:in `put_dir'
/home/cstrom/.gem/ruby/1.8/gems/couch_design_docs-1.1.0/lib/couch_design_docs.rb:26:in `upload_dir'
/home/cstrom/repos/eee-code/Rakefile:47
...
Aw, nuts!

I BDD'd a call to an instance method that is really a class method. I update the RSpec spec to expect a call to a class method instead:
  it "should be able to load documents into CouchDB" do
store = mock("Store")
Store.stub!(:new).and_return(store)

dir = mock("Document Directory")
dir.
stub!(:each_document).
and_yield('foo', {"foo" => "1"})

DocumentDirectory.stub!(:new).and_return(dir)

Store.
should_receive(:put!).
with('foo', {"foo" => "1"})


CouchDesignDocs.put_document_dir("uri", "fixtures")
end
Now I get the error that I should:
cstrom@jaynestown:~/repos/couch_design_docs$ spec ./spec/couch_design_docs_spec.rb 
..F.............

1)
Spec::Mocks::MockExpectationError in 'CouchDesignDocs should be able to load documents into CouchDB'
Mock 'Store' received unexpected message :put! with ("foo", {"foo"=>"1"})
/home/cstrom/repos/couch_design_docs/lib/couch_design_docs.rb:45:in `put_document_dir'
/home/cstrom/repos/couch_design_docs/lib/couch_design_docs.rb:44:in `put_document_dir'
./spec/couch_design_docs_spec.rb:46:

Finished in 0.016747 seconds

16 examples, 1 failure
I can make the example pass with:
  def self.put_document_dir(db_uri, dir)
store = Store.new(db_uri)
dir = DocumentDirectory.new(dir)
dir.each_document do |name, contents|
Store.put!("#{db_uri}/#{name}", contents)
end
end
I then re-install the gem, run the rake task again, and find the document uploaded as expected:



Yay!

With that, I am ready to commit my changes. I update the gemspec with the built-in Bones rake gem:spec task. Then commit my changes:
cstrom@jaynestown:~/repos/couch_design_docs$ git commit -m "Add the ability to upload documents in addition to design documents." -a
Created commit eb76354: Add the ability to upload documents in addition to design documents.
13 files changed, 169 insertions(+), 38 deletions(-)
rename fixtures/{ => _design}/a/b/c.js (100%)
rename fixtures/{ => _design}/a/b/d.js (100%)
create mode 100644 fixtures/bar.json
create mode 100644 fixtures/foo.json
rename lib/couch_design_docs/{directory.rb => design_directory.rb} (94%)
create mode 100644 lib/couch_design_docs/document_directory.rb
Before pushing to Github, I use another Bones rake task, git:create_tag, to tag this new version of the gem.

Check it out: two tags and two downloads (thanks to the tag) and some nice, well tested code: http://github.com/eee-c/couch_design_docs.

Thursday, July 30, 2009

New Features (and maybe a new name) for couch_design_docs

‹prev | My Chain | next›

I take a break from CSS tonight. As much fun as less CSS has been, too much CSS can be a bit... blah.

I decide, instead, to move back to my couch_design_docs gem. It has become apparent that I need to be able to upload regular documents in addition to CouchDB design documents (the gem may need a rename). Specifically, I need to upload a "kids" document when initially creating the database (I ran into this error at the end of the load from legacy application). So...

I dive back into the gem. First up, I rename the internal Directory class to DesignDirectoy. I am going to need another class that is responsible for uploading "regular" (non-design) documents in a directory, so renaming the class should cut down on confusion. I also update the specs to reflect this change.

Once that is done and all specs are passing, I describe examples for instantiating the new DocumentDirectory class:
describe DocumentDirectory do
it "should require a root directory for instantiation" do
lambda { DocumentDirectory.new }.
should raise_error

lambda { DocumentDirectory.new("foo") }.
should raise_error

lambda { DocumentDirectory.new("fixtures")}.
should_not raise_error
end
end
I confess that I skip a few steps in the BDD cycle here. These examples, and the implementation of the initializer are very similar to the implementation of the initializer for DesignDirectory. The code that makes these examples pass:
module CouchDesignDocs
class DocumentDirectory

attr_accessor :couch_doc_dir

def initialize(path)
Dir.new(path)
@couch_doc_dir = path
end
end
end
For now, I need an instance of this class to iterate over the document names and contents. Given two documents:
#fixtures/foo.json
{"foo":"1"}

#fixtures/bar.json
{"bar":"2"}
I expect to iterate over the documents so that I can build a data structure containing the document names and contents:
  context "a valid directory" do
before(:each) do
@it = DocumentDirectory.new("fixtures")
end

it "should be able to iterate over the documents" do
everything = []
@it.each_document do |name, contents|
everything << [name, contents]
end
everything.
should == [['bar', '{"bar":"2"}'],
['foo', '{"foo":"1"}']]
end
end
I can implement this with:
    def each_document
Dir["#{couch_doc_dir}/*.json"].each do |filename|
yield [ File.basename(filename, '.json'),
File.new(filename).read ]

end
end
That is all that I need. The Store class is already capable of puting documents into the specified CouchDB database. I need to iterate over each document and put them in the store. In my example, I simulate iterating over the documents with RSpec's and_yield:
  it "should be able to load documents into CouchDB" do
store = mock("Store")
Store.stub!(:new).and_return(store)

dir = mock("Document Directory")
dir.
stub!(:each_document).
and_yield('foo', {"foo" => "1"})

DocumentDirectory.stub!(:new).and_return(dir)

store.
should_receive(:put!).
with('foo', {"foo" => "1"})

CouchDesignDocs.put_document_dir("uri", "fixtures")
Now that I have both a put_document_dir and a put_design_dir I need a convenience method to upload both:
  it "should be able to load design and normal documents" do
CouchDesignDocs.
should_receive(:put_design_dir).
with("uri", "fixtures/_design")

CouchDesignDocs.
should_receive(:put_document_dir).
with("uri", "fixtures")

CouchDesignDocs.put_dir("uri", "fixtures")
end
Normally, I am not a big fan of two expectations in one example, but for simple methods, I am wont to make an exception. And the put_dir method is simple:
  def self.put_dir(db_uri, dir)
self.put_design_dir(db_uri, "#{dir}/_design")
self.put_document_dir(db_uri, dir)
end
That is a good stopping point for tonight. Tomorrow I will give it a try in my live code, maybe do some minor refactoring, and mess about with github branches for this new version of my gem.

Wednesday, July 29, 2009

Spotting Errors Visually

‹prev | My Chain | next›

Continuing my CSS work, I have a go at the meals by month and by year pages. I am getting into a nice flow with less CSS and quickly get the meals-by-month page into good shape.

The thing about nicely formatted pages is that mistakes quickly become obvious. On this page, I notice that the wiki-formatted meal references are not being parsed properly.



So it is off to the specs. For wiki text "[meal:2009/07/29 Different Title]", I expect a link to the meal with link text "Different Title". In other words:
    it "should wikify meal URIs, using supplied text for the link" do
wiki("[meal:2009/07/29 Different Title]").
should have_selector("a",
:href => "/meals/2009/07/29",
:content => "Different Title")
end
Running the spec fails (as expected because it is not working) with:
1)
'wiki data stored in CouchDB should wikify meal URIs, using supplied text for the link' FAILED
expected following output to contain a <a href='/meals/2009/07/29'>Different Title</a> tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><p>[meal:2009/07/29 Different Title]</p></body></html>
./spec/eee_helpers_spec.rb:114:
That is an easy example to get passing—I simply need to add a call to meal_link (paralleling the already defined recipe_link) to the wiki helper method:
    def meal_link(link, title=nil)
%Q|<a href="/meals/#{link}">#{title}</a>|
end
If the meal wiki text does not include a title (e.g. "[meal:2009/07/29]"), then it needs to be pulled back from CouchDB:
    it "should wikify meal URIs" do
RestClient.stub!(:get).
and_return('{"_id":"2009-07-29","title":"Title"}')

wiki("[meal:2009/07/29]").
should have_selector("a",
:href => "/meals/2009/07/29",
:content => "Title")
end
I can implement that by conditionally pulling back the title in the meal_link helper:
    def meal_link(link, title=nil)
permalink = link.gsub(/\//, '-')
title ||= JSON.parse(RestClient.get("#{_db}/#{permalink}"))['title']
%Q|<a href="/meals/#{link}">#{title}</a>|
end
With that, I have my meal wiki links working as desired:



The meals by year already look to be in good shape. A simple bullet list works just fine:



I quickly style the feedback page. Nothing fancy, just display the labels as block with some top margins to get separation:



The less CSS:
#feedback {
h1 { clear: both }
label, input, textarea { display: block; }
label { margin-top: 8px }
}
I call it a day at that point. I believe that I still have to style the no-search results page (or at least review), the "thanks for your feedback" page, and the homepage. After that, it may be time to deploy!

Tuesday, July 28, 2009

Refined Search Results

‹prev | My Chain | next›

I am still slogging through CSS today, starting with the search results pages. The search results from the legacy site look like this:



The naked search results on the CouchDB / Sinatra version of the application look like this:



The only place in the application that makes use of tables is the search results page, so I am free to do this work at the table selector level. In less CSS format:
table {
th {
background-color: @italian_green;
color: white;
a {color: white;}
}

.row1 {
background-color: @soft_gray;
}
}
A little bit of less CSS goes a long way:



I realize that I am doing the white letters on green background quite a bit, so I extract it out into a .section_header mixin:
.section_header {
background-color: @italian_green;
color: white;
a {color: white;}
}
This allows me to compact the table definition down to:
table {
th { .section_header; }
.row1 { background-color: @soft_gray; }
}
Another win for less CSS! One line to describe a table heading style and one line to describe zebra striping.

For pagination, I stick with the standard will paginate look and feel. It is simple, but takes more than a little CSS (even less CSS).

The pagination itself needs to be floated to the right. This reinforces the most common use case of pagination—moving to higher page numbers. Natural flow in western languages is from left to right. Placing the links to the very right side encourages such a flow.

The rest of the pagination styling is more straight-forward. The page numbers need a border (I stick with the standard "Italian green" for this Italian flavored cookbook). The page links need a little separation (margins) from each other to aid in readability. The color of the pages should be the same "Italian green" as the border to suggest cohesion. There is no need to underline the links—too many lines are busy.

The current page should be highlighted by coloring in the background and using white text. Inactive links (e.g. the "previous" link on the first page) should be grayed out.

Or, in less CSS:
.pagination {
float: right;
margin-top: 10px;
font-size: 0.8em;

a, .current {
border: 1px solid @italian_green;
margin: 2px;
padding: 2px 4px;
color: @italian_green;
text-decoration: none;
}
.current {
background-color: @italian_green;
color: white;
}
.inactive {
border:1px solid #eee;
color: #ddd;
margin: 2px;
padding: 2px 3px;
}
}
That CSS yields:



The last thing needed on the search results page is to get the search refinement text field closer to the results. Currently it looks to be more a part of the categories or top-level navigation:



It should be obvious that the search field and the results are intimately coupled. There should be a margin above the form, no margin below. Any floating text should be cleared. The less CSS corresponding to this:
#refine-search {
form { margin: 10px 0px 0px; }
clear: both;
}
And the resulting page:



Also of note in there is that the "Prep" time column is now right aligned. Numerical columns should always be right aligned for readability.

That does it for the search results page. Before moving on a final note about the column headings. Each is clickable for sorting the results. The first three are obvious—click the "Title" heading & get the results sorted by title alphabetically, click the "Date" or "Prep" time heading & get the results sorted by date or time. But what happens when you click on the "Ingredients" column heading?



As described way back when by the Cucumber scenario, this sorts on the number of ingredients. Sorting alphabetically by the first ingredient in the list (or any ingredient) is useless to a user. It may not be immediately obvious to the use what clicking this heading might do, but I like to reward curiosity. In this case, the user gets a visually obvious representation of the simplicity of various recipes. Corkscrew bacon being the easiest to prepare in this metric.

Mmmmm. Bacon.

Monday, July 27, 2009

20% Less CSS

‹prev | My Chain | next›

I am almost done with my less CSS work on the recipe and meal pages. Hopefully I am well past the last 20% stage in the 80/20 rule. But first..

I added a layer to the DOM in recipe.haml yesterday. I did not check to ensure that my specs still pass. They ought to, if I was not too specific in the examples, but you never know...
cstrom@jaynestown:~/repos/eee-code$ rake view_specs
(in /home/cstrom/repos/eee-code)

==
View specs
.....................................................................................

Finished in 0.721824 seconds

85 examples, 0 failures
Nice!

After tweaking a thing here and there, I am done with everything—save for the ability to search for other recipes. On the legacy site, the search box is located at the top-right corner of the page, just under the recipe category links:



A simple search form is pretty easy to add. In fact I have already done it. Twice. Once on the search page and once on the no results search page. I am somewhat disappointed that I violated DRY once, I am not going to do it again. I extract the search form Haml into a separate _search_form.haml template:
%form{:method => "get", :action => "/recipes/search"}
%input{:type => "text",
:value => @query,
:name => "q",
:size => 31,
:maxlength => 2048}
%input{:type => "submit",
:value => "Search",
:name => "s"}
I adopt the Rails convention for partial filenames (prefixing it with and underscore).

To work with partials in Sinatra / Haml, the Sinatra Book recommends this helper code:
    # Swiped from the Sinatra Book
# Usage: partial :foo
helpers do
def partial(page, options={})
haml page, options.merge!(:layout => false)
end
end
I then replace the search form Haml in the search results templates with:
= partial :_search_form
Before doing anything else, I run all of my tests. I get 37 failures, all of the form:
37)
NoMethodError in 'search.haml should link to sort by date'
undefined method `haml' for #
./helpers.rb:236:in `partial'
(haml):10:in `render'
/home/cstrom/.gem/ruby/1.8/gems/haml-2.2.0/lib/haml/engine.rb:167:in `render'
/home/cstrom/.gem/ruby/1.8/gems/haml-2.2.0/lib/haml/engine.rb:167:in `instance_eval'
/home/cstrom/.gem/ruby/1.8/gems/haml-2.2.0/lib/haml/engine.rb:167:in `render'
/home/cstrom/repos/eee-code/spec/spec_helper.rb:27:in `render'
./spec/views/search.haml_spec.rb:57:
Ugh. The haml method is part of Sinatra. Since I am unit testing my views, I do not have access to Sinatra's haml method. To resolve, I stub out calls to the partial method and add an RSpec example for the _search_form.haml partial:
describe "_search_form.haml" do
it "should display a form to refine your search" do
render("/views/_search_form.haml")
response.should have_selector("form",
:action => "/recipes/search") do |form|
form.should have_selector("input", :name => "q")
end
end
end
I add that partial to the recipe Haml template inside of a <div> for styling convenience:
#search
= partial :_search_form
I can then float the search to the right:
#search {
float:right;
}
And I am done:



I add similar code to the meal template so that users can search from meals as well. With that, I call it a day. There are still some other pages in need of styling. I will likely pick up with them tomorrow.

Sunday, July 26, 2009

More Less CSS

‹prev | My Chain | next›

The meals in my Sinatra / CouchDB application look to be in pretty decent shape. What's left to do for recipes?

In the legacy Rails application, recipes look like:



So far, the updated application has recipes looking like:



Thanks to the work on meal, the recipes are already in pretty good shape. The most obvious differences are style-less vital stats and tools sections. Both sections should have green borders. The titles should have green backgrounds with white letters. The lists should not be bulletted. In other words, recalling that the less CSS variable @italian_green was defined yesterday:
.recipe-tools, .recipe-stats {
border: 2px solid @italian_green;

h2 {
color: white;
background-color: @italian_green;
font-size: medium;
padding: 2px;
margin: 0px;
}

ul, li {
display: block;
padding: 1px;
margin: 1px;
}

}
This gets me to this point:



I still need to get both sections floated to the right of the ingredient list. I cannot apply a right float to both sections because they will end up next to each other:



I cannot clear one of the floats because the recipe photo is already floated (clearing these would place them below the photo). This leaves me with the only option of wrapping both sections in a <div> and floating it. In the Haml template, I add the outer div (and a title):
#recipe-meta
.recipe-stats
%h2= "Servings and Times"
%div= "Preparation Time: " + hours(@recipe['prep_time'])
- if @recipe['inactive_time']
%div= "Inactive Time: " + hours(@recipe['inactive_time'])

- if @recipe['tools']
.recipe-tools
%h2= "Tools and Appliances"
%ul
- @recipe['tools'].each do |tool|
%li
%a{:href => amazon_url(tool['asin'])}
= tool['title']
I can then float the outermost div:
#recipe-meta {
float: right;
width: 250px;
}
And I now have:



Much better.

I have a few more tweaks to add (fonts and padding) to the recipe stats and tools. I need to add a recipe search field to the page as well. The servings and the cooking time also appear to be missing. I will investigate these issues and more... tomorrow.

Saturday, July 25, 2009

Less CSS

‹prev | My Chain | next›

I am sorely tempted to clean up my "mediocre" solution from last night. I feel this temptation under normal circumstances. The temptation is even worse since this chain thing is for my personal edification. At times like these, I need to repeat the mantra "get it done, do it right, optimize". It is done, I have made a TODO note for myself on how to do it right (no need to even start thinking about optimization).

So, for now, I continue focusing on getting the entire project done. Up today: CSS. I started playing around with less CSS the other day. Today, I would like to get the meal pages looking nicer than:



The legacy site looks like:



In my public/stylesheets/style.less, I start with some color variables:
@italian_red:   #d00;
@italian_green: #090;
At the top of the page, I need to kill the border around the header image and add a border below the header. In CSS speak:
#header {
img {
border: none;
}
margin: 0px 2px;
border-bottom: 1px solid black;
}
What I like most about less CSS (and Sass) is the way that my CSS gets organized. In normal CSS, which is what is generated by lessc, the above would be represented as:
#header {
margin: 0px 2px;
border-bottom: 1px solid black;
}
#header img { border: none; }
Two top-level selectors instead of one is not a big deal, but CSS has the habit of growing. Quickly. And as it grows, it is no longer obvious how to organize selectors in a maintainable fashion. Tools like less are opinionated so that I do not have to be. I have better things to worry about.

Right now, I need to worry about the recipe categories at the top of the page. A bulleted list ain't gonna cut it. Semantically, it is nice, but visually it stinks. I need to display the <ul> as a block and the list items as inline elements of that block. The links should be bold, without the normal underlines. They should be black normally, green when hovering the mouse over them, and red when the meal or recipe on the current page belongs to that category (e.g. it is an Italian recipe). In less CSS speak:
ul#eee-categories {
display: block;
margin: 2px 0px;
float: right;

li {
display: inline;
padding-left: 0.5ex;

a {
font-weight: bold;
text-decoration: none;
color: black;
}

a:hover { color: @italian_green }
a.active { color: @italian_red }
}

}
With that CSS, and a few other classes, the web page now looks like:



Not too shabby.

At the end of day, my console looks something like:
cstrom@jaynestown:~/repos/eee-code$ lessc public/stylesheets/style.less
cstrom@jaynestown:~/repos/eee-code$ lessc public/stylesheets/style.less
cstrom@jaynestown:~/repos/eee-code$ lessc public/stylesheets/style.less
cstrom@jaynestown:~/repos/eee-code$ lessc public/stylesheets/style.less
cstrom@jaynestown:~/repos/eee-code$ lessc public/stylesheets/style.less
cstrom@jaynestown:~/repos/eee-code$ lessc public/stylesheets/style.less
cstrom@jaynestown:~/repos/eee-code$ lessc public/stylesheets/style.less
cstrom@jaynestown:~/repos/eee-code$ lessc public/stylesheets/style.less
cstrom@jaynestown:~/repos/eee-code$ lessc public/stylesheets/style.less
cstrom@jaynestown:~/repos/eee-code$ lessc public/stylesheets/style.less
cstrom@jaynestown:~/repos/eee-code$ lessc public/stylesheets/style.less
cstrom@jaynestown:~/repos/eee-code$ lessc public/stylesheets/style.less
cstrom@jaynestown:~/repos/eee-code$ lessc public/stylesheets/style.less
cstrom@jaynestown:~/repos/eee-code$ lessc public/stylesheets/style.less
cstrom@jaynestown:~/repos/eee-code$ lessc public/stylesheets/style.less
cstrom@jaynestown:~/repos/eee-code$ lessc public/stylesheets/style.less
cstrom@jaynestown:~/repos/eee-code$ lessc public/stylesheets/style.less
Compile the .less file, look in the browser. Compile the .less file, look in the browser. Rinse and repeat. Actually, I'd like not to repeat. I do not have much of a choice but to reload the browser, but I would really like to avoid the need to manually compile the .less files into CSS after each change. It would much more closely approximate the normal CSS design workflow.

I know how to watch files for changes, but not how to take action in response to those changes. Sounds like a job for a custom written script.

Luckily, I RTFM before I start on that. The output of lessc --help:
strom@jaynestown:~/repos/eee-code$ lessc --help
usage: lessc source [destination] [--watch]
-w, --watch watch for changes
-h, --help show this message
-d, --debug show full error messages
-v, --version show version
Say! That --watch option looks like it ought to do the trick. I run lessc once more, this time with -w, make a couple of changes and:
cstrom@jaynestown:~/repos/eee-code$ lessc public/stylesheets/style.less --watch
* Watching for changes in public/stylesheets/style.less... Ctrl-C to abort.
: Change detected... * [Updated] style.css
: Change detected... * [Updated] style.css
This less CSS thing is pretty nice!

Friday, July 24, 2009

A Mediocre Solution for Finding Adjacent CouchDB View Records

‹prev | My Chain | next›

I have one remaining open issue in my GitHub tracker: links between recipes are missing. I made a go at fixing the problem last night, but had to back out when the fix was growing too large.

Part of the problem is that the helper method responsible for linking to the next record in a CouchDB view has grown long and complicated:
    def link_to_adjacent_view_date(current, couch_view, options={})
# If looking for the record previous to this one, then we seek a
# date prior to the current one - build a Proc capable of
# finding that
compare = options[:previous] ?
Proc.new { |date_fragment, current| date_fragment < current} :
Proc.new { |date_fragment, current| date_fragment > current}

# If looking for the record previous to this one, then we need
# to reverse the list before using the compare Proc to detect
# the record
next_result = couch_view.
send(options[:previous] ? :reverse : :map).
detect{|result| compare[result['key'], current.to_s]}

# If a next record was found, then return link text
if next_result
next_uri = next_result['id'].gsub(/-/, '/')
link_text = block_given? ?
yield(next_result['id'], next_result['value']) :
next_result['id']

%Q|<a href="/meals/#{next_uri}">#{link_text}</a>|
else
""
end
end
The most obvious obstacle in adapting this helper for use with recipes is the line that produces the <a> tag—the "/meals" URI-space is hard coded in there. But how to drive the URI-space in that method?

The quickest means of accomplishing this is to add a urispace argument to the link_to_adjacent_view_date helper. That is not an acceptable option in my mind because link_to_adjacent_view_date already has an explicit arity of 3. I barely remember what the three arguments do—adding a fourth is going to make it impossible. Hiding it in the options hash argument is disingenuous, at best.

I called it an explicit arity of 3 earlier because this helper takes a fourth block argument. It is the responsibility of the block to build the link text. Given that the actual arity is 4, adding yet another argument is right out. But I ought to be able to increase the responsibility of the block—instead of building the link text, it can build the entire link.

In other words, given a CouchDB view with results from April and May of 2009:
    before(:each) do
@by_month = [{"key" => "2009-04", "value" => "foo"},
{"key" => "2009-05", "value" => "bar"}]
end
Then, when working with the record adjacent to April 2009, I should be able to build a link to the value from May, "bar" in this case:
    it "should link to the CouchDB view's key and value, if block is given" do
link_to_adjacent_view_date("2009-04", @by_month) do |rec|
%Q|<a href="/foo">#{rec}</a>|
end.
should have_selector("a",
:href => "/foo",
:content => "bar")
end
I can make that pass with a somewhat simpler output conditional:
      # If a next record was found, then return link text
if next_result
if block_given?
yield next_result['value']
else
next_uri = next_result['key'].gsub(/-/, '/')
%Q|<a href="/meals/#{next_uri}">#{next_result['key']}</a>|
end
else
""
end
If a block if given, the result of the conditional is the entire block itself. If no block is given, then a default, meal-centric (for backward compatibility) string results.

The specs all pass for the helper. As expected, the specs for the meal view fail (since it is not longer generating a link to the text. After correcting that, I drive intra-recipe links. The specs are largely based on the intra-meal links, making the process relatively easy. And, like that, intra-recipe links are working:



I close the issue with that fix, but I am not happy with the ultimate solution. I think that Rails concepts and habits have had a bad influence on me in this case. Specifically, I am treating helpers like Rails's view helpers and RestClient calls to CouchDB as model interactions. Neither is the case. In simple Sinatra apps, helpers can help views or controllers and RestClient is pulling back Hash objects, not MVC models.

Ultimately, I think a cleaner solution would have been to have the link_to_adjacent_view_date helper request the appropriate CouchDB view directly, using limit=1 to pull back the single next record. Since I have this working, I leave that to another day with a TODO note.

Thursday, July 23, 2009

Conveniences

‹prev | My Chain | next›

First up today, I would like to clear the issues log that I have been compiling. These are mostly minor issues so I hope to be able to clear them quickly and move onto more interesting topics.

Indeed, the first issue, missing images on the meal pages is a simple omission from the view. I copy & paste the specs for the recipe images and use the same helpers for the meal image. Simple enough.

When I commit the fix, I include the following text in the commit message: "Closes #3". In many SCM solutions, there are post-commit hooks that update tickets. Trac, for instance, has a popular SVN post-commit hook that updates tickets including text like "References #123" or "Closes #123". Github's issue tracker does the same—without even needing to add post-commit software. Nice!

After pushing the fix to github, the issue is automatically closed for me:



The commit even includes a link to the issue:



The automatic issue updater is not quite as full featured as Trac's. It does include the ability to up-vote an issue / feature request. I make immediate use of that feature by up-voting the ability to reference (without closing) issues—in github's own issue tracker.

I have a go at fixing the links between recipes (the only other issue that I am currently tracking). I have to admit defeat, though. The links between records is hard-coded to work with meals, not recipes. My first attempt at fixing it is worth only a complete revert. In git, of course, this is done with:
git checkout .
The last thing I work on today is giving less CSS a try. I would like to see if I can get the rounded corners on the green headers from my legacy application into the new implementation. The existing view:



I create a public/stylesheets/style.less file to hold my minimal CSS definitions. The first thing that I add is color definitions. We use a dark green / light green color scheme (coupled with red in the logo) to give an Italian flavor to the site. The green colors in style.less:
@primary_color: #090;
@secondary_color: #7CCD7C;
Next I define a class for the rounded corners:
.rounded_header {
-moz-border-radius-bottomleft: 0;
-moz-border-radius-bottomright: 0;
-moz-border-radius-topleft: 4px;
-moz-border-radius-topright: 0;
}
I then use the color and the class in the definition of <h1> tags:
h1 {
background-color: @primary_color;
color: white;
font-size: 1.2em;
.rounded_header;
}
I generate the corresponding style.css with the lessc command:
cstrom@jaynestown:~/repos/eee-code$ lessc public/stylesheets/style.less
And I have my rounded colors:



That is a good stopping point for today. I may pick back up with more less CSS tomorrow. That last issue bothers me a bit so I may give that another try first.

Wednesday, July 22, 2009

Annoyances

‹prev | My Chain | next›

Exploring through the site a bit, I notice that I need to default the output to UTF-8. All the data is stored in UTF-8. Things like em-dashes look OK when viewed directly in CouchDB:



But not so OK in Sinatra:



To set the default character encoding in Sinatra, use a before filter:
before do
content_type 'text/html', :charset => 'UTF-8'
end
With that, I have my em-dashes displaying OK:



I could have accomplished just as easily by adding a meta tag to the Haml template. It comes down to a personal preference for me. I rationalize it by thinking of data as a systemic consideration, not a presentation thing.

Next up, I take some to work through some of the issues that I am tracking. While fixing the search page, I notice that I am unable to search on multiple terms (e.g. "fish sticks"). This was caused by not URL encoding the search term when passing it off to couchdb-lucene. The example that I need to pass:
    it "should be able to search multiple terms" do
RestClient.should_receive(:get).
with(/q=foo\+bar/).
and_return('{"total_rows":30,"skip":0,"limit":20,"rows":[]}')

get "/recipes/search?q=foo+bar"
end
The code that makes this pass (using Rack::Utils.escape):
get '/recipes/search' do
@query = params[:q]

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

couchdb_url = "#{@@db}/_fti?limit=20" +
"&q=#{Rack::Utils.escape(@query)}" +
"&skip=#{skip}"

...
(commit)

I have two more minor issues in the tracker that need fixing, but everything else is looking good. After addressing those issues, it will finally be time to explore less to resolve my lack of CSS.

Tuesday, July 21, 2009

Appeasing the Gods with an Offering of Cucumbers

‹prev | My Chain | next›

I woke up today at 3 a.m. and drove to Pittsburgh. I spent all day in client meetings. I then drove home to Baltimore. 20 hours today was dedicated to the endeavor. Still I make a small offering to the gods of my chain in the hopes that they will continue to look upon me with favor.

Even when my brain is not forming coherent thinkings, I still seem to be able to write Cucumber scenarios. It is possible that Cucumber exercises a different part of my brain, enabling me to write scenarios even under extreme circumstances. That, or Cucumber scenarios are dirt simple to write. Either way, one should draw the conclusion that there are no excuses for not writing Cucumber scenarios.

I add scenarios for two features in the legacy application that I have yet to capture in this new version: alternate recipes preparations view and archiving old recipes when updated, better recipes are create added.

The scenario for alternate preparations:
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" in it
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" in it
And a "Buttermilk Pancake" recipe with "buttermilk" in it
And a "Pancake" recipe with "chocolate chips" in it
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
That is a relatively simple feature, requiring only two scenarios to describe it.

Similarly, the archiving feature:
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: No previous or next version of a recipe

Given a "Buttermilk Pancake" recipe with "buttermilk" in it
When I view the recipe
Then I should not see previous versions of the recipe
And I should not see updated versions of the recipe

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 recipe with "buttermilk" is marked as update of the previous 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
And I should not see updated versions of the recipe
And I visit the recipe with "lowfat milk" in it
Then I should see a link to the updated recipe with "buttermilk" in it
And I should not see previous versions of the recipe
Again, this is a relatively simple feature. I do compact two different views of the feature (one from the outdated recipe, the other from the update) into one scenario. I might have created two different scenarios for each perspective, but I prefer the proximity of keeping everything in one scenario—it feels much closer to how an actual user would use the feature (clicking back and forth).
(commit)

Up next: sleep. Once sleep has done the trick, I will use my brain again to figure out what to work on next.

Monday, July 20, 2009

How do you Encode JSON Dates?

‹prev | My Chain | next›

I continue the work today of resolving my failing Cucumber scenarios associated with the navigation between meals. After roughly two minutes of investigation, I come to the following conclusion...

"Ah, nuts!"

I have not been as disciplined as I ought to be with my dates. I have a mixture of two different date formats—"2009-07-20" and "2009/07/20". Rails, it seems, outputs the latter, by default, when Date.today.to_json is invoked. I prefer the pure ISO 8601 formatting of the former.

Sooo....

I need to enforce JSON output in the legacy Rails app and redo the import. In more recent versions of Rails, JSON date formatting is controlled by setting ActiveSupport::JSON::Encoding.use_standard_json_time_format to true. In my legacy application, which is running 2.1.2, I need to set ActiveSupport.use_standard_json_time_format to true. That seems like an odd default. Why would something called "use standard" anything default to false? No matter, I am migrating away from Rails here, so I explicitly set this to a sane value.

After re-running the procedure from the other night, with proper JSON date formatting, I get:



Yay! ISO 8601 feels much better.

The switch back to ISO 8601 date representation not only makes me feel better, but it also resolves every one of my failing Cucumber scenarios:
cstrom@jaynestown:~/repos/eee-code$ cucumber features
...
27 scenarios
9 skipped steps
2 undefined steps
226 passed steps
Nice! All of my previous work was done with the assumption of ISO 8601. I allowed Rails' odd default to influence me over the last couple of days, the end result being non-ISO 8601 dates creeping into the application. Reversing course (including correcting some specs) resolves all.
(commit)

Up next: maybe a little more smoke testing and then onto finally adding CSS.

Sunday, July 19, 2009

Forced Refactoring

‹prev | My Chain | next›

Having resolved my abuse of CouchDB yesterday, I need to make sure that I have not broken anything else. All of my "inside", unit level testing is passing, but I never did run my "outside", integration (a.k.a Cucumber) testing. Those Cucumber tests are where I will start today.

That turns out to be a good choice for a starting point. All of the scenarios in the "Browse Meals" feature are now failing. The first one that I will investigate is this failure:
  Scenario: Navigating between meals                                                # features/browse_meals.feature:54
Given a "Pumpkin is a Very Exciting Vegetable" meal enjoyed on December 3, 2008 # features/step_definitions/meal_details.rb:1
And a "Star Wars: The Dinner" meal enjoyed on February 28, 2009 # features/step_definitions/meal_details.rb:1
And a "Focaccia! The Dinner" meal enjoyed on March 3, 2009 # features/step_definitions/meal_details.rb:1
When I view the "Focaccia! The Dinner" meal # features/step_definitions/meal_details.rb:52
Then I should see the "Focaccia! The Dinner" title # features/step_definitions/meal_details.rb:69
When I click "Star Wars: The Dinner" # features/step_definitions/meal_details.rb:65
Could not find link with text or title or id "Star Wars: The Dinner" (Webrat::NotFoundError)
features/browse_meals.feature:61:in `When I click "Star Wars: The Dinner"'
Then I should see the "Star Wars: The Dinner" title # features/step_definitions/meal_details.rb:69
When I click "Pumpkin is a Very Exciting Vegetable" # features/step_definitions/meal_details.rb:65
Then I should see the "Pumpkin is a Very Exciting Vegetable" title # features/step_definitions/meal_details.rb:69
When I click "Star Wars: The Dinner" # features/step_definitions/meal_details.rb:65
Then I should see the "Star Wars: The Dinner" title # features/step_definitions/meal_details.rb:69
When I click "Focaccia! The Dinner" # features/step_definitions/meal_details.rb:65
Then I should see the "Focaccia! The Dinner" title # features/step_definitions/meal_details.rb:69
The page is rendering OK:



The reason for the Cucumber failure is that the links to previous / next meals no longer contain the meal title. The title used to be the second element of the by_date map function. Yesterday, I changed the map function to return the entire meal instead. It should be a simple matter to hook the code up to the new map function. Except...

When I look at the Sinatra action for meals, I notice that I am now pulling back every single meal:
get %r{/meals/(\d+)/(\d+)/(\d+)} do |year, month, day|
data = RestClient.get "#{@@db}/#{year}-#{month}-#{day}"
@meal = JSON.parse(data)

url = "#{@@db}/_design/meals/_view/by_date"
data = RestClient.get url
@meals_by_date = JSON.parse(data)['rows']


@recipes = @meal['menu'].map { |m| wiki_recipe(m) }.compact

@url = request.url

haml :meal
end
That was not so bad when I was only pulling back IDs and titles. It is bad now that I am pulling back everything in each meal. How bad?

I create a "/benchmarking" action to find out how bad:
get '/benchmarking' do
url = "#{@@db}/_design/meals/_view/by_date"
data = RestClient.get url
JSON.parse(data)

""
end
I test this with ApacheBench:
ab -n 100 http://127.0.0.1:4567/benchmarking
...
Requests per second: 4.77 [#/sec] (mean)
I create a "short" map that only includes title and date:
function (doc) {
if (doc['type'] == 'Meal') {
emit(doc['date'], {'title':doc['title'],'date':doc['date']});
}
}
Pointing "/benchmarking" against this new map:
get '/benchmarking' do
url = "#{@@db}/_design/meals/_view/short"
data = RestClient.get url
JSON.parse(data)

""
end
Running ApacheBench again, I find:
ab -n 100 http://127.0.0.1:4567/benchmarking
...
Requests per second: 24.46 [#/sec] (mean)
Five times faster is not bad. Still, I initially benchmarked Sinatra + CouchDB around 10 times faster. For now, I will stick with the "short" view. Replacing "by_date" view with "by_date_short" gets me decent performance. Updating the Haml view to use the "title" attribute from the updated view resolves the problem found by Cucumber:
  Scenario: Navigating between meals
Given a "Pumpkin is a Very Exciting Vegetable" meal enjoyed on December 3, 2008
And a "Star Wars: The Dinner" meal enjoyed on February 28, 2009
And a "Focaccia! The Dinner" meal enjoyed on March 3, 2009
When I view the "Focaccia! The Dinner" meal
Then I should see the "Focaccia! The Dinner" title
When I click "Star Wars: The Dinner"
Then I should see the "Star Wars: The Dinner" title
When I click "Pumpkin is a Very Exciting Vegetable"
Then I should see the "Pumpkin is a Very Exciting Vegetable" title
When I click "Star Wars: The Dinner"
Then I should see the "Star Wars: The Dinner" title
When I click "Focaccia! The Dinner"
Then I should see the "Focaccia! The Dinner" title
(commit)

Up tomorrow: trying to figure out why the remaining 3 scenarios in the "Browse Meals" Cucumber feature are currently failing.

Saturday, July 18, 2009

Day One: Cleaning up the CouchDB Mess from Day 139

‹prev | My Chain | next›

This is the first post of my second chain. My first lasted 139 days, broken just like (and by) my son's arm (he's OK now). Let's see how long this one can go..

At the end of my last chain, I discovered that I had over-used map/reduce. Over-used it when it was not even necessary.

The problem occurred in the listing of meals by month. I was mapping the meals by the month in which they where made, and then reducing so that all meals in a given month were included in the same result set. I did the reduce wrong, but do not even need map/reduce.

CouchDB views support a startkey and endkey query parameter for extracting a range of keys. If I want all meals from August of 2008, I can supply a startkey of "2008/08/00"—all dates in August are greater than that value (e.g. "2008/08/01" > "2008/08/00"). Similarly, all dates in August are less than "2008/08/99" (September dates are also excluded because "2008/09/01" > "2008/08/99").

Instead expecting to request the by-month reduced view:
      it "should ask CouchDB for meals from year YYYY and month MM" do
RestClient.
should_receive(:get).
with(/key=...2009-05/).
and_return('{"rows": [] }')

get "/meals/2009/05"
end
Now I expect to call the by-date view (non-reduced) with a startkey and endkey:
      it "should ask CouchDB for meals from year YYYY and month MM" do
RestClient.
should_receive(:get).
with(%r{by_date.+startkey=.+2009/05/00.+endkey=.+2009/05/99}).
and_return('{"rows": [] }')

get "/meals/2009/05"
end
Note: the additional match-anything regular expressions account for the quotes that need to surround the JSON formatted values that are required by startkey and endkey.

The resulting code becomes:
get %r{/meals/(\d+)/(\d+)} do |year, month|
url = "#{@@db}/_design/meals/_view/by_date?" +
"startkey=%22#{year}/#{month}/00%22&" +
"endkey=%22#{year}/#{month}/99%22"
data = RestClient.get url
@meals = JSON.parse(data)['rows'].map{|r| r['value']}

@month = "#{year}-#{month}"

url = "#{@@db}/_design/meals/_view/count_by_month?group=true"
data = RestClient.get url
@count_by_year = JSON.parse(data)['rows']

haml :meal_by_month
end
After a few minor fixes to the meals-by-month view spec, I can finally see the meals in August of 2008:



I take a little time to clean up the by-year view in the same way and call it a day.
(commit)

Tomorrow will likely involve some code clean-up. There are probably some CouchDB view no longer being used and the by-year and by-month Sinatra actions are starting to look alike—it may be time to apply some more DRY.

Thursday, July 16, 2009

How Not to Do CouchDB Views

‹prev | My Chain | next›

Tonight, I'm going to find some bugs (dammit). Last night, I indulged in my first look at the assembled Sinatra / CouchDB version of my family cookbook. I found an issue or two, but no crashes. I always have a crash or two.

Boundary conditions are always a good place to probe. I spent much time and wrote many a Cucumber scenario covering boundary conditions of pagination. I seem to have done my job well. Pagination works. Sorting works. Manually entering invalid page numbers even works. Bummer.

I do note a couple of issues in the github issues tracker for my project. I forgot a picture of the meal on the meal page. There are no links between date ordered recipes. The refined query is not always displayed and, when it is, does not supply the correct query parameter. Small little stuff. I'm hunting bigger game.

I finally find something as I am moving between meals-by-month listings. There is a problem when listing meals from August of 2008:



Ah, a type error converting from String to Integer. I knew there had to be one of those somewhere. I am strangely comforted knowing that I finally found it.

The error occurs in Haml template, trying to evaluate one of the meals' dates:
.meals
- @meals.each do |meal|
- date = Date.parse(meal['date'])
I am an old school debugger—no fancy debug mode for me. I stick with print STDERR (or $stderr.print in Ruby terms). I am not sure which meal is causing me trouble, so I dump all of them from July, 2008 in the Sinatra action:
get %r{/meals/(\d+)/(\d+)} do |year, month|
url = "#{@@db}/_design/meals/_view/by_month?group=true&key=%22#{year}-#{month}%22"
data = RestClient.get url
@meals = JSON.parse(data)['rows'].first['value']
@month = "#{year}-#{month}"

raise @meals.pretty_inspect

url = "#{@@db}/_design/meals/_view/count_by_month?group=true"
data = RestClient.get url
@count_by_year = JSON.parse(data)['rows']

haml :meal_by_month
end
What I get from that is:
[[{"title"=>"Star Wars: The Dinner",…},
{"title"=>"Lemony Start for the Weekend",…}],
[{"title"=>"Ideal Pasta for an 8 Year Old Boy",…},
{"title"=>"Spinach Pie",…},
{"title"=>"You Each Take a Side",…}]]
Hunh? An array of arrays? How did that happen?

Sigh. It turns out I wildly abused map-reduce. The reduce function for the meals/by_month design doc (ignore the map function for now):
function(keys, values, rereduce) { values }
The problem is the rereduce, which is set to true when CouchDB is combining intermediate reduce result sets. More info on reduce and rereduce can be found in the documentation. Essentially, when CouchDB is working with large datasets (like the 1,000+ documents in my development database) it divides and conquers—but it expects some help, which is why the rereduce parameter is supplied.

In this case, I need to combine and flatten the arrays of arrays in the values when re-reducing, which can be done with some ugly javascript:
function(keys, values, rereduce) {
if (rereduce) {
var a = [];
for (var i=0; i<values.length; i++) {
for (var j=0; j<values[i].length; j++) {
a.push(values[i][j]);
}
}
return a;
}
else {
return values;
}
}
Yup, that is some pretty ugly Javascript. What makes it even worse is that it is completely unnecessary!

The original problem that I was solving was finding all of the meals in a particular month. I solved this with a by_month map/reduce grouping. More experience with CouchDB has taught me that this can be solved easily, without map/reduce, by using a startkey/endkey on a simple view.

I already have a by_date view (map only). I can grab all of the meals from July 2008 by accessing the by_date view with a startkey of 2008-07-00 and an endkey or 2008-07-99. Demonstrating this in curl:
cstrom@jaynestown:~/repos/eee-code$ curl http://localhost:5984/eee/_design/meals/_view/by_date?startkey=%222008/07/00%22\&endkey=%222008/07/99%22
{"total_rows":500,"offset":491,"rows":[
{"id":"2008-07-10","key":"2008/07/10","value":["2008-07-10","You Each Take a Side"]},
{"id":"2008-07-13","key":"2008/07/13","value":["2008-07-13","Star Wars: The Dinner"]},
{"id":"2008-07-19","key":"2008/07/19","value":["2008-07-19","Lemony Start for the Weekend"]},
{"id":"2008-07-21","key":"2008/07/21","value":["2008-07-21","Spinach Pie"]},
{"id":"2008-07-28","key":"2008/07/28","value":["2008-07-28","Ideal Pasta for an 8 Year Old Boy"]}
]}
Grr... That's a much better solution than my silly map/reduce. Looks like I have some refactoring to do. Refactoring and some warnings to add to old blog posts.

I'll get started on that tomorrow.

Wednesday, July 15, 2009

Smoke Tests

‹prev | My Chain | next›

Today, I need to do a little smoke testing.

You see, I have looked at my Sinatra application approximately one time during the 4 months that I have been working on it. I have made all progress to date relying on unit tests and Cucumber scenarios. The closest I have come to a browser in that time has been Webrat.

Now that I have 1,000+ recipes and meals loaded from my legacy application, it's time to see how things work in live conditions.

To be clear, I expect a few minor problems. My testing should have caught the big stuff and most of the little stuff. But tests seem to rely on idealized data that does not necessarily show up in live data. The thing I almost always do is expect a number when the actual value is a string (or vice versa). Let's see how well I did avoiding that little trap...



With naked CSS, the links to the recipe categories force everything else on the homepage "below the fold" (out of the view port). This makes the most obvious thing to explore first in my smoke test the categories:



No results?! Oh, man! I spent a lot of time getting couchdb-lucene working, including integration tests. How did I mess that up? My smoke testing is off to a poor start.

It turns out that the load from the legacy Rails application completely missed categories (tag_names). All that is needed is a small addition to Recipe class:
class Recipe < ActiveRecord::Base
def tag_names; tags.map(&:name) end
end
(I updated last night's instructions so that I do not forget in the future).

With that, I rake couchdb:reset and re-run the legacy data load (all 1,000+ documents). Then, reloading the search results page for breakfast recipes, I find:



Yay!

Having spent the better part of four months working on this, it is pretty exciting to actually use the application with real data in it. Sorting works. Pagination works. I ought to expect it to work, what with all of the unit and integration/Cucumber tests, but there is something about using it in an actual browser that gives a little thrill.

That thrill is tempered by two things:
  • the search field is not displayed (it was shown when no results were found)
  • there is a weird character displayed in the "Next" link of the pagination
I make a note of the missing search field by adding it to a Cucumber scenario:
    Scenario: Matching a word in the ingredient list in full recipe search

Given a "pancake" recipe with "chocolate chips" in it
And a "french toast" recipe with "eggs" in it
And a 0.5 second wait to allow the search index to be updated
When I search for "chocolate"
Then I should see the "pancake" recipe in the search results
And I should not see the "french toast" recipe in the search results
And I should see the search field for refining my search
The weird character (at the bottom):



The weird character is just an encoding issue caused by including UTF-8 directly in the pagination helper:
      links <<
if current_page == last_page
%Q|<span class="inactive">Next »</span>|
else
%Q|<a href="#{link}&page=#{current_page + 1}">Next »</a>|
end
I am not sure why I included the actual double right angle quote in there rather than an HTML entity (I used the HTML entity in the Previous links). At any rate, replacing "»" with "&raquo;" resolves the issue:



No test changes are required because Webrat is HTML entity aware—it knows that "»" and "&raquo;" are the same thing.

Aside from the minor issues with the recipe search (which were mostly caused by a bad load from the legacy application) I do not hit any more issues in my cursory smoke test. From the recipe, I can navigate back up the breadcrumbs to meals, then lists of meals by month, lists of meals by year and then back to the homepage.

Not even a string that is treated as an integer?! This time, it would seem not.

Up tomorrow: I will likely smoke test tomorrow a bit more (seriously, I must have something buggy in there). I will also note a few more features that I am missing.