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.
Files changed (50) hide show
  1. data/README +312 -1
  2. data/lib/resource_controller.rb +10 -5
  3. data/lib/resource_controller/class_methods.rb +3 -1
  4. data/lib/resource_controller/controller.rb +6 -0
  5. data/lib/resource_controller/helpers/nested.rb +21 -3
  6. data/lib/resource_controller/helpers/singleton_customizations.rb +60 -0
  7. data/lib/resource_controller/helpers/urls.rb +5 -1
  8. data/lib/resource_controller/singleton.rb +15 -0
  9. data/lib/resource_controller/version.rb +1 -1
  10. data/lib/tasks/gem.rake +1 -1
  11. data/test/app/controllers/accounts_controller.rb +6 -0
  12. data/test/app/controllers/cms/products_controller.rb +1 -1
  13. data/test/app/controllers/images_controller.rb +4 -0
  14. data/test/app/controllers/options_controller.rb +8 -0
  15. data/test/app/helpers/accounts_helper.rb +2 -0
  16. data/test/app/helpers/images_helper.rb +2 -0
  17. data/test/app/models/account.rb +1 -0
  18. data/test/app/models/image.rb +3 -0
  19. data/test/app/models/user.rb +3 -0
  20. data/test/app/views/accounts/_form.html.erb +4 -0
  21. data/test/app/views/accounts/edit.html.erb +14 -0
  22. data/test/app/views/accounts/new.html.erb +12 -0
  23. data/test/app/views/accounts/show.html.erb +5 -0
  24. data/test/app/views/images/_form.html.erb +4 -0
  25. data/test/app/views/images/edit.html.erb +14 -0
  26. data/test/app/views/images/new.html.erb +12 -0
  27. data/test/app/views/options/_form.html.erb +8 -0
  28. data/test/app/views/options/edit.html.erb +16 -0
  29. data/test/app/views/options/index.html.erb +21 -0
  30. data/test/app/views/options/new.html.erb +12 -0
  31. data/test/app/views/options/show.html.erb +10 -0
  32. data/test/config/database.yml +0 -3
  33. data/test/config/environment.rb +2 -2
  34. data/test/config/routes.rb +8 -1
  35. data/test/db/migrate/002_create_products.rb +1 -1
  36. data/test/db/migrate/004_create_options.rb +3 -2
  37. data/test/db/migrate/011_create_images.rb +12 -0
  38. data/test/db/migrate/012_create_users.rb +11 -0
  39. data/test/db/schema.rb +13 -6
  40. data/test/test/fixtures/images.yml +6 -0
  41. data/test/test/fixtures/users.yml +5 -0
  42. data/test/test/functional/images_controller_test.rb +37 -0
  43. data/test/test/unit/helpers/current_objects_test.rb +6 -0
  44. data/test/test/unit/helpers/nested_test.rb +5 -1
  45. data/test/test/unit/helpers/singleton_current_objects_test.rb +68 -0
  46. data/test/test/unit/helpers/singleton_nested_test.rb +77 -0
  47. data/test/test/unit/helpers/singleton_urls_test.rb +67 -0
  48. data/test/test/unit/helpers/urls_test.rb +5 -1
  49. data/test/test/unit/image_test.rb +7 -0
  50. metadata +35 -2
data/README CHANGED
@@ -1 +1,312 @@
1
- README.rdoc
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]
@@ -1,14 +1,19 @@
1
1
  module ResourceController
2
- ACTIONS = [:index, :show, :new_action, :create, :edit, :update, :destroy].freeze
3
- FAILABLE_ACTIONS = ACTIONS - [:index, :new_action, :edit].freeze
4
- NAME_ACCESSORS = [:model_name, :route_name, :object_name]
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
- end
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 += (ResourceController::ACTIONS - [:new_action] + [:new]) - opts unless opts.first == :all
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
 
@@ -57,6 +57,12 @@ module ResourceController
57
57
  flash "Successfully removed!"
58
58
  wants.html { redirect_to collection_url }
59
59
  end
60
+
61
+ class << self
62
+ def singleton?
63
+ false
64
+ end
65
+ end
60
66
  end
61
67
  end
62
68
  end
@@ -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 ||= [*belongs_to].find { |parent| !params["#{parent}_id".to_sym].nil? }
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