Let's say you have two screens:
- Show a given project
- Show a report for all projects
Ideally you want both screens to be handled by different controllers like this:
GET /projects/:id => ProjectsController#show
GET /projects/report => Projects::ReportsController#show
What seems like a simple requirement is a little awkward to configure in your routes.
Obviously the report should be a singleton resource, but how can we nest it into the Projects::
namespace?
What does not work is this:
resources :projects, only: :show do
resource :report, only: :show
end
If we defined the routes above we would get a report for each project, instead of one report across all projects:
GET /projects/:id => ProjectsController#show
GET /projects/:id/report => Projects::ReportsController#show
Hacks to solve this include renaming the report route to something like /projects_report
, or defining
a new collection action ProjectsController#report
. But these are all unsatisfying.
What you can do is wrap the sub-resource in a collection
block like you would do with custom collection actions:
resources :projects, only: :show do
collection do
resource :report, only: :show, controller: 'projects/report'
resources :members, only: [:index, :show]
end
end
Run rake routes
and you get what you wanted in the first place:
GET /projects/:id => ProjectsController#show
GET /projects/report => Projects::ReportsController#show
GET /projects/members => MembersController#index
GET /projects/members/:id => MembersController#show
Note how /projects/report
, /projects/members
and /projects/members/:id
do not take a :project_id
.
Controllers
Take care when defining nested resources, as they don't behave identical to namespaced resources:
namespace :users do
resources :sign_ups, only: [:new]
end
# GET /users/sign_ups/new => Users::SignUpsController#new
resources :users, only: [] do
resources :sign_ups, only: [:new]
end
# GET /users/sign_ups/new => SignUpsController#new
Both solutions will generate the same routes, the controller paths will be different, though. In the first example rails will expect to find a Users::SignUpsController
in the subdirectory controllers/users/sign_ups_controller.rb
, but there has to be a SignUpsController
in controllers/sign_ups_controller.rb
in order to make the second example work.
You can alter this behaviour with a custom controller path, as shown in the very first example of this card (resource :report, only: :show, controller: 'projects/report'
).
Solution for legacy Rails versions
What you can do instead is simple re-use the :projects
symbol as both a resource and a namespace:
namespace :projects, as: :project do
resource :report, only: :show
end
resources :projects, only: :show
Note that the routes must appear in this order so projects/report
won't match the :projects
resource.
Run rake routes
and you get what you wanted in the first place:
GET /projects/report => Projects::ReportsController#show
GET /projects/:id => ProjectsController#show