cancancan 1.11.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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