Overriding a module method from a gem in Rails
Asked Answered
G

3

61

The will_paginate gem is broken on my version of Oracle. The default paginate_by_sql method in the WillPaginate module is inserting an extra 'AS' into a query and causing it to fail.

The code itself is easily fixed, but I'm not sure of the best way to get Rails to pick up my change.

I don't want to change the code in the gem itself, as that will leave my code broken on other machines.

I tried creating an lib/test.rb file containing:

module WillPaginate
  def paginate_by_sql
    (my code goes here)
  end
end

and requiring it from environment.rb, but it's not picking up my changes. I also tried requiring it from controllers/application.rb, but again, not picking up my changes.

Temporarily, I got it to work by overriding the method within the specific model itself, but this is a bit of a hack, and means I can't use it on any of the other models in this project.

I'm sure there's an easy way to do this, but I'm not having any luck tracking it down using Google.

Greasepaint answered 24/2, 2009 at 3:4 Comment(2)
If the gem code's broken, surely it's broken everywhere? Have you logged a bug with the project? wiki.github.com/mislav/will_paginate/report-bugsNitrite
Not yet - the bug is in a block handling oracle specific cases. The cause is because I'm using the oracleenhanced (rather than the oracle or oci) adapter. I think most ppl are using the oracle adaptor and wouldn't be running into this) Will be logging bug shortly anyway.Greasepaint
P
32

What you are doing will work, but your code needs to look like this:

module WillPaginate
  module Finder
    module ClassMethods
      def paginate_by_sql(sql, options)
        # your code here
      end
    end
  end
end

In other words, go into finder.rb, delete everything except the module headers and the method you want to override, then save to a file in lib and include in environment.rb. Voila, instant monkey patch!

Pease answered 24/2, 2009 at 4:44 Comment(1)
this should be in in initializer config/initializers - do not put this in the environment.rb file!Aramanta
S
82

A more concise solution:

WillPaginate::Finder::ClassMethods.module_eval do
 def paginate_by_sql sql, options
   # Your code here
 end
end

Put the the code into an initializer file in config/initializers. This is the correct place to put code that needs to be run when the environment is loaded. It also better organises your code, making each file's intent clearer, thus bugs will be easier to track down. Do not clutter up environment.rb!

Siam answered 5/12, 2009 at 15:14 Comment(3)
Interesting corner case if the underlying Class is loaded via autoload : #8736951Aramanta
Pretty cool. In the new paginate_by_sql, can I access its former version? Can I call something like super(sql, options)?Dickman
What is the reason that override gem method code must be in the initializers folder? I tried putting it somewhere else and requiring the file and it didn't work. If I'm not mistaken, what I did would work in a regular Ruby app, but for some reason not in a Rails app...Overcapitalize
B
66

Ok, I'm just going to make this easier for people like myself who come along and still struggle a bit after reading the other answers.

First find the code that you want to change on the github repo by searching for the line of code (you could easily find this using pry) you want to change in the gem, and then selecting Code on the left instead of Issues

enter image description here

enter image description here

Next Copy the content of the module you want to change and place it into an aptly named .rb file inside of your config/initializers folder. Here is an example:

module Forem
  module TopicsHelper
    def link_to_latest_post(post)
      text = "#{time_ago_in_words(post.created_at)} #{t("ago_by")} #{post.user}"
      link_to text, forum_topic_path(post.topic.forum, post.topic, :anchor => "post-#{post.id}")
    end
  end
end

Now, change it to:

Forem::TopicsHelper.module_eval do
  def link_to_latest_post(post)
    text = "#{time_ago_in_words(post.created_at)} #{t("ago_by")} #{post.user}"
    link_to text, forum_topic_path(post.topic.forum, post.topic, :anchor => "post-#{post.id}")
  end
end

Now, make any additional changes to the code and restart your server.

Away you go!

Bergh answered 30/1, 2014 at 1:10 Comment(2)
Would you be able to call the original method like this or do you have alias_method_chain (in Rails anyway) for that?Hughey
@Abram, What if we have structure like this: github.com/algolia/algoliasearch-client-ruby/blob/master/lib/… I want to override only initialize method. In the previous version algolia gem, it was accepting only argument. But in the latest version accepting 3-4 arguments. Since my project was large and used many places so i dont want to change in every file. So i want to override the initialize file that accepts one argument and then call the super.Liberia
P
32

What you are doing will work, but your code needs to look like this:

module WillPaginate
  module Finder
    module ClassMethods
      def paginate_by_sql(sql, options)
        # your code here
      end
    end
  end
end

In other words, go into finder.rb, delete everything except the module headers and the method you want to override, then save to a file in lib and include in environment.rb. Voila, instant monkey patch!

Pease answered 24/2, 2009 at 4:44 Comment(1)
this should be in in initializer config/initializers - do not put this in the environment.rb file!Aramanta

© 2022 - 2024 — McMap. All rights reserved.