Simplify ActiveRecord Aggregates and Other Goodies via named_scope

We’ve been huge fans of named_scope since its introduction in Rails 2.1. If you are new to the named_scope concept, please read Ryan Daigle’s intro to named_scope and watch Ryan Bates’ named_scope RailsCast. Ryan Daigle also had a good follow up article explaining his utility scope gem.

With a recent project, we really started utilizing extensions with named_scope which is very powerful and cleaned up our code considerably.

For example, let’s say you have an issue tracking system that has an Entry model that belongs_to both a User model and a Project model. Using named_scope, we can easily access Entries for a date range filtered to both the Project and User level using named_scope lambdas:

<code class='ruby'>
class Entry < ActiveRecord::Base
    named_scope :by_date, lambda { |*args| 
     {
     :order => "starts_at asc", 
     :conditions => ["starts_at BETWEEN ? AND ?", args[0].beginning_of_day.to_s(:db), (args[1]||Time.now).end_of_day.to_s(:db)] 
     } }
end

To find the Entries for the last week for the first user, we would simply do:

<code class='ruby'>u = User.first
u.entries.by_date(1.week.ago, Time.now)

To find the Entries for the last week for the first project, we would simply do:

<code class='ruby'>p = Project.first
p.entries.by_date(1.week.ago, Time.now)

What happens if we want to see the sum of hours worked for a date range? This is where extensions come in very handy. Let’s add a “total_duration” extension to our named_scope:

<code class='ruby'>
class Entry < ActiveRecord::Base
    named_scope :by_date, lambda { |*args| 
     {
     :order => "starts_at asc", 
     :conditions => ["starts_at BETWEEN ? AND ?", args[0].beginning_of_day.to_s(:db), (args[1]||Time.now).end_of_day.to_s(:db)] 
     } } do    

        def total_duration   
          self.sum(:duration)
        end
    end
end

Now to get the duration for the past week for the first user we simply do:

<code class='ruby'>u = User.first
u.entries.by_date(1.week.ago, Time.now).total_duration

with similar logic at the project level.

To group all of the entries by project, we would do:

<code class='ruby'>
class Entry < ActiveRecord::Base
    named_scope :by_date, lambda { |*args| 
     {
     :order => "starts_at asc", 
     :conditions => ["starts_at BETWEEN ? AND ?", args[0].beginning_of_day.to_s(:db), (args[1]||Time.now).end_of_day.to_s(:db)] 
     } } do
        def duration_by_project
          self.sum(:duration, :group => :project)
        end
    end
end

If you need the lambda arguments inside your named_scope extension, you will need to use “self.proxy_options”. For example, let’s say you need the from and to dates in order to calculate the average duration per weekday. That code would look something like this:

<code class='ruby'>
class Entry < ActiveRecord::Base
    named_scope :by_date, lambda { |*args| 
     {
     :order => "starts_at asc", 
     :conditions => ["starts_at BETWEEN ? AND ?", args[0].beginning_of_day.to_s(:db), (args[1]||Time.now).end_of_day.to_s(:db)] 
     } } do
        def duration_by_project_per_weekday        
            from_date = DateTime.parse(self.proxy_options[:conditions][1])
            to_date = DateTime.parse(self.proxy_options[:conditions][2])
            weekday_count = weekday_count(from_date, to_date)
            totals = self.sum(:duration, :group => :project)
            totals.map {|a| [a[0].display_name, ( a[1].to_f / weekday_count)]}
        end
    end
end

In the above example, “weekday_count” is a custom method we created to calculate the number of weekdays between 2 given dates.

The opportunities to simplify code using named_scope is great. As I was writing this post, one of our team members, Jason Derrett, created a named_scope to wrap a Xapian full text search into a Lesson model:

<code class='ruby'>
class Lesson < ActiveRecord::Base
    named_scope :find_with_xapian, lambda { |*args| {:conditions => ["lessons.id in (?)", ActsAsXapian::Search.new([Lesson], args.to_s).results.collect{|x| x[:model].id}]}}
end

In this example, using Xapian to search for a keyword becomes as simple as:

<code class='ruby'>Lesson.find_with_xapian "fishing"

How cool is that?!?

Are there any other cool named_scope tricks out there that any of you are using?