rc_rails 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/CHANGELOG +355 -0
- data/Gemfile +5 -0
- data/Gemfile.lock.development +117 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +71 -0
- data/Rakefile +33 -0
- data/Todo.txt +1 -0
- data/lib/rc_rails.rb +9 -0
- data/lib/resources_controller/actions.rb +147 -0
- data/lib/resources_controller/active_record/saved.rb +15 -0
- data/lib/resources_controller/helper.rb +123 -0
- data/lib/resources_controller/include_actions.rb +37 -0
- data/lib/resources_controller/named_route_helper.rb +154 -0
- data/lib/resources_controller/railtie.rb +14 -0
- data/lib/resources_controller/request_path_introspection.rb +83 -0
- data/lib/resources_controller/resource_methods.rb +32 -0
- data/lib/resources_controller/singleton_actions.rb +21 -0
- data/lib/resources_controller/specification.rb +119 -0
- data/lib/resources_controller/version.rb +3 -0
- data/lib/resources_controller.rb +849 -0
- data/resources_controller.gemspec +29 -0
- data/spec/app/database.yml +5 -0
- data/spec/app/views/accounts/show.html.erb +0 -0
- data/spec/app/views/addresses/edit.html.erb +0 -0
- data/spec/app/views/addresses/index.html.erb +0 -0
- data/spec/app/views/addresses/new.html.erb +0 -0
- data/spec/app/views/addresses/show.html.erb +0 -0
- data/spec/app/views/admin/forums/create.html.erb +0 -0
- data/spec/app/views/admin/forums/destroy.html.erb +0 -0
- data/spec/app/views/admin/forums/edit.html.erb +0 -0
- data/spec/app/views/admin/forums/index.html.erb +0 -0
- data/spec/app/views/admin/forums/new.html.erb +0 -0
- data/spec/app/views/admin/forums/show.html.erb +0 -0
- data/spec/app/views/admin/forums/update.html.erb +0 -0
- data/spec/app/views/comments/edit.html.erb +0 -0
- data/spec/app/views/comments/index.html.erb +0 -0
- data/spec/app/views/comments/new.html.erb +0 -0
- data/spec/app/views/comments/show.html.erb +0 -0
- data/spec/app/views/forum_posts/edit.html.erb +0 -0
- data/spec/app/views/forum_posts/index.html.erb +0 -0
- data/spec/app/views/forum_posts/new.html.erb +0 -0
- data/spec/app/views/forum_posts/show.html.erb +0 -0
- data/spec/app/views/forums/create.html.erb +0 -0
- data/spec/app/views/forums/destroy.html.erb +0 -0
- data/spec/app/views/forums/edit.html.erb +0 -0
- data/spec/app/views/forums/index.html.erb +0 -0
- data/spec/app/views/forums/new.html.erb +0 -0
- data/spec/app/views/forums/show.html.erb +0 -0
- data/spec/app/views/forums/update.html.erb +0 -0
- data/spec/app/views/infos/edit.html.erb +0 -0
- data/spec/app/views/infos/show.html.erb +0 -0
- data/spec/app/views/interests/index.html.erb +0 -0
- data/spec/app/views/interests/show.html.erb +0 -0
- data/spec/app/views/owners/edit.html.erb +0 -0
- data/spec/app/views/owners/new.html.erb +0 -0
- data/spec/app/views/owners/show.html.erb +0 -0
- data/spec/app/views/tags/index.html.erb +0 -0
- data/spec/app/views/tags/new.html.erb +0 -0
- data/spec/app/views/tags/show.html.erb +0 -0
- data/spec/app/views/users/edit.html.erb +0 -0
- data/spec/app/views/users/index.html.erb +0 -0
- data/spec/app/views/users/show.html.erb +0 -0
- data/spec/app.rb +315 -0
- data/spec/controllers/accounts_controller_spec.rb +77 -0
- data/spec/controllers/addresses_controller_spec.rb +346 -0
- data/spec/controllers/admin_forums_controller_spec.rb +638 -0
- data/spec/controllers/comments_controller_spec.rb +380 -0
- data/spec/controllers/comments_controller_with_models_spec.rb +202 -0
- data/spec/controllers/forum_posts_controller_spec.rb +426 -0
- data/spec/controllers/forums_controller_spec.rb +694 -0
- data/spec/controllers/infos_controller_spec.rb +71 -0
- data/spec/controllers/interests_controller_via_forum_spec.rb +80 -0
- data/spec/controllers/interests_controller_via_user_spec.rb +114 -0
- data/spec/controllers/owners_controller_spec.rb +277 -0
- data/spec/controllers/resource_saved_spec.rb +47 -0
- data/spec/controllers/resource_service_in_forums_controller_spec.rb +37 -0
- data/spec/controllers/resource_service_in_infos_controller_spec.rb +36 -0
- data/spec/controllers/resource_service_in_interests_controller_via_forum_spec.rb +51 -0
- data/spec/controllers/tags_controller_spec.rb +83 -0
- data/spec/controllers/tags_controller_via_account_info_spec.rb +131 -0
- data/spec/controllers/tags_controller_via_forum_post_comment_spec.rb +144 -0
- data/spec/controllers/tags_controller_via_forum_post_spec.rb +133 -0
- data/spec/controllers/tags_controller_via_forum_spec.rb +173 -0
- data/spec/controllers/tags_controller_via_user_address_spec.rb +130 -0
- data/spec/controllers/users_controller_spec.rb +248 -0
- data/spec/lib/action_view_helper_spec.rb +143 -0
- data/spec/lib/bug_0001_spec.rb +22 -0
- data/spec/lib/include_actions_spec.rb +35 -0
- data/spec/lib/load_enclosing_resources_spec.rb +245 -0
- data/spec/lib/request_path_introspection_spec.rb +130 -0
- data/spec/lib/resource_methods_spec.rb +204 -0
- data/spec/lib/resources_controller_spec.rb +57 -0
- data/spec/models/comment_saved_spec.rb +24 -0
- data/spec/rspec_generator_task.rb +105 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/verify_rcov.rb +52 -0
- 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
|