Cleaning Up Our App

Validation for Creating a Comment

Following the way we created validation for posts, let's create validation for comments:

  • The body and author field both have to be present.
  • If validation fails, the user should be brought back to the create comment page with the fields of the comment form populated.
### app/models/comment.rb ###

class Comment
  attr_reader :id, :body, :author, :post_id, :created_at, :errors
  # ...

  def initialize(attributes={})
    # ...

    @errors = {}
  end

  def valid?
    @errors['body']   = "can't be blank" if body.blank?
    @errors['author'] = "can't be blank" if author.blank?
    @errors.empty?
  end

  def new_record?
    @id.nil?
  end

  def save
    return false unless valid?

    if new_record?
      insert
    else
      # update # ...not defined
    end

    true
  end

  # ...

end
### app/models/post.rb ###

class Post

  # ...

  def build_comment(attributes)
    Comment.new(attributes.merge!('post_id' => id))
  end

  def create_comment(attributes)
    comment = build_comment(attributes)
    comment.save
  end

  # ...

end
### app/controllers/application_controller.rb ###

class ApplicationController < ActionController::Base

  # ...

  def create_comment
    post     = Post.find(params['post_id'])
    comments = post.comments
    # post.build_comment to set the post_id
    comment  = post.build_comment('body' => params['body'], 'author' => params['author'])
    if comment.save
      # redirect for success
      redirect_to "/show_post/#{params['post_id']}"
    else
      # render form again with errors for failure
      render 'application/show_post',
        locals: { post: post, comment: comment, comments: comments }
    end
  end

  # ...

end
<!-- app/views/application/show_post.html.erb -->

<html>
  <body>
    <div class="post">
      <!-- ... -->

      <div class="comments">
        <!-- ... -->

        <!-- display errors -->
        <div class="errors">
          <% comment.errors.each do |attribute, error| %>
            <p class="error" style="color: orange">
              <%= attribute %>: <%= error %>
            </p>
          <% end%>
        </div>

        <!-- populate comment <form> with values -->
        <form method="post" action="/create_comment_for_post/<%= post.id %>">

          <label for="body">Comment</label>
          <textarea id="body" name="body"><%= comment.body %></textarea>
          <br /> <br />

          <label for="author">Name</label>
          <input id="author" name="author" type="text" value="<%= comment.author %>"/>
          <br /> <br />

          <input type="submit" value="Add Comment" />

        </form>
        <hr />
      </div>
    </div>

    <!-- ... -->

  </body>
</html>

But now we need to consider a normal request to show_post, one that isn't a result of failed comment validation. In this case, we need to provide the view with a Comment object, because we're now calling comment.errors each time we render it. To set things straight, we'll simply instantiate an empty Comment in show_post and pass it into the view:

### app/controllers/application_controller.rb ###

class ApplicationController < ActionController::Base

  # ...

  def show_post
    post    = Post.find(params['id'])
    comment = Comment.new
    comments = post.comments
    render "application/show_post",
      locals: { post: post, comment: comment, comments: comments }
  end

  # ...

end

And with that, comment validation is complete!

Delete a Comment

Now that we have our Comment validation, lets move on to adding some more Comment related functionality. We're going to make it so we can delete a comment from a Post page.

First let's set up the route necessary to accomplish this.

### config/routes.rb

# ...

post '/list_posts/:post_id/delete_comment/:comment_id' => 'application#delete_comment'

Next, we'll add in a form that lets us delete a comment from a Post.

<!-- app/views/application/show_post.html.erb -->
<html>
  <body>
    <!-- display errors -->
    <div class="post">
      <!-- ... -->

      <div class="comments">
        <% comments.each do |comment| %>
          <!-- display each comment -->

          <form method="post" action="/list_posts/<%= post.id %>/delete_comment/<%=
          comment.id %>">
            <input type="submit" value="Delete Comment" />
          </form>
          <hr />
        <% end %>

        <!-- populate comment <form> with values -->

      </div>
    </div>

    <!-- ... -->

  </body>
</html>

For the time being, we'll use the POST action to do this. We'll show a way to simulate PATCH/PUT and DELETE actions later in this book.

### app/controllers/application_controller.rb ###

class ApplicationController < ActionController::Base

  # ...

  def delete_comment
    post = Post.find(params['post_id'])
    post.delete_comment(params['comment_id'])
    redirect_to "/show_post/#{params['post_id']}"
  end

  # ...

end
### app/models/post.rb ###

class Post

  # ...

  def build_comment(attributes)
    Comment.new(attributes.merge!('post_id' => id))
  end

  def create_comment(attributes)
    comment = build_comment(attributes)
    comment.save
  end

  def delete_comment(comment_id)
    Comment.find(comment_id).destroy
  end

  # ...

end
### app/models/comment.rb
  class Comment
    # ...
    def self.find(id)
      comment_hash = connection.execute("SELECT * FROM comments WHERE comments.id = ? LIMIT 1", id).first
      Comment.new(comment_hash)
    end

    def destroy
      connection.execute "DELETE FROM comments WHERE id = ?", id
    end

    # ...

    def self.connection
      db_connection = SQLite3::Database.new 'db/development.sqlite3'
      db_connection.results_as_hash = true
      db_connection
    end

    def connection
      self.class.connection
    end
  end

Notice the order in which we wrote our code. It was a very top down approach. We start with our route and move onto the view. From the view we went to the controller and wrote an action for deleting a comment. And based on what model related methods we needed, we then proceeded to the Comment model and set up those methods. We're writing the code we want to see and then implementing that code when it is needed. With the code listed above, we can now delete comments!

List Comments

We would also like to have a page that shows all the comments that have been made in our Blog app. Lets do that now. Let's start with the view template.

<!-- app/views/application/list_comments.html.erb -->

<html>
  <body>

    <div class="comments">
      <% comments.each do |comment| %>

        <div class="comment">
          <small class="comment_meta">
            <span class="comment_post_title">
              Comment on
              <strong><%= comment.post.title %></strong>
            </span>

            <span class="comment_author">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>

  </body>
</html>
### config/routes.rb ###

Rails.application.routes.draw do
  # ...
  get  '/list_comments' => 'application#list_comments'
end
### app/controllers/application_controller.rb ###

class ApplicationController < ActionController::Base

  def list_comments
    comments = Comment.all

    render 'application/list_comments', locals: { comments: comments }
  end

  # ...

end

Note, that in our view, list_comments.html.erb we are using the method comment.post. This is one of the methods we need to build in the Comment model.

### app/models/comment.rb ###

class Comment

  # ...

  def self.all
    comment_row_hashes = connection.execute("SELECT * FROM comments")
    comment_row_hashes.map do |comment_row_hash|
      Comment.new(comment_row_hash)
    end
  end

  def post
    Post.find(post_id) # This can be accomplished using an existing method
  end
end

Now we also have a nice view for seeing all comments made in our app. In the next chapter, we'll be cleaning up and consolidating the code we currently have in our application.

NOTE: if you previously deleted any posts that had comments, you may run into errors since we are not yet handling the deletion comments when a post is deleted.