Ain’t Release Early, Release Often wonderful ? I just received a patch from Evgeniy Pirogov that makes the failing test from the AutoScope plugin work.

Evgeniy’s patch is available here, but you should simply svn or piston update to get the latest version, since I eagerly accepted Evgeniy’s patch.

Thank you, Evgeniy for your work.

Well, in relation to my Useful #with_scope technique post, here’s a plugin that implements that idea. This code is used on a production system. It works perfectly for my needs at the moment.

AutoScope

Automatically create scoped access methods on your ActiveRecord models.

Examples

Declare your scopes within your ActiveRecord::Base subclasses.


1 class Contact < ActiveRecord::Base
2 auto_scope \
3 :old => {:find => {:conditions => ["born_on < ?", 30.years.ago]}},
4 :young => {:find => {:conditions => ["born_on > ?", 1.year.ago]}}
5 end
6
7 class Testimonial < ActiveRecord::Base
8 auto_scope \
9 :approved => {
10 :find => {:conditions => ["approved_at < ?", proc {Time.now}]},
11 :create => {:approved_at => proc {Time.now}}},
12 :unapproved => {
13 :find => {:conditions => "approved_at IS NULL"},
14 :create => {:approved_at => nil}}
15 end

These declarations give you access to the following scoped methods:


1 Testimonial.approved.count
2 Testimonial.unapproved.create!(params[:testimonial])
3 young_contacts</span> = <span class="co">Contact</span>.young <span class="no">4</span> <span class="iv">contacts = Contact.old.find(:all, :conditions => ["name LIKE ?", params[:name]])

The plugin’s home page is: http://xlsuite.org/plugins/auto_scope
The plugin’s Subversion repository is: http://svn.xlsuite.org/plugins/auto_scope

I just stumbled upon something very interesting. In my application, we accept anonymous testimonials from the web, but they must not be shown until they have been reviewed and approved by an administrator.

The first API I designed was this:

test/unit/testimonial_test.rb

1 def test_can_get_approved_only
2 testimonials = Testimonial.find(:completed)
3 assert_equal [@approved_testimonial], testimonials
4 end

That worked well enough, even after I started adding code to take care of :all and :first, and modifying #count to also use the same scoping rules.

But then, I hit a snag with scoped #has_many accesses. The following failed:

test/unit/testimonial_test.rb

1 def test_can_count_approved_testimonials_for_a_single_contact
2 testimonials = @contact.testimonials.count(:completed)
3 assert_equal 1, testimonials
4 end

This generated an invalid statement:


1 test_can_count_approved_testimonials_for_a_single_contact(TestimonialTest::AbilityToCountTest):
2 ActiveRecord::StatementInvalid: Mysql::Error: Unknown column ‘approved’ in ‘where clause’: SELECT count(*) AS count_all FROM testimonials WHERE (testimonials.contact_id = 1 AND (approved))

I did a bit of debugging and found that #has_many had already modified the #count parameters even before my #count_with_extensions method was called. So I decided to change my API. I decided I wanted something like this:

test/unit/testimonial_test.rb

1 def test_can_get_approved_only
2 testimonials = Testimonial.completed.find
3 assert_equal [@approved_testimonial], testimonials
4 end

This new API first defines the scope, then calls a normal ActiveRecord::Base method. My first implementation was:

app/models/testimonial.rb

1 def self.completed
2 with_scope(:find => {:conditions => ["approved_at IS NOT NULL"]}) do
3 yield self
4 end
5 end

When I ran my tests, my error was immediately apparent:


1 test_can_get_approved_only(TestimonialTest::AbilityToCountTest):
2 LocalJumpError: no block given

Ooops… So I thought a bit more. What I wanted to return from #completed was an object that could stand in for my class (Testimonial in this case), but for which the scope had been defined. I ended up with this implementation:

app/models/testimonial.rb

1 def completed
2 returning Object.new do |proxy|
3 class << proxy
4 def method_missing(method, *args)
5 Testimonial.with_scope(:find => {:conditions => ["approved_at IS NOT NULL"]},
6 :create => {:approved_at => Time.now}) do
7 Testimonial.send(method, *args)
8 end
9 end
10 end
11 end
12 end

What this implementation does is:

  1. It starts by creating a proxy object;
  2. It redefines #method_missing on that object;
  3. The new #method_missing method creates the scope we originally wanted and calls the method we were originally targetting;
  4. Then the proxy object is returned.

With that implementation, I get something nice:


1 $ rake test:recent
2 (in /home/francois/src)
3 /usr/local/bin/ruby -Ilib:test "/usr/local/lib/ruby/gems/1.8/gems/rake-0.7.2/lib/rake/rake_test_loader.rb" "test/unit/testimonial_test.rb"
4 Started
5 ……………
6 Finished in 3.041738 seconds.
7
8 15 tests, 31 assertions, 0 failures, 0 errors

Search

Your Host

A picture of me

I am François Beausoleil, a Ruby on Rails and Scala developer. During the day, I work on Seevibes, a platform to measure social interactions related to TV shows. At night, I am interested many things. Read my biography.

Top Tags

Books I read and recommend

Links

Projects I work on

Projects I worked on