karsthammer-inherited_resources 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. data/CHANGELOG +119 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +516 -0
  4. data/Rakefile +43 -0
  5. data/lib/generators/rails/USAGE +10 -0
  6. data/lib/generators/rails/inherited_resources_controller_generator.rb +11 -0
  7. data/lib/generators/rails/templates/controller.rb +5 -0
  8. data/lib/inherited_resources.rb +37 -0
  9. data/lib/inherited_resources/actions.rb +67 -0
  10. data/lib/inherited_resources/base.rb +44 -0
  11. data/lib/inherited_resources/base_helpers.rb +270 -0
  12. data/lib/inherited_resources/belongs_to_helpers.rb +97 -0
  13. data/lib/inherited_resources/blank_slate.rb +12 -0
  14. data/lib/inherited_resources/class_methods.rb +267 -0
  15. data/lib/inherited_resources/dsl.rb +26 -0
  16. data/lib/inherited_resources/polymorphic_helpers.rb +155 -0
  17. data/lib/inherited_resources/responder.rb +6 -0
  18. data/lib/inherited_resources/singleton_helpers.rb +95 -0
  19. data/lib/inherited_resources/url_helpers.rb +188 -0
  20. data/lib/inherited_resources/version.rb +3 -0
  21. data/test/aliases_test.rb +144 -0
  22. data/test/association_chain_test.rb +125 -0
  23. data/test/base_test.rb +278 -0
  24. data/test/belongs_to_test.rb +105 -0
  25. data/test/class_methods_test.rb +132 -0
  26. data/test/customized_base_test.rb +168 -0
  27. data/test/customized_belongs_to_test.rb +76 -0
  28. data/test/defaults_test.rb +70 -0
  29. data/test/nested_belongs_to_test.rb +108 -0
  30. data/test/optional_belongs_to_test.rb +164 -0
  31. data/test/polymorphic_test.rb +186 -0
  32. data/test/redirect_to_test.rb +51 -0
  33. data/test/singleton_test.rb +83 -0
  34. data/test/test_helper.rb +40 -0
  35. data/test/url_helpers_test.rb +665 -0
  36. metadata +142 -0
@@ -0,0 +1,97 @@
1
+ module InheritedResources
2
+
3
+ # = belongs_to
4
+ #
5
+ # Let's suppose that we have some tasks that belongs to projects. To specify
6
+ # this assoication in your controllers, just do:
7
+ #
8
+ # class TasksController < InheritedResources::Base
9
+ # belongs_to :project
10
+ # end
11
+ #
12
+ # belongs_to accepts several options to be able to configure the association.
13
+ # For example, if you want urls like /projects/:project_title/tasks, you
14
+ # can customize how InheritedResources find your projects:
15
+ #
16
+ # class TasksController < InheritedResources::Base
17
+ # belongs_to :project, :finder => :find_by_title!, :param => :project_title
18
+ # end
19
+ #
20
+ # It also accepts :route_name, :parent_class and :instance_name as options.
21
+ # Check the lib/inherited_resources/class_methods.rb for more.
22
+ #
23
+ # = nested_belongs_to
24
+ #
25
+ # Now, our Tasks get some Comments and you need to nest even deeper. Good
26
+ # practices says that you should never nest more than two resources, but sometimes
27
+ # you have to for security reasons. So this is an example of how you can do it:
28
+ #
29
+ # class CommentsController < InheritedResources::Base
30
+ # nested_belongs_to :project, :task
31
+ # end
32
+ #
33
+ # If you need to configure any of these belongs to, you can nested them using blocks:
34
+ #
35
+ # class CommentsController < InheritedResources::Base
36
+ # belongs_to :project, :finder => :find_by_title!, :param => :project_title do
37
+ # belongs_to :task
38
+ # end
39
+ # end
40
+ #
41
+ # Warning: calling several belongs_to is the same as nesting them:
42
+ #
43
+ # class CommentsController < InheritedResources::Base
44
+ # belongs_to :project
45
+ # belongs_to :task
46
+ # end
47
+ #
48
+ # In other words, the code above is the same as calling nested_belongs_to.
49
+ #
50
+ module BelongsToHelpers
51
+
52
+ protected
53
+
54
+ # Parent is always true when belongs_to is called.
55
+ #
56
+ def parent?
57
+ true
58
+ end
59
+
60
+ def parent
61
+ @parent ||= association_chain[-1]
62
+ end
63
+
64
+ def parent_type
65
+ parent.class.name.underscore.to_sym
66
+ end
67
+
68
+ private
69
+
70
+ # Evaluate the parent given. This is used to nest parents in the
71
+ # association chain.
72
+ #
73
+ def evaluate_parent(parent_symbol, parent_config, chain = nil) #:nodoc:
74
+ instantiated_object = instance_variable_get("@#{parent_config[:instance_name]}")
75
+ return instantiated_object if instantiated_object
76
+
77
+ parent = if chain
78
+ chain.send(parent_config[:collection_name])
79
+ else
80
+ parent_config[:parent_class]
81
+ end
82
+
83
+ parent = parent.send(parent_config[:finder], params[parent_config[:param]])
84
+
85
+ instance_variable_set("@#{parent_config[:instance_name]}", parent)
86
+ end
87
+
88
+ # Maps parents_symbols to build association chain. In this case, it
89
+ # simply return the parent_symbols, however on polymorphic belongs to,
90
+ # it has some customization.
91
+ #
92
+ def symbols_for_association_chain #:nodoc:
93
+ parents_symbols
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,12 @@
1
+ module InheritedResources
2
+ # An object from BlankSlate simply discards all messages sent to it.
3
+ class BlankSlate
4
+ instance_methods.each do |m|
5
+ undef_method m unless m =~ /^(__|object_id)/
6
+ end
7
+
8
+ def method_missing(*args)
9
+ nil
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,267 @@
1
+ module InheritedResources
2
+ module ClassMethods
3
+
4
+ protected
5
+
6
+ # Used to overwrite the default assumptions InheritedResources do. Whenever
7
+ # this method is called, it should be on the top of your controller, since
8
+ # almost other methods depends on the values given to <<tt>>defaults</tt>.
9
+ #
10
+ # == Options
11
+ #
12
+ # * <tt>:resource_class</tt> - The resource class which by default is guessed
13
+ # by the controller name. Defaults to Project in
14
+ # ProjectsController.
15
+ #
16
+ # * <tt>:collection_name</tt> - The name of the collection instance variable which
17
+ # is set on the index action. Defaults to :projects in
18
+ # ProjectsController.
19
+ #
20
+ # * <tt>:instance_name</tt> - The name of the singular instance variable which
21
+ # is set on all actions besides index action. Defaults to
22
+ # :project in ProjectsController.
23
+ #
24
+ # * <tt>:route_collection_name</tt> - The name of the collection route. Defaults to :collection_name.
25
+ #
26
+ # * <tt>:route_instance_name</tt> - The name of the singular route. Defaults to :instance_name.
27
+ #
28
+ # * <tt>:route_prefix</tt> - The route prefix which is automically set in namespaced
29
+ # controllers. Default to :admin on Admin::ProjectsController.
30
+ #
31
+ # * <tt>:singleton</tt> - Tells if this controller is singleton or not.
32
+ #
33
+ def defaults(options)
34
+ raise ArgumentError, 'Class method :defaults expects a hash of options.' unless options.is_a? Hash
35
+
36
+ options.symbolize_keys!
37
+ options.assert_valid_keys(:resource_class, :collection_name, :instance_name,
38
+ :class_name, :route_prefix, :route_collection_name,
39
+ :route_instance_name, :singleton)
40
+
41
+ self.resource_class = options.delete(:resource_class) if options.key?(:resource_class)
42
+ self.resource_class = options.delete(:class_name).constantize if options.key?(:class_name)
43
+
44
+ acts_as_singleton! if options.delete(:singleton)
45
+
46
+ config = self.resources_configuration[:self]
47
+ config[:route_prefix] = options.delete(:route_prefix) if options.key?(:route_prefix)
48
+
49
+ options.each do |key, value|
50
+ config[key] = value.to_sym
51
+ end
52
+
53
+ create_resources_url_helpers!
54
+ end
55
+
56
+ # Defines wich actions to keep from the inherited controller.
57
+ # Syntax is borrowed from resource_controller.
58
+ #
59
+ # actions :index, :show, :edit
60
+ # actions :all, :except => :index
61
+ #
62
+ def actions(*actions_to_keep)
63
+ raise ArgumentError, 'Wrong number of arguments. You have to provide which actions you want to keep.' if actions_to_keep.empty?
64
+
65
+ options = actions_to_keep.extract_options!
66
+ actions_to_remove = Array(options[:except])
67
+ actions_to_remove += ACTIONS - actions_to_keep.map { |a| a.to_sym } unless actions_to_keep.first == :all
68
+ actions_to_remove.map! { |a| a.to_sym }.uniq!
69
+ (instance_methods.map { |m| m.to_sym } & actions_to_remove).each do |action|
70
+ undef_method action, "#{action}!"
71
+ end
72
+ end
73
+
74
+ # Defines that this controller belongs to another resource.
75
+ #
76
+ # belongs_to :projects
77
+ #
78
+ # == Options
79
+ #
80
+ # * <tt>:parent_class</tt> - Allows you to specify what is the parent class.
81
+ #
82
+ # belongs_to :project, :parent_class => AdminProject
83
+ #
84
+ # * <tt>:class_name</tt> - Also allows you to specify the parent class, but you should
85
+ # give a string. Added for ActiveRecord belongs to compatibility.
86
+ #
87
+ # * <tt>:instance_name</tt> - The instance variable name. By default is the name of the association.
88
+ #
89
+ # belongs_to :project, :instance_name => :my_project
90
+ #
91
+ # * <tt>:finder</tt> - Specifies which method should be called to instantiate the parent.
92
+ #
93
+ # belongs_to :project, :finder => :find_by_title!
94
+ #
95
+ # This will make your projects be instantiated as:
96
+ #
97
+ # Project.find_by_title!(params[:project_id])
98
+ #
99
+ # Instead of:
100
+ #
101
+ # Project.find(params[:project_id])
102
+ #
103
+ # * <tt>:param</tt> - Allows you to specify params key to retrieve the id.
104
+ # Default is :association_id, which in this case is :project_id.
105
+ #
106
+ # * <tt>:route_name</tt> - Allows you to specify what is the route name in your url
107
+ # helper. By default is association name.
108
+ #
109
+ # * <tt>:collection_name</tt> - Tell how to retrieve the next collection. Let's
110
+ # suppose you have Tasks which belongs to Projects
111
+ # which belongs to companies. This will do somewhere
112
+ # down the road:
113
+ #
114
+ # @company.projects
115
+ #
116
+ # But if you want to retrieve instead:
117
+ #
118
+ # @company.admin_projects
119
+ #
120
+ # You supply the collection name.
121
+ #
122
+ # * <tt>:polymorphic</tt> - Tell the association is polymorphic.
123
+ #
124
+ # * <tt>:singleton</tt> - Tell it's a singleton association.
125
+ #
126
+ # * <tt>:optional</tt> - Tell the association is optional (it's a special
127
+ # type of polymorphic association)
128
+ #
129
+ def belongs_to(*symbols, &block)
130
+ options = symbols.extract_options!
131
+
132
+ options.symbolize_keys!
133
+ options.assert_valid_keys(:class_name, :parent_class, :instance_name, :param,
134
+ :finder, :route_name, :collection_name, :singleton,
135
+ :polymorphic, :optional)
136
+
137
+ optional = options.delete(:optional)
138
+ singleton = options.delete(:singleton)
139
+ polymorphic = options.delete(:polymorphic)
140
+ finder = options.delete(:finder)
141
+
142
+ include BelongsToHelpers if self.parents_symbols.empty?
143
+
144
+ acts_as_singleton! if singleton
145
+ acts_as_polymorphic! if polymorphic || optional
146
+
147
+ raise ArgumentError, 'You have to give me at least one association name.' if symbols.empty?
148
+ raise ArgumentError, 'You cannot define multiple associations with options: #{options.keys.inspect} to belongs to.' unless symbols.size == 1 || options.empty?
149
+
150
+ symbols.each do |symbol|
151
+ symbol = symbol.to_sym
152
+
153
+ if polymorphic || optional
154
+ self.parents_symbols << :polymorphic unless self.parents_symbols.include?(:polymorphic)
155
+ self.resources_configuration[:polymorphic][:symbols] << symbol
156
+ self.resources_configuration[:polymorphic][:optional] ||= optional
157
+ else
158
+ self.parents_symbols << symbol
159
+ end
160
+
161
+ config = self.resources_configuration[symbol] = {}
162
+
163
+ config[:parent_class] = options.delete(:parent_class) || begin
164
+ class_name = (options.delete(:class_name) || symbol).to_s.pluralize.classify
165
+ class_name.constantize
166
+ rescue NameError => e
167
+ raise unless e.message.include?(class_name)
168
+ nil
169
+ end
170
+
171
+ config[:collection_name] = options.delete(:collection_name) || symbol.to_s.pluralize.to_sym
172
+ config[:instance_name] = options.delete(:instance_name) || symbol
173
+ config[:param] = options.delete(:param) || :"#{symbol}_id"
174
+ config[:route_name] = options.delete(:route_name) || symbol
175
+ config[:finder] = finder || :find
176
+ end
177
+
178
+ if block_given?
179
+ class_eval(&block)
180
+ else
181
+ create_resources_url_helpers!
182
+ end
183
+ helper_method :parent, :parent?
184
+ end
185
+ alias :nested_belongs_to :belongs_to
186
+
187
+ # A quick method to declare polymorphic belongs to.
188
+ #
189
+ def polymorphic_belongs_to(*symbols, &block)
190
+ options = symbols.extract_options!
191
+ options.merge!(:polymorphic => true)
192
+ belongs_to(*symbols << options, &block)
193
+ end
194
+
195
+ # A quick method to declare singleton belongs to.
196
+ #
197
+ def singleton_belongs_to(*symbols, &block)
198
+ options = symbols.extract_options!
199
+ options.merge!(:singleton => true)
200
+ belongs_to(*symbols << options, &block)
201
+ end
202
+
203
+ # A quick method to declare optional belongs to.
204
+ #
205
+ def optional_belongs_to(*symbols, &block)
206
+ options = symbols.extract_options!
207
+ options.merge!(:optional => true)
208
+ belongs_to(*symbols << options, &block)
209
+ end
210
+
211
+ private
212
+
213
+ def acts_as_singleton! #:nodoc:
214
+ unless self.resources_configuration[:self][:singleton]
215
+ self.resources_configuration[:self][:singleton] = true
216
+ include SingletonHelpers
217
+ actions :all, :except => :index
218
+ end
219
+ end
220
+
221
+ def acts_as_polymorphic! #:nodoc:
222
+ unless self.parents_symbols.include?(:polymorphic)
223
+ include PolymorphicHelpers
224
+ helper_method :parent_type, :parent_class
225
+ end
226
+ end
227
+
228
+ # Initialize resources class accessors and set their default values.
229
+ #
230
+ def initialize_resources_class_accessors! #:nodoc:
231
+ # Initialize resource class
232
+ self.resource_class = begin
233
+ class_name = self.controller_name.classify
234
+ class_name.constantize
235
+ rescue NameError => e
236
+ raise unless e.message.include?(class_name)
237
+ nil
238
+ end
239
+
240
+ # Initialize resources configuration hash
241
+ self.resources_configuration ||= {}
242
+ config = self.resources_configuration[:self] = {}
243
+ config[:collection_name] = self.controller_name.to_sym
244
+ config[:instance_name] = self.controller_name.singularize.to_sym
245
+
246
+ config[:route_collection_name] = config[:collection_name]
247
+ config[:route_instance_name] = config[:instance_name]
248
+
249
+ # Deal with namespaced controllers
250
+ namespaces = self.controller_path.split('/')[0..-2]
251
+ config[:route_prefix] = namespaces.join('_') unless namespaces.empty?
252
+
253
+ # Initialize polymorphic, singleton, scopes and belongs_to parameters
254
+ self.parents_symbols ||= []
255
+ self.resources_configuration[:polymorphic] ||= { :symbols => [], :optional => false }
256
+ end
257
+
258
+ # Hook called on inheritance.
259
+ #
260
+ def inherited(base) #:nodoc:
261
+ super(base)
262
+ base.send :initialize_resources_class_accessors!
263
+ base.send :create_resources_url_helpers!
264
+ end
265
+
266
+ end
267
+ end
@@ -0,0 +1,26 @@
1
+ module InheritedResources
2
+ # Allows controllers to write actions using a class method DSL.
3
+ #
4
+ # class MyController < InheritedResources::Base
5
+ # create! do |success, failure|
6
+ # success.html { render :text => "It works!" }
7
+ # end
8
+ # end
9
+ #
10
+ module DSL
11
+ def self.included(base)
12
+ ACTIONS.each do |action|
13
+ base.class_eval <<-WRITTER
14
+ def self.#{action}!(options={}, &block)
15
+ define_method :__#{action}, &block
16
+ class_eval <<-ACTION
17
+ def #{action}
18
+ super(\#{options.inspect}, &method(:__#{action}))
19
+ end
20
+ ACTION
21
+ end
22
+ WRITTER
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,155 @@
1
+ module InheritedResources
2
+
3
+ # = polymorphic associations
4
+ #
5
+ # In some cases you have a resource that belongs to two different resources
6
+ # but not at the same time. For example, let's suppose you have File, Message
7
+ # and Task as resources and they are all commentable.
8
+ #
9
+ # Polymorphic associations allows you to create just one controller that will
10
+ # deal with each case.
11
+ #
12
+ # class Comment < InheritedResources::Base
13
+ # belongs_to :file, :message, :task, :polymorphic => true
14
+ # end
15
+ #
16
+ # Your routes should be something like:
17
+ #
18
+ # m.resources :files, :has_many => :comments #=> /files/13/comments
19
+ # m.resources :tasks, :has_many => :comments #=> /tasks/17/comments
20
+ # m.resources :messages, :has_many => :comments #=> /messages/11/comments
21
+ #
22
+ # When using polymorphic associations, you get some free helpers:
23
+ #
24
+ # parent? #=> true
25
+ # parent_type #=> :task
26
+ # parent_class #=> Task
27
+ # parent #=> @task
28
+ #
29
+ # This polymorphic controllers thing is a great idea by James Golick and he
30
+ # built it in resource_controller. Here is just a re-implementation.
31
+ #
32
+ # = optional polymorphic associations
33
+ #
34
+ # Let's take another break from ProjectsController. Let's suppose we are
35
+ # building a store, which sell products.
36
+ #
37
+ # On the website, we can show all products, but also products scoped to
38
+ # categories, brands, users. In this case case, the association is optional, and
39
+ # we deal with it in the following way:
40
+ #
41
+ # class ProductsController < InheritedResources::Base
42
+ # belongs_to :category, :brand, :user, :polymorphic => true, :optional => true
43
+ # end
44
+ #
45
+ # This will handle all those urls properly:
46
+ #
47
+ # /products/1
48
+ # /categories/2/products/5
49
+ # /brands/10/products/3
50
+ # /user/13/products/11
51
+ #
52
+ # = nested polymorphic associations
53
+ #
54
+ # You can have polymorphic associations with nested resources. Let's suppose
55
+ # that our File, Task and Message resources in the previous example belongs to
56
+ # a project.
57
+ #
58
+ # This way we can have:
59
+ #
60
+ # class CommentsController < InheritedResources::Base
61
+ # belongs_to :project {
62
+ # belongs_to :file, :message, :task, :polymorphic => true
63
+ # }
64
+ # end
65
+ #
66
+ # Or:
67
+ #
68
+ # class CommentsController < InheritedResources::Base
69
+ # nested_belongs_to :project
70
+ # nested_belongs_to :file, :message, :task, :polymorphic => true
71
+ # end
72
+ #
73
+ # Choose the syntax that makes more sense to you. :)
74
+ #
75
+ # Finally your routes should be something like:
76
+ #
77
+ # map.resources :projects do |m|
78
+ # m.resources :files, :has_many => :comments #=> /projects/1/files/13/comments
79
+ # m.resources :tasks, :has_many => :comments #=> /projects/1/tasks/17/comments
80
+ # m.resources :messages, :has_many => :comments #=> /projects/1/messages/11/comments
81
+ # end
82
+ #
83
+ # The helpers work in the same way as above.
84
+ #
85
+ module PolymorphicHelpers
86
+
87
+ protected
88
+
89
+ # Returns the parent type. A Comments class can have :task, :file, :note
90
+ # as parent types.
91
+ #
92
+ def parent_type
93
+ @parent_type
94
+ end
95
+
96
+ def parent_class
97
+ parent.class if @parent_type
98
+ end
99
+
100
+ # Returns the parent object. They are also available with the instance
101
+ # variable name: @task, @file, @note...
102
+ #
103
+ def parent
104
+ instance_variable_get("@#{@parent_type}") if @parent_type
105
+ end
106
+
107
+ # If the polymorphic association is optional, we might not have a parent.
108
+ #
109
+ def parent?
110
+ if resources_configuration[:polymorphic][:optional]
111
+ parents_symbols.size > 1 || !@parent_type.nil?
112
+ else
113
+ true
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ # Maps parents_symbols to build association chain.
120
+ #
121
+ # If the parents_symbols find :polymorphic, it goes through the
122
+ # params keys to see which polymorphic parent matches the given params.
123
+ #
124
+ # When optional is given, it does not raise errors if the polymorphic
125
+ # params are missing.
126
+ #
127
+ def symbols_for_association_chain #:nodoc:
128
+ polymorphic_config = resources_configuration[:polymorphic]
129
+ parents_symbols.map do |symbol|
130
+ if symbol == :polymorphic
131
+ params_keys = params.keys
132
+
133
+ keys = polymorphic_config[:symbols].map do |poly|
134
+ params_keys.include?(resources_configuration[poly][:param].to_s) ? poly : nil
135
+ end.compact
136
+
137
+ if keys.empty?
138
+ raise ScriptError, "Could not find param for polymorphic association. The request" <<
139
+ "parameters are #{params.keys.inspect} and the polymorphic " <<
140
+ "associations are #{polymorphic_config[:symbols].inspect}." unless polymorphic_config[:optional]
141
+
142
+ nil
143
+ else
144
+ @parent_type = keys[-1].to_sym
145
+ @parent_types = keys.map(&:to_sym)
146
+ end
147
+ else
148
+ symbol
149
+ end
150
+ end.flatten.compact
151
+ end
152
+
153
+ end
154
+ end
155
+