marnen-cancan 2.0.0.alpha.pre.f1cebde51a87be149b4970a3287826bb63c0ac0b
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.
- checksums.yaml +15 -0
- data/CHANGELOG.rdoc +381 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.rdoc +108 -0
- data/Rakefile +18 -0
- data/init.rb +1 -0
- data/lib/cancan.rb +13 -0
- data/lib/cancan/ability.rb +348 -0
- data/lib/cancan/controller_additions.rb +392 -0
- data/lib/cancan/controller_resource.rb +265 -0
- data/lib/cancan/exceptions.rb +53 -0
- data/lib/cancan/inherited_resource.rb +20 -0
- data/lib/cancan/matchers.rb +14 -0
- data/lib/cancan/model_adapters/abstract_adapter.rb +56 -0
- data/lib/cancan/model_adapters/active_record_adapter.rb +172 -0
- data/lib/cancan/model_adapters/data_mapper_adapter.rb +34 -0
- data/lib/cancan/model_adapters/default_adapter.rb +7 -0
- data/lib/cancan/model_adapters/mongoid_adapter.rb +54 -0
- data/lib/cancan/model_additions.rb +29 -0
- data/lib/cancan/rule.rb +178 -0
- data/lib/generators/cancan/ability/USAGE +5 -0
- data/lib/generators/cancan/ability/ability_generator.rb +16 -0
- data/lib/generators/cancan/ability/templates/ability.rb +24 -0
- data/lib/generators/cancan/ability/templates/ability_spec.rb +16 -0
- data/lib/generators/cancan/ability/templates/ability_test.rb +10 -0
- data/spec/README.rdoc +28 -0
- data/spec/cancan/ability_spec.rb +541 -0
- data/spec/cancan/controller_additions_spec.rb +118 -0
- data/spec/cancan/controller_resource_spec.rb +535 -0
- data/spec/cancan/exceptions_spec.rb +58 -0
- data/spec/cancan/inherited_resource_spec.rb +58 -0
- data/spec/cancan/matchers_spec.rb +33 -0
- data/spec/cancan/model_adapters/active_record_adapter_spec.rb +278 -0
- data/spec/cancan/model_adapters/data_mapper_adapter_spec.rb +120 -0
- data/spec/cancan/model_adapters/default_adapter_spec.rb +7 -0
- data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +227 -0
- data/spec/cancan/rule_spec.rb +55 -0
- data/spec/matchers.rb +13 -0
- data/spec/spec_helper.rb +49 -0
- metadata +197 -0
@@ -0,0 +1,265 @@
|
|
1
|
+
module CanCan
|
2
|
+
# Handle the load and authorization controller logic so we don't clutter up all controllers with non-interface methods.
|
3
|
+
# This class is used internally, so you do not need to call methods directly on it.
|
4
|
+
class ControllerResource # :nodoc:
|
5
|
+
def self.add_before_filter(controller_class, behavior, *args)
|
6
|
+
options = args.extract_options!.merge(behavior)
|
7
|
+
resource_name = args.first
|
8
|
+
before_filter_method = options.delete(:prepend) ? :prepend_before_filter : :before_filter
|
9
|
+
controller_class.send(before_filter_method, options.slice(:only, :except, :if, :unless)) do |controller|
|
10
|
+
controller.class.cancan_resource_class.new(controller, resource_name, options.except(:only, :except, :if, :unless)).process
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(controller, *args)
|
15
|
+
@controller = controller
|
16
|
+
@params = controller.params
|
17
|
+
@options = args.extract_options!
|
18
|
+
@name = args.first
|
19
|
+
end
|
20
|
+
|
21
|
+
def process
|
22
|
+
if @options[:load]
|
23
|
+
if load_instance?
|
24
|
+
self.resource_instance ||= load_resource_instance
|
25
|
+
elsif load_collection?
|
26
|
+
self.collection_instance ||= load_collection
|
27
|
+
current_ability.fully_authorized! @params[:action], @params[:controller]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
if @options[:authorize]
|
31
|
+
if resource_instance
|
32
|
+
if resource_params && (authorization_action == :create || authorization_action == :update)
|
33
|
+
resource_params.each do |key, value|
|
34
|
+
@controller.authorize!(authorization_action, resource_instance, key.to_sym)
|
35
|
+
end
|
36
|
+
else
|
37
|
+
@controller.authorize!(authorization_action, resource_instance)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def parent?
|
44
|
+
@options.has_key?(:parent) ? @options[:parent] : @name && @name != name_from_controller.to_sym
|
45
|
+
end
|
46
|
+
|
47
|
+
# def skip?(behavior) # This could probably use some refactoring
|
48
|
+
# options = @controller.class.cancan_skipper[behavior][@name]
|
49
|
+
# if options.nil?
|
50
|
+
# false
|
51
|
+
# elsif options == {}
|
52
|
+
# true
|
53
|
+
# elsif options[:except] && ![options[:except]].flatten.include?(@params[:action].to_sym)
|
54
|
+
# true
|
55
|
+
# elsif [options[:only]].flatten.include?(@params[:action].to_sym)
|
56
|
+
# true
|
57
|
+
# end
|
58
|
+
# end
|
59
|
+
|
60
|
+
protected
|
61
|
+
|
62
|
+
def load_resource_instance
|
63
|
+
if !parent? && new_actions.include?(@params[:action].to_sym)
|
64
|
+
build_resource
|
65
|
+
elsif id_param || @options[:singleton]
|
66
|
+
find_and_update_resource
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def load_instance?
|
71
|
+
parent? || member_action?
|
72
|
+
end
|
73
|
+
|
74
|
+
def load_collection?
|
75
|
+
resource_base.respond_to?(:accessible_by) && !current_ability.has_block?(authorization_action, subject_name)
|
76
|
+
end
|
77
|
+
|
78
|
+
def load_collection
|
79
|
+
resource_base.accessible_by(current_ability, authorization_action)
|
80
|
+
end
|
81
|
+
|
82
|
+
def build_resource
|
83
|
+
resource = resource_base.new(resource_params || {})
|
84
|
+
assign_attributes(resource)
|
85
|
+
end
|
86
|
+
|
87
|
+
def assign_attributes(resource)
|
88
|
+
resource.send("#{parent_name}=", parent_resource) if @options[:singleton] && parent_resource
|
89
|
+
initial_attributes.each do |attr_name, value|
|
90
|
+
resource.send("#{attr_name}=", value)
|
91
|
+
end
|
92
|
+
resource
|
93
|
+
end
|
94
|
+
|
95
|
+
def initial_attributes
|
96
|
+
current_ability.attributes_for(@params[:action].to_sym, subject_name).delete_if do |key, value|
|
97
|
+
resource_params && resource_params.include?(key)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def find_and_update_resource
|
102
|
+
resource = find_resource
|
103
|
+
if resource_params
|
104
|
+
@controller.authorize!(authorization_action, resource) if @options[:authorize]
|
105
|
+
resource.attributes = resource_params
|
106
|
+
end
|
107
|
+
resource
|
108
|
+
end
|
109
|
+
|
110
|
+
def find_resource
|
111
|
+
if @options[:singleton] && parent_resource.respond_to?(name)
|
112
|
+
parent_resource.send(name)
|
113
|
+
else
|
114
|
+
if @options[:find_by]
|
115
|
+
if resource_base.respond_to? "find_by_#{@options[:find_by]}!"
|
116
|
+
resource_base.send("find_by_#{@options[:find_by]}!", id_param)
|
117
|
+
else
|
118
|
+
resource_base.send(@options[:find_by], id_param)
|
119
|
+
end
|
120
|
+
else
|
121
|
+
adapter.find(resource_base, id_param)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def adapter
|
127
|
+
ModelAdapters::AbstractAdapter.adapter_class(resource_class)
|
128
|
+
end
|
129
|
+
|
130
|
+
def authorization_action
|
131
|
+
parent? ? :show : @params[:action].to_sym
|
132
|
+
end
|
133
|
+
|
134
|
+
def id_param
|
135
|
+
if @options[:id_param]
|
136
|
+
@params[@options[:id_param]]
|
137
|
+
else
|
138
|
+
@params[parent? ? :"#{name}_id" : :id]
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def member_action?
|
143
|
+
new_actions.include?(@params[:action].to_sym) || @options[:singleton] || ( (@params[:id] || @params[@options[:id_param]]) && !collection_actions.include?(@params[:action].to_sym))
|
144
|
+
end
|
145
|
+
|
146
|
+
# Returns the class used for this resource. This can be overriden by the :class option.
|
147
|
+
# If +false+ is passed in it will use the resource name as a symbol in which case it should
|
148
|
+
# only be used for authorization, not loading since there's no class to load through.
|
149
|
+
def resource_class
|
150
|
+
case @options[:class]
|
151
|
+
when false then name.to_sym
|
152
|
+
when nil then namespaced_name.to_s.camelize.constantize
|
153
|
+
when String then @options[:class].constantize
|
154
|
+
else @options[:class]
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def subject_name
|
159
|
+
resource_class.to_s.underscore.pluralize.to_sym
|
160
|
+
end
|
161
|
+
|
162
|
+
def subject_name_with_parent
|
163
|
+
parent_resource ? {parent_resource => subject_name} : subject_name
|
164
|
+
end
|
165
|
+
|
166
|
+
def resource_instance=(instance)
|
167
|
+
@controller.instance_variable_set("@#{instance_name}", instance)
|
168
|
+
end
|
169
|
+
|
170
|
+
def resource_instance
|
171
|
+
if load_instance?
|
172
|
+
if @controller.instance_variable_defined? "@#{instance_name}"
|
173
|
+
@controller.instance_variable_get("@#{instance_name}")
|
174
|
+
elsif @controller.respond_to?(instance_name, true)
|
175
|
+
@controller.send(instance_name)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def collection_instance=(instance)
|
181
|
+
@controller.instance_variable_set("@#{instance_name.to_s.pluralize}", instance)
|
182
|
+
end
|
183
|
+
|
184
|
+
def collection_instance
|
185
|
+
@controller.instance_variable_get("@#{instance_name.to_s.pluralize}")
|
186
|
+
end
|
187
|
+
|
188
|
+
# The object that methods (such as "find", "new" or "build") are called on.
|
189
|
+
# If the :through option is passed it will go through an association on that instance.
|
190
|
+
# If the :shallow option is passed it will use the resource_class if there's no parent
|
191
|
+
# If the :singleton option is passed it won't use the association because it needs to be handled later.
|
192
|
+
def resource_base
|
193
|
+
if @options[:through]
|
194
|
+
if parent_resource
|
195
|
+
@options[:singleton] ? resource_class : parent_resource.send(@options[:through_association] || name.to_s.pluralize)
|
196
|
+
elsif @options[:shallow]
|
197
|
+
resource_class
|
198
|
+
else
|
199
|
+
raise Unauthorized.new(nil, authorization_action, @params[:controller].to_sym) # maybe this should be a record not found error instead?
|
200
|
+
end
|
201
|
+
else
|
202
|
+
resource_class
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def parent_name
|
207
|
+
@options[:through] && [@options[:through]].flatten.detect { |i| fetch_parent(i) }
|
208
|
+
end
|
209
|
+
|
210
|
+
# The object to load this resource through.
|
211
|
+
def parent_resource
|
212
|
+
parent_name && fetch_parent(parent_name)
|
213
|
+
end
|
214
|
+
|
215
|
+
def fetch_parent(name)
|
216
|
+
if @controller.instance_variable_defined? "@#{name}"
|
217
|
+
@controller.instance_variable_get("@#{name}")
|
218
|
+
elsif @controller.respond_to?(name, true)
|
219
|
+
@controller.send(name)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def current_ability
|
224
|
+
@controller.send(:current_ability)
|
225
|
+
end
|
226
|
+
|
227
|
+
def name
|
228
|
+
@name || name_from_controller
|
229
|
+
end
|
230
|
+
|
231
|
+
def resource_params
|
232
|
+
if @options[:class]
|
233
|
+
@params[@options[:class].to_s.underscore.gsub('/', '_')]
|
234
|
+
else
|
235
|
+
@params[namespaced_name.to_s.underscore.gsub("/", "_")]
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def namespace
|
240
|
+
@params[:controller].split(/::|\//)[0..-2]
|
241
|
+
end
|
242
|
+
|
243
|
+
def namespaced_name
|
244
|
+
[namespace, name.camelize].join('::').singularize.camelize.constantize
|
245
|
+
rescue NameError
|
246
|
+
name
|
247
|
+
end
|
248
|
+
|
249
|
+
def name_from_controller
|
250
|
+
@params[:controller].sub("Controller", "").underscore.split('/').last.singularize
|
251
|
+
end
|
252
|
+
|
253
|
+
def instance_name
|
254
|
+
@options[:instance_name] || name
|
255
|
+
end
|
256
|
+
|
257
|
+
def collection_actions
|
258
|
+
[:index] + [@options[:collection]].flatten
|
259
|
+
end
|
260
|
+
|
261
|
+
def new_actions
|
262
|
+
[:new, :create] + [@options[:new]].flatten
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module CanCan
|
2
|
+
# A general CanCan exception
|
3
|
+
class Error < StandardError; end
|
4
|
+
|
5
|
+
# Raised when behavior is not implemented, usually used in an abstract class.
|
6
|
+
class NotImplemented < Error; end
|
7
|
+
|
8
|
+
# Raised when removed code is called, an alternative solution is provided in message.
|
9
|
+
class ImplementationRemoved < Error; end
|
10
|
+
|
11
|
+
# Raised when using check_authorization without calling authorized!
|
12
|
+
class AuthorizationNotPerformed < Error; end
|
13
|
+
|
14
|
+
# Raised when enable_authorization is used and not fully authorized by the end of the action
|
15
|
+
class InsufficientAuthorizationCheck < Error; end
|
16
|
+
|
17
|
+
# This error is raised when a user isn't allowed to access a given controller action.
|
18
|
+
# This usually happens within a call to ControllerAdditions#authorize! but can be
|
19
|
+
# raised manually.
|
20
|
+
#
|
21
|
+
# raise CanCan::Unauthorized.new("Not authorized!", :read, Article)
|
22
|
+
#
|
23
|
+
# The passed message, action, and subject are optional and can later be retrieved when
|
24
|
+
# rescuing from the exception.
|
25
|
+
#
|
26
|
+
# exception.message # => "Not authorized!"
|
27
|
+
# exception.action # => :read
|
28
|
+
# exception.subject # => Article
|
29
|
+
#
|
30
|
+
# If the message is not specified (or is nil) it will default to "You are not authorized
|
31
|
+
# to access this page." This default can be overridden by setting default_message.
|
32
|
+
#
|
33
|
+
# exception.default_message = "Default error message"
|
34
|
+
# exception.message # => "Default error message"
|
35
|
+
#
|
36
|
+
# See ControllerAdditions#authorize! for more information on rescuing from this exception
|
37
|
+
# and customizing the message using I18n.
|
38
|
+
class Unauthorized < Error
|
39
|
+
attr_reader :action, :subject
|
40
|
+
attr_writer :default_message
|
41
|
+
|
42
|
+
def initialize(message = nil, action = nil, subject = nil)
|
43
|
+
@message = message
|
44
|
+
@action = action
|
45
|
+
@subject = subject
|
46
|
+
@default_message = I18n.t(:"unauthorized.default", :default => "You are not authorized to access this page.")
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_s
|
50
|
+
@message || @default_message
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module CanCan
|
2
|
+
# For use with Inherited Resources
|
3
|
+
class InheritedResource < ControllerResource # :nodoc:
|
4
|
+
def load_resource_instance
|
5
|
+
if parent?
|
6
|
+
@controller.send :association_chain
|
7
|
+
@controller.instance_variable_get("@#{instance_name}")
|
8
|
+
elsif new_actions.include? @params[:action].to_sym
|
9
|
+
resource = @controller.send :build_resource
|
10
|
+
assign_attributes(resource)
|
11
|
+
else
|
12
|
+
@controller.send :resource
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def resource_base
|
17
|
+
@controller.send :end_of_association_chain
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
rspec_module = defined?(RSpec::Core) ? 'RSpec' : 'Spec' # for RSpec 1 compatability
|
2
|
+
Kernel.const_get(rspec_module)::Matchers.define :be_able_to do |*args|
|
3
|
+
match do |ability|
|
4
|
+
ability.can?(*args)
|
5
|
+
end
|
6
|
+
|
7
|
+
failure_message_for_should do |ability|
|
8
|
+
"expected to be able to #{args.map(&:inspect).join(" ")}"
|
9
|
+
end
|
10
|
+
|
11
|
+
failure_message_for_should_not do |ability|
|
12
|
+
"expected not to be able to #{args.map(&:inspect).join(" ")}"
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module CanCan
|
2
|
+
module ModelAdapters
|
3
|
+
class AbstractAdapter
|
4
|
+
def self.inherited(subclass)
|
5
|
+
@subclasses ||= []
|
6
|
+
@subclasses << subclass
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.adapter_class(model_class)
|
10
|
+
@subclasses.detect { |subclass| subclass.for_class?(model_class) } || DefaultAdapter
|
11
|
+
end
|
12
|
+
|
13
|
+
# Used to determine if the given adapter should be used for the passed in class.
|
14
|
+
def self.for_class?(member_class)
|
15
|
+
false # override in subclass
|
16
|
+
end
|
17
|
+
|
18
|
+
# Override if you need custom find behavior
|
19
|
+
def self.find(model_class, id)
|
20
|
+
model_class.find(id)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Used to determine if this model adapter will override the matching behavior for a hash of conditions.
|
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)
|
26
|
+
false
|
27
|
+
end
|
28
|
+
|
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."
|
32
|
+
end
|
33
|
+
|
34
|
+
# Used to determine if this model adapter will override the matching behavior for a specific condition.
|
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)
|
37
|
+
false
|
38
|
+
end
|
39
|
+
|
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."
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize(model_class, rules)
|
46
|
+
@model_class = model_class
|
47
|
+
@rules = rules
|
48
|
+
end
|
49
|
+
|
50
|
+
def database_records
|
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."
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
module CanCan
|
2
|
+
module ModelAdapters
|
3
|
+
class ActiveRecordAdapter < AbstractAdapter
|
4
|
+
def self.for_class?(model_class)
|
5
|
+
model_class <= ActiveRecord::Base
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.override_condition_matching?(subject, name, value)
|
9
|
+
name.kind_of?(MetaWhere::Column) if defined? MetaWhere
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.matches_condition?(subject, name, value)
|
13
|
+
subject_value = subject.send(name.column)
|
14
|
+
if name.method.to_s.ends_with? "_any"
|
15
|
+
value.any? { |v| meta_where_match? subject_value, name.method.to_s.sub("_any", ""), v }
|
16
|
+
elsif name.method.to_s.ends_with? "_all"
|
17
|
+
value.all? { |v| meta_where_match? subject_value, name.method.to_s.sub("_all", ""), v }
|
18
|
+
else
|
19
|
+
meta_where_match? subject_value, name.method, value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.meta_where_match?(subject_value, method, value)
|
24
|
+
case method.to_sym
|
25
|
+
when :eq then subject_value == value
|
26
|
+
when :not_eq then subject_value != value
|
27
|
+
when :in then value.include?(subject_value)
|
28
|
+
when :not_in then !value.include?(subject_value)
|
29
|
+
when :lt then subject_value < value
|
30
|
+
when :lteq then subject_value <= value
|
31
|
+
when :gt then subject_value > value
|
32
|
+
when :gteq then subject_value >= value
|
33
|
+
when :matches then subject_value =~ Regexp.new("^" + Regexp.escape(value).gsub("%", ".*") + "$", true)
|
34
|
+
when :does_not_match then !meta_where_match?(subject_value, :matches, value)
|
35
|
+
else raise NotImplemented, "The #{method} MetaWhere condition is not supported."
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns conditions intended to be used inside a database query. Normally you will not call this
|
40
|
+
# method directly, but instead go through ModelAdditions#accessible_by.
|
41
|
+
#
|
42
|
+
# If there is only one "can" definition, a hash of conditions will be returned matching the one defined.
|
43
|
+
#
|
44
|
+
# can :manage, User, :id => 1
|
45
|
+
# query(:manage, User).conditions # => { :id => 1 }
|
46
|
+
#
|
47
|
+
# If there are multiple "can" definitions, a SQL string will be returned to handle complex cases.
|
48
|
+
#
|
49
|
+
# can :manage, User, :id => 1
|
50
|
+
# can :manage, User, :manager_id => 1
|
51
|
+
# cannot :manage, User, :self_managed => true
|
52
|
+
# query(:manage, User).conditions # => "not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))"
|
53
|
+
#
|
54
|
+
def conditions
|
55
|
+
if @rules.size == 1 && @rules.first.base_behavior
|
56
|
+
# Return the conditions directly if there's just one definition
|
57
|
+
tableized_conditions(@rules.first.conditions).dup
|
58
|
+
else
|
59
|
+
@rules.reverse.inject(false_sql) do |sql, rule|
|
60
|
+
merge_conditions(sql, tableized_conditions(rule.conditions).dup, rule.base_behavior)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def tableized_conditions(conditions, model_class = @model_class)
|
66
|
+
return conditions unless conditions.kind_of? Hash
|
67
|
+
conditions.inject({}) do |result_hash, (name, value)|
|
68
|
+
if value.kind_of? Hash
|
69
|
+
association_class = model_class.reflect_on_association(name).class_name.constantize
|
70
|
+
name = model_class.reflect_on_association(name).table_name.to_sym
|
71
|
+
value = tableized_conditions(value, association_class)
|
72
|
+
end
|
73
|
+
result_hash[name] = value
|
74
|
+
result_hash
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns the associations used in conditions for the :joins option of a search.
|
79
|
+
# See ModelAdditions#accessible_by
|
80
|
+
def joins
|
81
|
+
joins_hash = {}
|
82
|
+
@rules.each do |rule|
|
83
|
+
merge_joins(joins_hash, rule.associations_hash)
|
84
|
+
end
|
85
|
+
clean_joins(joins_hash) unless joins_hash.empty?
|
86
|
+
end
|
87
|
+
|
88
|
+
def database_records
|
89
|
+
if override_scope
|
90
|
+
@model_class.scoped.merge(override_scope)
|
91
|
+
elsif @model_class.respond_to?(:where) && @model_class.respond_to?(:joins)
|
92
|
+
mergeable_conditions = @rules.select {|rule| rule.unmergeable? }.blank?
|
93
|
+
if mergeable_conditions
|
94
|
+
@model_class.where(conditions).joins(joins)
|
95
|
+
else
|
96
|
+
@model_class.where(*(@rules.map(&:conditions))).joins(joins)
|
97
|
+
end
|
98
|
+
else
|
99
|
+
@model_class.scoped(:conditions => conditions, :joins => joins)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def override_scope
|
106
|
+
conditions = @rules.map(&:conditions).compact
|
107
|
+
if defined?(ActiveRecord::Relation) && conditions.any? { |c| c.kind_of?(ActiveRecord::Relation) }
|
108
|
+
if conditions.size == 1
|
109
|
+
conditions.first
|
110
|
+
else
|
111
|
+
rule = @rules.detect { |rule| rule.conditions.kind_of?(ActiveRecord::Relation) }
|
112
|
+
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."
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def merge_conditions(sql, conditions_hash, behavior)
|
118
|
+
if conditions_hash.blank?
|
119
|
+
behavior ? true_sql : false_sql
|
120
|
+
else
|
121
|
+
conditions = sanitize_sql(conditions_hash)
|
122
|
+
case sql
|
123
|
+
when true_sql
|
124
|
+
behavior ? true_sql : "not (#{conditions})"
|
125
|
+
when false_sql
|
126
|
+
behavior ? conditions : false_sql
|
127
|
+
else
|
128
|
+
behavior ? "(#{conditions}) OR (#{sql})" : "not (#{conditions}) AND (#{sql})"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def false_sql
|
134
|
+
sanitize_sql(['?=?', true, false])
|
135
|
+
end
|
136
|
+
|
137
|
+
def true_sql
|
138
|
+
sanitize_sql(['?=?', true, true])
|
139
|
+
end
|
140
|
+
|
141
|
+
def sanitize_sql(conditions)
|
142
|
+
@model_class.send(:sanitize_sql, conditions)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Takes two hashes and does a deep merge.
|
146
|
+
def merge_joins(base, add)
|
147
|
+
add.each do |name, nested|
|
148
|
+
if base[name].is_a?(Hash) && !nested.empty?
|
149
|
+
merge_joins(base[name], nested)
|
150
|
+
else
|
151
|
+
base[name] = nested
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Removes empty hashes and moves everything into arrays.
|
157
|
+
def clean_joins(joins_hash)
|
158
|
+
joins = []
|
159
|
+
joins_hash.each do |name, nested|
|
160
|
+
joins << (nested.empty? ? name : {name => clean_joins(nested)})
|
161
|
+
end
|
162
|
+
joins
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
ActiveSupport.on_load(:active_record) do
|
169
|
+
ActiveRecord::Base.class_eval do
|
170
|
+
include CanCan::ModelAdditions
|
171
|
+
end
|
172
|
+
end
|