Patching Models for Ransack 4.0.0: Extending ActiveRecord for Gem Compatibility

Collin Jilbert

August 22, 2023

Here at GoRails, we are running ActiveAdmin to power our administration area and we recently upgraded the version which we were running to version 3.0.0. The ActiveAdmin engine has a dependency on Ransack and along with upgrading to the latest version of ActiveAdmin that brought along an upgrade of Ransack to version 4.0.0.

Going up a major version usually introduces breaking changes and sure enough this was our experience.

As noted in the Ransack CHANGELOG for the version 4.0.0 release there is indeed a breaking change introduced:

Require explicit allowlisting of attributes and associations

So with this change indicating that we needed to explicitly denote which attributes and associations we want to allow ransack to search the thought process started. Previously, we had the ability to search all attributes of the models we pull into our admin area so while Ransack provided helpful error messages of how to implement the needed methods to explicitly list the attributes, we wanted to continue to retain the ability to search all attributes/associations used in the admin area. So what now?

Initially, my approach was to create a module which I could extend on the necessary models which would implement the two methods noted in the Ransack error message in the browser while running in development. The body of the methods would return an array of the attributes and associations for each model leverage some Rails methods to achieve this result. An example (but not exact) implementation of this can be seen below:

module RansackSearchable
  def ransackable_attributes(auth_object = nil)
    attribute_names
  end

  def ransackable_associations(auth_object = nil)
    reflect_on_all_associations.map { |assoc| assoc.name.to_s }
  end
end

The initial implementation did resolve a lot of the issues encountered in most of the admin panels for most models... but not all.

The next issue was the fact that in our admin area we manage our tags (via ActsAsTaggableOn) for resources like this blog post, our series, and more. Additionally, We have an admin section for the Ahoy gem. These gems also needed to be extended with the above module. But how would we achieve this without re-opening the various class/modules?

The first thought was to switch from extending the module on the models used in the admin area and instead extend ApplicationRecord itself in the hopes of extending the ActsAsTaggableOn and Ahoy gems in the process.

Unfortunately, this didn't work as expected. After diving into the source code for the two gems, it turns out the reason this didn't happen as expected is because the ActsAsTaggableOn and Ahoy gems (engines/plugins) don't inherit from ApplicationRecord but instead they inherit from ActiveRecord::Base. So with this knowledge in place it was back to the drawing board.

Before taking another pass at implementing a fix, I decided to stop by the ActiveAdmin repository to check the issues and discussions to see if there was any talk of this issue. Sure enough, there was a great discussion that also had a some insight into some methods available from Ransack to grab the attribute names and associations in a cleaner way.

Now it was all starting to come together nicely, but there was still the issue of how to patch the gems.

Knowing that we needed to drop down a level from ApplicationRecord and instead extend ActiveRecord::Base I started to think about where a good place would be do the extending. Thinking about this more, it seemed that we would want to extend ActiveRecord::Base once it is loaded in our Rails application, that way the module code will be placed in the inheritance hierarchy. So creating an initializer to do this seemed like an ideal place for the code.

Since the module consisted of only two methods, I ultimately decided to define the module in the opening line of the initializer and then outside the module definition, and then setup an on_load hook for :active_record and inside the block passed to the hook, perform the extending.

Below is the final implementation that lead to a successful patch to resolve the requirement of the breaking Ransack v4.0.0 change.

# config/initializers/ransack_patch.rb

module RansackPatch
  def ransackable_attributes(auth_object = nil)
    authorizable_ransackable_attributes
  end

  def ransackable_associations(auth_object = nil)
    # The method below calls the exact code I had in my initial implementation which is: reflect_on_all_associations.map { |a| a.name.to_s }
    authorizable_ransackable_associations
  end
end

ActiveSupport.on_load(:active_record) do
  extend RansackPatch
end

P.S. You might enjoy following me on Twitter.


Comments