Adventures with Ruby

Filtering with named scopes

View Comments

Suppose you have an index page with people and you want to have a series of neat filters to show a selection of people. For example only the people still alive of only the adults. How would one do that?

I like the method of using a named_scope and delegating to specified filters. This way, you can structure your filters properly and get clean URLs. Also, you can chain other named scopes to the filter.

This is an example of how I would do that.

The view

In your index view, add a list of all filters:

[sourcecode language='ruby']
%h3= t(:people, :scope => :filter_titles)
%ul
– Person.available_filters.each do |filter|
%li= link_to t(filter, :scope => [:filter_names, :people]), people_path(:filter => filter)
[/sourcecode]

This will generate links that go to your index page (e.g. /people?filter=adults). You can even make a route that will clean up your views even more.

[sourcecode language='ruby']map.connect “/people/filter/:filter”, :controller => “people”, :action => “index”[/sourcecode]

I use i18n to get the displayed link text for each link, so my locale file might look something like:

[sourcecode language='js']
en:
filter_titles:
people: Select a subset
filter_names:
people:
deceased: Select deceased people
alive: Select people that are (still) alive
adults: Select people over 18
[/sourcecode]

The controller

Add the named_scope to your query:

[sourcecode language='ruby']
def index
@people = Person.filter(params[:filter]).paginate(:page => params[:page])
end
[/sourcecode]

The model

Here’s the interesting stuff. Define the available filters as a class method:

[sourcecode language='ruby']
def self.available_filters
[ :deceased, :alive, :adults ]
end
[/sourcecode]

Then, define class methods for each those filters, specifying what they need to do. I like to prepend them with “filter_“, so it shows more intent. You can go crazy with these filter methods if you’d like. Just return valid ActiveRecord find-options.

[sourcecode language='ruby']
def self.filter_deceased
{ :conditions => “deceased_on IS NOT NULL” }
end

def self.filter_alive
{ :conditions => “deceased_on IS NULL” }
end

def self.filter_adults
{ :conditions => ["birthday <= ?", 18.years.ago.to_date] }
end
[/sourcecode]

And finally, add the named scope that uses these filters:

[sourcecode language='ruby']
named_scope :filter, lambda { |f| available_filters.include?(f) ? send(“filter_#{f}”) : {} }
[/sourcecode]

We check to see if the filter is available, excluding any invalid filter. Also, by default no filter is given from the controller. Then params[:filter] will be nil and so it won’t try to call Person.filter_. You can replace the empty hash with a default filter if you like.

Conclusion

These predefined filters can really help the usability of your new fancy web application. And I like the code too, because it looks very clear and it’s easy to test.

Named scopes can get quite messy, certainly if you use a lambda and some logic. Delegating the body of the lambda to a class method is a good idea. Just be sure that the method returns a hash of some sort.

[sourcecode language='ruby']
named_scope :foo, lambda { |*args| foo_parameters(*args) }
[/sourcecode]

You can make this into a named_scope generator even, but I’ll save that for another time and post. Also, stay tuned for the encore: DRYing up the code for re-use!

Written by Iain Hecker

June 12th, 2009 at 4:59 pm

Posted in Uncategorized

  • http://www.moneybird.nl Edwin Vlieg

    Nice article, but I’m wondering if this is the right way to go. The purpose of named_scope is to defined named scopes. Just defining a filter scope which accepts a name for the scope isn’t really the way to go in my opinion.

    You can find my approach on the BlueTools weblog (dutch). As you can see, I’m using a new named_scope for each filter. Only for the advanced filtering in which the user can define the conditions for the filter, I’m using the approach you are taking.

  • http://www.moneybird.nl Edwin Vlieg

    Nice article, but I’m wondering if this is the right way to go. The purpose of named_scope is to defined named scopes. Just defining a filter scope which accepts a name for the scope isn’t really the way to go in my opinion.

    You can find my approach on the BlueTools weblog (dutch). As you can see, I’m using a new named_scope for each filter. Only for the advanced filtering in which the user can define the conditions for the filter, I’m using the approach you are taking.

  • http://iain.nl Iain Hecker

    @Edwin I understand your concern.

    I like the way it works in DataMapper, in which you can chain any method. The query only gets executed once you try to read a record from the set.

    There is a proper use and balance for everything. So with this. I like the flexibility of it, because you can use the “filter_*”-methods in any place.

    And I don’t think controllers is the place to use “send”. I like to keep the more rubyesque methods out of there. But then again, there is no absolute rule for anything.

  • http://iain.nl Iain Hecker

    @Edwin I understand your concern.

    I like the way it works in DataMapper, in which you can chain any method. The query only gets executed once you try to read a record from the set.

    There is a proper use and balance for everything. So with this. I like the flexibility of it, because you can use the “filter_*”-methods in any place.

    And I don’t think controllers is the place to use “send”. I like to keep the more rubyesque methods out of there. But then again, there is no absolute rule for anything.

  • Phil Kursawe

    Just found this blog entry. Nice one. Would it also be possible to combine several selected filters like “:adult, :adults”?

  • Phil Kursawe

    Just found this blog entry. Nice one. Would it also be possible to combine several selected filters like “:adult, :adults”?

blog comments powered by Disqus
Fork me on GitHub