Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Augmenting a model from an external gem

I'm using refinerycms in our site to let the less technical staff update content. Inside the gem, they have a Page class that maps each top level page on the site. I'd like to use the acts_as_taggable gem on this Page class. Now I can add the acts_as_taggle declaration directly to the page.rb file, but then I'd have to maintain a separate git repo to track differences between my version and the official release.

Based on some other questions here on SO I created an initializer and extension like so:

lib/page_extensions.rb:

module Pants
  module Extensions

    module Page
      module ClassMethods
        def add_taggable
          acts_as_taggable
        end
      end

      def self.included(base)
        base.extend(ClassMethods).add_taggable
      end

    end

  end
end

config/initializers/pants.rb

require 'page_extensions'

Page.send :include, Pants::Extensions::Page

app/views/layouts/application.html.erb

...
Tags: <%= @page.tag_list %>

The first time I request a page from the server it correctly outputs all the tags on the page. However, if I hit refresh I instead get a NoMethodError indicating that tag_list is undefined.

I'm new to rails so perhaps my assumptions are wrong, but I expected that call to Page.send would make a permanent change to the Page class rather than to a specific instance of the class. So how would I get the acts_as_taggable added to the Page class on each request?

like image 782
Paul Alexander Avatar asked Oct 25 '10 20:10

Paul Alexander


1 Answers

You will need to put your module_eval code into a config.to_prepare do block. The easiest place to do this is in config/application.rb or to create an engine. The code is identical except it executes every time you run the site not just the first time (which especially applies to development mode) and code which only executes before the initialisation process (aka requiring files) into a config.before_initialize do block.

The reason that config.to_prepare is important is because in development mode, the code is reloaded on every request but initializers generally aren't. This means that Page, which you are running a module_eval on, will only have the module_eval run once but will reload itself every request. config.to_prepare is a Rails hook that runs every single time providing great convenience for situations like this.

config/application.rb approach

class Application < Rails::Application
  # ... other stuff ...

  config.before_initialize do
    require 'page_extensions'
  end

  config.to_prepare do
    Page.send :include, Pants::Extensions::Page
  end
end

Engine approach

If you don't want to modify config/application.rb then you can, in Refinery CMS, create vendor/engines/add_page_extensions/lib/add_page_extensions.rb which would look like this:

require 'refinery'

module Refinery
  module AddPageExtensions
    class Engine < Rails::Engine

      config.before_initialize do
        require 'page_extensions'
      end

      config.to_prepare do
        Page.send :include, Pants::Extensions::Page
      end

    end
  end
end

If you use the engines approach you will also need to create vendor/engines/add_page_extensions/add_page_extensions.gemspec which should contain a simple gemspec:

Gem::Specification.new do |s|
  s.name = 'add_page_extensions'
  s.require_paths = %w(lib)
  s.version = 1.0
  s.files = Dir["lib/**/*"]
end

And then in your Gemfile add this line:

gem 'add_page_extensions', :path => 'vendor/engines'

If you go the engine approach, you will probably want to put all of your logic inside the engine's lib directory including the Pants::Extensions::Page code.

Hope this helps

like image 84
parndt Avatar answered Nov 15 '22 05:11

parndt