Rails.cache: Memcached, development mode and offline cache invalidation

Posted by jeff

Rails.cache rocks, but it can be tricky to set it up for development mode. For my purposes I need to:

  • Keep config.cache_classes to false so that I don’t have to restart my server while I develop
  • Cache all kinds of objects, not just strings
  • Be able to invalidate the cache easily from cron scripts or other offline processes
  • Test caching locally before deploying

The first thing I did was check out the excellent railscast and I read through the blog posts mentioned there. However, I couldn’t quite figure out how to get things to work – I kept getting strange errors where all of the methods were being stripped from my classes, rails was complaining that my classes didn’t exist or I was getting dreadful “singleton can’t be dumped” errors. After a lot of googling and experimentation, here is what finally worked for me:

Environment files

I like to develop quickly, test caching on my local and then deploy. To accomplish this I have 3 environments, setup like so:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  # config/environments/development.rb
  config.action_controller.perform_caching = false
  config.cache_classes = false
  config.cache_store = :mem_cache_store, '127.0.0.1:11211', {:namespace => "dev"}

  # config/environments/dev_with_caching.rb
  config.action_controller.perform_caching  = true
  config.cache_classes = true
  config.cache_store = :mem_cache_store, '127.0.0.1:11211', {:namespace => "dev_with_caching"}

  # config/environments/production.rb
  config.action_controller.perform_caching  = true
  config.cache_classes = true
  config.cache_store = :mem_cache_store, '127.0.0.1:11211', {:namespace => "production"}

Here are a few interesting points:

You don’t need to have memcached installed to develop locally

If you run your app locally without memcached installed, or without memcached running, you will see entries like this in your log

MemCacheError (No connection to server): No connection to server
Cache miss: Post.all ({:force=>false})

However, your app will work just fine. Rails will always execute the contents of the fetch blocks, and will return nil for any reads.

If memcached is running, you need to set cache_classes to true

To run memcached locally, you need to install memcached. I develop on a mac and manage packages with macports, so for me it was as easy as:

sudo port install memcached

Once memcached is installed, you can start it with

memcached -m 500 -l 127.0.0.1 -p 11211 -vv

which will print verbose logging to STDERR, or you can start it as a daemon like so:

memcached -m 500 -l 127.0.0.1 -p 11211 -d

Either of these will start a memcached process running on port 11211, and it will allocate 500MB RAM (most apps can get by with 128MB, or so I’ve heard).

Once this is running, though, you need to set config.cache_classes to true – otherwise you’re app will blow up.

Marshal.dump is finicky

Rails.cache calls Marshal.dump on any object you try to put in the cache. Marshal won’t work on everything though – and you may need to write your own serialization script. I’ve had problems with classes that have lots of module_eval statements that create methods dynamically and similar meta-programming techniques. If you start getting errors like “singleton can’t be dumped”, check to see if you have any meta-programming going on. I’ve also had issues with REXML objects.

If you do have an issue with a class that Rails won’t cache, you can easily bypass the built-in serialization by writing your own _dump and _load methods. See the ruby docs for more info.

Use a separate environment to test locally

I have a new environment named dev_with_caching that I use to test caching locally. I set up my database.yml file so that it points to the development database, but performs caching and in all other respects mirrors the production environment. To test locally with that environment, I use:

script/server -e dev_with_caching -p 3001

Clearing the cache

I mostly use Rails.cache to cache data – and mostly for arrays of objects – like Category.all. As such, it’s to keep all of this in the model, but cache invalidation can be trickly to manage. Here’s a pattern I’ve started to use a lot:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Category < ActiveRecord::Base
  
  after_save      :reset_cache
  after_destroy :reset_cache
  
  def reset_cache
    self.class.reset_cache
  end

  class << self
    
    def reset_cache
      cached_all(true)
    end
    
    def cached_all(force = false)
      Rails.cache.fetch("Category.all", :force => force) do
        Category.find(:all, :conditions => {:active=>true}, :order=>'position')
      end
    end
  end
end

Here’s what’s happening:

The first time you call Category.cached_all it looks for the “Category.all” item in the cache. If it’s not there, it executes the contents of the block, and adds it to the cache. When you save or destroy a record the cache is invalidated.

If you want to force a refresh of the cache, just specify Category.cached_all(true) and it will be reloaded from the database. Once this is in place, it’s easy to write cache invalidation scripts that both clear the cache and reload it at the same time.

I’ve done this by adding a class method that reloads the data, which is triggered by after_save and after_destroy callbacks. I’m sure there are a number of plugins that will do all that and more, but for my purposes this simple pattern works for me most of the time.

Clearing the cache with cron

Finally, if you want to clear the cache at specified intervals you can do so easily with rake and cron. First, create a rake task that calls the model’s reset_cache method – since I normally have several classes with caching behavior I normally create a loop like so:

1
2
3
4
5
6
7
8
9
10
namespace :cache do
  namespace :reset do
    %w{Category Forum Post}.each do |klass|
      desc "Clear the #{klass} cache"
      task klass.underscore.gsub("/","_").pluralize => :environment do
        klass.constantize.reset_cache
      end
    end
  end
end

Now you can run

rake cache:reset:categories
and your Category.reset_cache method will be called. To make this work with cron, you’ll need a slightly different syntax. The following command is suitable to execute from a cron script, or manually from the command line:

RAILS_ENV=production rake -f /var/www/apps/yourapp/current/Rakefile cache:reset:categories

It might take a little while to grok Rails.cache – but once you do your apps will be faster and you’ll quickly become a wild caching fiend!

Networking Mac and Windows with VMWare

Posted by jeff

I recently discovered how easy it is to view my local development websites on multiple OS’s using VMWare. I use this primarily to see how awful my apps look in IE. Here’s how you can do it too:

Step1: Get Setup (the expensive part)

  1. Buy a mac
  2. Buy VMWare Fusion
  3. Buy Windows. Yes – if you want to run IE6 and IE7 you’ll have to buy two licenses. Yes, it will take you several hours of frustration and several hours on the phone with MicroSoft to get your licenses installed with VMWare.
  4. Download a few real OS’s and add them as virtual machines

Step 2: Find your network address

When you installed VMWare, it configured all of the necessary IP addresses for you. To find out what they are, open Terminal and type:

ifconfig

You’ll see something like:

lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
    inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1 
    inet 127.0.0.1 netmask 0xff000000 
    inet6 ::1 prefixlen 128 
    inet6 fdd3:5091:e6df:4c3d:21b:63ff:feab:d72e prefixlen 128 
gif0: flags=8010<POINTOPOINT,MULTICAST> mtu 1280
stf0: flags=0<> mtu 1280
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    ether 00:1b:63:ab:d7:2e 
    media: autoselect status: inactive
    supported media: autoselect 10baseT/UTP <half-duplex> 10baseT/UTP <full-duplex> 10baseT/UTP <full-duplex,hw-loopback> 10baseT/UTP <full-duplex,flow-control> 100baseTX <half-duplex> 100baseTX <full-duplex> 100baseTX <full-duplex,hw-loopback> 100baseTX <full-duplex,flow-control> 1000baseT <full-duplex> 1000baseT <full-duplex,hw-loopback> 1000baseT <full-duplex,flow-control> none
en1: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    inet6 fe80::21c:b3ff:fe7c:916e%en1 prefixlen 64 scopeid 0x5 
    inet6 2002:4452:63ee::21c:b3ff:fe7c:916e prefixlen 64 autoconf 
    inet 10.0.1.199 netmask 0xffffff00 broadcast 10.0.1.255
    ether 00:1c:b3:7c:91:6e 
    media: autoselect status: active
    supported media: autoselect
fw0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 4078
    lladdr 00:1d:4f:ff:fe:73:a1:ba 
    media: autoselect <full-duplex> status: inactive
    supported media: autoselect <full-duplex>
vmnet8: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    inet 172.16.192.1 netmask 0xffffff00 broadcast 172.16.192.255
    ether 00:50:56:c0:00:08 
vmnet1: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    inet 172.16.43.1 netmask 0xffffff00 broadcast 172.16.43.255
    ether 00:50:56:c0:00:01 

Notice the last entry, vmnet1 – the inet address listed there is the address that all of your virtual machines can use to access your localhost. In my case, this is 172.16.43.1

Let’s say you have a local rails app running on http://localhost:3000/ – to access that app from anywhere (your mac or any or your virtual machines) just type http://172.16.43.1:3000/ in your browser.

Step 3

Just kidding ;->

References

Tutorial: Creating plugins in Rails

Posted by jeff

There’s a new effort underway to add tutorials to the Rails API documentation. Here’s the first draft of a tutorial I just wrote describing how to create plugins:

Creating Plugin Basics

Pretend for a moment that you are an avid bird watcher. Your favorite bird is the Yaffle, and you want to create a plugin that allows other developers to share in the Yaffle goodness.

In this tutorial you will learn how to create a plugin that includes:

Core Extensions – extending String:

  1. Anywhere “hello”.squawk # => “squawk! hello! squawk!”

An acts\_as\_yaffle method for Active Record models that adds a “squawk” method:

class Hickwall < ActiveRecord::Base
  acts_as_yaffle :yaffle_text_field => :last_sang_at
end
Hickwall.new.squawk("Hello World")

A view helper that will print out squawking info:

squawk_info_for(@hickwall)

A generator that creates a migration to add squawk columns to a model:

script/generate yaffle hickwall

A custom generator command:

class YaffleGenerator < Rails::Generator::NamedBase
  def manifest
      m.yaffle_definition
    end
  end
end

A custom route method:

ActionController::Routing::Routes.draw do |map|
  map.yaffles
end

In addition you’ll learn how to:

  • test your plugins
  • work with init.rb, how to store model, views, controllers, helpers and even other plugins in your plugins
  • create documentation for your plugin.

Create the basic app

In this tutorial we will create a basic rails application with 1 resource: bird. Start out by building the basic rails app:

The following instructions will work for sqlite3. For more detailed instructions on how to create a rails app for other databases see the API docs.

rails plugin_demo cd plugin_demo script/generate scaffold bird name:string rake db:migrate script/server

Then navigate to http://localhost:3000/birds. Make sure you have a functioning rails app before continuing.

Create the plugin

The built-in Rails plugin generator stubs out a new plugin. Pass the plugin name, either CamelCased or under_scored, as an argument. Pass—with-generator to add an example generator also.

This creates a plugin in vendor/plugins including an init.rb and README as well as standard lib, task, and test directories.

Examples:

./script/generate plugin BrowserFilters
./script/generate plugin BrowserFilters --with-generator

Later in the plugin we will create a generator, so go ahead and add the—with-generator option now:

script/generate plugin yaffle --with-generator

You should see the following output:

create  vendor/plugins/yaffle/lib
create  vendor/plugins/yaffle/tasks
create  vendor/plugins/yaffle/test
create  vendor/plugins/yaffle/README
create  vendor/plugins/yaffle/MIT-LICENSE
create  vendor/plugins/yaffle/Rakefile
create  vendor/plugins/yaffle/init.rb
create  vendor/plugins/yaffle/install.rb
create  vendor/plugins/yaffle/uninstall.rb
create  vendor/plugins/yaffle/lib/yaffle.rb
create  vendor/plugins/yaffle/tasks/yaffle_tasks.rake
create  vendor/plugins/yaffle/test/core_ext_test.rb
create  vendor/plugins/yaffle/generators
create  vendor/plugins/yaffle/generators/yaffle
create  vendor/plugins/yaffle/generators/yaffle/templates
create  vendor/plugins/yaffle/generators/yaffle/yaffle_generator.rb
create  vendor/plugins/yaffle/generators/yaffle/USAGE

For this plugin you won’t need the file vendor/plugins/yaffle/lib/yaffle.rb so you can delete that.

rm vendor/plugins/yaffle/lib/yaffle.rb

Editor’s note: many plugin authors prefer to keep this file, and add all of the require statements in it. That way, they only line in init.rb would be `require “yaffle”` If you are developing a plugin that has a lot of files in the lib directory, you may want to create a subdirectory like lib/yaffle and store your files in there. That way your init.rb file stays clean

Testing Setup

Testing plugins that use the entire Rails stack can be complex, and the generator doesn’t offer any help. In this tutorial you will learn how to test your plugin against multiple different adapters using ActiveRecord. This tutorial will not cover how to use fixtures in plugin tests.

To setup your plugin to allow for easy testing you’ll need to add 3 files:

  • A database.yml file with all of your connection strings
  • A schema.rb file with your table definitions
  • A test helper that sets up the database before your tests

For this plugin you’ll need 2 tables/models, Hickwalls and Wickwalls, so add the following files:

  1. File: vendor/plugins/yaffle/test/database.yml
sqlite:
  :adapter: sqlite
  :dbfile: yaffle_plugin.sqlite.db
sqlite3:
  :adapter: sqlite3
  :dbfile: yaffle_plugin.sqlite3.db
postgresql:
  :adapter: postgresql
  :username: postgres
  :password: postgres
  :database: yaffle_plugin_test
  :min_messages: ERROR
mysql:
  :adapter: mysql
  :host: localhost
  :username: rails
  :password:
  :database: yaffle_plugin_test
  1. File: vendor/plugins/yaffle/test/test_helper.rb
ActiveRecord::Schema.define(:version => 0) do
  create_table :hickwalls, :force => true do |t|
    t.string :name
    t.string :last_squawk
    t.datetime :last_squawked_at
  end
  create_table :wickwalls, :force => true do |t|
    t.string :name
    t.string :last_tweet
    t.datetime :last_tweeted_at
  end
end
  1. File: vendor/plugins/yaffle/test/test_helper.rb
ENV['RAILS_ENV'] = 'test'
ENV['RAILS_ROOT'] ||= File.dirname(FILE) + '/../../../..'
require 'test/unit'
require File.expand_path(File.join(ENV['RAILS_ROOT'], 'config/environment.rb'))
config = YAML::load(IO.read(File.dirname(FILE) + '/database.yml'))
ActiveRecord::Base.logger = Logger.new(File.dirname(FILE) + "/debug.log")
db_adapter = ENV['DB']
  1. no db passed, try one of these fine config-free DBs before bombing. db_adapter ||= begin require ‘rubygems’ require ‘sqlite’ ‘sqlite’ rescue MissingSourceFile begin require ‘sqlite3’ ‘sqlite3’ rescue MissingSourceFile end end
if db_adapter.nil?
  raise "No DB Adapter selected. Pass the DB= option to pick one, or install Sqlite or Sqlite3." 
end
ActiveRecord::Base.establish_connection(config[db_adapter])
load(File.dirname(FILE) + "/schema.rb")
require File.dirname(FILE) + '/../init.rb'
class Hickwall < ActiveRecord::Base
  acts_as_yaffle
end
class Wickwall < ActiveRecord::Base
  acts_as_yaffle :yaffle_text_field => :last_tweet, :yaffle_date_field => :last_tweeted_at
end

Update a core class: Adding “to_squawk” to String

To update a core class you will have to:

  • Write tests for the desired functionality
  • Create a file for the code you wish to use
  • Require that file from your init.rb

Most plugins store their code classes in the plugin’s lib directory. When you add a file to the lib directory, you must also require that file from init.rb. The file you are going to add for this tutorial is `lib/core_ext.rb`

First, you need to write the tests. Testing plugins is very similar to testing rails apps. The generated test file should look something like this:

  1. File: vendor/plugins/yaffle/test/core_ext_test.rb
require 'test/unit'
class CoreExtTest < Test::Unit::TestCase
  # Replace this with your real tests.
  def test_this_plugin
    flunk
  end
end

Start off by removing the default test, and adding a require statement for your test helper.

  1. File: vendor/plugins/yaffle/test/core_ext_test.rb
require 'test/unit'
require File.dirname(FILE) + '/test_helper.rb'
class CoreExtTest < Test::Unit::TestCase
end

Navigate to your plugin directory and run `rake test`

cd vendor/plugins/yaffle
rake test

Your test should fail with `no such file to load—./test/../lib/core_ext.rb (LoadError)` because we haven’t created any file yet. Create the file `lib/core_ext.rb` and re-run the tests. You should see a different error message:

1.) Failure ...
No tests were specified

Great – now you are ready to start development. The first thing we’ll do is to add a method to String called `to_squawk` which will prefix the string with the word “squawk! ”. The test will look something like this:

  1. File: vendor/plugins/yaffle/init.rb
class CoreExtTest < Test::Unit::TestCase
  def test_string_should_respond_to_squawk
    assert_equal true, "".respond_to?(:to_squawk)
  end
  def test_string_prepend_empty_strings_with_the_word_squawk
    assert_equal "squawk!", "".to_squawk
  end
  def test_string_prepend_non_empty_strings_with_the_word_squawk
    assert_equal "squawk! Hello World", "Hello World".to_squawk
  end
end
  1. File: vendor/plugins/yaffle/init.rb
require "core_ext"
  1. File: vendor/plugins/yaffle/lib/core_ext.rb
class String
  # returns the current string, prefixed by "squawk!" 
  def to_squawk
    "squawk! #{self}".strip
  end
end

To test that your method does what it says it does, run the unit tests. To make sure your code is picked up by

To test this, fire up a console and start squawking:

script/console
>> "Hello World".to_squawk
=> "squawk! Hello World"

If that worked, congratulations! You just created your first test-driven plugin that extends a core ruby class.

Adding an `acts_as_yaffle` method to ActiveRecord models

A common pattern in plugins is to add a method called `acts_as_something` to models. In this case, you want to write a method called `acts_as_yaffle` that adds a squawk method to your models.

To keep things clean, create a new test file called `acts_as_yaffle_test.rb` in your plugin’s test directory and require your test helper.

  1. File: vendor/plugins/yaffle/test/acts_as_yaffle_test.rb
require File.dirname(FILE) + '/test_helper.rb'
class Hickwall < ActiveRecord::Base
  acts_as_yaffle
end
class ActsAsYaffleTest < Test::Unit::TestCase
end
  1. File: vendor/plugins/lib/acts_as_yaffle.rb
module Yaffle
end

One of the most common plugin patterns for `acts_as_yaffle` plugins is to structure your file like so:

module Yaffle
  def self.included(base)
    base.send :extend, ClassMethods
  end
end
module ClassMethods
  # any method placed here will apply to classes, like Hickwall
  def acts_as_something
    send :include, InstanceMethods
  end
end
module InstanceMethods
  # any method placed here will apply to instaces, like @hickwall
end

With structure you can easily separate the methods that will be used for the class (like `Hickwall.some_method`) and the instance (like `@hickwell.some_method`).

Let’s add class method named `acts_as_yaffle` – testing it out first. You already defined the ActiveRecord models in your test helper, so if you run tests now they will fail.

Back in your `acts\_as\_yaffle` file, update ClassMethods like so:

module ClassMethods
  def acts_as_yaffle(options = {})
    send :include, InstanceMethods
  end
end

Now that test should pass. Since your plugin is going to work with field names, you need to allow people to define the field names, in case there is a naming conflict. You can write a few simple tests for this:

  1. File: vendor/plugins/yaffle/test/acts_as_yaffle_test.rb
require File.dirname(FILE) + '/test_helper.rb'
class ActsAsYaffleTest < Test::Unit::TestCase
  def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
    assert_equal "last_squawk", Hickwall.yaffle_text_field
  end
  def test_a_hickwalls_yaffle_date_field_should_be_last_squawked_at
    assert_equal "last_squawked_at", Hickwall.yaffle_date_field
  end
  def test_a_wickwalls_yaffle_text_field_should_be_last_tweet
    assert_equal "last_tweet", Wickwall.yaffle_text_field
  end
  def test_a_wickwalls_yaffle_date_field_should_be_last_tweeted_at
    assert_equal "last_tweeted_at", Wickwall.yaffle_date_field
  end
end

To make these tests pass, you could modify your `acts_as_yaffle` file like so:

  1. File: vendor/plugins/yaffle/lib/acts_as_yaffle.rb
module Yaffle
  def self.included(base)
    base.send :extend, ClassMethods
  end
end
module ClassMethods
  def acts_as_yaffle(options = {})
    cattr_accessor :yaffle_text_field, :yaffle_date_field
    self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s
    self.yaffle_date_field = (options[:yaffle_date_field] || :last_squawked_at).to_s
    send :include, InstanceMethods
  end
end
module InstanceMethods
end

Now you can add tests for the instance methods, and the instance method itself:

  1. File: vendor/plugins/yaffle/test/acts_as_yaffle_test.rb
require File.dirname(FILE) + '/test_helper.rb'
class ActsAsYaffleTest < Test::Unit::TestCase
end
def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
  assert_equal "last_squawk", Hickwall.yaffle_text_field
end
def test_a_hickwalls_yaffle_date_field_should_be_last_squawked_at
  assert_equal "last_squawked_at", Hickwall.yaffle_date_field
end
def test_a_wickwalls_yaffle_text_field_should_be_last_squawk
  assert_equal "last_tweet", Wickwall.yaffle_text_field
end
def test_a_wickwalls_yaffle_date_field_should_be_last_squawked_at
  assert_equal "last_tweeted_at", Wickwall.yaffle_date_field
end
def test_hickwalls_squawk_should_populate_last_squawk
  hickwall = Hickwall.new
  hickwall.squawk("Hello World")
  assert_equal "squawk! Hello World", hickwall.last_squawk
end
def test_hickwalls_squawk_should_populate_last_squawked_at
  hickwall = Hickwall.new
  hickwall.squawk("Hello World")
  assert_equal Date.today, hickwall.last_squawked_at
end
def test_wickwalls_squawk_should_populate_last_tweet
  wickwall = Wickwall.new
  wickwall.squawk("Hello World")
  assert_equal "squawk! Hello World", wickwall.last_tweet
end
def test_wickwalls_squawk_should_populate_last_tweeted_at
  wickwall = Wickwall.new
  wickwall.squawk("Hello World")
  assert_equal Date.today, wickwall.last_tweeted_at
end
  1. File: vendor/plugins/yaffle/lib/acts_as_yaffle.rb
module Yaffle
  def self.included(base)
    base.send :extend, ClassMethods
  end
end
module ClassMethods
  def acts_as_yaffle(options = {})
    cattr_accessor :yaffle_text_field, :yaffle_date_field
    self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s
    self.yaffle_date_field = (options[:yaffle_date_field] || :last_squawked_at).to_s
    send :include, InstanceMethods
  end
end
module InstanceMethods
  def squawk(string)
    write_attribute(self.class.yaffle_text_field, string.to_squawk)
    write_attribute(self.class.yaffle_date_field, Date.today)
  end
end

Note the use of write_attribute to write to the field in model.

Create a view helper

Creating a view helper is a 3-step process:

  • Add an appropriately named file to the lib directory
  • Require the file and hooks in init.rb
  • Write the tests

First, create the test to define the functionality you want:

  1. File: vendor/plugins/yaffle/test/view_helpers_test.rb
require File.dirname(FILE) + '/test_helper.rb'
include YaffleViewHelper
class ViewHelpersTest < Test::Unit::TestCase
  def test_squawk_info_for_should_return_the_text_and_date
    time = Time.now
    hickwall = Hickwall.new
    hickwall.last_squawk = "Hello World" 
    hickwall.last_squawked_at = time
    assert_equal "Hello World, #{time.to_s}", squawk_info_for(hickwall)
  end
end

Then add the following statements to init.rb:

  1. File: vendor/plugins/yaffle/init.rb
require "view_helpers" 
ActionView::Base.send :include, YaffleViewHelper

Then add the view helpers file and

  1. File: vendor/plugins/yaffle/lib/view_helpers.rb
module YaffleViewHelper
  def squawk_info_for(yaffle)
    returning "" do |result|
      result << yaffle.read_attribute(yaffle.class.yaffle_text_field)
      result << ", " 
      result << yaffle.read_attribute(yaffle.class.yaffle_date_field).to_s
    end
  end
end

You can also test this in script/console by using the “helper” method:

script/console
>> helper.squawk_info_for(@some_yaffle_instance)

Create a migration generator

When you created the plugin above, you specified the—with-generator option, so you already have the generator stubs in your plugin.

We’ll be relying on the built-in rails generate template for this tutorial. Going into the details of generators is beyond the scope of this tutorial.

Type:

script/generate

You should see the line:

Plugins (vendor/plugins): yaffle

When you run `script/generate yaffle` you should see the contents of your USAGE file. For this plugin, the USAGE file looks like this:

Description:
    Creates a migration that adds yaffle squawk fields to the given model
Example:
    ./script/generate yaffle hickwall
This will create:
    db/migrate/TIMESTAMP_add_yaffle_fields_to_hickwall

Now you can add code to your generator:

  1. File: vendor/plugins/yaffle/generators/yaffle/yaffle_generator.rb
class YaffleGenerator < Rails::Generator::NamedBase
  def manifest
    record do |m|
      m.migration_template 'migration:migration.rb', "db/migrate", {:assigns => yaffle_local_assigns, 
        :migration_file_name => "add_yaffle_fields_to_#{custom_file_name}" 
      }
    end
  end
end
private  
  def custom_file_name
    custom_name = class_name.underscore.downcase
    custom_name = custom_name.pluralize if ActiveRecord::Base.pluralize_table_names
  end
def yaffle_local_assigns
  returning(assigns = {}) do
    assigns[:migration_action] = "add" 
    assigns[:class_name] = "add_yaffle_fields_to_#{custom_file_name}" 
    assigns[:table_name] = custom_file_name
    assigns[:attributes] = [Rails::Generator::GeneratedAttribute.new("last_squawk", "string")]
    assigns[:attributes] << Rails::Generator::GeneratedAttribute.new("last_squawked_at", "datetime")
  end
end

Note that you need to be aware of whether or not table names are pluralized.

This does a few things:

  • Reuses the built in rails migration_template method
  • Reuses the built-in rails migration template

When you run the generator like

script/generate yaffle bird

You will see a new file:

  1. File: db/migrate/20080529225649_add_yaffle_fields_to_birds.rb
class AddYaffleFieldsToBirds < ActiveRecord::Migration
  def self.up
    add_column :birds, :last_squawk, :string
    add_column :birds, :last_squawked_at, :datetime
  end
end
def self.down
  remove_column :birds, :last_squawked_at
  remove_column :birds, :last_squawk
end

Adding custom generator commands

You may have noticed above that you can used one of the built-in rails migration commands `m.migration_template`. You can create your own commands for these, using the following steps:

1. Add the require and hook statements to init.rb 2. Create the commands – creating 3 sets, Create, Destroy, List 3. Add the method to your generator

Working with the internals of generators is beyond the scope of this tutorial, but here is a basic example:

  1. File: vendor/plugins/yaffle/init.rb
require "commands" 
Rails::Generator::Commands::Create.send   :include,  Yaffle::Generator::Commands::Create
Rails::Generator::Commands::Destroy.send  :include,  Yaffle::Generator::Commands::Destroy
Rails::Generator::Commands::List.send     :include,  Yaffle::Generator::Commands::List
  1. File: vendor/plugins/yaffle/lib/commands.rb
require 'rails_generator'
require 'rails_generator/commands'
module Yaffle #:nodoc:
  module Generator #:nodoc:
    module Commands #:nodoc:
      module Create
        def yaffle_definition
          file("definition.txt", "definition.txt")
        end
      end
end
module Destroy
      def yaffle_definition
        file("definition.txt", "definition.txt")
      end
    end
end
module List
    def yaffle_definition
      file("definition.txt", "definition.txt")
    end
  end
end
  1. File: vendor/plugins/yaffle/generators/yaffle/templates/definition.txt
Yaffle is a bird
  1. File: vendor/plugins/yaffle/generators/yaffle/yaffle_generator.rb
class YaffleGenerator < Rails::Generator::NamedBase
  def manifest
      m.yaffle_definition
    end
  end
end

This example just uses the built-in “file” method, but you could do anything that ruby allows.

Adding Routes

Testing routes in plugins can be complex, especially if the controllers are also in the plugin itself. Jamis Buck showed a great example of this in http://weblog.jamisbuck.org/2006/10/26/monkey-patching-rails-extending-routes-2

  1. File: vendor/plugins/yaffle/test/routing_test.rb
require "#{File.dirname(FILE)}/test_helper"
class RoutingTest < Test::Unit::TestCase
end
def setup
  ActionController::Routing::Routes.draw do |map|
    map.yaffles
  end
end
def test_yaffles_route
  assert_recognition :get, "/yaffles", :controller => "yaffles_controller", :action => "index" 
end
private
  1. yes, I know about assert_recognizes, but it has proven problematic to
  2. use in these tests, since it uses RouteSet#recognize (which actually
  3. tries to instantiate the controller) and because it uses an awkward
  4. parameter order. def assert_recognition(method, path, options) result = ActionController::Routing::Routes.recognize_path(path, :method => method) assert_equal options, result end
  1. File: vendor/plugins/yaffle/init.rb
require "routing" 
ActionController::Routing::RouteSet::Mapper.send :include, Yaffle::Routing::MapperExtensions
  1. File: vendor/plugins/yaffle/lib/routing.rb
module Yaffle #:nodoc:
  module Routing #:nodoc:
    module MapperExtensions
      def yaffles
        @set.add_route("/yaffles", {:controller => "yaffles_controller", :action => "index"})
      end
    end
  end
end
  1. File: config/routes.rb
ActionController::Routing::Routes.draw do |map|
  ...
  map.yaffles
end

You can also see if your routes work by running `rake routes` from your app directory.

Generate RDoc Documentation

Once your plugin is stable, the tests pass on all database and you are ready to deploy do everyone else a favor and document it! Luckily, writing documentation for your plugin is easy.

The first step is to update the README file with detailed information about how to use your plugin. A few key things to include are:

  • Your name
  • How to install
  • How to add the functionality to the app (several examples of common use cases)
  • Warning, gotchas or tips that might help save users time

Once your README is solid, go through and add rdoc comments to all of the methods that developers will use.

Before you generate your documentation, be sure to go through and add nodoc comments to those modules and methods that are not important to your users.

Once your comments are good to go, navigate to your plugin directory and run

rake rdoc

Working with init.rb

The plugin initializer script init.rb is invoked via `eval` (not require) so it has slightly different behavior.

If you reopen any classes in init.rb itself your changes will potentially be made to the wrong module. There are 2 ways around this:

The first way is to explicitly define the top-level module space for all modules and classes, like ::Hash

  1. File: vendor/plugins/yaffle/init.rb
class ::Hash
  def is_a_special_hash?
    true
  end
end

OR you can use module\_eval or class\_eval

  1. File: vendor/plugins/yaffle/init.rb
Hash.class_eval do
  def is_a_special_hash?
    true
  end
end

Storing models, views, helpers, and controllers in your plugins

You can easily store models, views, helpers and controllers in plugins. Just create a folder for each in the lib folder, add them to the load path and remove them from the load once path:

  1. File: vendor/plugins/yaffle/init.rb
%w{ models controllers helpers }.each do |dir|
  path = File.join(directory, 'lib', dir)
  $LOAD_PATH << path
  Dependencies.load_paths << path
  Dependencies.load_once_paths.delete(path)
end

Adding directories to the load path makes them appear just like files in the the main app directory – except that they are only loaded once, so you have to restart the web server to see the changes in the browser.

Adding directories to the load once paths allow those changes to picked up as soon as you save the file – without having to restart the web server.

Storing plugins in alternate locations

You can store plugins wherever you want – you just have to add those plugins to the plugins path in environment.rb

Since the plugin is only loaded after the plugin paths are defined, you can’t redefine this in your plugins – but it may be good to now.

You can even store plugins inside of other plugins for complete plugin madness!

config.plugin_paths << File.join(RAILS_ROOT,"vendor","plugins","yaffle","lib","plugins")

Plugin Loaders and Plugin Locators

If the built-in plugin behavior is inadequate, you can change almost every aspect of the location and loading process. You can write your own plugin locators and plugin loaders, but that’s beyond the scope of this tutorial.

Custom Plugin Generators

If you are an RSpec fan, you can install the rspec\_plugin\_generator, which will generate the spec folder and database for you.

References

Demo apps

Posted by jeff

I love demo apps. When coutenay came out with his rails demo app I learned a lot. I’ve since created a few feature-specific demo apps and I hope that others can learn something from them:

As I’ve built these, I’ve realized that it would be helpful if all of these demo apps had a similar structure, so I’m going to propose one here. If you have any thoughts, either fork one of those apps at the top and add them to your fork, or add comments here below. Here’s my first round of thoughts:

There should be a demo app start kit

This starter kit would have a readme with a README.rdoc page with a few sections outlined already, like “Installation”, “What this demonstrates” etc…

Another great feature for a starter kit would be a helper that reads source files and displays them, syntax highlighted, to the users so that while you are looking at a page in the app and you say “what does the controller look like for this page” you could just click “view the controller code” and have it appear.

Yet another feature that would be slick in a demo app would be a page that detects whether or not the database is installed, and if not, provides you with a simple database configuration form that allows you to specify the provider, username/password and optionally the timeout or the socket.

The idea for the starter kit would be to make it easy for developers to create a starter kit that “just works” by installing the app and running script/server.

Vendor everything

Rails makes this easy with the new gem dependencies, but I think all rails apps should have vendor/rails and all of the related gems vendored. I haven’t been consistent about this with my demo apps, but I realize how this would be important.

Pre-populate the database

Whether you use the dataset plugin, custom rake tasks or sqlite3 databases prepopulated, every demo app should have an initial dataset so people can test the full range of the app without having to first add data – especially important for things like the complex forms demo, where the app shows how multiple models relate to each other.

Demos should ideally have specs

I haven’t put meaninful specs into any of my demos, but as people download them and point out bugs I realize how helpful they would have been. I hope to add specs to these apps as time goes on and fix whatever bugs I find. If you do this, be sure to vendor your rspec/rspec-rails along with your vendor/rails.

Documentation should be redundant

Things that are in the README should also appear on the actual rendered pages, as well as in the source code. When developers first load up the app it should be obvious what’s happening.

Again – I haven’t done most of these in my demo apps, but I realize how important they would be, especially if more than 1 or 2 people are downloading the app. If someone is checking out your demo app, it probably means that don’t know how to do whatever you are demonstrating, and a long or frustrating demo (or a buggy one) can be worse than none at all.

Have you built a demo app? Any other suggestions or anything I’ve missed?

Tables aren't evil

Posted by jeff

Tables are for tabular data. What’s so hard about that?

I was just checking the election results on http://www.barackobama.com/resultscenter/index.php and out of habit I checked to see how it would look without stylesheets (using the fantastic web developer toolbar for FireFox).

At first everything looked great – the navigation was built with unordered lists and there were skip navigation links, the content was laid out with divs and the tables were…also laid out with divs. That’s right – some person or program generated hundreds of divs, all carefully placed to make the tabular data look like a table.

For those of you who missed the table vs. div saga it goes something like this: before css the only simple way to stucture web page contents was with html tables. Then css came along and fixed that, but browsers didn’t support the same css properties correctly (and some still don’t) and software makers like FrontPage and DreamWeaver made it very easy to create table-based layouts but not so easy to use divs.

Then the internet started becoming less of a luxury and more of a utility, and accessibility finally started getting the attention it deserved and a number of issues related to table-based layouts began to surface – it wreaked havoc on screen-readers, it affected the way content was loaded on pages, it didn’t degrade gracefully to text-only browsers like cell phones, it increased the page size because of all of the unnecessary cells used for complex nesting and it made the source HTML difficult to read and maintain.

Browsers have evolved to the point where all modern browsers render enough CSS so that problem is mostly solved. DreamWeaver now offers excellent support for CSS in design mode, which solves that problem. There are number of libraries and tutorials to help with common layout problems like nested menus and multi-column layouts so developers and designers don’t have spend too much time sweating the small stuff. Several huge and popular sites have moved to div-based layouts (with some notable exceptions) and the web is a better place for it.

But now it seems the pendulum is going a bit too far the other way – all of the same reasons to not use tables for non-tabular data are great reasons for using tables when appropriate. Namely, screen-readers have an easier time speaking the data, the columns stay columns even with no CSS and it makes the page size smaller by cutting out all of the unnecessary css and class/id tags necessary to make divs look like rows and cells.

Rails View Tips

Posted by jeff

Here is a list of quick tips that I find helpful using Ruby and Rails, inspired by the Railscasts Contest

1. Access view helpers from the console

Sometimes I need a quick reminder of the syntax of view helpers – like is it truncate(length, text) or truncate(text, length)? When this happens I just fire up a script/console and use the “helper” method. For example:

1
2
helper.truncate "Big sentence", 5
=> "Bi..."

2. Word Wrap

Speaking of view helpers, you probably know about pluralize, truncate and simple_format, but did you know that rails can word wrap for you? Frequently I find that I have a column of text that just doesn’t look good when some lines are a lot longer than others, and I need to have more fine-grained control over the output. Word wrap comes to the rescue. You can test this in script/console by doing the following:

1
2
3
text = "This is some very very long text that needs to be word wrapped"
helper.word_wrap text, 50
=> "This is some very very long text that needs to be\nword wrapped"

Notice that it inserts a newline, not a br element. If you want to convert that newline to an html br element, you can always use simple_format. What I particularly like about word_wrap is that it only breaks on whitespace characters, so you’ll never end up with a half-word.

3. Partial Counter

Let’s say you need to render a partial for a collection, and that you need to generate a sequential number that can’t be inferred from the database, like “Post #0, Post #1, Post #2” etc. where the numbers will always start with 0 and always be sequential. To start you might try something like this:

1
2
3
@posts.each_with_index do |post, index|
  render :partial => "post", :object => post, :locals => {:index => index}
end

Then from your partial, you would reference it like:

1
Post #<%= index %>

However, if you render a partial from a collection, Rails keeps track of the index for you auto-magically, in a local variable named partial_counter (in this case it would be post_counter). Refactored, it would look like this:

1
render :partial => @posts

And from the partial itself:

1
Post #<%= post_counter %>

Remember that partial_counter is zero-based. For regular partials, called with just an object, it will return 0.

4. Keep selects and model validations in sync with constants

The problem: you have a field in your model that validates_inclusion_of some array of values. You want to provide a dropdown list in your UI that matches these values, but provides a human readable description beyond what “humanize” can provide.

The solution: Constants. Create a constant in your model that has a hash with the names and the descriptions like so:

1
2
3
4
class Page < ActiveRecord::Base
  VISIBILITIES = {"Anyone can see it"=>"public", "Nobody can see it"=>"private"}
  validates_inclusion_of :visibility, :in => VISIBILITIES.values
end

Notice how you can validate the inclusion of the field just in the values. Then in the view just add a select field helper and pass it the constant:

1
select :page, :visibility, Page::VISIBILITIES

This keeps the code simple, readable and DRY.

5. Safely render environment-specific content

I like to use google analytics to keep track of my client’s site visitors. However, I don’t want to track my development machine, or the staging version of the site – just the production site. Rails makes this easy by making the RAILS_ENV environmental variable available. So anywhere in your rails app you can execute code conditionally – like this snippet from a view:

1
2
3
<%- if "production" == RAILS_ENV -%>
some google analytics javascript...
<%- end -%>

If you’re not careful, however, you can actually change the rails environment! Take this snippet of code, for example:

1
2
3
<%- if RAILS_ENV = "development" -%>
output some super-secret code trace...
<%- end -%>

Can you spot what’s wrong? Notice the ”=” instead of the ”==” – when the view execute this code it will actually change the rails_env to development, potentially wreaking all kinds of havoc on your app. In script/console this outputs a warning, and I haven’t tried it on production mode, but just to be safe always put the “production” part before the RAILS_ENV part – even if you forget an ”=” it will just throw an error.

Another great use of this is creating a banner to alert users to what environment they are looking at. Since staging sites often look exactly like production sites, I like to output a banner at the top of my page to say “hey – you’re on the staging site”. In my application.html.erb file I often have the following snippet:

1
2
3
4
5
<%- unless 'production' == RAILS_ENV -%>
  <div style="padding:.5em;font-weight:bold;text-align:center;background:orange;">
    YOU ARE CURRENTLY ON THE <%= RAILS_ENV.upcase %> SITE
  </div>
<%- end -%>

In edge rails this is a little easier with the Rails object – stay tuned for more on that later.

6. Locals with default values in partials

You can pass locals to partials, making partials very flexible. But sometimes you want to be able to optionally pass a local variable to a partial. You can accomplish this a variety of ways, but my favorite is just to use a simple ||= block at the top. For example if you have an optional “author” local you want to pass in, you can just write:

1
2
<%- author ||= nil -%>
Written by <%= author %>

The added benefit of this method is that the partial is self documenting, since you see all of the optional locals you can pass into it right at the top. You can optionally add comments for the required locals as well – depending on how busy you like your partials to be.

Starting ferret at reboot using Capistrano

Posted by jeff

I like using Ferret and acts_as_ferret to add full-text search to my models in Rails in a database-independent way. I’ve had a very difficult time getting this to work smoothly with my deployment process because the ferret server needs to be run in a separate process.

Every time the server reboots, you need to start the ferret server, and everytime you deploy your app you need to restart the ferret server. You can go to the acts_as_ferret site to get more information about deployment strategies, but here’s what’s been working for me for a while now.

First, you need to create a startup script that can be run on boot. I’ve taken mine from the railsmachine tutorial, but you can also use the one from the acts_as_ferret tutorial. Mine looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# located in config/templates/ferret_ctl.erb
#!/bin/bash
#
# This script starts and stops the ferret DRb server
# chkconfig: 2345 89 36
# description: Ferret search engine for ruby apps.
#
# save the current directory
CURDIR=`pwd`
PATH=/usr/local/bin:$PATH

RORPATH="<%= current_path %>"

case "$1" in
  start)
     cd $RORPATH
     echo "Starting ferret DRb server."
     RAILS_ENV=<%= rails_env %> script/ferret_start
     ;;
  stop)
     cd $RORPATH
     echo "Stopping ferret DRb server."
     RAILS_ENV=<%= rails_env %> script/ferret_stop
    ;;
  *)
     echo $"Usage: $0 {start, stop}"
     exit 1
     ;;
esac

cd $CURDIR

Next, add the following capistrano tasks to your deploy.rb file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# =============================================================================
# FERRET
# =============================================================================
set :ferret_script_name, "ferret_#{application}_ctl"
set :ferret_ctl, "/etc/init.d/#{ferret_script_name}"

namespace :ferret do
  desc "Uploads the ferret startup script"
  task :install, :roles => :app, :only => {:primary => true} do 
    require 'erb'
    upload_path = "#{shared_path}/ferret" 
    template = File.read("config/templates/ferret_ctl.erb")
    file = ERB.new(template).result(binding) 
    put file, upload_path, :mode => 0755
    sudo "cp #{upload_path} #{ferret_ctl}"
    sudo "chmod +x #{ferret_ctl}"
    sudo "/sbin/chkconfig #{ferret_script_name} on"
  end 

  desc "Starts the ferret server"
  task :start, :roles => :app, :only => {:primary => true} do
    sudo "#{ferret_ctl} start"
  end

  desc "Stops the ferret server"
  task :stop, :roles => :app, :only => {:primary => true} do
    sudo "#{ferret_ctl} stop"
  end

  desc "Restarts the ferret server"
  task :restart, :roles => :app, :only => {:primary => true} do
    ferret.stop
    ferret.start
  end
  
  desc "Deletes the ferret startup script"
  task :uninstall, :roles => :app, :only => {:primary => true} do 
    sudo "/sbin/chkconfig #{ferret_script_name} off"
    sudo "rm -rf #{ferret_ctl}"
  end 
  
end
after "deploy:symlink", "ferret:restart"

Now you are ready to deploy – just execute the following commands to get set up with your script:

cap ferret:install
cap deploy

The installation will:

  • Upload your startup script to /etc/init.d and set the correct permissions
  • When you deploy it will restart the drb servers before restarting mongrel

Thanks to the folks at Railsmachine for providing detailed instructions about chkconfig, and the acts_as_ferret folks for providing the drb server.

Adding patches to Rails now that it's on git

Posted by jeff

It took me a while to figure out how to try to contribute to Rails now that it’s on Lighthouse / Github. Here’s what I do now, and it seems to work:

First, create a clone of the main git repository (not your fork of it – there’s really no reason to fork unless you want others to pull your changes before core accepts them):

Setup your development directories

mkdir rails
cd rails
mkdir patches
git clone git://github.com/rails/rails.git source

Now you have:

rails
|--patches
`--source

Set up your dev branch

Then create a new branch where you’ll store just the changes you make for this patch:

git checkout -b your_patch_name master

Create the patch

Make your test-driven changes and when you are ready to create the patch run:

git diff -p master > ../patches/your_patch_name.patch

Setup a throwaway test branch

If you want to test your change

  • create a new branch and apply the patch (this is useful if other commits have happened since you first created your patch)
  • run the tests and make sure that everything passes
  • clear your changes and delete the test branch
git checkout master
git pull
git checkout -b your_patch_name_test master
git apply ../patches/your_patch_name.patch
git stash ...
git checkout master
git stash clear
git branch -D your_patch_name

Using git stash allows you to move back to the master branch without taking your changes with you – leaving you with a clean master branch. To learn more about git stash syntax, see the git documentation

Continue development

When it’s time to update the code you put in the patch, you can just rebase from the master branch:

git co your_patch_name
git rebase master

Then you can fix whatever changes you need to fix and recreate your patch (or create a new one if your old changes were accepted).

Share you patch

The next step is to go to http://rails.lighthouseapp.com/ and create a ticket. Make sure that you

  • tag it with “patch” as well as whatever else it applies to
  • don’t forget to upload the patch itself.

When you upload, it appear in the middle of the right-hand column as a blue link – it’s hard to find, so look carefully. Then get as many people as possible to grab your changes and test them and add +1’s where necessary.

Summary

When all is said and done you are left with a directory full of patches you can apply, a clean master working copy and individual branches for all of your patches that you can maintain over time. While this was possible with subversion, it’s way cleaner with git. Contributing to rails is easier than ever!

References

New Rails Core Feature Proposal: Super Sexy Migrations

Posted by jeff

If you are running Rails Edge from github, you can now get Super Sexy Migrations, like this:

1
2
3
4
5
6
    change_table :videos do |t|
      t.timestamps
      t.belongs_to :goat
      t.string :name, :email, :limit => 20
      t.remove :name, :email # => that's right - remove finally takes an array!
    end

Check out the railscast video for more details.

ActsAsStateMachine gets hippified

Posted by jeff

I was very pleased to learn earlier tonight that acts_as_state_machine has become infinitely hipper.

  • It’s on github, making patches oh so easy to push
  • It works with any class, not just active record
  • The “state” column is renamed to aasm_state so that you can have a city, state and zip in your active record model (man that was a pain before)
  • It’s a gem (in both senses of the word)
  • It’s tested with specs, and there’s decent coverage
  • The api is basically the same, making it easy to upgrade

It still has the same issues that the old subversion plugin had, so I incorporated my acts_as_state_machine_hacks plugin into my own fork of aasm on github, which you can check out at http://github.com/zilkey/aasm/wikis

I also published some rdocs for my fork at http://aasm.zilkey.com/

Thanks to Scott Barron for updating me on the latest changes, and for a killer gem.

To install my fork of the app:

1
2
3
4
5
git clone git://github.com/zilkey/aasm.git aasm
cd aasm
rake gem
sudo uninstall aasm # => if you already have a version installed
sudo gem install pkg/aasm-3.0.0.gem

Enjoy!

New Plugin: acts_as_state_machine_hacks

Posted by jeff

UPDATE: while this plugin will work for those of you who don’t want to upgrade, there is is a much hipper version of acts_as_state_machine called aasm (that link goes to my fork of it, with the changes listed below incorporated).

Inspired by the evil twin plugin architecture I just made a few small, test-driven tweaks to one of my favorite plugins: acts_as_state_machine

The biggest thing that bugged me about aasm was when you call a method like object.event! it writes to the database, instead of updating the current state, which makes it hard to set a state before validation.

Installation

script/plugin install http://elitists.textdriven.com/svn/plugins/acts_as_state_machine/trunk/
script/plugin install git://github.com/zilkey/acts_as_state_machine_hacks.git

What this plugin does

This plugin hack adds new methods without the exclamation point – it just updates the attribute. So if you have:

1
2
3
4
5
6
7
8
9
  class Conversation < ActiveRecord::Base
    acts_as_state_machine :initial => :open
    state :opened
    state :closed

    event :open do
      transitions :to => :opened,   :from => :closed
    end
  end

ActsAsStateMachine would give you:

1
  conversation.open!

That uses update_attribute under the hood. This plugin adds:

1
  conversation.open

Which just sets state to opened, without touching the database. Suitable for calling before you save, like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  def create
    @conversation = Conversation.new(params[:conversation])
    @conversation.open if params[:conversation][:publish] == "open"
    if @conversation.save
      ...
    else
      ...
    end
  end

  def update
    @conversation = Conversation.find(params[:id])
    @conversation.open if params[:conversation][:publish] == "open"
    if @conversation.update_attributes(params[:conversation]) # => if this doesn't validate, the state column will not have been written
      ...
    else
      ...
    end
  end

I wrote this primarily because I often change state based on fields in the model – like if you check a “published” check box that changes a state from pending to published.

This also allows the possibility of adding a dropdown list to an admin screen where admins can set the status, while maintaining the integrity by still relying on events.

Requirements

You must have the acts_as_state_machine plugin installed. To get the tests to run the acts_as_state_machine directory must be named “acts_as_state_machine”.

Warning

This copies methods directly from acts_as_state_machine. If acts_as_state_machine is updated, this plugin hack will likely break.

For developers

If you want to add your own hacks, you can fork this project on http://github.com/zilkey/acts_as_state_machine_hacks/tree/master

It includes working tests, which can be the hardest part of testing a plugin.

Lightrail just got a lot hipper

Posted by jeff

I just released a new version of Lightrail (http://lightrail.zilkey.com/) that is significantly hipper than the earlier version. This version comes with support for taking your existing models and turning them into lightrail’s structure.

Soon to come will be similar options for controllers, helpers, migrations, specs, views, plugins, static assets and whatever else I can shove into a few near-term releases.

If you are one of the people thinking about using lightrail please be aware that the api may change significantly, and the locations of files and the names of generators etc.. are all subject to change.

Make any comments here, or at the github repository (http://github.com/zilkey/multiple_migration_paths/tree/master). See the Lightrail Rdoc (http://lightrail.zilkey.com/) for a more detailed walk through.

New plugin: multiple_migration_paths

Posted by jeff

When Rails 2 came out it made it much easier to manage the view_paths and the plugin locations. Now edge rails has timestamped migrations, which opens up a whole new possibility for multiple migration paths with the same ease as view_paths.

I’ve written a plugin that does just that, and you can get it at http://github.com/zilkey/multiple_migration_paths/tree/master

Installation

Make sure you are edge rails, then:

script/plugin install git://github.com/zilkey/multiple_migration_paths.git

Summary

This allows you to store migrations in multiple directories, and they will all be run together, as if they were in the same directory. No changes have been made to any rails rake tasks, no additional tables are needed to migrate.

Take the following directory structure:

  db
  |-- migrate
  |   |-- 20080427101407_first.rb
  |   |-- 20080427101408_second.rb
  |   `-- 20080427101409_third.rb
  |-- schema.rb
  `-- your_plugin_migrations
      |-- 20080427101308_fourth.rb
      `-- 20080427101408_third.rb

You would add your custom directory by adding the following at the bottom or environment.rb or in an initializer or plugin:

1
2

  ActiveRecord::Migration.migration_paths << File.join(RAILS_ROOT,"db","your_plugin_migrations")

In this case, migrations would be run in the following order:

  • your/plugin_migrations/20080427101308_fourth.rb (it’s the first chronologically, even though it’s named “fourth”)
  • migrate/20080427101407_first.rb
  • migrate/20080427101408_second.rb
  • migrate/20080427101409_third.rb

What happens if you add a migration that comes chronologically before the last one? Rails automatically picks it up for you – so no worries!

Notice how it takes a full path – so your migrations don’t even need to be in your app (think migrations in gems)! Also, because you get to set the paths, you can have migrations from multiple gems/plugins all at the same time, and have any kind of directory structure – like:

  db
  |-- migrate
  |   |-- some_sub_folder
  |   |   `-- 20080427101409_third.rb
  |   `-- 20080427101308_fourth.rb

Important note on ordering

Rails will treat migrations in order (I believe) of the version – not in the order that they were applied to the database.

This means that if you add migration X, then add migration Y with a prior date, then migrate down and migrate up again, it will migrate down X then Y.

Also, if you add X, then a prior Y before you migrate, Y will migrate before X.

This is all standard rails behavior and would hold true for any migrations that were checked into version control at different times. I’m just reiterating it here because there is a slightly higher risk of conflicts with this plugin. Backup your database before running them.

Rake Tasks

Since all kinds of mayhem can break loose with migrations, you may want to see what migrations are in the database. You can do so with:

  rake db:migrations:files
  rake db:migrations:paths

If I have time, I can make this a bit more human readable by presenting the dates, but that’s for later…

Behind the scenes

This change was accomplished in roughly 5 lines of code, not counting whitespace and comments, but because of the way the methods were structured, I had to copy/paste an entire method from rails. As such, it’s not very future-proof, but I’ll submit it to rails core so hopefully this will be obsolete soon!

Extending Complex Forms: generate valid dom ids

Posted by jeff

A few weeks ago I was hired to build a simple event registration form. The requirements were that users needed to be able to:

  • Register multiple attendees for the same organization in one step
  • Specify a meal preference for each attendee, using radio buttons

Since I had recently bought Advanced Rails Recipes and watched the Ryan Bates’ excellent Railscast on complex forms I thought I was all set to go.

I followed Ryan’s instructions exactly and within a few minutes I was up and running with the javascript additions, skinny controllers and fat models and life was good. I fired up my browser, added a few attendee subforms and went to set their meal preference with the radio button when, to my dismay, clicking on any attendee’s meal preference set the meal preference for the first! Not good.

Whenever these things happen I run the page through the w3c html validator to make sure that I’ve got valid HTML. When I ran it through, it gave me several errors – I had lots of repeated ids. Why? If you follow the ARR/railscast example, all of the new subforms will have the same id. In reality, there are 2 things that need to happen:

  • when the page is loaded Rails needs to set an :index on all of the fields
  • when the form is added dynamically via javascript, the javascript needs to insert the correct id

Do your homework

ARR prohibits people from reproducing their code from their tutorials without permission (which I didn’t obtain). So this blog post won’t make much sense unless you read Advanced Rails Recipes and/or watch Railscast on complex forms – both of which I highly recommend. Once you have a working example of that, the rest of this post will make sense.

The setup

For this example, I’ll use the following classes:

1
2
3
4
5
6
7
class Registration < ActiveRecord::Base
  has_many :attendees
end

class Attendee < ActiveRecord::Base
  belongs_to :registration
end

The javascript

So let’s get started. First, we need to create a javascript method that will enable us to increment the index value every time – I decided to go with a generic one so that I could reuse it across my app, and have mutliple subfrms of different types on the same page but still be dry:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var Subform = Class.create({
  lineIndex: 1,
  parentElement: "",
  initialize: function(rawHTML, lineIndex, parentElement) {
    this.rawHTML        = rawHTML;
    this.lineIndex      = lineIndex;
    this.parentElement  = parentElement;
  },
  parsedHTML: function() {
    return this.rawHTML.replace(/INDEX/g, this.lineIndex++);
  },
  add: function() {
    new Insertion.Bottom($(this.parentElement), this.parsedHTML());
  }
});

Next, we need to set the index when the page loads. I accomplished that like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# /app/views/layouts/application.html.erb

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title><%= yield :title %></title>
    <%= javascript_include_tag :defaults, :cache => 'defaults' %>
    <%= yield :head %>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

# /app/views/registrations/_form.html.erb

<%- content_for :head do -%>
  <script type="text/javascript" charset="utf-8">
    //<![CDATA[
    attendeeForm = new Subform('<%= escape_javascript(render(:partial => "attendee", :object => Attendee.new)) %>',<%= @registration.attendees.length %>,'attendees');
    //]]>
  </script>
<%- end -%>

In Ryan’s original recipe he creates a rails helper to create the add link. Now that we’ve written the javscript ourselves we no longer need the helper – our “add” link now look like this:

1
<%= link_to_function 'Add Attendee', "attendeeForm.add()" %>

The partial

We need to add the index to all of the form helpers, which will require some work. In addition, we need to make sure that all of the radio buttons have unique ids and that all of the labels have ids that match up with the radio buttons. So here’s what my partial looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div class="attendee"><%- 
  index ||= "INDEX" 
  new_or_existing = attendee.new_record? ? 'new' : 'existing'
  id_or_index = attendee.new_record? ? index : attendee.id 
  prefix = "registration[#{new_or_existing}_attendee_attributes][]" 
-%> 

  <% fields_for prefix, attendee do |attendee_form| -%>     
  <p><%= attendee_form.label :email, nil, :index => id_or_index %> <%= attendee_form.text_field :email, :index => id_or_index %> </p>

  <%- MealPreference.find(:all).each do |preference| -%>
    <p>
       <%- radio_id = "registration_#{new_or_existing}_attendee_attributes_#{id_or_index}_meal_preference_#{preference.name}" -%>
       <%= attendee_form.radio_button :meal_preference, preference.name, :id => radio_id, :index => id_or_index %>
       <%= content_tag :label, preference.name, :class => "radio", :for => radio_id %> 
     </p>
  <%- end -%>
  <%= content_tag :p, link_to_function("Remove this attendee", "if(confirm('Are you sure?')){$(this).up('.attendee').remove()}") %> 
  <% end -%>
</div> 

What just happened? I added an index to every field. For existing records, this index will correspond to the attendee.id. For new records, it will be the string “INDEX”. If you recall from the javascript above the parsedHTML function replaced the word INDEX with the correct numeric index.

NOTE: getting correct radio_button ids requires my label_with_index plugin if you are running gem rails, but it looks like radio buttons create correct ids in edge.

The form

This means, however, that you need to pass an index into the partial for existing records. In my form, I’ve got the following:

1
2
3
4
5
6
7
8
9
  <h3>Attendees</h3>
  <div id="attendees"> 
    <%- @registration.attendees.each_with_index do |attendee, index| -%>
      <%= render :partial => "attendee", :object => attendee, :locals => {:index => index} %>
    <%- end -%>
  </div> 
  <p id="add-attendee"> 
  <%= link_to_function 'Add Attendee', "attendeeForm.add()" %>
  </p> 

Note the use of each_with_index so that we can pass the correct index in. “But wait!” you say, “What if the attendee is an existing record? Won’t that mess this up?” Fear not – in the partial we first check whether it’s a new record, and only use the index if it’s a new record.

The model

Making the changes in the model is trivial. The existing_attributes stay exactly the same – but we have to make one small change to the new attributes:

Old:

1
2
3
4
5
6
7
8
# app/models/registration.rb

# add all new attendees
def new_attendee_attributes=(attendee_attributes) 
  attendee_attributes.each do |attributes| 
    attendees.build(attributes) 
  end 
end 

Changes to:

1
2
3
4
5
6
7
8
# app/models/registration.rb

# add all new attendees
def new_attendee_attributes=(attendee_attributes) 
  attendee_attributes.each do |index, attributes| 
    attendees.build(attributes) 
  end 
end 

And voila! Correct dom ids and reusable javascript.

Credits

While I wrote the javascript in that example I reconstructed it from another javascript snippet I had seen (maybe on a railscast, or in the source code for some other rails app like Basecamp or Blinksale). If that looks like your javascript, please contact me at jeff at zilkey . com – I can’t find the original source right now. Other credits include:

References

Introducing label_with_index: a simple rails plugin

Posted by jeff

The problem

A while back Rails introduced the label helper, so you can can easily add labels to forms like so:

1
2
<%= label :user, :name %>
# => <label for="user_2_name">Name</label>

Unfortunately the person who committed that change neglected to add support for the auto index and :index options. For example, take the following code snippet:

1
2
<%= text_field :user, :name, :index => 1%>
# => <input id="user_1_name" name="user[1][name]" size="30" type="text" />

Following the principle of least surprise, you might assume that labels would work the same way, but they don’t:

1
2