Following the way we created validation for posts, let's create validation for comments:
### 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!
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!
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.