cancancan 1.15.0 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +5 -5
  2. data/cancancan.gemspec +18 -18
  3. data/init.rb +2 -0
  4. data/lib/cancan.rb +9 -11
  5. data/lib/cancan/ability.rb +90 -203
  6. data/lib/cancan/ability/actions.rb +93 -0
  7. data/lib/cancan/ability/rules.rb +93 -0
  8. data/lib/cancan/ability/strong_parameter_support.rb +41 -0
  9. data/lib/cancan/conditions_matcher.rb +106 -0
  10. data/lib/cancan/controller_additions.rb +29 -36
  11. data/lib/cancan/controller_resource.rb +46 -211
  12. data/lib/cancan/controller_resource_builder.rb +26 -0
  13. data/lib/cancan/controller_resource_finder.rb +42 -0
  14. data/lib/cancan/controller_resource_loader.rb +120 -0
  15. data/lib/cancan/controller_resource_name_finder.rb +23 -0
  16. data/lib/cancan/controller_resource_sanitizer.rb +32 -0
  17. data/lib/cancan/exceptions.rb +17 -5
  18. data/lib/cancan/matchers.rb +12 -3
  19. data/lib/cancan/model_adapters/abstract_adapter.rb +10 -8
  20. data/lib/cancan/model_adapters/active_record_4_adapter.rb +39 -43
  21. data/lib/cancan/model_adapters/active_record_5_adapter.rb +68 -0
  22. data/lib/cancan/model_adapters/active_record_adapter.rb +77 -82
  23. data/lib/cancan/model_adapters/conditions_extractor.rb +75 -0
  24. data/lib/cancan/model_adapters/conditions_normalizer.rb +49 -0
  25. data/lib/cancan/model_adapters/default_adapter.rb +2 -0
  26. data/lib/cancan/model_additions.rb +2 -1
  27. data/lib/cancan/parameter_validators.rb +9 -0
  28. data/lib/cancan/relevant.rb +29 -0
  29. data/lib/cancan/rule.rb +76 -106
  30. data/lib/cancan/rules_compressor.rb +23 -0
  31. data/lib/cancan/unauthorized_message_resolver.rb +24 -0
  32. data/lib/cancan/version.rb +3 -1
  33. data/lib/cancancan.rb +2 -0
  34. data/lib/generators/cancan/ability/ability_generator.rb +4 -2
  35. data/lib/generators/cancan/ability/templates/ability.rb +2 -0
  36. metadata +66 -57
  37. data/.gitignore +0 -15
  38. data/.rspec +0 -1
  39. data/.travis.yml +0 -33
  40. data/Appraisals +0 -104
  41. data/CHANGELOG.rdoc +0 -527
  42. data/CONTRIBUTING.md +0 -23
  43. data/Gemfile +0 -3
  44. data/LICENSE +0 -22
  45. data/README.md +0 -217
  46. data/Rakefile +0 -9
  47. data/gemfiles/activerecord_3.2.gemfile +0 -17
  48. data/gemfiles/activerecord_4.0.gemfile +0 -18
  49. data/gemfiles/activerecord_4.1.gemfile +0 -18
  50. data/gemfiles/activerecord_4.2.gemfile +0 -19
  51. data/gemfiles/activerecord_5.0.gemfile +0 -19
  52. data/gemfiles/mongoid_2.x.gemfile +0 -17
  53. data/gemfiles/sequel_3.x.gemfile +0 -17
  54. data/lib/cancan/inherited_resource.rb +0 -20
  55. data/lib/cancan/model_adapters/active_record_3_adapter.rb +0 -16
  56. data/lib/cancan/model_adapters/mongoid_adapter.rb +0 -75
  57. data/lib/cancan/model_adapters/sequel_adapter.rb +0 -87
  58. data/spec/README.rdoc +0 -27
  59. data/spec/cancan/ability_spec.rb +0 -544
  60. data/spec/cancan/controller_additions_spec.rb +0 -151
  61. data/spec/cancan/controller_resource_spec.rb +0 -643
  62. data/spec/cancan/exceptions_spec.rb +0 -58
  63. data/spec/cancan/inherited_resource_spec.rb +0 -71
  64. data/spec/cancan/matchers_spec.rb +0 -29
  65. data/spec/cancan/model_adapters/active_record_4_adapter_spec.rb +0 -154
  66. data/spec/cancan/model_adapters/active_record_adapter_spec.rb +0 -405
  67. data/spec/cancan/model_adapters/default_adapter_spec.rb +0 -7
  68. data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +0 -247
  69. data/spec/cancan/model_adapters/sequel_adapter_spec.rb +0 -132
  70. data/spec/cancan/rule_spec.rb +0 -52
  71. data/spec/matchers.rb +0 -13
  72. data/spec/spec.opts +0 -2
  73. data/spec/spec_helper.rb +0 -27
  74. data/spec/support/ability.rb +0 -7
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanCan
4
+ module ControllerResourceNameFinder
5
+ protected
6
+
7
+ def name_from_controller
8
+ @params[:controller].split('/').last.singularize
9
+ end
10
+
11
+ def namespaced_name
12
+ [namespace, name].join('/').singularize.camelize.safe_constantize || name
13
+ end
14
+
15
+ def name
16
+ @name || name_from_controller
17
+ end
18
+
19
+ def namespace
20
+ @params[:controller].split('/')[0..-2]
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanCan
4
+ module ControllerResourceSanitizer
5
+ protected
6
+
7
+ def sanitize_parameters
8
+ case params_method
9
+ when Symbol
10
+ @controller.send(params_method)
11
+ when String
12
+ @controller.instance_eval(params_method)
13
+ when Proc
14
+ params_method.call(@controller)
15
+ end
16
+ end
17
+
18
+ def params_methods
19
+ methods = ["#{@params[:action]}_params".to_sym, "#{name}_params".to_sym, :resource_params]
20
+ methods.unshift(@options[:param_method]) if @options[:param_method].present?
21
+ methods
22
+ end
23
+
24
+ def params_method
25
+ params_methods.each do |method|
26
+ return method if (method.is_a?(Symbol) && @controller.respond_to?(method, true)) ||
27
+ method.is_a?(String) || method.is_a?(Proc)
28
+ end
29
+ nil
30
+ end
31
+ end
32
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  # A general CanCan exception
3
5
  class Error < StandardError; end
@@ -8,9 +10,18 @@ module CanCan
8
10
  # Raised when removed code is called, an alternative solution is provided in message.
9
11
  class ImplementationRemoved < Error; end
10
12
 
11
- # Raised when using check_authorization without calling authorized!
13
+ # Raised when using check_authorization without calling authorize!
12
14
  class AuthorizationNotPerformed < Error; end
13
15
 
16
+ # Raised when a rule is created with both a block and a hash of conditions
17
+ class BlockAndConditionsError < Error; end
18
+
19
+ # Raised when an unexpected argument is passed as an attribute
20
+ class AttributeArgumentError < Error; end
21
+
22
+ # Raised when using a wrong association name
23
+ class WrongAssociationName < Error; end
24
+
14
25
  # This error is raised when a user isn't allowed to access a given controller action.
15
26
  # This usually happens within a call to ControllerAdditions#authorize! but can be
16
27
  # raised manually.
@@ -30,17 +41,18 @@ module CanCan
30
41
  # exception.default_message = "Default error message"
31
42
  # exception.message # => "Default error message"
32
43
  #
33
- # See ControllerAdditions#authorized! for more information on rescuing from this exception
44
+ # See ControllerAdditions#authorize! for more information on rescuing from this exception
34
45
  # and customizing the message using I18n.
35
46
  class AccessDenied < Error
36
- attr_reader :action, :subject
47
+ attr_reader :action, :subject, :conditions
37
48
  attr_writer :default_message
38
49
 
39
- def initialize(message = nil, action = nil, subject = nil)
50
+ def initialize(message = nil, action = nil, subject = nil, conditions = nil)
40
51
  @message = message
41
52
  @action = action
42
53
  @subject = subject
43
- @default_message = I18n.t(:"unauthorized.default", :default => "You are not authorized to access this page.")
54
+ @conditions = conditions
55
+ @default_message = I18n.t(:"unauthorized.default", default: 'You are not authorized to access this page.')
44
56
  end
45
57
 
46
58
  def to_s
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  rspec_module = defined?(RSpec::Core) ? 'RSpec' : 'Spec' # RSpec 1 compatability
2
4
 
3
5
  if rspec_module == 'RSpec'
@@ -9,16 +11,23 @@ end
9
11
 
10
12
  Kernel.const_get(rspec_module)::Matchers.define :be_able_to do |*args|
11
13
  match do |ability|
12
- ability.can?(*args)
14
+ actions = args.first
15
+ if actions.is_a? Array
16
+ break false if actions.empty?
17
+
18
+ actions.all? { |action| ability.can?(action, *args[1..-1]) }
19
+ else
20
+ ability.can?(*args)
21
+ end
13
22
  end
14
23
 
15
24
  # Check that RSpec is < 2.99
16
25
  if !respond_to?(:failure_message) && respond_to?(:failure_message_for_should)
17
- alias :failure_message :failure_message_for_should
26
+ alias_method :failure_message, :failure_message_for_should
18
27
  end
19
28
 
20
29
  if !respond_to?(:failure_message_when_negated) && respond_to?(:failure_message_for_should_not)
21
- alias :failure_message_when_negated :failure_message_for_should_not
30
+ alias_method :failure_message_when_negated, :failure_message_for_should_not
22
31
  end
23
32
 
24
33
  failure_message do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  module ModelAdapters
3
5
  class AbstractAdapter
@@ -11,7 +13,7 @@ module CanCan
11
13
  end
12
14
 
13
15
  # Used to determine if the given adapter should be used for the passed in class.
14
- def self.for_class?(member_class)
16
+ def self.for_class?(_member_class)
15
17
  false # override in subclass
16
18
  end
17
19
 
@@ -22,24 +24,24 @@ module CanCan
22
24
 
23
25
  # Used to determine if this model adapter will override the matching behavior for a hash of conditions.
24
26
  # 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)
27
+ def self.override_conditions_hash_matching?(_subject, _conditions)
26
28
  false
27
29
  end
28
30
 
29
31
  # 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."
32
+ def self.matches_conditions_hash?(_subject, _conditions)
33
+ raise NotImplemented, 'This model adapter does not support matching on a conditions hash.'
32
34
  end
33
35
 
34
36
  # Used to determine if this model adapter will override the matching behavior for a specific condition.
35
37
  # If this returns true then matches_condition? will be called. See Rule#matches_conditions_hash
36
- def self.override_condition_matching?(subject, name, value)
38
+ def self.override_condition_matching?(_subject, _name, _value)
37
39
  false
38
40
  end
39
41
 
40
42
  # 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."
43
+ def self.matches_condition?(_subject, _name, _value)
44
+ raise NotImplemented, 'This model adapter does not support matching on a specific condition.'
43
45
  end
44
46
 
45
47
  def initialize(model_class, rules)
@@ -49,7 +51,7 @@ module CanCan
49
51
 
50
52
  def database_records
51
53
  # 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."
54
+ raise NotImplemented, 'This model adapter does not support fetching records from the database.'
53
55
  end
54
56
  end
55
57
  end
@@ -1,9 +1,31 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CanCan
2
4
  module ModelAdapters
3
- class ActiveRecord4Adapter < AbstractAdapter
4
- include ActiveRecordAdapter
5
- def self.for_class?(model_class)
6
- model_class <= ActiveRecord::Base
5
+ class ActiveRecord4Adapter < ActiveRecordAdapter
6
+ AbstractAdapter.inherited(self)
7
+
8
+ class << self
9
+ def for_class?(model_class)
10
+ version_lower?('5.0.0') && model_class <= ActiveRecord::Base
11
+ end
12
+
13
+ def override_condition_matching?(subject, name, _value)
14
+ subject.class.defined_enums.include?(name.to_s)
15
+ end
16
+
17
+ def matches_condition?(subject, name, value)
18
+ # Get the mapping from enum strings to values.
19
+ enum = subject.class.send(name.to_s.pluralize)
20
+ # Get the value of the attribute as an integer.
21
+ attribute = enum[subject.send(name)]
22
+ # Check to see if the value matches the condition.
23
+ if value.is_a?(Enumerable)
24
+ value.include? attribute
25
+ else
26
+ attribute == value
27
+ end
28
+ end
7
29
  end
8
30
 
9
31
  private
@@ -18,51 +40,25 @@ module CanCan
18
40
  relation
19
41
  end
20
42
 
21
- def self.override_condition_matching?(subject, name, value)
22
- # ActiveRecord introduced enums in version 4.1.
23
- (ActiveRecord::VERSION::MAJOR > 4 || ActiveRecord::VERSION::MINOR >= 1) &&
24
- subject.class.defined_enums.include?(name.to_s)
25
- end
26
-
27
- def self.matches_condition?(subject, name, value)
28
- # Get the mapping from enum strings to values.
29
- enum = subject.class.send(name.to_s.pluralize)
30
- # Get the value of the attribute as an integer.
31
- attribute = enum[subject.send(name)]
32
- # Check to see if the value matches the condition.
33
- value.is_a?(Enumerable) ?
34
- (value.include? attribute) :
35
- attribute == value
36
- end
37
-
38
43
  # Rails 4.2 deprecates `sanitize_sql_hash_for_conditions`
39
44
  def sanitize_sql(conditions)
40
- if ActiveRecord::VERSION::MAJOR > 4 && Hash === conditions
41
- table = @model_class.send(:arel_table)
42
- table_metadata = ActiveRecord::TableMetadata.new(@model_class, table)
43
- predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
44
-
45
- conditions = predicate_builder.resolve_column_aliases(conditions)
46
- conditions = @model_class.send(:expand_hash_conditions_for_aggregates, conditions)
47
-
48
- conditions.stringify_keys!
49
-
50
- predicate_builder.build_from_hash(conditions).map { |b|
51
- @model_class.send(:connection).visitor.compile b
52
- }.join(' AND ')
53
- elsif ActiveRecord::VERSION::MINOR >= 2 && Hash === conditions
54
- table = Arel::Table.new(@model_class.send(:table_name))
55
-
56
- conditions = ActiveRecord::PredicateBuilder.resolve_column_aliases @model_class, conditions
57
- conditions = @model_class.send(:expand_hash_conditions_for_aggregates, conditions)
58
-
59
- ActiveRecord::PredicateBuilder.build_from_hash(@model_class, conditions, table).map { |b|
60
- @model_class.send(:connection).visitor.compile b
61
- }.join(' AND ')
45
+ if self.class.version_greater_or_equal?('4.2.0') && conditions.is_a?(Hash)
46
+ sanitize_sql_activerecord4(conditions)
62
47
  else
63
48
  @model_class.send(:sanitize_sql, conditions)
64
49
  end
65
50
  end
51
+
52
+ def sanitize_sql_activerecord4(conditions)
53
+ table = Arel::Table.new(@model_class.send(:table_name))
54
+
55
+ conditions = ActiveRecord::PredicateBuilder.resolve_column_aliases @model_class, conditions
56
+ conditions = @model_class.send(:expand_hash_conditions_for_aggregates, conditions)
57
+
58
+ ActiveRecord::PredicateBuilder.build_from_hash(@model_class, conditions, table).map do |b|
59
+ @model_class.send(:connection).visitor.compile b
60
+ end.join(' AND ')
61
+ end
66
62
  end
67
63
  end
68
64
  end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanCan
4
+ module ModelAdapters
5
+ class ActiveRecord5Adapter < ActiveRecord4Adapter
6
+ AbstractAdapter.inherited(self)
7
+
8
+ def self.for_class?(model_class)
9
+ version_greater_or_equal?('5.0.0') && model_class <= ActiveRecord::Base
10
+ end
11
+
12
+ # rails 5 is capable of using strings in enum
13
+ # but often people use symbols in rules
14
+ def self.matches_condition?(subject, name, value)
15
+ return super if Array.wrap(value).all? { |x| x.is_a? Integer }
16
+
17
+ attribute = subject.send(name)
18
+ raw_attribute = subject.class.send(name.to_s.pluralize)[attribute]
19
+ !(Array(value).map(&:to_s) & [attribute, raw_attribute]).empty?
20
+ end
21
+
22
+ private
23
+
24
+ def build_relation(*where_conditions)
25
+ if joins.present?
26
+ inner = @model_class.unscoped do
27
+ @model_class.left_joins(joins).where(*where_conditions)
28
+ end
29
+ @model_class.where(@model_class.primary_key => inner)
30
+ else
31
+ @model_class.where(*where_conditions)
32
+ end
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 { |b| visit_nodes(b) }.join(' AND ')
54
+ end
55
+
56
+ def visit_nodes(node)
57
+ # Rails 5.2 adds a BindParam node that prevents the visitor method from properly compiling the SQL query
58
+ if self.class.version_greater_or_equal?('5.2.0')
59
+ connection = @model_class.send(:connection)
60
+ collector = Arel::Collectors::SubstituteBinds.new(connection, Arel::Collectors::SQLString.new)
61
+ connection.visitor.accept(node, collector).value
62
+ else
63
+ @model_class.send(:connection).visitor.compile(node)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -1,6 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'conditions_extractor.rb'
4
+ require 'cancan/rules_compressor'
1
5
  module CanCan
2
6
  module ModelAdapters
3
- module ActiveRecordAdapter
7
+ class ActiveRecordAdapter < AbstractAdapter
8
+ def self.version_greater_or_equal?(version)
9
+ Gem::Version.new(ActiveRecord.version).release >= Gem::Version.new(version)
10
+ end
11
+
12
+ def self.version_lower?(version)
13
+ Gem::Version.new(ActiveRecord.version).release < Gem::Version.new(version)
14
+ end
15
+
16
+ def initialize(model_class, rules)
17
+ super
18
+ @compressed_rules = RulesCompressor.new(@rules.reverse).rules_collapsed.reverse
19
+ ConditionsNormalizer.normalize(model_class, @compressed_rules)
20
+ end
21
+
4
22
  # Returns conditions intended to be used inside a database query. Normally you will not call this
5
23
  # method directly, but instead go through ModelAdditions#accessible_by.
6
24
  #
@@ -17,94 +35,91 @@ module CanCan
17
35
  # query(:manage, User).conditions # => "not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))"
18
36
  #
19
37
  def conditions
20
- if @rules.size == 1 && @rules.first.base_behavior
38
+ conditions_extractor = ConditionsExtractor.new(@model_class)
39
+ if @compressed_rules.size == 1 && @compressed_rules.first.base_behavior
21
40
  # Return the conditions directly if there's just one definition
22
- tableized_conditions(@rules.first.conditions).dup
41
+ conditions_extractor.tableize_conditions(@compressed_rules.first.conditions).dup
23
42
  else
24
- @rules.reverse.inject(false_sql) do |sql, rule|
25
- merge_conditions(sql, tableized_conditions(rule.conditions).dup, rule.base_behavior)
26
- end
27
- end
28
- end
29
-
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
43
+ extract_multiple_conditions(conditions_extractor, @compressed_rules)
50
44
  end
51
45
  end
52
46
 
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)
47
+ def extract_multiple_conditions(conditions_extractor, rules)
48
+ rules.reverse.inject(false_sql) do |sql, rule|
49
+ merge_conditions(sql, conditions_extractor.tableize_conditions(rule.conditions).dup, rule.base_behavior)
59
50
  end
60
- clean_joins(joins_hash) unless joins_hash.empty?
61
51
  end
62
52
 
63
53
  def database_records
64
54
  if override_scope
65
55
  @model_class.where(nil).merge(override_scope)
66
56
  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
57
+ build_relation(conditions)
72
58
  else
73
- @model_class.all(:conditions => conditions, :joins => joins)
59
+ @model_class.all(conditions: conditions, joins: joins)
60
+ end
61
+ end
62
+
63
+ # Returns the associations used in conditions for the :joins option of a search.
64
+ # See ModelAdditions#accessible_by
65
+ def joins
66
+ joins_hash = {}
67
+ @compressed_rules.reverse_each do |rule|
68
+ deep_merge(joins_hash, rule.associations_hash)
74
69
  end
70
+ deep_clean(joins_hash) unless joins_hash.empty?
75
71
  end
76
72
 
77
73
  private
78
74
 
79
- def mergeable_conditions?
80
- @rules.find {|rule| rule.unmergeable? }.blank?
75
+ # Removes empty hashes and moves everything into arrays.
76
+ def deep_clean(joins_hash)
77
+ joins_hash.map { |name, nested| nested.empty? ? name : { name => deep_clean(nested) } }
81
78
  end
82
79
 
83
- def override_scope
84
- 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
80
+ # Takes two hashes and does a deep merge.
81
+ def deep_merge(base_hash, added_hash)
82
+ added_hash.each do |key, value|
83
+ if base_hash[key].is_a?(Hash)
84
+ deep_merge(base_hash[key], value) unless value.empty?
88
85
  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."
86
+ base_hash[key] = value
91
87
  end
92
88
  end
93
89
  end
94
90
 
91
+ def override_scope
92
+ conditions = @compressed_rules.map(&:conditions).compact
93
+ return unless conditions.any? { |c| c.is_a?(ActiveRecord::Relation) }
94
+ return conditions.first if conditions.size == 1
95
+
96
+ raise_override_scope_error
97
+ end
98
+
99
+ def raise_override_scope_error
100
+ rule_found = @compressed_rules.detect { |rule| rule.conditions.is_a?(ActiveRecord::Relation) }
101
+ raise Error,
102
+ 'Unable to merge an Active Record scope with other conditions. '\
103
+ "Instead use a hash or SQL for #{rule_found.actions.first} #{rule_found.subjects.first} ability."
104
+ end
105
+
95
106
  def merge_conditions(sql, conditions_hash, behavior)
96
107
  if conditions_hash.blank?
97
108
  behavior ? true_sql : false_sql
98
109
  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
110
+ merge_non_empty_conditions(behavior, conditions_hash, sql)
111
+ end
112
+ end
113
+
114
+ def merge_non_empty_conditions(behavior, conditions_hash, sql)
115
+ conditions = sanitize_sql(conditions_hash)
116
+ case sql
117
+ when true_sql
118
+ behavior ? true_sql : "not (#{conditions})"
119
+ when false_sql
120
+ behavior ? conditions : false_sql
121
+ else
122
+ behavior ? "(#{conditions}) OR (#{sql})" : "not (#{conditions}) AND (#{sql})"
108
123
  end
109
124
  end
110
125
 
@@ -119,30 +134,10 @@ module CanCan
119
134
  def sanitize_sql(conditions)
120
135
  @model_class.send(:sanitize_sql, conditions)
121
136
  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
137
  end
143
138
  end
144
139
  end
145
140
 
146
- ActiveRecord::Base.class_eval do
147
- include CanCan::ModelAdditions
141
+ ActiveSupport.on_load(:active_record) do
142
+ send :include, CanCan::ModelAdditions
148
143
  end