cancan 1.2.0 → 1.3.2

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.
@@ -1,54 +1,129 @@
1
1
  module CanCan
2
- # Used internally to load and authorize a given controller resource.
3
- # This manages finding or building an instance of the resource. If a
4
- # parent is given it will go through the association.
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.
5
4
  class ControllerResource # :nodoc:
6
- def initialize(controller, name, parent = nil, options = {})
7
- raise ImplementationRemoved, "The :class option has been renamed to :resource for specifying the class in CanCan." if options.has_key? :class
5
+ def self.add_before_filter(controller_class, method, *args)
6
+ options = args.extract_options!
7
+ resource_name = args.first
8
+ controller_class.before_filter(options.slice(:only, :except)) do |controller|
9
+ ControllerResource.new(controller, resource_name, options.except(:only, :except)).send(method)
10
+ end
11
+ end
12
+
13
+ def initialize(controller, *args)
8
14
  @controller = controller
9
- @name = name
10
- @parent = parent
11
- @options = options
12
- end
13
-
14
- # Returns the class used for this resource. This can be overriden by the :resource option.
15
- # Sometimes one will use a symbol as the resource if a class does not exist for it. In that
16
- # case "find" and "build" should not be called on it.
17
- def model_class
18
- resource_class = @options[:resource]
19
- if resource_class.nil?
20
- @name.to_s.camelize.constantize
21
- elsif resource_class.kind_of? String
22
- resource_class.constantize
15
+ @params = controller.params
16
+ @options = args.extract_options!
17
+ @name = args.first
18
+ raise CanCan::ImplementationRemoved, "The :nested option is no longer supported, instead use :through with separate load/authorize call." if @options[:nested]
19
+ raise CanCan::ImplementationRemoved, "The :name option is no longer supported, instead pass the name as the first argument." if @options[:name]
20
+ raise CanCan::ImplementationRemoved, "The :resource option has been renamed back to :class, use false if no class." if @options[:resource]
21
+ end
22
+
23
+ def load_and_authorize_resource
24
+ load_resource
25
+ authorize_resource
26
+ end
27
+
28
+ def load_resource
29
+ if !resource_instance && (parent? || member_action?)
30
+ @controller.instance_variable_set("@#{instance_name}", load_resource_instance)
31
+ end
32
+ end
33
+
34
+ def authorize_resource
35
+ @controller.authorize!(authorization_action, resource_instance || resource_class)
36
+ end
37
+
38
+ def parent?
39
+ @options.has_key?(:parent) ? @options[:parent] : @name && @name != name_from_controller.to_sym
40
+ end
41
+
42
+ private
43
+
44
+ def load_resource_instance
45
+ if !parent? && new_actions.include?(@params[:action].to_sym)
46
+ build_resource
47
+ elsif id_param || @options[:singleton]
48
+ find_resource
49
+ end
50
+ end
51
+
52
+ def build_resource
53
+ method_name = @options[:singleton] ? "build_#{name}" : "new"
54
+ resource_base.send(*[method_name, @params[name]].compact)
55
+ end
56
+
57
+ def find_resource
58
+ if @options[:singleton]
59
+ resource_base.send(name)
23
60
  else
24
- resource_class # could be a symbol
61
+ @options[:find_by] ? resource_base.send("find_by_#{@options[:find_by]}!", id_param) : resource_base.find(id_param)
25
62
  end
26
63
  end
27
-
28
- def find(id)
29
- self.model_instance ||= base.find(id)
64
+
65
+ def authorization_action
66
+ parent? ? :read : @params[:action].to_sym
30
67
  end
31
-
32
- # Build a new instance of this resource. If it is a class we just call "new" otherwise
33
- # it's an associaiton and "build" is used.
34
- def build(attributes)
35
- self.model_instance ||= (base.kind_of?(Class) ? base.new(attributes) : base.build(attributes))
68
+
69
+ def id_param
70
+ @params[parent? ? :"#{name}_id" : :id]
36
71
  end
37
-
38
- def model_instance
39
- @controller.instance_variable_get("@#{@name}")
72
+
73
+ def member_action?
74
+ !collection_actions.include? @params[:action].to_sym
40
75
  end
41
-
42
- def model_instance=(instance)
43
- @controller.instance_variable_set("@#{@name}", instance)
76
+
77
+ # Returns the class used for this resource. This can be overriden by the :class option.
78
+ # If +false+ is passed in it will use the resource name as a symbol in which case it should
79
+ # only be used for authorization, not loading since there's no class to load through.
80
+ def resource_class
81
+ case @options[:class]
82
+ when false then name.to_sym
83
+ when nil then name.to_s.camelize.constantize
84
+ when String then @options[:class].constantize
85
+ else @options[:class]
86
+ end
44
87
  end
45
-
46
- private
47
-
88
+
89
+ def resource_instance
90
+ @controller.instance_variable_get("@#{instance_name}")
91
+ end
92
+
48
93
  # The object that methods (such as "find", "new" or "build") are called on.
49
- # If there is a parent it will be the association, otherwise it will be the model's class.
50
- def base
51
- @parent ? @parent.model_instance.send(@name.to_s.pluralize) : model_class
94
+ # If the :through option is passed it will go through an association on that instance.
95
+ # If the :singleton option is passed it won't use the association because it needs to be handled later.
96
+ def resource_base
97
+ if through_resource
98
+ @options[:singleton] ? through_resource : through_resource.send(name.to_s.pluralize)
99
+ else
100
+ resource_class
101
+ end
102
+ end
103
+
104
+ # The object to load this resource through.
105
+ def through_resource
106
+ @options[:through] && [@options[:through]].flatten.map { |i| @controller.instance_variable_get("@#{i}") }.compact.first
107
+ end
108
+
109
+ def name
110
+ @name || name_from_controller
111
+ end
112
+
113
+ def name_from_controller
114
+ @params[:controller].sub("Controller", "").underscore.split('/').last.singularize
115
+ end
116
+
117
+ def instance_name
118
+ @options[:instance_name] || name
119
+ end
120
+
121
+ def collection_actions
122
+ [:index] + [@options[:collection]].flatten
123
+ end
124
+
125
+ def new_actions
126
+ [:new, :create] + [@options[:new]].flatten
52
127
  end
53
128
  end
54
129
  end
@@ -0,0 +1,97 @@
1
+ module CanCan
2
+
3
+ # Generates the sql conditions and association joins for use in ActiveRecord queries.
4
+ # Normally you will not use this class directly, but instead through ActiveRecordAdditions#accessible_by.
5
+ class Query
6
+ def initialize(sanitizer, can_definitions)
7
+ @sanitizer = sanitizer
8
+ @can_definitions = can_definitions
9
+ end
10
+
11
+ # Returns conditions intended to be used inside a database query. Normally you will not call this
12
+ # method directly, but instead go through ActiveRecordAdditions#accessible_by.
13
+ #
14
+ # If there is only one "can" definition, a hash of conditions will be returned matching the one defined.
15
+ #
16
+ # can :manage, User, :id => 1
17
+ # query(:manage, User).conditions # => { :id => 1 }
18
+ #
19
+ # If there are multiple "can" definitions, a SQL string will be returned to handle complex cases.
20
+ #
21
+ # can :manage, User, :id => 1
22
+ # can :manage, User, :manager_id => 1
23
+ # cannot :manage, User, :self_managed => true
24
+ # query(:manage, User).conditions # => "not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))"
25
+ #
26
+ def conditions
27
+ if @can_definitions.size == 1 && @can_definitions.first.base_behavior
28
+ # Return the conditions directly if there's just one definition
29
+ @can_definitions.first.tableized_conditions
30
+ else
31
+ @can_definitions.reverse.inject(false_sql) do |sql, can_definition|
32
+ merge_conditions(sql, can_definition.tableized_conditions, can_definition.base_behavior)
33
+ end
34
+ end
35
+ end
36
+
37
+ # Returns the associations used in conditions for the :joins option of a search.
38
+ # See ActiveRecordAdditions#accessible_by for use in Active Record.
39
+ def joins
40
+ joins_hash = {}
41
+ @can_definitions.each do |can_definition|
42
+ merge_joins(joins_hash, can_definition.associations_hash)
43
+ end
44
+ clean_joins(joins_hash) unless joins_hash.empty?
45
+ end
46
+
47
+ private
48
+
49
+ def merge_conditions(sql, conditions_hash, behavior)
50
+ if conditions_hash.blank?
51
+ behavior ? true_sql : false_sql
52
+ else
53
+ conditions = sanitize_sql(conditions_hash)
54
+ case sql
55
+ when true_sql
56
+ behavior ? true_sql : "not (#{conditions})"
57
+ when false_sql
58
+ behavior ? conditions : false_sql
59
+ else
60
+ behavior ? "(#{conditions}) OR (#{sql})" : "not (#{conditions}) AND (#{sql})"
61
+ end
62
+ end
63
+ end
64
+
65
+ def false_sql
66
+ sanitize_sql(['?=?', true, false])
67
+ end
68
+
69
+ def true_sql
70
+ sanitize_sql(['?=?', true, true])
71
+ end
72
+
73
+ def sanitize_sql(conditions)
74
+ @sanitizer.send(:sanitize_sql, conditions)
75
+ end
76
+
77
+ # Takes two hashes and does a deep merge.
78
+ def merge_joins(base, add)
79
+ add.each do |name, nested|
80
+ if base[name].is_a?(Hash) && !nested.empty?
81
+ merge_joins(base[name], nested)
82
+ else
83
+ base[name] = nested
84
+ end
85
+ end
86
+ end
87
+
88
+ # Removes empty hashes and moves everything into arrays.
89
+ def clean_joins(joins_hash)
90
+ joins = []
91
+ joins_hash.each do |name, nested|
92
+ joins << (nested.empty? ? name : {name => clean_joins(nested)})
93
+ end
94
+ joins
95
+ end
96
+ end
97
+ end
@@ -16,50 +16,81 @@ describe CanCan::Ability do
16
16
  @ability.can?(:foodfight, String).should be_false
17
17
  end
18
18
 
19
- it "should return what block returns on a can call" do
19
+ it "should pass true to `can?` when non false/nil is returned in block" do
20
20
  @ability.can :read, :all
21
21
  @ability.can :read, Symbol do |sym|
22
- sym
22
+ "foo" # TODO test that sym is nil when no instance is passed
23
23
  end
24
- @ability.can?(:read, Symbol).should be_nil
25
- @ability.can?(:read, :some_symbol).should == :some_symbol
24
+ @ability.can?(:read, :some_symbol).should == true
25
+ end
26
+
27
+ it "should pass to previous can definition, if block returns false or nil" do
28
+ @ability.can :read, Symbol
29
+ @ability.can :read, Integer do |i|
30
+ i < 5
31
+ end
32
+ @ability.can :read, Integer do |i|
33
+ i > 10
34
+ end
35
+ @ability.can?(:read, Symbol).should be_true
36
+ @ability.can?(:read, 11).should be_true
37
+ @ability.can?(:read, 1).should be_true
38
+ @ability.can?(:read, 6).should be_false
26
39
  end
27
40
 
28
41
  it "should pass class with object if :all objects are accepted" do
29
42
  @ability.can :preview, :all do |object_class, object|
30
- [object_class, object]
43
+ object_class.should == Fixnum
44
+ object.should == 123
45
+ @block_called = true
31
46
  end
32
- @ability.can?(:preview, 123).should == [Fixnum, 123]
47
+ @ability.can?(:preview, 123)
48
+ @block_called.should be_true
33
49
  end
34
50
 
35
51
  it "should pass class with no object if :all objects are accepted and class is passed directly" do
36
52
  @ability.can :preview, :all do |object_class, object|
37
- [object_class, object]
53
+ object_class.should == Hash
54
+ object.should be_nil
55
+ @block_called = true
38
56
  end
39
- @ability.can?(:preview, Hash).should == [Hash, nil]
57
+ @ability.can?(:preview, Hash)
58
+ @block_called.should be_true
40
59
  end
41
60
 
42
61
  it "should pass action and object for global manage actions" do
43
62
  @ability.can :manage, Array do |action, object|
44
- [action, object]
63
+ action.should == :stuff
64
+ object.should == [1, 2]
65
+ @block_called = true
45
66
  end
46
- @ability.can?(:stuff, [1, 2]).should == [:stuff, [1, 2]]
47
- @ability.can?(:stuff, Array).should == [:stuff, nil]
67
+ @ability.can?(:stuff, [1, 2]).should
68
+ @block_called.should be_true
48
69
  end
49
70
 
50
71
  it "should alias update or destroy actions to modify action" do
51
72
  @ability.alias_action :update, :destroy, :to => :modify
52
- @ability.can(:modify, :all) { :modify_called }
53
- @ability.can?(:update, 123).should == :modify_called
54
- @ability.can?(:destroy, 123).should == :modify_called
73
+ @ability.can :modify, :all
74
+ @ability.can?(:update, 123).should be_true
75
+ @ability.can?(:destroy, 123).should be_true
76
+ end
77
+
78
+ it "should allow deeply nested aliased actions" do
79
+ @ability.alias_action :increment, :to => :sort
80
+ @ability.alias_action :sort, :to => :modify
81
+ @ability.can :modify, :all
82
+ @ability.can?(:increment, 123).should be_true
55
83
  end
56
84
 
57
85
  it "should return block result for action, object_class, and object for any action" do
58
86
  @ability.can :manage, :all do |action, object_class, object|
59
- [action, object_class, object]
87
+ action.should == :foo
88
+ object_class.should == Fixnum
89
+ object.should == 123
90
+ @block_called = true
60
91
  end
61
- @ability.can?(:foo, 123).should == [:foo, Fixnum, 123]
62
- @ability.can?(:bar, Fixnum).should == [:bar, Fixnum, nil]
92
+ @ability.can?(:foo, 123)
93
+ @block_called.should be_true
63
94
  end
64
95
 
65
96
  it "should automatically alias index and show into read calls" do
@@ -69,10 +100,10 @@ describe CanCan::Ability do
69
100
  end
70
101
 
71
102
  it "should automatically alias new and edit into create and update respectively" do
72
- @ability.can(:create, :all) { :create_called }
73
- @ability.can(:update, :all) { :update_called }
74
- @ability.can?(:new, 123).should == :create_called
75
- @ability.can?(:edit, 123).should == :update_called
103
+ @ability.can :create, :all
104
+ @ability.can :update, :all
105
+ @ability.can?(:new, 123).should be_true
106
+ @ability.can?(:edit, 123).should be_true
76
107
  end
77
108
 
78
109
  it "should not respond to prepare (now using initialize)" do
@@ -104,6 +135,13 @@ describe CanCan::Ability do
104
135
  @ability.can?(:read, :nonstats).should be_false
105
136
  end
106
137
 
138
+ it "should check ancestors of class" do
139
+ @ability.can :read, Numeric
140
+ @ability.can?(:read, Integer).should be_true
141
+ @ability.can?(:read, 1.23).should be_true
142
+ @ability.can?(:read, "foo").should be_false
143
+ end
144
+
107
145
  it "should support 'cannot' method to define what user cannot do" do
108
146
  @ability.can :read, :all
109
147
  @ability.cannot :read, Integer
@@ -121,6 +159,40 @@ describe CanCan::Ability do
121
159
  @ability.can?(:read, 123).should be_false
122
160
  end
123
161
 
162
+ it "should pass to previous can definition, if block returns false or nil" do
163
+ #same as previous
164
+ @ability.can :read, :all
165
+ @ability.cannot :read, Integer do |int|
166
+ int > 10 ? nil : ( int > 5 )
167
+ end
168
+ @ability.can?(:read, "foo").should be_true
169
+ @ability.can?(:read, 3).should be_true
170
+ @ability.can?(:read, 8).should be_false
171
+ @ability.can?(:read, 123).should be_true
172
+
173
+ end
174
+
175
+ it "should always return `false` for single cannot definition" do
176
+ @ability.cannot :read, Integer do |int|
177
+ int > 10 ? nil : ( int > 5 )
178
+ end
179
+ @ability.can?(:read, "foo").should be_false
180
+ @ability.can?(:read, 3).should be_false
181
+ @ability.can?(:read, 8).should be_false
182
+ @ability.can?(:read, 123).should be_false
183
+ end
184
+
185
+ it "should pass to previous cannot definition, if block returns false or nil" do
186
+ @ability.cannot :read, :all
187
+ @ability.can :read, Integer do |int|
188
+ int > 10 ? nil : ( int > 5 )
189
+ end
190
+ @ability.can?(:read, "foo").should be_false
191
+ @ability.can?(:read, 3).should be_false
192
+ @ability.can?(:read, 10).should be_true
193
+ @ability.can?(:read, 123).should be_false
194
+ end
195
+
124
196
  it "should append aliased actions" do
125
197
  @ability.alias_action :update, :to => :modify
126
198
  @ability.alias_action :destroy, :to => :modify
@@ -174,30 +246,9 @@ describe CanCan::Ability do
174
246
  @ability.can?(:read, [[4, 5, 6]]).should be_false
175
247
  end
176
248
 
177
- it "should return conditions for a given ability" do
178
- @ability.can :read, Array, :first => 1, :last => 3
179
- @ability.conditions(:show, Array).should == {:first => 1, :last => 3}
180
- end
181
-
182
- it "should raise an exception when a block is used on condition" do
183
- @ability.can :read, Array do |a|
184
- true
185
- end
186
- lambda { @ability.conditions(:show, Array) }.should raise_error(CanCan::Error, "Cannot determine ability conditions from block for :show Array")
187
- end
188
-
189
- it "should return an empty hash for conditions when there are no conditions" do
190
- @ability.can :read, Array
191
- @ability.conditions(:show, Array).should == {}
192
- end
193
-
194
- it "should return false when performed on an action which isn't defined" do
195
- @ability.conditions(:foo, Array).should == false
196
- end
197
-
198
249
  it "should has eated cheezburger" do
199
250
  lambda {
200
251
  @ability.can? :has, :cheezburger
201
- }.should raise_exception(CanCan::Error, "Nom nom nom. I eated it.")
252
+ }.should raise_error(CanCan::Error, "Nom nom nom. I eated it.")
202
253
  end
203
254
  end