Adding Reviews from the Browser

Now you can see your beautiful reviews, but wouldn’t it be nice if you could also add reviews??

  1. Let’s start by taking a look at your application’s routes file. In your text editor, open config/routes.rb.

    So far, you’ve specified one resource in your routes file - books.

    resources :books
    

    It’s been a while since we’ve been inside the routes file, but your application now has a new resource - reviews. You’ve defined a relationship between these two resources in your application. A book can have many reviews, and review belongs to a book.

    We can define this relationship in your routes file.

  2. In your routes file, change

    resources :books
    

    to

    resources :books do
      resources :reviews
    end
    
  3. Save your changes.

1
2
3
4
5
6
7
8
  Rails.application.routes.draw do
    # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
    root "books#index"

    resources :books do
      resources :reviews
    end
  end

Let’s see how that changed your application’s routes.

  1. Open Terminal, go to the bookstore directory, and run rake routes.

    Whoa! Your application’s routing table got waaaaay bigger.

    Your application now has a bunch of review routes. Do you notice a pattern in their paths?

    /books/:book_id/reviews
    /books/:book_id/reviews
    /books/:book_id/reviews/new
    

    They all start with /books/:book_id because of the change you made to your application’s routes file:

    resources :books do
      resources :reviews
    end
    

    By nesting :reviews inside of :books, you’re setting up all your review routes to go through the book they belong to.

    For example, instead of having a route to get all your application’s reviews (/reviews), you have a route to get all of a books reviews (/books/:book_id/reviews).

    Why are we doing this again?

    We’re setting up your application’s routes to reflect the belongs_to/has_many relationship that exists between books and reviews.

    It might feel a little abstract, but this will come in handy when we’re adding reviews.

pwd
  /Users/alimi/Projects/bookstore

  › rake routes
            Prefix Verb   URI Pattern                                Controller#Action
              root GET    /                                          books#index
      book_reviews GET    /books/:book_id/reviews(.:format)          reviews#index
                   POST   /books/:book_id/reviews(.:format)          reviews#create
   new_book_review GET    /books/:book_id/reviews/new(.:format)      reviews#new
  edit_book_review GET    /books/:book_id/reviews/:id/edit(.:format) reviews#edit
       book_review GET    /books/:book_id/reviews/:id(.:format)      reviews#show
                   PATCH  /books/:book_id/reviews/:id(.:format)      reviews#update
                   PUT    /books/:book_id/reviews/:id(.:format)      reviews#update
                   DELETE /books/:book_id/reviews/:id(.:format)      reviews#destroy
             books GET    /books(.:format)                           books#index
                   POST   /books(.:format)                           books#create
          new_book GET    /books/new(.:format)                       books#new
         edit_book GET    /books/:id/edit(.:format)                  books#edit
              book GET    /books/:id(.:format)                       books#show
                   PATCH  /books/:id(.:format)                       books#update
                   PUT    /books/:id(.:format)                       books#update
                   DELETE /books/:id(.:format)                       books#destroy

What Route Do We Use to Add Reviews?

Given the changes to your application’s routes, what route would we use to add new reviews?

One route stands out:

new_book_review GET    /books/:book_id/reviews/new(.:format)      reviews#new

I mean…it has a prefix of new_book_review. 😉

Let’s try visiting that path.

  1. Start your application’s web server, and go to http://localhost:3000/books/1/reviews/new. Since the path starts with /books/1, this is where we’d go to add a review for your first book.

    Those errors sure do get old, don’t they…

  › rails server
  => Booting Puma
  => Rails 5.0.0.1 application starting in development on http://localhost:3000

Browser showing Routing Error: "uninitialized constant ReviewsController"

You got a Routing Error with a message that says “uninitialized constant ReviewsController”. Any ideas why you’re seeing that error?

You don’t have a ReviewsController!

  1. Go back to Terminal, stop your application’s web server, and run the following command:

    touch app/controllers/reviews_controller.rb
    

    This will create an empty file named app/controllers/reviews_controller.rb.

  2. Restart your application’s web server, and open app/controllers/reviews_controller.rb in your text editor.

› rails server
=> Booting Puma
...
^CExiting

› touch app/controllers/reviews_controller.rb

› rails server
=> Booting Puma
  1. Now you have a file for the ReviewsController, but you still haven’t defined it.

  2. Add the following code to app/controllers/reviews_controller.rb

    class ReviewsController < ApplicationController
    end
    
  3. Save your changes and revisit http://localhost:3000/books/1/reviews/new.

1
2
  class ReviewsController < ApplicationController
  end

Browser showing Unknown action error: "The action 'new' could not be found for ReviewsController"

Another error, but we know how to deal with these.

Right?!

Fake it 'til you make it

You’re seeing an Unknown action error that says “The action ‘new’ could not be found for ReviewsController”. How would you fix this error?

The ReviewsController needs a new action!

  1. Add an empty new method to the ReviewsController.

    def new
    end
    
  2. Save your changes and revisit http://localhost:3000/books/1/reviews/new.

1
2
3
4
  class ReviewsController < ApplicationController
    def new
    end
  end

Browser showing ActionController::UnknownFormat error: "ReviewsController#new is missing a template for this request format and variant."

You have a ReviewsController that has a new method, but you’re still getting an error when you visit the new reviews path (http://localhost:3000/books/1/reviews/new).

The ActionController::UnknownFormat error might seem intense, but do you remember seeing it before? There’s one key piece of information burried in the weeds.

ReviewsController#new is missing a template for this request format and variant.

We’re missing a template!

  1. First, go back to Terminal and stop your application’s web server.

  2. Now, you’ll need to create directory for the review templates. Run the following command:

    mkdir app/views/reviews
    

    This created an empty reviews directory inside of app/views.

  3. Then, you can create the new reviews template with the following command:

    touch app/views/reviews/new.html.erb
    
  4. Restart your application’s web server, and try going back to http://localhost:3000/books/1/reviews/new.

    What do you think will happen?

    Fingers crossed!

  › rails server
  ...
  ^CExiting

  › mkdir app/views/reviews

  › touch app/views/reviews/new.html.erb

  › rails server
  => Booting Puma
  => Rails 5.0.0.1 application starting in development on http://localhost:3000

Have you ever been so excited to see a blank page?!

Please, contain your excitement!

Someone's not excited

We’re rendering a blank page for the new reviews page, but what should we be rendering instead?

We’re going to follow the same we pattern we used back in Chapter 5 to add books.

To add a book, you start by visiting the new book page (http://localhost:3000/books/new). When you get there, you see a form where you can enter data for the new book you want to add.

Browser showing new book page

When you’re done adding the data for the new book, you click on “Create Book” to add the new book.

We’re going to repeat this workflow, but instead of creating a book we’re going to create a review for a given book.

Let’s get started by showing the title of the book that’s being reviewed at the top of the page.

  1. Open the ReviewsController in your text editor, and add the following code to new method.

    @book = Book.find(params[:book_id])
    

    This will make the book’s data available in the new review template.

  2. Save your changes.

1
2
3
4
5
  class ReviewsController < ApplicationController
    def new
      @book = Book.find(params[:book_id])
    end
  end
  1. Now, open app/views/reviews/new.html.erb and add the following line:

    <h1> New review for <%= @book.title %> </h1>
    
  2. Save your changes and revisit http://localhost:3000/books/1/reviews/new.

    The top of the page should have a message that says “New review for why’s (poignant) Guide to Ruby”.

  3. Try visiting the new review path for different books in your bookstore.

    For example, to visit the new review path for the second book in your bookstore you would go to http://localhost:3000/books/2/reviews/new.

    As you go to different paths, the message at the top of the page should change to show that book’s title.

1
  <h1> New Review for <%= @book.title %> </h1>

Now, we need to make the new review form.

To get started, we’ll update the ReviewsController new method to make a new review available in the new review template.

That’s a lot of new 😅

  1. Open the ReviewsController and add the following line at the end of the new method.

    @review = @book.reviews.build
    

    This will create an empty review for the book that we can use in the new review template.

  2. Save your changes.

1
2
3
4
5
6
  class ReviewsController < ApplicationController
    def new
      @book = Book.find(params[:book_id])
      @review = @book.reviews.build
    end
  end

Now, we can build the review form!

Jake and Finn are excited!

  1. This form is going to be pretty simmilar to the new book form, but with less fields.

    <%= form_for(@book) do |f| %>
      <ul>
        <li>
          <%= f.label :title %>
          <%= f.text_field :title %>
        </li>
    
        <li>
          <%= f.label :author %>
          <%= f.text_field :author %>
        </li>
    
        <li>
          <%= f.label :price_cents %>
          <%= f.number_field :price_cents %>
        </li>
    
        <li>
          <%= f.label :quantity %>
          <%= f.number_field :quantity %>
        </li>
    
        <li>
          <%= f.label :description %>
          <%= f.text_area :description %>
        </li>
      </ul>
    
      <%= f.submit(class: "button") %>
    <% end %>
    
  2. Using the new book form as an example, add a new review form to app/views/reviews/new.html.erb. The only field you need to set on a review is its body.

    When you’re done, try add a review!

    Psst…not to be a downer, but chances are your solution won’t work

What did you come up with? If you’re anything like me, you followed the pattern in the new book form and came up with something like this:

<%= form_for(@review) do |f| %>
  <ul>
    <li>
      <%= f.label :body %>
      <%= f.text_field :body %>
    </li>
  </ul>

  <%= f.submit(class: "button") %>
<% end %>

For some reason, that doesn’t work. This solution leads to an error…

Browser showing NoMethodError in Reviews#new: "undefined method `reviews_path'"

This is a tricky error, but the gist of it is that we have an undefined method for reviews_path.

By doing <%= form_for(@review) do |f| %>, we’re creating a form for a new review that will get submitted to the reviews_path. What’s wrong with that?

We don’t have a reviews_path…we have a book_reviews_path.

Do you remember what your application’s routes file looks like?

Rails.application.routes.draw do
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
  root "books#index"

  resources :books do
    resources :reviews
  end
end

Reviews are resources nested under books. Therefore, all review routes have to be referenced through a book.

  1. To make the new review form work, we need to change

    <%= form_for(@review) do |f| %>
    

    to

    <%= form_for([@book, @review]) do |f| %>
    

    With this change, the form will use the book_reviews_path instead of the invalid reviews_path.

  2. Your form should now look like this:

    <%= form_for([@book, @review]) do |f| %>
      <ul>
        <li>
          <%= f.label :body %>
          <%= f.text_field :body %>
        </li>
      </ul>
    
      <%= f.submit(class: "button") %>
    <% end %>
    

    Update your form to match this solution.

  3. Save your changes and try adding a review to your first book by visiting http://localhost:3000/books/1/reviews/new.

1
2
3
4
5
6
7
8
9
10
11
12
  <h1> New Review for <%= @book.title %> </h1>

  <%= form_for([@book, @review]) do |f| %>
    <ul>
      <li>
        <%= f.label :body %>
        <%= f.text_field :body %>
      </li>
    </ul>

    <%= f.submit(class: "button") %>
  <% end %>

Browser showing an Unknown action error when trying to add a review

An Unknown action error?! But we were so close!!!

Jake is upset

Don’t be so down! We’ve gotten through this before…we can fix this.

Fix it!

The error is telling us what we need to do.

The action 'create' could not be found for ReviewsController.
  1. Add an empty create method to the ReviewsController.

    def create
    end
    
  2. Save your changes and try adding a review for your first book again by visiting http://localhost:3000/books/1/reviews/new.

1
2
3
4
5
6
7
8
9
  class ReviewsController < ApplicationController
    def new
      @book = Book.find(params[:book_id])
      @review = @book.reviews.build
    end

    def create
    end
  end

Nothing happened, right?

RIGHT?!?

Of course nothing happened, the create method is empty.

We need to update the ReviewsController create method so it creates a review for the requested book.

  1. Add the followling lines to the ReviewsController create method:

    book = Book.find(params[:book_id])
    book.reviews.create(params)
    

    First, we find the requested book and assign it to a variable called book. Then, we create a review for the book using the data that was sent with the form. The data is available in the params hash.

  2. Save your changes, revisit http://localhost:3000/books/1/reviews/new, and try adding a review one more time.

1
2
3
4
5
6
7
8
9
10
11
  class ReviewsController < ApplicationController
    def new
      @book = Book.find(params[:book_id])
      @review = @book.reviews.build
    end

    def create
      book = Book.find(params[:book_id])
      book.reviews.create(params)
    end
  end

Browser showing ActiveModel::ForbiddenAttributesError in ReviewsController#create

Are you really surprised to see another error? 😉

What do you think is going on? You’re getting an ActiveModel::ForbiddenAttributesError, but why?

If you look closely at the browser, Rails is telling you where the problem is. It’s inside the create method.

book.reviews.create(params)

Why would you get an ActiveModel::ForbiddenAttributesError from doing book.reviews.create(params)…🤔

Check out the ActiveModel::ForbiddenAttributesError doc. What does the description say?

Raised when forbidden attributes are used for mass assignment.

Ah! We’re trying to create the review using all the data from the params hash, but we haven’t allowed any attributes to be set. This is that security feature we looked at earlier called strong parameters.

Strong parameters forces us to explcitly state what attributes can be set to prevent malicious data from entering our applications. When you try to assign attributes that haven’t been permitted, you get an ActiveModel::ForbiddenAttributesError error.

To fix the ReviewsController create method, we need to specify the attributes that can be set on reviews. There is only one attribute we care about - body.

  1. Add the following method to the end of the ReviewsController:

    def review_params
      params.require(:review).permit(:body)
    end
    

    Inside this review_params method, we’re saying the params hash must have a review hash. Inside the review hash, we’ll allow a body attribute to be used on a new review.

  2. Do you remember what the params hash looks like? It looks something like this:

    {"utf8"=>"✓", "authenticity_token"=>"JYZD9Lsnb6RbMX7OW/QUXNY6Uzxd0MYZNcDKHKZjAmggVeGwKb6lPkOE60zgptRlquzdhJo9tOU5r7mLthxXog==", "review"=>{"body"=>"Wooooooooo"}, "commit"=>"Create Review", "book_id"=>"1"}
    

    There’s a bunch of stuff in there, but now we’re restricted to only using the body attribute from the review hash.

  3. Now we can use review_params inside the create method. Replace

    book.reviews.create(params)
    

    with

    book.reviews.create(review_params)
    
  4. Save your changes, revisit http://localhost:3000/books/1/reviews/new, and try adding a review!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  class ReviewsController < ApplicationController
    def new
      @book = Book.find(params[:book_id])
      @review = @book.reviews.build
    end

    def create
      book = Book.find(params[:book_id])
      book.reviews.create(review_params)
    end

    def review_params
      params.require(:review).permit(:body)
    end
  end

Nothing happened…or did it?

  1. Go back to your first book’s details page at http://localhost:3000/books/1.

    Notice anything different???

    Your new review!!! 🎉

  2. Nothing happens at the end of the ReviewsController create method so it feels like your review wasn’t created. Let’s fix this.

    Add the following line to the end of the ReviewsController create method:

    redirect_to book_path(book)
    

    After a review is added for the book, we’ll redirect to the book details page for the book so we can verify the new review was created.

  3. Save your changes, revisit http://localhost:3000/books/1/reviews/new, and watch what happens when you add a new review.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  class ReviewsController < ApplicationController
    def new
      @book = Book.find(params[:book_id])
      @review = @book.reviews.build
    end

    def create
      book = Book.find(params[:book_id])
      book.reviews.create(review_params)
      redirect_to book_path(book)
    end

    def review_params
      params.require(:review).permit(:body)
    end
  end

You’ve done a lot of work in this chapter, but look at your bookstore! Now you can write all those book reviews you’ve been putting off for all these years.

Typing!

Before you jump into writing those reviews, let’s clean up one more thing…

To create a review for a book, you have to know the URL you want to visit. We can make this a little nicer by adding a link to the new review page from the book details pages.

You’ll be able to quickly write a review for the book you’re looking at.

  1. Let’s start by looking at your application’s routes. Go to Terminal, stop your application’s web server, and run rake routes.

    Which row has the path we’ve been using to get to the new review page?

    new_book_review GET    /books/:book_id/reviews/new(.:format)      reviews#new
    

    The first column in the row is a prefix. The new review page has a prefix of new_book_review. We’ll use this prefix to build a link to the new review path.

  2. Restart your application’s web server.

  › rails server
  => Booting Puma
  ...
  ^CExiting

  › rake routes
            Prefix Verb   URI Pattern                                Controller#Action
              root GET    /                                          books#index
      book_reviews GET    /books/:book_id/reviews(.:format)          reviews#index
                   POST   /books/:book_id/reviews(.:format)          reviews#create
   new_book_review GET    /books/:book_id/reviews/new(.:format)      reviews#new
  edit_book_review GET    /books/:book_id/reviews/:id/edit(.:format) reviews#edit
       book_review GET    /books/:book_id/reviews/:id(.:format)      reviews#show
                   PATCH  /books/:book_id/reviews/:id(.:format)      reviews#update
                   PUT    /books/:book_id/reviews/:id(.:format)      reviews#update
                   DELETE /books/:book_id/reviews/:id(.:format)      reviews#destroy
             books GET    /books(.:format)                           books#index
                   POST   /books(.:format)                           books#create
          new_book GET    /books/new(.:format)                       books#new
         edit_book GET    /books/:id/edit(.:format)                  books#edit
              book GET    /books/:id(.:format)                       books#show
                   PATCH  /books/:id(.:format)                       books#update
                   PUT    /books/:id(.:format)                       books#update
                   DELETE /books/:id(.:format)                       books#destroy

  › rails server
  => Booting Puma
  ...
  1. Now, open app/views/books/show.html.erb in your text editor.

  2. Find the “Edit book” link.

    <%= link_to("Edit book", edit_book_path(@book), class: "button") %>
    
  3. After the link “Edit book” link, add a link to “Add a Review”.

    The link should say “Add a Review”. It should link to the new_book_review_path for @book, and it should have a “button” class so it gets styled like an actionable element.

  1. What did you come up with?

    The link should look something like this:

    <%= link_to("Add a Review", new_book_review_path(@book), class: "button") %>
    
  2. Update your solution to match this solution.

  3. Save your changes, find some books, and review them!

  4. When you’re done writing reviews, stop your application’s web server.

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
  <dl>
    <dt>Id</dt>
    <dd><%= @book.id %></dd>

    <dt>Title</dt>
    <dd><%= @book.title %></dd>

    <dt>Author</dt>
    <dd><%= @book.author %></dd>

    <dt>Price</dt>
    <dd><%= number_to_currency(@book.price_cents / 100.0) %></dd>

    <dt>Quantity</dt>
    <dd><%= @book.quantity %></dd>

    <dt>Description</dt>
    <dd><%= @book.description %></dd>
  </dl>

  <p> Number of reviews: <%= @book.reviews.count %> </p>

  <ul>
    <% @book.reviews.each do |review| %>
      <li> <%= review.body %> </li>
    <% end %>
  </ul>

  <%= link_to("Edit book", edit_book_path(@book), class: "button") %>

  <%= link_to("Add a Review", new_book_review_path(@book), class: "button") %>

  <%= button_to("Delete Book", book_path(@book), method: :delete, class: "button danger") %>

Browser showing book details page with a link to "Add a Review"