Preventing model explosion via Rails serialization
A great thing about ActiveRecord is you can easily add a new model to your application and play around with it as you progress. However, this power can easily be overused leading to unnecessary overhead in your code.
Consider the case where you have preferences for each user. For example, a user may opt to show or hide his email address, adjust his timezone, or language. One solution is to simply add new columns to the users table that correspond to each preference type. For example, you can have a ‘show_email’, ‘timezone’, ‘locale’ columns in the ‘users’ table, which can make your table become wide as you add more preferences options. Another option is to use a separate ‘preferences’ table.
class User < ActiveRecord::Base
has_many :preferences
end
class Preferences < ActiveRecord::Base
belongs_to :user
# name - preference name
# value - preference value
end
Note there is no user interface to add or remove ‘preferences’, i.e. the kinds of preferences are fixed. Of course, in the future you may add a new kind of preference but this kind of work is better done outside of the user interface. Since that is the case, there is no need to represent ‘preferences’ as a separate model.
One better alternative is to use Rails serialization to store the different kinds and user-specific values. The code would look like this:
class User < ActiveRecord::Base
serialize :preferences, Hash
end
u = User.new
u.preferences = {:show_email => true, :locale => :en }
u.save
# somewhere in your view using haml
- if @user.preferences[:show_email]
= @user.email
Using ‘serialize’ results in less code, fewer tables, and less overall complexity. However, with serialization you lose the ability to efficiently search the preferences data. The million-dollar question is do you need to query these preferences? Do you need a finder that returns all users who wants to show their email?
One issue I had with ‘serialize’ is that by using it, I expose the implementation details. In the display example above, it is obvious I had it stored as a Hash. I would rather hide this detail and present the preferences attributes as user attributes instead. I also want default values for every user.
For example:
u = User.new # automatically assigns the default preferences
u.preferences
=> {:show_email => false, :locale => :en}
u.show_email = true # I can change it like an attribute via @user.update_attributes(params[:user])
u.preferences
=> {:show_email => true, :locale => :en}
I have created a module to support this. It is not a unique problem so others may have probably released a gem or plugin to do this. (I actually never bothered to search for one.) Nevertheless, it was a good exercise in metaprogramming.
To use my implementation, simply call ‘serializeable’ with the column you want to serialize and the default values.
class User < ActiveRecord::Base
serializeable :preferences, :show_email => true, :locale => :en
end
Below is the implementation of ‘serializeable’. The convention is to save it under your ‘lib’ folder and include it in your ‘config/application.rb’ if you are using Rails 3.
module AttributeSerializer
module ActiveRecordExtensions
module ClassMethods
def serializeable(serialized, serialized_accessors={})
serialize serialized, serialized_accessors.class
serialized_attr_accessor serialized, serialized_accessors
default_serialized_attr serialized, serialized_accessors
end
# Creates the accessors
def serialized_attr_accessor(serialized, accessors)
accessors.keys.each do |k|
define_method("#{k}") do
self[serialized] && self[serialized][k]
end
define_method("#{k}=") do |value|
self[serialized][k] = value
end
end
end
# Sets the default value of the serialized field
def default_serialized_attr(serialized, accessors)
method_name = "set_default_#{serialized}"
after_initialize method_name
define_method(method_name) do
self[serialized] = accessors if self[serialized].nil?
end
end
end
end
end
class ActiveRecord::Base
extend AttributeSerializer::ActiveRecordExtensions::ClassMethods
end
ActiveRecord is both easy and powerful. It can also lead to misuse and abuse. Even though you are adding just one model, remember that it is not just the model class itself. You are also adding the database migrations, unit tests, factories, finders, and validations that go along with the model. Next time you have a new requirement, see if serialization can do a better job.
Update: Adam Cuppy converted this code into a Rails plugin while Jay added dynamic finder methods. I also moved this into a gem I called ‘fancy_serializer‘.
Ruby 101: Improving your code by defining methods dynamically
Let’s say you have a user and you want to check its role.
class User
attr_accessor :role
end
u = User.new
u.role = 'admin'
# somewhere in your code you check the role
if u.role == 'admin'
puts 'admin'
elsif u.role == 'moderator'
puts 'moderator'
elsif u.role == 'guest'
puts 'guest'
end
Using a string value is bad code and you can improve this by using constants instead. But still, this is bad code becauses it exposes implementation details of your User class.
For our first improvement, we define methods that check the user’s role and hide the implementation of the role checking inside the User class.
class User
attr_accessor :role
def is_admin?
self.role == 'admin'
end
def is_moderator?
self.role == 'moderator'
end
def is_guest?
self.role == 'guest'
end
end
u = User.new
u.role = 'guest'
if u.is_admin?
puts 'admin'
elsif u.is_moderator?
puts 'moderator'
elsif u.is_guest?
puts 'guest'
end
Our first improvement is definitely better than the original but there are duplicate code in the role checking. You can eliminate the duplicate code by delegating the role checking to a single method.
class User
attr_accessor :role
def is_admin?
is_role? 'admin'
end
def is_moderator?
is_role? 'moderator'
end
def is_guest?
is_role? 'guest'
end
protected
def is_role?(name)
self.role == name
end
end
Our second improvement is a classic refactoring technique and common in any modern programming language. In other words, there is nothing “Ruby” about it. Before you get bored, I will now show the Ruby version.
The Ruby version uses ‘define_method()’ to further eliminate duplicate code.
class User
attr_accessor :role
def self.has_role(name)
define_method("is_#{name}?") do
self.role == "#{name}"
end
end
has_role :admin
has_role :moderator
has_role :guest
end
By using ‘define_method()’, we were able to add instance methods to our class User. You can check the new instance methods via irb.
ruby-1.9.2-p0 > User.instance_methods.grep /^is/ => [:is_admin?, :is_moderator?, :is_guest?, :is_a?]
Note that ‘has_role()’ is just another method and as such you can modify it to accept several parameters, an array, or other class. For example, we can make ‘has_role’ accept a list of roles.
class User
attr_accessor :role
def self.has_roles(*names)
names.each do |name|
define_method("is_#{name}?") do
self.role == "#{name}"
end
end
end
has_roles :admin, :moderator, :guest
end
How to use OpenAmplify with Ruby
The OpenAmplify API reads text you supply and returns linguistic data explaining and classifying the content. What you do with that analysis is, in the fine tradition of APIs and mashups, up to you. Some possibilities might include pairing ads with articles, creating rich tag-clouds, or monitoring the tone of forum threads.
I created a ruby gem to simplify the use of the OpenAmplify API. It’s still in the early stages but should be enough to get you started.
Output Format
In case you need a different format, OpenAmplify supports XML, JSON, RDF, CSV. It can also return the result as a fancy HTML page.
The source code is available in github: http://github.com/gregmoreno/openamplify
Using RabbitMQ and AMQP with Ruby
AMQP stands for Advanced Message Queuing Protocol. RabbitMQ is a server that implements the protocol. RabbitMQ is available on Linux, OSX, and Windows. Installation instructions for RabbitMQ are available at http://www.rabbitmq.com/install.html
I will be running RabbitMQ on Ubuntu 9.10. First, let’s install the server:
Next, we install the amqp gem by Aman Gupta. The gem can also be found at http://github.com/tmm1/amqp
Let’s now build a simple publisher code. Note the difference in the gem’s name and the file you need to use the gem.
All queues are created automatically the first time it is accessed. Make sure this is the same queue our consumers will use.
Next, the consumer code:
The ‘subscribe’ method registers with the queue telling it to call the block when a message has arrived. Alternatively, you can use ‘pop’ but this would constantly poll the server for new messages making unnecessary calls even if the queue is empty.
If you want to run AMQP on several machines, just specify the location of the broker in your publisher and consumer code:
You only need to run 1 rabbitmq server, which in my case is on Ubuntu. When you try the code in other machines, you only need the amqp gem installed.
I highly recommend Distributed Programming with Ruby by Mark Bates if you’re interested in distributed programming.
Deploy a Rails 3, Sqlite3 application in Tomcat using JRuby
and have a Ruby version running side-by-side.
A few months ago I got interested in JRuby while researching for text mining algorithms. I found some gems but they are either unmaintained or inadequate while the mature libraries I found were written in Java. No problem! JRuby to the rescue. Thank God.
Next stop, I decided to take Rails 3 and JRuby for a spin. Incidentally, I will be on a 3-city Rails tour in the Philippines this September and since there are many Filipino Java developers, they might find it interesting to see their favorite Java platform works nicely with Ruby on Rails.
Setup
I will be using the following for this tutorial:
java 1.6 + JDK
tomcat 7.0.2
rvm 1.0.1
jruby 1.5.0
ruby 1.9.2p0
Further below, I outline how to install these software. First, let’s see my current environment.
$ more /etc/issue Ubuntu 9.10 \n \l $ java -version java version "1.6.0_20" Java(TM) SE Runtime Environment (build 1.6.0_20-b02) Java HotSpot(TM) Server VM (build 16.3-b01, mixed mode) $ rvm -v rvm 1.0.1 by Wayne E. Seguin (wayneeseguin@gmail.com) [http://rvm.beginrescueend.com/] $ jruby -v jruby 1.5.0 (ruby 1.8.7 patchlevel 249) (2010-05-12 6769999) (Java HotSpot(TM) Client VM 1.6.0_20) [i386-java] $ TOMCAT/bin/version.sh Using CATALINA_BASE: /usr/local/apache-tomcat-7.0.2 Using CATALINA_HOME: /usr/local/apache-tomcat-7.0.2 Using CATALINA_TMPDIR: /usr/local/apache-tomcat-7.0.2/temp Using JRE_HOME: /usr Using CLASSPATH: /usr/local/apache-tomcat-7.0.2/bin/bootstrap.jar:/usr/local/apache-tomcat-7.0.2/bin/tomcat-juli.jar Server version: Apache Tomcat/7.0.2 Server built: Aug 4 2010 12:23:47 Server number: 7.0.2.0 OS Name: Linux OS Version: 2.6.31-22-generic Architecture: i386 JVM Version: 1.6.0_20-b02 JVM Vendor: Sun Microsystems Inc. $ ruby -v ruby 1.9.2p0 (2010-08-18 revision 29036) [i686-linux] # install jdk and tomcat $ aptitude install curl sun-java6-bin sun-java6-jre sun-java6-jdk $ wget http://apache.mobiles5.com/tomcat/tomcat-7/v7.0.2-beta/bin/apache-tomcat-7.0.2.tar.gz $ tar zxvf apache-tomcat-7.0.2.tar.gz $ mv apache-tomcat-7.0.2 /usr/local
Of course, these assume you want to use 7.0.2 and you want it installed at your /usr/local.
Install JRuby, Rails 3
I assume you already have rvm installed. If not, I highly recommend that you do. I can’t imagine a Ruby developer not using rvm
$ rvm install jruby $ rvm jruby $ rvm gemset create railsjam $ rvm jruby@railsjam $ gem install rails
Try a sample app
I’ve created sample app for the RailsJam tour. This have several functionalities already and better than creating a Rails app from scratch.
$ git clone git://github.com/gregmoreno/railsjam.git
Update the Gemfile
You need a separate set of gems to make your Rails 3 application work with JRuby. For learning purposes, I want my Rails 3 application to work other than JRuby. To accomplish that, we need to specify what gems are needed solely by JRuby.
source 'http://rubygems.org'
gem 'rails', '3.0.0'
if defined?(JRUBY_VERSION)
gem 'jdbc-sqlite3'
gem 'activerecord-jdbc-adapter'
gem 'activerecord-jdbcsqlite3-adapter'
gem 'jruby-openssl'
gem 'jruby-rack'
gem 'warbler'
else
gem 'sqlite3-ruby', :require => 'sqlite3'
end
(A copy of this Gemfile is available at the ‘jruby’ folder of the railsjam application.)
Now, it’s time to intall the gems. You must delete ‘Gemfile.lock’. Otherwise, bundle picks up wrong version of jdbc
$ rm Gemfile.lock $ jruby -S bundle install
Prepare the database.
The first time I worked on this tutorial, I needed to specify the jdbcsqlite3 as the database adapter. However, when I tried the tutorial on the same machine with a fresh gemset, it worked pretty well with just ‘sqlite3’. Just to be sure, I modified ‘database.yml’ to check for JRuby.
development:
adapter: <%= defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' %>
database: db/development.sqlite3
pool: 5
timeout: 5000
production:
adapter: <%= defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' %>
database: /home/greg/dev/railsjam/db/development.sqlite3
pool: 5
timeout: 5000
When you deploy to Tomcat, it will be on ‘production’ mode by default. Since sqlite3 is file based and for simplicity, I used the same development database.
Now, do the migration.
$ jruby -S rake db:migrate
Deploy to Tomcat
We use ‘warble’ which is an excellent tool for packaging your Rails application. It packages everything you need to run your Rails application inside a Java container.
$ warble $ cp railsjam.war $TOMCAT/webapps # start Tomcat # assuming you arein $TOMCAT dir $ sudo ./startup.sh
Check your Rails 3 application
# You should see the famous Rails welcome localhost:3000/railsjam # Play around with your application localhost:3000/railsjam/users
Deploy Rails 3 using Ruby 1.9.2
Without shutting down your JRuby and Tomcat version, let’s try to run our app using Ruby 1.9.2
# In a new console $ rvm 1.9.2 $ rvm gemset create railsjam $ rvm 1.9.2@railsjam $ gem install rails # Assuming you are in the ‘railsjam’ folder # This will install sqlite3-ruby gem $ bundle install $ rails server Now, go play with your Rails 3 applications # jruby + tomcat http://localhost:8080/railsjam/users # ruby 1.9.2 http://localhost:3000/users
In case you encountered some problems, here are some ways to solve them. If your problem is not listed here, you can email me. I only accept Paypal
JRuby does not support native extensions
You did not update the Gemfile to use the jdbc version of sqlite3. You will encounter this error when you install the gems.
$ bundle install
....
Installing sqlite3-ruby (1.3.1) with native extensions /home/greg/.rvm/rubies/jruby-1.5.2/lib/ruby/site_ruby/1.8/rubygems/installer.rb:482:in `build_extensions': ERROR: Failed to build gem native extension. (Gem::Installer::ExtensionBuildError)
/home/greg/.rvm/rubies/jruby-1.5.2/bin/jruby extconf.rb
WARNING: JRuby does not support native extensions or the `mkmf' library.
Check http://kenai.com/projects/jruby/pages/Home for alternatives.
extconf.rb:9: undefined method `dir_config' for main:Object (NoMethodError)
undefined method `attributes_with_quotes’ for class `ActiveRecord::Base’
I first encountered this problem when doing migration.
$ rake db:migrate rake aborted! undefined method 'attributes_with_quotes' for class 'ActiveRecord::Base'
This is caused by an old version of your jdbc gems. In my case, sometimes bundler installs the old versions:
Installing activerecord-jdbc-adapter (0.9.2) Installing activerecord-jdbcsqlite3-adapter (0.9.2)
As of this writing, the latest version is 0.9.7
Installing activerecord-jdbc-adapter-0.9.7-java Installing activerecord-jdbcsqlite3-adapter-0.9.7-java
Bundler keeps installing 0.9.2
$ rm Gemfile.lock $ jruby -S bundle install
no such file to load — sqlite3
$ rake db:migrate (in /home/greg/dev/projects/jruby/railsjam) rake aborted! no such file to load -- sqlite3
‘sqlite3′ is the default name of the database adapter but with jruby, it should be ‘jdbcsqlite3′.
But, when I tried ‘sqlite3′ with a fresh gemset and a new machine, it went well.
Anyway, just in case you run into the same problem in the future, add a condition
in your database.yml
development:
adapter: <%= defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' %>
database: db/development.sqlite3
pool: 5
timeout: 5000
production:
adapter: <%= defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' %>
database: /home/greg/dev/railsjam/db/development.sqlite3
pool: 5
timeout: 5000
We’re sorry, but something went wrong.
If you see the famous Rails error message, you need to dig in Tomcat’s log files.
$ cd /usr/local/apache-tomcat-7.0.2/logs $ ls -al localhost* -rw-r--r-- 1 root root 1181 2010-09-01 00:17 localhost.2010-09-01.log -rw-r--r-- 1 root root 1062 2010-09-01 00:18 localhost_access_log.2010-09-01.txt $ tail -f localhost.2010-09-01.log
In the log file, you will see the errors like missing database.
org.jruby.rack.RackInitializationException: The driver encountered an error: java.sql.SQLException: path to '/home/greg/dev/tmp/apache-tomcat-7.0.2/webapps/railsjam/WEB-INF/db/production.sqlite3': '/home/greg/dev/tmp/apache-tomcat-7.0.2/webapps/railsjam/WEB-INF/db' does not exist
Rails 3 upgrade part 4: Prototype helpers and Javascript
Rails 3 is embracing the unobtrusive Javascript (or UJS) mantra which is good because it is the right way; at the same time, it is bad because many applications will break when they upgrade to Rails 3. On the other hand, who’s expecting a smooth upgrade anyway
In my test application, I used jrails because I am more interested in jQuery than Prototype. But since jrails doesn’t work with Rails 3, I removed it.
When jrails was removed, I received this error:
undefined method `observe_field' for #<#<Class:0xb6867e58>:0xb6865b6c>
Install Prototype helper plugin
‘observe_field’ is a Prototype helper and Rails 3 removed the the link between its Javascript helpers and Prototype. The goal in Rails 3 is for developers to use their preferred Javascript library. Also note that remote_#{method} helpers have been removed from Rails and moved to Prototype Legacy Helper plugin . To install this plugin, just do:
rails plugin install git://github.com/rails/prototype_legacy_helper
Remove jQuery
Once the prototype_legacy_helper is installed, the missing method is gone but observe_field is not triggering. Removing jQuery fixes this problem.
Now what if you want to use jQuery instead of Prototype? It depends how dependent your application is to Prototype. I have not found a jQuery equivalent for Prototype helper plugin yet so that would be an issue like in my case. Based on this jQuery and Rails 3 tutorial, using the jQuery UJS driver looks very easy.
Rails 3 upgrade part 3: Code fixes, views, and forms
This is part 3 of my Rails 2 to Rails 3 upgrade experience. Part 1 is about the initial code upgrade and getting the application to boot while part 2 deals with routes. While Part 2 is mainly about routes, getting it work involved changes in other parts of the code which I’ll share this time. So while you are updating your routes, you may need to check this post in between changes.
Update ApplicationController
After regenerating your application with rails (i.e. rails new appname -d dbadapter), your ApplicationController would look like this:
class ApplicationController < ActionController::Base
protect_from_forgery
end
There’s no need to panic because rails:upgrade:backup made a copy of the controller to application_controller.rb.rails2.
If you have a lot of helper modules, you’ll most likely have this code in your Rails 2 ApplicationController:
helper :all
If you encounter a missing method error while monkey clicking your application, you probably forgot to update your Rails 3 ApplicationController.
Update ApplicationHelper
The ApplicationHelper module was also modified by the rails upgrde. So don’t forget to update this, too.
RAILS_* constants are deprecated is not entirely true
When you run rails:upgrade:check, it will list items you need to update including deprecated code. There is no need to change these as the word ‘deprecated’ means but I encountered several “can’t convert nil into String” errors.
rake rails:upgrade:check
(in /mnt/hgfs/greg-mini/dev/projects/propsify)
Deprecated constant(s)
Constants like RAILS_ENV, RAILS_ROOT, and RAILS_DEFAULT_LOGGER are now deprecated.
More information: http://litanyagainstfear.com/blog/2010/02/03/the-rails-module/
The culprits:
...
The weird part is some constants are just doing fine. In any case, here are the conversion:
RAILS_ROOT -> Rails.root RAILS_ENV -> Rails.env RAILS_DEFAULT_LOGGER -> Rails.logger
You can also check your environment the Ruby way:
# before
if RAILS_ENV == 'production'
...
# Rails 3
if Rails.env.production?
Output strings are automatically escaped
We should all be rejoicing that Rails is now serious about XSS protection except now your pages have become ugly with all those HTML tags. For example the code below will not give you a clickable link.
- signup = link_to('create one here', signup_path)
= "If you do not have an account, #{signup}."
To fix this, use the raw() helper.
= raw "If you do not have an account, #{signup}."
Too bad for me, I got tons of views that were coded like this.
Check for ‘concat’
A popular technique to simplify your view code is to use content blocks. You create a helper that takes a block and wraps it in some HTML tags. A simple implementation would look like this:
module LayoutHelper
def main_column(options={}, &block)
# calls column()
end
def column(options={}, &block)
# concat is not needed in Rails 3
concat content_tag(:div, capture(&block), options)
end
end
# in your view
- main_column do
= render 'form'
This works fine in Rails 2 but in Rails 3 the block gets outputted twice. concat is the way to output text in a non-output block (i.e. <% %> in erb) but it seems like erb blocks in Rails 3 do not need concat.
Helpers with blocks
Before Rails 3, form_for or fields_for use non-output syntax; it means no equals sign.
# erb
<% form_for @offer do |f| %>
# ...
<% end %>
# haml
- form_for @offer do |f|
# ...
In Rails 3, it should now be written as an output block.
# erb
<%= form_for @offer do |f| %>
# ...
<% end %>
# haml
= form_for @offer do |f|
= f.fields_for :items do |ff|
# ...
The rule is if the method is expected to return a string, it should use the output syntax. If it just buffering the returned string like content_for, it should NOT have the equals sign.
Rails 3 upgrade part 2: Routes
In the previous post, I outlined the steps I took to upgrade and boot a Rails 3 application. This time, I share my experience upgrading the routes file. By the way, I forgot to mention in the last post that I’m using Rails 3 Upgrade Handbook by Jeremy McAnally.
The task rails:upgrade:routes (comes with the rails_upgrade plugin) converts your Rails 2 routes into Rails 3 format. It handles most cases but you may still need to edit the generated routes depending on your setup.
map.root
Below, I show the old route and the generated version.
# Rails 2 map.root :controller => 'search' # Rails 3 match '/' => 'search#index'
The conversion is correct but since I use the named route ‘root_path’ in my application, I had to change it:
root :to => 'search#index'
:as, :member, :any, :path_names
# Rails 2
map.resources :workspaces, :as => 'b', :member => { :widget => :get } do |workspace|
# ...
end
# Rails 3
resources :workspaces do
# ...
end
In Rail 3, :as is for overriding the normal naming for named routes witout affecting the path. For example, the code below will recognize the path ‘/workspaces’ and the named route becomes offices_path.
resources :workspaces, :as => 'offices'
In Rails 2, :as affects the path. In my example, ‘/b’ routes the request to WorkspacesController. So for Rails 3 to recognize the path ‘/b’, I need to add another route.
match 'b' => 'workspaces#index'
The rails:upgrade:routes did not convert the following member route and had to be added.
:member => { :widget => :get }
The new route becomes:
resources :workspaces do
get :widget, :on => :member
end
In Rails 2, you can use the :any option to define a custom route that responds to any request method.
# Rails 2
workspace.resource :twitter_account, :member => { :authorize => :any }, :path_names => { :edit => 'request_authorization' }
# generated by rails:upgrade:routes
resource :twitter_account do
member do
any :authorize
end
end
The rails:upgrade:routes converted the :any option. However, when I booted the application, it raised an exception:
undefined method 'any' for #<ActionDispatch::Routing::Mapper:0xb71b6fcc> (NoMethodError)
To fix this, I replaced the offending line with a match method.
resource :twitter_account do
match :authorize, :on => :member
end
:path_names was also not included in the generated route so has to be added as well.
resource :twitter_account, :path_names => { :edit => 'request_authorization' } do
match :authorize, :on => :member
end
Specifying a different controller
# Rails 2
map.resource :settings, :controller => 'users' do |settings|
settings.resource :twitter_account, :name_prefix => nil, :member => { :authorize => :any }, :path_names => { :edit => 'request_authorization' }
end
# generated by rake:upgrade:routes
resource :settings do
resource :twitter_account do
member do
any :authorize
end
end
end
To fix, just specify the controller
resource :settings, :controller => :users do
# ...
end
Undefined named route helper
I encountered this exception while trying the application:
undefined method 'edit_twitter_account_path'
In Rails 2, this is the route that created this named route:
map.resource :settings, :controller => 'users' do |settings|
settings.resource :twitter_account, :name_prefix => nil, :member => { :authorize => :any }, :path_names => { :edit => 'request_authorization' }
end
This is a bit tricky for me because I cannot remember why I nested it
Nevertheless, to fix the Rails 3 error, I moved :twitter_account outside of :settings. The correct Rails routes now look like these:
resource :settings, :controller => :users
resource :twitter_account, :path_names => { :edit => 'request_authorization' } do
match :authorize, :on => :member
end
Custom polymorphic named route helper
A long time ago, I played around with polymorphic paths. In hindsight, that is a waste of time but back then it was fun or should I say a time well wasted. I have a named route helper that takes any object and used like this:
# in views
link_to 'invitations', invitations_path(@voteable)
# definition
module RoutesHelper
def invitations_path(voteable)
send("#{voteable.class.name.underscore}_invitations_path", voteable)
end
def workspace_invitations_path(workspace)
super(:workspace_id => workspace)
end
# ...
end
In Rails 3, my named route helper is not being called. Thus, wrong URL is generated. I know, I know it should have been a simple polymorphic_path call but I still wonder why my method is not called. Moving on, the new ruby is:
link_to 'invitations', polymorphic_path([@voteable, :invitations])
I cheated a bit here because I want this post to focus on routes. Along the way, I had to update non-route related code to discover the route problems. You can learn more about Rails 3 routes from this RailsGuides page.
There are still more updates to be done and I’ll share them in other posts. Just like your favorite late night infomercial, “Wait! There’s more”.
Rails 3 upgrade part 1: Booting the application
It’s time for another Rails upgrade! We all have our share of bad experiences and frustrations every time we upgrade a piece of software. Even for technical people who live and breath on the edge, upgrades are one of these things we try to avoid as much as possible. Still, there is always a sense of excitement in trying something new even if it adds problems to an already stable piece of code.
For a little background, I am upgrading a Rails app several of friends and I have written last year. The code is available at github.
In this post, I share the steps I did to boot the application. This doesn’t mean the upgrade went fine neither the app is ready to go. It only means all the required initialization are OK. In succeeding posts, I share my experiences in upgrading the app to a green state.
First, my environment.
greg@piccolo:~/dev/projects/propsify3$ rvm info
ruby-1.8.7-p299@propsify:
system:
uname: "Linux piccolo 2.6.31-22-generic #61-Ubuntu SMP Wed Jul 28 01:57:06 UTC 2010 i686 GNU/Linux"
shell: "bash"
version: "4.0.33(1)-release"
rvm:
version: "rvm 0.1.44 by Wayne E. Seguin (wayneeseguin@gmail.com) [http://rvm.beginrescueend.com/]"
ruby:
interpreter: "ruby"
version: "1.8.7"
date: "2010-06-23"
platform: "i686-linux"
patchlevel: "2010-06-23 patchlevel 299"
full_version: "ruby 1.8.7 (2010-06-23 patchlevel 299) [i686-linux]"
greg@piccolo:~/dev/projects/propsify3$ script/about
About your application's environment
Ruby version 1.8.7 (i686-linux)
RubyGems version 1.3.7
Rack version 1.0 bundled
Rails version 2.3.2
Active Record version 2.3.2
Action Pack version 2.3.2
Active Resource version 2.3.2
Action Mailer version 2.3.2
Active Support version 2.3.2
Application root /mnt/hgfs/greg-mini/dev/projects/propsify
Environment development
Database adapter postgresql
Database schema version 20100113032723
greg@piccolo:~/dev/projects/propsify3$ gem list
*** LOCAL GEMS ***
actionmailer (2.3.2)
actionpack (2.3.2)
activerecord (2.3.2)
activeresource (2.3.2)
activesupport (2.3.2)
geokit (1.5.0)
json (1.4.5)
mime-types (1.16)
oauth (0.4.1)
pg (0.9.0)
rails (2.3.2)
rake (0.8.7)
RedCloth (4.2.2)
twitter_oauth (0.3.2)
greg@piccolo:~/dev/projects/propsify3$ ls vendor/gems/
authlogic-2.1.3 geokit-1.5.0 haml-2.2.16 macaddr-1.0.0 twitter_oauth-0.3.2 uuid-2.1.0
greg@piccolo:~/dev/projects/propsify3$ ls vendor/plugins/
acts_as_commentable geokit-rails is_taggable thinking-sphinx will_paginate
declarative_authorization gravatar-plugin jrails validates_date_time
exception_notification haml subdomain-fu vote_fu
Step 1: Install rails 3
gem install rails –pre
Step 2: Install the plugin tool
script/plugin install git://github.com/rails/rails_upgrade.git
Step 3: Show upgrade checklist
rake rails:upgrade:check
This task lists the items you should watch out for when doing the upgrade. You don’t need to fix everything right away (some are deprecation notice) but review the checklist nevertheless.
Step 4: Generate the new routes
rake rails:upgrade:routes
This task reads the current config/routes.rb and outputs a Rails 3 version.
Don’t worry, it doesn’t override your routes file. Keep this in a safe place for later use.
IMPORTANT: I actually didn’t realize I did the right thing until after the actual code upgrade. When I tried generating the new routes after the code change, it outputted an empty block. I have no idea if this is unique to my case but just to be sure, generate the routes beforehand and keep a copy.
Step 5: Create Gemfiles
rails:upgrade:gems
Next is to generate the file ‘Gemfile’. In Rails 2, the gems you need are listed in config/environment.rb while in Rails 3 the gems are listed in the Gemfile. Gemfile is used by the program ‘bundler’ to manage the gems required by your application. Unfortunately, this task didn’t include the gems I listed in environment.rb so I have to add it later.
Step 6: Backup your files
rails:upgrade:backup
I hope you are working on another branch (or a copy) but just in case you are not, run this task to make copies of the files that will be affected during the upgrade.
Now comes the juicy part.
Step 7: Generate the Rails 3 app on top of your Rails 2 app
rails new propsify3 -d postgresql
Run this command in your app’s parent folder. In my case, my app’s name and pathname is ‘propsify3′ and I am using postgresql as my database. This command created and replaced a bunch of files. Since you’ve backed-up everything, there’s nothing to worry.
Step 8: Move code from environment.rb to application.rb
Your new config/environment.rb file looks like it went through a rigorous diet. You can leave this file for now. What is important now is you move the initializer code from your config/environment.rb.rails2 to config/application.rb. These are the config.* lines except the config.gem which goes to Gemfile.
Step 9: Convert the new routes
You can still use the existing routes until 3.1 but since there’s a tool to help you migrate, I suggest doing it. At this point, when I tried the rails:upgrade:routes, no routes were generated. So make sure you generate the routes before Step 7.
Step 10: Delete new_rails_defaults.rb
rm config/initializers/new_rails_defaults
Step 11: Upgrade the plugins and gems
Many plugins are now available as gems. Check your plugins and gems at http://railsplugins.org. In my case, the following plugins were converted to gems:
acts_as_commentable
declarative_authorization
haml
will_paginate
thinking-sphinx
Unfortunately, the plugins below are not yet ready for Rails 3. I removed them for now and all code that references them.
jrails
subdomain-fu
vote_fu
IMPORTANT: In your Gemfile, make sure you check specify the right version that is compatible with Rails 3. Some gems are still in the pre-release version and will not be downloaded if you don’t specify a version in your Gemfile. For example, this is a snippet from my Gemfile:
gem 'pg' gem 'acts_as_commentable' gem 'declarative_authorization' gem 'haml' gem 'thinking-sphinx', '2.0.0.rc1', :require => 'thinking_sphinx' gem 'will_paginate', '3.0.pre2' gem 'uuid' gem 'geokit'
Step 12: Update initialization code
After step 10 you are good to go, if you’re lucky. In my case, I had to remove some patches and change code to boot the application.
ActiveSupport::CoreExtensions::Date::Conversions::DATE_FORMATS.merge!(date_time_formats)
This fails in Rails 3 because core extensions have been moved out of their modules and are now included in classes they extend. For example, to fix the date format problem do:
Date::DATE_FORMATS.merge!(date_time_formats)
Step 13: Boot the app
rails server
Yay! If you are wondering what happened to script/server command, Rails went the “Merb way” and consolidated the script/* commands into the rails script.
By now, you should see the famous Rails’ “Welcome aboard” message in your browser.
Step 14: Remove public/index.html
Now, you can try if your application is working.
There are still more work to do like moving to the ActiveRecord/ActiveRelation API and removing the deprecation notices. Before moving on, I still need to fix the problems in my routes and unsupported gems which I will tackle in my next post.