inherited_resources 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
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