giraffesoft-resource_controller 0.4.9 → 0.4.10
Sign up to get free protection for your applications and to get access to all the features.
- data/README +312 -1
- data/lib/resource_controller.rb +10 -5
- data/lib/resource_controller/class_methods.rb +3 -1
- data/lib/resource_controller/controller.rb +6 -0
- data/lib/resource_controller/helpers/nested.rb +21 -3
- data/lib/resource_controller/helpers/singleton_customizations.rb +60 -0
- data/lib/resource_controller/helpers/urls.rb +5 -1
- data/lib/resource_controller/singleton.rb +15 -0
- data/lib/resource_controller/version.rb +1 -1
- data/lib/tasks/gem.rake +1 -1
- data/test/app/controllers/accounts_controller.rb +6 -0
- data/test/app/controllers/cms/products_controller.rb +1 -1
- data/test/app/controllers/images_controller.rb +4 -0
- data/test/app/controllers/options_controller.rb +8 -0
- data/test/app/helpers/accounts_helper.rb +2 -0
- data/test/app/helpers/images_helper.rb +2 -0
- data/test/app/models/account.rb +1 -0
- data/test/app/models/image.rb +3 -0
- data/test/app/models/user.rb +3 -0
- data/test/app/views/accounts/_form.html.erb +4 -0
- data/test/app/views/accounts/edit.html.erb +14 -0
- data/test/app/views/accounts/new.html.erb +12 -0
- data/test/app/views/accounts/show.html.erb +5 -0
- data/test/app/views/images/_form.html.erb +4 -0
- data/test/app/views/images/edit.html.erb +14 -0
- data/test/app/views/images/new.html.erb +12 -0
- data/test/app/views/options/_form.html.erb +8 -0
- data/test/app/views/options/edit.html.erb +16 -0
- data/test/app/views/options/index.html.erb +21 -0
- data/test/app/views/options/new.html.erb +12 -0
- data/test/app/views/options/show.html.erb +10 -0
- data/test/config/database.yml +0 -3
- data/test/config/environment.rb +2 -2
- data/test/config/routes.rb +8 -1
- data/test/db/migrate/002_create_products.rb +1 -1
- data/test/db/migrate/004_create_options.rb +3 -2
- data/test/db/migrate/011_create_images.rb +12 -0
- data/test/db/migrate/012_create_users.rb +11 -0
- data/test/db/schema.rb +13 -6
- data/test/test/fixtures/images.yml +6 -0
- data/test/test/fixtures/users.yml +5 -0
- data/test/test/functional/images_controller_test.rb +37 -0
- data/test/test/unit/helpers/current_objects_test.rb +6 -0
- data/test/test/unit/helpers/nested_test.rb +5 -1
- data/test/test/unit/helpers/singleton_current_objects_test.rb +68 -0
- data/test/test/unit/helpers/singleton_nested_test.rb +77 -0
- data/test/test/unit/helpers/singleton_urls_test.rb +67 -0
- data/test/test/unit/helpers/urls_test.rb +5 -1
- data/test/test/unit/image_test.rb +7 -0
- metadata +35 -2
data/README
CHANGED
@@ -1 +1,312 @@
|
|
1
|
-
|
1
|
+
= Resource Controller
|
2
|
+
|
3
|
+
resource_controller makes RESTful controllers easier, more maintainable, and super readable. With the RESTful controller pattern hidden away, you can focus on what makes your controller special.
|
4
|
+
|
5
|
+
== Get It
|
6
|
+
|
7
|
+
svn export http://svn.jamesgolick.com/resource_controller/tags/stable vendor/plugins/resource_controller
|
8
|
+
|
9
|
+
SVN (stable): {http://svn.jamesgolick.com/resource_controller/tags/stable}[http://svn.jamesgolick.com/resource_controller/tags/stable]
|
10
|
+
|
11
|
+
SVN (ongoing): {http://svn.jamesgolick.com/resource_controller/trunk}[http://svn.jamesgolick.com/resource_controller/trunk]
|
12
|
+
|
13
|
+
= Usage
|
14
|
+
|
15
|
+
Creating a basic RESTful controller is as easy as...
|
16
|
+
|
17
|
+
class PostsController < ResourceController::Base
|
18
|
+
end
|
19
|
+
|
20
|
+
...or if you prefer, you can use the method-call syntax. If you need to inherit from some other class, this syntax is definitely for you:
|
21
|
+
|
22
|
+
class PostsController < ApplicationController
|
23
|
+
resource_controller
|
24
|
+
end
|
25
|
+
|
26
|
+
Both syntaxes are identical in their behavior. Just make sure you call resource_controller before you use any other r_c functionality in your controller.
|
27
|
+
|
28
|
+
|
29
|
+
Nobody just uses the default RESTful controller, though. resource_controller provides a simple API for customizations.
|
30
|
+
|
31
|
+
== Action Lifecycle
|
32
|
+
|
33
|
+
It's really easy to make changes to the lifecycle of your actions.
|
34
|
+
|
35
|
+
Note: We had to call the new accessor "new_action", since new is somewhat reserved in ruby.
|
36
|
+
|
37
|
+
=== Before and After
|
38
|
+
|
39
|
+
class ProjectsController < ResourceController::Base
|
40
|
+
|
41
|
+
new_action.before do
|
42
|
+
3.times { object.tasks.build }
|
43
|
+
end
|
44
|
+
|
45
|
+
create.after do
|
46
|
+
object.creator = current_user
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
=== Flash
|
52
|
+
|
53
|
+
class ProjectsController < ResourceController::Base
|
54
|
+
create.flash "Can you believe how easy it is to use resource_controller? Neither could I!"
|
55
|
+
end
|
56
|
+
|
57
|
+
=== respond_to
|
58
|
+
|
59
|
+
You can add to what's already there...
|
60
|
+
|
61
|
+
class ProjectsController < ResourceController::Base
|
62
|
+
create.wants.js { render :template => "show.rjs" }
|
63
|
+
end
|
64
|
+
|
65
|
+
Or you can create a whole new block. This syntax destroys everything that's there, and starts again...
|
66
|
+
|
67
|
+
class ProjectsController < ResourceController::Base
|
68
|
+
create.response do |wants|
|
69
|
+
wants.html
|
70
|
+
wants.js { render :template => "show.rjs" }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
=== Scoping
|
75
|
+
|
76
|
+
Because sometimes you want to make a bunch of customizations at once, most of the helpers accept blocks that make grouping calls really easy. Is it a DSL? Maybe; maybe not. But, it's definitely awesome.
|
77
|
+
|
78
|
+
With actions that can fail, the scoping defaults to success. That means that create.flash == create.success.flash.
|
79
|
+
|
80
|
+
class ProjectsController < ResourceController::Base
|
81
|
+
|
82
|
+
create do
|
83
|
+
flash "Object successfully created!"
|
84
|
+
wants.js { render :template => "show.rjs" }
|
85
|
+
|
86
|
+
failure.wants.js { render :template => "display_errors.rjs" }
|
87
|
+
end
|
88
|
+
|
89
|
+
destroy do
|
90
|
+
flash "You destroyed your project. Good work."
|
91
|
+
|
92
|
+
failure do
|
93
|
+
flash "You cannot destroy that project. Stop trying!"
|
94
|
+
wants.js { render :template => "display_errors.rjs" }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
== Helpers (ResourceController::Helpers)
|
101
|
+
|
102
|
+
=== Loading objects
|
103
|
+
|
104
|
+
You want to add something like pagination to your controller...
|
105
|
+
|
106
|
+
class PostsController < ResourceController::Base
|
107
|
+
private
|
108
|
+
def collection
|
109
|
+
@collection ||= end_of_association_chain.find(:all, :page => {:size => 10, :current => params[:page]})
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
Or maybe you used a permalink...
|
114
|
+
|
115
|
+
class PostsController < ResourceController::Base
|
116
|
+
private
|
117
|
+
def object
|
118
|
+
@object ||= end_of_association_chain.find_by_permalink(param)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
=== Building objects
|
123
|
+
|
124
|
+
Maybe you have some alternative way of building objects...
|
125
|
+
|
126
|
+
class PostsController < ResourceController::Base
|
127
|
+
private
|
128
|
+
def build_object
|
129
|
+
@object ||= end_of_association_chain.build_my_object_some_funky_way object_params
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
...and there are tons more helpers in the ResourceController::Helpers
|
134
|
+
|
135
|
+
== Nested Resources
|
136
|
+
|
137
|
+
Nested controllers can be a pain, especially if routing is such that you may or may not have a parent. Not so with Resource Controller.
|
138
|
+
|
139
|
+
class CommentsController < ResourceController::Base
|
140
|
+
belongs_to :post
|
141
|
+
end
|
142
|
+
|
143
|
+
All of the finding, and creation, and everything will be done at the scope of the post automatically.
|
144
|
+
|
145
|
+
== Namespaced Resources
|
146
|
+
|
147
|
+
...are handled automatically, and any namespaces are always available, symbolized, in array form @ ResourceController::Helpers#namespaces
|
148
|
+
|
149
|
+
== Polymorphic Resources
|
150
|
+
|
151
|
+
Everything, including url generation is handled completely automatically. Take this example...
|
152
|
+
|
153
|
+
## comment.rb
|
154
|
+
class Comment
|
155
|
+
belongs_to :commentable, :polymorphic => true
|
156
|
+
end
|
157
|
+
|
158
|
+
## comments_controller.rb
|
159
|
+
class CommentsController < ResourceController::Base
|
160
|
+
belongs_to :post, :product, :user
|
161
|
+
end
|
162
|
+
*Note:* Your model doesn't have to be polymorphic in the ActiveRecord sense. It can be associated in whichever way you want.
|
163
|
+
|
164
|
+
## routes.rb
|
165
|
+
map.resources :posts, :has_many => :comments
|
166
|
+
map.resources :products, :has_many => :comments
|
167
|
+
map.resources :users, :has_many => :comments
|
168
|
+
|
169
|
+
All you have to do is that, and r_c will infer whichever relationship is present, and perform all the actions at the scope of the parent object.
|
170
|
+
|
171
|
+
=== Parent Helpers
|
172
|
+
|
173
|
+
You also get some helpers for reflecting on your parent.
|
174
|
+
|
175
|
+
parent? # => true/false is there a parent present?
|
176
|
+
parent_type # => :post
|
177
|
+
parent_model # => Post
|
178
|
+
parent_object # => @post
|
179
|
+
|
180
|
+
=== Non-standard resource names
|
181
|
+
|
182
|
+
resource_controller supports overrides for every non-standard configuration of resources.
|
183
|
+
|
184
|
+
The most common example is where the resource has a different name than the associated model. Simply overriding the model_name helper will get resource_controller working with your model.
|
185
|
+
|
186
|
+
map.resources :tags
|
187
|
+
...
|
188
|
+
class PhotoTag < ActiveRecord::Base
|
189
|
+
...
|
190
|
+
class TagsController < ResourceController::Base
|
191
|
+
private
|
192
|
+
def model_name
|
193
|
+
'photo_tag'
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
In the above example, the variable, and params will be set to @tag, @tags, and params[:tag]. If you'd like to change that, override object_name.
|
198
|
+
|
199
|
+
def object_name
|
200
|
+
'photo_tag'
|
201
|
+
end
|
202
|
+
|
203
|
+
If you're using a non-standard controller name, but everything else is standard, overriding resource_name will propagate through all of the other helpers.
|
204
|
+
|
205
|
+
map.resources :tags, :controller => "somethings"
|
206
|
+
...
|
207
|
+
class Tag < ActiveRecord::Base
|
208
|
+
...
|
209
|
+
class SomethingsController < ResourceController::Base
|
210
|
+
private
|
211
|
+
def resource_name
|
212
|
+
'tag'
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
Finally, the route_name helper is used by Urligence to determine which url helper to call, so if you have non-standard route names, override it.
|
217
|
+
|
218
|
+
map.resources :tags, :controller => "taggings"
|
219
|
+
...
|
220
|
+
class Taggings < ActiveRecord::Base
|
221
|
+
...
|
222
|
+
class TaggingsController < ResourceController::Base
|
223
|
+
private
|
224
|
+
def route_name
|
225
|
+
'tag'
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
== Singleton Resource
|
230
|
+
|
231
|
+
If you want to create a singleton RESTful controller inherit from ResourceController::Singleton.
|
232
|
+
|
233
|
+
class AccountsController < ResourceController::Singleton
|
234
|
+
end
|
235
|
+
|
236
|
+
*Note:* This type of controllers handle a single resource only so the index action and all the collection helpers (collection_url, collection_path...) are not available for them.
|
237
|
+
|
238
|
+
Loading objects in singletons is similar to plural controllers with one exception. For non-nested singleton controllers you should override the object method as it defaults to nil for them.
|
239
|
+
|
240
|
+
class AccountsController < ResourceController::Singleton
|
241
|
+
private
|
242
|
+
def object
|
243
|
+
@object ||= Account.find(session[:account_id])
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
In other cases you can use the default logic and override it only if you use permalinks or anything special.
|
248
|
+
|
249
|
+
Singleton nesting with both :has_many and :has_one associations is provided...
|
250
|
+
|
251
|
+
map.resource :account, :has_many => :options # /account/options, account is a singleton parent
|
252
|
+
map.resources :users, :has_one => :image # /users/1/image, image is a singleton child
|
253
|
+
|
254
|
+
If you have the :has_many association with a singleton parent remember to override parent_object for your :has_many controller as it returns nil by default in this case.
|
255
|
+
|
256
|
+
class OptionsController < ResourceController::Base
|
257
|
+
belongs_to :account
|
258
|
+
|
259
|
+
protected
|
260
|
+
def parent_object
|
261
|
+
Account.find(session[:account_id])
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
== Url Helpers
|
266
|
+
|
267
|
+
Thanks to Urligence, you also get some free url helpers.
|
268
|
+
|
269
|
+
No matter what your controller looks like...
|
270
|
+
|
271
|
+
[edit_|new_]object_url # is the equivalent of saying [edit_|new_]post_url(@post)
|
272
|
+
[edit_|new_]object_url(some_other_object) # allows you to specify an object, but still maintain any paths or namespaces that are present
|
273
|
+
|
274
|
+
collection_url # is like saying posts_url
|
275
|
+
|
276
|
+
Url helpers are especially useful when working with polymorphic controllers.
|
277
|
+
|
278
|
+
# /posts/1/comments
|
279
|
+
object_url # => /posts/1/comments/#{@comment.to_param}
|
280
|
+
object_url(comment) # => /posts/1/comments/#{comment.to_param}
|
281
|
+
edit_object_url # => /posts/1/comments/#{@comment.to_param}/edit
|
282
|
+
collection_url # => /posts/1/comments
|
283
|
+
|
284
|
+
# /products/1/comments
|
285
|
+
object_url # => /products/1/comments/#{@comment.to_param}
|
286
|
+
object_url(comment) # => /products/1/comments/#{comment.to_param}
|
287
|
+
edit_object_url # => /products/1/comments/#{@comment.to_param}/edit
|
288
|
+
collection_url # => /products/1/comments
|
289
|
+
|
290
|
+
# /comments
|
291
|
+
object_url # => /comments/#{@comment.to_param}
|
292
|
+
object_url(comment) # => /comments/#{comment.to_param}
|
293
|
+
edit_object_url # => /comments/#{@comment.to_param}/edit
|
294
|
+
collection_url # => /comments
|
295
|
+
|
296
|
+
Or with namespaced, nested controllers...
|
297
|
+
|
298
|
+
# /admin/products/1/options
|
299
|
+
object_url # => /admin/products/1/options/#{@option.to_param}
|
300
|
+
object_url(option) # => /admin/products/1/options/#{option.to_param}
|
301
|
+
edit_object_url # => /admin/products/1/options/#{@option.to_param}/edit
|
302
|
+
collection_url # => /admin/products/1/options
|
303
|
+
|
304
|
+
You get the idea. Everything is automagical! All parameters are inferred.
|
305
|
+
|
306
|
+
== Credits
|
307
|
+
|
308
|
+
resource_controller was created, and is maintained by {James Golick}[http://jamesgolick.com].
|
309
|
+
|
310
|
+
== License
|
311
|
+
|
312
|
+
resource_controller is available under the {MIT License}[http://en.wikipedia.org/wiki/MIT_License]
|
data/lib/resource_controller.rb
CHANGED
@@ -1,14 +1,19 @@
|
|
1
1
|
module ResourceController
|
2
|
-
ACTIONS
|
3
|
-
|
4
|
-
|
2
|
+
ACTIONS = [:index, :show, :new_action, :create, :edit, :update, :destroy].freeze
|
3
|
+
SINGLETON_ACTIONS = (ACTIONS - [:index]).freeze
|
4
|
+
FAILABLE_ACTIONS = ACTIONS - [:index, :new_action, :edit].freeze
|
5
|
+
NAME_ACCESSORS = [:model_name, :route_name, :object_name]
|
5
6
|
|
6
7
|
module ActionControllerExtension
|
7
8
|
unloadable
|
8
9
|
|
9
|
-
def resource_controller
|
10
|
+
def resource_controller(*args)
|
10
11
|
include ResourceController::Controller
|
11
|
-
|
12
|
+
|
13
|
+
if args.include?(:singleton)
|
14
|
+
include ResourceController::Helpers::SingletonCustomizations
|
15
|
+
end
|
16
|
+
end
|
12
17
|
end
|
13
18
|
end
|
14
19
|
|
@@ -10,8 +10,10 @@ module ResourceController
|
|
10
10
|
config = {}
|
11
11
|
config.merge!(opts.pop) if opts.last.is_a?(Hash)
|
12
12
|
|
13
|
+
all_actions = (singleton? ? ResourceController::SINGLETON_ACTIONS : ResourceController::ACTIONS) - [:new_action] + [:new]
|
14
|
+
|
13
15
|
actions_to_remove = []
|
14
|
-
actions_to_remove +=
|
16
|
+
actions_to_remove += all_actions - opts unless opts.first == :all
|
15
17
|
actions_to_remove += [*config[:except]] if config[:except]
|
16
18
|
actions_to_remove.uniq!
|
17
19
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# Nested and Polymorphic Resource Helpers
|
2
2
|
#
|
3
3
|
module ResourceController::Helpers::Nested
|
4
|
-
protected
|
4
|
+
protected
|
5
5
|
# Returns the relevant association proxy of the parent. (i.e. /posts/1/comments # => @post.comments)
|
6
6
|
#
|
7
7
|
def parent_association
|
@@ -11,7 +11,19 @@ module ResourceController::Helpers::Nested
|
|
11
11
|
# Returns the type of the current parent
|
12
12
|
#
|
13
13
|
def parent_type
|
14
|
-
@parent_type ||=
|
14
|
+
@parent_type ||= parent_type_from_params || parent_type_from_request
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns the type of the current parent extracted from params
|
18
|
+
#
|
19
|
+
def parent_type_from_params
|
20
|
+
[*belongs_to].find { |parent| !params["#{parent}_id".to_sym].nil? }
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns the type of the current parent extracted form a request path
|
24
|
+
#
|
25
|
+
def parent_type_from_request
|
26
|
+
[*belongs_to].find { |parent| request.path.split('/').include? parent.to_s }
|
15
27
|
end
|
16
28
|
|
17
29
|
# Returns true/false based on whether or not a parent is present.
|
@@ -20,6 +32,12 @@ module ResourceController::Helpers::Nested
|
|
20
32
|
!parent_type.nil?
|
21
33
|
end
|
22
34
|
|
35
|
+
# Returns true/false based on whether or not a parent is a singleton.
|
36
|
+
#
|
37
|
+
def parent_singleton?
|
38
|
+
!parent_type_from_request.nil?
|
39
|
+
end
|
40
|
+
|
23
41
|
# Returns the current parent param, if there is a parent. (i.e. params[:post_id])
|
24
42
|
def parent_param
|
25
43
|
params["#{parent_type}_id".to_sym]
|
@@ -34,7 +52,7 @@ module ResourceController::Helpers::Nested
|
|
34
52
|
# Returns the current parent object if a parent object is present.
|
35
53
|
#
|
36
54
|
def parent_object
|
37
|
-
parent? ? parent_model.find(parent_param) : nil
|
55
|
+
parent? && !parent_singleton? ? parent_model.find(parent_param) : nil
|
38
56
|
end
|
39
57
|
|
40
58
|
# If there is a parent, returns the relevant association proxy. Otherwise returns model.
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# Singleton Resource Helpers
|
2
|
+
#
|
3
|
+
# Used internally to transform a plural RESTful controller into a singleton
|
4
|
+
#
|
5
|
+
module ResourceController::Helpers::SingletonCustomizations
|
6
|
+
def self.included(subclass)
|
7
|
+
subclass.class_eval do
|
8
|
+
methods_to_undefine = [:param, :index, :collection, :load_collection, :collection_url,
|
9
|
+
:collection_path, :hash_for_collection_url, :hash_for_collection_path]
|
10
|
+
methods_to_undefine.each { |method| undef_method(method) if method_defined? method }
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def singleton?
|
14
|
+
true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
# Used to fetch the current object in a singleton controller.
|
22
|
+
#
|
23
|
+
# By defult this method is able to fetch the current object for resources nested with the :has_one association only. (i.e. /users/1/image # => @user.image)
|
24
|
+
# In other cases you should override this method and provide your custom code to fetch a singleton resource object, like using a session hash.
|
25
|
+
#
|
26
|
+
# class AccountsController < ResourceController::Singleton
|
27
|
+
# private
|
28
|
+
# def object
|
29
|
+
# @object ||= Account.find(session[:account_id])
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
def object
|
34
|
+
@object ||= parent? ? end_of_association_chain : nil
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns the :has_one association proxy of the parent. (i.e. /users/1/image # => @user.image)
|
38
|
+
#
|
39
|
+
def parent_association
|
40
|
+
@parent_association ||= parent_object.send(model_name.to_sym)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Used internally to provide the options to smart_url in a singleton controller.
|
44
|
+
#
|
45
|
+
def object_url_options(action_prefix = nil, alternate_object = nil)
|
46
|
+
[action_prefix] + namespaces + [parent_url_options, route_name.to_sym]
|
47
|
+
end
|
48
|
+
|
49
|
+
# Builds the object, but doesn't save it, during the new, and create action.
|
50
|
+
#
|
51
|
+
def build_object
|
52
|
+
@object ||= singleton_build_object_base.send parent? ? "build_#{model_name}".to_sym : :new, object_params
|
53
|
+
end
|
54
|
+
|
55
|
+
# Singleton controllers don't build off of association proxy, so we can't use end_of_association_chain here
|
56
|
+
#
|
57
|
+
def singleton_build_object_base
|
58
|
+
parent? ? parent_object : model
|
59
|
+
end
|
60
|
+
end
|