Associating Reviews with Users
Now that you can keep track of logged in users, you can keep track of their actions. A great example where this could be useful is with your reviews. Wouldn’t it be nice to know which users are leaving which reviews?
Has Many Relationships
To track which users are leaving which reviews, we need to establish a relationship between users and reviews.
A user can leave many reviews, and a review can only be written by one user. Does this sound familiar?
You’ve already defined a similar relaltionship between books and reviews.
class Book < ApplicationRecord
has_many :reviews
end
class Review < ApplicationRecord
belongs_to :book
end
A book has many reviews, and a review belongs to a book.
-
Open the
User
class (app/models/user.rb
) in your text editor.Using the
Book
class as an example, updateUser
so a user has many reviews.
-
What do your changes look like? You should have added the following line to
User
:has_many :reviews
-
If you didn’t already, make this change to
User
. -
Save your changes.
1
2
3
4
5
class User < ApplicationRecord
has_secure_password
has_many :reviews
end
Now that we’ve defined a relationship between users and reviews, let’s see what we can do with it on the rails console
.
-
Open Terminal, make sure you’re in the
bookstore
directory, and start therails console
. -
Find your first user and assign it to a variable called
my_first_user
. -
Now, try running
my_first_user.reviews
.Yikes! That looks like a gnarly error… 😅
The
ActiveRecord::StatementInvalid
error is giving you hint to what’s going on. Burried inside the error, you have this message:no such column: reviews.user_id
We’ve defined the relationship between users and reviews on the model level, but we haven’t defined it on the database level.
Since we said a user can have many reviews, Rails is expecting the reviews table to have a
user_id
column. Let’s add it! -
Before you continue, exit the
rails console
.
To add a user_id
column to the reviews
table, we’ll need to create a new migration.
-
In Terminal, run the following command:
rails generate migration add_user_id_to_reviews
This will generate a new timestamped migration file named
AddUserIdtoReviews
. It’s filename will be something along the lines ofdb/migrate/TIMESTAMP_add_user_id_to_reviews.rb
-
The
AddUserIdtoReviews
migration will be very similiar to another migration you’ve created. Can you think of which one?AddUserIdtoReviews
is gonna look a lot likeAddBookIdToReviews
.class AddBookIdToReviews < ActiveRecord::Migration[5.0] def change add_column :reviews, :book_id, :integer end end
You added a
book_id
column to thereviews
table because you wanted to define the relationship between books and reviews on the database level. Remember, a book can have many reviews.We want to do the same thing in
AddUserIdtoReviews
, except we’re adding auser_id
toreviews
. -
Using this as an example, update the
change
method of theAddUserIdtoReviews
migration to add auser_id
column to thereviews
table. Theuser_id
column should be an integer column.
-
What does your change look like? You should’ve added the following line to the
change
method:add_column :reviews, :user_id, :integer
-
If you didn’t already, add this line, save your changes, and run the migration.
Now that we’ve defined the relationship between users and reviews on both the model and the database levels, we can go back to exploring the relationship on the rails console
.
-
Go to Terminal and start the
rails console
. -
Assign your first user to a variable called
my_first_user
. -
Now, try running
my_first_user.reviews
.You get an empty collection because
my_first_user
doesn’t have any reviews. Let’s change that!
-
Assign your first book to a variable called
my_first_book
. -
Now, run the following code to create a new review for
my_first_book
that will also be associated withmy_first_user
.my_first_book.reviews.create(body: "This review was written by a user!", user_id: my_first_user.id)
On the new review, we’re setting a
body
and auser_id
.By setting the
user_id
tomy_first_user
’s id, we’ll know that the review belongs tomy_first_user
.
-
Now that
my_first_user
has a review, try runningmy_first_user.reviews
again.I thought
my_first_user
would have reviews 😔But
my_first_user
does have reviews! -
Do you remember when you assigned the first user in your database to
my_first_user
?my_first_user = User.first
It’s been a while - it was the first thing you did when you entered the
rails console
.my_first_user
is a variable, and it only knows about the data that existed when it was defined. Even though your first user now has a review in the database, the variablemy_first_user
doesn’t know anything about it. -
To update
my_first_user
with the new review data, run:my_first_user.reload
-
Now, try running
my_first_user.reviews
.🎉
Let’s take a closer look at that new review.
-
Run the following code to assign the new review to a variable called
my_last_review
:my_last_review = Review.last
-
my_last_review
should have auser_id
. Try runningmy_last_review.user_id
.This will return the
user_id
of the user who’s associated tomy_last_review
. Since we set it tomy_first_user
’s id, it will most likely be 1. -
Now, let’s try to get the user from the review. Try running
my_last_review.user
.Hmm…that’s an interesting error. Any thoughts on why you got a
NoMethodError
.(The hint is in the error)
You got a
NoMethodError
because we haven’t completely defined the relationship between users and reviews. You defined part of the relationship - a user has many reviews. However, you haven’t defined the relationship between reviews and users.
We need to define the relationship between reviews and users - a review belongs to a user.
-
Open
app/models/reviews.rb
. You might notice another relationship we already defined 😉Since a review also belongs to a book, you previously added this line:
belongs_to :book
Now, we need to do the same thing for users.
-
Update
Review
so a reviewbelongs_to
a user.
1
2
3
class Review < ApplicationRecord
belongs_to :book
end
That probably sounded more challenging than it was 😅
-
You should’ve added this line
belongs_to :user
-
If you didn’t already, add this line to
Review
and save your changes.
1
2
3
4
class Review < ApplicationRecord
belongs_to :book
belongs_to :user
end
Now, let’s see if we can’t get your last review’s user.
-
Go back to the
rails console
and run the following line to pull in your changes.reload!
-
Get your last review again and assign it to
my_last_review
. -
Now, try running
my_last_review.user
. -
Yayay!!!
-
Exit the
rails console
.
Now that we have a way to assign reviews to their users, let’s update your bookstore so we always know who’s leaving reviews.
How Do Users Leave Reviews Again?
Remember, we can assign a review to a user by setting the user_id
on the review. But do you remember how users create reviews?
Users create reviews by submitting forms to the ReviewsController
create
method. Let’s take a look at it.
In your text editor, open the ReviewsController
(app/controllers/reviews_controller.rb
) and find the create
method.
def create
book = Book.find(params[:book_id])
book.reviews.create(review_params)
redirect_to book_path(book)
end
It’s been a while since you added this method, so let’s do a quick review.
In the create
method, you start by finding the book that’s being reviewed.
book = Book.find(params[:book_id])
Then, you create a new review for it using review_params
.
book.reviews.create(review_params)
review_params
is a method that defines which of the form’s fields can be set on the new review.
def review_params
params.require(:review).permit(:body)
end
Using strong parameters, you set up the review_params
method so the form must submit a review
hash. The only field you’re permitting in that hash is the body
field.
Now, we need to update review_params
so we can also set user_id
.
But how do we know what the user_id
should be?
Only logged in users can create new reviews, and we know which user is logged in because their user id is saved in the session as user_id
.
We’ll have to change the review_params
method so it lets us set user_id
to the user_id
saved in the session…
-
In the
ReviewsController
, change thereview_params
method fromdef review_params params.require(:review).permit(:body) end
to
def review_params permitted_params = params.require(:review).permit(:body) permitted_params.merge(user_id: session[:user_id]) end
We’re still using strong parameters to restrict which form fields can get set on a new review, but we’ll assign them to a new variable called
permitted_params
.Then, we’ll use the
merge
method to adduser_id
topermitted_params
.user_id
will be set tosession[:user_id]
. -
Save your changes and start your application’s web server.
-
Now, login and write a new book review!
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
class ReviewsController < ApplicationController
before_action :verify_user_session
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
permitted_params = params.require(:review).permit(:body)
permitted_params.merge(user_id: session[:user_id])
end
def verify_user_session
if session[:user_id].blank?
flash[:alert] = "Please login to continue"
redirect_to new_session_path
end
end
end
-
Did you write a really inspiring review?? 😉
Let’s take a look at it on the
rails console
. -
Stop your application’s web server and start the
rails console
. -
Now, get your last review and assign it to a variable called
my_last_review
. -
Let’s see who write your last review…
Run
my_last_review.user
.Yay! I hope “CatPower” left a good review 😝
-
Exit the
rails console
and restart your application’s web server.
Now that we know who’s leaving reviews, let’s share that information with your bookstore’s visitors.
-
Do you remember where reviews are shown?
In your text editor, open
app/views/books/show.html.erb
. -
Towards the end of the template, you’re showing all of a books reviews in an unordered list. Let’s include review usernames in the unordered list.
Change the reviews unordered list from
<ul> <% @book.reviews.each do |review| %> <li> <%= review.body %> </li> <% end %> </ul>
to
<ul> <% @book.reviews.each do |review| %> <% if(review.user.present?) %> <li> <%= review.user.username %> - <%= review.body %> </li> <% else %> <li> Anonymous - <%= review.body %> </li> <% end %> <% end %> </ul>
We’ll show the usernames for reviews who have users. However, you probably have some older reviews in your database that don’t have users. When that happens, we’ll show “Anonymous” instead.
-
Save your changes and visit a book with reviews. You should see a mix of anonymous and not so anonymous users that have left behind some reviews 😊
-
Try adding new reviews. If you’re still logged in as “CatPower”, she’ll start getting a lot of reviews in her name…
-
When you’re done, 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
34
35
36
37
<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| %>
<% if(review.user.present?) %>
<li> <%= review.user.username %> - <%= review.body %> </li>
<% else %>
<li> Anonymous - <%= review.body %> </li>
<% end %>
<% end %>
</ul>
<% if session[:user_id].present? %>
<%= 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") %>
<% end %>