header image

Intercepting activerecord attributes

Source: http://smartic.us/2007/3/15/intercepting-activerecord-attributes

So, you wrote a Rails application to track Marathon events. In a sudden change of heart, you pick up your company and move it from the United States to Germany. Why rewrite your app to handle metric distances? Rails provides a solution: write_attribute and read_attribute. For more information, you can refer to the official documentation.

 1 class Marathon < ActiveRecord::Base
 2   def distance=(meters)
 3     write_attribute(:distance, meters * 1609.344)
 4   end
 5 
 6   def distance
 7     read_attribute(:distance) / 0.000621371192
 8   end
 9 end

Language: Ruby / Tags: rails, activerecord / Posted by: spicycode / On: March 18th, 2007

Production data loading with Capistrano

Source: http://push.cx/2007/capistrano-task-to-load-production-data

I was working on a migration that had a decent chance of messing up my database and wanted assurance it would work with production data, not just my fixtures. Building on Bojan Mihelac's code to download files, I wrote a Capistrano task to load the production database into my local database. And then capistrano 1.4.0 was released with a built-in get method for downloading files. So if you have 1.4.0, you can just throw away that whole function.

 1 # Get file remote_path from FIRST server targetted by
 2 # the current task and transfer it to local machine as path, SFTP required
 3 # http://source.mihelac.org/articles/2007/01/11/capistrano-get-method-download-files-from-server
 4 def actor.get(remote_path, path, options = {})
 5   execute_on_servers(options) do |servers|
 6     self.sessions[servers.first].sftp.connect do |tsftp|
 7       logger.info "Get #{remote_path} to #{path}"
 8       tsftp.get_file remote_path, path
 9     end
10   end
11 end
 1 desc "Load production data into development database"
 2 task :load_production_data, :roles => :db, :only => { :primary => true } do
 3   require 'yaml'
 4  
 5   database = YAML::load_file('config/database.yml')
 6  
 7   filename = "dump.#{Time.now.strftime '%Y-%m-%d_%H:%M:%S'}.sql"
 8   on_rollback { delete "/tmp/#{filename}" }
 9  
10   run "mysqldump -u #{database['production']['username']} –password=#{database['production']['password']} #{database['production']['database']} > /tmp/#{filename}" do |channel, stream, data|
11     puts data
12   end
13   get "/tmp/#{filename}", filename
14   exec "/tmp/#{filename}"
15   exec "mysql -u #{database['development']['username']} –password=#{database['development']['password']} #{database['development']['database']} < #{filename}; rm -f #{filename}"
16 end

After adding the code to the tail of my config/deploy.rb, I ran cap load_production_data. Then I had a full copy of the production database to tinker with until I was content my migration was flawless. Production data is also useful for other things. I can run complicated stats queries without worrying about bogging down the site, and see layout bugs that don't happen unless there's a few dozen tags on the page

Language: Ruby / Tags: capistrano, rails, activerecord / Posted by: spicycode / On: March 18th, 2007

Managing Memcache with Capistrano

Source: http://blog.caboo.se/articles/2007/2/18/bust-a-cap-in-dat-ass

Capistrano rocks for deployment. If you haven’t ever used it to deploy an app, you’re seriously missing out. There’s a lot of good recipes floating around online and this stuff changes so often that there’s probably something out there that’s better. However I figured I’d share how I’m currently managing our memcache daemons at work. memcached, rails wtf?

We’re using defunkt’s cache_fu plugin right now to cache models in our system. It’s pretty straightforward to use; an acts_as class method, and a config/memcached.yml file. The memcached.yml file lets you provide one or more servers that the memcache clients can connect to. Unfortunately there’s no centralized way to manage those memcache daemons on your deploy hosts. With our good friend cap, some ruby, and a little time invested we can roll our own solution. Whatcha Want?

The requirements for this task are dead simple.

use cap to get out to the remote hosts
the memcache daemon list comes from cache_fu’s yml file
write a script to start, stop, restart, get the status of, and commit mass genocide against(I want all processes kill -9’d right now).

It turned out to be a pretty simple hack; parse the yaml, get the host’s ip address, see if the host is in the server list, if it is do whatever command line operation we were given. Check it out.

 1 #!/usr/bin/env ruby
 2 # this goes in your script/ directory
 3 # it parses your memcached.yml file and hooks you up w/ some info
 4 # it keeps you from having to mess w/ stale memcached daemons for whatever reason.
 5 require 'yaml'
 6 
 7 class MemcachedCtl
 8   attr_accessor :memcached, :memory, :pids, :servers, :ip_address, :ethernet_device
 9   def initialize
10     env = ENV['RAILS_ENV'] || 'development'
11     self.memcached = `which memcached`.chomp
12     self.servers = [ ]
13     self.pids    = { }
14     self.ethernet_device = ENV['ETH'] || 'eth0'
15     self.ip_address = get_ip_address || '0.0.0.0'
16     self.memory = '128'
17     
18     config = YAML.load_file(File.expand_path(File.dirname(__FILE__) + "/../config/memcached.yml"))
19     self.servers = [ config['defaults']['servers'] ].flatten rescue ['127.0.0.1:11211']
20     self.servers = [ config[env]['servers'] ].flatten if config[env]['servers']
21     self.servers.reject! { |server| host,port = server.split(/:/); self.ip_address == host }
22     self.memory = config[env]['memory'] unless config[env]['memory'].nil?
23     
24     each_server do |host,port|
25       `ps auwwx | grep memcached | grep '\\-l #{ip_address} \\-p #{port}' | grep -v grep`.split(/\n/).each do |line|
26         self.pids[port] = line.split(/\s+/)[1]
27       end
28       self.pids[port] ||= 'Down'
29     end
30   end
31   
32   def execute(cmd)
33     send(cmd)
34   end
35   
36   def restart; stop; sleep 1; start; end
37   
38   def status
39     each_server { |host,port| puts "Port #{port} -> #{pids[port] =~ /\d+/ ? 'Up' : 'Down'}" }
40   end
41   
42   def kill
43     each_server { |host,port| `kill -9 #{pids[port]} > /dev/null 2>&1` if pids[port] =~ /\d+/ }
44   end
45   
46   def stop; kill; end
47   
48   def start
49     each_server do |host,port|
50       `#{memcached} -d -m #{memory} -l #{ip_address} -p #{port}`
51       STDERR.puts "Try memcached_ctl status" unless $? == 0
52     end
53   end
54     
55   protected
56   def each_server(&block)
57     servers.each do |server|
58       host,port = server.split(/:/)
59       yield host, port
60     end
61   end
62   
63   def get_ip_address # this works on linux you might have to tweak this on other oses
64     line = `/sbin/ifconfig #{ethernet_device} | grep inet | grep -v inet6`.chomp
65     if line =~ /\s*inet addr:((\d+\.){3}\d+)\s+.*/
66       self.ip_address = $1
67     end
68   end
69 end
70 ###########################################################################
71 cmd = ARGV.shift
72 unless cmd.nil?
73   MemcachedCtl.new.execute(cmd)
74 end

Get Your Cap On

So I named the script memcached_ctl and added it to my script/ directory in svn and redeployed. Now I can start poppin caps in our memcache daemons. If I throw the following code at the bottom of my config/deploy.rb I can see what’s up with my memcache daemons on my production hosts. If I were smart/motivated I’d make a gem and let you good people include these tasks, you know like you should be doing for mongrel_cluster.

 1 # ====================================
 2 # Memcached Server TASKS
 3 # ====================================
 4 %w(start stop restart kill status).each do |cmd|
 5   desc "#{cmd} your memcached servers"
 6   task "memcached_#{cmd}".to_sym, :roles => :app do
 7     run "RAILS_ENV=production #{ruby} #{current_path}/script/memcached_ctl #{cmd}"
 8   end
 9 end

Language: Ruby / Tags: rails, capistrano, memcache, caching / Posted by: spicycode / On: March 18th, 2007

Automatically tagging releases with capistrano

Source: http://blog.teksol.info/articles/2006/10/20/automatically-tagging-your-releases-using-capistrano Even though Capistrano “tags” each release by creating a new folder on the production server(s), it might be interesting to have a historical perspective in your repository anyway. This makes it easier to know exactly what went up for a release. I would like to share the following Capistrano recipe for your pleasure: config/deploy.rb

1 require 'uri'
2 task :after_deploy do
3   source = repository
4   dest = URI.parse(repository).merge("../releases/#{File.basename(release_path)}")
5   cmd = "svn copy --revision=#{revision} --quiet --message \"Auto tagging release #{release_path}\" #{source} #{dest}"
6   puts cmd
7   `#{cmd}`
8 end

First, we start by requiring uri, because Subversion does not like relative URLs. Next, we find the location into which to tag the release, and finally, we just do it. Simple, effective. Enjoy !

Language: Ruby / Tags: rails, subversion, capistrano / Posted by: spicycode / On: March 18th, 2007

Backup your production database with capistrano

Source: http://blog.caboo.se/articles/2006/12/28/a-better-capistrano-backup Say you have a remote server and you want to back up your remote DB to your home machine but you're behind a firewall. Here's my quick solution/hack of the default capistrano backup task. Remotely, it will dump your database to /tmp and bzip2 it. Locally, it will scp the file to your RAILS_ROOT/backups. You need to have all your ssh keys set up for this to work. It probably will fail if you're using a gateway.

 1 role :db,  "caboo.se", :primary => true
 2 set :user, "bananas"
 3 
 4 desc "Backup the database"
 5 task :backup, :roles => :db, :only => { :primary => true } do
 6   filename = "/tmp/#{application}.dump.#{Time.now.to_f}.sql.bz2"
 7 
 8   on_rollback { delete filename }
 9   run "mysqldump -u root -p mephisto_production | bzip2 -c > #{filename}" do |ch, stream, out|
10     ch.send_data "assword\n" if out =~ /^Enter password:/
11     # set this to your db password.. yuk!
12   end
13   `rsync #{user}@#{roles[:db][0].host}:#{filename} #{File.dirname(__FILE__)}/../backups/`
14   delete filename
15 end

Invoke with rake remote:exec ACTION=backup Suggested improvements: - is it necessary to shell out to scp? (update: now uses rsync) - is there a better way of doing the db password? - any way to use the 'backup gem', a favorite of rails professionals everywhere?
Language: Ruby / Tags: rails, capistrano, backup / Posted by: spicycode / On: March 18th, 2007

Manage Gems within Capistrano

 1 require 'capistrano'
 2 # Installs within Capistrano as the plugin _gem_.
 3 # Prefix all calls to the library with <tt>gem.</tt>
 4 # Manages installing gems and versioned gems.
 5 module GemInstaller
 6 
 7   # Default install command
 8   #
 9   # * doesn't install documentation
10   # * installs all required dependencies automatically.
11   #
12   GEM_INSTALL="gem install -y --no-rdoc"
13 
14   # Upgrade the *gem* system to the latest version. Runs via *sudo*
15   def update_system
16     sudo "gem update --system"
17   end
18 
19   # Updates all the installed gems to the latest version. Runs via *sudo*.
20   # Don't use this command if any of the gems require a version selection.
21   def upgrade
22     sudo "gem update --no-rdoc"
23   end
24 
25   # Removes old versions of gems from installation area.
26   def cleanup
27     sudo "gem cleanup" 
28   end
29 
30   # Installs the gems detailed in +packages+, selecting version +version+ if
31   # specified.
32   #
33   # +packages+ can be a single string or an array of strings.
34   #  
35   def install(packages, version=nil)
36     sudo "#{GEM_INSTALL} #{if version then '-v '+version.to_s end} #{packages.to_a.join(' ')}"
37   end
38 
39   # Auto selects a gem from a list and installs it.
40   #
41   # *gem* has no mechanism on the command line of disambiguating builds for
42   # different platforms, and instead asks the user. This method has the necessary
43   # conversation to select the +version+ relevant to +platform+ (or the one nearest
44   # the top of the list if you don't specify +version+).
45   def select(package, version=nil, platform='ruby')
46     selections={}
47     cmd="#{GEM_INSTALL} #{if version then '-v '+version.to_s end} #{package}"
48     sudo cmd do |channel, stream, data|
49       data.each_line do | line |
50   case line
51   when /\s(\d+).*\(#{platform}\)/
52     if selections[channel[:host]].nil?
53       selections[channel[:host]]=$1.dup+"\n"
54       logger.info "Selecting #$&", "#{stream} :: #{channel[:host]}"
55     end
56   when /\s\d+\./
57     # Discard other selections from data stream
58   when /^>/
59     channel.send_data selections[channel[:host]]
60     logger.debug line, "#{stream} :: #{channel[:host]}"
61   else
62     logger.info line, "#{stream} :: #{channel[:host]}"
63   end
64       end
65     end
66   end
67 
68 end
1 Capistrano.plugin :gem, GemInstaller

Language: Ruby / Tags: capistrano, rubygems, rails / Posted by: spicycode / On: March 18th, 2007

Renaming your .rhtml to .erb on EdgeRails

 1 ## just slap this in your rake file or in lib/tasks
 2 
 3 namespace 'views' do
 4   desc 'Renames all your rhtml views to erb'
 5   task 'rename' do
 6     Dir.glob('app/views/**/*.rhtml').each do |file|
 7       puts `svn mv #{file} #{file.gsub(/\.rhtml$/, '.erb')}`
 8     end
 9   end
10 end
11 
12 ## or if you have rhtml and rxml you could probably use the following (untested)
13 
14 namespace 'views' do
15   desc 'Renames all your rhtml views to erb'
16   task 'rename' do
17     Dir.glob('app/views/**/*.rhtml').each do |file|
18       puts `svn mv #{file} #{file.gsub(/\.(rhtml|rxml)$/, '.erb')}`
19     end
20   end
21 end

Language: Ruby / Tags: rake, views, rails / Posted by: spicycode / On: March 18th, 2007

Spec Helpers for Login Testing

Source: http://pastie.caboo.se/29577
Found about the internet...

 1 module UserSpecHelpers
 2   module ClassMethods
 3     def require_login_and_correct_user(action, &request)
 4       require_login action, &request
 5       require_user action, &request
 6     end
 7 
 8     def require_login(action, &request)
 9       specify "when calling #{action} should redirect to the login screen if the User is not logged in" do
10         controller.should_redirect_to :controller => "sessions", :action => "new"
11         instance_eval &request
12       end
13     end
14 
15     def require_user(action, &request)
16       specify "when calling #{action} should render a 403 if the logged in User isn't the User requested" do
17         mock_user = mock("user")
18         login_as mock_user, "2"
19         controller.should_render :status => 403, :nothing => true
20         User.stub!(:find).and_return(mock("user2"))
21         instance_eval &request
22       end
23     end
24     
25     def should_find_user_on(http_verb)
26       specify "should find the relevant User" do
27         User.should_receive(:find).with("1").and_return mock_user
28         send("do_#{http_verb}")
29       end
30     end
31   end
32   
33   def self.included(receiver)
34     receiver.extend ClassMethods
35   end
36   
37   def login_as(user, id = "1")
38     user.stub!(:id).and_return(id)
39     user.stub!(:to_param).and_return(id)
40     controller.send :current_user=, user
41   end
42 end

Language: Ruby / Tags: rspec, rails, testing / Posted by: spicycode / On: March 18th, 2007

File Upload Helpers

Source: http://cleanair.highgroove.com/articles/2006/10/03/mini-file-uploads Handy little helpers to check if a file was provided to a controller, and to save it.

 1 def file_provided?
 2   [StringIO, Tempfile].include?(@file.class) and @file.size.nonzero?
 3 end
 4 
 5 
 6 def save_file
 7   if @file.is_a? Tempfile
 8     FileUtils.cp(@file.path, path)
 9   elsif @file.is_a? StringIO
10     File.open(path, "wb") { |disk_file| disk_file << @file.read }
11   end
12 end

Language: Ruby / Tags: rails, upload, files / Posted by: spicycode / On: March 18th, 2007

class attr with default

1 class Class # :nodoc:
 2   def cattr_reader_with_default(sym, default_value = nil)
 3       class_eval(<<-EOS, __FILE__, __LINE__)
 4         unless defined? @@#{sym}
 5           @@#{sym} = default_value
 6         end
 7 
 8         def self.#{sym}
 9           @@#{sym}
10         end
11 
12         def #{sym}
13           @@#{sym}
14         end
15       EOS
16   end
17 end

which gives you:

1 cattr_reader_with_default :default_usa_tax_rate, 0.65

Language: Ruby / Tags: metaprogramming / Posted by: spicycode / On: March 16th, 2007