Skip to content

Commit

Permalink
FEATURE: Allow comments to be voted on. (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
tgxworld authored Mar 1, 2022
1 parent c8133a1 commit 4ee4ffc
Show file tree
Hide file tree
Showing 27 changed files with 547 additions and 68 deletions.
56 changes: 52 additions & 4 deletions app/controllers/question_answer/votes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
module QuestionAnswer
class VotesController < ::ApplicationController
before_action :ensure_logged_in
before_action :find_vote_post
before_action :ensure_can_see_post
before_action :find_vote_post, only: [:create, :destroy, :set_as_answer, :voters]
before_action :ensure_can_see_post, only: [:create, :destroy, :set_as_answer, :voters]
before_action :ensure_qa_enabled, only: [:create, :destroy]
before_action :ensure_staff, only: [:set_as_answer]

Expand Down Expand Up @@ -32,6 +32,25 @@ def create
end
end

def create_comment_vote
comment = find_comment
ensure_can_see_comment!(comment)

if QuestionAnswerVote.exists?(votable: comment, user: current_user)
raise Discourse::InvalidAccess.new(
nil,
nil,
custom_message: 'vote.error.one_vote_per_comment'
)
end

if QuestionAnswer::VoteManager.vote(comment, current_user, direction: QuestionAnswerVote.directions[:up])
render json: success_json
else
render json: failed_json, status: 422
end
end

def destroy
if !Topic.qa_votes(@post.topic, current_user).exists?
raise Discourse::InvalidAccess.new(
Expand All @@ -57,6 +76,25 @@ def destroy
end
end

def destroy_comment_vote
comment = find_comment
ensure_can_see_comment!(comment)

if !QuestionAnswerVote.exists?(votable: comment, user: current_user)
raise Discourse::InvalidAccess.new(
nil,
nil,
custom_message: 'vote.error.user_has_not_voted'
)
end

if QuestionAnswer::VoteManager.remove_vote(comment, current_user)
render json: success_json
else
render json: failed_json, status: 422
end
end

def set_as_answer
Post.transaction do
@post.update!(reply_to_post_number: nil)
Expand All @@ -72,7 +110,7 @@ def voters
# TODO: Probably a site setting to hide/show voters
voters = User
.joins(:question_answer_votes)
.where(question_answer_votes: { post_id: @post.id })
.where(question_answer_votes: { votable_id: @post.id, votable_type: 'Post' })
.order("question_answer_votes.created_at DESC")
.select("users.*", "question_answer_votes.direction")
.limit(VOTERS_LIMIT)
Expand All @@ -85,7 +123,7 @@ def voters
private

def vote_params
params.permit(:post_id, :direction)
params.permit(:post_id, :comment_id, :direction)
end

def find_vote_post
Expand All @@ -108,5 +146,15 @@ def ensure_can_see_post
def ensure_qa_enabled
raise Discourse::InvalidAccess.new unless Topic.qa_enabled(@post.topic)
end

def find_comment
comment = QuestionAnswerComment.find_by(id: vote_params[:comment_id])
raise Discourse::NotFound if comment.blank?
comment
end

def ensure_can_see_comment!(comment)
@guardian.ensure_can_see!(comment.post)
end
end
end
2 changes: 2 additions & 0 deletions app/models/question_answer_comment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ class QuestionAnswerComment < ActiveRecord::Base
validate :ensure_can_comment, on: [:create]
before_validation :cook_raw, if: :will_save_change_to_raw?

has_many :votes, class_name: "QuestionAnswerVote", as: :votable, dependent: :delete_all

MARKDOWN_FEATURES = %w{
censored
emoji
Expand Down
24 changes: 21 additions & 3 deletions app/models/question_answer_vote.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
# frozen_string_literal: true

class QuestionAnswerVote < ActiveRecord::Base
belongs_to :post
belongs_to :votable, polymorphic: true
belongs_to :user

VOTABLE_TYPES = %w{Post QuestionAnswerComment}

validates :direction, inclusion: { in: ['up', 'down'] }
validates :post_id, presence: true
validates :votable_type, presence: true, inclusion: { in: VOTABLE_TYPES }
validates :votable_id, presence: true
validates :user_id, presence: true
validate :ensure_valid_post
validate :ensure_valid_post, if: -> { votable_type == 'Post' }
validate :ensure_valid_comment, if: -> { votable_type == 'QuestionAnswerComment' }

def self.directions
@directions ||= {
Expand All @@ -28,7 +32,21 @@ def self.reverse_direction(direction)

private

def ensure_valid_comment
comment = votable

if direction != QuestionAnswerVote.directions[:up]
errors.add(:base, I18n.t("post.qa.errors.comment_cannot_be_downvoted"))
end

if !comment.post.qa_enabled
errors.add(:base, I18n.t("post.qa.errors.qa_not_enabled"))
end
end

def ensure_valid_post
post = votable

if !post.qa_enabled
errors.add(:base, I18n.t("post.qa.errors.qa_not_enabled"))
elsif post.reply_to_post_number.present?
Expand Down
14 changes: 13 additions & 1 deletion app/serializers/question_answer_comment_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ class QuestionAnswerCommentSerializer < ApplicationSerializer
:username,
:created_at,
:raw,
:cooked
:cooked,
:qa_vote_count,
:user_voted

attr_accessor :comments_user_voted

def name
object.user.name
Expand All @@ -16,4 +20,12 @@ def name
def username
object.user.username
end

def user_voted
if @comments_user_voted
@comments_user_voted[object.id]
else
object.votes.exists?(user: scope.user)
end
end
end
76 changes: 74 additions & 2 deletions assets/javascripts/discourse/widgets/qa-comment.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,24 @@ import { h } from "virtual-dom";
import RawHtml from "discourse/widgets/raw-html";
import { dateNode } from "discourse/helpers/node";
import { formatUsername } from "discourse/lib/utilities";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";

export default createWidget("qa-comment", {
tagName: "div.qa-comment",
buildKey: (attrs) => `qa-comment-${attrs.id}`,

buildClasses(attrs) {
return [`qa-comment-${attrs.id}`];
},

sendShowLogin() {
const appRoute = this.register.lookup("route:application");
appRoute.send("showLogin");
},

defaultState() {
return { isEditing: false };
return { isEditing: false, isVoting: false };
},

html(attrs, state) {
Expand Down Expand Up @@ -43,10 +54,71 @@ export default createWidget("qa-comment", {
result.push(this.attach("qa-comment-actions", attrs));
}

return [h("div.qa-comment-post", result)];
return [
h("div.qa-comment-actions-vote", [
h(
"span.qa-comment-actions-vote-count",
`${attrs.qa_vote_count > 0 ? attrs.qa_vote_count : ""}`
),
this.attach("qa-button", {
direction: "up",
loading: state.isVoting,
voted: attrs.user_voted,
}),
]),
h("div.qa-comment-post", result),
];
}
},

removeVote() {
this.state.isVoting = true;

this.attrs.qa_vote_count--;
this.attrs.user_voted = false;

return ajax("/qa/vote/comment", {
type: "DELETE",
data: { comment_id: this.attrs.id },
})
.catch((e) => {
this.attrs.qa_vote_count++;
this.attrs.user_voted = true;
popupAjaxError(e);
})
.finally(() => {
this.state.isVoting = false;
});
},

vote(direction) {
if (!this.currentUser) {
return this.sendShowLogin();
}

if (direction !== "up") {
return;
}

this.state.isVoting = true;

this.attrs.qa_vote_count++;
this.attrs.user_voted = true;

return ajax("/qa/vote/comment", {
type: "POST",
data: { comment_id: this.attrs.id },
})
.catch((e) => {
this.attrs.qa_vote_count--;
this.attrs.user_voted = false;
popupAjaxError(e);
})
.finally(() => {
this.state.isVoting = false;
});
},

expandEditor() {
this.state.isEditing = true;
},
Expand Down
35 changes: 33 additions & 2 deletions assets/stylesheets/common/question-answer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,41 @@
.qa-comment {
font-size: $font-down-1;
border-top: 1px solid var(--primary-low);
display: flex;
flex-direction: row;
padding: 0.7em 0;

.qa-comment-actions-vote {
margin-right: 0.5em;
display: inline-grid;
grid-auto-flow: column;
align-items: center;

.qa-comment-actions-vote-count {
text-align: center;
width: auto;
min-width: 16px;
color: var(--primary-low-mid-or-secondary-high);
}

.qa-comment-post {
padding: 0.7em 0;
.btn {
font-size: var(--font-up-3);
background: none;

&.qa-button-voted {
.d-icon {
color: var(--success);
}
}

.d-icon {
color: var(--primary-low-mid-or-secondary-high);
margin: 0;
}
}
}

.qa-comment-post {
.qa-comment-cooked {
p {
display: inline;
Expand Down
8 changes: 8 additions & 0 deletions assets/stylesheets/mobile/question-answer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,11 @@
.qa-post-toggle-voters {
padding: 0.5em;
}

.qa-comment {
.qa-comment-actions-vote {
.qa-button-upvote {
padding: 0;
}
}
}
2 changes: 2 additions & 0 deletions config/locales/server.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ en:
replying_to_post_not_permited: "You are not allowed to create a post in reply to another post."
qa_not_enabled: "QnA is not enabled."
voting_not_permitted: "You are not allowed to vote on this post."
comment_cannot_be_downvoted: "You can only upvote comments."

site_settings:
qa_tags: "Tags to enable QnA on topic"
Expand Down Expand Up @@ -34,6 +35,7 @@ en:
already_voted: "You can only vote once per question."
undo_vote_action_window: "You can only undo votes %{minutes} after voting."
one_vote_per_post: "You can only vote once for each post."
one_vote_per_comment: "You can only vote once for each comment."

qa:
comment:
Expand Down
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
post "comments" => 'comments#create'
delete "comments" => 'comments#destroy'
put "comments" => 'comments#update'
post 'vote/comment' => 'votes#create_comment_vote'
delete 'vote/comment' => 'votes#destroy_comment_vote'
end

Discourse::Application.routes.append do
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

class AddPolymorphicColumnsToQuestionAnswerVotes < ActiveRecord::Migration[6.1]
def up
add_column :question_answer_votes, :votable_type, :string
add_column :question_answer_votes, :votable_id, :integer

execute <<~SQL
UPDATE question_answer_votes
SET votable_type = 'Post', votable_id = question_answer_votes.post_id
SQL

change_column_null :question_answer_votes, :votable_type, false
change_column_null :question_answer_votes, :votable_id, false

begin
# At this point in time, this plugin has not been publicaly released so just dropping it
Migration::SafeMigrate.disable!
remove_column :question_answer_votes, :post_id
ensure
Migration::SafeMigrate.enable!
end

add_index :question_answer_votes, [:votable_type, :votable_id, :user_id], unique: true, name: 'idx_votable_user_id'
end

def down
raise ActiveRecord::IrreversibleMigration
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

class AddQaVoteCountToQuestionAnswerComments < ActiveRecord::Migration[6.1]
def up
add_column :question_answer_comments, :qa_vote_count, :integer, default: 0
execute "ALTER TABLE question_answer_comments ADD CONSTRAINT qa_vote_count_positive CHECK (qa_vote_count >= 0)"
end

def down
execute "ALTER TABLE question_answer_comments DROP CONSTRAINT qa_vote_count_positive"
remove_column :question_answer_comments, :qa_vote_count
end
end
Loading

0 comments on commit 4ee4ffc

Please sign in to comment.