Ruby on Rails - Building a Reddit-like Commenting System with Rails - ruby on rails tutorial - rails guides - rails tutorial - ruby rails
What is Reddit?
- Reddit is a website in which a community of registered users (redditors) submits content.
- Its format resembles a traditional bulletin board system allowing users to post messages and links to other websites and comment on each other's posts.
- Entries are ranked by a voting system; other users can vote comments and posts either up (upvoted) or down.
- Users who post and comment receive "karma" for upvotes and lose karma for downvotes.
Approach:
- Massive platforms like YouTube and reddit are driven by their threaded commenting features.
- Reddit as the starting point for this tutorial. Assume your project involves someone submitting a story link to share on the site.
- Other users can then comment directly on that story, or comment on another user’s comment.
- To build this, you could also use one model, with one column for “story_id” and one for “comment_id” (meaning one column would always be nil). Or you could use polymorphic associations.
- With a polymorphic association, we’ll create one Comments model. There will then be one column for “commentable_id”, which will store the ID of the object we’re commenting on, and then “commentable_type”, which will indicate what type of object we’re commenting on (in this case, a Story or a Comment).
Models:
- we’ll be starting with a brand-new Rails app (using Rails 4). We’re only going to add what we need to demonstrate this feature, so for starters, let’s create the stories model.
- This is what users submit to our Reddit-like site, so it’s just a Title and URL (that the title would link out to).
rails g model story title:text url:string
rake db:migrate
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
- So now we have the objects that make the core of our user experience.
- We decided to use “text” for the title, so we’re not limited by length (string is limited to 255 characters). Now let’s add the model for comments.
rails g model comment body:text commentable_id:integer commentable_type:string
rake db:migrate
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
- Now that we have these two models created, we need to create the associations between them.
- The easiest place to start is with Stories. A story can have many comments, so we need to add that.
- However, Rails would normally assume that comments would include a column called “story_id” which it doesn’t so we need include the name we gave to the polymorphic association:
app/models/story.rb
class Story < ActiveRecord::Base
has_many :comments, as: :commentable
end
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
apps/models.comment.rb
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
has_many :comments, as :commentable
end
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
- So, to recap, a Story can have many comments. A Comment can have many comments.
- And since a comment can belong to more than one model, we specify that a comment belongs to a polymorphic association through commentable.
- Now let’s see how this works. Fire up your trusty Rails console:
rails c
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
- Since we have no entries in our database for our Reddit-like site, let’s create one:
Story.create(title: "Live 1:1 help from expert developers", url: "http://www.codementor.io")
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
- You should then see this entry be created:
INSERT INTO "stories" ("title", "url", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["title", "Live 1:1 help from expert developers"], ["url", "http://www.codementor.io"], ["created_at", "2016-01-11 22:02:54.637379"], ["updated_at", "2016-01-11 22:02:54.637379"]]
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
- We then want to add a comment to that story, to make sure our associations work properly.
- Let’s enter it through the comments association with the story, to mimic if we were to leave a comment on a story on the story’s show page.
- We can just use Story.first since it’s the only entry in our database.
Story.first.comments.create(body: "What a helpful resource!")
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
And we should see the following:
INSERT INTO "comments" ("body", "commentable_id", "commentable_type", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["body", "What a helpful resource"], ["commentable_id", 1], ["commentable_type", "Story"], ["created_at", "2016-01-11 22:08:03.701599"], ["updated_at", "2016-01-11 22:08:03.701599"]]
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
- You’ll see that Rails knew that the commentable_type was Story, since it was a comment on a story, and that it used the story_id (1, since this was our first entry) as the commentable_id.
- Now let’s add a comment to that first comment, creating our first threaded/nested comment.
- We could do it the same way:
Comment.first.comments.create(body: "I agree, very helpful!")
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
- And we should see the following:
INSERT INTO "comments" ("commentable_id", "commentable_type", "body", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["commentable_id", 1], ["commentable_type", "Comment"], ["body", "I agree, very helpful"], ["created_at", "2016-01-11 22:33:35.018312"], ["updated_at", "2016-01-11 22:33:35.018312"]]
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
Routes:
- As the traffic cop for Rails, we have to tell our routes.rb what to do with web requests.
- First we’ll tell the app to use the index page for stories as the root for this site. Then we’ll add the resources for stories and comments.
- This will create all of the default actions (index, show, new, edit, create, update and destroy) for each controller.
- But to each, we’ll also add a nested resource, to nest those actions for comments within each set of routes. So it looks like this:
config/routes.rb
Rails.application.routes.draw do
root 'stories#index'
resources :stories do
resources :comments
end
resources :comments do
resources :comments
end
end
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
- Let’s see if everything is routing correctly. Since we didn’t limit the default routes, we’ll see more than we need (for instance, we won’t have a comments Index page), but this will help you see how it’s all nested. In the Terminal, check your routes with:
rake routes
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
- This is what you should see:
Prefix Verb URI Pattern Controller#Action
root GET / stories#index
story_comments GET /stories/:story_id/comments(.:format) comments#index
POST /stories/:story_id/comments(.:format) comments#create
new_story_comment GET /stories/:story_id/comments/new(.:format) comments#new
edit_story_comment GET /stories/:story_id/comments/:id/edit(.:format) comments#edit
story_comment GET /stories/:story_id/comments/:id(.:format) comments#show
PATCH /stories/:story_id/comments/:id(.:format) comments#update
PUT /stories/:story_id/comments/:id(.:format) comments#update
DELETE /stories/:story_id/comments/:id(.:format) comments#destroy
stories GET /stories(.:format) stories#index
POST /stories(.:format) stories#create
new_story GET /stories/new(.:format) stories#new
edit_story GET /stories/:id/edit(.:format) stories#edit
story GET /stories/:id(.:format) stories#show
PATCH /stories/:id(.:format) stories#update
PUT /stories/:id(.:format) stories#update
DELETE /stories/:id(.:format) stories#destroy
comment_comments GET /comments/:comment_id/comments(.:format) comments#index
POST /comments/:comment_id/comments(.:format) comments#create
new_comment_comment GET /comments/:comment_id/comments/new(.:format) comments#new
edit_comment_comment GET /comments/:comment_id/comments/:id/edit(.:format) comments#edit
comment_comment GET /comments/:comment_id/comments/:id(.:format) comments#show
PATCH /comments/:comment_id/comments/:id(.:format) comments#update
PUT /comments/:comment_id/comments/:id(.:format) comments#update
DELETE /comments/:comment_id/comments/:id(.:format) comments#destroy
comments GET /comments(.:format) comments#index
POST /comments(.:format) comments#create
new_comment GET /comments/new(.:format) comments#new
edit_comment GET /comments/:id/edit(.:format) comments#edit
comment GET /comments/:id(.:format) comments#show
PATCH /comments/:id(.:format) comments#update
PUT /comments/:id(.:format) comments#update
DELETE /comments/:id(.:format) comments#destroy
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
- Success again! You’ll see there are stories, there are comments, then there are comments nested within stories, and comments nested within comments.
- Now that the requests are going where they need to go, let’s make sure we’re providing the proper response for each.
Controllers:
- As we’re dealing with stories and comments, we’ll need to create a controller for each. We’ll start with stories.
- For the sake of brevity in this tutorial, we’re going to skip creating, editing, or deleting stories, since we have at least one to already work with.
- So we’ll just handle the index and show requests, so that we can view the story we created. Notice that we don’t have to address comments at all here.
app/controllers/stories_controller.rb
class StoriesController < ApplicationController
def index
@stories = Story.all
end
def show
@story = Story.find(params[:id])
end
end
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
- Now we move onto the comments controller. Here we don’t need index and show, as comments always live on story view pages, and don’t have their own pages.
- But we’ll need new and create, because we want to create new comments.
- We also need to create a method to let Rails know if we’re creating a comment for a story or for a comment.
app/controllers/comments_controller.rb
class CommentsController < ApplicationController
before_action :find_commentable
def new
@comment = Comment.new
end
def create
@comment = @commentable.comments.new comment_params
if @comment.save
redirect_to :back, notice: 'Your comment was successfully posted!'
else
redirect_to :back, notice: "Your comment wasn't posted!"
end
end
private
def comment_params
params.require(:comment).permit(:body)
end
def find_commentable
@commentable = Comment.find_by_id(params[:comment_id]) if params[:comment_id]
@commentable = Story.find_by_id(params[:story_id]) if params[:story_id]
end
end
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
- Since our comments are nested within other comments or stories, we’re using the instance variable @commentable in the create action.
- We have a private method (find_commentable) that is telling Rails that if the params contains a comment_id, it’s a comment on a comment, and if it has story_id, it’s a comment on a story.
- We then added a filter at the top of the controller, telling Rails to run the private method before performing any other action (otherwise it wouldn’t know what @commentable was when it got to the create action).
- Lastly, we need to create the views to see all this magic we created work.
Views:
- There are four elements we need for all of this to work.
- We need a show page for a story, and index page for stories, a way to see comments, and a way to post comments.
- Let’s start with the index page and show page.
- The index page will display all of the stories in our database, along with a link to the show page for each story.
app/views/stories/index.html.erb:
<h1>Stories</h1>
<% @stories.each do |story| %>
<p>
<%= link_to(story.title, story.url, target: "_blank") %> - <%= link_to("Show Page", story) %>
</p>
<% end %>
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
- Now we need the show page for each story. Inspired by a show page on reddit, this page will have details about the story, a form for submitting a comment about the story, the display of each comment, and the ability to comment on a comment.
app/views/stories/show.html.erb
<%= link_to(@story.title, @story.url, target: "_blank") %><br/>
<small>Submitted <%= time_ago_in_words(@story.created_at) %> ago</small>
<h3>Comments</h3>
<%= form_for [@story, Comment.new] do |f| %>
<%= f.text_area :body, placeholder: "Add a comment" %><br/>
<%= f.submit "Add Comment" %>
<% end %>
<ul>
<%= render(partial: 'comments/comment', collection: @story.comments) %>
</ul>
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
- You’ll see that we broke out some elements into a partial. So, we need to add that view file.
apps/views/comments/_comment.html.erb
<li>
<%= comment.body %> -
<small>Submitted <%= time_ago_in_words(comment.created_at) %> ago</small>
<%= form_for [comment, Comment.new] do |f| %>
<%= f.text_area :body, placeholder: "Add a Reply" %><br/>
<%= f.submit "Reply" %>
<% end %>
<ul>
<%= render partial: 'comments/comment', collection: comment.comments %>
</ul>
</li>
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
- Because we’re passing this partial the collection of comments, it displays each comment and the form for replying to that comment.
- But then it renders itself within itself (recursive!), to display the replies each comment might have.
- And by using the ul/li structure, we’re making sure they all nest correctly when they display. Fancy.
See Our Handiwork in Action:
- Fire up the server, and take a look at what we did.
rails s
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
- Because we created a story, a comment, and a reply already, so you should see them all.
- Then play around with adding a new comment on that story, and then a reply to that comment.
- If you add the new and create actions and new view to Stories, you will basically have the core functionality of reddit built.