discourse/plugins/discourse-post-voting/app/controllers/post_voting/votes_controller.rb
Régis HANOL fd0ddd5260
FIX: negative voter count in post voting who-voted popup (#39313)
When a post in a post-voting topic had both upvotes and downvotes,
clicking the vote count to view the voter list could display a "and -N
more users..." label with a negative number.

The root cause was that `calcRemainingCount` used the post's
`post_voting_vote_count` (the net score, i.e. upvotes − downvotes)
instead of the actual total number of voters. With 2 upvotes and 3
downvotes, net score is −1, minus 5 displayed voters = −6. Since −6 is
truthy in JS, the label would render.

The fix adds `total_voters_count` to the `/post_voting/voters` JSON
response so the client has a reliable total to subtract from. The
component now stores this value and uses it in `calcRemainingCount` with
a `Math.max(0, ...)` guard.

Also removes a no-op `{{#if whoVoted}}` template guard that referenced
the imported function (always truthy) instead of a result, and fixes the
voter popup z-index so it renders above subsequent posts.

Ref - t/181822
2026-04-17 16:43:38 +10:00

180 lines
5 KiB
Ruby

# frozen_string_literal: true
module PostVoting
class VotesController < ::ApplicationController
requires_plugin PLUGIN_NAME
before_action :ensure_logged_in
before_action :find_vote_post, only: %i[create destroy voters]
before_action :ensure_can_see_post, only: %i[create destroy voters]
before_action :ensure_post_voting_enabled, only: %i[create destroy]
def create
ensure_can_vote(@post)
if PostVoting::VoteManager.vote(@post, current_user, direction: vote_params[:direction])
render json: success_json
else
render json: failed_json, status: :unprocessable_entity
end
end
def create_comment_vote
comment = find_comment
ensure_can_see_comment!(comment)
ensure_can_vote(comment)
if PostVoting::VoteManager.vote(
comment,
current_user,
direction: PostVotingVote.directions[:up],
)
render json: success_json
else
render json: failed_json, status: :unprocessable_entity
end
end
def destroy
if !Topic.post_voting_votes(@post.topic, current_user).exists?
raise Discourse::InvalidAccess.new(
nil,
nil,
custom_message: "vote.error.user_has_not_voted",
)
end
if !PostVoting::VoteManager.can_undo(@post, current_user)
msg =
I18n.t(
"vote.error.undo_vote_action_window",
count: SiteSetting.post_voting_undo_vote_action_window.to_i,
)
render_json_error(msg, status: 403)
return
end
if PostVoting::VoteManager.remove_vote(@post, current_user)
render json: success_json
else
render json: failed_json, status: :unprocessable_entity
end
end
def destroy_comment_vote
comment = find_comment
ensure_can_see_comment!(comment)
if !PostVotingVote.exists?(votable: comment, user: current_user)
raise Discourse::InvalidAccess.new(
nil,
nil,
custom_message: "vote.error.user_has_not_voted",
)
end
if PostVoting::VoteManager.remove_vote(comment, current_user)
render json: success_json
else
render json: failed_json, status: :unprocessable_entity
end
end
VOTERS_LIMIT = 20
def voters
# TODO: Probably a site setting to hide/show voters
vote_scope = PostVotingVote.where(votable_id: @post.id, votable_type: "Post")
voters =
User
.joins(:post_voting_votes)
.merge(vote_scope)
.order("post_voting_votes.created_at DESC")
.select("users.*", "post_voting_votes.direction")
.limit(VOTERS_LIMIT)
render_json_dump(
voters: serialize_data(voters, BasicVoterSerializer),
total_voters_count: vote_scope.count,
)
end
private
def vote_params
params.permit(:post_id, :comment_id, :direction)
end
def find_vote_post
if params[:vote].present?
post_id = vote_params[:post_id]
else
params.require(:post_id)
post_id = params[:post_id]
end
@post = Post.find_by(id: post_id)
raise Discourse::NotFound unless @post
end
def ensure_can_see_post
@guardian.ensure_can_see!(@post)
end
def ensure_post_voting_enabled
raise Discourse::InvalidAccess.new unless @post.is_post_voting_topic?
end
def find_comment
comment = PostVotingComment.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
def ensure_can_vote(votable)
if votable.user_id == current_user.id
raise_error("post.post_voting.errors.self_voting_not_permitted")
end
if votable.class.name == "Post"
raise_error("post.post_voting.errors.vote_archived_topic") if votable.topic.archived?
raise_error("post.post_voting.errors.vote_closed_topic") if votable.topic.closed?
direction = vote_params[:direction] || PostVotingVote.directions[:up]
if PostVotingVote.exists?(votable: votable, user_id: current_user.id, direction: direction)
raise_error("vote.error.one_vote_per_post")
elsif !PostVoting::VoteManager.can_undo(votable, current_user)
raise_error(
"vote.error.undo_vote_action_window",
{ count: SiteSetting.post_voting_undo_vote_action_window.to_i },
)
end
if votable.class.name == "PostVotingComment" &&
PostVotingVote.exists?(votable: votable, user: current_user)
raise_error("vote.error.one_vote_per_comment")
end
end
end
private
def raise_error(error_message, error_message_params = nil)
raise Discourse::InvalidAccess.new(
nil,
nil,
custom_message: error_message,
custom_message_params: error_message_params,
)
end
end
end