6

I'm trying to extend an ActiveRecord model (Vote) that a gem (https://github.com/peteonrails/vote_fu) provides to my application. (I.e., there is no vote.rb in app/models)

My first approach was to create a file called lib/extend_vote.rb that contains the code:

Vote.class_eval do
  after_create :create_activity_stream_event
  has_one :activity_stream_event

  def create_activity_stream_event
    # something..
  end
end

This works when the first vote is created, but when I try to create each subsequent vote I get the error TypeError (can't dup NilClass).

I think this error is caused by the fact that the Vote class is reloaded automatically after every request, but the code in lib/extend_vote.rb is loaded only once when the server starts and this causes the has_one :activity_stream_event association to behave weirdly. (Also, the problem goes away if I set config.cache_classes = true in development.rb)

To solve this problem, I tried to make the vote extensions reload on every request by adding a to_prepare block to my development.rb:

config.to_prepare do
  load 'extend_vote.rb'
end

This solves the (can't dup NilClass) problem, but now whenever I create a new vote, the create_activity_stream_event callback gets called an additional time. I.e., the first vote calls it once, the second calls it twice, etc, etc. So it seems like the to_prepare block is reloading the extension TOO aggressively and adding duplicate callbacks.

What's the best way to add methods and callbacks to this Vote model?

Tom Lehman
  • 85,973
  • 71
  • 200
  • 272
  • 1
    Does it work if you just use `class Vote` instead of `Vote.class_eval`? One thing you could do as well is edit the code in the gem itself, and just use your modified version. – agmcleod May 25 '12 at 21:22
  • `class Vote` behaves the same as `Vote.class_eval` – neither work. I guess I could modify the gem, but I really really don't want to lol. What a mess! – Tom Lehman May 31 '12 at 22:55
  • Why do you think than the Vote class is reloaded? In the resposity, the class is in the lib directory so it's the same than you... – Dougui Jun 01 '12 at 13:57

6 Answers6

6

[UPDATE: should be the right solution to prevent the module being include several times in the same class]

I believe you can use ActiveSupport::Concern to prevent the module being include several times which result by callback called several time. See the example below :

module VotePatch
  extend ActiveSupport::Concern

  included do
    after_create :create_activity_stream_event
    has_one :activity_stream_event
  end

  module InstanceMethods
    def create_activity_stream_event
      #your code here
    end  
  end

end

Vote.send(:include, VotePatch)
NM Pennypacker
  • 6,704
  • 11
  • 36
  • 38
Adrien Coquio
  • 4,870
  • 2
  • 24
  • 37
4

A word of caution: this is a very old gem (last commit is 3 years old) and by the looks of it won't work with rails 3.x as is. In Rails 3.x engines makes this sort of stuff way easier.

As I understand it the problem in the first case is not that the vote model gets reloaded (it shouldn't) but that the activity_stream_event model is reloaded. Because the vote model isn't reloaded the association is left hanging onto the version of the activity_stream_event class from before the reload. Since rails guts out classes before they get reloaded, this causes problems.

With this in mine, try this hack:

#in config/initializers/abstract_vote.rb
AbstractVote = Vote
AbstractVote.abstract_class = true
Object.send :remove_const, :Vote

#in app/models/vote.rb

class Vote < AbstractVote
  after_create :create_activity_stream_event
  has_one :activity_stream_event

  def create_activity_stream_event
  end
end

What this does is allow you to have your own Vote class that inherits from the one in the gem.

But again, I urge you to find something more up to date or roll your own (the gem is only ~250 lines of ruby)

Frederick Cheung
  • 83,189
  • 8
  • 152
  • 174
2

I'd try what agmcleod suggested in the comments but instead of putting it in lib, put it in config/initializers/vote.rb:

 class Vote
   after_create :create_activity_stream_event
   has_one :activity_stream_event

   def create_activity_stream_event
   # something..
   end
 end

Of course, you could fork the gem, make your modifications and link to your forked version in your Gemfile (that's my preference).

miked
  • 4,489
  • 1
  • 17
  • 18
  • why `config/initializers` instead of `lib`? I assume I need to keep the `load` statement in the `to_prepare` block? – Tom Lehman May 26 '12 at 15:27
  • nope shouldn't need that load statement since the items in the initializer files are loaded once at startup for all environments. – miked May 26 '12 at 22:04
  • This doesn't work – if I leave the `load` in the `to_prepare` block, I get the same error I got when the file was in `lib`. If I REMOVE the `load`, then I get the `(can't dup NilClass)` error – Tom Lehman May 31 '12 at 22:56
  • Seriously did you actually down vote me for that suggestion?!! Wow. Sorry for the attempt to help but this works for me just fine with EVERY gem I've ever monkey patched like this. You might also try the other suggestion of forking the gem - but not if you're just going to penalize me for it...geesh. – miked Jun 03 '12 at 03:01
1

Adrien Coquio has the right idea with ActiveSupport::Concerns, which are the Rails way to extend models. His code will work, and you should use it.

However this will not work all the time in development because when Rails reloads your classes when a file changes, it will not re-evaulate the #send line. The only solution I could find was attaching to an ActionDispatch callback in production to ensure the file is re-required after every page load:

if Rails.env.development?
  ActionDispatch::Callbacks.to_prepare do
    require_dependency "../../lib/vote_fu_extensions"
  end
end

In production, or if you set cache_classes to true in your config, you won't need to do this.

Community
  • 1
  • 1
Jalada
  • 298
  • 2
  • 7
0

could you try something like this :

class Vote
    after_create :create_activity_stream_event
    has_one :activity_stream_event

    def create_activity_stream_event
        # something..
    end
end

I think than it will add your function and call functions "after_create" and "has_hone".

BenMorel
  • 34,448
  • 50
  • 182
  • 322
Dougui
  • 7,142
  • 7
  • 52
  • 87
  • Sorry, I don't seen the agmcleod's comment. It's probably the same think... It should work but it doesn't. – Dougui Jun 01 '12 at 14:31
  • heh, at least you didn't get a down vote like I did...and our suggestions were, well, the same! lol – miked Jun 04 '12 at 19:33
  • This is may be because you say to put it in a initializer. Or, may be, I say it after you. – Dougui Jun 04 '12 at 19:47
0

Your issue might be due to the fact that you are monkey patching the class. When rails tries to reload the constants it is not considering your file.

Try to use module technique as given below.

Add a file called lib/vote_fu_extension.rb

module VoteFuExtension
  def self.included(base)
    base.has_one :activity_stream_event
    base.after_create :create_activity_stream_event
  end
  def create_activity_stream_event
    # something..
  end  
end
Vote.send(:include, VoteFuExtension)

Add an initializer called config/initializers/vote_fu.rb

require "vote_fu_extension"

Note

If you want to add class methods to the Vote model refer to this answer.

Shameless plug: My fork of the vote_fu gem has some new features and enhancements.

Community
  • 1
  • 1
Harish Shetty
  • 64,083
  • 21
  • 152
  • 198