Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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 the same name as the keys in <tt>filtering_params</tt>
# with their associated values. 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(key, value) if value.present?
end
results
end
end
end
@steveyken

This comment has been minimized.

Copy link

commented Feb 22, 2014

Thanks this is a really useful pattern. Bookmarked!

@skateinmars

This comment has been minimized.

Copy link

commented Feb 25, 2014

This could be seriously dangerous if you don't whitelist the params in the controller:

params = {destroy: 1}

Product.filter(params)
@rafeeqskr

This comment has been minimized.

Copy link

commented Sep 17, 2014

@skateinmars If you look at the original blog. we only send that params that were sliced( filtered )
http://www.justinweiss.com/blog/2014/02/17/search-and-filter-rails-models-without-bloating-your-controller/

@jameskerr

This comment has been minimized.

Copy link

commented Sep 19, 2014

This is a pretty nice way of doing this sort of thing. @justinweiss, what would you do if you needed to do a bit of preprocessing on the params? For example, lets say I want to filter a Post model by Tags. My query string is /posts?tag_ids=10,11,12 Would you put a method in the scope? in the model? in a model concern? What if I need to do this same operation on several models? Interested in hearing your ideas! Thanks!

@thetrung

This comment has been minimized.

Copy link

commented Feb 13, 2015

Why don't you make a gem 😄 so we can include in Gemfile ?
Thanks anyway !!!

@nicolas-besnard

This comment has been minimized.

Copy link

commented Apr 8, 2015

I think present? isn't the right method to chose. You can't do /hashtags?parent_id=nil.

@maebeale

This comment has been minimized.

Copy link

commented Oct 2, 2015

thanks @justinweiss! this is awesome!!! hope all is well w you.

@mbchandar

This comment has been minimized.

Copy link

commented Jan 13, 2016

can this be included in the base class? which will then inherit ?

@krokrob

This comment has been minimized.

Copy link

commented Feb 16, 2016

Hi @justinweiss! And thanks for the gist 😄
I try it and something went wrong line 16, I can't call the method 'key' (named 'orientation') on my results which is an array of instances of class Product. I do not understand of does that could work.

@asory

This comment has been minimized.

Copy link

commented Mar 27, 2016

work in rails 4 ??
sorry how used in view?
i really want used this is simple and useful

@gusridd

This comment has been minimized.

Copy link

commented Apr 11, 2016

Thanks for your code it worked for me right from the gist. Nonetheless, I've created a scope with 'joins' so the results could appear more than once. In your opinion, Should I include a 'distinct' in that scope only, or should that behavior be included globally in the filter method as in https://gist.github.com/gusridd/9e80f763bae56a425aff310eeef0ae0f ?

@cyb-ahmadh

This comment has been minimized.

Copy link

commented Jun 9, 2016

That helped a lot. Thanks :) 👍

@guru28

This comment has been minimized.

Copy link

commented Aug 19, 2016

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

@elentras

This comment has been minimized.

Copy link

commented Oct 27, 2016

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
@Superpencil

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

commented Jul 1, 2017

Thank you so much!

@markhallen

This comment has been minimized.

Copy link

commented May 9, 2018

Thank you!

@memoht

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

commented Feb 1, 2019

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

commented Aug 7, 2019

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..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.