I am working on a website where I have an admin section to add data and public read only sections. I like to have a clean hierarchy of URLs. I first implemented it using the standard generator code. I had a /restaurants and a /locations url for both. Locations doesn’t make sense as a top level URL because Restaurants have locations. I’d rather see things like /restaurants/1/locations. In fact, that isn’t quite right either. I only want to show the restaurants for the logged in user. I really want it to look like /admin/restaurants/1/locations.
Nested Resources
So the first thing I did was to make locations be a resource of restaurants. In my config/routes.rb file, I did the following:
map.resources :restaurants, :has_many => [ :locations ]
In the app/controllers/locations_controller.rb, I created a before_filter to set the @restaurant reference. Every place where I used Location.find, I changed it to @restaurants.locations.find. This only gets the locations for the restaurant from the restaurant’s locations attribute.
class LocationsController < ApplicationController
before_filter :get_restaurant
def index
@locations = @restaurant.locations
...
end
private
def get_restaurant
@restaurant = Restaurant.find(params[:restaurant_id])
end
end
Now my URLs look like /restaurants/1/locations.
You will need to change the form_for on your new.html.erb and edit.html.erb. If you are using partials for both of these (you should be) then edit that file. Change the form_for tag to
<% form_for([@restaurant, @location]) do |f| %>
Namespace
Now, I’d like to move these into the /admin url. I do this by modifying the config.routes.rb file again.
map.namespace :admin do |admin|
admin.resources :restaurants, :has_many => [ :locations ]
end
This creates the admin namespace with URLs that look like /admin/restaurants/1/locations. When you add this namespace, Rails will then look for your controllers inside the module Admin. So you will need to create the app/controllers/admin directory and move the restaurants_controller.rb and locations_controller.rb inside it. Do the same for the app/views/admin directory. Move the locations and restaurants folder inside it. You will need to modify the /app/controllers/admin/restaurants_controller.rb and the /app/controllers/admin/locations_controller.rb files. Add Admin:: in front of the class name like follows:
class Admin::LocationsController < ApplicationController
When I opened up /admin/restaurants/1/locations in my browser, I got errors saying things like restaurant_location_path is invalid. To correct this, I had to modify the views to prepend “admin_” to all the path functions and add the location parameter. A safe guess is that Ruby on Rails dynamically generates these functions.
<td><%= link_to 'Show', admin_restaurant_location_path(@restaurant, location) %></td>
<td><%= link_to 'Edit', edit_admin_restaurant_location_path(@restaurant, location) %></td>
<td><%= link_to 'Destroy', admin_restaurant_location_path(@restaurant, location), :confirm => 'Are you sure?', :method => :delete %></td>
I really don’t like embedding the “admin” into the code because I could change my mind and want it all to live inside /administration. I don’t want to have to change all my code just because of where it lives. Too tightly coupled. so, I am not going to use the function created for me. Instead, I’ll build the URL myself. If you do not specify the controller or namespace, it uses the current one. Change
<td><%= link_to 'Show', url_for(:action => 'show', :id => location) %></td>
<td><%= link_to 'Edit', url_for(:action => 'edit', :id => location) %></td>
<td><%= link_to 'Destroy', url_for(:action => 'destroy',:id => location), :confirm => 'Are you sure?', :method => :delete %></td>
This will build the same URLs as using the “admin_” functions but without hardcoding the admin path. You need to do the same thing inside the controllers. For the link back to the parent, you will need to add the controller as a parameter.
<%= link_to 'Back', admin_restaurant_path(@restaurant) %>
Change to
<%= link_to 'Back', url_for(:controller => 'restaurants', :action => 'show', :id => @restaurant) %>
The next thing you will need to change is the form edit.
<% form_for( :location, @location, :url => { :action => "create"} ) do |f| %>