forked from activeadmin/inherited_resources
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathREADME
517 lines (376 loc) · 16.5 KB
/
README
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
Inherited Resources
License: MIT
Version: 0.8.2
You can also read this README in pretty html at the GitHub project Wiki page:
http://wiki.github.com/josevalim/inherited_resources
Description
-----------
Inherited Resources speeds up development by making your controllers inherit
all restful actions so you just have to focus on what is important. It makes
your controllers more powerful and cleaner at the same time.
Plus, making your controllers follow a pattern, it helps you to write better
code by following fat models and skinny controllers convention.
Inherited Resources is tested and compatible with Rails 2.2.x and Rails 2.3.x.
keywords: resources, controller, singleton, belongs_to, polymorphic, named_scope and I18n
Installation
------------
Install Inherited Resources is very easy. It is stored in GitHub, so just run
the following:
gem sources -a http://gems.github.com
sudo gem install josevalim-inherited_resources
If you want it as plugin, just do:
script/plugin install git://github.com/josevalim/inherited_resources.git
rspec-rails <= 1.1.12 known bug
-------------------------------
InheritedResources has a known bug with rspec-rails. Please upgrade your rspec
version or use the fix which ships with InheritedResources:
require 'inherited_resources/spec'
Basic Usage
-----------
To use Inherited Resources you just have to inherit (duh) it:
class ProjectsController < InheritedResources::Base
end
And all actions are defined and working, check it! Your projects collection
(in the index action) is still available in the instance variable @projects
and your project resource (all other actions) is available as @ project.
The next step is to define which mime types this controller provides:
class ProjectsController < InheritedResources::Base
respond_to :html, :xml, :json
end
You can also specify them based per action:
class ProjectsController < InheritedResources::Base
respond_to :html, :xml, :json
respond_to :js, :only => :create
respond_to :iphone, :except => [ :edit, :update ]
end
For each request, it first checkes if the "controller/action.format" file is
available (for example "projects/create.xml") and if it's not, it checks if
the resource respond to :to_format (in this case, :to_xml). Otherwise returns 404.
Another option is to specify which actions the controller will inherit from
the InheritedResources::Base:
class ProjectsController < InheritedResources::Base
actions :index, :show, :new, :create
end
Or:
class ProjectsController < InheritedResources::Base
actions :all, :except => [ :edit, :update, :destroy ]
end
In your views, you will get the following helpers:
resource #=> @project
collection #=> @projects
resource_class #=> Project
As you might expect, collection (@projects instance variable) is only available
on index actions.
If for some reason you cannot inherit from InheritedResources::Base, you can
call inherit_resources or resource_controller in your controller class scope:
class AccountsController < ApplicationController
inherit_resources # or resource_controller
end
Overwriting defaults
--------------------
Whenever you inherit from InheritedResources, several defaults are assumed.
For example you can have an AccountsController to account management while the
resource is an User:
class AccountsController < InheritedResources::Base
defaults :resource_class => User, :collection_name, 'users', :instance_name => 'user'
end
In the case above, in your views you will have @users and @user variables, but
the routes used will still be accounts_url and account_url. If you plan also to
change the routes, you can use :route_collection_name and :route_instance_name.
Namespaced controllers work out of the box, but if you need to specify a
different route prefix, you can do the following:
class Administrators::PeopleController < InheritedResources::Base
defaults :route_prefix => 'admin'
end
Then your named routes will be: 'admin_people_url', 'admin_person_url' instead
of 'administrators_people_url' and 'administrators_person_url'.
If you want to customize how resources are retrieved you can overwrite
collection and resource methods. The first is called on index action and the
second on all other actions. Let's suppose you want to add pagination to your
projects collection:
class ProjectsController < InheritedResources::Base
protected
def collection
@projects ||= end_of_association_chain.paginate(params[:page]).all
end
end
The end_of_association_chain returns your resource after nesting all associations
and scopes (more about this below).
InheritedResources also introduces another method called begin_of_association_chain.
It's mostly used when you want to create resources based on the @current_user and
you have urls like "account/projects". In such cases, you have to do
@current_user.projects.find or @current_user.projects.build in your actions.
You can deal with it just doing:
class ProjectsController < InheritedResources::Base
protected
def begin_of_association_chain
@current_user
end
end
Overwriting actions
-------------------
Let's suppose that after destroying a project you want to redirect to your
root url instead of redirecting to projects url. You just have to do:
class ProjectsController < InheritedResources::Base
def destroy
super do |format|
format.html { redirect_to root_url }
end
end
end
You are opening your action and giving the parent action a new behavior. No
tricks, no DSL, just Ruby.
On the other hand, I have to agree that calling super is not very readable.
That's why all methods have aliases. So this is equivalent:
class ProjectsController < InheritedResources::Base
def destroy
destroy! do |format|
format.html { redirect_to root_url }
end
end
end
Even more, since most of the times when you change a create, update or destroy
action is because you want to to change to where it redirects, a shortcut is
provided. So you can do:
class ProjectsController < InheritedResources::Base
def destroy
destroy!{ root_url }
end
end
Now let's suppose that before create a project you have to do something special
but you don't want to create a before filter for it:
class ProjectsController < InheritedResources::Base
def create
@project = Project.new(params[:project])
@project.something_special!
create!
end
end
Yes, that simple! The nice part is since you already set the instance variable
@project, it will not build a project again.
Before we finish this topic, we should talk about one more thing: "success/failure
blocks". Let's suppose that when we update our project, in case of failure, we
want to redirect to the project url instead of re-rendering the edit template.
Our first attempt to do this would be:
class ProjectsController < InheritedResources::Base
def update
update! do |format|
unless @project.errors.empty? # failure
format.html { redirect_to project_url(@project) }
end
end
end
end
Looks to verbose, right? We can actually do:
class ProjectsController < InheritedResources::Base
def update
update! do |success, failure|
failure.html { redirect_to project_url(@project) }
end
end
end
Much better! So explaining everything: when you give a block which expects one
argument it will be executed in both scenarios: success and failure. But If you
give a block that expects two arguments, the first will be executed only in
success scenarios and the second in failure scenarios. You keep everything
clean and organized inside the same action.
Flash messages and I18n
-----------------------
Flash messages are powered by I18n api. It checks for messages in the following
order:
flash.controller_name.action_name.status
flash.actions.action_name.status
If none is available, a default message in english set. In a create action
on projects controller, it will search for:
flash.projects.create.status
flash.actions.create.status
The status can be :notice (when the object can be created, updated
or destroyed with success) or :error (when the objecy cannot be created
or updated).
Those messages are interpolated by using the resource class human name, which
is also localized and it means you can set:
flash:
actions:
create:
notice: "Hooray! {{resource_name}} was successfully created!"
It will replace {{resource_name}} by the human name of the resource class,
which is "Project" in this case.
But sometimes, flash messages are not that simple. Sometimes you want to say
the title of the project while updating a project. Well, that's easy also:
flash:
projects:
update:
notice: "Hooray! The project "{{project_title}}" was updated!"
Since :project_title is not available for interpolation by default, you have
to overwrite interpolation_options.
def interpolation_options
{ :project_title => @project.title }
end
Then you will finally have:
"Hooray! The project "Plataforma" was updated!"
By default, resource name is capitalized. If you want to make it lower case, you
can add to your application controller:
def interpolation_options
{ :resource_name => resource_class.human_name.downcase }
end
Finally, if your controller is namespaced, for example Admin::ProjectsController,
the messages will be checked in the following order:
flash.admin.projects.create.notice
flash.admin.actions.create.notice
flash.projects.create.notice
flash.actions.create.notice
Has Scope
---------
InheritedResources tries to integrate nicely with your model. In order to do so,
it also is named_scope fluent. Let's suppose our Project model with the scopes:
class ProjectsController < ActiveRecord::Base
named_scope :featured, :conditions => { :featured => true }
named_scope :by_methodology, proc {|methodology| { :conditions => { :methodology => methodology } } }
named_scope :limit, proc{|limit| :limit => limit.to_i }
end
Your controller:
class ProjectsController < InheritedResources::Base
has_scope :featured, :boolean => true, :only => :index
has_scope :by_methodology
has_scope :limit, :default => 10
end
Then for each request:
/projects
#=> acts like a normal request, but returning 10 projects
/projects?featured=true
#=> calls the featured named scope and bring 10 featured projects
/projects?featured=true&by_methodology=agile&limit=20
#=> brings 20 featured projects with methodology agile
You can retrieve the current scopes in use with :current_scopes method.
In the last case, it would return:
{ :featured => "true", :by_methodology => "agile", :limit => "20" }
Finally, let's suppose you store on the session how many projects the user sees
per page. In such cases, you can give a proc as default value:
has_scope :limit, :default => proc{|c| c.session[:limit] || 10 }
Belongs to
----------
Finally, our Projects are going to get some Tasks. Then you create a
TasksController and do:
class TasksController < InheritedResources::Base
belongs_to :project
end
belongs_to accepts several options to be able to configure the association.
For example, if you want urls like /projects/:project_title/tasks, you can
customize how InheritedResources find your projects:
class TasksController < InheritedResources::Base
belongs_to :project, :finder => :find_by_title!, :param => :project_title
end
It also accepts :route_name, :parent_class and :instance_name as options.
Check the lib/inherited_resources/class_methods.rb for more.
Nested belongs to
-----------------
Now, our Tasks get some Comments and you need to nest even deeper. Good
practices says that you should never nest more than two resources, but sometimes
you have to for security reasons. So this is an example of how you can do it:
class CommentsController < InheritedResources::Base
nested_belongs_to :project, :task
end
If you need to configure any of these belongs to, you can nested them using blocks:
class CommentsController < InheritedResources::Base
belongs_to :project, :finder => :find_by_title!, :param => :project_title do
belongs_to :task
end
end
Warning: calling several belongs_to is the same as nesting them:
class CommentsConroller < InheritedResources::Base
belongs_to :project
belongs_to :task
end
In other words, the code above is the same as calling nested_belongs_to.
Polymorphic belongs to
----------------------
We can go even further. Let's suppose our Projects can now have Files, Messages
and Tasks, and they are all commentable. In this case, the best solution is to
use polymorphism:
class CommentsController < InheritedResources::Base
belongs_to :task, :file, :message, :polymorphic => true
# polymorphic_belongs_to :task, :file, :message
end
You can even use it with nested resources:
class CommentsController < InheritedResources::Base
belongs_to :project do
belongs_to :task, :file, :message, :polymorphic => true
end
end
The url in such cases can be:
/project/1/task/13/comments
/project/1/file/11/comments
/project/1/message/9/comments
When using polymorphic associations, you get some free helpers:
parent? #=> true
parent_type #=> :task
parent_class #=> Task
parent #=> @task
Optional belongs to
-------------------
Later you decide to create a view to show all comments, independent if they belong
to a task, file or message. You can reuse your polymorphic controller just doing:
class ProjectsController < InheritedResources::Base
belongs_to :task, :file, :message, :optional => true
# optional_belongs_to :task, :file, :message
end
This will handle all those urls properly:
/comment/1
/tasks/2/comment/5
/files/10/comment/3
/messages/13/comment/11
This is treated as a special type of polymorphic associations, thus all helpers
are available. As you expect, when no parent is found, the helpers return:
parent? #=> false
parent_type #=> nil
parent_class #=> nil
parent #=> nil
Singletons
----------
Now we are going to add manager to projects. We say that Manager is a singleton
resource because a Project has just one manager. You should declare it as
has_one (or resource) in your routes.
To declare an association as singleton, you just have to give the :singleton
option.
class ManagersController < InheritedResources::Base
belongs_to :project, :singleton => true
# singleton_belongs_to :project
end
It will deal with everything again and hide the action :index from you.
URL Helpers
-----------
When you use InheritedResources it creates some URL helpers.
And they handle everything for you. :)
# /posts/1/comments
resource_url # => /posts/1/comments/#{@comment.to_param}
resource_url(comment) # => /posts/1/comments/#{comment.to_param}
new_resource_url # => /posts/1/comments/new
edit_resource_url # => /posts/1/comments/#{@comment.to_param}/edit
edit_resource_url(comment) #=> /posts/1/comments/#{comment.to_param}/edit
collection_url # => /posts/1/comments
# /projects/1/tasks
resource_url # => /projects/1/tasks/#{@task.to_param}
resource_url(task) # => /projects/1/tasks/#{task.to_param}
new_resource_url # => /projects/1/tasks/new
edit_resource_url # => /projects/1/tasks/#{@task.to_param}/edit
edit_resource_url(task) # => /projects/1/tasks/#{task.to_param}/edit
collection_url # => /projects/1/tasks
# /users
resource_url # => /users/#{@user.to_param}
resource_url(user) # => /users/#{user.to_param}
new_resource_url # => /users/new
edit_resource_url # => /users/#{@user.to_param}/edit
edit_resource_url(user) # => /users/#{user.to_param}/edit
collection_url # => /users
Those urls helpers also accepts a hash as options, just as in named routes.
# /projects/1/tasks
collection_url(:page => 1, :limit => 10) #=> /projects/1/tasks?page=1&limit=10
Another nice thing is that those urls are not guessed during runtime. They are
all created when your application is loaded (except for polymorphic
associations, that relies on Rails polymorphic_url).
Bugs and Feedback
-----------------
If you discover any bugs, please send an e-mail to jose.valim@gmail.com
If you just want to give some positive feedback or drop a line, that's fine too!
Copyright (c) 2009 José Valim
http://josevalim.blogspot.com/