July 19, 2007

Advanced Radiant extensions

Filed under: engines, rails — Casper Fabricius @ 6:29 pm

Radiant is probably the best Ruby on Rails content management system (CMS) available at the moment. I’m using it in several customer projects, and while Radiant is still quite limited and aims to stay that way, it is quite easy to extend and blend with ordinary Rails resources through its pendant to engines; extensions.

Like engines, extensions allows developers to share not just “plain” Ruby code between applications (which is done through plugins), but also to share controllers, views and routes between them, as well as maintaining separate migration versions for each extension. One difference between engines and extensions I’ve spotted, is that while engines allows the developer to override and add individual actions in controllers, extensions allow only for replacing an entire controller.

Recently, John W. Long, creator of Radiant, wrote a nice tutorial on the basics of extensions, so if you haven’t had a look at that, I’d recommend it before proceeding with this article. The rest of this text puts focus on how to alter the behavior of Radiant to suit your needs by developing an extension.

I’ll use one of my own extensions, the first to be published as open-source; File Based Layout, as an example on how to override, modify and add functionality to the core of Radiant. I developed this extension to bridge a critical gap between “static” Radiant pages and “dynamic” Rails pages; the non-existent ability to let them share the same layout. To achieve this, I had to alter the Layout model and the Site- and LayoutControllers. This is possible through the dynamic nature of Ruby, but it requires some knowledge about “metaprogramming” which I for one didn’t have before I started to develop extensions.

Take a look at file_based_layout_extension.rb. The activate method, invoked by Radiant at the proper time, includes code in the Layout model and the Site- and LayoutControllers located in modules placed in the lib directory of the extension:

  def activate
    Layout.send :include, FileBasedLayout::LayoutExt
    SiteController.send :include, FileBasedLayout::SiteControllerExt
    Admin::LayoutController.send :include, FileBasedLayout::LayoutControllerExt
  end

As other extensions might also want to extend models and controllers using similar module names, I have opted to namespace the modules with FileBasedLayout. Also, to make the three modules available to file_based_layout_extension.rb, they have to be required using require_dependency as I have in the top of the file.

To be able to store the name of a physical layout file with the Radiant Layout, I had to add a field the layouts table; layout_file. This is done by adding a migration file to the db/migrate directory, and running the command rake db:migrate:extensions. I also wanted to validate the length of this field, and add a convenience method to the Layout model called file_based? (can you guess what it does?). This is done in the self.included method in layout_ext.rb by invoking base.class_eval with a block of code to be added to the class:

    def self.included(base)
      base.class_eval {
        validates_length_of :layout_file, :allow_nil => true, :maximum => 100, :message => '%d-character limit'
        include InstanceMethods
      }
    end

    module InstanceMethods
      def file_based?
        ! layout_file.nil? && ! layout_file.empty?
      end
    end

This is pretty simple, as we are merely adding stuff to the Layout class, but if what if we wanted to override stuff of, say, the SiteController? To create the File Based Layout extension, I needed to override some pretty low-level stuff in Radiant, in particular the show_uncached_page method, which is invoked when Radiant gets a request for a page not placed in the cache. It’s not enough to just include a module with this method, as I do in layout_controller_ext.rb, as methods in the class takes precedence over included methods.

Instead, I had to place my new show_uncached_page method directly inside the block given to base.class_eval in site_controller_ext.rb. This way, the new method is, so to speak, placed last in the SiteController class, overriding previous definitions of the method:

def self.included(base)
  base.class_eval {
    private

    def show_uncached_page(url)
      @page = find_page(url)
      unless @page.nil?
        if @page.layout && @page.layout.file_based?
          render :text => process_with_file_based_layout(@page), :layout => File.basename(@page.layout.layout_file, File.extname(@page.layout.layout_file))
        else
		...
   end

   ...

  }
end

And that’s it, really. When extending and changing Radiant or other Rails applications, the include, self.included and base.class_eval methods are your friends. There are, of course, performance penalties connected with especially class_eval, but as Radiant pages are cached, we don’t have to care too much about them.

1 Comment

  1. This is pretty cool, i’ve been googleling (is that a word) for something like this.

    Comment by Tom — August 1, 2007 @ 4:18 pm

RSS feed for comments on this post.

Sorry, the comment form is closed at this time.