Skip to content

Instantly share code, notes, and snippets.

@jwilsonsprings
Created September 22, 2010 17:06
Show Gist options
  • Save jwilsonsprings/592078 to your computer and use it in GitHub Desktop.
Save jwilsonsprings/592078 to your computer and use it in GitHub Desktop.

#Dr StrangeCode or "How I Learned to Stop Worrying and Love the Rails 3.0 Upgrade"* I recently upgraded one of my Rails applications from 2.3.5 to 3.0.0 (and from ruby 1.8.7 to 1.9.2). I took a series of notes of all the problems and issues I ran into. ##Ruby 1.8.7 to 1.9.2 upgrade

  • FasterCSV is part of 1.9.2, but not 1.8.7. If you want to maintain compatibility with both, then keep using FasterCSV.
  • ftools no longer exists in ruby 1.9.2. Was using it in a require. I just used fileutils instead.
  • I had a bunch of old-style case/when statements that no longer work in 1.9.
    hours = case r[6..6]
        when "M": [0,11]
        when "A": [12, 18]
        when "E": [18, 23]
    end

In 1.9 is:

    hours = case r[6..6]
        when "M" then [0,11]
        when "A" then [12, 18]
        when "E" then [18, 23]
    end
  • Ruby 1.9 can no longer just read binary files by default. For my upload file testing I needed to add the binary=true last arg to fixture_file_upload (as well as adding "b" when writing them):
    @p = Photo.new(:file => fixture_file_upload("photo.jpg", 'image/jpeg', true), :caption=> "caption",
        :field => fields(:one))
  • Ruby 1.9 defaults to UTF-8. I had a test file in 8859-1, so had to read it the correct way:
    File.open(File.join(Rails.root, "test", "fixtures", "test_file.html"), "r:ISO-8859-1") do |f|
  • OK, here is a good one... In 1.8.7:
    Date.parse('3/06/2010') = Sat, 06 Mar 2010

but in 1.9.2:

    Date.parse('3/06/2010') = Thu, 03 Jun 2010

The default for 1.9 is to use European formats... This fixes it, ignoring any time zone issues:

    Date.strptime(date, "%m/%d/%Y")
  • Burnt by 1.9 treatment of block variable scope:
    sum = partnerships.inject(0) do |i, (k,v)|
      i = v.inject(i) {|sum, g| sum += g.member_id == members(:aaron).id ? 1 : 0}
    end
    assert_equal 4, sum

This was pretty bad code, as I shouldn't reuse the same variable both inside and outside the scope. This is better...

    total = partnerships.inject(0) do |i, (k,v)|
      i = v.inject(i) {|sum, g| sum += g.member_id == members(:aaron).id ? 1 : 0}
    end
    assert_equal 4, total

##Rails 2.3.5 to 3.0.0 upgrade ###General comments

  • Using 3'rd party Gems/plugins is nice until they go out of favor.. Or are greatly enhanced while you are not looking. I had to migrate from an old version of SemanticFormBulder to the more modern Formtastic.
  • If you don't have rather complete unit/functional tests, I would forgoe any attempt at upgrading... Too many things will no longer work... My tests found lots of subtle problems.
  • I am using Jammit, so routes would not update until after I installed gem for Jammit. From my config/routes.rb, I removed:
    Jammit::Routes.draw(map) 
  • Give the upgrade script a try, as it can highlight areas of conversion/upgrade:
    cd ROOT/vendor/plugins
    git clone http://github.com/rails/rails_upgrade.git
    ruby install.rb
    cd ../..
    rake rails:upgrade:backup
    rake rails:upgrade:check
    rake rails:upgrade:routes

###Specific issues/problems

  • Move your config.gem's to Gemfile
  • Was using tiny_mce plugin, but is throwing deprecation errors... Removed from vendor/plugins and switched to the gem.
  gem install tinymce
  • I used the routes conversion tool rails:upgrade:routes from the rails_upgrade, but I had to hand tweak my routes. It was pretty close however. Looks like it is messing up my HTTP method names (converting my method :put to put). I'd recommend reviewing the converted routes.rb by hand, closely examining what the converter did and correcting where necessary. It converted:
  map.remote_log "/remote_log/:level", :controller => 'remote_logger', :action => "log",
  :conditions => { :method => :put } 

to

  match '/remote_log/:level' => 'remote_logger#log', :as => :remote_log, :via => put

The put should be :put.

  • session_store settings have changed. In 2.3.5 I have this in my environment.rb
config.action_controller.session = {
    :session_key => '_my_key',
    :secret      => '5e97a47...'}

Its now in config/initializers/session_store.rb:

Rails.application.config.session_store :cookie_store, :key => '_my_key'

and in config/initializers/secret_token.rb:

Rails.application.config.secret_token = '5e97a47...'
  • Fix deprecation warnings by changing
RAILS_ROOT
RAILS_ENV

to

Rails.root
Rails.env
  • Merge the backed-up .rails2 files from the rails:upgrade:backup above

    • application_helper.rb - I just used my old one.
    • application_controller.rb - I used my old one but preserved the "protect_from_forgery" from the new one.
    • Merged my specific settings in environments/production.rb and development.rb.
  • I was using ExceptionNotification. You need to use the rails 3 gem in your Gemfile:

gem 'exception_notification', :git => 'git://github.com/rails/exception_notification.git', :require => 'exception_notifier'

You also need to update your notification settings in your config/environments/production.rb (assuming that is where you enable it):

  require 'exception_notifier'
  config.middleware.use ExceptionNotifier,
  :email_prefix => "[Server Error] ",
  :sender_address => %Q("Application Error" )
  :exception_recipients => %w{you@home.com}
  • Logging setup appears to be a good bit different. I had to move stuff around between application.rb and the various environments in order to get logging initialized correctly. I have extended a Logger to get me timestamped entries(TimestampedLogger).
class TimestampedLogger < ActiveSupport::BufferedLogger
  def initialize(log="%s.log" % Rails.env, level = DEBUG)
    FileUtils.mkdir_p(File.expand_path('log', Rails.root))
    super(File.join(File.expand_path('log', Rails.root), log), level)
  end
  
  def add(severity, message = nil, progname = nil, &block)
    return if @level > severity
    ts = Time.now.strftime("%Y-%m-%d %H:%M:%S")
    message = (message || (block && block.call) || progname).to_s
    # If a newline is necessary then create a new message ending with a newline.
    # Ensures that the original message is not mutated.
    message = "%s %s %s" % [ts, level_to_s(severity), message]
    message << "\n" unless message[-1] == ?\n
    buffer << message
    auto_flush
    message
  end
  protected
  def level_to_s(level)
    case level
      when 0 then "DEBUG"
      when 1 then "INFO"
      when 2 then "WARN"
      when 3 then "ERROR"
      when 4 then "FATAL"
    else "UNKNOWN"
    end
  end
end

My application.rb looks like:

    config.active_support.deprecation = :log
    config.colorize_logging = false
    Rails.logger = config.logger = TimestampedLogger.new

    ActiveRecord::Base.logger = TimestampedLogger.new("active_record_%s.log" % Rails.env)

In each of my environments, I set the logging level I want (e.g., for production):

  config.logger.level = Logger::INFO

###Stuff my unit tests caught

  • It appears that my lib/ files were automatically required in 2.3.x, but no longer in 3.0. Add this to your application.rb:
   config.autoload_paths += [File.join(Rails.root, "lib")]
  • I was using TMail to validate my email addresses. TMail is no longer a part of Rails 3.0. Another way recommended is discussed here. It worked for me.
  • If you have tests using file upload, you must change (in your test_helper.rb) to the below. See this link.
class ActiveSupport::TestCase
# this allows fixture_file_upload to work in models
#  include ActionController::TestProcess
  include ActionDispatch::TestProcess
  • This is an old app and was still using ActiveSupport::TestCase (not RSpec, etc). Gotta fix this warning, as errors.on is deprecated:
DEPRECATION WARNING: Errors#on have been deprecated, use Errors#[] instead.

Also note that the behaviour of Errors#[] has changed. Errors#[] now always returns an Array. An empty Array is returned when there are no errors on the specified attribute.

  • For one of my models, I was overriding to_json. Alas, in 3.0(actually, this started in 2.3.x somewhere) I need to override as_json. Very subtle failure...

  • The default for content_tag in view helpers is now to escape the output, so if you are nesting them:

  content(:div, content_tag(:table, content_tag(:tr)....

then be careful about inadvertent escaping of the nested content. There may be better ways of contructing view helpers than what I had, but i had to tweak the old code so that it would not escape when it shouldn't... I also had lots of partials that generated content (my own). All these need to be prefaced with raw or use a .html_safe.

  • A bit of funkiness from ActiveRecord. This used to work:
    self.class.find(:all, :conditions => [conditions,
                                          {:member_id => member.id, :id => id,
                                            :game_id => game_id
                                           }])

but in 3.0 it generates an extra clause (the test for NULL at the end). Not sure what AR is now trying to do here:

SELECT `partner_games`.* FROM `partner_games` 
    WHERE ((967174477 in (member_id, partner1_id, partner2_id, partner3_id)) 
      and (game_id = 309456473) ) AND (`partner_games`.`member_id` IS NULL)

I fixed by changing to a where clause:

    self.class.where(conditions,
                             {:member_id => member.id, :id => id,
                               :game_id => game_id
                             }).all

This did not generate the extra clause above (which of course broke things for me):

 SELECT `partner_games`.* FROM `partner_games` 
     WHERE ((192593060 in (member_id, partner1_id, partner2_id, partner3_id)) and (game_id = 309456473) )
  • You can no longer override validate, so I had to rewrite my custom validations... Its better code now...
  • Nice one... I was building a date via a custom date format string:
   date = Time.zone.local(d[0..1].to_i, d[2..3].to_i, d[4..5].to_i, hour)

but I was using just two digits for the year... Worked in 2.3.x, but not 3.0. Oh well, might as well do it the right way...

   date = Time.zone.local(2000 + d[0..1].to_i, d[2..3].to_i, d[4..5].to_i, hour)
  • I had some code that relied, inadvertently, on the default sort order of hash keys. Shame on me...
  • Changed all my mailers to the new syntax. Changed:
      UserMailer.delvier_broadcast(Member.contact_emails, msg, current_member.email)

to:

      UserMailer.broadcast(Member.contact_emails, msg, current_member.email).deliver

###Stuff my functional tests caught

  • Looks like observe_field (Prototype) is no longer there (beyond deprecated). I'll use this as an excuse to convert this to use jQuery.change().
  • Looks like some of my assert_routing's don't work. It seems to have difficulty with routes that have default parameters. I switched to using assert_recognizes and that helped some of them, however problems remain that make no sense. In routes.rb:
  match '/games/index/:year/:month.:format' => 'games#index', :as => :games, :via => :get, 
        :defaults => { :month => nil, :year => nil, :format => "html" }, 
        :month => /\d{1,2}/, :year => /\d{4}/

my test:

    assert_recognizes({:controller => 'games', :action => 'index', 
      :year => "2000", :month => "1", :format => "html"}, games_path(:year => 2000, :month => 1))
The recognized options <{"action"=>"index", "controller"=>"games"}> did not match <{"controller"=>"games",
 "action"=>"index",
 "year"=>"2000",
 "month"=>"1",
 "format"=>"html"}>, difference: <{"year"=>"2000", "month"=>"1", "format"=>"html"}>

Well, I think it has something to do with the fact that I am routing to the index method (the default), as an extremely similiar case (not to index) works as expected. I get it to pass by:

    assert_recognizes({:controller => 'games', :action => 'index'}, games_path(:year => 2000, :month => 1))

Makes no sense to me...

  • @controller (used in some of my layouts and helpers) is deprecated. Plus I was sharing a helper method that was both used by a controller filter and by a view. Therefore in one case (the filter) the controller is implied (self = controller), however when called from a view, I need to be explicit about referencing the controller. Perhaps I structured my code incorrectly...
  • Controller methods included in ApplicationController may no longer be visible in views:
class ApplicationController < ActionController::Base
  include Authentication # has method "current_user"

in 2.3 I could access current user from a view. In 3.0 I cannot. Fixed by creating a helper method that fetches from controller.

  • My authentication code had a reset_session. When I convert to 3.0, I get a:
NoMethodError: undefined method `destroy' for #
    actionpack (3.0.0) lib/action_dispatch/http/request.rb:202:in `reset_session'

Looking at the code, it appears it expects a Session object, but in test mode I have a hash. Created a rails ticket with suggested patch: See ticket

  • Humm, I did not do a lot of view testing originally. Now that I have to (or am) converting to Formtastic, I need tests to validate my forms. Bummer. But turns out it wasn't that bad... Most of the conversion was mechanical...
  • Delete links were not working.
    <%= link_to('delete', member_path(member.id), :method => :delete,
      :confirm => "Are you sure you want to delete member: #{member.full_name} ?") %>
    <% end %>

You must add the crsf_meta_tag to your application layout. Of course, it is generated by default for a new Rails app, but I want to use my old layout... I am sure there are upgrade notes on this, but a very easy one to miss. Your application.html.erb should have this

 <head> 
   <%= csrf_meta_tag %>

See this link for details.
The plot thickens... I forgot to add the new rails.js to my list of javascripts. But that presumes you are using prototype, which I am not. I prefer jQuery, so you have to find that version here and use that as your rails.js.
Alas, still not working... Duh.... load jquery before rails.js...
OK, the actual problem is a Google Chrome problem, where the jQuery.live() function does not work correctly. I monkey patched my copy of rails.js by changing the live to a bind and all is well. I can see various issues via google search where chrome has problems with jQuery.live.

  • This is not really a 3.0 problem, but my file downloads were not quite working correctly. The Content-Type header was always set to text/html, even thought I might be downloading an image/gif. Curl and Chrome did not like this, but Firefox did not seem to care. I probably ran into this because I was using Chrome. Without fix:
  # GET /documents/1
  def show
    @document = Document.find(params[:id])
    send_file @document.internal_file, :type => @document.content_type, :filename => "%s" % [@document.file_name]
  end
>curl -I http://localhost:3000/documents/65
HTTP/1.1 200 OK 
Content-Type: text/html; charset=utf-8
Content-Disposition: attachment; filename="bar_expand.gif"
Content-Transfer-Encoding: binary
Cache-Control: private
X-Ua-Compatible: IE=Edge
X-Runtime: 0.675472
Content-Length: 0
Server: WEBrick/1.3.1 (Ruby/1.9.2/2010-08-18)
Date: Fri, 17 Sep 2010 23:55:43 GMT
Connection: Keep-Alive

This fixes it:

  # GET /documents/1
  def show
    @document = Document.find(params[:id])
    response.headers['Content-Type'] = @document.content_type # this forces correct header setting
    send_file @document.internal_file, :type => @document.content_type, :filename => "%s" % [@document.file_name]
  end
>curl -I http://localhost:3000/documents/65
HTTP/1.1 200 OK 
Content-Type: image/gif
Content-Disposition: attachment; filename="bar_expand.gif"
Content-Transfer-Encoding: binary
Cache-Control: private
X-Ua-Compatible: IE=Edge
X-Runtime: 0.568216
Content-Length: 0
Server: WEBrick/1.3.1 (Ruby/1.9.2/2010-08-18)
Date: Fri, 17 Sep 2010 23:56:50 GMT
Connection: Keep-Alive
  • I also ran into a problem with my action caching (via file_store). Submitted ticket with patch here. I was getting a:
NoMethodError: undefined method `ord' for nil:NilClass
    from /home/jwilson/.rvm/gems/ruby-1.9.2-p0@cosb/gems/activesupport-3.0.0/lib/active_support/whiny_nil.rb:48:in `method_missing'
    from /home/jwilson/.rvm/gems/ruby-1.9.2-p0@cosb/gems/activesupport-3.0.0/lib/active_support/cache/file_store.rb:164:in `block in file_path_key'
    from /home/jwilson/.rvm/gems/ruby-1.9.2-p0@cosb/gems/activesupport-3.0.0/lib/active_support/cache/file_store.rb:164:in `gsub'
    from /home/jwilson/.rvm/gems/ruby-1.9.2-p0@cosb/gems/activesupport-3.0.0/lib/active_support/cache/file_store.rb:164:in `file_path_key'
  • If using Passenger, Rails 3.0, and Apache VHost's, then you may need to set RackEnv (as well as RailsEnv, or instead of...) in your vhost file for apache. I was getting the wrong Rails.env until I set RackEnv in my vhost file.
  • I gave Apache, Passenger and RVM a shot, but punted as I could not really get it to work smoothly. Besides not sure there were any advantages using RVM gemsets over bundling gems within the app (vendor/bundle). Stayed with RVM in my dev environment however. And of course you cannot run Passenger on two different ruby versions (1.8.7, 1.9.2) at the same time... ###Live integration testing
  • A few cases of escaped content snuck thru, as some of my pages are built up on the fly. I suppose I could have some view tests that scan the output for escaped output ("&lt;"), but oh well...
  • Messed up one of the Formtastic conversions (dropped the "multipart" for form doing a file upload).
  • Other than that deployed the app live yesterday.

I hope this tour of a real life Rails 3.0 upgrade proves useful for others.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment