Skip to content

Instantly share code, notes, and snippets.

@justinweiss
Last active January 11, 2024 07:28
Show Gist options
  • Save justinweiss/9065666 to your computer and use it in GitHub Desktop.
Save justinweiss/9065666 to your computer and use it in GitHub Desktop.
Filterable
# Call scopes directly from your URL params:
#
# @products = Product.filter(params.slice(:status, :location, :starts_with))
module Filterable
extend ActiveSupport::Concern
module ClassMethods
# Call the class methods with names based on the keys in <tt>filtering_params</tt>
# with their associated values. For example, "{ status: 'delayed' }" would call
# `filter_by_status('delayed')`. Most useful for calling named scopes from
# URL params. Make sure you don't pass stuff directly from the web without
# whitelisting only the params you care about first!
def filter(filtering_params)
results = self.where(nil) # create an anonymous scope
filtering_params.each do |key, value|
results = results.public_send("filter_by_#{key}", value) if value.present?
end
results
end
end
end
@guru28
Copy link

guru28 commented Aug 19, 2016

thanks but i have a doubt that how i can create scope for join table

@elentras
Copy link

Thank for this gist, I use it often, but with some extras :

cattr_accessor :filters

def available_filters(*filters)
  self.filters ||= []
  self.filters += scopes
  self.filters.uniq!
end

And in method def filter(filtering_params) :

  filtering_params = filtering_params.symbolize_keys.slice(*self.filters)

I use symbolise_keys to ensure it will match with the available_filters.
The slice extract only the authorised params defined in models like this :

class Person < ActiveRecord::Base
[...]
  available_scopes :name, :role, :age, :phone_number
[...]
end

And, I use also the role definition on available_filters like this (not handled in my examples) :

  available_scopes :name, :role, :age, :phone_number, as: :admin
  available_scopes :name, :role, as: :user

@PascalPixel
Copy link

PascalPixel commented Apr 4, 2017

For Join table (many to many through)

class Item < ApplicationRecord
  has_many :categorizations
  has_many :categories, through: :categorizations

  scope :category, ->(category) { Item.joins(:categories).where category: category }
  scope :other, ->(other) { where other: other }
end

@zx1986
Copy link

zx1986 commented Jul 1, 2017

Thank you so much!

@markhallen
Copy link

Thank you!

@memoht
Copy link

memoht commented Dec 26, 2018

@justinweiss Just upgraded to Ruby 2.6.0 and started getting an ArgumentError wrong number of arguments (given 1, expected 0) and using the example in your article, it would be this line in the controller @products = Product.filter(params.slice(:status, :location, :starts_with)) that generates the error.

If I go back to the step before you put this into the Filterable module, the error disappears.

    filtering_params(params).each do |key, value|
      @products = @products.public_send(key, value) if value.present?
    end
    def filtering_params(params)
      params.slice(:status, :location, :starts_with)
    end

More context from my app. Given this snippet, post Ruby 2.6.0 upgrade, yields the error where prior there was no issue.

  def index
    @companies = Company.includes(:agency).order(Company.sortable(params[:sort]))
    @companies = @companies.filter(params.slice(:ferret, :geo, :status))
  end

It seems that my first line creates the issue, so the question would be how to preserve my includes and order params on line 1 and keep this working as a module.

With this option it resumes working (includes and order as well as filtering) without me modifying line 1.

  def index
    @companies = Company.includes(:agency).order(Company.sortable(params[:sort]))
    filtering_params(params).each do |key, value|
      @companies = @companies.public_send(key, value) if value.present?
    end
  end

@memoht
Copy link

memoht commented Dec 26, 2018

@justinweiss Been looking at this some more this morning. The following also appears to work. Preserves filtering, preserves includes and order. Doesn't generate Argument Error.

def index
    @companies = Company.filter(params.slice(:ferret, :geo, :status))
    @companies = @companies.includes(:agency).order(Company.sortable(params[:sort]))
end

Any insights?

@memoht
Copy link

memoht commented Dec 27, 2018

Got some advice from @shuriu on this error.

ruby 2.6 added #filter method on enumerables that takes a block. So I’m guessing that’s why you’re getting this issue. More info:

In the first example the filter call was applied to an enumerable, which conflicts with the ruby method. In the second example, the filter call is applied to the Company class which doesn’t conflict with the ruby method, and that’s why it works.

My advice is to rename your filterable class method to something like filter_from_params so it’s: 1) expressing intent more clearly, 2) it reduces the possibility of clashing with other generic methods.

So, I renamed def filter(filtering_params) to def filter_from_params(filtering_params) and all is happy again. Leaving this here for anyone else who runs across this issue.

@rubendinho
Copy link

I ran into this issue also after upgrading to Ruby 2.6. I was able to get around this by renaming my filter method to filter_by, but it's surprising that the definition in the model does not take precedence over the Enumerables alias for select.

@khalilgharbaoui
Copy link

khalilgharbaoui commented May 22, 2019

My very own version:
without the explicit anonymous scope and redundant self's

module Filterable
  extend ActiveSupport::Concern
  class_methods do
    def filter(filter_attributes, params)
      results = all;
      filter_attributes.each do |attribute|
        results = results.send(attribute, params[attribute]) if params[attribute].present?
      end
      results
    end
  end
end

@RameshGhk
Copy link

Hi All,
I would like to know how can I implement this Filterable module in my views. I ma new to Rails..
I have table with 40 plus columns but I would like to apply filters for few columns like :status, :name etc
I should be able to dropdown, select the required status and apply filters

thanks inadvance for your support..

@heratyian
Copy link

I had some issues using this with pundit gem policy scopes. I suggest renaming filter to filter_by. Has the benefit of consistent naming with scopes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment