Threading on polymorphic comments - ruby-on-rails

I've setup two models that are commentable through same comments table:
My comments schema:
create_table "comments", force: true do |t|
t.text "body"
t.integer "commentable_id"
t.string "commentable_type"
t.integer "user_id"
t.datetime "created_at"
t.datetime "updated_at"
end
My comment model:
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
belongs_to :user
acts_as_votable
end
My movie model
class Movie < ActiveRecord::Base
belongs_to :user
has_many :comments, as: :commentable
end
My book model:
class Book < ActiveRecord::Base
belongs_to :user
has_many :comments, as: :commentable
end
My comments controller:
def index
#commentable = find_commentable
#comments = #commentable.comments
end
def create
#commentable = find_commentable
#comment = #commentable.comments.build(params[:comment])
#comment.user = current_user
if #comment.save
flash[:notice] = "Successfully created comment."
redirect_to #commentable
else
render :action => 'new'
end
end
def upvote_movie
#movie = Movie.find(params[:movie_id])
#comment = #movie.comments.find(params[:id])
#comment.liked_by current_user
respond_to do |format|
format.html {redirect_to :back}
end
end
def upvote_book
#book = Book.find(params[:book_id])
#comment = #book.comments.find(params[:id])
#comment.liked_by current_user
respond_to do |format|
format.html {redirect_to :back}
end
end
private
def find_commentable
params[:commentable_type].constantize.find(params[:commentable_id])
end
end
How can I add threading(reply to comments) to what I already have?
Here is a blog that talks about threading:http://www.davychiu.com/blog/threaded-comments-in-ruby-on-rails.html
I'm just not sure how to put the two together.
Here is what I have in my movie show view:
<%= render partial: "comments/form", locals: { commentable: #movie } %>
<% #comments.each do |comment| %>
<hr>
<p>
<strong><%= link_to comment.user.username, user_path(comment.user), :class => "user" %>
</strong> <a><%= "(#{time_ago_in_words(comment.created_at)} ago)" %></a>
</p>
<p>
<%= simple_format(auto_link(comment.body, :html => { :target => '_blank' } )) %>
<% end %>
Here is what my comment form looks like:
<%= form_for [commentable, Comment.new] do |f| %>
<%= hidden_field_tag :commentable_type, commentable.class.to_s %>
<%= hidden_field_tag :commentable_id, commentable.id %>
<p>
<%= f.text_area :body %>
</p>
<p><%= f.submit "Submit" %></p>
<% end %>

I scanned the article you mentioned and found the solution there is quite limited.
The basic idea in the article is to set a comment itself as commentable. So a nested comment is actually NOT a comment of the post, but of the parent comment.
The drawbacks are apparent and unacceptable:
It's hard to get other things right. For example, posts.comments.size is no long correct.
You'll have hard dependency on this structure. If in one day you don't want to display comments in thread but plainly, you...will kick a stone.
If you want to do it on current comment system, it's hard.
Actually a simple solution could solve the problem:
Add an extra field reply_to to comment model, referring to other comment's id.
When adding comment, add a reply_to id if it replied to one.
When showing, show a list of all comments with reply_to null.
Then for each comment, show nested comments has its id. And do it recursively.
If you want to limit the nested level, you can add an extra nested_level field, getting in from the front-end. If nest limit is 3, no comments is allowed to reply a comment with nest level of 3.
add: demo helper to render recursively
def render_replied_comments(comment)
if comment.has_reply
comments.replies.each do |reply|
render partial: 'comment', locals: {comment: reply}
render_replied_comment(reply)
end
end
end
# View
#post.top_level_comments.each do |comment|
render partial: 'comment', locals: {comment: comment}
end

You would add a parent_id to the comments model that is a self-referencing relationship. So parent comments would have a parent_id of nil and all child comments would have a parent_id of that parent comment. You are essentially constructing a tree.
The Ancestory Gem is ideal for this or roll your own, good learning experience.

Related

Creating a comment reply won't work while comment works Rails 7

I am trying to allow reply to comments and for some reason when I create a comment it works uneventfully but when I reply the "body" on params comes back empty. The weird part is that I am using the same form. Please take a look:
Note: I am using Action Text rich_text_area, if I use a simple text_area it works just fine.
models/comment.rb
class Comment < ApplicationRecord
belongs_to :post
belongs_to :user
belongs_to :parent, class_name: "Comment", optional: true
has_many :comments, class_name: "Comment", foreign_key: :parent_id
has_rich_text :body
validates_presence_of :body
end
routes.rb
resources :posts do
resources :comments, only: [:create, :update, :destroy]
end
comments_controller.rb
def create
#post = Post.find(params[:post_id])
#comment = #post.comments.build(comment_params)
#comment.user = current_user
respond_to do |format|
format.html do
if #comment.save
flash[:success] = "Comment was successfully created."
else
flash[:danger] = #comment.errors.full_messages.to_sentence
end
redirect_to post_url(#post)
end
end
end
private
def comment_params
params.require(:comment).permit(:body, :parent_id)
end
_comment.rb
// this works (creating a comment without parent)
= render "comments/form", post: #post, comment: #post.comments.build, submit_label: "Comment"
// this won't work (creating a comment with a parent being the current comment)
= render "comments/form", post: #post, comment: #post.comments.build, parent: comment, submit_label: "Reply"
- _form.rb
<%= form_with model: [post, comment], class: "form" do |f| %>
<% if !parent.nil? %>
<%= f.hidden_field :parent_id, value: parent.id %>
<% end %>
<div class="field">
<%= f.rich_text_area :body, placeholder: "What do you think?" %>
</div>
<div class="actions">
<%= f.submit submit_label, class: "btn btn-primary" %>
</div>
<% end %>
What puzzles me the most is that the problem is that the body comes back empty. I cant't see the relation between the body and everything else.
Parameters: {"authenticity_token"=>"[FILTERED]", "comment"=>{"body"=>"", "parent_id"=>"14"}, "commit"=>"Reply", "post_id"=>"4"}
Thanks in advance!

I cannot make a function to delete likes

I try to make simple voting system for comments.
Just one button "voteup", and if the user has already clicked it, it changes to "delete vote". And everything seems to work, except for the voice removal feature. If I click "delete vote" an error Couldn't find Post with 'id'=11 appears.
And I do not understand why this is so, because this is, in fact, one method that is used both for voting and for removing one's vote. Only in one case does everything work, but in the other not.
votes_controller:
class VotesController < ApplicationController
before_action :find_comment
before_action :find_vote, only: [:destroy]
def create
if already_voted?
flash[:notice] = "You can't like more than once"
else
#comment.votes.create(author_id: current_author.id)
end
redirect_to post_path(#post)
end
def destroy
if !(already_voted?)
flash[:notice] = "Cannot unlike"
else
#vote.destroy
end
redirect_to post_path(#post)
end
private
def find_comment
#post = Post.find(params[:post_id])
#comment = Comment.find(params[:comment_id])
end
def already_voted?
Vote.where(author_id: current_author.id, comment_id:
params[:comment_id]).exists?
end
def find_vote
#vote = #comment.votes.find(params[:id])
end
end
Votes elements in _comment.html.erb:
<% pre_vote = comment.votes.find { |vote| vote.author_id == current_author.id} %>
<% if pre_vote %>
<%= button_to 'Delete Vote', post_comment_vote_path(comment, pre_vote), method: :delete %>
<% else %>
<%= button_to 'UpVote', post_comment_votes_path(post, comment), method: :post %>
<% end %>
<p><%= comment.votes.count %> <%= (comment.votes.count) == 1 ? 'Like' : 'Likes'%></p>
UPD
This post has id - 3, not 11.
The comment has id 11.
For some reason, it confused everything during the removal of like.
UPD 2
Migration:
def change
create_table :votes do |t|
t.references :comment, null: false, foreign_key: true
t.references :author, null: false, foreign_key: true
t.timestamps
end
end
vote.rb :
class Vote < ApplicationRecord
belongs_to :comment
belongs_to :author
end
comment.rb and author.rb : has_many :votes, dependent: :destroy
It looks like you're missing an argument from your delete link. Try adding post as the first argument:
<%= button_to 'Delete Vote', post_comment_vote_path(post, comment, pre_vote), method: :delete %>

Calling certain comments from the data base

How can I call comments + user information that is specified to the specific post the comment was created under. For example:
/articles/8
new comment created with user_id = 3
Page will show Username + comment + created_at
This is my current code:
Post Show Page
<%= #post.title %>
<%= render '/comments/form' %>
<% #post.user.comments.reverse.each do |comment| %>
<li>
<%= comment.user.email %>
<%= comment.comment %>
</li>
<% end %>
I grab user information associated with the comment but the problem is, it's listing all the comments. How do I make only comments with article_id of 8 for example appear.
Post Controller
def create
#post = Post.new(post_params)
#post.user = current_user
if #post.save!
flash[:notice] = "Successfully created..."
redirect_to posts_path
else
flash[:danger] = "failed to add a post"
render 'new'
end
end
def show
#comment = Comment.new
#comments = Comment.find(params[:id])
end
Comment Controller
def create
#post = Post.find(params[:post_id])
#comment = #post.comments.build(comment_params)
#comment.user = current_user
if #comment.save
flash[:notice] = "Successfully created..."
redirect_to post_path(#post)
else
flash[:alert] = "failed"
redirect_to root_path
end
end
Routes
resources :sessions, only: [:new, :create, :destroy]
resources :users
resources :posts do
resources :comments
end
Schema of the comments
create_table "comments", force: :cascade do |t|
t.text "comment"
t.integer "user_id"
t.integer "post_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
I am assuming that your model looks like
class Comments < ApplicationRecord
belongs_to :user
belongs_to :post
end
class User < ApplicationRecord
has_many :posts
has_many :comments
end
class Post < ApplicationRecord
belongs_to :user
has_many :comments
end
We want to get all the comments for a post so we we can do something like this
# Note that I took the user from this line
<% #post.comments.reverse.each do |comment| %>
<li>
<%= comment.user.email %>
<%= comment.comment %>
</li>
<% end %>
I hope that this should work.

How to get the user that published the comment and time it was created from the database so I can present it to the view

I'm creating a forum. I have successfully created a Post model presenting posts in the html view with the user email and created_at time. I have also created a Comment model for replying to posts. I've been following a tutorial and understand most of it, but am lost in now getting the user and created_at values of the comments from the database so I can display them. Even though I did it with the post model, it's different because I'm using partials that display in the show html view from the Post controller, and it's confusing me that both the post and comments are displaying in the post controller show view. (i.e. the comments don't have their own show view). I'm a newbie. Any help would be appreciated. Thank you.
routes.rb
Rails.application.routes.draw do
devise_for :users
resources :posts do
resources :comments
end
root 'posts#index'
end
Migration for create_comments
class CreateComments < ActiveRecord::Migration[5.0]
def change
create_table :comments do |t|
t.text :comment
t.references :post, foreign_key: true
t.references :user, foreign_key: true
t.timestamps
end
end
end
comments_controller.rb
class CommentsController < ApplicationController
def create
#post = Post.find(params[:post_id])
#comment = #post.comments.create(params[:comment].permit(:comment))
#comment.user = current_user
if #comment.save
redirect_to post_path(#post)
else
render 'new'
end
end
end
_form.html.haml
= simple_form_for([#post, #post.comments.new]) do |f|
= f.input :comment
= f.submit
models/comment.rb
class Comment < ApplicationRecord
belongs_to :post
belongs_to :user
end
show.html.haml
#post_content
%h1= #post.title
- if user_allowed_post
= link_to "Delete", post_path(#post), method: :delete, data: { confirm: "Are you sure you want to delete this?"}, class: "button"
= link_to "Edit", edit_post_path(#post), class: "button"
- else
%br
%br
%br
%p= #post.content
%p
Published
= time_ago_in_words(#post.created_at)
by
= #post.user.email
#comments
%h2
- if #post.comments.size == 1
= #post.comments.size
Comment
- else
= #post.comments.size
Comments
= render #post.comments
%h3 Reply to thread
= render "comments/form"
Let me know if you need any other files or info.
A local variable comment will be available to you in your partial _comment.html.haml when you specify = render #post.comments .
You should be able to do = comment.user and = comment.created_at in the partial.

Comments on multiple models

Within my rails app, I currently have comments setup to work with my posts model, which is functioning properly. How do I add comments to my books model?
Here is what I have so far:
Here is what I have in my schema for the comments:
create_table "comments", force: true do |t|
t.text "body"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "user_id"
t.integer "post_id"
t.integer "book_id"
end
In my user model:
class User < ActiveRecord::Base
has_many :comments
acts_as_voter
end
In my post model:
class Post < ActiveRecord::Base
has_many :comments
end
In my book model:
class Book < ActiveRecord::Base
has_many :comments
end
In my comment model:
class Comment < ActiveRecord::Base
belongs_to :post
belongs_to :book
belongs_to :user
acts_as_votable
end
In my comments controller:
class CommentsController < ApplicationController
def create
post.comments.create(new_comment_params) do |comment|
comment.user = current_user
end
respond_to do |format|
format.html {redirect_to post_path(post)}
end
end
def upvote
#post = Post.find(params[:post_id])
#comment = #post.comments.find(params[:id])
#comment.liked_by current_user
respond_to do |format|
format.html {redirect_to #post}
end
end
private
def new_comment_params
params.require(:comment).permit(:body)
end
def post
#post = Post.find(params[:post_id])
end
end
In my routes file:
resources :posts do
resources :comments do
member do
put "like", to: "comments#upvote"
end
end
end
In my view:
<% #post.comments.each do |comment| %>
<%= comment.body %>
<% if user_signed_in? && (current_user != comment.user) && !(current_user.voted_for? comment) %>
<%= link_to “up vote”, like_post_comment_path(#post, comment), method: :put %>
<%= comment.votes.size %>
<% else %>
<%= comment.votes.size %></a>
<% end %>
<% end %>
<br />
<%= form_for([#post, #post.comments.build]) do |f| %>
<p><%= f.text_area :body, :cols => "80", :rows => "10" %></p>
<p><%= f.submit “comment” %></p>
<% end %>
What do I add to my comments controller to get comments working on both posts and books? What do I add to my routes file?
Thanks in advance for any help.
You don't want to specify each type of object that can hold Comment objects. That creates a headache of if-elsif-else blocks all over the place. Instead, you want things to be Commentable, and they all will have .comments on them.
This is called a polymorphic association in Active Record. So you would have your models something like:
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
end
class Post < ActiveRecord::Base
has_many :comments, as: :commentable
end
class Book < ActiveRecord::Base
has_many :comments, as: :commentable
end
And modify your database accordingly, it's all in the linked article. Now when you build a Comment object for a form, it will have pre-populated a commentable_id and commentable_type, which you can toss in hidden fields. Now it doesn't matter what the Comment is associated with, you always treat it the same.
I'd leave User as a separate association, since it's not really the same idea.

Resources