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!

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
<%= label :user, :name, nil, :index => 1%>
# => <label for="user_name" index="1">Name</label>

The fix

I’ve just released a plugin called label_with_index that fixes this. Now label helpers take advantage of the autoindex feature and :index keys as you would expect:

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

Fixing radio button ids

Rails has also had a bug for quite some time where it will by default emit radio button ids that will fail XHTML validation. To boot, they even advertise this in the docs. Here’s what’s written on http://api.rubyonrails.com/classes/ActionView/Helpers/FormHelper.html :

1
2
3
4
5
# Let's say that @post.category returns "rails":
radio_button("post", "category", "rails")
radio_button("post", "category", "java")
# => <input type="radio" id="post_category" name="post[category]" value="rails" checked="checked" />
#    <input type="radio" id="post_category" name="post[category]" value="java" />

See how those two radio buttons have the same id? Not only will that fail validation, but it can cause unexpected javascript behavior and will cause very odd label-clicking results (clicking the label will most likely highlight the first radio button, regardless of which label is clicked).

label_with_index also fixes this in two ways:

  • Makes the id unique (or at least, as unique as the option is)
  • Adds a :value option to the label helper that can match the radio button’s value

Before:

1
2
3
4
radio_button("post", "category", "java")
label("post", "category")
 # => <input type="radio" id="post_category" name="post[category]" value="java" />
 # => <label for="post_category">Category</label>

After:

1
2
3
4
radio_button("post", "category", "java")
label("post", "category", nil, :value => "java")
 # => <input type="radio" id="post_category_java" name="post[category]" value="java" />
 # => <label for="post_category_java">Category</label>

So with this plugin the rails label helper, and the radio_button helper have support for creating valid ids out of the box.

Installation

git clone git://github.com/zilkey/label_with_index.git vendor/plugins/label_with_index

See the rdoc at http://label_with_index.zilkey.com/

To Do

  • Create a fork of rails on github and add this to core with tests

References

Notes:

It appears that the radio button id may have been fixed on edge rails – I haven’t tested it out fully and I’m not sure if all of the svn patches are synced up on github, so I’ll wait until things settle a bit and make changes as necessary.

Introducing Lightrail: Productize your rails apps with generators

Posted by jeff

NOTE: this is an almost complete rewrite of the earlier article, written for v0.5.0.

Lightrail is a ruby on rails plugin generator intended to make it easier to share controllers, models, helpers, views and migrations across multiple apps without hacking Rails internals.

Basically, lightrail is a scaffold generator that generates the code into a plugin, rather than the main app directory.

Installation

Currently this is a gem, but you have to go through some hoops to install it:

1. Install the gems

  sudo gem install hoe newgem
  git clone git://github.com/zilkey/lightrail.git lightrail
  cd lightrail
  rake local_deploy

2. Generate your plugin generator

Once the plugin is installed, you can create a plugin which will store your reusable code.


script/generate lightrail_plugin plugin_name namespace

3. Install your reusable code with your plugin generator

Once you‘ve generated your plugin, you‘ll see that there is a new generator available. If you ran script/generate lightrail_plugin zilkey/product your generator would be named zilkey_product


script/generate zilkey_product

When you run your own generator, it:

  • Copies the controller, model, helper and views to the appropriate app directory
  • Copies all of the migrations in the plugins lib/db/migrate directory to the app‘s db/migrate directory

Example

1
2
3
4
5
6
7
8
9
rails lightrail-demo
cd lightrail-demo
script/generate lightrail_plugin zilkey cms
script/generate model page name:string
rake db:migrate
script/generate lightrail_pluginize_model page #=> creates a base class and an inherited class and sets the table name correctly
script/generate zilkey cms # => moves the generated files back to the app directory
# make some changes to the generated base class then run
script/generate lightrail_clone # => moves all of the changes you made in the _app_ back to the _plugin_

How it works

When you create a lightrail plugin like zilkey cms, the plugin has a directory structure 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
  |-- MIT-LICENSE
  |-- README
  |-- Rakefile
  |-- generators
  |   `-- your_plugin
  |       |-- USAGE
  |       `-- your_plugin_generator.rb
  |-- init.rb
  |-- install.rb
  |-- lib
  |   |-- create
  |   |   |-- app
  |   |   |   |-- controllers
  |   |   |   |   `-- your_namespace
  |   |   |   |-- helpers
  |   |   |   |   `-- your_namespace
  |   |   |   |-- models
  |   |   |   |   `-- your_namespace
  |   |   |   `-- views_your_namespace
  |   |   |-- config
  |   |   |   `-- initializers
  |   |   |-- db
  |   |   |   |-- migrate
  |   |   |   |   `-- migrate_your_namespace
  |   |   |-- lib
  |   |   |-- public
  |   |   `-- spec
  |   |       |-- controllers
  |   |       |-- fixtures
  |   |       |-- helpers
  |   |       |-- models
  |   |       `-- views
  |   `-- destroy
  |       |-- app
  |       |   |-- controllers
  |       |   |   `-- your_namespace
  |       |   |-- helpers
  |       |   |   `-- your_namespace
  |       |   |-- models
  |       |   |   `-- your_namespace
  |       |   `-- views_your_namespace
  |       |-- config
  |       |   `-- initializers
  |       |-- db
  |       |   |-- migrate
  |       |   |   `-- migrate_your_namespace
  |       |-- lib
  |       |-- public
  |       `-- spec
  |           |-- controllers
  |           |-- fixtures
  |           |-- helpers
  |           |-- models
  |           `-- views
  |-- tasks
  |   `-- your_plugin_tasks.rake
  `-- uninstall.rb

When you run script/generate on your plugin, you‘ll notice that your main RAILS_ROOT directory looks like this:

1
2
3
4
5
6
7
8
  |-- app
  |-- controllers
  |    `-- your_namespace
  |-- helpers
  |    `-- your_namespace
  |-- models
  |    `-- your_namespace
  `-- views_your_namespace

Notice that it‘s placed a copy of each of your controller, model, helpers and views and migrations in the main app directory.

So for every controller, helper, model, view, migration and route in your plugin there is a corresponding file in the usual location in your main app. In addition, the migration fits logically into your migration scheme (that is, it‘s renamed to have the correct numbering scheme). The files in your app directory inherit from, or include, the files in the plugin, and the base classes are generated into the app directory for easy editing and cloning.

You can create modules for your controllers and helpers, so that the files look like this:

app/controllers/articles_controller.rb

1
2
3
class ArticlesController < ApplicationController
  include Zilkey::ArticlesController
end

app/helpers/articles_helper.rb

1
2
3
module ArticlesHelper
  include Zilkey::ArticlesHelper
end

app/models/article.rb

1
2
class Article < Zilkey::Article
end

This means that you can override specific methods in the controller, helpers and model and those changes extend or replace the plugin‘s functionality. Changes to the base classes/modules can be shared across all apps. Customizations to each individual app remain untouched. Upgrading

Let‘s say you‘ve added a new field to Article and you‘ve updated one of the views. When you reinstall your plugin, and regenerate the code Rails will ask if you want to overwrite the existing files the same way it normally would.

New models, views, controllers, migrations and routes are added (actually, all files in the plugin’s create directory are added – so you can even add dependent plugins to your generator!)

Let‘s say you have an Article model that you want to share across 2 apps. One app has a separate Publisher model that has_many :articles, the other app does not. When you add a field to Article, you want to be able to seamlessly update it across both installations, but you don‘t want to have to rewrite your Publisher customizations each time. With Lightrail, it‘s easy – here‘s how it works:

  • Create an Article plugin
  • Install it on app 1
  • Install it on app 2
  • Add the publisher model to app 2 and modify the files in your app folder with the changes

When Rails updates

Rails is constantly coming out with new versions and upgrades. Other plugins and systems like appable_plugins and engines rely on changing rails internals. Lightrail changes no rails internals and instead adds a few new methods to the generator commands. Other than that it is non-obtrusive.

As an example, rails just updated it‘s migration names to be named-based as opposed to numeric. This plugin, without making any changes, works the same on Rails 2.0.2 and edge (rev 9166)

Guides for making code more reusable

  • Cut your views into smaller partials – this way you can update some parts of the page without updating others
  • Never change migrations – always add new migrations with the changes

Ease of development

When developing plugins, it‘s helpful to be able to make changes to the base classes. As such, all models, controllers, helpers and views are removed from the load_once paths so as you make changes to the plugin, you don‘t have to restart your web server to see changes (in development mode).

Introducing webmaster_tools: a new rails plugin

Posted by jeff

If you manage websites and track them with Google, Yahoo! and MSN Live you know what a chore it can be to manage the webmaster verification process and building sitemaps. This should all be much easier now with my webmaster_tools plugin

To install it with git, run

1
git clone git://github.com/zilkey/webmaster_tools.git vendor/plugins/webmaster_tools

You can also check out the readme and rdocs at http://webmaster_tools.zilkey.com/

References

NOTE: this is a complete replacement of webmaster_verification

Webmaster Verification Plugin

Posted by jeff

[UPDATE: this is now the webmaster_tools]

As a contractor I often help clients with basic Search Engine-related tasks such as setting up Google Analytics and webmaster accounts with Google Webmaster Tools, Yahoo! Site Explorer and MSN Live Webmaster Tools.

Three great tastes that don’t taste great together

Each of the Big 3 search engines require webmasters to verify control of their websites and each has their own way to verify ownership. All three offer the option to add a meta-tag to the site, which works well for small teams but can quickly become unwieldy when there are a dozen people that would like access.

In addition, each search engine offers the option to upload a file to the server with a unique name and/or content. Yahoo! requires you to add specific content, Google does not require any content in the file and MSN Live requires a specially formatted XML document with the id’s of all of the authenticated users. With either method, the search engines re-authenticate your site ownership periodically, so you must keep the meta-tags or uploaded files on the server.

While Google allows you to see who else has authenticated (and the key that they use, so you could remove it from the site if need be), Yahoo! and MSN Live do not. So in addition to having multiple files with long, ugly filenames lying around, I also had to keep track of which files pertained to which users.

Keepin’ it clean

After getting that not-so-fresh-feeling from the ways I had handled it in the past, I recently developed the webmaster_verification plugin. This plugin let’s you keep track of each of your authenticated webmasters for all 3 search engines in a single YAML file that looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
  google:
    someone@example.com: google121212.html
    edna: google131415.html
    betty: google3435365.html
  yahoo.com:
    eugene:
      filename: y_key_185746.html
      content: 098374
    edna:
      filename: y_key_185746.html
      content: 323454
  live:
    edna: 6565656564734839

Now, whenever a new user wants to see the webmaster tools they just create an account and email you their passcode along with the email address they used to access the webmaster account. You add the appropriate entries to the YAML file (which doesn’t require the server to be restarted) and the new user can successfully authenticate using the file-upload method.

If one of them stops working on the project, you can easily delete them from verification from all three sites by removing their entries from the YAML config, and the next time the webmaster tools sites re-authenticate those users will be un-verified.

Installation

Via Git:

1
git clone git://github.com/zilkey/webmaster_tools.git vendor/plugins/webmaster_tools

To install, simply run

1
script/generate webmaster_verification

The generator creates a file named config/webmaster_verification.yml and adds a map.webmaster_verification to config/routes.rb. The controller routes will be created auto-magically, thanks to Jamis Buck’s excellent post on routing internals

Can you hear me Tommy?

Google, Yahoo!, MSN Live – if you are listening, here’s what would make your webmaster tools services much more useful:

  • Once a person has verified that they control the site, give them a simple “Add User” option that pre-verifies the new user and sends an email with instructions on how to create an account
  • Allow users to be admins or simply viewers
  • Agree on a standard format for verification – say, a single xml file that looks like:
1
2
3
4
5
<webmasters>
  <webmaster service="Google">1bt57xyt45s</webmaster>
  <webmaster service="Yahoo!">EEbt57xE6gt%</webmaster>
  <webmaster service="Live">AIbt57xHDHD77es</webmaster>
</webmasters>
  • Allow any admin user view all other users and un-verify any user

You agreed on the sitemaps protocol – how about making it a little easier on the admin side?