A Second Resource

Adding Comments to Our App

Now that we have all the CRUD actions working for posts and we have a Post model, let's add on to our web app's functionality by adding post comments.

Each post will be able to have many comments, so let's start off by giving ourselves some comments in our DB to play with. This process will be the same as it was with posts, we'll:

  1. create a new table (define a structure)
  2. populate that table with rows (place data in that structure)

Our comments will also have id, body, author, and created_at columns, just like our posts. But there's another thing we'll have to keep track of for each comment: what post does the comment belong to?

In relational databases, we keep track of this using a foreign key.

For both tables, our id column holds the primary key for each row, that is, a value that uniquely identifies that row within that table. A foreign key is called "foreign" because it references a row in a different ("foreign") table.

Since our comments belong to a post, we'll follow Rails' convention and name this foreign key column post_id. The integer held in this column will be the ID of the post row that a given comment row belongs to.

But our process for creating the table and populating it with data remains the same as it was with posts. First we have some SQL to create the table:

-- db/comments.sql --

CREATE TABLE "comments" (
  "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
  "body" text,
  "author" varchar,
  "post_id" integer,
  "created_at" datetime NOT NULL);

And some CSV we'll import to create some comments, three for each post:

db/comments.csv

1,Lorem ipsum dolor sit amet.,Matz,1,2014-12-09
2,Lorem ipsum dolor sit amet.,DHH,1,2014-12-09
3,Lorem ipsum dolor sit amet.,tenderlove,1,2014-12-09
4,Lorem ipsum dolor sit amet.,Matz,2,2014-12-09
5,Lorem ipsum dolor sit amet.,DHH,2,2014-12-09
6,Lorem ipsum dolor sit amet.,tenderlove,2,2014-12-09
7,Lorem ipsum dolor sit amet.,Matz,3,2014-12-09
8,Lorem ipsum dolor sit amet.,DHH,3,2014-12-09
9,Lorem ipsum dolor sit amet.,tenderlove,3,2014-12-09

Notice the post_id values for each comment. These are after the author name and before the date.

First we run the SQL against our db/development.sqlite3 database:

$ sqlite3 db/development.sqlite3 < db/comments.sql

Then we can use the sqlite3 console to see that we have a comments table now, but no rows within:

$ sqlite3 db/development.sqlite3

sqlite> .tables
comments posts

sqlite> SELECT * FROM comments;
sqlite>

And lastly, we'll import our comments CSV:

sqlite> .mode csv
sqlite> .import db/comments.csv comments

sqlite> SELECT * FROM comments;
8,"Lorem ipsum dolor sit amet.",DHH,3,2014-12-09
7,"Lorem ipsum dolor sit amet.",Matz,3,2014-12-09
6,"Lorem ipsum dolor sit amet.",tenderlove,2,2014-12-09
5,"Lorem ipsum dolor sit amet.",DHH,2,2014-12-09
4,"Lorem ipsum dolor sit amet.",Matz,2,2014-12-09
3,"Lorem ipsum dolor sit amet.",tenderlove,1,2014-12-09
2,"Lorem ipsum dolor sit amet.",DHH,1,2014-12-09
1,"Lorem ipsum dolor sit amet.",Matz,1,2014-12-09

And now we have comments in our database!

Displaying Comments

Now that we have rows of comments in a comments table in our database, let's show the comments that belong to a post in our show_post view.

We'll do this in two steps:

  1. collect the comments from the DB for a post inside the controller action and pass them to the view
  2. display the comments in the view

Let's start with the show_post action changes:

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base

  # ...

  def show_post
    post     = Post.find(params['id'])
    comments = connection.execute('SELECT * FROM comments WHERE comments.post_id = ?', params['id'])

    render 'application/show_post', locals: { post: post, comments: comments }
  end

  # ...

end

Here we simply find our comments by specifying in our query:

WHERE comments.post_id = ?

Where params['id'] (the post's ID) is substituted for the ?. This where clause is how we only pull comments that are "associated" to a post.

Then we pass the resulting hash along into our view using locals, so let's now make use of comments in our show_post view:

app/views/application/show_post.html.erb

<html>
  <body>
    <div class="post">
      <!-- post title, meta data, and body displayed here -->
      <br /><br />
      <div class="comments">
        <h3>Comments:</h3>
        <hr />
        <% comments.each_with_index do |comment, index| %>
          <div class="comment">
            <small class="comment_meta">
              <span class="comment_author">#<%= index+1 %> by <%= comment['author'] %> -</span>
              <em class="comment_created_at"><%= comment['created_at'] %></em>
            </small>
            <p class="comment_body"><%= comment['body'] %></p>
          </div>
          <hr />
        <% end %>
      </div>
    </div>
    <br />
    <a href="/list_posts">Back to Posts</a>

  </body>
</html>

Here we loop again, using <% %> instead of <%= %>, just like we did with our post titles in list_posts, but this time we use each_with_index so that we can number our comments. Just as a quick Ruby-only illustration of how this works, here's what each_with_index can do:

['a','b','c'].each_with_index do |char, idx|
  puts "##{idx+1}: #{char}"  
end  

# prints:
#
# #1: a
# #2: b
# #3: c

So now if we visit /show_post/1, we'll see the comments for the post as well.

NOTE: As a reminder, if you ended up deleting any of the three original posts in a previous assignment, you will not be able to see the comments associated with the deleted post(s).

A Comment Model

Just like the way we have a Post model for the posts, let's create a Comment model for the comments:

app/models/comment.rb

class Comment
  attr_reader :id, :body, :author, :post_id, :created_at

  def initialize(attributes={})
    @id = attributes['id'] if new_record?
    @body = attributes['body']
    @author = attributes['author']
    @post_id = attributes['post_id']
    @created_at ||= attributes['created_at']
  end

  def new_record?
    @id.nil?
  end
end

Now we have the beginning of a Comment model. The file app/models/comment.rb contains an initialize and new_record? method similar to those found in Post.