Adding Reviews from the Browser
Now you can see your beautiful reviews, but wouldn’t it be nice if you could also add reviews??
-
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.
-
In your routes file, change
resources :books
to
resources :books do resources :reviews end
-
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.
-
Open Terminal, go to the
bookstore
directory, and runrake 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.
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.
-
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…
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
!
-
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
. -
Restart your application’s web server, and open
app/controllers/reviews_controller.rb
in your text editor.
-
Now you have a file for the
ReviewsController
, but you still haven’t defined it. -
Add the following code to
app/controllers/reviews_controller.rb
class ReviewsController < ApplicationController end
-
Save your changes and revisit http://localhost:3000/books/1/reviews/new.
1
2
class ReviewsController < ApplicationController
end
Another error, but we know how to deal with these.
Right?!
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!
-
Add an empty
new
method to theReviewsController
.def new end
-
Save your changes and revisit http://localhost:3000/books/1/reviews/new.
1
2
3
4
class ReviewsController < ApplicationController
def new
end
end
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!
-
First, go back to Terminal and stop your application’s web server.
-
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 ofapp/views
. -
Then, you can create the new reviews template with the following command:
touch app/views/reviews/new.html.erb
-
Restart your application’s web server, and try going back to http://localhost:3000/books/1/reviews/new.
What do you think will happen?
Have you ever been so excited to see a blank page?!
Please, contain your excitement!
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.
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.
-
Open the
ReviewsController
in your text editor, and add the following code tonew
method.@book = Book.find(params[:book_id])
This will make the book’s data available in the new review template.
-
Save your changes.
1
2
3
4
5
class ReviewsController < ApplicationController
def new
@book = Book.find(params[:book_id])
end
end
-
Now, open
app/views/reviews/new.html.erb
and add the following line:<h1> New review for <%= @book.title %> </h1>
-
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”.
-
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 😅
-
Open the
ReviewsController
and add the following line at the end of thenew
method.@review = @book.reviews.build
This will create an empty review for the book that we can use in the new review template.
-
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!
-
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 %>
-
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…
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.
-
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 invalidreviews_path
. -
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.
-
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 %>
An Unknown action
error?! But we were so close!!!
Don’t be so down! We’ve gotten through this before…we can fix this.
The error is telling us what we need to do.
The action 'create' could not be found for ReviewsController.
-
Add an empty
create
method to theReviewsController
.def create end
-
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.
-
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. -
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
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
.
-
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 theparams
hash must have areview
hash. Inside thereview
hash, we’ll allow abody
attribute to be used on a new review. -
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 thereview
hash. -
Now we can use
review_params
inside thecreate
method. Replacebook.reviews.create(params)
with
book.reviews.create(review_params)
-
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?
-
Go back to your first book’s details page at http://localhost:3000/books/1.
Notice anything different???
Your new review!!! 🎉
-
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.
-
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.
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.
-
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 aprefix
ofnew_book_review
. We’ll use thisprefix
to build a link to the new review path. -
Restart your application’s web server.
-
Now, open
app/views/books/show.html.erb
in your text editor. -
Find the “Edit book” link.
<%= link_to("Edit book", edit_book_path(@book), class: "button") %>
-
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.
-
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") %>
-
Update your solution to match this solution.
-
Save your changes, find some books, and review them!
-
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") %>