#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{[email protected]}
- 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
- Deprecation errors from jammit gem. Get latest from http://github.com/documentcloud/jammit.git.
###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 ("<"), 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.