From 37f482e8d575c383540b1292aba338cb557af9cf Mon Sep 17 00:00:00 2001 From: Ryan Bates Date: Thu, 15 Apr 2010 23:18:49 -0700 Subject: [PATCH] default ActiveRecordAdditions#can method action to :read and use 'scoped' if 'where' is not available --- README.rdoc | 21 +++++++++++++++++ lib/cancan/ability.rb | 4 +--- lib/cancan/active_record_additions.rb | 26 +++++++++++++++++++-- spec/cancan/active_record_additions_spec.rb | 8 ++++++- 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/README.rdoc b/README.rdoc index 03cb4e8..dcd3c6d 100644 --- a/README.rdoc +++ b/README.rdoc @@ -178,6 +178,21 @@ If you want to customize the loading behavior on certain actions, you can do so Here the @book instance variable is already set so it will not be loaded again for that action. This works for nested resources as well. +== Fetching Records + +Sometimes you need to restrict which records are returned from the database based on what the user is able to access. This can be done with the +can+ method on any Active Record model. Simply pass it the current ability and an action. + + @articles = Article.can(current_ability, :read) + +The action defaults to :read so that can optionally be left out. + + @articles = Article.can(current_ability) + +Here only the records which the user can read will be returned. This is an Active Record scope so other scopes and pagination can be chained onto it. + +Note: This does not work for abilities which were defined by a block because the conditions hash can not be determined from them. Instead an exception will be raised. + + == Assumptions & Configuring CanCan makes two assumptions about your application. @@ -205,6 +220,12 @@ It is very easy to test the Ability model since you can call "can?" directly on assert ability.cannot?(:destroy, Project.new) end +A matcher called +be_able_to+ is provided for testing abilities in RSpec. + + require "cancan/matchers" + # ... + ability.should be_able_to(:destroy, Project.new) + == Special Thanks diff --git a/lib/cancan/ability.rb b/lib/cancan/ability.rb index b4298c5..938c50b 100644 --- a/lib/cancan/ability.rb +++ b/lib/cancan/ability.rb @@ -191,9 +191,7 @@ module CanCan # can :read, Article, :visible => true # conditions :read, Article # returns { :visible => true } # - # For example, you can use this in Active Record find conditions to only fetch articles the user has permission to read. - # - # Article.where(current_ability.conditions(:read, Article)) + # Normally you will not call this method directly, but instead go through ActiveRecordAdditions#can method. # # If the ability is not defined then false is returned so be sure to take that into consideration. # If the ability is defined using a block then this will raise an exception since a hash of conditions cannot be diff --git a/lib/cancan/active_record_additions.rb b/lib/cancan/active_record_additions.rb index 1bfe9d3..7b4bb0d 100644 --- a/lib/cancan/active_record_additions.rb +++ b/lib/cancan/active_record_additions.rb @@ -2,8 +2,30 @@ module CanCan # This module is automatically included into all Active Record. module ActiveRecordAdditions module ClassMethods - def can(ability, action) - where(ability.conditions(action, self) || {:id => nil}) + # Returns a scope which fetches only the records that the passed ability + # can perform a given action on. The action defaults to :read. This + # is usually called from a controller and passed the +current_ability+. + # + # @articles = Article.can(current_ability) + # + # Here only the articles which the user is able to read will be returned. + # If the user does not have permission to read any articles then an empty + # result is returned. Since this is a scope it can be combined with any + # other scopes or pagination. + # + # An alternative action can optionally be passed as a second argument. + # + # @articles = Article.can(current_ability, :update) + # + # Here only the articles which the user can update are returned. This + # internally uses Ability#conditions method, see that for more information. + def can(ability, action = :read) + conditions = ability.conditions(action, self) || {:id => nil} + if respond_to? :where + where(conditions) + else + scoped(:conditions => conditions) + end end end diff --git a/spec/cancan/active_record_additions_spec.rb b/spec/cancan/active_record_additions_spec.rb index 4e4628a..317a53b 100644 --- a/spec/cancan/active_record_additions_spec.rb +++ b/spec/cancan/active_record_additions_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" describe CanCan::ActiveRecordAdditions do before(:each) do @model_class = Class.new - stub(@model_class).where { :where_stub } + stub(@model_class).scoped { :scoped_stub } @model_class.send(:include, CanCan::ActiveRecordAdditions) @ability = Object.new @ability.extend(CanCan::Ability) @@ -19,4 +19,10 @@ describe CanCan::ActiveRecordAdditions do stub(@model_class).where(:foo => 1) { :found_records } @model_class.can(@ability, :read).should == :found_records end + + it "should default to :read ability and use scoped when where isn't available" do + @ability.can :read, @model_class, :foo => 1 + stub(@model_class).scoped(:conditions => {:foo => 1}) { :found_records } + @model_class.can(@ability).should == :found_records + end end