The Artima Developer Community
Sponsored Link

Ruby Buzz Forum
New Rails plugin: before_assignment

0 replies on 1 page.

Welcome Guest
  Sign In

Go back to the topic listing  Back to Topic List Click to reply to this topic  Reply to this Topic Click to search messages in this forum  Search Forum Click for a threaded view of the topic  Threaded View   
Previous Topic   Next Topic
Flat View: This topic has 0 replies on 1 page
Obie Fernandez

Posts: 608
Nickname: obie
Registered: Aug, 2005

Obie Fernandez is a Technologist for ThoughtWorks
New Rails plugin: before_assignment Posted: Dec 12, 2006 9:07 PM
Reply to this message Reply

This post originated from an RSS feed registered with Ruby Buzz by Obie Fernandez.
Original Post: New Rails plugin: before_assignment
Feed Title: Obie On Rails (Has It Been 9 Years Already?)
Feed URL: http://jroller.com/obie/feed/entries/rss
Feed Description: Obie Fernandez talks about life as a technologist, mostly as ramblings about software development and consulting. Nowadays it's pretty much all about Ruby and Ruby on Rails.
Latest Ruby Buzz Posts
Latest Ruby Buzz Posts by Obie Fernandez
Latest Posts From Obie On Rails (Has It Been 9 Years Already?)

Advertisement

I was writing about the belongs_to association in Rails today when I ran into a limitation of ActiveRecord that I am not happy with. To make a long story short, there is no way to make the :conditions option of a relationship apply on assignment, which disturbs my desire to fail fast.

If you know exactly what I'm talking about then checkout the plugin I wrote as a result: http://obiefernandez.com/svn/plugins/before_assignment/

If you want an explanation, then keep reading...

For example, take the following model (simplified):

  class Timesheet < ActiveRecord::Base
    belongs_to :approver,
               :class_name => 'User',
               :conditions => ['authorized = ?', true]

    ...
  end
  

The conditions placed on the association apply only on reads! Yes most people probably know that already. However, I was on a pretty good flow, doing TDD and I wrote the following set of tests that express my intention of how :conditions should work.

class TimesheetTest < Test::Unit::TestCase
  fixtures :users

  def test_only_authorized_user_may_be_associated_as_approver
    sheet = Timesheet.create
    sheet.approver = users(:approver)
    assert_not_nil sheet.approver, "approver assignment failed"
  end

  def test_non_authorized_user_cannot_be_associated_as_approver
    sheet = Timesheet.create
    sheet.approver = users(:joe)
    assert sheet.approver.nil?, "approver assignment should have failed"
  end

Please don't get hung up on the incorrectness of just ignoring a variable assignment just yet, because it was just a first cut and of course, it failed. "Ahh", I think to myself... "Rails will happily let me assign that non-authorized approver despite my conditions to the contrary and when I look for it later it won't be there."

Here is the revised test which captures my intention and the way that Rails actually works. Notice that I save the sheet and reload the association, to make sure that the assignment definitely did or did not 'stick':

  def test_only_authorized_user_may_be_associated_as_approver
    sheet = Timesheet.create
    sheet.approver = users(:approver)
    assert sheet.save
    assert_not_nil sheet.approver(true), "approver assignment failed"
  end

  def test_non_authorized_user_cannot_be_associated_as_approver
    sheet = Timesheet.create
    sheet.approver = users(:joe)
    assert sheet.save
    assert sheet.approver(true).nil?, "approver assignment should have failed"
  end

I'm not particularly happy about that. What I actually want is something like this:

  def test_non_authorized_user_cannot_be_associated_as_approver
    sheet = Timesheet.create
    begin
      sheet.approver = users(:non_approver)
      fail "approver assignment should have failed"
    rescue ConditionsViolatedException
      # expected
    end
  end

If a bug or malicious user causes an assignment to take place, I want to know about it right away with a big error. This is not a task to be left to validation -- which IMHO is for expected error states that are okay to handle in cooperation with the user, like with error messages and the like... No, this is a big fat bug or security hole and I want serious consequences, like an Exception raised.

So, the question became how best to make that test pass. I poked around associations.rb and hacked in a line to check for the existence of a before_assignment method and invoke it as a callback if it is there. My first attempt to implement the callback failed because (to my surprise), belongs_to does not take an extension block like the array associations (has_many, habtm). In other words, the following code does NOT work, although I think it should:

class Timesheet < ActiveRecord::Base
  belongs_to :approver,
             :class_name => 'User',
             :foreign_key => 'approver_id',
             :conditions => ['authorized = ?', true] do
  
    def before_assignment(approver)
      raise ConditionsViolatedException unless approver.authorized
    end
  
  end
end

However, using the :extend option DOES work, although it isn't documented for belongs_to!

After much discussion in #caboose and talking with Jeremy of the Rails core-team I decided to turn my little hack into a real plugin, giving you the ability to add a before_assignment callback. The end solution (after installing the plugin) looks like this:

class Timesheet < ActiveRecord::Base
  belongs_to :approver,
             :class_name => 'User',
             :foreign_key => 'approver_id',
             :conditions => ['authorized_approver = ?', true],
             :extend => CheckApproverExtension  
end

module CheckApproverExtension
  def before_assignment(approver)
    raise UnauthorizedApproverException unless approver.authorized_approver
  end
end

Type script/plugin install http://obiefernandez.com/svn/plugins/before_assignment to give it a spin.

I want to do a little more work on this plugin in the future, such as figuring out if the same sort of thing makes sense on the other side of the belongs_to relationship. I'm also thinking of a plugin that DRYs the whole thing up by actually using the :conditions in the way I've described above -- why shouldn't they be used to check the validity of the assignment automatically? Of course, that's a slightly bigger task.

Read: New Rails plugin: before_assignment

Topic: Mongrel Comet Update Previous Topic   Next Topic Topic: Infinite loop while configuring SVK on Mac OS X

Sponsored Links



Google
  Web Artima.com   

Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use