allow model adapter to override condition hash matching in Rule, also clean up Mongoid adapter and specs

This commit is contained in:
Ryan Bates 2011-01-04 11:43:41 -08:00
parent 685e926d96
commit cef6c21232
4 changed files with 96 additions and 109 deletions

View File

@ -15,6 +15,17 @@ module CanCan
false # override in subclass false # override in subclass
end end
# Used to determine if this model adapter will override the matching behavior for a hash of conditions.
# If this returns true then matches_conditions_hash? will be called. See Rule#matches_conditions_hash
def self.override_conditions_hash_matching?(subject, conditions)
false
end
# Override if override_conditions_hash_matching? returns true
def self.matches_conditions_hash?(subject, conditions)
raise NotImplemented, "This model adapter does not support matching on a conditions hash."
end
def initialize(model_class, rules) def initialize(model_class, rules)
@model_class = model_class @model_class = model_class
@rules = rules @rules = rules

View File

@ -5,6 +5,14 @@ module CanCan
model_class <= Mongoid::Document model_class <= Mongoid::Document
end end
def self.override_conditions_hash_matching?(subject, conditions)
conditions.any? { |k,v| !k.kind_of?(Symbol) }
end
def self.matches_conditions_hash?(subject, conditions)
subject.class.where(conditions).include?(subject) # just use Mongoid's where function
end
def database_records def database_records
@model_class.where(conditions) @model_class.where(conditions)
end end
@ -23,33 +31,6 @@ module CanCan
end end
end end
end end
# customize to handle Mongoid queries in ability definitions conditions
# Mongoid Criteria are simpler to check than normal conditions hashes
# When no conditions are given, true should be returned.
# The default CanCan behavior relies on the fact that conditions.all? will return true when conditions is empty
# The way ruby handles all? for empty hashes can be unexpected:
# {}.all?{|a| a == 5}
# => true
# {}.all?{|a| a != 5}
# => true
class Rule
def matches_conditions_hash_with_mongoid_subject?(subject, conditions = @conditions)
if defined?(::Mongoid) && subject.class.include?(::Mongoid::Document) && conditions.any?{|k,v| !k.kind_of?(Symbol)}
if conditions.empty?
true
else
subject.class.where(conditions).include?(subject) # just use Mongoid's where function
end
else
matches_conditions_hash_without_mongoid_subject? subject, conditions
end
end
# could use alias_method_chain, but it's not worth adding activesupport as a gem dependency
alias_method :matches_conditions_hash_without_mongoid_subject?, :matches_conditions_hash?
alias_method :matches_conditions_hash?, :matches_conditions_hash_with_mongoid_subject?
end
end end
# simplest way to add `accessible_by` to all Mongoid Documents # simplest way to add `accessible_by` to all Mongoid Documents

View File

@ -88,7 +88,17 @@ module CanCan
@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)) } @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)) }
end end
# Checks if the given subject matches the given conditions hash.
# This behavior can be overriden by a model adapter by defining two class methods:
# override_matching_for_conditions?(subject, conditions) and
# matches_conditions_hash?(subject, conditions)
def matches_conditions_hash?(subject, conditions = @conditions) def matches_conditions_hash?(subject, conditions = @conditions)
if conditions.empty?
true
else
if model_adapter(subject).override_conditions_hash_matching? subject, conditions
model_adapter(subject).matches_conditions_hash? subject, conditions
else
conditions.all? do |name, value| conditions.all? do |name, value|
attribute = subject.send(name) attribute = subject.send(name)
if value.kind_of?(Hash) if value.kind_of?(Hash)
@ -104,6 +114,8 @@ module CanCan
end end
end end
end end
end
end
def nested_subject_matches_conditions?(subject_hash) def nested_subject_matches_conditions?(subject_hash)
parent, child = subject_hash.shift parent, child = subject_hash.shift
@ -117,5 +129,9 @@ module CanCan
@block.call(action, subject.class, subject, *extra_args) @block.call(action, subject.class, subject, *extra_args)
end end
end end
def model_adapter(subject)
ModelAdapters::AbstractAdapter.adapter_class(subject_class?(subject) ? subject : subject.class)
end
end end
end end

View File

@ -18,31 +18,8 @@ if ENV["MODEL_ADAPTER"] == "mongoid"
end end
describe CanCan::ModelAdapters::MongoidAdapter do describe CanCan::ModelAdapters::MongoidAdapter do
context "Mongoid not defined" do
before(:all) do
@mongoid_class = Object.send(:remove_const, :Mongoid)
end
after(:all) do
Object.const_set(:Mongoid, @mongoid_class)
end
it "should not raise an error for ActiveRecord models" do
@model_class = Class.new(Project)
stub(@model_class).scoped { :scoped_stub }
@ability = Object.new
@ability.extend(CanCan::Ability)
@ability.can :read, @model_class
lambda {
@ability.can? :read, @model_class.new
}.should_not raise_error
end
end
context "Mongoid defined" do context "Mongoid defined" do
before(:each) do before(:each) do
@model_class = MongoidProject
@ability = Object.new @ability = Object.new
@ability.extend(CanCan::Ability) @ability.extend(CanCan::Ability)
end end
@ -55,121 +32,123 @@ if ENV["MODEL_ADAPTER"] == "mongoid"
it "should be for only Mongoid classes" do it "should be for only Mongoid classes" do
CanCan::ModelAdapters::MongoidAdapter.should_not be_for_class(Object) CanCan::ModelAdapters::MongoidAdapter.should_not be_for_class(Object)
CanCan::ModelAdapters::MongoidAdapter.should be_for_class(@model_class) CanCan::ModelAdapters::MongoidAdapter.should be_for_class(MongoidProject)
CanCan::ModelAdapters::AbstractAdapter.adapter_class(@model_class).should == CanCan::ModelAdapters::MongoidAdapter CanCan::ModelAdapters::AbstractAdapter.adapter_class(MongoidProject).should == CanCan::ModelAdapters::MongoidAdapter
end end
it "should compare properties on mongoid documents with the conditions hash" do it "should compare properties on mongoid documents with the conditions hash" do
model = @model_class.new model = MongoidProject.new
@ability.can :read, @model_class, :id => model.id @ability.can :read, MongoidProject, :id => model.id
@ability.should be_able_to :read, model @ability.should be_able_to(:read, model)
end end
it "should return [] when no ability is defined so no records are found" do it "should return [] when no ability is defined so no records are found" do
@model_class.create :title => 'Sir' MongoidProject.create(:title => 'Sir')
@model_class.create :title => 'Lord' MongoidProject.create(:title => 'Lord')
@model_class.create :title => 'Dude' MongoidProject.create(:title => 'Dude')
@model_class.accessible_by(@ability, :read).entries.should == [] MongoidProject.accessible_by(@ability, :read).entries.should == []
end end
it "should return the correct records based on the defined ability" do it "should return the correct records based on the defined ability" do
@ability.can :read, @model_class, :title => "Sir" @ability.can :read, MongoidProject, :title => "Sir"
sir = @model_class.create :title => 'Sir' sir = MongoidProject.create(:title => 'Sir')
lord = @model_class.create :title => 'Lord' lord = MongoidProject.create(:title => 'Lord')
dude = @model_class.create :title => 'Dude' dude = MongoidProject.create(:title => 'Dude')
@model_class.accessible_by(@ability, :read).should == [sir] MongoidProject.accessible_by(@ability, :read).should == [sir]
end end
it "should return everything when the defined ability is manage all" do it "should return everything when the defined ability is manage all" do
@ability.can :manage, :all @ability.can :manage, :all
sir = @model_class.create :title => 'Sir' sir = MongoidProject.create(:title => 'Sir')
lord = @model_class.create :title => 'Lord' lord = MongoidProject.create(:title => 'Lord')
dude = @model_class.create :title => 'Dude' dude = MongoidProject.create(:title => 'Dude')
@model_class.accessible_by(@ability, :read).entries.should == [sir, lord, dude] MongoidProject.accessible_by(@ability, :read).entries.should == [sir, lord, dude]
end end
describe "Mongoid::Criteria where clause Symbol extensions using MongoDB expressions" do describe "Mongoid::Criteria where clause Symbol extensions using MongoDB expressions" do
it "should handle :field.in" do it "should handle :field.in" do
obj = @model_class.create :title => 'Sir' obj = MongoidProject.create(:title => 'Sir')
@ability.can :read, @model_class, :title.in => ["Sir", "Madam"] @ability.can :read, MongoidProject, :title.in => ["Sir", "Madam"]
@ability.can?(:read, obj).should == true @ability.can?(:read, obj).should == true
@model_class.accessible_by(@ability, :read).should == [obj] MongoidProject.accessible_by(@ability, :read).should == [obj]
obj2 = @model_class.create :title => 'Lord' obj2 = MongoidProject.create(:title => 'Lord')
@ability.can?(:read, obj2).should == false @ability.can?(:read, obj2).should == false
end end
describe "activates only when there are Criteria in the hash" do describe "activates only when there are Criteria in the hash" do
it "Calls where on the model class when there are criteria" do it "Calls where on the model class when there are criteria" do
obj = @model_class.create :title => 'Bird' obj = MongoidProject.create(:title => 'Bird')
@conditions = {:title.nin => ["Fork", "Spoon"]} @conditions = {:title.nin => ["Fork", "Spoon"]}
mock(@model_class).where(@conditions) {[obj]} mock(MongoidProject).where(@conditions) {[obj]}
@ability.can :read, @model_class, @conditions @ability.can :read, MongoidProject, @conditions
@ability.should be_able_to(:read, obj) @ability.should be_able_to(:read, obj)
end end
it "Calls the base version if there are no mongoid criteria" do it "Calls the base version if there are no mongoid criteria" do
obj = @model_class.new :title => 'Bird' obj = MongoidProject.new(:title => 'Bird')
@conditions = {:id => obj.id} @conditions = {:id => obj.id}
@ability.can :read, @model_class, @conditions @ability.can :read, MongoidProject, @conditions
@ability.should be_able_to(:read, obj) @ability.should be_able_to(:read, obj)
end end
end end
it "should handle :field.nin" do it "should handle :field.nin" do
obj = @model_class.create :title => 'Sir' obj = MongoidProject.create(:title => 'Sir')
@ability.can :read, @model_class, :title.nin => ["Lord", "Madam"] @ability.can :read, MongoidProject, :title.nin => ["Lord", "Madam"]
@ability.can?(:read, obj).should == true @ability.can?(:read, obj).should == true
@model_class.accessible_by(@ability, :read).should == [obj] MongoidProject.accessible_by(@ability, :read).should == [obj]
obj2 = @model_class.create :title => 'Lord' obj2 = MongoidProject.create(:title => 'Lord')
@ability.can?(:read, obj2).should == false @ability.can?(:read, obj2).should == false
end end
it "should handle :field.size" do it "should handle :field.size" do
obj = @model_class.create :titles => ['Palatin', 'Margrave'] obj = MongoidProject.create(:titles => ['Palatin', 'Margrave'])
@ability.can :read, @model_class, :titles.size => 2 @ability.can :read, MongoidProject, :titles.size => 2
@ability.can?(:read, obj).should == true @ability.can?(:read, obj).should == true
@model_class.accessible_by(@ability, :read).should == [obj] MongoidProject.accessible_by(@ability, :read).should == [obj]
obj2 = @model_class.create :titles => ['Palatin', 'Margrave', 'Marquis'] obj2 = MongoidProject.create(:titles => ['Palatin', 'Margrave', 'Marquis'])
@ability.can?(:read, obj2).should == false @ability.can?(:read, obj2).should == false
end end
it "should handle :field.exists" do it "should handle :field.exists" do
obj = @model_class.create :titles => ['Palatin', 'Margrave'] obj = MongoidProject.create(:titles => ['Palatin', 'Margrave'])
@ability.can :read, @model_class, :titles.exists => true @ability.can :read, MongoidProject, :titles.exists => true
@ability.can?(:read, obj).should == true @ability.can?(:read, obj).should == true
@model_class.accessible_by(@ability, :read).should == [obj] MongoidProject.accessible_by(@ability, :read).should == [obj]
obj2 = @model_class.create obj2 = MongoidProject.create
@ability.can?(:read, obj2).should == false @ability.can?(:read, obj2).should == false
end end
it "should handle :field.gt" do it "should handle :field.gt" do
obj = @model_class.create :age => 50 obj = MongoidProject.create(:age => 50)
@ability.can :read, @model_class, :age.gt => 45 @ability.can :read, MongoidProject, :age.gt => 45
@ability.can?(:read, obj).should == true @ability.can?(:read, obj).should == true
@model_class.accessible_by(@ability, :read).should == [obj] MongoidProject.accessible_by(@ability, :read).should == [obj]
obj2 = @model_class.create :age => 40 obj2 = MongoidProject.create(:age => 40)
@ability.can?(:read, obj2).should == false @ability.can?(:read, obj2).should == false
end end
end end
it "should call where with matching ability conditions" do it "should call where with matching ability conditions" do
obj = @model_class.create :foo => {:bar => 1} obj = MongoidProject.create(:foo => {:bar => 1})
@ability.can :read, @model_class, :foo => {:bar => 1} @ability.can :read, MongoidProject, :foo => {:bar => 1}
@model_class.accessible_by(@ability, :read).entries.first.should == obj MongoidProject.accessible_by(@ability, :read).entries.first.should == obj
end end
it "should not allow to fetch records when ability with just block present" do it "should not allow to fetch records when ability with just block present" do
@ability.can :read, @model_class do false end @ability.can :read, MongoidProject do
false
end
lambda { lambda {
@model_class.accessible_by(@ability) MongoidProject.accessible_by(@ability)
}.should raise_error(CanCan::Error) }.should raise_error(CanCan::Error)
end end
end end