From e20081454f49ad8b79fc43049b6e773901748247 Mon Sep 17 00:00:00 2001 From: Ryan Bates Date: Tue, 20 Apr 2010 17:02:28 -0700 Subject: [PATCH] adding joins clause to accessible_by when conditions are across associations --- CHANGELOG.rdoc | 2 ++ lib/cancan/ability.rb | 10 +++++++ lib/cancan/active_record_additions.rb | 5 ++-- lib/cancan/can_definition.rb | 15 ++++++++++ spec/cancan/active_record_additions_spec.rb | 10 +++---- spec/cancan/can_definition_spec.rb | 33 +++++++++++++++++++++ 6 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 spec/cancan/can_definition_spec.rb diff --git a/CHANGELOG.rdoc b/CHANGELOG.rdoc index 0cc01fd..080bcd1 100644 --- a/CHANGELOG.rdoc +++ b/CHANGELOG.rdoc @@ -1,3 +1,5 @@ +* Adding joins clause to accessible_by when conditions are across associations + 1.1.1 (April 17, 2010) * Fixing behavior in Rails 3 by properly initializing ResourceAuthorization diff --git a/lib/cancan/ability.rb b/lib/cancan/ability.rb index d173c65..535663e 100644 --- a/lib/cancan/ability.rb +++ b/lib/cancan/ability.rb @@ -200,6 +200,16 @@ module CanCan end end + # Returns the associations used in conditions. This is usually used in the :joins option for a search. + # See ActiveRecordAdditions#accessible_by for use in Active Record. + def association_joins(action, subject) + can_definition = matching_can_definition(action, subject) + if can_definition + raise Error, "Cannot determine association joins from block for #{action.inspect} #{subject.inspect}" if can_definition.block + can_definition.association_joins + end + end + private def can_definitions diff --git a/lib/cancan/active_record_additions.rb b/lib/cancan/active_record_additions.rb index 2a688e2..2afa33d 100644 --- a/lib/cancan/active_record_additions.rb +++ b/lib/cancan/active_record_additions.rb @@ -21,10 +21,11 @@ module CanCan # internally uses Ability#conditions method, see that for more information. def accessible_by(ability, action = :read) conditions = ability.conditions(action, self) || {:id => nil} + joins = ability.association_joins(action, self) if respond_to? :where - where(conditions) + where(conditions).joins(joins) else - scoped(:conditions => conditions) + scoped(:conditions => conditions, :joins => joins) end end end diff --git a/lib/cancan/can_definition.rb b/lib/cancan/can_definition.rb index 5c598ea..8e65569 100644 --- a/lib/cancan/can_definition.rb +++ b/lib/cancan/can_definition.rb @@ -25,6 +25,21 @@ module CanCan result = can_without_base_behavior?(action, subject, extra_args) @base_behavior ? result : !result end + + def association_joins(conditions = @conditions) + joins = [] + conditions.each do |name, value| + if value.kind_of? Hash + nested = association_joins(value) + if nested + joins << {name => nested} + else + joins << name + end + end + end + joins unless joins.empty? + end private diff --git a/spec/cancan/active_record_additions_spec.rb b/spec/cancan/active_record_additions_spec.rb index 8e995df..640f44a 100644 --- a/spec/cancan/active_record_additions_spec.rb +++ b/spec/cancan/active_record_additions_spec.rb @@ -10,19 +10,19 @@ describe CanCan::ActiveRecordAdditions do end it "should call where(:id => nil) when no ability is defined so no records are found" do - stub(@model_class).where(:id => nil) { :no_where } + stub(@model_class).where(:id => nil).stub!.joins(nil) { :no_where } @model_class.accessible_by(@ability, :read).should == :no_where end it "should call where with matching ability conditions" do - @ability.can :read, @model_class, :foo => 1 - stub(@model_class).where(:foo => 1) { :found_records } + @ability.can :read, @model_class, :foo => {:bar => 1} + stub(@model_class).where(:foo => { :bar => 1 }).stub!.joins([:foo]) { :found_records } @model_class.accessible_by(@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 } + @ability.can :read, @model_class, :foo => {:bar => 1} + stub(@model_class).scoped(:conditions => {:foo => {:bar => 1}}, :joins => [:foo]) { :found_records } @model_class.accessible_by(@ability).should == :found_records end end diff --git a/spec/cancan/can_definition_spec.rb b/spec/cancan/can_definition_spec.rb new file mode 100644 index 0000000..697dc74 --- /dev/null +++ b/spec/cancan/can_definition_spec.rb @@ -0,0 +1,33 @@ +require "spec_helper" + +describe CanCan::CanDefinition do + before(:each) do + @conditions = {} + @can = CanCan::CanDefinition.new(true, :read, Integer, @conditions, nil) + end + + it "should return no association joins if none exist" do + @can.association_joins.should be_nil + end + + it "should return no association for joins if just attributes" do + @conditions[:foo] = :bar + @can.association_joins.should be_nil + end + + it "should return single association for joins" do + @conditions[:foo] = {:bar => 1} + @can.association_joins.should == [:foo] + end + + it "should return multiple associations for joins" do + @conditions[:foo] = {:bar => 1} + @conditions[:test] = {1 => 2} + @can.association_joins.map(&:to_s).sort.should == [:foo, :test].map(&:to_s).sort + end + + it "should return nested associations for joins" do + @conditions[:foo] = {:bar => {1 => 2}} + @can.association_joins.should == [{:foo => [:bar]}] + end +end