Skip to content
Permalink
Browse files Browse the repository at this point in the history
FIX: Validate number of votes allowed per poll per user. (#15001)
* DEV: Remove spec that we no longer need.

As far as we know, the migration has been successful for a number of
years.

* FIX: Validate number of votes allowed per poll per user.
  • Loading branch information
tgxworld committed Nov 19, 2021
1 parent 254689b commit 1d0faed
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 339 deletions.
7 changes: 7 additions & 0 deletions plugins/poll/config/locales/server.en.yml
Expand Up @@ -47,6 +47,13 @@ en:

topic_must_be_open_to_vote: "The topic must be open to vote."
poll_must_be_open_to_vote: "Poll must be open to vote."
one_vote_per_user: "Only 1 vote is allowed for this poll."
max_vote_per_user:
one: Only %{count} vote is allowed for this poll.
other: A maximum of %{count} votes is allowed for this poll.
min_vote_per_user:
one: A minimum of %{count} vote is required for this poll.
other: A minimum of %{count} votes is required for this poll.

topic_must_be_open_to_toggle_status: "The topic must be open to toggle status."
only_staff_or_op_can_toggle_status: "Only a staff member or the original poster can toggle a poll status."
Expand Down
44 changes: 43 additions & 1 deletion plugins/poll/lib/poll.rb
Expand Up @@ -2,7 +2,10 @@

class DiscoursePoll::Poll
def self.vote(user, post_id, poll_name, options)
poll_id = nil

serialized_poll = DiscoursePoll::Poll.change_vote(user, post_id, poll_name) do |poll|
poll_id = poll.id
# remove options that aren't available in the poll
available_options = poll.poll_options.map { |o| o.digest }.to_set
options.select! { |o| available_options.include?(o) }
Expand All @@ -13,6 +16,8 @@ def self.vote(user, post_id, poll_name, options)
obj << option.id if options.include?(option.digest)
end

self.validate_votes!(poll, new_option_ids)

old_option_ids = poll.poll_options.each_with_object([]) do |option, obj|
if option.poll_votes.where(user_id: user.id).exists?
obj << option.id
Expand All @@ -31,6 +36,24 @@ def self.vote(user, post_id, poll_name, options)
end
end

# Ensure consistency here as we do not have a unique index to limit the
# number of votes per the poll's configuration.
DB.query(<<~SQL, poll_id: poll_id, user_id: user.id, offset: serialized_poll[:type] == "multiple" ? serialized_poll[:max] : 1)
DELETE FROM poll_votes
USING (
SELECT
poll_id,
user_id
FROM poll_votes
WHERE poll_id = :poll_id
AND user_id = :user_id
ORDER BY created_at DESC
OFFSET :offset
) to_delete_poll_votes
WHERE poll_votes.poll_id = to_delete_poll_votes.poll_id
AND poll_votes.user_id = to_delete_poll_votes.user_id
SQL

[serialized_poll, options]
end

Expand Down Expand Up @@ -288,7 +311,26 @@ def self.extract(raw, topic_id, user_id = nil)
end
end

private
def self.validate_votes!(poll, options)
num_of_options = options.length

if poll.multiple?
if num_of_options < poll.min
raise DiscoursePoll::Error.new(I18n.t(
"poll.min_vote_per_user",
count: poll.min
))
elsif num_of_options > poll.max
raise DiscoursePoll::Error.new(I18n.t(
"poll.max_vote_per_user",
count: poll.max
))
end
elsif num_of_options > 1
raise DiscoursePoll::Error.new(I18n.t("poll.one_vote_per_user"))
end
end
private_class_method :validate_votes!

def self.change_vote(user, post_id, poll_name)
Poll.transaction do
Expand Down

1 comment on commit 1d0faed

@discoursebot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commit has been mentioned on Discourse Meta. There might be relevant details there:

https://meta.discourse.org/t/500-internal-server-error-when-voting-on-a-poll/213480/3

Please sign in to comment.