Friday, January 30, 2009

Nicer URLs in Rails

By default in Rails, a resource has a URL that indicates what kind of resource it is, and what its id number is. So you might have a website with information about bands, and the URL www.music.com/bands/123 might refer to the Rolling Stones.

For lots of reasons it'd be preferable to be able to have www.music.com/bands/the-rolling-stones instead. Here's a straightforward way to do this - but note, as things stand it will break any incoming links you might have.

The basic idea is that rather than retrieving a record based on its id, we retrieve records based on another field that corresponds to the desired URL. We'll call this field munged_name.

So first we need to create the following migration

class AddMungedNameToBands < ActiveRecord::Migration
def self.up
add_column 'bands', 'munged_name', :string
end

def self.down
remove_column 'bands', 'munged_name'
end
end

Then, to the Band model, add

def to_param
name.gsub(/\s/, "-").gsub(/[^\w&-]/,'').downcase
end

With Rails, every object has a to_param method. By default, ActiveRecord objects return the object's id. Here, we override that with a method that returns the band's name, with any spaces replaced with dashes (the first regexp), any punctuation stripped out (the second regexp), and converted to lower case. This is apparently known as munging. So now, applying the to_param method to a Band object returns the munged form of the band's name

Also to the Band model, add

def calculate_munged_name
self.munged_name = to_param
end

This sets the munged_name field and will be called after record has been created.

To the create and update methods of the bands controller, add

@band.calculate_munged_name

just after @band has been set

Given that we already have records in our database we need to give them munged names, so at the console do

>> Band.find(:all).each do |b|
b.calculate_munged_name
b.save
end

Finally, in the show, edit, update, and destroy methods of the bands controller, change

@band = Band.find(params[:id]
to
@band = Band.find_by_munged_name(params[:id])


Tuesday, September 30, 2008

Setting up ssh keys

At the command line on your local computer
1. ssh-keygen -t rsa
2. Enter a passphrase
3. scp ~/.ssh/id_rsa.pub user@remotehost.com:~/.ssh/id_rsa_temp.pub
(will need to enter your password for user@remotehost here)
Then
4. ssh user@remotehost
5. chmod ~/.ssh 700
6. cat ~/.ssh/id_rsa_temp.pub >> ~/.ssh/authorized-keys
7. rm ~/.ssh/id_rsa_temp.pub
8. chmod 600 ~/.ssh/authorized_keys (not strictly necessary)
9. log out of your shell
10. log back in
11. ssh user@remotehost.com (should now be prompted for key passphrase)

To set it up so you can log in with no passphrase:
1. ssh-agent bash
2. ssh-add (then enter key passphrase)
3. ssh user@remotehost.com

Ferret on shared hosting

I'm using ferret for a full text search on an app deployed on site5. These are the steps I took to get it set up:

1. Install the ferret gem

2. Tell the rails app where to find the gem:
# config/environment.rb
# Be sure to restart your server when you modify this file

# Uncomment below to force Rails into production mode when
# you don't control web/app server and can't set it the proper way
ENV['RAILS_ENV'] ||= 'production'

if ENV['RAILS_ENV'] == 'production'
ENV['GEM_PATH'] = '/home/forfolks/gems:/usr/lib/ruby/gems/1.8'
end


It seems that it is necessary to do this in environment.rb and not in config/environments/production.rb.

3. On shared hosting you can't run daemons so the DRb server approach for updating the ferret index won't work. I followed instructions here to set up a rake tast to update the index instead. I also commented out the production part in config/ferret_server.yml.

  # app/models/product.rb
def before_save
# disable automatic ferret indexing...move it to a cron job
self.disable_ferret(:always)
end


# ferret_index.rake
desc "Updates the ferret index for the application."
task :ferret_index => [ :environment ] do | t |
Product.rebuild_index
# here I could add other model index rebuilds
puts "Completed Ferret Index Rebuild"
end


I then call this in my code via:
    system "rake ferret_index &"

as detailed in this railscast.

Tuesday, September 16, 2008

Rails ActionMailer on Site5 2

So, it turned out that it wasn't as straight forward as I thought. I dumped the config stuff after the 'Rails::Initializer.run do |config| ' part in environment.rb and tried to send an email. The server timed out and gave a 'Rails Application failed to start' error, which meant that the fcgi process was being killed (I looked in the site's error log in Backstage to figure this out.)

Couldn't work out what was going on, because it had worked in the console. Eventually I went back to the console. I tried
ActionMailer::Base.smtp_settings

to see what settings it was reading. It dumped a load of default-looking settings to the command line. Somewhere my configuration settings were being over-ridden.

Eventually I discovered that this was happening in config/initializers/mail.rb. It seems that the initializers are run after the main initializer stuff in environment.rb. I put my config stuff in there instead and it worked :) .

Wednesday, September 10, 2008

Rails ActionMailer on Site5

ActionMailer is the rails class that allows you to send emails from within a rails app. To get it working I did the following:

1. Visit the 'Backstage' admin area and create an email account for 'email_user' with password 'email_password'. At this point it should tell you the SMTP server is 'mail.mydomain.com'.

At this stage I decided that I would try to get it working in the console first. So I went to my application root and typed 'script/console production' and then

2. Set the configuration

ActionMailer::Base.delivery_method = :smtp
ActionMailer::Base.smtp_settings = {
:address => "localhost",
:port => 25,
:authentication => :login,
:user_name => 'email_user+mydomain.com',
:password => 'email_password'
}


3. Created a mailer

class Page < ActionMailer::Base
def about
recipients 'tom.closeman@gmail.com'
subject "Wey hey - it works - again"
from "email_user@forfolkssake.com"
end
end


This relied on the (blank) template 'App/Views/Page/about.html.erb' existing in my application folder.

4. Sent the message
 Page.deliver_about 


This then sent me the message. Which I thought was pretty cool.

Copying production data to development environment

I wanted to copy all the data from the production environment on the server to my development environment on my laptop. The following worked:

1. In the Backstage area of site5 choose SiteAdmin > Web Site Tools > Download an SQL database backup and then choose the development database.

This produces an SQL dump of the database and all the data therein.

2. Download and save this file (say we save it as 'production_dump.sql' in 'some_path')

3. Run the following


mysql -h localhost -u root -p password myapp_development < some_path/production_dump.sql


(Syntax is "mysql -hHOSTNAME -uUSER -pPASSWORD DATABASE < DUMPED_TEXTFILE")

4. Download any files that may have been uploaded to the server using facilities such as attachment_fu.