Nested resources with independent views in Ruby on Rails

This is an archive of blog post I wrote during my first venture (beginning of 8thcolor).

Introduction๐Ÿ”—

In REST-style architecture , a resource is simply a source of information, the one you want to expose. The resource is referenced thanks to a global identifier such as an URI.

For example, in our own PullReview, Code Reviews and Users are resources with their related routes and actions.

When you deal with information structure such as composition, you'll use nested resources. The reference of the embedded resource is then built over the reference of the composite resource.

Ruby on Rails allows you to set up nested resources. For instance in the "Getting started" guide where you build a very simple blog, Post and Comment are nested resources. Indeed, it is impossible to consider a lone comment without any post. A Comment belongs to a Post. Resources being referenced by URIs, the setup of nested resources in RoR is done through routing as following:

resources :posts do
 resources :comments
end

But in the example of the guide, a Comment hasn't got its own view. A Comment is managed through the views of its Post. It's totaly suited to a blog, but in another case, maybe you would like to program nested resources with independant views, i.e. each resource has their views. This is the goal of this tutorial. I start from the same blog example, but this time I'll generate scaffolds for both models, Post and Comment.

Same first steps: Post model๐Ÿ”—

First, create the blog application:

rails new blog
cd blog

Generate a scaffolded Post resource:

rails generate scaffold Post name:string title:string content:text

Add some validation to it (app/models/post.rb):


class Post < ActiveRecord::Base
 validates :name, :presence => true
 validates :title, :presence => true, :length => { :minimum => 5 }
end

Until now, it's totally the same than in the guide. The next step starts the fork!

Fork: Comment Model๐Ÿ”—

Generate a scaffolded Comment resource:

rails generate scaffold Comment commenter:string body:text post:references

Edit the app/models/post.rb file to add the other side of the association:

class Post < ActiveRecord::Base
 validates :name, :presence => true
 validates :title, :presence => true, :length => { :minimum => 5 }

 has\_many :comments
end

Setup nested resources (config/routes.rb):

resources :posts do
 resources :comments
end

Add some validations to the Comment resource (app/models/comment.rb):

class Comment < ActiveRecord::Base
 validates :commenter, :presence => true
 validates :body, :presence => true

 belongs\_to :post
end

Comment controller๐Ÿ”—

Edit the Comment controller app/controllers/comments_controller.rb:

class CommentsController < ApplicationController
 # GET /posts/:post\_id/comments
 # GET /posts/:post\_id/comments.xml
 def index
   #1st you retrieve the post thanks to params[:post\_id]
   post = Post.find(params[:post\_id])
   #2nd you get all the comments of this post
   @comments = post.comments

   respond\_to do \|format\|
     format.html # index.html.erb
     format.xml { render :xml => @comments }
    end
  end

 # GET /posts/:post\_id/comments/:id
 # GET /comments/:id.xml
 def show
   #1st you retrieve the post thanks to params[:post\_id]
   post = Post.find(params[:post\_id])
   #2nd you retrieve the comment thanks to params[:id]
   @comment = post.comments.find(params[:id])

   respond\_to do \|format\|
     format.html # show.html.erb
     format.xml { render :xml => @comment }
   end
 end

 # GET /posts/:post\_id/comments/new
 # GET /posts/:post\_id/comments/new.xml
 def new
   #1st you retrieve the post thanks to params[:post\_id]
   post = Post.find(params[:post\_id])
   #2nd you build a new one
   @comment = post.comments.build

   respond\_to do \|format\|
     format.html # new.html.erb
     format.xml { render :xml => @comment }
   end
 end

 # GET /posts/:post\_id/comments/:id/edit
 def edit
   #1st you retrieve the post thanks to params[:post\_id]
   post = Post.find(params[:post\_id])
   #2nd you retrieve the comment thanks to params[:id]
   @comment = post.comments.find(params[:id])
 end

 # POST /posts/:post\_id/comments
 # POST /posts/:post\_id/comments.xml
 def create
   #1st you retrieve the post thanks to params[:post\_id]
   post = Post.find(params[:post\_id])
   #2nd you create the comment with arguments in params[:comment]
   @comment = post.comments.create(params[:comment])

   respond\_to do \|format\|
     if @comment.save
       #1st argument of redirect\_to is an array, in order to build the correct route to the nested resource comment
       format.html { redirect\_to([@comment.post, @comment], :notice => 'Comment was successfully created.') }
       #the key :location is associated to an array in order to build the correct route to the nested resource comment
       format.xml { render :xml => @comment, :status => :created, :location => [@comment.post, @comment] }
     else
       format.html { render :action => "new" }
       format.xml { render :xml => @comment.errors, :status => :unprocessable\_entity }
     end
   end
 end

 # PUT /posts/:post\_id/comments/:id
 # PUT /posts/:post\_id/comments/:id.xml
 def update
   #1st you retrieve the post thanks to params[:post\_id]
   post = Post.find(params[:post\_id])
   #2nd you retrieve the comment thanks to params[:id]
   @comment = post.comments.find(params[:id])

   respond\_to do \|format\|
     if @comment.update\_attributes(params[:comment])
       #1st argument of redirect\_to is an array, in order to build the correct route to the nested resource comment
       format.html { redirect\_to([@comment.post, @comment], :notice => 'Comment was successfully updated.') }
       format.xml { head :ok }
     else
       format.html { render :action => "edit" }
       format.xml { render :xml => @comment.errors, :status => :unprocessable\_entity }
     end
   end
 end

 # DELETE /posts/:post\_id/comments/1
 # DELETE /posts/:post\_id/comments/1.xml
 def destroy
   #1st you retrieve the post thanks to params[:post\_id]
   post = Post.find(params[:post\_id])
   #2nd you retrieve the comment thanks to params[:id]
   @comment = post.comments.find(params[:id])
   @comment.destroy

   respond\_to do \|format\|
     #1st argument reference the path /posts/:post\_id/comments/
     format.html { redirect\_to(post\_comments\_url) }
     format.xml { head :ok }
   end
 end
end

The important changes are the following:

  • Retrieve a Comment
    post = Post.find(params[:post\_id])
    @comment = post.comments.find(params[:id])
    
  • Retrieve all Comments
    post = Post.find(params[:post\_id])
    @comments = post.comments
    
  • Building of a new Comment
    post = Post.find(params[:post\_id])
    @comment = post.comments.build
    
  • Creation of a new Comment
    post = Post.find(params[:post\_id])
    @comment = post.comments.create(params[:comment])
    
  • Redirection to the Comment resource
    redirect\_to([@comment.post, @comment], :notice => 'Comment was successfully created.')
    
  • Redirection to the list of Comments
    redirect\_to(post\_comments\_url)
    

Comment Views๐Ÿ”—

Edit the views app/views/comments/:

  • form.html.erb: remove the :post member
    <%= form\_for([@comment.post, @comment]) do \|f\| %>
      <% if @comment.errors.any? %>
        ## <%= pluralize(@comment.errors.count, "error") %> prohibited this comment from being saved:
        <% @comment.errors.full\_messages.each do \|msg\| %>
          <%= msg %>
        <% end %>
      <% end %>
    
      <%= f.label :commenter %>
      <%= f.text\_field :commenter %>
      <%= f.label :body %>
      <%= f.text\_area :body %>
      <%= f.submit %>
    <% end %>
    
  • edit.html.erb: update link
    # Editing comment
    <%= render 'form' %>
    <%= link\_to 'Show', [@comment.post, @comment] %> \|
    <%= link\_to 'Back', post\_comments\_path(@comment.post) %>
    
  • index.html.erb: remove :post member and update link
    # Listing comments
    <% @comments.each do \|comment\| %>
    <% end %>
    
    CommenterBodyPost<%= comment.commenter %><%= comment.body %><%= link\_to 'Show', [comment.post, comment] %><%= link\_to 'Edit', edit\_post\_comment\_path(comment.post, comment)%><%= link\_to 'Destroy', [comment.post, comment], :confirm => 'Are you sure?', :method => :delete %>
    
    <%= link\_to 'New Comment', new\_post\_comment\_path %>
    
  • new.html.erb: update link
    # New comment
    <%= render 'form' %>
    <%= link\_to 'Back', post\_comments\_path(@comment.post)%>
    
  • show.html.erb: remove :post member and update link
    <%= notice %>
    **Commenter:**
     <%= @comment.commenter %>
    
    **Body:**
     <%= @comment.body %>
    
    <%= link\_to 'Edit', edit\_post\_comment\_path(@comment.post, @comment) %> \|
    <%= link\_to 'Back', post\_comments\_path(@comment.post) %>
    
  • And finally, add a link to the show view of post app/views/posts/show.html.erb:
    <%= notice %>
    
    **Name:**
    <%= @post.name %>
    
    **Title:**
    <%= @post.title %>
    
    **Content:**
    <%= @post.content %>
    
    <%= link\_to 'Comments', post\_comments\_path(@post) %> \|
    <%= link\_to 'Edit', edit\_post\_path(@post) %> \|
    <%= link\_to 'Back', posts\_path %>
    

Final commands๐Ÿ”—

Check the routes:

rake routes
 post\_comments GET /posts/:post\_id/comments(.:format) {:controller=>"comments", :action=>"index"}
 POST /posts/:post\_id/comments(.:format) {:controller=>"comments", :action=>"create"}
 new\_post\_comment GET /posts/:post\_id/comments/new(.:format) {:controller=>"comments", :action=>"new"}
edit\_post\_comment GET /posts/:post\_id/comments/:id/edit(.:format) {:controller=>"comments", :action=>"edit"}
 post\_comment GET /posts/:post\_id/comments/:id(.:format) {:controller=>"comments", :action=>"show"}
 PUT /posts/:post\_id/comments/:id(.:format) {:controller=>"comments", :action=>"update"}
 DELETE /posts/:post\_id/comments/:id(.:format) {:controller=>"comments", :action=>"destroy"}
 posts GET /posts(.:format) {:controller=>"posts", :action=>"index"}
 POST /posts(.:format) {:controller=>"posts", :action=>"create"}
 new\_post GET /posts/new(.:format) {:controller=>"posts", :action=>"new"}
 edit\_post GET /posts/:id/edit(.:format) {:controller=>"posts", :action=>"edit"}
 post GET /posts/:id(.:format) {:controller=>"posts", :action=>"show"}
 PUT /posts/:id(.:format) {:controller=>"posts", :action=>"update"}
 DELETE /posts/:id(.:format) {:controller=>"posts", :action=>"destroy"}

Create and migrate the db:

rake db:create
rake db:migrate

Run the server:

rails server

Voilร !


Atom feed icon Subscribe to the blog!