Double Shot #318
I seem to be developing more expertise than I really wanted in liquid templates.
Simple Test Metrics in Your Rails App, and What They Mean - Some thoughts about appropriate code/test rations and rcov numbers.
Better Controllers with finder_filter and from_param
I've been nibbling away at how to write the code in Rails controllers recently, and I'm at a point where I'm fairly happy with what I'm doing. The two design requirements for me were:
Rails already supports friendly URLs, sort of, with to_param. Consider the canonical Post model from a blogging application:
[sourcecode language='ruby']
class Post < ActiveRecord::Base
def to_param
"#{id}-#{title}".gsub(" ","-").downcase.gsub(/[^a-z0-9-]/,"").gsub(/(-)*$/,"")
end
end
[/sourcecode]
Armed with that definition, it's possible to write a controller that works with URLs like /blogs/6-my-latest-blog-entry:
[sourcecode language='ruby']
class PostsController < ApplicationController
def index
@posts = Post.find(:all)
end
def show
@post = Post.find(params[:id])
end
def new
@post = Post.new
end
def edit
@post = Post.find(params[:id])
end
def create
@post = Post.new(params[:post])
if @post.save
flash[:notice] = 'Post was successfully created.'
redirect_to(@post)
else
render :action => "new"
end
end
def update
@post = Post.find(params[:id])
if @post.update_attributes(params[:post])
flash[:notice] = 'Post was successfully updated.'
redirect_to(@post)
else
render :action => "edit"
end
end
def destroy
@post = Post.find(params[:id])
@post.destroy
redirect_to(posts_url)
end
end
[/sourcecode]
OK, works fine. But there are a couple of things that annoy me. First is repeating the code to turn things into a slug in every model (though, with Inflector.paramterize coming in 2.2, this is going to get easier). Second is having to include a numeric ID at the start of the parameter. Third is those repeated calls to find(params[:id]).
Enter a couple of plug-ins: finder_filter, which gets rid of some of the duplication, and from_param, which handles the URLs a bit better. With those installed, I can rewrite the code this way:
[sourcecode language='ruby']
class Post < ActiveRecord::Base
def to_param
title.urlize
end
end
class PostsController < ApplicationController
finder_filter :only => [:show, :edit, :update, :destroy]
def index
@posts = Post.find(:all)
end
def show
end
def new
@post = Post.new
end
def edit
end
def create
@post = Post.new(params[:post])
if @post.save
flash[:notice] = 'Post was successfully created.'
redirect_to(@post)
else
render :action => "new"
end
end
def update
if @post.update_attributes(params[:post])
flash[:notice] = 'Post was successfully updated.'
redirect_to(@post)
else
render :action => "edit"
end
end
def destroy
@post.destroy
redirect_to(posts_url)
end
end
[/sourcecode]
Perfect? Naw, not really. But to my eyes, better. And getting better a little bit at a time is good enough for me.
The originals of those two plugins are not mine: finder_filter comes from Matthew Bass, and from_param comes from Michael Bleigh. But I've tweaked them both to work better together, which is why I linked to my forks of the projects.
- Write as little code as possible
- Get decent SEO-friendly URLs
Rails already supports friendly URLs, sort of, with to_param. Consider the canonical Post model from a blogging application:
[sourcecode language='ruby']
class Post < ActiveRecord::Base
def to_param
"#{id}-#{title}".gsub(" ","-").downcase.gsub(/[^a-z0-9-]/,"").gsub(/(-)*$/,"")
end
end
[/sourcecode]
Armed with that definition, it's possible to write a controller that works with URLs like /blogs/6-my-latest-blog-entry:
[sourcecode language='ruby']
class PostsController < ApplicationController
def index
@posts = Post.find(:all)
end
def show
@post = Post.find(params[:id])
end
def new
@post = Post.new
end
def edit
@post = Post.find(params[:id])
end
def create
@post = Post.new(params[:post])
if @post.save
flash[:notice] = 'Post was successfully created.'
redirect_to(@post)
else
render :action => "new"
end
end
def update
@post = Post.find(params[:id])
if @post.update_attributes(params[:post])
flash[:notice] = 'Post was successfully updated.'
redirect_to(@post)
else
render :action => "edit"
end
end
def destroy
@post = Post.find(params[:id])
@post.destroy
redirect_to(posts_url)
end
end
[/sourcecode]
OK, works fine. But there are a couple of things that annoy me. First is repeating the code to turn things into a slug in every model (though, with Inflector.paramterize coming in 2.2, this is going to get easier). Second is having to include a numeric ID at the start of the parameter. Third is those repeated calls to find(params[:id]).
Enter a couple of plug-ins: finder_filter, which gets rid of some of the duplication, and from_param, which handles the URLs a bit better. With those installed, I can rewrite the code this way:
[sourcecode language='ruby']
class Post < ActiveRecord::Base
def to_param
title.urlize
end
end
class PostsController < ApplicationController
finder_filter :only => [:show, :edit, :update, :destroy]
def index
@posts = Post.find(:all)
end
def show
end
def new
@post = Post.new
end
def edit
end
def create
@post = Post.new(params[:post])
if @post.save
flash[:notice] = 'Post was successfully created.'
redirect_to(@post)
else
render :action => "new"
end
end
def update
if @post.update_attributes(params[:post])
flash[:notice] = 'Post was successfully updated.'
redirect_to(@post)
else
render :action => "edit"
end
end
def destroy
@post.destroy
redirect_to(posts_url)
end
end
[/sourcecode]
Perfect? Naw, not really. But to my eyes, better. And getting better a little bit at a time is good enough for me.
The originals of those two plugins are not mine: finder_filter comes from Matthew Bass, and from_param comes from Michael Bleigh. But I've tweaked them both to work better together, which is why I linked to my forks of the projects.
Double Shot #317
Yesterday was all about frustration. Hopefully today will be better.
Why online advertising sucks, and is a bubble - Doc Searls nails it. In an era of economic contraction, I expect online ad budgets to get slashed as sponsors get tired of the lack of demonstrable ROI. This will be followed by many marginal Web 2.0 companies going belly up. Have a nice day.
wysihat - In true NIH fashion, 37signals is building a framework for WYSIWYG javascript editors.
Suspenders - The thoughbot guys join the list of those with their own "tweaked Rails" starter projects.
Announcing Mingle 2.1 - Version bump to ThoughtWorks' agile project management tool.
Double Shot #316
Spent part of yesterday scheming over new business venture. I believe every consultant should always have at least one scheme coming soon.
Observation - Rails Consulting Rates Lower Than .NET Rates - Rob Bazinet muses and asks for feedback.
Rails Rumble 2008 Apps - Just in case you want to see what others managed to build in 48 hours.
Double Shot #315
Weekends are always so much better for writing code than Mondays are.
The Hobo Cookbook - Guidance for folks who want to use the Hobo framework for Rails.
Using DTrace to observe the SQL statements on a live running MySQL database - Something I've missed from SQL Server. Here's instructions to make it easy in OS X.
Non-test code is trivial - Yes, testing is hard.
Elements of Ruby Style - An attempt at a Ruby style guide. In my experience, such things lead only to arguments or concentration on form over function, but hey, maybe this one will be different. (via RubyFlow)
Validating slugs against existing routes in Rails - Useful if you allow unadorned /:user-supplied-id routes.
Multiselect control in Rails with a Many to Many relationship - One of several guides out there on this subject.
suprails - "Super Rails" command that comes with its own DSL to let you install gems, freeze rails, set up git, and perform other pre-coding steps.
Response Splitting Risk - A security issue in Rails is causing new point versions to come out. Here's the 2.0.5 release announcement.
JavaScript: TraceMonkey - Instructions for turning on the new, faster JavaScript engine in Firefox 3.1 beta 1.
Coming in Rails 2.2: Delegate Prefixes
Delegates are a useful feature that I haven't seen used in that many Rails codebases - perhaps I've just been looking at the wrong codebases. Active Support makes them available for Modules generally, but the use case I find myself most often exercising is with Active Record classes. Delegates let you take some methods and send them off to another object to be processed.
For example, suppose you have a User class for anyone registered on your site, and a Customer class for those who have actually placed orders:
[sourcecode language='ruby']
class User << ActiveRecord::Base
belongs_to :customer
end
class Customer << ActiveRecord::Base
has_one :user
end
[/sourcecode]
If you're hanging on to a Customer instance, you can get their User information with methods like @customer.user.name and @customer.user.email.
Delegation allows you to simplify this:
[sourcecode language='ruby']
class User << ActiveRecord::Base
belongs_to :customer
end
class Customer << ActiveRecord::Base
has_one :user
delegate :name, :email, :to => :user
end
[/sourcecode]
Now you can refer to @customer.name and @customer.email directly. That's all been around and works in the current release version of Rails.
Delegate prefixes just appeared in edge Rails and will work in Rails 2.2. If you delegate behavior from one class to another, you can now specify a prefix that will be used to identify the delegated methods. For example:
[sourcecode language='ruby']
class User << ActiveRecord::Base
belongs_to :customer
end
class Customer << ActiveRecord::Base
has_one :user
delegate :name, :email, :to => :user, :prefix => true
end
[/sourcecode]
This will produce delegated methods @customer.user_name and @customer.user_email. You can also specify a custom prefix:
[sourcecode language='ruby']
class User << ActiveRecord::Base
belongs_to :customer
end
class Customer << ActiveRecord::Base
has_one :user
delegate :name, :email, :to => :user, :prefix => :account
end
[/sourcecode]
This will produce delegated methods @customer.account_name and @customer.account_email.
For example, suppose you have a User class for anyone registered on your site, and a Customer class for those who have actually placed orders:
[sourcecode language='ruby']
class User << ActiveRecord::Base
belongs_to :customer
end
class Customer << ActiveRecord::Base
has_one :user
end
[/sourcecode]
If you're hanging on to a Customer instance, you can get their User information with methods like @customer.user.name and @customer.user.email.
Delegation allows you to simplify this:
[sourcecode language='ruby']
class User << ActiveRecord::Base
belongs_to :customer
end
class Customer << ActiveRecord::Base
has_one :user
delegate :name, :email, :to => :user
end
[/sourcecode]
Now you can refer to @customer.name and @customer.email directly. That's all been around and works in the current release version of Rails.
Delegate prefixes just appeared in edge Rails and will work in Rails 2.2. If you delegate behavior from one class to another, you can now specify a prefix that will be used to identify the delegated methods. For example:
[sourcecode language='ruby']
class User << ActiveRecord::Base
belongs_to :customer
end
class Customer << ActiveRecord::Base
has_one :user
delegate :name, :email, :to => :user, :prefix => true
end
[/sourcecode]
This will produce delegated methods @customer.user_name and @customer.user_email. You can also specify a custom prefix:
[sourcecode language='ruby']
class User << ActiveRecord::Base
belongs_to :customer
end
class Customer << ActiveRecord::Base
has_one :user
delegate :name, :email, :to => :user, :prefix => :account
end
[/sourcecode]
This will produce delegated methods @customer.account_name and @customer.account_email.
Double Shot #314
Just mass-unfollowed a bunch of people on Twitter. Sorry, folks, but something had to give.
Liquid Wiki - The Liquid markup language is something I'm currently struggling to make work. Documentation is sketchy, to say the least.
Rails for .NET Developers - Latest book from Pragmatic. I read it in manuscript; good stuff.
RailsConf'09: Accepting Proposals for Vegas! - Call me back when RailsConf is held in a city fit for human beings.
Rails 2.2 Deprecations
Rails 2.2 won't be all about new features. The Rails team is also taking advantage of this release to deprecate some existing features. Here's a list of what will be going away in the 2.2 release:
The country_select helper has been removed. This is the one most likely to affect existing applications, I think, but it's going away because of arguments over which list of countries to use. The deprecation page points to a couple of replacement plugins.
Rails::SecretKeyGenerator has been replaced by ActiveSupport::SecureRandom.
render_component is deprecated. If you need it, grab the render_components plugin.
ActiveRecord::Base.allow_concurrency no longer has any effect. Might as well stop calling it. Remember, though, that ActiveRecord is getting connection pool management.
ActiveRecord::Errors.default_error_messages has been deprecated in favor of I18n.translate('activerecord.errors.messages')
The %s and %d interpolation syntax for internationalization is deprecated.
String#chars has been deprecated in favor of String#mb_chars.
Durations of fractional months or fractional years are deprecated. Use Ruby's core +Date+ and +Time+ class arithmetic instead.
render_component is deprecated. If you need it, grab the render_components plugin.
Double Shot #313
Tonight I'm demonstrating Rails to the local .NET user group. I may survive.
Firefox 3.1 beta 1 now available for download - The alphas were pretty rough, but I'll be giving the beta a try shortly.
Slim-Attributes v0.5.0 released - ActiveRecord patch to speed up performance if you don't feel like limiting your finds to only the fields you need.
ScrumNinja - New hosted project management tool built for Scrum teams.
BackgrounDRb 1.1 - Better scripting, logging, tests, yay.
Double Shot #312
The week started out with a bang: I completed the first draft of Getting Started with Rails and got started on a new client project as well.
Rails Migrations Cheatsheet - A compact guide, available in either HTML or PDF.
Announcing follow cost: Is that Twitter celebrity worth the pain? - According to this tool, following me results in much pain.
Blank: A Starter App for r_c and shoulda Users - Another attempt at a Rails starter app with all the trimmings. (via RailsGuts, Blank)
suprails - Early days yet, but worth watching: the plan is to create a configurable Rails application generator driven by its own little DSL.
Life in the fast lane - Thoughtbot introduces Pacecar, a plugin to generate a whole bunch of named scopes for you.
More Goodies from Rails 2.2
You'll be seeing plenty about the big new Rails 2.2 features soon enough, I'm sure. But there is also a sprinkling of new helpers of one sort or another coming in this version. Here's a selection that you might like:
Model.find_last_by_attributes - The opposite of find_first_by_attributes.
Model.find_by_attributes! - Raises an exception instead of returning nil if nothing can be found.
Date and Time classes now supply past?, today?, and future? for comparisons.
Array#second through Array#tenth are aliases for Array#[1] through Array#[9]
Enumerable#several is the equivalent of collection.size > 1
Inflector#parameterize produces a URL-ready version of its input, for use in +to_param+.
Double Shot #311
It was a fairly productive weekend for me: a new version of db_populate, a minor update to from_param, the first complete draft of The Rails Initialization Process, and a big chunk of work on Getting Started with Rails.
FiveRuns TuneUp for Merb - The FiveRun guys bring their monitoring solution to Merb as well as Rails.
About metric_fu 0.8.0 - Solution for generating metrics reports, ideal for use with continuous integration.
Coming home to Vim - I'm glad it works for some people (in this case, Jamis Buck). Personally, I wouldn't touch vi/vim/emacs with a stick.
2 Weeks in Rails - Another new roundup.
Exceptional.launch(:paid_plans => true) Exceptional has announced their formal launch & paid plans.
Test Your Rake Tasks - Something I really need to get a handle on for a project I'm on now.
Rails and the (Possibly) Coming Economic Downturn - John Moody proposes a discussion topic: how do you avoid becoming roadkill?
Double Shot #310
Double Shot #309
Authentication in Cucumber Tests
I was complaining a few days ago that I was having trouble with authentication in cucumber stories in conjunction with RSpec. It turned out I was making a very stupid mistake ("administrator" and "admin" are not, in fact, the same thing). But in the interests of helping out anyone who ends up here via search engine, here's how I've got it sorted out for now.
Here's one of our current cucumber scenarios:
[sourcecode language='ruby']
Scenario: See all vendors
Given I am logged in as a user in the administrator role
And There are 3 vendors
When I go to the manage vendors page
Then I should see the first 3 vendor names
[/sourcecode]
And here's the corresponding part of the step file that handles the login:
[sourcecode language='ruby']
Given /i am logged in as a user in the (.*) role/i do |role|
@user = Factory.create(:user, :name => "the user",
:login => "the_login",
:password => "password",
:password_confirmation => "password")
@role = Factory.create(:role, :rolename => role)
@user.roles << @role
visits "/login"
fills_in("login", :with => "the_login")
fills_in("password", :with => "password")
clicks_button("Log in")
end
[/sourcecode]
Note that I'm using Factory Girl to instantiate objects in this application's tests.
There may be a better pattern for this - I'm just getting started with cucumber. But for now, it works for me.
Here's one of our current cucumber scenarios:
[sourcecode language='ruby']
Scenario: See all vendors
Given I am logged in as a user in the administrator role
And There are 3 vendors
When I go to the manage vendors page
Then I should see the first 3 vendor names
[/sourcecode]
And here's the corresponding part of the step file that handles the login:
[sourcecode language='ruby']
Given /i am logged in as a user in the (.*) role/i do |role|
@user = Factory.create(:user, :name => "the user",
:login => "the_login",
:password => "password",
:password_confirmation => "password")
@role = Factory.create(:role, :rolename => role)
@user.roles << @role
visits "/login"
fills_in("login", :with => "the_login")
fills_in("password", :with => "password")
clicks_button("Log in")
end
[/sourcecode]
Note that I'm using Factory Girl to instantiate objects in this application's tests.
There may be a better pattern for this - I'm just getting started with cucumber. But for now, it works for me.
Double Shot #308
- webrat_story_steps - Looks useful for webrat-based testing, though it's a bit out of sync with the latest webrat release.
- Captor: A Capistrano GUI - Interesting, though unfinished.
- zena - New Rails-based CMS just going into beta after extensive alpha testing.
Routing Changes in Rails 2.2
For the most part, routing is unchanged in the upcoming Rails 2.2 release. But there are two changes that add a little extra syntactic sugar to your routes.
First, as Ryan Daigle covered last month, there's a new :shallow option for nested routes. This change goes a long way to answer the classic objections to deeply-nested routes. What shallow nesting does is give you additional route helpers. For example, with this declaration:
[sourcecode language='ruby']
map.resources :rooms, :shallow => true do |room|
room.resources :shelves do |shelf|
shelf.resources : books
end
end
[/sourcecode]
Any of these routes will be recognized:
[sourcecode language='ruby']
/rooms/1/shelves/2/books/3 ==> room_shelf_book_path(1,2,3)
/shelves/2/books/3 ==> shelf_book_path(2,3)
/books/3 ==> book_path(3)
[/sourcecode]
You can also combine shallow nesting with :has_one or :has_many for a more compressed syntax:
[sourcecode language='ruby']
map.resources :rooms, :has_many => { :shelves => :books }, :shallow => true
[/sourcecode]
The second change is that you can now supply an array of methods for new member or collection routes on resources. With Rails 2.1, you had to either pick a method or use the wildcard :any method. With Rails 2.2, you can specify just the verbs that a route really needs:
[sourcecode language='ruby']
map.resources :books, :collection => { :search => [:get, :post] }
[/sourcecode]
First, as Ryan Daigle covered last month, there's a new :shallow option for nested routes. This change goes a long way to answer the classic objections to deeply-nested routes. What shallow nesting does is give you additional route helpers. For example, with this declaration:
[sourcecode language='ruby']
map.resources :rooms, :shallow => true do |room|
room.resources :shelves do |shelf|
shelf.resources : books
end
end
[/sourcecode]
Any of these routes will be recognized:
[sourcecode language='ruby']
/rooms/1/shelves/2/books/3 ==> room_shelf_book_path(1,2,3)
/shelves/2/books/3 ==> shelf_book_path(2,3)
/books/3 ==> book_path(3)
[/sourcecode]
You can also combine shallow nesting with :has_one or :has_many for a more compressed syntax:
[sourcecode language='ruby']
map.resources :rooms, :has_many => { :shelves => :books }, :shallow => true
[/sourcecode]
The second change is that you can now supply an array of methods for new member or collection routes on resources. With Rails 2.1, you had to either pick a method or use the wildcard :any method. With Rails 2.2, you can specify just the verbs that a route really needs:
[sourcecode language='ruby']
map.resources :books, :collection => { :search => [:get, :post] }
[/sourcecode]
Double Shot #307
Now I know that getting to the Rapleaf API from Rails is trivial. I may never need this knowledge again.
- Tunnel 1.1 - Nothing to do with Rails, really; just a cute game for MacBooks that uses the accelerometer as a control device.
- Is the Hourly Model Broken? - Musings from RedMonk's Stephen O'Grady. He's writing from the analyst perspective, but this applies just as much to developers.
- Release 0.8.0 - Of the Mack framework, that is.
- Splitting models into several smaller files in Rails - I'm personally not convinced big models are a problem (I'd rather look in one large file than 6 small ones when tracing code). But if you disagree, here's a solution for loading the pieces.
- Can't activate rubyforge (=0.4.5) problems - Adding a bit of Google juice to this solution (uninstall and reinstall) since I ran smack into the problem tonight.
Upgrading MySQL 5.0 to 5.1 on OS X
I'm too lazy to build from source if there's not a good reason, and for running MySQL on OS X, I've yet to come across a good reason. So, I just use the MySQL Community downloads. Over the weekend I upgraded from 5.0.37 to the latest 5.1 download, because I was hard up against a MySQL bug that was fixed in later builds.
Unfortunately, while just running the installer for 5.1 worked great, it pointed the new server at a brand new set of databases - orphaning the couple of dozen databases I was working with. This was Not Good. The fix comes in two parts.
First, create /etc/my.cnf, with a single entry pointing to the old database files:
[mysqld]
datadir=/usr/local/mysql-5.0.37-osx10.4-i686/data
Second, tell MySQL to upgrade the files:
sudo mysql_upgrade -u root
Unfortunately, while just running the installer for 5.1 worked great, it pointed the new server at a brand new set of databases - orphaning the couple of dozen databases I was working with. This was Not Good. The fix comes in two parts.
First, create /etc/my.cnf, with a single entry pointing to the old database files:
[mysqld]
datadir=/usr/local/mysql-5.0.37-osx10.4-i686/data
Second, tell MySQL to upgrade the files:
sudo mysql_upgrade -u root
Rails 2.2 Migration Goodies
With the imminent release of Rails 2.2, I've been spending a lot of time looking at the source code and git checkins. One area where developers will appreciate some small tweaks is migrations.
If you're not working on a multi-developer team, the default (UTC-based) numbering for migrations is a nuisance. You'll be able to change that by setting a configuration variable:
[sourcecode language='ruby']
config.active_record.timestamped_migrations = false
[/sourcecode]
That will give you the old, integer prefixes instead of the new fancy ones. Once you have those, it becomes easier to edit and re-run a specific migration, thanks to the fact that rake db:migrate:redo now takes an optional VERSION to specify the target migration to redo.
Finally, Rails 2.2 is getting transactional migrations - if you use PostgreSQL. Historically, multiple-step Rails migrations have been a source of trouble. If something went wrong during a migration, everything before the error changed the database and everything after the error wasn't applied. Also, the migration version was stored as having been executed, which means that it couldn't be simply rerun by rake db:migrate:redo after you fix the problem. Transactional migrations change this by wrapping migration steps in a DDL transaction, so that if any of them fail, the entire migration is undone.
If you're not working on a multi-developer team, the default (UTC-based) numbering for migrations is a nuisance. You'll be able to change that by setting a configuration variable:
[sourcecode language='ruby']
config.active_record.timestamped_migrations = false
[/sourcecode]
That will give you the old, integer prefixes instead of the new fancy ones. Once you have those, it becomes easier to edit and re-run a specific migration, thanks to the fact that rake db:migrate:redo now takes an optional VERSION to specify the target migration to redo.
Finally, Rails 2.2 is getting transactional migrations - if you use PostgreSQL. Historically, multiple-step Rails migrations have been a source of trouble. If something went wrong during a migration, everything before the error changed the database and everything after the error wasn't applied. Also, the migration version was stored as having been executed, which means that it couldn't be simply rerun by rake db:migrate:redo after you fix the problem. Transactional migrations change this by wrapping migration steps in a DDL transaction, so that if any of them fail, the entire migration is undone.
subscribe via RSS