CodeX
Published in

CodeX

Ruby on Rails and How to Fight the Nested Resources Boss

Originally submitted in July 2020. This article is my Ruby on Rails project blog. I hope it is helpful and inspiring for you.

Yes, this is how I called my Rails Project Blog title. So it feels like that, like getting to the final boss and having a hard time, like a good old video game.

Dr. Wily and everyone I had to choose to confront before him are my childhood’s worst enemy.

For the Rails project, I decided to go ahead and continue with the Small Reviews project. In case this is the first blog post you read from me, Small Reviews is my real-life movie review project. One of my goals in life is Small Reviews (take from the Spanish word reseñita, kind of I would say pequeña reseña rather than reseña in Puerto Rican slang) turning into a website or app for people who share the same passion as me.

In this project, I decided to create the entire CRUD structure using gems Devise for authentication, Omniauth for Facebook users to log in (I decided on Facebook cause I want this to be as close as the real Go-Live thing as possible). There are many gems to choose from. Pick and use them accordingly but be careful not to get too greedy or addicted to them.

After creating the users and allowing them to log on, I proceeded to add the reviews and categories tables to start talking about our favorite movies and categorize them by genres. To add a Nested Resource, what better than adding comments? Like an author getting away with lazy writing by killing a character, I chose to simplify the project by just adding comments. After all, this is all we do on the Internet, comment about anything, right?

Well, nested resources will always feel like a labyrinth. Is scary, but I will get this done. First, let see the review and comment models.

clas Review < ApplicationRecord
belongs_to :user
belongs_to :category
has_many :comments, dependent: :destroy
has_many :users, through: :comments
validates :title, presence: true, length: { in: 3..30 }
validates :content, presence: true, length: { maximum: 250 }
default_scope { order(created_at: :desc)}
end
class Comment < ApplicationRecord
belongs_to :user
belongs_to :review
validates :content, presence: true, length: { maximum: 250 }
end

The reviews will belong to the user, to a category, and will have many comments as well as many users through comments. The comments will belong to the user who wrote it and the review in it is written. Now, let’s take a look at the controllers.

Reviews Controller:

class ReviewsController < ApplicationController
before_action :set_review, only: [:show, :edit, :update, :destroy]
before_action :authorize!, only: [:edit, :destroy]

def index
@reviews = Review.all
end

def new
@review = Review.new
@comment = Comment.new
@comment.review_id = @review.id
#We need to declare the comments in the new action.
end

def create
@review = Review.new(review_params)
@review.user_id = current_user.id
if @review.save
redirect_to review_path(@review)
else
render 'new'
end
end

def show
@comment = Comment.new
#We also need to declare the new comment in the show action.
end

def edit
end

def update
if @review.update(review_params)
redirect_to review_path(@review)
else
render 'edit'
end
end

def destroy
@review.destroy
redirect_to reviews_path
end

private

def set_review
@review = Review.find_by(id: params[:id])
end

def review_params
params.require(:review).permit(:title, :content, :category_id)
end

def authorize!
authorize @review #authorize method using the Pundit gem
end
end

Comments Controller:

class CommentsController < ApplicationController
before_action :get_comment, only: [:edit, :update, :destroy]
before_action :authorize!, only: [:edit, :destroy]
before_action :set_comments, only: [:edit, :update, :destroy]

def new
@comment = Comment.new
end

def create
@comment = Comment.new(comment_params)
@comment.user_id = current_user.id
@comment.review_id = params[:review_id]
#Declare the review id.
@comment.save
redirect_to review_path(@comment.review)
end

def edit
end

def update
@comment.update(comment_params)
redirect_to review_path(@review)
end

def destroy
@comment.destroy
redirect_to review_path(@review)
end

private

def get_comment
@comment = Comment.find_by(id: params[:id])
end

def set_comments
@review = @comment.review
@comments = @review.comments.find_by(id: params[:id])
end

def comment_params
params.require(:comment).permit(:content, :review_id)
end

def authorize!
authorize @comment #authorize method using the Pundit gem
end

end

Now that we’re done with the controllers, let’s work with the code in the views. This is how the comment-related codes will look like in our show review file.

<% if user_signed_in? %>
<%= render partial: 'comments/form' %>
<% end %>

If the user is signed in, he’ll be able to see the comment form rendered from file /views/comments/form.

Here’s the code from that helpful file.

<%= form_for [ @review, @comment ] do |f| %>

<% if @comment.errors.any? %>
<div id="error_explanation">
<h2>
<%= pluralize(@comment.errors.count, "error") %>
prohibited this comment from being saved:
</h2>

<ul>
<% @comment.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>

<p>
<%= f.label :content, "What Do You Think?" %><br/>
<%= f.hidden_field :review_id %>
<%= f.text_area :content %>
</p>
<p>
<%= f.submit 'Submit' %>
</p>
<% end %>

As you can see, that form is iterating the errors and everything. Pretty cool.

Now that we have the form, let’s show those passionate film connoisseurs' comments.

Now that we have the form, let’s show those passionate film connoisseurs’ comments.

<% if @review.comments.present? %>
<h5>Comments:</h5>
<%= render partial: 'reviews/comment', collection: @review.comments %>
<% end %>

Here, we are stating that reviews will be shown if they’re created. We don’t want that Review page to look ugly with Comments: header with nothing. So we are rendering from a partial file in the views/reviews directory. Let’s take a look at how we are iterating the data from that file.

<div>
<p><strong><%= comment.user.username %></strong><br>
<%= comment.content %></p>
<% if user_signed_in? && (current_user.wrote_this(comment) || current_user.admin) %>
<%= link_to 'edit', edit_review_comment_path(@review, comment) %>
<%= link_to 'delete', comment_path(comment), method: :delete, data: { confirm: 'Are you sure?' } %>
<% end %>
</div>

The last touch I gave is another statement. If the user is signed in and is the one who wrote the comment or an admin, the user will have access to either edit it and delete it. A good friend of mine showed me how to create that method in the Usermodel and I’ll pay it forward by showing it to you.

def wrote_this(obj)
if obj.class == Review && obj.user_id == self.id
true
elsif obj.class == Comment && obj.user_id == self.id
true
else
false
end
end

Pretty neat, huh? Finally, let’s do the most important thing when creating the nested resource. The one thing that made that resource….nested. Let’s go to the routes file and add the following.

resources :reviews do
resources :comments
end

You can even modify it by adding only:[:show, :new, :create, :edit, :update, :destroy] however you like or what your project requires to function as expected.

As you can see, there’s a lot of steps. However, the result will be rewarding, and it’s going to be an awesome experience to get it done. Remember to plan your models and the way you want them to interact ahead with flowchart diagrams or maybe even on a whiteboard. Planning is part of any project.

Code line by code line, you’ll finally be able to beat the boss. There’s no guarantee the princess will be in that castle.

Summary:

  1. Ruby on Rails project planning.
  2. Nested Resources.
  3. Models.
  4. Controllers.
  5. Views using ERB code.
  6. Routes.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Norberto Santiago

Norberto Santiago

Norberto Santiago is a bilingual full-stack developer. Currently living in the Twin Cities. https://www.norbertosantiago.com/