hacking at associations in role-based systems

Courtenay : November 19th, 2006

Your application, ContrivedExample (beta!), has a variety of roles, one being administrator. Your projects are associated with one user (the owner of the project). Users can only view their own projects; however, the admin can view all projects. You want to use the simplest interface possible, the standard has_many association, so you can paginate, and keep the logic in your models. First, a test case

class UserTest < Test::Unit::TestCase

  def test_user_project_association_exists
    user = User.find(5)
    assert user.respond_to?(:projects) 
  end
  
  def test_project_count_per_role
    project_count = Project.count

    user = NormalUser.find :first
    assert user.is_a?User

    # normal users should not have access to the entire set of projects
    assert_not_equal project_count, user.projects.size

    # admin users should be able to view all projects
    user = AdminUser.find :first
    assert user.is_a?User
    assert_equal project_count, user.projects.size
  end
end
What does this test suite tell us? First, users all inherit from User; second, user/projects have a common interface, but the result count differs per-role. How do we achieve this simply?

class User < ActiveRecord::Base
end

class NormalUser < User # STI
  has_many :projects
end

user.projects.find(:all, :limit => 5, :offset => params[:page])

class Admin < User
  ???
end
The easy solution is to define some finder sql and extend the association's methods with the splat.

class Admin < User
  has_many :projects, :finder_sql => 'select * from projects' do 
    def find(*args)
      Project.find(*args)
    end
  end
end  

user = User.find(3)
user.projects # success!
For homework, create a proc that you just :extend, so it can be shared around like a cheap floozy.

7 Responses to “hacking at associations in role-based systems”

  1. Dave Smith Says:

    Nifty, though the tests don’t quite match the prose. The assert_not_equal is an interesting trap. It’ll pass (or continue to pass) if a user gets the wrong projects (or no projects, instead of the expected ones).

  2. Stumped Says:

    I guess this one went a little above my head. What is the purpose of the block attached to has_many?

    Also, wouldn’t your test cast fail in instances where you only have 1 regular user (who has 1 project).

  3. Stumped Says:

    I guess this one went a little above my head. What is the purpose of the block attached to has_many?

    Also, wouldn’t your test cast fail in instances where you only have 1 regular user (who has 1 project).

  4. court3nay Says:

    it allows you to do admin.projects.find(:all, :conditions…) which (in my quick testing) doesn’t work with finder_sql.

    Yes, it’ll fail in that case, but we don’t normally have just one item in the fixtures table.

  5. Travis Bell Says:

    Hey man, I don’t know how else to contact you. Feel free to delete this comment after reading it.

    We’re looking for a Rails contractor and wasn’t sure if you do that sort of thing. If you do, use the email I attached to this comment to contact me.

    Thanks!

  6. court3nay Says:

    sorry, not available right now :)

  7. James H. Says:

    Thank you! I’ve been Googling around for an elegant solution to my finder problems with such a situation for a few hours. This post elicited an ‘Oh! Brilliant!’ from me.

Sorry, comments are closed for this article.