Skip to content

Commit 38931de

Browse files
Doc voting and comments (#220)
* add up down votes * add comments --------- Co-authored-by: Vijay Swamidass <[email protected]>
1 parent 0020760 commit 38931de

23 files changed

+428
-21
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# frozen_string_literal: true
2+
3+
class CommentsController < ApplicationController
4+
before_action :set_document
5+
before_action :set_comment, only: %i[update destroy]
6+
before_action :authenticate_user!
7+
8+
# POST /documents/:document_id/comments
9+
def create
10+
@comment = @document.comments.build(comment_params)
11+
@comment.user = current_user
12+
13+
authorize @comment
14+
15+
respond_to do |format|
16+
if @comment.save
17+
format.html { redirect_to @document, notice: 'Comment was successfully added.' }
18+
format.json { render json: @comment, status: :created }
19+
format.turbo_stream
20+
else
21+
format.html { redirect_to @document, alert: 'Failed to add comment.' }
22+
format.json { render json: @comment.errors, status: :unprocessable_entity }
23+
end
24+
end
25+
end
26+
27+
# PATCH/PUT /documents/:document_id/comments/:id
28+
def update
29+
authorize @comment
30+
31+
respond_to do |format|
32+
if @comment.update(comment_params)
33+
format.html { redirect_to @document, notice: 'Comment was successfully updated.' }
34+
format.json { render json: @comment }
35+
format.turbo_stream
36+
else
37+
format.html { redirect_to @document, alert: 'Failed to update comment.' }
38+
format.json { render json: @comment.errors, status: :unprocessable_entity }
39+
end
40+
end
41+
end
42+
43+
# DELETE /documents/:document_id/comments/:id
44+
def destroy
45+
authorize @comment
46+
@comment.destroy
47+
48+
respond_to do |format|
49+
format.html { redirect_to @document, notice: 'Comment was successfully deleted.' }
50+
format.json { head :no_content }
51+
format.turbo_stream
52+
end
53+
end
54+
55+
private
56+
57+
def set_document
58+
@document = Document.find(params[:document_id])
59+
end
60+
61+
def set_comment
62+
@comment = @document.comments.find(params[:id])
63+
end
64+
65+
def comment_params
66+
params.require(:comment).permit(:content)
67+
end
68+
69+
def authenticate_user!
70+
redirect_to root_path, alert: 'You must be logged in to comment.' unless current_user
71+
end
72+
end

app/controllers/documents_controller.rb

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,21 @@ def show
2222
@document.undisliked_by current_user, vote_scope: 'flag'
2323
end
2424

25-
redirect_to action: :show if params[:flag] || params[:unflag]
25+
# Handle upvote/downvote functionality (requires user to be logged in)
26+
if params[:upvote] && current_user
27+
authorize @document, :upvote?
28+
@document.liked_by current_user, vote_scope: 'rating'
29+
elsif params[:downvote] && current_user
30+
authorize @document, :downvote?
31+
@document.disliked_by current_user, vote_scope: 'rating'
32+
elsif params[:unvote] && current_user
33+
@document.unliked_by current_user, vote_scope: 'rating'
34+
@document.undisliked_by current_user, vote_scope: 'rating'
35+
end
36+
37+
redirect_to action: :show if params[:flag] || params[:unflag] || params[:upvote] || params[:downvote] || params[:unvote]
2638

2739
@related_docs = related_documents(@document).first(5)
40+
@comments = @document.comments.includes(:user).ordered
2841
end
2942
end

app/models/comment.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
class Comment < ApplicationRecord
2+
belongs_to :document
3+
belongs_to :user
4+
5+
validates :content, presence: true, length: { minimum: 1, maximum: 2000 }
6+
validates :document, presence: true
7+
validates :user, presence: true
8+
9+
scope :ordered, -> { order(created_at: :desc) }
10+
end

app/models/document.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ class Document < ApplicationRecord
66

77
has_many :documents_questions
88
has_many :questions, through: :documents_questions
9+
has_many :comments, dependent: :destroy
910

1011
pg_search_scope :search_by_title_and_document,
1112
against: %i[title document],

app/models/user.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class User < ApplicationRecord
1515

1616
has_many :assistant_users
1717
has_many :assistants, through: :assistant_users
18+
has_many :comments, dependent: :destroy
1819

1920
private
2021

app/policies/comment_policy.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# frozen_string_literal: true
2+
3+
class CommentPolicy < ApplicationPolicy
4+
def create?
5+
user.present?
6+
end
7+
8+
def update?
9+
user.present? && (user == record.user || user.admin?)
10+
end
11+
12+
def destroy?
13+
user.present? && (user == record.user || user.admin?)
14+
end
15+
16+
class Scope < Scope
17+
def resolve
18+
scope.all
19+
end
20+
end
21+
end

app/policies/document_policy.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ def unflag?
2424
!user.nil? # Any logged-in user can unflag their flags
2525
end
2626

27+
def upvote?
28+
!user.nil? # Any logged-in user can upvote documents
29+
end
30+
31+
def downvote?
32+
!user.nil? # Any logged-in user can downvote documents
33+
end
34+
2735
private
2836

2937
def user_is_editor?
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<div id="<%= dom_id(comment) %>" class="my-5 flex <%= current_user == comment.user ? 'justify-start' : 'justify-end' %>">
2+
<div class="w-3/4">
3+
<!-- Name and date outside the bubble -->
4+
<div class="flex justify-between items-center mb-1">
5+
<p class="text-xs text-gray-500 <%= current_user != comment.user ? 'text-right' : '' %>">
6+
<span class="font-semibold"><%= comment.user.email %></span>
7+
| <%= comment.created_at.strftime("%B %d, %Y at %I:%M %p") %>
8+
<% if comment.updated_at != comment.created_at %>
9+
(edited <%= time_ago_in_words(comment.updated_at) %> ago)
10+
<% end %>
11+
</p>
12+
<% if current_user && (current_user == comment.user || current_user.admin?) %>
13+
<div class="flex space-x-2">
14+
<button onclick="toggleEditComment('<%= dom_id(comment) %>')" class="text-xs text-sky-600 hover:text-sky-800">
15+
Edit
16+
</button>
17+
<%= link_to "Delete", document_comment_path(@document, comment),
18+
data: {
19+
turbo_method: :delete,
20+
turbo_confirm: "Are you sure you want to delete this comment?"
21+
},
22+
class: "text-xs text-red-600 hover:text-red-800" %>
23+
</div>
24+
<% end %>
25+
</div>
26+
27+
<!-- Comment bubble -->
28+
<div class="comment-content">
29+
<div class="p-4 rounded-bl-2xl rounded-br-2xl <%= current_user == comment.user ? 'rounded-tr-2xl' : 'rounded-tl-2xl' %> <%= current_user == comment.user ? 'bg-gray-100 text-stone-800' : 'bg-sky-100 text-sky-800' %> border <%= current_user == comment.user ? 'border-stone-200' : 'border-sky-300' %> shadow-sm">
30+
<p class="leading-relaxed"><%= simple_format(comment.content) %></p>
31+
</div>
32+
</div>
33+
34+
<!-- Edit form -->
35+
<% if current_user && (current_user == comment.user || current_user.admin?) %>
36+
<div class="comment-edit-form hidden mt-3">
37+
<div class="p-4 rounded-bl-2xl rounded-br-2xl <%= current_user == comment.user ? 'rounded-tr-2xl' : 'rounded-tl-2xl' %> bg-gray-50 border border-gray-200 shadow-sm">
38+
<%= form_with model: [@document, comment], local: true, class: "space-y-3" do |form| %>
39+
<%= form.text_area :content, rows: 3, class: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent resize-vertical" %>
40+
<div class="flex space-x-2">
41+
<%= form.submit "Update Comment", class: "px-4 py-2 bg-sky-600 text-white rounded-md hover:bg-sky-700 focus:outline-none focus:ring-2 focus:ring-sky-500" %>
42+
<button type="button" onclick="toggleEditComment('<%= dom_id(comment) %>')" class="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-500">
43+
Cancel
44+
</button>
45+
</div>
46+
<% end %>
47+
</div>
48+
</div>
49+
<% end %>
50+
</div>
51+
</div>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<div class="comments-section p-3 mt-3 bg-white rounded-lg border border-gray-200">
2+
<h2 id="comments-count" class="text-2xl font-semibold text-gray-900 mb-6">
3+
Comments (<%= @document.comments.count %>)
4+
</h2>
5+
6+
<div id="comment_form">
7+
<%= render 'comments/form' %>
8+
</div>
9+
10+
<div id="comments-list" class="comments-list">
11+
<% if @document.comments.any? %>
12+
<%= render @document.comments.ordered %>
13+
<% end %>
14+
</div>
15+
</div>
16+
17+
<script>
18+
function toggleEditComment(commentId) {
19+
const comment = document.getElementById(commentId);
20+
const content = comment.querySelector('.comment-content');
21+
const editForm = comment.querySelector('.comment-edit-form');
22+
23+
if (editForm.classList.contains('hidden')) {
24+
content.classList.add('hidden');
25+
editForm.classList.remove('hidden');
26+
} else {
27+
content.classList.remove('hidden');
28+
editForm.classList.add('hidden');
29+
}
30+
}
31+
</script>

app/views/comments/_form.html.erb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<% if current_user %>
2+
<div class="bg-white rounded-lg mb-6">
3+
<%= form_with model: [@document, Comment.new], local: true, class: "space-y-3" do |form| %>
4+
<%= form.text_area :content,
5+
placeholder: "Share your thoughts about this document...",
6+
rows: 4,
7+
class: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent resize-vertical" %>
8+
9+
<div class="flex justify-between items-center">
10+
<p class="text-sm text-gray-500">Maximum 2000 characters</p>
11+
<%= form.submit "Post Comment",
12+
class: "px-6 py-2 bg-sky-600 text-white rounded-md hover:bg-sky-700 focus:outline-none focus:ring-2 focus:ring-sky-500 disabled:opacity-50 disabled:cursor-not-allowed" %>
13+
</div>
14+
<% end %>
15+
</div>
16+
<% else %>
17+
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4 mb-6 text-center">
18+
<p class="text-gray-600">
19+
<%= link_to "Sign in", new_session_path, class: "text-sky-600 hover:text-sky-800 font-medium" %>
20+
to add a comment.
21+
</p>
22+
</div>
23+
<% end %>

0 commit comments

Comments
 (0)