Create the Relation between Notes and Comments

From Ocean Framework Documentation Wiki
Jump to: navigation, search

Now that we have Notes and Comments, we can set up the relation between them. Thanks to Rails and Ocean, this is a straightforward task.


Notes have many Comments

First, in the relations section of spec/models/note_spec.rb, add

it "should include a collection of Comments" do
  create(:note).comments.should == []
end

Run rspec and see it fail. To make it pass, add the following line to the Note model, immediately under the Relations comment:

has_many :comments

Comments belong to Notes

Now examine spec/models/comment_spec.rb. Add a belongs_to :note declaration under the Relations comment.

Comments require a Note

At this point, we need to think a little about a couple of fundamental properties of the relation. First of all, a Comment should always have a corresponding Note. This means that the following spec should be added to the Comment model spec file:

it "should always be associated with a Note" do
  build(:comment, note: nil).should_not be_valid
end

Run the specs, see the new one fail, and add the following validation to the comment model:

validates :note_id, presence: true

When you run the specs again, there will be over 40 errors. This is due to the fact that we are now requiring every Comment we create to have an associated Note, and we're simply not creating a Note for each Comment we create when running the specs. To remedy this situation, open the Comment factory and change the line reading

note nil

to just

note

This will create a correctly linked Note resource for each Comment you create using FactoryGirl.


How do we create Comments?

The next fundamental thing we have to take into consideration at this point has to do with the two remaining failing tests, which both pertain to creating new comments using POST. The reason they fail is that the ID of the associated Note is missing (the attribute we just made mandatory). It's obvious that we need to specify, in some way, for which Note we want to create a Comment.

We can solve this by:

Passing in the UUID of the Note
This would however require the user to know and keep track of the internal UUID, and that's something the entire architecture is designed to avoid. Thus, this alternative is not an option.
Passing in the URI of the Note resource
This is better and is what we do for connect operations. However, it requires server-side parsing of the URI. We could definitely do this, but there's a simpler way:
Not POSTing to the Comments collection URI at all, but to a comments hyperlink for the Note
This is the best alternative. It's logical that a GET request to a Note's comments hyperlink should return all the Comments for that Note. It makes sense that a POST to the same URI creates a new Comment for the specific Note.

Thus, we will create a comments hyperlink for Notes, and we will also create a note hyperlink for Comments. We'll start with the latter, as it is trivial.

Creating the note hyperlink

Begin by commenting out the entire contents of the POST spec for Comments, as we won't be using the create controller action for Comments to create new instances. We will soon migrate the relevant parts of this file to another spec file for the hyperlink.

Make sure all tests pass.

Now open the view spec for comments, spec/views/comments/_comment_spec.rb. Add the following to the hyperlink section:

it "should have a note hyperlink" do
  @links.should be_hyperlinked('note', /notes/)
end

Also change the test for the total number of hyperlinks, which should be 4. Run the tests and see them fail. To make them succeed again, open the corresponding view fragment. To the hyperlink section, add the following just under the self hyperlink line:

note:    note_url(comment.note),

Run the specs again. They should all succeed.

Creating the comments hyperlink

Now that we have a note hyperlink for Comments, we'll proceed to implement the comments hyperlink for Notes. Open the view spec file for Notes and add a spec for the new hyperlink:

 it "should have a comments hyperlink" do
   @links.should be_hyperlinked('comments', /comments/)
 end

Also update the test checking the hyperlink count. Now update the note fragment view with the new hyperlink:

comments: comments_note_url(note),

The specs will fail due to the route not being defined yet.


Routing of the comments hyperlink

Now it's time to add the routing specs for our two new actions. Add the following to the Notes routing spec file:

   it "routes to #comments" do
     get("/v1/notes/1-2-3-4-5/comments").should route_to("notes#comments", id: "1-2-3-4-5")
   end

   it "routes to #comment_create" do
     post("/v1/notes/1-2-3-4-5/comments").should route_to("notes#comment_create", id: "1-2-3-4-5")
   end

Open config/routes.rb and change the declaration for Notes to

resources :notes, except: [:new, :edit],
                  constraints: {id: UUID_REGEX} do
  member do
    get 'comments'
    post 'comments' => 'notes#comment_create'
  end
end

As you see, in addition to the GET route used to obtain Comment collections, we have also added the POST route we will use to create Comments for a member Note. (NB: get 'comments' is equivalent to get 'comments' => 'notes#comments'.)

All tests should pass.

Updating the Notes controller

Let's set up the controller first for both actions. Open the NotesController and alter the first lines so that they read:

 ocean_resource_controller extra_actions: {'comments' =>       ['comments', "GET"], 
                                           'comment_create' => ['comments', "POST"]}
                           
 respond_to :json
 
 before_action :find_note, :only => [:show, :update, :destroy, :comments, :comment_create]

Now it's time to tackle the controller functionality. Create two new spec files, one for each new action:

touch spec/controllers/notes/comments_spec.rb
touch spec/controllers/notes/comment_create_spec.rb


GET of Comment collections

Paste in the following in comments_spec.rb:

require 'spec_helper'

describe NotesController do
 
  render_views

  describe "GET comments" do
   
    before :each do
      permit_with 200
      @n1 = create :note
      @c1 = create :comment, note: @n1
      @c2 = create :comment, note: @n1
      @n2 = create :note
      @c3 = create :comment, note: @n2
      request.headers['HTTP_ACCEPT'] = "application/json"
      request.headers['X-API-Token'] = "boy-is-this-fake"
    end
    
    
    it "should return JSON" do
      get :comments, id: @n1
      response.content_type.should == "application/json"
    end
    
    it "should return a 400 if the X-API-Token header is missing" do
      request.headers['X-API-Token'] = nil
      get :comments, id: @n1
      response.status.should == 400
      response.content_type.should == "application/json"
    end
    
    it "should return a 404 when the Note can't be found" do
      get :comments, id: "0-0-0-0-0"
      response.status.should == 404
      response.content_type.should == "application/json"
    end

    it "should return a 200 when successful" do
      get :comments, id: @n1
      response.should render_template(partial: "comments/_comment", count: 2)
      response.status.should == 200
    end
    
    it "should return a collection" do
      get :comments, id: @n1
      response.status.should == 200
      JSON.parse(response.body).should be_an Array
    end

  end
  
end

The specs will fail. Add the following action to the NotesController:

 # GET /notes/1/comments
 def comments
   expires_in 0, 's-maxage' => DEFAULT_CACHE_TIME
   if stale?(@note.comments)
     api_render @note.comments
   end
 end

The tests should now pass.

POST a new Comment

Next, paste in the following in comment_create_spec.rb:

require 'spec_helper'

describe NotesController do
 
  render_views
 
  describe "POST" do
   
    before :each do
      permit_with 200
      request.headers['HTTP_ACCEPT'] = "application/json"
      request.headers['X-API-Token'] = "incredibly-fake!"
      @note = create :note
      c = build :comment, note: @note
      @args = { 'id' => @note.id, 'body' => "The body." }
    end 

    
    it "should return JSON" do
      post :comment_create, @args
      response.content_type.should == "application/json"
    end
    
    it "should return a 400 if the X-API-Token header is missing" do
      request.headers['X-API-Token'] = nil
      post :comment_create, @args
      response.status.should == 400
    end
    
    it "should return a 404 when the Note can't be found" do
      post :comment_create, id: "0-0-0-0-0"
      response.status.should == 404
      response.content_type.should == "application/json"
    end

    it "should return a 422 when there are validation errors" do
      post :comment_create, @args.merge('body' => "      ")
      response.status.should == 422
      response.content_type.should == "application/json"
      JSON.parse(response.body).should == {"body"=>["can't be blank"]}
    end
                
    it "should return a 201 when successful" do
      post :comment_create, @args
      response.should render_template(partial: "comments/_comment", count: 1)
      response.status.should == 201
    end

    it "should contain a Location header when successful" do
      post :comment_create, @args
      response.headers['Location'].should be_a String
    end

    it "should return the new resource in the body when successful" do
      post :comment_create, @args
      response.body.should be_a String
    end

    it "should increase the number of associated Comments for the Note by one" do
      @note.comments.count.should == 0
      post :comment_create, @args
      @note.comments.count.should == 1
    end 

  end
  
end

The specs should fail. Add the following action to the NoteController:

 # POST /notes/1/comments
 def comment_create
   @comment = @note.comments.new(filtered_params Comment)
   set_updater(@comment)
   @comment.save!
   api_render @comment, new: true
 end

Again, the specs should pass.

The only difference between this version and the old one is this line:

   @comment = @note.comments.new(filtered_params Comment)

The old version was:

   @comment = Comment.new(filtered_params Comment)

Cleanup

We're almost finished. To remove the spec file we commented out previously - it's no longer needed - issue the following command:

rm spec/controllers/comments/create_spec.rb

You should also open the CommentsController and remove the create action, as we've now successfully moved this functionality to the NotesController. As the create action is no longer present in the CommentController, you should modify the Comment routing in config/routes.rb thus:

   resources :comments, except: [:new, :edit, :create]

When you do this, the routing spec for the POST will fail. Modify the failing spec so that it becomes:

   it "routes to #create" do
     post("/v1/comments").should_not be_routable
   end

One remaining feature

There's only one remaining feature to be added. When a Note is DELETEd, we want all its associated Comments to be DELETEd also. This can easily be done by first writing the following spec (for spec/models/note_spec.rb):

   it "should destroy its Comments when itself is destroyed" do
     note = create :note
     create :comment, note: note
     create :comment, note: note
     create :comment, note: note
     note.destroy
     Comment.count.should == 0
   end

Run the specs, see them fail, then edit the Note model file. Change this line:

  has_many :comments

to

 has_many :comments, dependent: :destroy

This concludes the implementation of the Sandbox service. All tests should pass, and test coverage should exceed 96 percent.


Next : Setting up Auth for Sandbox