inherited_resources 0.9.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.
Files changed (37) hide show
  1. data/CHANGELOG +103 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +524 -0
  4. data/Rakefile +40 -0
  5. data/lib/inherited_resources.rb +23 -0
  6. data/lib/inherited_resources/actions.rb +79 -0
  7. data/lib/inherited_resources/base.rb +42 -0
  8. data/lib/inherited_resources/base_helpers.rb +363 -0
  9. data/lib/inherited_resources/belongs_to_helpers.rb +89 -0
  10. data/lib/inherited_resources/class_methods.rb +338 -0
  11. data/lib/inherited_resources/dsl.rb +26 -0
  12. data/lib/inherited_resources/dumb_responder.rb +20 -0
  13. data/lib/inherited_resources/has_scope_helpers.rb +83 -0
  14. data/lib/inherited_resources/legacy/respond_to.rb +156 -0
  15. data/lib/inherited_resources/legacy/responder.rb +200 -0
  16. data/lib/inherited_resources/polymorphic_helpers.rb +155 -0
  17. data/lib/inherited_resources/singleton_helpers.rb +95 -0
  18. data/lib/inherited_resources/url_helpers.rb +179 -0
  19. data/test/aliases_test.rb +139 -0
  20. data/test/association_chain_test.rb +125 -0
  21. data/test/base_test.rb +225 -0
  22. data/test/belongs_to_test.rb +87 -0
  23. data/test/class_methods_test.rb +138 -0
  24. data/test/customized_base_test.rb +162 -0
  25. data/test/customized_belongs_to_test.rb +76 -0
  26. data/test/defaults_test.rb +70 -0
  27. data/test/flash_test.rb +88 -0
  28. data/test/has_scope_test.rb +139 -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/respond_to_test.rb +155 -0
  34. data/test/singleton_test.rb +83 -0
  35. data/test/test_helper.rb +38 -0
  36. data/test/url_helpers_test.rb +537 -0
  37. metadata +89 -0
@@ -0,0 +1,89 @@
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
+ private
61
+
62
+ # Evaluate the parent given. This is used to nest parents in the
63
+ # association chain.
64
+ #
65
+ def evaluate_parent(parent_symbol, parent_config, chain = nil) #:nodoc:
66
+ instantiated_object = instance_variable_get("@#{parent_config[:instance_name]}")
67
+ return instantiated_object if instantiated_object
68
+
69
+ parent = if chain
70
+ chain.send(parent_config[:collection_name])
71
+ else
72
+ parent_config[:parent_class]
73
+ end
74
+
75
+ parent = parent.send(parent_config[:finder], params[parent_config[:param]])
76
+
77
+ instance_variable_set("@#{parent_config[:instance_name]}", parent)
78
+ end
79
+
80
+ # Maps parents_symbols to build association chain. In this case, it
81
+ # simply return the parent_symbols, however on polymorphic belongs to,
82
+ # it has some customization.
83
+ #
84
+ def symbols_for_association_chain #:nodoc:
85
+ parents_symbols
86
+ end
87
+
88
+ end
89
+ end
@@ -0,0 +1,338 @@
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
+ # Detects params from url and apply as scopes to your classes.
75
+ #
76
+ # Your model:
77
+ #
78
+ # class Graduation < ActiveRecord::Base
79
+ # named_scope :featured, :conditions => { :featured => true }
80
+ # named_scope :by_degree, proc {|degree| { :conditions => { :degree => degree } } }
81
+ # end
82
+ #
83
+ # Your controller:
84
+ #
85
+ # class GraduationsController < InheritedResources::Base
86
+ # has_scope :featured, :boolean => true, :only => :index
87
+ # has_scope :by_degree, :only => :index
88
+ # end
89
+ #
90
+ # Then for each request:
91
+ #
92
+ # /graduations
93
+ # #=> acts like a normal request
94
+ #
95
+ # /graduations?featured=true
96
+ # #=> calls the named scope and bring featured graduations
97
+ #
98
+ # /graduations?featured=true&by_degree=phd
99
+ # #=> brings featured graduations with phd degree
100
+ #
101
+ # You can retrieve the current scopes in use with <tt>current_scopes</tt>
102
+ # method. In the last case, it would return: { :featured => "true", :by_degree => "phd" }
103
+ #
104
+ # == Options
105
+ #
106
+ # * <tt>:boolean</tt> - When set to true, call the scope only when the param is true or 1,
107
+ # and does not send the value as argument.
108
+ #
109
+ # * <tt>:only</tt> - In which actions the scope is applied. By default is :all.
110
+ #
111
+ # * <tt>:except</tt> - In which actions the scope is not applied. By default is :none.
112
+ #
113
+ # * <tt>:as</tt> - The key in the params hash expected to find the scope.
114
+ # Defaults to the scope name.
115
+ #
116
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine
117
+ # if the scope should apply
118
+ #
119
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine
120
+ # if the scope should NOT apply.
121
+ #
122
+ # * <tt>:default</tt> - Default value for the scope. Whenever supplied the scope
123
+ # is always called. This is useful to add easy pagination.
124
+ #
125
+ def has_scope(*scopes)
126
+ options = scopes.extract_options!
127
+
128
+ options.symbolize_keys!
129
+ options.assert_valid_keys(:boolean, :key, :only, :except,
130
+ :if, :unless, :default, :as)
131
+
132
+ if options[:key]
133
+ ActiveSupport::Deprecation.warn "has_scope :key is deprecated, use :as instead"
134
+ options[:as] ||= options[:key]
135
+ end
136
+
137
+ if self.scopes_configuration.empty?
138
+ include HasScopeHelpers
139
+ helper_method :current_scopes
140
+ end
141
+
142
+ scopes.each do |scope|
143
+ self.scopes_configuration[scope] ||= {}
144
+ self.scopes_configuration[scope][:as] = options[:as] || scope
145
+ self.scopes_configuration[scope][:only] = Array(options[:only])
146
+ self.scopes_configuration[scope][:except] = Array(options[:except])
147
+
148
+ [:if, :unless, :boolean, :default].each do |opt|
149
+ self.scopes_configuration[scope][opt] = options[opt] if options.key?(opt)
150
+ end
151
+ end
152
+ end
153
+
154
+ # Defines that this controller belongs to another resource.
155
+ #
156
+ # belongs_to :projects
157
+ #
158
+ # == Options
159
+ #
160
+ # * <tt>:parent_class</tt> - Allows you to specify what is the parent class.
161
+ #
162
+ # belongs_to :project, :parent_class => AdminProject
163
+ #
164
+ # * <tt>:class_name</tt> - Also allows you to specify the parent class, but you should
165
+ # give a string. Added for ActiveRecord belongs to compatibility.
166
+ #
167
+ # * <tt>:instance_name</tt> - The instance variable name. By default is the name of the association.
168
+ #
169
+ # belongs_to :project, :instance_name => :my_project
170
+ #
171
+ # * <tt>:finder</tt> - Specifies which method should be called to instantiate the parent.
172
+ #
173
+ # belongs_to :project, :finder => :find_by_title!
174
+ #
175
+ # This will make your projects be instantiated as:
176
+ #
177
+ # Project.find_by_title!(params[:project_id])
178
+ #
179
+ # Instead of:
180
+ #
181
+ # Project.find(params[:project_id])
182
+ #
183
+ # * <tt>:param</tt> - Allows you to specify params key to retrieve the id.
184
+ # Default is :association_id, which in this case is :project_id.
185
+ #
186
+ # * <tt>:route_name</tt> - Allows you to specify what is the route name in your url
187
+ # helper. By default is association name.
188
+ #
189
+ # * <tt>:collection_name</tt> - Tell how to retrieve the next collection. Let's
190
+ # suppose you have Tasks which belongs to Projects
191
+ # which belongs to companies. This will do somewhere
192
+ # down the road:
193
+ #
194
+ # @company.projects
195
+ #
196
+ # But if you want to retrieve instead:
197
+ #
198
+ # @company.admin_projects
199
+ #
200
+ # You supply the collection name.
201
+ #
202
+ # * <tt>:polymorphic</tt> - Tell the association is polymorphic.
203
+ #
204
+ # * <tt>:singleton</tt> - Tell it's a singleton association.
205
+ #
206
+ # * <tt>:optional</tt> - Tell the association is optional (it's a special
207
+ # type of polymorphic association)
208
+ #
209
+ def belongs_to(*symbols, &block)
210
+ options = symbols.extract_options!
211
+
212
+ options.symbolize_keys!
213
+ options.assert_valid_keys(:class_name, :parent_class, :instance_name, :param,
214
+ :finder, :route_name, :collection_name, :singleton,
215
+ :polymorphic, :optional)
216
+
217
+ optional = options.delete(:optional)
218
+ singleton = options.delete(:singleton)
219
+ polymorphic = options.delete(:polymorphic)
220
+ finder = options.delete(:finder)
221
+
222
+ include BelongsToHelpers if self.parents_symbols.empty?
223
+
224
+ acts_as_singleton! if singleton
225
+ acts_as_polymorphic! if polymorphic || optional
226
+
227
+ raise ArgumentError, 'You have to give me at least one association name.' if symbols.empty?
228
+ raise ArgumentError, 'You cannot define multiple associations with options: #{options.keys.inspect} to belongs to.' unless symbols.size == 1 || options.empty?
229
+
230
+ symbols.each do |symbol|
231
+ symbol = symbol.to_sym
232
+
233
+ if polymorphic || optional
234
+ self.parents_symbols << :polymorphic unless self.parents_symbols.include?(:polymorphic)
235
+ self.resources_configuration[:polymorphic][:symbols] << symbol
236
+ self.resources_configuration[:polymorphic][:optional] ||= optional
237
+ else
238
+ self.parents_symbols << symbol
239
+ end
240
+
241
+ config = self.resources_configuration[symbol] = {}
242
+ config[:parent_class] = options.delete(:parent_class)
243
+ config[:parent_class] ||= (options.delete(:class_name) || symbol).to_s.pluralize.classify.constantize rescue nil
244
+ config[:collection_name] = options.delete(:collection_name) || symbol.to_s.pluralize.to_sym
245
+ config[:instance_name] = options.delete(:instance_name) || symbol
246
+ config[:param] = options.delete(:param) || :"#{symbol}_id"
247
+ config[:route_name] = options.delete(:route_name) || symbol
248
+ config[:finder] = finder || :find
249
+ end
250
+
251
+ if block_given?
252
+ class_eval(&block)
253
+ else
254
+ create_resources_url_helpers!
255
+ end
256
+ end
257
+ alias :nested_belongs_to :belongs_to
258
+
259
+ # A quick method to declare polymorphic belongs to.
260
+ #
261
+ def polymorphic_belongs_to(*symbols, &block)
262
+ options = symbols.extract_options!
263
+ options.merge!(:polymorphic => true)
264
+ belongs_to(*symbols << options, &block)
265
+ end
266
+
267
+ # A quick method to declare singleton belongs to.
268
+ #
269
+ def singleton_belongs_to(*symbols, &block)
270
+ options = symbols.extract_options!
271
+ options.merge!(:singleton => true)
272
+ belongs_to(*symbols << options, &block)
273
+ end
274
+
275
+ # A quick method to declare optional belongs to.
276
+ #
277
+ def optional_belongs_to(*symbols, &block)
278
+ options = symbols.extract_options!
279
+ options.merge!(:optional => true)
280
+ belongs_to(*symbols << options, &block)
281
+ end
282
+
283
+ private
284
+
285
+ def acts_as_singleton! #:nodoc:
286
+ unless self.resources_configuration[:self][:singleton]
287
+ self.resources_configuration[:self][:singleton] = true
288
+ include SingletonHelpers
289
+ actions :all, :except => :index
290
+ end
291
+ end
292
+
293
+ def acts_as_polymorphic! #:nodoc:
294
+ unless self.parents_symbols.include?(:polymorphic)
295
+ include PolymorphicHelpers
296
+ helper_method :parent, :parent_type, :parent_class, :parent?
297
+ end
298
+ end
299
+
300
+ # Initialize resources class accessors and set their default values.
301
+ #
302
+ def initialize_resources_class_accessors! #:nodoc:
303
+ # Initialize resource class
304
+ self.resource_class = begin
305
+ self.controller_name.classify.constantize
306
+ rescue NameError
307
+ nil
308
+ end
309
+
310
+ # Initialize resources configuration hash
311
+ self.resources_configuration ||= {}
312
+ config = self.resources_configuration[:self] = {}
313
+ config[:collection_name] = self.controller_name.to_sym
314
+ config[:instance_name] = self.controller_name.singularize.to_sym
315
+
316
+ config[:route_collection_name] = config[:collection_name]
317
+ config[:route_instance_name] = config[:instance_name]
318
+
319
+ # Deal with namespaced controllers
320
+ namespaces = self.controller_path.split('/')[0..-2]
321
+ config[:route_prefix] = namespaces.join('_') unless namespaces.empty?
322
+
323
+ # Initialize polymorphic, singleton, scopes and belongs_to parameters
324
+ self.parents_symbols ||= []
325
+ self.scopes_configuration ||= {}
326
+ self.resources_configuration[:polymorphic] ||= { :symbols => [], :optional => false }
327
+ end
328
+
329
+ # Hook called on inheritance.
330
+ #
331
+ def inherited(base) #:nodoc:
332
+ super(base)
333
+ base.send :initialize_resources_class_accessors!
334
+ base.send :create_resources_url_helpers!
335
+ end
336
+
337
+ end
338
+ 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