adding attributes as 3rd argument to can and can? calls

This commit is contained in:
Ryan Bates 2011-03-25 11:24:10 -07:00
parent a03d35272b
commit 85efbdb8d0
4 changed files with 83 additions and 34 deletions

View File

@ -53,9 +53,9 @@ module CanCan
# end # end
# #
# Also see the RSpec Matchers to aid in testing. # Also see the RSpec Matchers to aid in testing.
def can?(action, subject, *extra_args) def can?(action, subject, attribute = nil)
match = relevant_rules_for_match(action, subject).detect do |rule| match = relevant_rules_for_match(action, subject, attribute).detect do |rule|
rule.matches_conditions?(action, subject, extra_args) rule.matches_conditions?(action, subject, attribute)
end end
match ? match.base_behavior : false match ? match.base_behavior : false
end end
@ -121,8 +121,8 @@ module CanCan
# # check the database and return true/false # # check the database and return true/false
# end # end
# #
def can(action = nil, subject = nil, conditions = nil, &block) def can(*args, &block)
rules << Rule.new(true, action, subject, conditions, block) rules << Rule.new(true, *args, &block)
end end
# Defines an ability which cannot be done. Accepts the same arguments as "can". # Defines an ability which cannot be done. Accepts the same arguments as "can".
@ -137,8 +137,8 @@ module CanCan
# product.invisible? # product.invisible?
# end # end
# #
def cannot(action = nil, subject = nil, conditions = nil, &block) def cannot(*args, &block)
rules << Rule.new(false, action, subject, conditions, block) rules << Rule.new(false, *args, &block)
end end
# Alias one or more actions into another one. # Alias one or more actions into another one.
@ -282,16 +282,16 @@ module CanCan
# Returns an array of Rule instances which match the action and subject # Returns an array of Rule instances which match the action and subject
# This does not take into consideration any hash conditions or block statements # This does not take into consideration any hash conditions or block statements
def relevant_rules(action, subject) def relevant_rules(action, subject, attribute = nil)
rules.reverse.select do |rule| rules.reverse.select do |rule|
rule.expanded_actions = expand_aliases(:actions, rule.actions) rule.expanded_actions = expand_aliases(:actions, rule.actions)
rule.expanded_subjects = expand_aliases(:subjects, rule.subjects) rule.expanded_subjects = expand_aliases(:subjects, rule.subjects)
rule.relevant? action, subject rule.relevant? action, subject, attribute
end end
end end
def relevant_rules_for_match(action, subject) def relevant_rules_for_match(action, subject, attribute)
relevant_rules(action, subject).each do |rule| relevant_rules(action, subject, attribute).each do |rule|
if rule.only_raw_sql? if rule.only_raw_sql?
raise Error, "The can? and cannot? call cannot be used with a raw sql 'can' definition. The checking code cannot be determined for #{action.inspect} #{subject.inspect}" raise Error, "The can? and cannot? call cannot be used with a raw sql 'can' definition. The checking code cannot be determined for #{action.inspect} #{subject.inspect}"
end end
@ -299,7 +299,7 @@ module CanCan
end end
def relevant_rules_for_query(action, subject) def relevant_rules_for_query(action, subject)
relevant_rules(action, subject).each do |rule| relevant_rules(action, subject, nil).each do |rule|
if rule.only_block? if rule.only_block?
raise Error, "The accessible_by call cannot be used with a block 'can' definition. The SQL cannot be determined for #{action.inspect} #{subject.inspect}" raise Error, "The accessible_by call cannot be used with a block 'can' definition. The SQL cannot be determined for #{action.inspect} #{subject.inspect}"
end end

View File

@ -10,28 +10,29 @@ module CanCan
# value. True for "can" and false for "cannot". The next two arguments are the action # value. True for "can" and false for "cannot". The next two arguments are the action
# and subject respectively (such as :read, @project). The third argument is a hash # and subject respectively (such as :read, @project). The third argument is a hash
# of conditions and the last one is the block passed to the "can" call. # of conditions and the last one is the block passed to the "can" call.
def initialize(base_behavior, action, subject, conditions, block) def initialize(base_behavior, action = nil, subject = nil, *extra_args, &block)
raise Error, "You are not able to supply a block with a hash of conditions in #{action} #{subject} ability. Use either one." if conditions.kind_of?(Hash) && !block.nil?
@match_all = action.nil? && subject.nil? @match_all = action.nil? && subject.nil?
@base_behavior = base_behavior @base_behavior = base_behavior
@actions = [action].flatten.compact.map(&:to_sym) @actions = [action].flatten
@subjects = [subject].flatten.compact.map(&:to_sym) @subjects = [subject].flatten
@conditions = conditions || {} @attributes = [extra_args.shift].flatten if extra_args.first.kind_of?(Symbol) || extra_args.first.kind_of?(Array) && extra_args.first.first.kind_of?(Symbol)
raise Error, "You are not able to supply a block with a hash of conditions in #{action} #{subject} ability. Use either one." if extra_args.first && !block.nil?
@conditions = extra_args.first || {}
@block = block @block = block
end end
# Matches both the subject and action, not necessarily the conditions # Matches the subject, action, and given attribute. Conditions are not checked here.
def relevant?(action, subject) def relevant?(action, subject, attribute)
subject = subject.values.first if subject.class == Hash subject = subject.values.first if subject.class == Hash
@match_all || (matches_action?(action) && matches_subject?(subject)) @match_all || (matches_action?(action) && matches_subject?(subject) && matches_attribute?(attribute))
end end
# Matches the block or conditions hash # Matches the block or conditions hash
def matches_conditions?(action, subject, extra_args) def matches_conditions?(action, subject, attribute)
if @match_all if @match_all
call_block_with_all(action, subject, extra_args) call_block_with_all(action, subject, attribute)
elsif @block && subject_object?(subject) elsif @block && subject_object?(subject)
@block.call(subject, *extra_args) @block.arity == 1 ? @block.call(subject) : @block.call(subject, attribute)
elsif @conditions.kind_of?(Hash) && subject.class == Hash elsif @conditions.kind_of?(Hash) && subject.class == Hash
nested_subject_matches_conditions?(subject) nested_subject_matches_conditions?(subject)
elsif @conditions.kind_of?(Hash) && subject_object?(subject) elsif @conditions.kind_of?(Hash) && subject_object?(subject)
@ -87,6 +88,10 @@ module CanCan
@expanded_subjects.include?(:all) || @expanded_subjects.include?(subject.to_sym) # || matches_subject_class?(subject) @expanded_subjects.include?(:all) || @expanded_subjects.include?(subject.to_sym) # || matches_subject_class?(subject)
end end
def matches_attribute?(attribute)
@attributes.nil? || attribute.nil? || @attributes.include?(attribute.to_sym)
end
# TODO deperecate this # TODO deperecate this
def matches_subject_class?(subject) def matches_subject_class?(subject)
@expanded_subjects.any? { |sub| sub.kind_of?(Module) && (subject.kind_of?(sub) || subject.class.to_s == sub.to_s || subject.kind_of?(Module) && subject.ancestors.include?(sub)) } @expanded_subjects.any? { |sub| sub.kind_of?(Module) && (subject.kind_of?(sub) || subject.class.to_s == sub.to_s || subject.kind_of?(Module) && subject.ancestors.include?(sub)) }
@ -130,11 +135,11 @@ module CanCan
matches_conditions_hash?(parent, @conditions[parent.class.name.downcase.to_sym] || {}) matches_conditions_hash?(parent, @conditions[parent.class.name.downcase.to_sym] || {})
end end
def call_block_with_all(action, subject, extra_args) def call_block_with_all(action, subject, attribute)
if subject_object? subject if subject_object? subject
@block.call(action, subject_name(subject), subject, *extra_args) @block.call(action, subject_name(subject), subject, attribute)
else else
@block.call(action, subject, nil, *extra_args) @block.call(action, subject, nil, attribute)
end end
end end

View File

@ -35,11 +35,9 @@ describe CanCan::Ability do
@ability.can?(:paint, :cars).should be_false @ability.can?(:paint, :cars).should be_false
end end
it "allows strings instead of symbols" do it "allows strings instead of symbols in ability check" do
@ability.can "paint", "fences" @ability.can :paint, :fences
@ability.can?("paint", "fences").should be_true @ability.can?("paint", "fences").should be_true
@ability.can "access", "all"
@ability.can?("wax", "cars").should be_true
end end
@ -182,7 +180,7 @@ describe CanCan::Ability do
@block_called.should be_true @block_called.should be_true
end end
it "should raise an error when attempting to use a block with a hash condition since it's not likely what they want" do it "raises an error when attempting to use a block with a hash condition since it's not likely what they want" do
lambda { lambda {
@ability.can :read, :ranges, :published => true do @ability.can :read, :ranges, :published => true do
false false
@ -191,6 +189,52 @@ describe CanCan::Ability do
end end
# Attributes
it "allows permission on attributes" do
@ability.can :update, :users, :name
@ability.can :update, :users, [:email, :age]
@ability.can?(:update, :users, :name).should be_true
@ability.can?(:update, :users, :email).should be_true
@ability.can?(:update, :users, :password).should be_false
end
it "allows permission on all attributes when none are given" do
@ability.can :update, :users
@ability.can?(:update, :users, :password).should be_true
end
it "allows strings when chekcing attributes" do
@ability.can :update, :users, :name
@ability.can?(:update, :users, "name").should be_true
end
it "combines attribute check with conditions hash" do
@ability.can :update, :ranges, :begin => 1
@ability.can :update, :ranges, :name, :begin => 2
@ability.can?(:update, 1..3, :foobar).should be_true
@ability.can?(:update, 2..4, :foobar).should be_false
@ability.can?(:update, 2..4, :name).should be_true
@ability.can?(:update, 3..5, :name).should be_false
end
it "passes attribute to block and nil if no attribute checked" do
@ability.can :update, :ranges do |range, attribute|
attribute == :name
end
@ability.can?(:update, 1..3, :name).should be_true
@ability.can?(:update, 2..4).should be_false
end
it "passes attribute to block for global can definition" do
@ability.can do |action, subject, object, attribute|
attribute == :name
end
@ability.can?(:update, 1..3, :name).should be_true
@ability.can?(:update, 2..4).should be_false
end
# Cannot # Cannot
it "offers cannot? method which inverts can?" do it "offers cannot? method which inverts can?" do
@ -259,7 +303,7 @@ describe CanCan::Ability do
end end
end end
it "should raise access denied exception with default message if not specified" do it "raises access denied exception with default message if not specified" do
begin begin
@ability.authorize! :read, :books @ability.authorize! :read, :books
rescue CanCan::AccessDenied => e rescue CanCan::AccessDenied => e

View File

@ -4,7 +4,7 @@ require "spec_helper"
describe CanCan::Rule do describe CanCan::Rule do
before(:each) do before(:each) do
@conditions = {} @conditions = {}
@rule = CanCan::Rule.new(true, :read, :integers, @conditions, nil) @rule = CanCan::Rule.new(true, :read, :integers, @conditions)
end end
it "should return no association joins if none exist" do it "should return no association joins if none exist" do
@ -33,7 +33,7 @@ describe CanCan::Rule do
end end
it "should return no association joins if conditions is nil" do it "should return no association joins if conditions is nil" do
rule = CanCan::Rule.new(true, :read, :integers, nil, nil) rule = CanCan::Rule.new(true, :read, :integers)
rule.associations_hash.should == {} rule.associations_hash.should == {}
end end
end end