cancancan 1.11.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +5 -5
  2. data/cancancan.gemspec +15 -19
  3. data/lib/cancan/ability/actions.rb +91 -0
  4. data/lib/cancan/ability/rules.rb +85 -0
  5. data/lib/cancan/ability.rb +74 -136
  6. data/lib/cancan/conditions_matcher.rb +93 -0
  7. data/lib/cancan/controller_additions.rb +34 -40
  8. data/lib/cancan/controller_resource.rb +47 -212
  9. data/lib/cancan/controller_resource_builder.rb +24 -0
  10. data/lib/cancan/controller_resource_finder.rb +40 -0
  11. data/lib/cancan/controller_resource_loader.rb +116 -0
  12. data/lib/cancan/controller_resource_name_finder.rb +21 -0
  13. data/lib/cancan/controller_resource_sanitizer.rb +30 -0
  14. data/lib/cancan/exceptions.rb +7 -3
  15. data/lib/cancan/matchers.rb +12 -3
  16. data/lib/cancan/model_adapters/abstract_adapter.rb +8 -8
  17. data/lib/cancan/model_adapters/active_record_4_adapter.rb +33 -10
  18. data/lib/cancan/model_adapters/active_record_5_adapter.rb +70 -0
  19. data/lib/cancan/model_adapters/active_record_adapter.rb +41 -81
  20. data/lib/cancan/model_adapters/can_can/model_adapters/active_record_adapter/joins.rb +39 -0
  21. data/lib/cancan/model_adapters/conditions_extractor.rb +75 -0
  22. data/lib/cancan/model_additions.rb +0 -1
  23. data/lib/cancan/rule.rb +36 -92
  24. data/lib/cancan/rules_compressor.rb +20 -0
  25. data/lib/cancan/version.rb +1 -1
  26. data/lib/cancan.rb +5 -12
  27. data/lib/generators/cancan/ability/ability_generator.rb +1 -1
  28. metadata +54 -65
  29. data/.gitignore +0 -15
  30. data/.rspec +0 -1
  31. data/.travis.yml +0 -55
  32. data/Appraisals +0 -136
  33. data/CHANGELOG.rdoc +0 -503
  34. data/CONTRIBUTING.md +0 -23
  35. data/Gemfile +0 -3
  36. data/LICENSE +0 -22
  37. data/README.md +0 -188
  38. data/Rakefile +0 -9
  39. data/gemfiles/activerecord_3.0.gemfile +0 -18
  40. data/gemfiles/activerecord_3.1.gemfile +0 -20
  41. data/gemfiles/activerecord_3.2.gemfile +0 -20
  42. data/gemfiles/activerecord_4.0.gemfile +0 -17
  43. data/gemfiles/activerecord_4.1.gemfile +0 -17
  44. data/gemfiles/activerecord_4.2.gemfile +0 -18
  45. data/gemfiles/datamapper_1.x.gemfile +0 -14
  46. data/gemfiles/mongoid_2.x.gemfile +0 -20
  47. data/gemfiles/sequel_3.x.gemfile +0 -20
  48. data/lib/cancan/inherited_resource.rb +0 -20
  49. data/lib/cancan/model_adapters/active_record_3_adapter.rb +0 -47
  50. data/lib/cancan/model_adapters/data_mapper_adapter.rb +0 -34
  51. data/lib/cancan/model_adapters/mongoid_adapter.rb +0 -54
  52. data/lib/cancan/model_adapters/sequel_adapter.rb +0 -87
  53. data/spec/README.rdoc +0 -27
  54. data/spec/cancan/ability_spec.rb +0 -487
  55. data/spec/cancan/controller_additions_spec.rb +0 -141
  56. data/spec/cancan/controller_resource_spec.rb +0 -632
  57. data/spec/cancan/exceptions_spec.rb +0 -58
  58. data/spec/cancan/inherited_resource_spec.rb +0 -71
  59. data/spec/cancan/matchers_spec.rb +0 -29
  60. data/spec/cancan/model_adapters/active_record_4_adapter_spec.rb +0 -85
  61. data/spec/cancan/model_adapters/active_record_adapter_spec.rb +0 -446
  62. data/spec/cancan/model_adapters/data_mapper_adapter_spec.rb +0 -119
  63. data/spec/cancan/model_adapters/default_adapter_spec.rb +0 -7
  64. data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +0 -227
  65. data/spec/cancan/model_adapters/sequel_adapter_spec.rb +0 -132
  66. data/spec/cancan/rule_spec.rb +0 -52
  67. data/spec/matchers.rb +0 -13
  68. data/spec/spec.opts +0 -2
  69. data/spec/spec_helper.rb +0 -28
  70. data/spec/support/ability.rb +0 -7
@@ -0,0 +1,116 @@
1
+ require_relative 'controller_resource_finder.rb'
2
+ require_relative 'controller_resource_name_finder.rb'
3
+ require_relative 'controller_resource_builder.rb'
4
+ require_relative 'controller_resource_sanitizer.rb'
5
+ module CanCan
6
+ module ControllerResourceLoader
7
+ include CanCan::ControllerResourceNameFinder
8
+ include CanCan::ControllerResourceFinder
9
+ include CanCan::ControllerResourceBuilder
10
+ include CanCan::ControllerResourceSanitizer
11
+
12
+ def load_resource
13
+ return if skip?(:load)
14
+ if load_instance?
15
+ self.resource_instance ||= load_resource_instance
16
+ elsif load_collection?
17
+ self.collection_instance ||= load_collection
18
+ end
19
+ end
20
+
21
+ protected
22
+
23
+ def new_actions
24
+ %i[new create] + Array(@options[:new])
25
+ end
26
+
27
+ def resource_params_by_key(key)
28
+ return unless @options[key] && @params.key?(extract_key(@options[key]))
29
+ @params[extract_key(@options[key])]
30
+ end
31
+
32
+ def resource_params_by_namespaced_name
33
+ resource_params_by_key(:instance_name) || resource_params_by_key(:class) || (
34
+ params = @params[extract_key(namespaced_name)]
35
+ params.respond_to?(:to_h) ? params : nil)
36
+ end
37
+
38
+ def resource_params
39
+ if parameters_require_sanitizing? && params_method.present?
40
+ sanitize_parameters
41
+ else
42
+ resource_params_by_namespaced_name
43
+ end
44
+ end
45
+
46
+ def fetch_parent(name)
47
+ if @controller.instance_variable_defined? "@#{name}"
48
+ @controller.instance_variable_get("@#{name}")
49
+ elsif @controller.respond_to?(name, true)
50
+ @controller.send(name)
51
+ end
52
+ end
53
+
54
+ # The object to load this resource through.
55
+ def parent_resource
56
+ parent_name && fetch_parent(parent_name)
57
+ end
58
+
59
+ def parent_name
60
+ @options[:through] && [@options[:through]].flatten.detect { |i| fetch_parent(i) }
61
+ end
62
+
63
+ def resource_base_through_parent_resource
64
+ if @options[:singleton]
65
+ resource_class
66
+ else
67
+ parent_resource.send(@options[:through_association] || name.to_s.pluralize)
68
+ end
69
+ end
70
+
71
+ def resource_base_through
72
+ if parent_resource
73
+ resource_base_through_parent_resource
74
+ elsif @options[:shallow]
75
+ resource_class
76
+ else
77
+ # maybe this should be a record not found error instead?
78
+ raise AccessDenied.new(nil, authorization_action, resource_class)
79
+ end
80
+ end
81
+
82
+ # The object that methods (such as "find", "new" or "build") are called on.
83
+ # If the :through option is passed it will go through an association on that instance.
84
+ # If the :shallow option is passed it will use the resource_class if there's no parent
85
+ # If the :singleton option is passed it won't use the association because it needs to be handled later.
86
+ def resource_base
87
+ @options[:through] ? resource_base_through : resource_class
88
+ end
89
+
90
+ def parent_authorization_action
91
+ @options[:parent_action] || :show
92
+ end
93
+
94
+ def authorization_action
95
+ parent? ? parent_authorization_action : @params[:action].to_sym
96
+ end
97
+
98
+ def load_collection
99
+ resource_base.accessible_by(current_ability, authorization_action)
100
+ end
101
+
102
+ def load_resource_instance
103
+ if !parent? && new_actions.include?(@params[:action].to_sym)
104
+ build_resource
105
+ elsif id_param || @options[:singleton]
106
+ find_resource
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ def extract_key(value)
113
+ value.to_s.underscore.tr('/', '_')
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,21 @@
1
+ module CanCan
2
+ module ControllerResourceNameFinder
3
+ protected
4
+
5
+ def name_from_controller
6
+ @params[:controller].split('/').last.singularize
7
+ end
8
+
9
+ def namespaced_name
10
+ [namespace, name].join('/').singularize.camelize.safe_constantize || name
11
+ end
12
+
13
+ def name
14
+ @name || name_from_controller
15
+ end
16
+
17
+ def namespace
18
+ @params[:controller].split('/')[0..-2]
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,30 @@
1
+ module CanCan
2
+ module ControllerResourceSanitizer
3
+ protected
4
+
5
+ def sanitize_parameters
6
+ case params_method
7
+ when Symbol
8
+ @controller.send(params_method)
9
+ when String
10
+ @controller.instance_eval(params_method)
11
+ when Proc
12
+ params_method.call(@controller)
13
+ end
14
+ end
15
+
16
+ def params_methods
17
+ methods = ["#{@params[:action]}_params".to_sym, "#{name}_params".to_sym, :resource_params]
18
+ methods.unshift(@options[:param_method]) if @options[:param_method].present?
19
+ methods
20
+ end
21
+
22
+ def params_method
23
+ params_methods.each do |method|
24
+ return method if (method.is_a?(Symbol) && @controller.respond_to?(method, true)) ||
25
+ method.is_a?(String) || method.is_a?(Proc)
26
+ end
27
+ nil
28
+ end
29
+ end
30
+ end
@@ -11,6 +11,9 @@ module CanCan
11
11
  # Raised when using check_authorization without calling authorized!
12
12
  class AuthorizationNotPerformed < Error; end
13
13
 
14
+ # Raised when using a wrong association name
15
+ class WrongAssociationName < Error; end
16
+
14
17
  # This error is raised when a user isn't allowed to access a given controller action.
15
18
  # This usually happens within a call to ControllerAdditions#authorize! but can be
16
19
  # raised manually.
@@ -33,14 +36,15 @@ module CanCan
33
36
  # See ControllerAdditions#authorized! for more information on rescuing from this exception
34
37
  # and customizing the message using I18n.
35
38
  class AccessDenied < Error
36
- attr_reader :action, :subject
39
+ attr_reader :action, :subject, :conditions
37
40
  attr_writer :default_message
38
41
 
39
- def initialize(message = nil, action = nil, subject = nil)
42
+ def initialize(message = nil, action = nil, subject = nil, conditions = nil)
40
43
  @message = message
41
44
  @action = action
42
45
  @subject = subject
43
- @default_message = I18n.t(:"unauthorized.default", :default => "You are not authorized to access this page.")
46
+ @conditions = conditions
47
+ @default_message = I18n.t(:"unauthorized.default", default: 'You are not authorized to access this page.')
44
48
  end
45
49
 
46
50
  def to_s
@@ -9,13 +9,22 @@ end
9
9
 
10
10
  Kernel.const_get(rspec_module)::Matchers.define :be_able_to do |*args|
11
11
  match do |ability|
12
- ability.can?(*args)
12
+ actions = args.first
13
+ if actions.is_a? Array
14
+ break false if actions.empty?
15
+ actions.all? { |action| ability.can?(action, *args[1..-1]) }
16
+ else
17
+ ability.can?(*args)
18
+ end
13
19
  end
14
20
 
15
21
  # Check that RSpec is < 2.99
16
22
  if !respond_to?(:failure_message) && respond_to?(:failure_message_for_should)
17
- alias :failure_message :failure_message_for_should
18
- alias :failure_message_when_negated :failure_message_for_should_not
23
+ alias_method :failure_message, :failure_message_for_should
24
+ end
25
+
26
+ if !respond_to?(:failure_message_when_negated) && respond_to?(:failure_message_for_should_not)
27
+ alias_method :failure_message_when_negated, :failure_message_for_should_not
19
28
  end
20
29
 
21
30
  failure_message do
@@ -11,7 +11,7 @@ module CanCan
11
11
  end
12
12
 
13
13
  # Used to determine if the given adapter should be used for the passed in class.
14
- def self.for_class?(member_class)
14
+ def self.for_class?(_member_class)
15
15
  false # override in subclass
16
16
  end
17
17
 
@@ -22,24 +22,24 @@ module CanCan
22
22
 
23
23
  # Used to determine if this model adapter will override the matching behavior for a hash of conditions.
24
24
  # If this returns true then matches_conditions_hash? will be called. See Rule#matches_conditions_hash
25
- def self.override_conditions_hash_matching?(subject, conditions)
25
+ def self.override_conditions_hash_matching?(_subject, _conditions)
26
26
  false
27
27
  end
28
28
 
29
29
  # Override if override_conditions_hash_matching? returns true
30
- def self.matches_conditions_hash?(subject, conditions)
31
- raise NotImplemented, "This model adapter does not support matching on a conditions hash."
30
+ def self.matches_conditions_hash?(_subject, _conditions)
31
+ raise NotImplemented, 'This model adapter does not support matching on a conditions hash.'
32
32
  end
33
33
 
34
34
  # Used to determine if this model adapter will override the matching behavior for a specific condition.
35
35
  # If this returns true then matches_condition? will be called. See Rule#matches_conditions_hash
36
- def self.override_condition_matching?(subject, name, value)
36
+ def self.override_condition_matching?(_subject, _name, _value)
37
37
  false
38
38
  end
39
39
 
40
40
  # Override if override_condition_matching? returns true
41
- def self.matches_condition?(subject, name, value)
42
- raise NotImplemented, "This model adapter does not support matching on a specific condition."
41
+ def self.matches_condition?(_subject, _name, _value)
42
+ raise NotImplemented, 'This model adapter does not support matching on a specific condition.'
43
43
  end
44
44
 
45
45
  def initialize(model_class, rules)
@@ -49,7 +49,7 @@ module CanCan
49
49
 
50
50
  def database_records
51
51
  # This should be overridden in a subclass to return records which match @rules
52
- raise NotImplemented, "This model adapter does not support fetching records from the database."
52
+ raise NotImplemented, 'This model adapter does not support fetching records from the database.'
53
53
  end
54
54
  end
55
55
  end
@@ -3,7 +3,26 @@ module CanCan
3
3
  class ActiveRecord4Adapter < AbstractAdapter
4
4
  include ActiveRecordAdapter
5
5
  def self.for_class?(model_class)
6
- model_class <= ActiveRecord::Base
6
+ ActiveRecord::VERSION::MAJOR == 4 && model_class <= ActiveRecord::Base
7
+ end
8
+
9
+ # TODO: this should be private
10
+ def self.override_condition_matching?(subject, name, _value)
11
+ subject.class.defined_enums.include?(name.to_s)
12
+ end
13
+
14
+ # TODO: this should be private
15
+ def self.matches_condition?(subject, name, value)
16
+ # Get the mapping from enum strings to values.
17
+ enum = subject.class.send(name.to_s.pluralize)
18
+ # Get the value of the attribute as an integer.
19
+ attribute = enum[subject.send(name)]
20
+ # Check to see if the value matches the condition.
21
+ if value.is_a?(Enumerable)
22
+ value.include? attribute
23
+ else
24
+ attribute == value
25
+ end
7
26
  end
8
27
 
9
28
  private
@@ -20,19 +39,23 @@ module CanCan
20
39
 
21
40
  # Rails 4.2 deprecates `sanitize_sql_hash_for_conditions`
22
41
  def sanitize_sql(conditions)
23
- if ActiveRecord::VERSION::MINOR >= 2 && Hash === conditions
24
- table = Arel::Table.new(@model_class.send(:table_name))
25
-
26
- conditions = ActiveRecord::PredicateBuilder.resolve_column_aliases @model_class, conditions
27
- conditions = @model_class.send(:expand_hash_conditions_for_aggregates, conditions)
28
-
29
- ActiveRecord::PredicateBuilder.build_from_hash(@model_class, conditions, table).map { |b|
30
- @model_class.send(:connection).visitor.compile b
31
- }.join(' AND ')
42
+ if ActiveRecord::VERSION::MINOR >= 2 && conditions.is_a?(Hash)
43
+ sanitize_sql_activerecord4(conditions)
32
44
  else
33
45
  @model_class.send(:sanitize_sql, conditions)
34
46
  end
35
47
  end
48
+
49
+ def sanitize_sql_activerecord4(conditions)
50
+ table = Arel::Table.new(@model_class.send(:table_name))
51
+
52
+ conditions = ActiveRecord::PredicateBuilder.resolve_column_aliases @model_class, conditions
53
+ conditions = @model_class.send(:expand_hash_conditions_for_aggregates, conditions)
54
+
55
+ ActiveRecord::PredicateBuilder.build_from_hash(@model_class, conditions, table).map do |b|
56
+ @model_class.send(:connection).visitor.compile b
57
+ end.join(' AND ')
58
+ end
36
59
  end
37
60
  end
38
61
  end
@@ -0,0 +1,70 @@
1
+ module CanCan
2
+ module ModelAdapters
3
+ class ActiveRecord5Adapter < ActiveRecord4Adapter
4
+ AbstractAdapter.inherited(self)
5
+
6
+ def self.for_class?(model_class)
7
+ ActiveRecord::VERSION::MAJOR == 5 && model_class <= ActiveRecord::Base
8
+ end
9
+
10
+ # rails 5 is capable of using strings in enum
11
+ # but often people use symbols in rules
12
+ def self.matches_condition?(subject, name, value)
13
+ return super if Array.wrap(value).all? { |x| x.is_a? Integer }
14
+
15
+ attribute = subject.send(name)
16
+ if value.is_a?(Enumerable)
17
+ value.map(&:to_s).include? attribute
18
+ else
19
+ attribute == value.to_s
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ # As of rails 4, `includes()` no longer causes active record to
26
+ # look inside the where clause to decide to outer join tables
27
+ # you're using in the where. Instead, `references()` is required
28
+ # in addition to `includes()` to force the outer join.
29
+ def build_relation(*where_conditions)
30
+ relation = @model_class.where(*where_conditions)
31
+ relation = relation.includes(joins).references(joins) if joins.present?
32
+ relation
33
+ end
34
+
35
+ # Rails 4.2 deprecates `sanitize_sql_hash_for_conditions`
36
+ def sanitize_sql(conditions)
37
+ if conditions.is_a?(Hash)
38
+ sanitize_sql_activerecord5(conditions)
39
+ else
40
+ @model_class.send(:sanitize_sql, conditions)
41
+ end
42
+ end
43
+
44
+ def sanitize_sql_activerecord5(conditions)
45
+ table = @model_class.send(:arel_table)
46
+ table_metadata = ActiveRecord::TableMetadata.new(@model_class, table)
47
+ predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
48
+
49
+ conditions = predicate_builder.resolve_column_aliases(conditions)
50
+
51
+ conditions.stringify_keys!
52
+
53
+ predicate_builder.build_from_hash(conditions).map do |b|
54
+ visit_nodes(b)
55
+ end.join(' AND ')
56
+ end
57
+
58
+ def visit_nodes(b)
59
+ # Rails 5.2 adds a BindParam node that prevents the visitor method from properly compiling the SQL query
60
+ if ActiveRecord::VERSION::MINOR >= 2
61
+ connection = @model_class.send(:connection)
62
+ collector = Arel::Collectors::SubstituteBinds.new(connection, Arel::Collectors::SQLString.new)
63
+ connection.visitor.accept(b, collector).value
64
+ else
65
+ @model_class.send(:connection).visitor.compile(b)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -1,6 +1,11 @@
1
+ require_relative 'can_can/model_adapters/active_record_adapter/joins.rb'
2
+ require_relative 'conditions_extractor.rb'
3
+ require 'cancan/rules_compressor'
1
4
  module CanCan
2
5
  module ModelAdapters
3
6
  module ActiveRecordAdapter
7
+ include CanCan::ModelAdapters::ActiveRecordAdapter::Joins
8
+
4
9
  # Returns conditions intended to be used inside a database query. Normally you will not call this
5
10
  # method directly, but instead go through ModelAdditions#accessible_by.
6
11
  #
@@ -17,94 +22,69 @@ module CanCan
17
22
  # query(:manage, User).conditions # => "not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))"
18
23
  #
19
24
  def conditions
20
- if @rules.size == 1 && @rules.first.base_behavior
25
+ compressed_rules = RulesCompressor.new(@rules.reverse).rules_collapsed.reverse
26
+ conditions_extractor = ConditionsExtractor.new(@model_class)
27
+ if compressed_rules.size == 1 && compressed_rules.first.base_behavior
21
28
  # Return the conditions directly if there's just one definition
22
- tableized_conditions(@rules.first.conditions).dup
29
+ conditions_extractor.tableize_conditions(compressed_rules.first.conditions).dup
23
30
  else
24
- @rules.reverse.inject(false_sql) do |sql, rule|
25
- merge_conditions(sql, tableized_conditions(rule.conditions).dup, rule.base_behavior)
26
- end
31
+ extract_multiple_conditions(conditions_extractor, compressed_rules)
27
32
  end
28
33
  end
29
34
 
30
- def tableized_conditions(conditions, model_class = @model_class)
31
- return conditions unless conditions.kind_of? Hash
32
- conditions.inject({}) do |result_hash, (name, value)|
33
- if value.kind_of? Hash
34
- value = value.dup
35
- association_class = model_class.reflect_on_association(name).klass.name.constantize
36
- nested = value.inject({}) do |nested,(k,v)|
37
- if v.kind_of? Hash
38
- value.delete(k)
39
- nested[k] = v
40
- else
41
- result_hash[model_class.reflect_on_association(name).table_name.to_sym] = value
42
- end
43
- nested
44
- end
45
- result_hash.merge!(tableized_conditions(nested,association_class))
46
- else
47
- result_hash[name] = value
48
- end
49
- result_hash
35
+ def extract_multiple_conditions(conditions_extractor, rules)
36
+ rules.reverse.inject(false_sql) do |sql, rule|
37
+ merge_conditions(sql, conditions_extractor.tableize_conditions(rule.conditions).dup, rule.base_behavior)
50
38
  end
51
39
  end
52
40
 
53
- # Returns the associations used in conditions for the :joins option of a search.
54
- # See ModelAdditions#accessible_by
55
- def joins
56
- joins_hash = {}
57
- @rules.each do |rule|
58
- merge_joins(joins_hash, rule.associations_hash)
59
- end
60
- clean_joins(joins_hash) unless joins_hash.empty?
61
- end
62
-
63
41
  def database_records
64
42
  if override_scope
65
43
  @model_class.where(nil).merge(override_scope)
66
44
  elsif @model_class.respond_to?(:where) && @model_class.respond_to?(:joins)
67
- if mergeable_conditions?
68
- build_relation(conditions)
69
- else
70
- build_relation(*(@rules.map(&:conditions)))
71
- end
45
+ mergeable_conditions? ? build_relation(conditions) : build_relation(*@rules.map(&:conditions))
72
46
  else
73
- @model_class.all(:conditions => conditions, :joins => joins)
47
+ @model_class.all(conditions: conditions, joins: joins)
74
48
  end
75
49
  end
76
50
 
77
51
  private
78
52
 
79
53
  def mergeable_conditions?
80
- @rules.find {|rule| rule.unmergeable? }.blank?
54
+ @rules.find(&:unmergeable?).blank?
81
55
  end
82
56
 
83
57
  def override_scope
84
58
  conditions = @rules.map(&:conditions).compact
85
- if defined?(ActiveRecord::Relation) && conditions.any? { |c| c.kind_of?(ActiveRecord::Relation) }
86
- if conditions.size == 1
87
- conditions.first
88
- else
89
- rule = @rules.detect { |rule| rule.conditions.kind_of?(ActiveRecord::Relation) }
90
- raise Error, "Unable to merge an Active Record scope with other conditions. Instead use a hash or SQL for #{rule.actions.first} #{rule.subjects.first} ability."
91
- end
92
- end
59
+ return unless conditions.any? { |c| c.is_a?(ActiveRecord::Relation) }
60
+ return conditions.first if conditions.size == 1
61
+ raise_override_scope_error
62
+ end
63
+
64
+ def raise_override_scope_error
65
+ rule_found = @rules.detect { |rule| rule.conditions.is_a?(ActiveRecord::Relation) }
66
+ raise Error,
67
+ 'Unable to merge an Active Record scope with other conditions. '\
68
+ "Instead use a hash or SQL for #{rule_found.actions.first} #{rule_found.subjects.first} ability."
93
69
  end
94
70
 
95
71
  def merge_conditions(sql, conditions_hash, behavior)
96
72
  if conditions_hash.blank?
97
73
  behavior ? true_sql : false_sql
98
74
  else
99
- conditions = sanitize_sql(conditions_hash)
100
- case sql
101
- when true_sql
102
- behavior ? true_sql : "not (#{conditions})"
103
- when false_sql
104
- behavior ? conditions : false_sql
105
- else
106
- behavior ? "(#{conditions}) OR (#{sql})" : "not (#{conditions}) AND (#{sql})"
107
- end
75
+ merge_non_empty_conditions(behavior, conditions_hash, sql)
76
+ end
77
+ end
78
+
79
+ def merge_non_empty_conditions(behavior, conditions_hash, sql)
80
+ conditions = sanitize_sql(conditions_hash)
81
+ case sql
82
+ when true_sql
83
+ behavior ? true_sql : "not (#{conditions})"
84
+ when false_sql
85
+ behavior ? conditions : false_sql
86
+ else
87
+ behavior ? "(#{conditions}) OR (#{sql})" : "not (#{conditions}) AND (#{sql})"
108
88
  end
109
89
  end
110
90
 
@@ -119,30 +99,10 @@ module CanCan
119
99
  def sanitize_sql(conditions)
120
100
  @model_class.send(:sanitize_sql, conditions)
121
101
  end
122
-
123
- # Takes two hashes and does a deep merge.
124
- def merge_joins(base, add)
125
- add.each do |name, nested|
126
- if base[name].is_a?(Hash)
127
- merge_joins(base[name], nested) unless nested.empty?
128
- else
129
- base[name] = nested
130
- end
131
- end
132
- end
133
-
134
- # Removes empty hashes and moves everything into arrays.
135
- def clean_joins(joins_hash)
136
- joins = []
137
- joins_hash.each do |name, nested|
138
- joins << (nested.empty? ? name : {name => clean_joins(nested)})
139
- end
140
- joins
141
- end
142
102
  end
143
103
  end
144
104
  end
145
105
 
146
- ActiveRecord::Base.class_eval do
147
- include CanCan::ModelAdditions
106
+ ActiveSupport.on_load(:active_record) do
107
+ send :include, CanCan::ModelAdditions
148
108
  end
@@ -0,0 +1,39 @@
1
+ module CanCan
2
+ module ModelAdapters
3
+ module ActiveRecordAdapter
4
+ module Joins
5
+ # Returns the associations used in conditions for the :joins option of a search.
6
+ # See ModelAdditions#accessible_by
7
+ def joins
8
+ joins_hash = {}
9
+ @rules.reverse.each do |rule|
10
+ merge_joins(joins_hash, rule.associations_hash)
11
+ end
12
+ clean_joins(joins_hash) unless joins_hash.empty?
13
+ end
14
+
15
+ private
16
+
17
+ # Removes empty hashes and moves everything into arrays.
18
+ def clean_joins(joins_hash)
19
+ joins = []
20
+ joins_hash.each do |name, nested|
21
+ joins << (nested.empty? ? name : { name => clean_joins(nested) })
22
+ end
23
+ joins
24
+ end
25
+
26
+ # Takes two hashes and does a deep merge.
27
+ def merge_joins(base, add)
28
+ add.each do |name, nested|
29
+ if base[name].is_a?(Hash)
30
+ merge_joins(base[name], nested) unless nested.empty?
31
+ else
32
+ base[name] = nested
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end