FIX: Poll: ensure it is not possible to vote with all abstentions in Ranked Choice (#29601)
Make sure Cast Vote button is disabled when all abstain and remove any historic data that fails new zero rank vote validation
This commit is contained in:
parent
c7e471d35a
commit
a535798659
|
@ -15,6 +15,7 @@ end
|
|||
# user_id :bigint
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# rank :integer default(0), not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
|
|
@ -72,11 +72,13 @@ export default class PollComponent extends Component {
|
|||
areRanksValid = (arr) => {
|
||||
let ranks = new Set(); // Using a Set to keep track of unique ranks
|
||||
let hasNonZeroDuplicate = false;
|
||||
let allZeros = true;
|
||||
|
||||
arr.forEach((obj) => {
|
||||
const rank = obj.rank;
|
||||
|
||||
if (rank !== 0) {
|
||||
allZeros = false; // Set to false if any rank is non-zero
|
||||
if (ranks.has(rank)) {
|
||||
hasNonZeroDuplicate = true;
|
||||
return; // Exit forEach loop if a non-zero duplicate is found
|
||||
|
@ -85,7 +87,7 @@ export default class PollComponent extends Component {
|
|||
}
|
||||
});
|
||||
|
||||
return !hasNonZeroDuplicate;
|
||||
return !hasNonZeroDuplicate && !allZeros;
|
||||
};
|
||||
|
||||
_toggleOption = (option, rank = 0) => {
|
||||
|
|
|
@ -39,6 +39,7 @@ en:
|
|||
named_poll_with_multiple_choices_has_invalid_parameters: "Poll named <strong>%{name}</strong> with multiple choice has invalid parameters."
|
||||
|
||||
requires_at_least_1_valid_option: "You must select at least 1 valid option."
|
||||
requires_that_at_least_one_option_is_ranked: "In ranked-choice polls, you must rank at least one option."
|
||||
|
||||
edit_window_expired:
|
||||
cannot_edit_default_poll_with_votes: "You cannot change a poll after the first %{minutes} minutes."
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
class DeleteZeroRankUserVoteCollectionsFromPollVotes < ActiveRecord::Migration[7.1]
|
||||
def up
|
||||
execute <<~SQL
|
||||
DELETE FROM poll_votes
|
||||
WHERE (poll_id, user_id) IN (
|
||||
SELECT poll_votes.poll_id, poll_votes.user_id
|
||||
FROM poll_votes
|
||||
JOIN polls ON polls.id = poll_votes.poll_id
|
||||
WHERE polls.type = 3
|
||||
GROUP BY poll_votes.poll_id, poll_votes.user_id
|
||||
HAVING SUM(poll_votes.rank) = 0
|
||||
);
|
||||
SQL
|
||||
end
|
||||
|
||||
def down
|
||||
raise ActiveRecord::IrreversibleMigration
|
||||
end
|
||||
end
|
|
@ -17,6 +17,12 @@ class DiscoursePoll::Poll
|
|||
if poll.ranked_choice?
|
||||
options = options.values.map { |hash| hash }
|
||||
options.select! { |o| available_options.include?(o[:digest]) }
|
||||
|
||||
if options.all? { |o| o[:rank] == "0" }
|
||||
raise DiscoursePoll::Error.new I18n.t(
|
||||
"poll.requires_that_at_least_one_option_is_ranked",
|
||||
)
|
||||
end
|
||||
else
|
||||
options.select! { |o| available_options.include?(o) }
|
||||
end
|
||||
|
|
|
@ -43,6 +43,36 @@ RSpec.describe DiscoursePoll::Poll do
|
|||
end.to raise_error(DiscoursePoll::Error, I18n.t("poll.one_vote_per_user"))
|
||||
end
|
||||
|
||||
it "should not allow a ranked vote with all abstentions" do
|
||||
poll = post_with_ranked_choice_poll.polls.first
|
||||
poll_options = poll.poll_options
|
||||
|
||||
expect do
|
||||
DiscoursePoll::Poll.vote(
|
||||
user,
|
||||
post_with_ranked_choice_poll.id,
|
||||
"poll",
|
||||
{
|
||||
"0": {
|
||||
digest: poll_options.first.digest,
|
||||
rank: "0",
|
||||
},
|
||||
"1": {
|
||||
digest: poll_options.second.digest,
|
||||
rank: "0",
|
||||
},
|
||||
"2": {
|
||||
digest: poll_options.third.digest,
|
||||
rank: "0",
|
||||
},
|
||||
},
|
||||
)
|
||||
end.to raise_error(
|
||||
DiscoursePoll::Error,
|
||||
I18n.t("poll.requires_that_at_least_one_option_is_ranked"),
|
||||
)
|
||||
end
|
||||
|
||||
it "should clean up bad votes for a regular poll" do
|
||||
poll = post_with_regular_poll.polls.first
|
||||
|
||||
|
|
|
@ -41,6 +41,135 @@ module("Poll | Component | poll", function (hooks) {
|
|||
});
|
||||
});
|
||||
|
||||
test("valid ranks with which you can vote", async function (assert) {
|
||||
this.setProperties({
|
||||
post: EmberObject.create({
|
||||
id: 42,
|
||||
topic: {
|
||||
archived: false,
|
||||
},
|
||||
user_id: 29,
|
||||
polls_votes: {
|
||||
poll: [
|
||||
{
|
||||
digest: "1f972d1df351de3ce35a787c89faad29",
|
||||
rank: 1,
|
||||
},
|
||||
{
|
||||
digest: "d7ebc3a9beea2e680815a1e4f57d6db6",
|
||||
rank: 2,
|
||||
},
|
||||
{
|
||||
digest: "6c986ebcde3d5822a6e91a695c388094",
|
||||
rank: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
poll: new TrackedObject({
|
||||
name: "poll",
|
||||
type: "ranked_choice",
|
||||
status: "open",
|
||||
results: "on_close",
|
||||
options: [
|
||||
{
|
||||
id: "1f972d1df351de3ce35a787c89faad29",
|
||||
html: "this",
|
||||
votes: 0,
|
||||
rank: 1,
|
||||
},
|
||||
{
|
||||
id: "d7ebc3a9beea2e680815a1e4f57d6db6",
|
||||
html: "that",
|
||||
votes: 0,
|
||||
rank: 2,
|
||||
},
|
||||
{
|
||||
id: "6c986ebcde3d5822a6e91a695c388094",
|
||||
html: "other",
|
||||
votes: 0,
|
||||
rank: 3,
|
||||
},
|
||||
],
|
||||
voters: 0,
|
||||
chart_type: "bar",
|
||||
}),
|
||||
});
|
||||
|
||||
await render(hbs`<Poll @post={{this.post}} @poll={{this.poll}} />`);
|
||||
|
||||
assert.dom(".poll-buttons .cast-votes:disabled").doesNotExist();
|
||||
assert.dom(".poll-buttons .cast-votes").exists();
|
||||
});
|
||||
|
||||
test("invalid ranks with which you cannot vote", async function (assert) {
|
||||
this.setProperties({
|
||||
post: EmberObject.create({
|
||||
id: 42,
|
||||
topic: {
|
||||
archived: false,
|
||||
},
|
||||
user_id: 29,
|
||||
}),
|
||||
poll: new TrackedObject({
|
||||
name: "poll",
|
||||
type: "ranked_choice",
|
||||
status: "open",
|
||||
results: "always",
|
||||
options: [
|
||||
{
|
||||
id: "1f972d1df351de3ce35a787c89faad29",
|
||||
html: "this",
|
||||
votes: 0,
|
||||
rank: 0,
|
||||
},
|
||||
{
|
||||
id: "d7ebc3a9beea2e680815a1e4f57d6db6",
|
||||
html: "that",
|
||||
votes: 0,
|
||||
rank: 0,
|
||||
},
|
||||
{
|
||||
id: "6c986ebcde3d5822a6e91a695c388094",
|
||||
html: "other",
|
||||
votes: 0,
|
||||
rank: 0,
|
||||
},
|
||||
],
|
||||
voters: 0,
|
||||
chart_type: "bar",
|
||||
}),
|
||||
});
|
||||
|
||||
await render(hbs`<Poll @post={{this.post}} @poll={{this.poll}} />`);
|
||||
|
||||
await click(
|
||||
".ranked-choice-poll-option[data-poll-option-id='1f972d1df351de3ce35a787c89faad29'] button",
|
||||
"open dropdown"
|
||||
);
|
||||
|
||||
assert
|
||||
.dom(".dropdown-menu__item:nth-child(2)")
|
||||
.hasText(`1 ${I18n.t("poll.options.ranked_choice.highest_priority")}`);
|
||||
|
||||
await click(
|
||||
".dropdown-menu__item:nth-child(2) button",
|
||||
"select 1st priority"
|
||||
);
|
||||
|
||||
assert.dom(".poll-buttons .cast-votes:disabled").doesNotExist();
|
||||
assert.dom(".poll-buttons .cast-votes").exists();
|
||||
|
||||
await click(
|
||||
".ranked-choice-poll-option[data-poll-option-id='1f972d1df351de3ce35a787c89faad29'] button",
|
||||
"open dropdown"
|
||||
);
|
||||
|
||||
await click(".dropdown-menu__item:nth-child(1) button", "select Abstain");
|
||||
|
||||
assert.dom(".poll-buttons .cast-votes:disabled").exists();
|
||||
});
|
||||
|
||||
test("shows vote", async function (assert) {
|
||||
this.setProperties({
|
||||
post: EmberObject.create({
|
||||
|
|
Loading…
Reference in New Issue