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
@steveyken
Copy link

Thanks this is a really useful pattern. Bookmarked!

@skateinmars
Copy link

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

params = {destroy: 1}

Product.filter(params)

@rafeeqskr
Copy link

@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
Copy link

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
Copy link

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

@nicolas-besnard
Copy link

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

@maebeale
Copy link

maebeale commented Oct 2, 2015

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

@mbchandar
Copy link

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

@krokrob
Copy link

krokrob 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
Copy link

asory commented Mar 27, 2016

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

@gusridd
Copy link

gusridd 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
Copy link

That helped a lot. Thanks :) 👍

@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