How to: Advanced Filtering with Ransack and "OR" Groupings

2019-06-26 By Zian Aguirre

A while ago, I was working on a CMS-like project that needed a custom filter for its User model; the filter was supposed to be a select-like component displaying all the available roles and the user should be able to pick more than one role and the filter should pick up any user with the selected roles.

So I decided to Ransack it.

Prep Work

First, I needed a way to get all the roles available, I didn’t want to hardcode them since we keep adding new roles and that could cause some trouble.

All our role columns have a _role suffix, so it's easy to get them this way:

# app/models/user.rb
def self.available_roles
  column_names.select { |column_name| column_name =~ /_role/ }
end

Working on the Filter

Next I needed to create the select used for displaying all the available roles and selecting them for the filter.

# app/views/users/_search.haml
= f.select :with_roles, |
 @available_roles, |
 { include_false: false }, |
 class: 'select2 form-control users-roles-select', |
 'data-placeholder': 'roles', |
 'data-width': '80%', |
 multiple: true

It is worth noting that for this select, I used Select2.

I decided to make a small test selecting two roles (full_time_role and manager_role) and sending the form to check how the params are being sent to the backend.

{“utf8"=>"", "q"=>{ "with_roles"=>["", "full_time_role", "manager_role"]}, "commit"=>"Search", "controller"=>"users", "action"=>"index”}

Nice! It was time to move to the good stuff: creating the scope and using Ransack’s groupings. Adding the custom scope and the "OR" grouping

Adding the Custom Scope and the "OR" Grouping

I added a custom scope to the User model, called it with_roles to be used like User.with_roles([array, of, roles]). Pretty nice, huh?

Since I wanted to pass an Array of arguments as *args to the ActiveRecord scope, I needed to make sure whatever it receives is an actual role defined in the model so I placed a guard condition to return an empty array if nothing matches any role. It is important the name of the scope matches with the symbol used for the select.

# app/models/user.rb
scope :with_roles, -> (*args) {
  roles = args.flatten.select { |role| available_roles.include?(role) }
  return [] if roles.empty?
}

Ransack expects a Hash with the keys being the matchers and the values wanted to filter with. For this case in particular I used the *_eq (equal) matcher. Now I needed to build our search conditions Hash for each role.

# app/models/user.rb
scope :with_roles, -> (*args) {
  roles = args.flatten.select { |role| available_roles.include?(role) }
  return [] if roles.empty?

  search_conditions = {}
  roles.each { |role| search_conditions["#{role}_eq"] = true }
}

That set search_conditions to have contents like {“full_time_role_eq” => true, “manager_role_eq” => true}. Then I passed this to Ransack and the result was a SQL query that looked up users that had both roles.

SELECT "users".* FROM "users" WHERE ('t'='t') AND ("users"."full_time_role" = 't' AND "users"."manager_role" = 't')

However, that’s not what I wanted which was to get any user that had one role or the other. Ransack by default groups the conditions with AND, so if I wanted the conditions to be grouped with an OR instead I just needed to merge m: 'or' in the search_conditions query hash. This is how the scope ended up looking:

# app/models/user.rb
scope :with_roles, -> (*args) {
  roles = args.flatten.select { |role| available_roles.include?(role) }
  return [] if roles.empty?

  search_conditions = {}
  roles.each { |role| search_conditions["#{role}_eq"] = true }

  search(search_conditions.merge(m: 'or')).result
}

One last and important step is to add this new scope as a Ransackable Scope, otherwise it won’t work properly with our form. For that I just needed to define the following method in the User model.

# app/models/user.rb
def self.ransackable_scopes(auth_object = nil)
  # if you add more ransackable scopes,
  # just add the names here as a symbol
  [:with_roles]
end

And that’s it.

The groupings can also be changed in the controllers too, as shown in the documentation:

def index
  @q = Artist.ransack(params[:q].try(:merge, m: 'or'))
  @artists = @q.result
end

Closing Thoughts

Ransack is a versatile gem that allows you to build simple yet flexible search forms with little effort. By using Advanced Filtering and Custom Groupings in Ransack you can build complex search queries on the fly in an elegant manner. It’s because of things like these is that I like Ransack.

So what do you think about these features? Do you see yourself using these Ransack features?

Let me know in the comments!

Follow us

Copyright © 2019 Density Labs LLC. All Rights Reserved