Tasteful Routes
I was browsing Github today when I noticed the URL pattern they had employed for Pull Requests 2.0. The index is at /pulls as you’d expect, however the show action was in the format of /pull/1. Here’s an example. I actually think it’s rather nice and reads better than the Rails default of /pulls/1.
Here is how to corral Rails into giving you these types of links for a collection and a nested collection (excuse the Ruby 1.9 hash syntax):
match 'jobs' => 'jobs#index', as: :jobs
match 'jobs' => 'jobs#create', via: :post
resources :job, controller: 'jobs', except: [:index, :create] do
match 'items' => 'items#index', as: :items
match 'items' => 'items#create', via: :post
resources :item, controller: 'items', except: [:index, :create]
end
This gives you the following:
$ rake routes
jobs GET /jobs(.:format) {:action=>"index", :controller=>"jobs"}
POST /jobs(.:format) {:action=>"create", :controller=>"jobs"}
job GET /job/:id(.:format) {:action=>"show", :controller=>"jobs"}
edit_job GET /job/:id/edit(.:format) {:action=>"edit", :controller=>"jobs"}
PUT /job/:id(.:format) {:action=>"update", :controller=>"jobs"}
DELETE /job/:id(.:format) {:action=>"destroy", :controller=>"jobs"}
new_job GET /job/new(.:format) {:action=>"new", :controller=>"jobs"}
job_items GET /job/:job_id/items(.:format) {:action=>"index", :controller=>"items"}
POST /job/:job_id/items(.:format) {:action=>"create", :controller=>"items"}
job_item GET /job/:job_id/item/:id(.:format) {:action=>"show", :controller=>"items"}
edit_job_item GET /job/:job_id/item/:id/edit(.:format) {:action=>"edit", :controller=>"items"}
PUT /job/:job_id/item/:id(.:format) {:action=>"update", :controller=>"items"}
DELETE /job/:job_id/item/:id(.:format) {:action=>"destroy", :controller=>"items"}
new_job_item GET /job/:job_id/item/new(.:format) {:action=>"new", :controller=>"items"}
Update: I’ve created a gem to more easily acheive this. You can read about it on the Icelab blog.
Railscamp VI
There is much to be said about the latest railscamp instalment. Simply, it rocked. From international speakers to multiplayer zombie rampages, railscamp hasn’t lost any of it’s mojo. And that’s despite it doubling in size. So big props must go out to Ben Schwarz, John Barton and Pat Allen for bringing it all together.
Then there was the hacking. Ben Webster, Matt Allen and myself made an app which will be used to benefit charities, swinging ourselves some booty. I’ll post some more info when we are closer to pushing it out. It’s rather exciting really.
Handling Deploys with Gem Bundler
The best method of usage for Gem Bundler we have found is to check in only your Gemfile and the gem cache dir, ignoring the rest in .gitignore:
vendor/bundler_gems/environment.rb
vendor/bundler_gems/gems
vendor/bundler_gems/specifications
Now we have our known working .gem files checked in we can ensure that on deploy only these will be used on the server by passing Bundler the cached flag when deploying with Capistrano:
after 'deploy:update_code', 'gems:bundle'
namespace :gems do
task :bundle, :roles => :app do
run "cd #{release_path} && gem bundle --cached"
end
end
Otherwise Bundler will look for updated gems and download them when you have not locked your Gemfile gems to specific versions. Stinging you big time, especially when APIs change on point releases..
Anyhow, the reason you would want to do this as opposed to just checking in the whole vendor/bundler_gems dir is that you won’t dirty up you history with thousand line commits when upgrading gems.
Homebrew, FreeTDS and RubyODBC
Homebrew is awesome. On my new macbook I elected to use it in place of MacPorts and now I’m in love. I don’t want to give you too much in the way of a sales pitch but all I’ll say is that I feel I’m now that much closer to The Perfect Setup™ (Along with RVM, which is awesome as well).
One thing I used Homebrew for was installing the packages required to get my Rails app talking to SQL Server. There already existed a formula for UnixODBC, but I had to write ones for FreeTDS and RubyODBC which was simple enough (hopefully these will get pulled upstream soon).
After this getting ODBC to work it was a simple matter of:
brew install unixodbc
brew install freetds
brew install ruby-odbc
Configure ~/.freetds.conf and ~/.odbc.ini
Combine this with the awesome activerecord-sqlserver-adapter and it all works a charm. I should also add that this is on Snow Leopard, hence the need for UnixODBC.
Translate Legacy Attribute Names
Recently I’ve been working a lot with legacy databases, wrapping them with rails apps and exposing them to REST actions. One thing that annoyed me was the terribly inconsistent column naming conventions I came across. So I used alias_attribute to give them nicer names, but when it came to running to_xml on the models I found I needed to specify which attributes to leave out and use the alias’s instead. Not so dry. So this is what I came up with:
class Candidate < Legacy::Base
set_table_name 'CandidateData'
set_primary_key 'Fileno'
@@translations = {
'Fileno' => 'id',
'Familyname' => 'last_name',
'GivenNames' => 'first_name',
'DOB' => 'date_of_birth',
'Emailaddr' => 'email',
'Address' => 'address',
'City' => 'city',
'State' => 'state',
'PostCode' => 'postal_code',
'PhoneHome' => 'phone_home',
'PhoneWork' => 'phone_work',
'mobile' => 'phone_mobile'
}
@@translations.each {|k,v| alias_attribute(v, k)}
alias_method :ar_to_xml, :to_xml
def to_xml(options = {}, &block)
default_options = {
:except => @@translations.keys,
:methods => @@translations.values
}
self.ar_to_xml(options.merge(default_options), &block)
end
Also you only have to translate the attributes that need it as they will come through to to_xml as normal if not included in the translation hash.
Sphinx on Dreamhost
If you wish to to install the Sphinx search engine on Dreamhost, you can and it is really quite easy and painless.
The first step is to download, compile and install sphinx into your home directory (as you do not have permissions to install this elsewhere):
cd ~/
mkdir -p local
wget http://sphinxsearch.com/downloads/sphinx-0.9.8.1.tar.gz
tar -xzf sphinx-0.9.8.1.tar.gz
cd sphinx-0.9.8.1/
./configure --prefix=$HOME/local/ --exec-prefix=$HOME/local/
make
install
Now you simply need to modify your $PATH to include ~/local/bin:
echo "export PATH=\"$PATH:~/local/bin\"" >> ~/.bash_profile
source ~/.bash_profile
Now you should have a working searchd and your only remaining concern is how to keeping it running, as Dreamhost will kill off long running processes. My solution was to add it to crontab:
* * * * * /home/you/local/bin/searchd --config /home/you/path/sphinx.conf
This appears to be a grey area with Dreamhost, but I’ve never really seen sphinx use any real CPU or memory (on my sites at least) and personally I think they should make it available as part of their standard build.
Active Record Association Methods
I often forget that you can define methods for an Active Record associations in a block after the association declaration like so:
class Order < ActiveRecord::Base
has_many :items, :dependent => :destroy do
def total
inject(0) {|sum, s| sum += s.price}
end
end
end
>> @order = Order.last
>> @order.items.total
=> 27.45
Most of the time this makes more sense than defining them as instance methods in the parent model and much more sense than class methods in the child because it will require the association to work anyhow (perhaps though I haven’t chosen the best example to illustrate this as you could potentially use an Item.total method independently eg. Item.all.total, but you get the picture).
Load Gems in test.rb
Just a quick tip. Don’t load gems that you only require for testing purposes in your Rails environment.rb. Put them in your test.rb instead, like so:
config.gem 'thoughtbot-shoulda', :lib => 'shoulda', :source => 'http://gems.github.com'
This works fine and will give you the added advantage of shaving off a few fractions from your production startup time.
Startup time is something of which I have become very sensitive to lately due to way Dreamhost kills off idle Passenger processes rather quickly. So if you too are hosting with Passenger pay careful attention to the startup time of your application.
Sinatra 0.9 on Dreamhost
There is lots of good information out there on how to deploy Sinatra apps to Dreamhost but they don’t really cover off the Sinatara 0.9 release which unfortunately depends on a newer version of the Rack gem that dreamhost does not yet have installed. The missing piece of the puzzle was that you can actually override the Rack that Passenger loads by vendoring Rack and requiring it in your config.ru file.
require 'rubygems'
require 'vendor/rack/lib/rack'
require 'vendor/sinatra/lib/sinatra'
disable :run
set :app_file, 'yourapp.rb'
set :views, '/full/path/views'
require 'yourapp'
run Sinatra::Application
Now just to tidy things up and prevent me from having junk in my repository I updated my Capistrano script to gem install Sinatra/Rack and unpack them to /vendor on deploy:setup.
after 'deploy:setup', 'vendor_gems:install_and_unpack'
after 'deploy:update_code', 'vendor_gems:symlink'
namespace :vendor_gems do
task :install_and_unpack do
run 'gem install sinatra -v 0.9.0.4' # Also installs rack 0.9.1
run "cd #{shared_path}/system && gem unpack rack && mv rack-* rack"
run "cd #{shared_path}/system && gem unpack sinatra && mv sinatra-* sinatra"
end
task :symlink do
run "mkdir -p #{release_path}/vendor/"
run "ln -nfs #{shared_path}/system/rack #{release_path}/vendor/rack"
run "ln -nfs #{shared_path}/system/sinatra #{release_path}/vendor/sinatra"
end
end
Now I am a pretty happy camper as my move back to shared hosting seems to be going quite well so far. Thanks Passenger!
PS. If you’re interested in trying Dreamhost you should know that they have a pretty generous affiliate program. You can use my affiliate link or a friends, but either way you should make sure someone benefits from it when you signup.
Enter Sinatra
Sinatra, I absolutely love it. After watching toolmantim present on the topic at the last RORO I borrowed some inspiration (and some code) and came up with the new hughevans.net.
require 'rubygems'
require 'sinatra'
require 'haml'
require 'time'
require 'lib/article'
Article.path = File.join(Sinatra::Application.root, 'articles')
helpers do
def article_body(article)
haml(article.template, :layout => false)
end
def article_path(article)
"/#{article.published.strftime("%Y/%m/%d")}/#{article.id}"
end
end
get '/' do
@articles = Article.all.sort[0..4]
haml :home
end
get '/:year/:month/:day/:id' do
@article = Article[params[:id]] || raise(Sinatra::NotFound)
@single_view = true
haml :article
end
get '/articles.atom' do
@articles = Article.all.sort
content_type 'application/atom+xml'
haml :feed, :layout => false
end
get '/:style.css' do
content_type 'text/css', :charset => 'utf-8'
sass :"stylesheets/#{params[:style]}"
end
Nothing too clever, just simple clean ruby code. The full source is available on GitHub.
Rails Date Range String Conversions
It has been widely publicised that you should use string conversions to display dates and times in rails, but there is another object type that can make use of these handy conversions and that is date ranges. I honestly didn’t realise Ruby had date ranges until recently and I have to say they are pretty cool. Anyway onto my first date range conversion:
ActiveSupport::CoreExtensions::Range::Conversions::RANGE_FORMATS.merge!(
:long => Proc.new { |start, stop| "#{start.to_date.to_s(:dmy_long)} to #{stop.to_date.to_s(:dmy_long)}" }
)
Using a Proc you can pick up on the first and last date from the range and concatenate them into the string format of your choosing. I’ve used a date string conversion there aswell:
ActiveSupport::CoreExtensions::Date::Conversions::DATE_FORMATS.merge!(
:dmy_long => Proc.new { |date| "#{date.day.ordinalize} #{date.strftime '%B %Y'}" }
)
With these conversions loaded you can do the following:
>> date_range = 20.days.from_now..30.days.from_now
=> Wed, 15 Oct 2008 11:09:03 UTC +00:00..Sat, 25 Oct 2008 11:09:03 UTC +00:00
>> date_range.to_s(:long)
=> "15th October 2008 to 25th October 2008"
Pretty nice but if you want to make it a little dry-er in terms of the output like ‘15-25th October 2008’ you could do something like this:
ActiveSupport::CoreExtensions::Range::Conversions::RANGE_FORMATS.merge!(
:condensed => Proc.new do |start, stop|
if (start.year == stop.year) and (start.month == stop.month)
"#{start.day}-#{stop.to_date.to_s(:dmy_long)}"
else
"#{start.to_date.to_s(:dmy_long)} to #{stop.to_date.to_s(:dmy_long)}"
end
end
)
So when the date range is limited to the one month it will give you the condensed version:
>> date_range = 20.days.from_now..30.days.from_now
=> Wed, 15 Oct 2008 11:35:13 UTC +00:00..Sat, 25 Oct 2008 11:35:13 UTC +00:00
>> date_range.to_s(:condensed)
=> "15-25th October 2008"
And if the range spans more than one month it jumps back to the full version:
>> date_range = 20.days.from_now..50.days.from_now
=> Wed, 15 Oct 2008 11:35:22 UTC +00:00..Fri, 14 Nov 2008 11:35:22 UTC +00:00
>> date_range.to_s(:condensed)
=> "15th October 2008 to 14th November 2008"
Of course don’t forget the more logic you put in these conversions the greater the need for some tests.