Posts tagged ruby on rails

More notes from a Rails 3.0pre upgrade

This is a follow-on from my piece on how I got the (development version of) Catapult Magazine up and running with Rails 3.0pre. If you haven’t already done so, I’d recommend you read that first.

Catapult makes use of the permalink_fu plugin which fails in Rails 3. It fails because of a reliance on the evaluate_attribute_method method which no longer exists in version 3. I’ve temporarily worked around that by replacing it with class_eval, but lately I’ve been using friendly_id a lot more and I suspect I’ll be focussing on porting to that if it works cleanly in Rails 3.

***

A dependency I’ve long wanted to part with is has_many_polymorphs. I’m using it to manage the categorisation of issues and posts but all that’s ever really saved me is one table/model, so perhaps now is the time to clean up the models and lose that dependency.

***

open_id_authentication uses a config.to_prepare block to include itself in ActionController::Base but that’ll work just as well if I add

include OpenIdAuthentication

to my ApplicationController, so that’s what I’ve done. I suspect a number of other changes will be needed (there’s a reference to config.gem in there) but so far, so good.

***

The key issue I’ve run into is that the init.rb files in my gem plugins aren’t run automatically, meaning that in addition, say, to declaring a model has_attached_file (for paperclip) I also need to require ‘paperclip’. That’s not totally unreasonable, but in a complex app it could be quite time consuming and begins to move away from the sense that a plugin/gem is a self-contained unit.

Looking through the new rails source I found the method that loads the init.rb files for plugins installed in vendor/plugins. Rails::Plugin::Vendored (in railties/lib/rails/plugin.rb) contains:

initializer :load_init_rb, :before => :load_application_initializers do |app|
  file   = "#{@path}/init.rb"
  config = app.config
  eval File.read(file), binding, file if File.file?(file)
end

but that only looks through those, not gem plugins. It wouldn’t be too hard to add some code in the initializer to glob the relevant folder and load those files but I’ve not had time yet to decide if that’s my best way forward.

Upgrading an app to Rails 3.0pre

I used to be a strong adherent to tracking edge rails. Up until the release of rails 2.3 I let most of my frequently updated projects track edge with a vendored copy of rails, and it rarely caused me any trouble. When 2.3 hit I rethought all that.

With Rails 3 development ramping up I suspected there’d be significant disruption taking place; even with comprehensive test suits I didn’t want the headaches of keeping track of that, and I didn’t want to spoil the pleasant surprises I expected when Rails 3 landed.

A year on from the Rails 3 announcement, with that release seemingly approaching, and with a few plugins to keep up to date, it seemed like this Christmas break was the time to start work on porting at least one relatively complex app over and see what it took. So I picked Catapult Magazine (a relatively stable codebase, no impending releases, uses my fork of theme_support) and got to work.

[Please don't take any of this as authoritative. I've not been keeping up with rails-core lately and I'm posting these notes in case they're useful to someone. Hopefully when Rails 3 is actually released there'll be clearer and more definitive explanations of the changes.]

I cloned the current rails HEAD into vendor/rails and for good measure used the rails command to create myself a skeleton application elsewhere that I could use for reference. I then called the tried and tested rake rails:update, hoped for the best, and hit my first stumbling block.

The structure of the rails initializers has changed significantly and the usual rake call won’t work. Instead I had to pull config/boot.rb across from my sample app and do a little restructuring of my startup files:

  1. As has been well covered, rails 3 will use the new Gem Bundler and I created a Gemfile in my app’s root folder listing out my gem dependencies, then called ‘gem bundle‘ to pull them all down
  2. Everything is built on rack these days so we need a config.ru file in the root of our rails folder. Mine contains:
      # Require your environment file to bootstrap Rails
      require ::File.expand_path('../config/environment',  __FILE__)
     
      # Dispatch the request
      run Catapult::Application.instance
  3. Most of what we would have had in config/environment.rb now lives in config/application.rb. Mine looks like:
      require File.expand_path('../boot', __FILE__)
      module Catapult
        class Application < Rails::Application
          config.time_zone = 'UTC'
     
          config.generators do |g|
            g.orm             :active_record
            g.template_engine :erb
            g.test_framework  :rspec
          end
        end
      end
     
      # For clearance
      DO_NOT_REPLY = "donotreply@catapultmagazine.com"
  4. With all of that moved, config/environment.rb is now quite minimal:
      # Load the rails application
      require File.expand_path('../application', __FILE__)
     
      # Initialize the rails application
      Catapult::Application.initialize!
  5. Each of the files in config/environments needs to be wrapped in a block:
      Catapult::Application.configure do
        # previous contents of file
      end
  6. The Rakefile now looks like:
        require File.expand_path('../config/application', __FILE__)
     
        require 'rake'
        require 'rake/testtask'
        require 'rake/rdoctask'
     
        Catapult::Application.load_tasks
  7. I also had to install the i18n gem and moved all my plugins out of the way so I can reintroduce them one-by-one.
  8. Changes to config/routes.rb aren't essential but I reworked mine following the examples from Yehuda's blog entry
  9. In Rails 3 RAILS_ROOT has been deprecated in favour of Rails.root and my app relied quite a bit on the former. A quick search & replace sorted that out.

So with all of those changes complete I was able to run rake rails:update to pick up the updated scripts, etc. and a call to script/server launched the app. Kinda. What followed was a long process of confusing errors, almost all of which could be traced back to plugin-related code that needed to be commented out. But once that was taken care of the skeletal remains ran cleanly.

The next step will be trying to get the plugins back in and running cleanly. I'll try and blog my progress on that as it unfolds.

Rails 2.3 final and theme_support updates

Somewhere between Ruby on Rails versions 2.3.0 (RC1) and 2.3.2 (final) a change was made to the arguments required for one of the methods the theme_support plugin requires. I must confess I hadn’t spotted it, but github user knapo kindly sent me a message with a patch. That patch is now applied in the main repository, as are a few tweaks to the ActionMailer integration to better handle multipart emails. You can get the new version from github.

Perhaps more significant than those small patches is that I’ve now pushed up a new rails app for use in testing theme_support. The tests are a series of cucumber stories, and the app is very barebones designed entirely to test the various features. I could do with tightening up the way the test app and the main plugin repository are connected, but it’s a start and has been very helpful with the aforementioned patches. Any improvements to the test suite would be much appreciated. You can, of course, find it on github.

Rails 2.3 and theme_support part 3: Layouts

In my ongoing efforts to bring my fork of theme_support in line with Rails 2.3 I’ve covered the core views and email, but when I left off earlier today layouts still weren’t working.

The key problem with overriding layouts is that the process of identifying them relies on some class methods on ActionController::Base (provided in the ActionController::Layout module). Roughly put we have:

  1. ActionController::Base#render calls ActionController::Base#pick_layout
  2. ActionController::Base#pick_layout checks to see if there’s a specific layout requested and calls ActionController::Base#active_layout
  3. ActionController::Base#active_layout checks whether there’s a candidate layout for this controller or action and makes sure the layout exists by calling ActionController::Base.find_layout
  4. ActionController::Base.find_layout (class method) is called which checks the view_paths to find the appropriate layout in there

The issue is that as a class method ActionController::Base.active_layout has no knowledge of the specific controller instance, or the request object it contains, and so it can’t access our logic to determine the current theme.

One option would be to patch a number of these methods to hand a controller instance around which could be queried for the theme, but that seems rather clunky.

Another option is to wrap the active_layout method so that it (as an instance method) checks whether there is a current theme and if so, adds that theme’s path to the class level view_paths, removing it again after that check:

  class ActionController::Base
    alias_method :theme_support_active_layout, :active_layout
 
    def active_layout(passed_layout = nil)
      if current_theme
        theme_path = File.join(RAILS_ROOT, "themes", controller.current_theme, "views")
        if File.exists?(theme_path) and ! self.class.view_paths.include?(theme_path)
          self.class.view_paths.unshift(theme_path)
          result = theme_support_active_layout(passed_layout)
          self.class.view_paths.shift
          return result
        end
      end
 
      theme_support_active_layout(passed_layout)
    end
  end

And it works!

So that’s views, emails and now layouts all working with theme_support on Rails 2.3. As before, you can get the results in the branch on github:

http://github.com/jystewart/theme_support/tree/rails-2-3-support

Over the next couple of weeks I’m hoping to test my patches on other releases in the 2.x series, probably using a test app with specs or stories that will put it through its paces. Once that’s done I may turn my attention to the other corners of the plugin (that I don’t personally use) like the asset helper methods, but if anyone wants to contribute patches in the meantime I’d love to get them.

Rails 2.3 and theme_support part 2: ActionMailer

Stage 2 of fixing up theme_support for Rails 2.3 was making sure that ActionMailer picked up themed templates (for stage 1 information see here). That’s something I’d not quite cracked in the 2.2 version, so starting afresh with 2.3 forced me to spend the time to look through the full render path and figure out what was going on.

ActionMailer is a little more complicated than ActionView in that there are multiple routes of entry (ways of sending emails) and each email can have multiple templates associated with it to allow for multipart email. But at the core of it all is the ActionMailer::Base#create! method. This executes the specific method that populates the mailer variables (ie. the code you actually write in your mailers) and then uses Dir.glob to look for appropriate templates for this email:

Dir.glob("#{template_path}/#{@template}.*").each do |path|
  template = template_root["#{template_path}/#{File.basename(path)}"]
 
  # Skip unless template has a multipart format
  next unless template && template.multipart?
 
  @parts < < Part.new(
    :content_type => template.content_type,
    :disposition => "inline",
    :charset => charset,
    :body => render_message(template, @body)
  )
end

The initial patch provides a couple of ways to specify the theme for an email. I’m mainly using the approach of specifying self.current_theme within a mailer method. eg:

class UserMailer < ActionMailer::Base
  def activation(user)
    setup_email(user)
    self.current_theme = user.site.theme
    @subject    += 'Your account has been activated!'
    @body[:url]  = "http://www.catapultmagazine.com/"
  end
end

With that in place the next step was to patch ActionMailer::Base#create! so that instead of just looking at the main template_path it looked first in the relevant theme folder and then in the main template folder. So the code above becomes:

tpaths = []
tpaths < < File.join(RAILS_ROOT, "themes", self.current_theme, "views", mailer_name) if self.current_theme
tpaths << template_path
 
tpaths.each do |tpath|
  Dir.glob("#{tpath}/#{@template}.*").each do |path|
    template = template_root["#{tpath}/#{File.basename(path)}"]
 
    # Skip unless template has a multipart format
    next unless template && template.multipart?
 
    @parts << Part.new(
      :content_type => template.content_type,
      :disposition => "inline",
      :charset => charset,
      :body => render_message(template, @body)
    )
  end
  break if @parts.any?
end

The keen-eyed among you will notice that this means you can't have, say, your HTML part themed and your plain text part in your main app/views folder. There would be ways around that, but this seemed the cleanest approach to take.

UPDATE (5pm): I've now got layouts working too. More on that over here.