rc_rails 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. data/.gitignore +7 -0
  2. data/CHANGELOG +355 -0
  3. data/Gemfile +5 -0
  4. data/Gemfile.lock.development +117 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.rdoc +71 -0
  7. data/Rakefile +33 -0
  8. data/Todo.txt +1 -0
  9. data/lib/rc_rails.rb +9 -0
  10. data/lib/resources_controller/actions.rb +147 -0
  11. data/lib/resources_controller/active_record/saved.rb +15 -0
  12. data/lib/resources_controller/helper.rb +123 -0
  13. data/lib/resources_controller/include_actions.rb +37 -0
  14. data/lib/resources_controller/named_route_helper.rb +154 -0
  15. data/lib/resources_controller/railtie.rb +14 -0
  16. data/lib/resources_controller/request_path_introspection.rb +83 -0
  17. data/lib/resources_controller/resource_methods.rb +32 -0
  18. data/lib/resources_controller/singleton_actions.rb +21 -0
  19. data/lib/resources_controller/specification.rb +119 -0
  20. data/lib/resources_controller/version.rb +3 -0
  21. data/lib/resources_controller.rb +849 -0
  22. data/resources_controller.gemspec +29 -0
  23. data/spec/app/database.yml +5 -0
  24. data/spec/app/views/accounts/show.html.erb +0 -0
  25. data/spec/app/views/addresses/edit.html.erb +0 -0
  26. data/spec/app/views/addresses/index.html.erb +0 -0
  27. data/spec/app/views/addresses/new.html.erb +0 -0
  28. data/spec/app/views/addresses/show.html.erb +0 -0
  29. data/spec/app/views/admin/forums/create.html.erb +0 -0
  30. data/spec/app/views/admin/forums/destroy.html.erb +0 -0
  31. data/spec/app/views/admin/forums/edit.html.erb +0 -0
  32. data/spec/app/views/admin/forums/index.html.erb +0 -0
  33. data/spec/app/views/admin/forums/new.html.erb +0 -0
  34. data/spec/app/views/admin/forums/show.html.erb +0 -0
  35. data/spec/app/views/admin/forums/update.html.erb +0 -0
  36. data/spec/app/views/comments/edit.html.erb +0 -0
  37. data/spec/app/views/comments/index.html.erb +0 -0
  38. data/spec/app/views/comments/new.html.erb +0 -0
  39. data/spec/app/views/comments/show.html.erb +0 -0
  40. data/spec/app/views/forum_posts/edit.html.erb +0 -0
  41. data/spec/app/views/forum_posts/index.html.erb +0 -0
  42. data/spec/app/views/forum_posts/new.html.erb +0 -0
  43. data/spec/app/views/forum_posts/show.html.erb +0 -0
  44. data/spec/app/views/forums/create.html.erb +0 -0
  45. data/spec/app/views/forums/destroy.html.erb +0 -0
  46. data/spec/app/views/forums/edit.html.erb +0 -0
  47. data/spec/app/views/forums/index.html.erb +0 -0
  48. data/spec/app/views/forums/new.html.erb +0 -0
  49. data/spec/app/views/forums/show.html.erb +0 -0
  50. data/spec/app/views/forums/update.html.erb +0 -0
  51. data/spec/app/views/infos/edit.html.erb +0 -0
  52. data/spec/app/views/infos/show.html.erb +0 -0
  53. data/spec/app/views/interests/index.html.erb +0 -0
  54. data/spec/app/views/interests/show.html.erb +0 -0
  55. data/spec/app/views/owners/edit.html.erb +0 -0
  56. data/spec/app/views/owners/new.html.erb +0 -0
  57. data/spec/app/views/owners/show.html.erb +0 -0
  58. data/spec/app/views/tags/index.html.erb +0 -0
  59. data/spec/app/views/tags/new.html.erb +0 -0
  60. data/spec/app/views/tags/show.html.erb +0 -0
  61. data/spec/app/views/users/edit.html.erb +0 -0
  62. data/spec/app/views/users/index.html.erb +0 -0
  63. data/spec/app/views/users/show.html.erb +0 -0
  64. data/spec/app.rb +315 -0
  65. data/spec/controllers/accounts_controller_spec.rb +77 -0
  66. data/spec/controllers/addresses_controller_spec.rb +346 -0
  67. data/spec/controllers/admin_forums_controller_spec.rb +638 -0
  68. data/spec/controllers/comments_controller_spec.rb +380 -0
  69. data/spec/controllers/comments_controller_with_models_spec.rb +202 -0
  70. data/spec/controllers/forum_posts_controller_spec.rb +426 -0
  71. data/spec/controllers/forums_controller_spec.rb +694 -0
  72. data/spec/controllers/infos_controller_spec.rb +71 -0
  73. data/spec/controllers/interests_controller_via_forum_spec.rb +80 -0
  74. data/spec/controllers/interests_controller_via_user_spec.rb +114 -0
  75. data/spec/controllers/owners_controller_spec.rb +277 -0
  76. data/spec/controllers/resource_saved_spec.rb +47 -0
  77. data/spec/controllers/resource_service_in_forums_controller_spec.rb +37 -0
  78. data/spec/controllers/resource_service_in_infos_controller_spec.rb +36 -0
  79. data/spec/controllers/resource_service_in_interests_controller_via_forum_spec.rb +51 -0
  80. data/spec/controllers/tags_controller_spec.rb +83 -0
  81. data/spec/controllers/tags_controller_via_account_info_spec.rb +131 -0
  82. data/spec/controllers/tags_controller_via_forum_post_comment_spec.rb +144 -0
  83. data/spec/controllers/tags_controller_via_forum_post_spec.rb +133 -0
  84. data/spec/controllers/tags_controller_via_forum_spec.rb +173 -0
  85. data/spec/controllers/tags_controller_via_user_address_spec.rb +130 -0
  86. data/spec/controllers/users_controller_spec.rb +248 -0
  87. data/spec/lib/action_view_helper_spec.rb +143 -0
  88. data/spec/lib/bug_0001_spec.rb +22 -0
  89. data/spec/lib/include_actions_spec.rb +35 -0
  90. data/spec/lib/load_enclosing_resources_spec.rb +245 -0
  91. data/spec/lib/request_path_introspection_spec.rb +130 -0
  92. data/spec/lib/resource_methods_spec.rb +204 -0
  93. data/spec/lib/resources_controller_spec.rb +57 -0
  94. data/spec/models/comment_saved_spec.rb +24 -0
  95. data/spec/rspec_generator_task.rb +105 -0
  96. data/spec/spec_helper.rb +17 -0
  97. data/spec/verify_rcov.rb +52 -0
  98. metadata +193 -0
@@ -0,0 +1,849 @@
1
+ require 'resources_controller/active_record/saved'
2
+ require 'resources_controller/railtie' if defined?(Rails)
3
+
4
+ require 'resources_controller/actions'
5
+ require 'resources_controller/helper'
6
+ require 'resources_controller/include_actions'
7
+ require 'resources_controller/named_route_helper'
8
+ require 'resources_controller/request_path_introspection'
9
+ require 'resources_controller/resource_methods'
10
+ require 'resources_controller/singleton_actions'
11
+ require 'resources_controller/specification'
12
+
13
+ # With resources_controller you can quickly add
14
+ # an ActiveResource compliant controller for your your RESTful models.
15
+ #
16
+ # = Examples
17
+ # Here are some examples - for more on how to use RC go to the Usage section at the bottom,
18
+ # for syntax head to resources_controller_for
19
+ #
20
+ # ==== Example 1: Super simple usage
21
+ # Here's a simple example of how it works with a Forums has many Posts model:
22
+ #
23
+ # class ForumsController < ApplicationController
24
+ # resources_controller_for :forums
25
+ # end
26
+ #
27
+ # Your controller will get the standard CRUD actions, @forum will be set in member actions, @forums in
28
+ # index.
29
+ #
30
+ # ==== Example 2: Specifying enclosing resources
31
+ # class PostsController < ApplicationController
32
+ # resources_controller_for :posts, :in => :forum
33
+ # end
34
+ #
35
+ # As above, but the controller will load @forum on every action, and use @forum to find and create @posts
36
+ #
37
+ # ==== Wildcard enclosing resources
38
+ # All of the above examples will work for any routes that match what it specified
39
+ #
40
+ # PATH RESOURCES CONTROLLER WILL DO:
41
+ #
42
+ # Example 1 /forums @forums = Forum.find(:all)
43
+ #
44
+ # /users/2/forums @user = User.find(2)
45
+ # @forums = @user.forums.find(:all)
46
+ #
47
+ # Example 2 /posts This won't work as the controller specified
48
+ # that :posts are :in => :forum
49
+ #
50
+ # /forums/2/posts @forum = Forum.find(2)
51
+ # @posts = @forum.posts.find(:all)
52
+ #
53
+ # /sites/4/forums/3/posts @site = Site.find(4)
54
+ # @forum = @site.forums.find(3)
55
+ # @posts = @forum.posts.find(:all)
56
+ #
57
+ # /users/2/posts/1 This won't work as the controller specified
58
+ # that :posts are :in => :forum
59
+ #
60
+ #
61
+ # It is up to you which routes to open to the controller (in config/routes.rb). When
62
+ # you do, RC will use the route segments to drill down to the specified resource. This means
63
+ # that if User 3 does not have Post 5, then /users/3/posts/5 will raise a RecordNotFound Error.
64
+ # You dont' have to write any extra code to do this oft repeated controller pattern.
65
+ #
66
+ # With RC, your route specification flows through to the controller - no need to repeat yourself.
67
+ #
68
+ # If you don't want to have RC match wildcard resources just pass :load_enclosing => false
69
+ #
70
+ # resources_controller_for :posts, :in => :forum, :load_enclosing => false
71
+ #
72
+ # ==== Example 3: Singleton resource
73
+ # Here's an example of a singleton, the account pattern that is so common.
74
+ #
75
+ # class AccountController < ApplicationController
76
+ # resources_controller_for :account, :class => User, :singleton => true do
77
+ # @current_user
78
+ # end
79
+ # end
80
+ #
81
+ # Your controller will use the block to find the resource. The @account will be assigned to @current_user
82
+ #
83
+ # ==== Example 4: Allowing PostsController to be used all over
84
+ # First thing to do is remove :in => :forum
85
+ #
86
+ # class PostsController < ApplicationController
87
+ # resources_controller_for :posts
88
+ # end
89
+ #
90
+ # This will now work for /users/2/posts.
91
+ #
92
+ # ==== Example 4 and a bit: Mapping non standard resources
93
+ # How about /account/posts? The account is found in a non standard way - RC won't be able
94
+ # to figure out how tofind it if it appears in the route. So we give it some help.
95
+ #
96
+ # (in PostsController)
97
+ #
98
+ # map_enclosing_resource :account, :singleton => true, :class => User, :find => :current_user
99
+ #
100
+ # Now, if :account apears in any part of a route (for PostsController) it will be mapped to
101
+ # (in this case) the current_user method of teh PostsController.
102
+ #
103
+ # To make the :account mapping available to all, just chuck it in ApplicationController
104
+ #
105
+ # This will work for any resource which can't be inferred from its route segment name
106
+ #
107
+ # map_enclosing_resource :users, :segment => :peeps, :key => 'peep_id'
108
+ # map_enclosing_resource :posts, :class => OddlyNamedPostClass
109
+ #
110
+ # ==== Example 5: Singleton association
111
+ # Here's another singleton example - one where it corresponds to a has_one or belongs_to association
112
+ #
113
+ # class ImageController < ApplicationController
114
+ # resources_controller_for :image, :singleton => true
115
+ # end
116
+ #
117
+ # When invoked with /users/3/image RC will find @user, and use @user.image to find the resource, and
118
+ # @user.build_image, to create a new resource.
119
+ #
120
+ # ==== Example 6: :resource_path (equivalent resource path): aliasing a named route to a RESTful route
121
+ #
122
+ # You may have a named route that maps a url to a particular controller and action,
123
+ # this causes resources_controller problems as it relies on the route to load the
124
+ # resources. You can get around this by specifying :resource_path as a param in routes.rb
125
+ #
126
+ # map.root :controller => :forums, :action => :index, :resource_path => '/forums'
127
+ #
128
+ # When the controller is invoked via the '' url, rc will use :resource_path to recognize the
129
+ # route.
130
+ #
131
+ # This is only necessary if you have wildcard enclosing resources enabled (the default)
132
+ #
133
+ # ==== Putting it all together
134
+ #
135
+ # An exmaple app
136
+ #
137
+ # config/routes.rb:
138
+ #
139
+ # map.resource :account do |account|
140
+ # account.resource :image
141
+ # account.resources :posts
142
+ # end
143
+ #
144
+ # map.resources :users do |user|
145
+ # user.resource :image
146
+ # user.resources :posts
147
+ # end
148
+ #
149
+ # map.resources :forums do |forum|
150
+ # forum.resources :posts
151
+ # forum.resource :image
152
+ # end
153
+ #
154
+ # map.root :controller => :forums, :action => :index, :resource_path => '/forums'
155
+ #
156
+ # app/controllers:
157
+ #
158
+ # class ApplicationController < ActionController::Base
159
+ # map_enclosing_resource :account, :singleton => true, :find => :current_user
160
+ #
161
+ # def current_user # get it from session or whatnot
162
+ # end
163
+ #
164
+ # class ForumsController < AplicationController
165
+ # resources_controller_for :forums
166
+ # end
167
+ #
168
+ # class PostsController < AplicationController
169
+ # resources_controller_for :posts
170
+ # end
171
+ #
172
+ # class UsersController < AplicationController
173
+ # resources_controller_for :users
174
+ # end
175
+ #
176
+ # class ImageController < AplicationController
177
+ # resources_controller_for :image, :singleton => true
178
+ # end
179
+ #
180
+ # class AccountController < ApplicationController
181
+ # resources_controller_for :account, :singleton => true, :find => :current_user
182
+ # end
183
+ #
184
+ # This is how the app will handle the following routes:
185
+ #
186
+ # PATH CONTROLLER WHICH WILL DO:
187
+ #
188
+ # /forums forums @forums = Forum.find(:all)
189
+ #
190
+ # /forums/2/posts posts @forum = Forum.find(2)
191
+ # @posts = @forum.forums.find(:all)
192
+ #
193
+ # /forums/2/image image @forum = Forum.find(2)
194
+ # @image = @forum.image
195
+ #
196
+ # /image <no route>
197
+ #
198
+ # /posts <no route>
199
+ #
200
+ # /users/2/posts/3 posts @user = User.find(2)
201
+ # @post = @user.posts.find(3)
202
+ #
203
+ # /users/2/image POST image @user = User.find(2)
204
+ # @image = @user.build_image(params[:image])
205
+ #
206
+ # /account account @account = self.current_user
207
+ #
208
+ # /account/image image @account = self.current_user
209
+ # @image = @account.image
210
+ #
211
+ # /account/posts/3 PUT posts @account = self.current_user
212
+ # @post = @account.posts.find(3)
213
+ # @post.update_attributes(params[:post])
214
+ #
215
+ # === Views
216
+ #
217
+ # Ok - so how do I write the views?
218
+ #
219
+ # For most cases, just in exactly the way you would expect to. RC sets the instance variables
220
+ # to what they should be.
221
+ #
222
+ # But, in some cases, you are going to have different variables set - for example
223
+ #
224
+ # /users/1/posts => @user, @posts
225
+ # /forums/2/posts => @forum, @posts
226
+ #
227
+ # Here are some options (all are appropriate for different circumstances):
228
+ # * test for the existence of @user or @forum in the view, and display it differently
229
+ # * have two different controllers UserPostsController and ForumPostsController, with different views
230
+ # (and direct the routes to them in routes.rb)
231
+ # * use enclosing_resource - which always refers to the... immediately enclosing resource.
232
+ #
233
+ # Using the last technique, you might write your posts index as follows
234
+ # (here assuming that both Forum and User have .name)
235
+ #
236
+ # <h1>Posts for <%= link_to enclosing_resource_path, "#{enclosing_resource_name.humanize}: #{enclosing_resource.name}" %></h1>
237
+ #
238
+ # <%= render :partial => 'post', :collection => @posts %>
239
+ #
240
+ # Notice *enclosing_resource_name* - this will be something like 'user', or 'post'.
241
+ # Also *enclosing_resource_path* - in RC you get all of the named route helpers relativised to the current resource
242
+ # and enclosing_resource. See NamedRouteHelper for more details.
243
+ #
244
+ # This can useful when writing the _post partial:
245
+ #
246
+ # <p>
247
+ # <%= post.name %>
248
+ # <%= link_to 'edit', edit_resource_path(tag) %>
249
+ # <%= link_to 'destroy', resource_path(tag), :method => :delete %>
250
+ # </p>
251
+ #
252
+ # when viewed at /users/1/posts it will show
253
+ #
254
+ # <p>
255
+ # Cool post
256
+ # <a href="/users/1/posts/1/edit">edit</a>
257
+ # <a href="js nightmare with /users/1/posts/1">delete</a>
258
+ # </p>
259
+ # ...
260
+ #
261
+ # when viewd at /forums/1/posts it will show
262
+ #
263
+ # <p>
264
+ # Other post
265
+ # <a href="/forums/1/posts/3/edit">edit</a>
266
+ # <a href="js nightmare with /forums/1/posts/3">delete</a>
267
+ # </p>
268
+ # ...
269
+ #
270
+ # This is like polymorphic urls, except that RC will just use whatever enclosing resources are loaded to generate the urls/paths.
271
+ #
272
+ # = Usage
273
+ # To use RC, there are just three class methods on controller to learn.
274
+ #
275
+ # resources_controller_for <name>, <options>, <&block>
276
+ #
277
+ # ClassMethods#nested_in <name>, <options>, <&block>
278
+ #
279
+ # map_enclosing_resource <name>, <options>, <&block>
280
+ #
281
+ # === Customising finding and creating
282
+ # If you want to implement something like query params you can override *find_resources*. If you want to change the
283
+ # way your new resources are created you can override *new_resource*.
284
+ #
285
+ # class PostsController < ApplicationController
286
+ # resources_controller_for :posts
287
+ #
288
+ # def find_resources
289
+ # resource_service.find :all, :order => params[:sort_by]
290
+ # end
291
+ #
292
+ # # you can call super to help yourself to the existing implementation
293
+ # def new_resource
294
+ # super.tap {|r| r.ip_address = request.ip_address }
295
+ # end
296
+ #
297
+ # In the same way, you can override *find_resource*.
298
+ #
299
+ # === Writing controller actions
300
+ #
301
+ # You can make use of RC internals to simplify your actions.
302
+ #
303
+ # Here's an example where you want to re-order an acts_as_list model. You define a class method
304
+ # on the model (say *order_by_ids* which takes and array of ids). You can then make use of *resource_service*
305
+ # (which makes use of awesome rails magic) to send correctly scoped messages to your models.
306
+ #
307
+ # Here's how to write an order action
308
+ #
309
+ # def order
310
+ # resource_service.order_by_ids["things_order"]
311
+ # end
312
+ #
313
+ # the route
314
+ #
315
+ # map.resources :things, :collection => {:order => :put}
316
+ #
317
+ # and the view can conatin a scriptaculous drag and drop with param name 'things_order'
318
+ #
319
+ # When this controller is invoked of /things the :order_by_ids message will be sent to the Thing class,
320
+ # when it's invoked by /foos/1/things, then :order_by_ids message will be send to Foo.find(1).things association
321
+ #
322
+ # === using non standard ids
323
+ #
324
+ # Lets say you want to set to_param to login, and use find_by_login
325
+ # for your users in your URLs, with routes as follows:
326
+ #
327
+ # map.reosurces :users do |user|
328
+ # user.resources :addresses
329
+ # end
330
+ #
331
+ # First, the users controller needs to find reosurces using find_by_login
332
+ #
333
+ # class UsersController < ApplicationController
334
+ # resources_controller_for :users
335
+ #
336
+ # protected
337
+ # def find_resource(id = params[:id])
338
+ # resource_service.find_by_login(id)
339
+ # end
340
+ # end
341
+ #
342
+ # This controller will find users (for editing, showing, and destroying) as
343
+ # directed. (this controller will work for any route where user is the
344
+ # last resource, including the /users/dave route)
345
+ #
346
+ # Now you need to specify that the user as enclosing resource needs to be found
347
+ # with find_by_login. For the addresses case above, you would do this:
348
+ #
349
+ # class AddressesController < ApplicationController
350
+ # resources_controller_for :addresses
351
+ # nested_in :user do
352
+ # User.find_by_login(params[:user_id])
353
+ # end
354
+ # end
355
+ #
356
+ # If you wanted to open up more nested resources under user, you could repeat
357
+ # this specification in all such controllers, alternatively, you could map the
358
+ # resource in the ApplicationController, which would be usable by any controller
359
+ #
360
+ # If you know that user is never nested (i.e. /users/dave/addresses), then do this:
361
+ #
362
+ # class ApplicationController < ActionController::Base
363
+ # map_enclosing_resource :user do
364
+ # User.find(params[:user_id])
365
+ # end
366
+ # end
367
+ #
368
+ # or, if user is sometimes nested (i.e. /forums/1/users/dave/addresses), do this:
369
+ #
370
+ # map_enclosing_resource :user do
371
+ # ((enclosing_resource && enclosing_resource.users) || User).find(params[:user_id])
372
+ # end
373
+ #
374
+ # Your Addresses controller will now be the very simple one, and the resource map will
375
+ # load user as specified when it is hit by a route /users/dave/addresses.
376
+ #
377
+ # class AddressesController < ApplicationController
378
+ # resources_controller_for :addresses
379
+ # end
380
+ #
381
+ module ResourcesController
382
+ mattr_accessor :actions, :singleton_actions
383
+ self.actions = ResourcesController::Actions
384
+ self.singleton_actions = ResourcesController::SingletonActions
385
+
386
+ def self.extended(base)
387
+ base.class_eval do
388
+ class_attribute :resource_specification_map
389
+ self.resource_specification_map = {}
390
+ end
391
+ end
392
+
393
+ # Specifies that this controller is a REST style controller for the named resource
394
+ #
395
+ # Enclosing resources are loaded automatically by default, you can turn this off with
396
+ # :load_enclosing (see options below)
397
+ #
398
+ # resources_controller_for <name>, <options>, <&block>
399
+ #
400
+ # ==== Options:
401
+ # * <tt>:singleton:</tt> (default false) set this to true if the resource is a Singleton
402
+ # * <tt>:find:</tt> (default null) set this to a symbol or Proc to specify how to find the resource.
403
+ # Use this if the resource is found in an unconventional way. Passing a block has the same effect as
404
+ # setting :find => a Proc
405
+ # * <tt>:in:</tt> specify the enclosing resources, by name. ClassMethods#nested_in can be used to
406
+ # specify this more fully.
407
+ # * <tt>:load_enclosing:</tt> (default true) loads enclosing resources automatically.
408
+ # * <tt>:actions:</tt> (default nil) set this to false if you don't want the default RC actions. Set this
409
+ # to a module to use that module for your own actions.
410
+ # * <tt>:only:</tt> only include the specified actions.
411
+ # * <tt>:except:</tt> include all actions except the specified actions.
412
+ #
413
+ # ===== Options for unconvential use
414
+ # (otherwise these are all inferred from the _name_)
415
+ # * <tt>:route:</tt> the route name (without name_prefix) if it can't be inferred from _name_.
416
+ # For a collection resource this should be plural, for a singleton it should be singular.
417
+ # * <tt>:source:</tt> a string or symbol (e.g. :users, or :user). This is used to find the class or association name
418
+ # * <tt>:class:</tt> a Class. This is the class of the resource (if it can't be inferred from _name_ or :source)
419
+ # * <tt>:segment:</tt> (e.g. 'users') the segment name in the route that is matched
420
+ #
421
+ # === The :in option
422
+ # The default behavior is to set up before filters that load the enclosing resource, and to use associations on
423
+ # that model to find and create the resources. See ClassMethods#nested_in for more details on this, and
424
+ # customising the default behaviour.
425
+ #
426
+ # === load_enclosing_resources
427
+ # By default, a before_filter is added by resources_controller called :load_enclosing_resources - which
428
+ # does all the work of loading the enclosing resources. You can use ActionControllers standard filter
429
+ # mechanisms to control when this filter is invoked. For example - you can choose not to load resources
430
+ # on an action
431
+ #
432
+ # resources_controller_for :foos
433
+ # skip_before_filter :load_enclosing_resources, :only => :static_page
434
+ #
435
+ # Or, you can change the order of when the filter is invoked by adding the filter call yourself (rc will
436
+ # only add the filter if it doesn't exist)
437
+ #
438
+ # before_filter :do_something
439
+ # prepend_before_filter :load_enclosing_resources
440
+ # resources_controller_for :foos
441
+ # before_filter :do_something_else # chain => [:load_enclosing_resources, :do_something, :do_something_else]
442
+ #
443
+ # === Default actions module
444
+ # If you have your own actions module you prefer to use other than the standard resources_controller ones
445
+ # you can set ResourcesController.actions to that module to have this be included by default
446
+ #
447
+ # ResourcesController.actions = MyAwesomeActions
448
+ # ResourcesController.singleton_actions = MyAweseomeSingletonActions
449
+ #
450
+ # class AwesomenessController < ApplicationController
451
+ # resources_controller_for :awesomenesses # includes MyAwesomeActions by default
452
+ # end
453
+ def resources_controller_for(name, options = {}, &block)
454
+ options.assert_valid_keys(:class, :source, :singleton, :actions, :in, :find, :load_enclosing, :route, :segment, :as, :only, :except, :resource_methods)
455
+ when_options = {:only => options.delete(:only), :except => options.delete(:except)}
456
+
457
+ unless included_modules.include? ResourcesController::InstanceMethods
458
+ class_attribute :specifications, :route_name
459
+ hide_action :specifications, :route_name
460
+ extend ResourcesController::ClassMethods
461
+ helper ResourcesController::Helper
462
+ include ResourcesController::InstanceMethods, ResourcesController::NamedRouteHelper
463
+ include ResourcesController::ResourceMethods unless options.delete(:resource_methods) == false || included_modules.include?(ResourcesController::ResourceMethods)
464
+ end
465
+
466
+ before_filter(:load_enclosing_resources, when_options.dup) unless load_enclosing_resources_filter_exists?
467
+
468
+ self.specifications = []
469
+ specifications << '*' unless options.delete(:load_enclosing) == false
470
+
471
+ unless (actions = options.delete(:actions)) == false
472
+ actions ||= options[:singleton] ? ResourcesController.singleton_actions : ResourcesController.actions
473
+ include_actions actions, when_options
474
+ end
475
+
476
+ route = (options.delete(:route) || name).to_s
477
+ name = options[:singleton] ? name.to_s : name.to_s.singularize
478
+ self.route_name = options[:singleton] ? route : route.singularize
479
+
480
+ nested_in(*options.delete(:in)) if options[:in]
481
+
482
+ class_attribute :resource_specification, :instance_writer => false
483
+ self.resource_specification = Specification.new(name, options, &block)
484
+ end
485
+
486
+ # Creates a resource specification mapping. Use this to specify how to find an enclosing resource that
487
+ # does not obey usual rails conventions. Most commonly this would be a singleton resource.
488
+ #
489
+ # See Specification#new for details of how to call this
490
+ def map_enclosing_resource(name, options = {}, &block)
491
+ spec = Specification.new(name, options, &block)
492
+ resource_specification_map[spec.segment] = spec
493
+ end
494
+
495
+ # this will be deprecated soon as it's badly named - use map_enclosing_resource
496
+ def map_resource(*args, &block)
497
+ map_enclosing_resource(*args, &block)
498
+ end
499
+
500
+ # Include the specified module, optionally specifying which public methods to include, for example:
501
+ # include_actions ActionMixin, :only => :index
502
+ # include_actions ActionMixin, :except => [:create, :new]
503
+ def include_actions(mixin, options = {})
504
+ mixin.extend(IncludeActions) unless mixin.respond_to?(:include_actions)
505
+ mixin.include_actions(self, options)
506
+ end
507
+
508
+ private
509
+ def load_enclosing_resources_filter_exists?
510
+ if respond_to?(:find_filter) # BC 2.0-stable branch
511
+ find_filter(:load_enclosing_resources)
512
+ else
513
+ _process_action_callbacks.detect {|c| c.filter == :load_enclosing_resources}
514
+ end
515
+ end
516
+
517
+ module ClassMethods
518
+ # Specifies that this controller has a particular enclosing resource.
519
+ #
520
+ # This can be called with an array of symbols (in which case options can't be specified) or
521
+ # a symbol with options.
522
+ #
523
+ # See Specification#new for details of how to call this.
524
+ def nested_in(*names, &block)
525
+ options = names.extract_options!
526
+ raise ArgumentError, "when giving more than one nesting, you may not specify options or a block" if names.length > 1 and (block_given? or options.length > 0)
527
+
528
+ # convert :polymorphic option to '?'
529
+ if options.delete(:polymorphic)
530
+ raise ArgumentError, "when specifying :polymorphic => true, no block or other options may be given" if block_given? or options.length > 0
531
+ names = ["?#{names.first}"]
532
+ end
533
+
534
+ # ignore first '*' if it has already been specified by :load_enclosing == true
535
+ names.shift if specifications == ['*'] && names.first == '*'
536
+
537
+ names.each do |name|
538
+ ensure_sane_wildcard if name == '*'
539
+ specifications << (name.to_s =~ /^(\*|\?(.*))$/ ? name.to_s : Specification.new(name, options, &block))
540
+ end
541
+ end
542
+
543
+ private
544
+ # ensure that specifications array is determinate w.r.t route matching
545
+ def ensure_sane_wildcard
546
+ idx = specifications.length
547
+ while (idx -= 1) >= 0
548
+ if specifications[idx] == '*'
549
+ raise ArgumentError, "Can only specify one wildcard '*' in between resource specifications"
550
+ elsif specifications[idx].is_a?(Specification)
551
+ break
552
+ end
553
+ end
554
+ true
555
+ end
556
+ end
557
+
558
+ module InstanceMethods
559
+ def self.included(controller)
560
+ controller.send :hide_action, *instance_methods
561
+ end
562
+
563
+ def resource_service=(service)
564
+ @resource_service = service
565
+ end
566
+
567
+ def name_prefix
568
+ @name_prefix ||= ''
569
+ end
570
+
571
+ # name of the singular resource
572
+ def resource_name
573
+ resource_specification.name
574
+ end
575
+
576
+ # name of the resource collection
577
+ def resources_name
578
+ @resources_name ||= resource_specification.name.pluralize
579
+ end
580
+
581
+ # returns the controller's resource class
582
+ def resource_class
583
+ resource_specification.klass
584
+ end
585
+
586
+ # returns the controller's current resource.
587
+ def resource
588
+ instance_variable_get("@#{resource_name}")
589
+ end
590
+
591
+ # sets the controller's current resource, and
592
+ # decorates the object with a save hook, so we know if it's been saved
593
+ def resource=(record)
594
+ instance_variable_set("@#{resource_name}", record)
595
+ end
596
+
597
+ # returns the controller's current resources collection
598
+ def resources
599
+ instance_variable_get("@#{resources_name}")
600
+ end
601
+
602
+ # sets the controller's current resource collection
603
+ def resources=(collection)
604
+ instance_variable_set("@#{resources_name}", collection)
605
+ end
606
+
607
+ # returns the immediately enclosing resource
608
+ def enclosing_resource
609
+ enclosing_resources.last
610
+ end
611
+
612
+ # returns the name of the immediately enclosing resource
613
+ def enclosing_resource_name
614
+ @enclosing_resource_name
615
+ end
616
+
617
+ # returns the resource service for the controller - this will be lazilly created
618
+ # to a ResourceService, or a SingletonResourceService (if :singleton => true)
619
+ def resource_service
620
+ @resource_service ||= resource_specification.singleton? ? SingletonResourceService.new(self) : ResourceService.new(self)
621
+ end
622
+
623
+ # returns the instance resource_specification
624
+ def resource_specification
625
+ self.class.resource_specification
626
+ end
627
+
628
+ # returns an array of the controller's enclosing (nested in) resources
629
+ def enclosing_resources
630
+ @enclosing_resources ||= []
631
+ end
632
+
633
+ # returns an array of the collection (non singleton) enclosing resources, this is used for generating routes.
634
+ def enclosing_collection_resources
635
+ @enclosing_collection_resources ||= []
636
+ end
637
+
638
+ # NOTE: This method is overly complicated and unecessary. It's much clearer just to keep
639
+ # track of record saves yourself, this is here for BC. For an example of how it should be
640
+ # done look at the actions module in http://github.com/ianwhite/response_for_rc
641
+ #
642
+ # Has the resource been saved successfully?, if no save has been attempted, save the
643
+ # record and return the result
644
+ #
645
+ # This method uses the @resource_saved tracking var, or the model's state itself if
646
+ # that is not available (which means if you do resource.update_attributes, then this
647
+ # method will return the correct result)
648
+ def resource_saved?
649
+ save_resource if @resource_saved.nil? && !resource.validation_attempted?
650
+ @resource_saved = resource.saved? if @resource_saved.nil?
651
+ @resource_saved
652
+ end
653
+
654
+ # NOTE: it's clearer to just keep track of record saves yourself, this is here for BC
655
+ # See the comment on #resource_saved?
656
+ #
657
+ # @resource_saved = resource.update_attributes(params[resource_name])
658
+ #
659
+ # Save the resource, and keep track of the result
660
+ def save_resource
661
+ @resource_saved = resource.save
662
+ end
663
+
664
+ private
665
+ # this is the before_filter that loads all specified and wildcard resources
666
+ def load_enclosing_resources
667
+ namespace_segments.each {|segment| update_name_prefix("#{segment}_") }
668
+ specifications.each_with_index do |spec, idx|
669
+ case spec
670
+ when '*' then load_wildcards_from(idx)
671
+ when /^\?(.*)/ then load_wildcard($1)
672
+ else load_enclosing_resource_from_specification(spec)
673
+ end
674
+ end
675
+ end
676
+
677
+ # load a wildcard resource by either
678
+ # * matching the segment to mapped resource specification, or
679
+ # * creating one using the segment name
680
+ # Optionally takes a variable name to set the instance variable as (for polymorphic use)
681
+ def load_wildcard(as = nil)
682
+ seg = nesting_segments[enclosing_resources.size] or ResourcesController.raise_resource_mismatch(self)
683
+
684
+ segment = seg[:segment]
685
+ singleton = seg[:singleton]
686
+
687
+ if resource_specification_map[segment]
688
+ spec = resource_specification_map[segment]
689
+ spec = spec.dup.tap {|s| s.as = as} if as
690
+ else
691
+ spec = Specification.new(singleton ? segment : segment.singularize, :singleton => singleton, :as => as)
692
+ end
693
+ load_enclosing_resource_from_specification(spec)
694
+ end
695
+
696
+ # loads a series of wildcard resources, from the specified specification idx
697
+ #
698
+ # To do this, we need to figure out where the next specified resource is
699
+ # and how many single wildcards are prior to that. What is left over from
700
+ # the current route enclosing names will be the number of wildcards we need to load
701
+ def load_wildcards_from(start)
702
+ specs = specifications.slice(start..-1)
703
+ encls = nesting_segments.slice(enclosing_resources.size..-1)
704
+
705
+ if spec = specs.find {|s| s.is_a?(Specification)}
706
+ spec_seg = encls.index({:segment => spec.segment, :singleton => spec.singleton?}) or ResourcesController.raise_resource_mismatch(self)
707
+ number_of_wildcards = spec_seg - (specs.index(spec) -1)
708
+ else
709
+ number_of_wildcards = encls.length - (specs.length - 1)
710
+ end
711
+
712
+ number_of_wildcards.times { load_wildcard }
713
+ end
714
+
715
+ def load_enclosing_resource_from_specification(spec)
716
+ spec.segment == nesting_segments[enclosing_resources.size][:segment] or ResourcesController.raise_resource_mismatch(self)
717
+ spec.find_from(self).tap do |resource|
718
+ add_enclosing_resource(resource, :name => spec.name, :name_prefix => spec.name_prefix, :is_singleton => spec.singleton?, :as => spec.as)
719
+ end
720
+ end
721
+
722
+ def add_enclosing_resource(resource, options = {})
723
+ name = options[:name] || resource.class.name.underscore
724
+ update_name_prefix(options[:name_prefix] || (options[:name_prefix] == false ? '' : "#{name}_"))
725
+ enclosing_resources << resource
726
+ enclosing_collection_resources << resource unless options[:is_singleton]
727
+ instance_variable_set("@enclosing_resource_name", options[:name])
728
+ instance_variable_set("@#{name}", resource)
729
+ instance_variable_set("@#{options[:as]}", resource) if options[:as]
730
+ end
731
+
732
+ # The name prefix is used for forwarding urls and will be different depending on
733
+ # which route the controller was invoked by. The resource specifications build
734
+ # up the name prefix as the resources are loaded.
735
+ def update_name_prefix(name_prefix)
736
+ @name_prefix = "#{@name_prefix}#{name_prefix}"
737
+ end
738
+ end
739
+
740
+ # Proxy class to provide a consistent API for resource_service. This is mostly
741
+ # required for Singleton resources. Also allows decoration of the resource service with custom finders
742
+ class ResourceService < ActiveSupport::BasicObject
743
+ attr_reader :controller
744
+ delegate :resource_specification, :resource_class, :enclosing_resource, :to => :controller
745
+
746
+ def initialize(controller)
747
+ @controller = controller
748
+ end
749
+
750
+ def method_missing(*args, &block)
751
+ service.send(*args, &block)
752
+ end
753
+
754
+ def find(*args, &block)
755
+ resource_specification.find ? resource_specification.find_custom(controller) : super
756
+ end
757
+
758
+ # build association on the enclosing resource if there is one, otherwise call new
759
+ def new(*args, &block)
760
+ enclosing_resource ? service.build(*args, &block) : service.new(*args, &block)
761
+ end
762
+
763
+ # find the resource
764
+ # If we have a resource service, we call destroy on it with the reosurce id, so that any callbacks can be triggered
765
+ # Otherwise, just call destroy on the resource
766
+ def destroy(*args)
767
+ resource = find(*args)
768
+ if enclosing_resource
769
+ service.destroy(*args)
770
+ resource
771
+ else
772
+ resource.destroy
773
+ end
774
+ end
775
+
776
+ def respond_to?(method, include_private = false)
777
+ super || service.respond_to?(method)
778
+ end
779
+
780
+ def service
781
+ @service ||= enclosing_resource ? enclosing_resource.send(resource_specification.source) : resource_class
782
+ end
783
+ end
784
+
785
+ class SingletonResourceService < ResourceService
786
+ def find(*args)
787
+ if resource_specification.find
788
+ resource_specification.find_custom(controller)
789
+ elsif controller.enclosing_resources.size > 0
790
+ enclosing_resource.send(resource_specification.source)
791
+ else
792
+ ::ResourcesController.raise_cant_find_singleton(controller.resource_name, controller.resource_class)
793
+ end
794
+ end
795
+
796
+ # build association on the enclosing resource if there is one, otherwise call new
797
+ def new(*args, &block)
798
+ enclosing_resource ? enclosing_resource.send("build_#{resource_specification.source}", *args, &block) : service.new(*args, &block)
799
+ end
800
+
801
+ def destroy(*args)
802
+ find.destroy
803
+ end
804
+
805
+ def service
806
+ resource_class
807
+ end
808
+ end
809
+
810
+ class CantFindSingleton < RuntimeError #:nodoc:
811
+ end
812
+
813
+ class ResourceMismatch < RuntimeError #:nodoc:
814
+ end
815
+
816
+ class << self
817
+ def raise_cant_find_singleton(name, klass) #:nodoc:
818
+ raise CantFindSingleton, <<-end_str
819
+ Can't get singleton resource from class #{klass.name}. You have have probably done something like:
820
+
821
+ nested_in :#{name}, :singleton => true # <= where this is the first nested_in
822
+
823
+ You should tell resources_controller how to find the singleton resource like this:
824
+
825
+ nested_in :#{name}, :singleton => true do
826
+ #{klass.name}.find(<.. your find args here ..>)
827
+ end
828
+
829
+ Or:
830
+ nested_in :#{name}, :singleton => true, :find => <.. method name or lambda ..>
831
+
832
+ Or, you may be relying on the route to load the resource, in which case you need to give RC some
833
+ help. Do this by mapping the route segment to a resource in the controller, or a parent or mixin
834
+
835
+ map_enclosing_resource :#{name}, :segment => ..., :singleton => true <.. as above ..>
836
+ end_str
837
+ end
838
+
839
+ def raise_resource_mismatch(controller) #:nodoc:
840
+ raise ResourceMismatch, <<-end_str
841
+ resources_controller can't match the route to the resource specification
842
+ path: #{controller.send(:request_path)}
843
+ specification: enclosing: [#{controller.specifications.collect{|s| s.is_a?(Specification) ? ":#{s.segment}" : s}.join(', ')}], resource :#{controller.resource_specification.segment}
844
+
845
+ the successfully loaded enclosing resources are: #{controller.enclosing_resources.join(', ')}
846
+ end_str
847
+ end
848
+ end
849
+ end