From bae492efeebf7204f334921b5125ed70740d3086 Mon Sep 17 00:00:00 2001 From: Robert <35533304+merefield@users.noreply.github.com> Date: Wed, 17 Jul 2024 10:49:14 +0100 Subject: [PATCH] FEATURE: Add Ranked Choice Voting using Instant Run-off Voting algorithm to Poll Plugin (Part 2 add Ranked Choice) --------- Co-authored-by: Joffrey JAFFEUX Co-authored-by: Jarek Radosz --- .../poll/app/controllers/polls_controller.rb | 33 ++- plugins/poll/app/models/poll.rb | 6 +- .../poll/app/serializers/poll_serializer.rb | 11 +- .../components/modal/poll-ui-builder.hbs | 56 +++-- .../components/modal/poll-ui-builder.js | 9 + .../components/poll-buttons-dropdown.gjs | 4 +- .../poll-option-ranked-choice-dropdown.gjs | 61 +++++ .../components/poll-option-ranked-choice.gjs | 40 +++ .../discourse/components/poll-options.gjs | 82 +++--- .../components/poll-results-ranked-choice.gjs | 73 ++++++ .../components/poll-results-standard.gjs | 25 +- .../components/poll-results-tabs.gjs | 79 ++++++ .../components/poll-voters-ranked-choice.gjs | 47 ++++ .../discourse/components/poll-voters.gjs | 15 +- .../javascripts/discourse/components/poll.gjs | 147 +++++++++-- .../stylesheets/common/poll-ui-builder.scss | 3 + .../poll/assets/stylesheets/common/poll.scss | 103 ++++---- plugins/poll/config/locales/client.en.yml | 22 ++ plugins/poll/config/locales/server.en.yml | 8 +- plugins/poll/config/settings.yml | 4 + .../20240416105733_add_rank_to_poll_votes.rb | 7 + plugins/poll/lib/poll.rb | 237 ++++++++++++++---- plugins/poll/lib/ranked_choice.rb | 130 ++++++++++ plugins/poll/plugin.rb | 24 +- .../poll/spec/fabricators/poll_fabricator.rb | 2 + .../spec/integration/poll_endpoints_spec.rb | 127 ++++++++++ .../poll/spec/integration/user_merger_spec.rb | 27 ++ plugins/poll/spec/lib/poll_spec.rb | 87 +++++++ plugins/poll/spec/lib/ranked_choice_spec.rb | 69 +++++ .../component/poll-options-test.js | 16 ++ .../poll-results-ranked-choice-test.js | 61 +++++ .../component/poll-results-tabs-test.js | 155 ++++++++++++ 32 files changed, 1548 insertions(+), 222 deletions(-) create mode 100644 plugins/poll/assets/javascripts/discourse/components/poll-option-ranked-choice-dropdown.gjs create mode 100644 plugins/poll/assets/javascripts/discourse/components/poll-option-ranked-choice.gjs create mode 100644 plugins/poll/assets/javascripts/discourse/components/poll-results-ranked-choice.gjs create mode 100644 plugins/poll/assets/javascripts/discourse/components/poll-results-tabs.gjs create mode 100644 plugins/poll/assets/javascripts/discourse/components/poll-voters-ranked-choice.gjs create mode 100644 plugins/poll/db/migrate/20240416105733_add_rank_to_poll_votes.rb create mode 100644 plugins/poll/lib/ranked_choice.rb create mode 100644 plugins/poll/spec/lib/ranked_choice_spec.rb create mode 100644 plugins/poll/test/javascripts/component/poll-results-ranked-choice-test.js create mode 100644 plugins/poll/test/javascripts/component/poll-results-tabs-test.js diff --git a/plugins/poll/app/controllers/polls_controller.rb b/plugins/poll/app/controllers/polls_controller.rb index 154d12402f3..1a7a29135eb 100644 --- a/plugins/poll/app/controllers/polls_controller.rb +++ b/plugins/poll/app/controllers/polls_controller.rb @@ -61,18 +61,29 @@ class DiscoursePoll::PollsController < ::ApplicationController poll_name = params.require(:poll_name) user_field_name = params.require(:user_field_name) - begin + poll = Poll.find_by(post_id: post_id, name: poll_name) + + if poll.nil? + render json: { error: I18n.t("poll.errors.poll_not_found") }, status: :not_found + elsif poll.ranked_choice? render json: { - grouped_results: - DiscoursePoll::Poll.grouped_poll_results( - current_user, - post_id, - poll_name, - user_field_name, - ), - } - rescue DiscoursePoll::Error => e - render_json_error e.message + error: I18n.t("poll.ranked_choice.no_group_results_support"), + }, + status: :unprocessable_entity + else + begin + render json: { + grouped_results: + DiscoursePoll::Poll.grouped_poll_results( + current_user, + post_id, + poll_name, + user_field_name, + ), + } + rescue DiscoursePoll::Error => e + render_json_error e.message + end end end end diff --git a/plugins/poll/app/models/poll.rb b/plugins/poll/app/models/poll.rb index 922e398dbab..e04e6b9b0d9 100644 --- a/plugins/poll/app/models/poll.rb +++ b/plugins/poll/app/models/poll.rb @@ -9,7 +9,7 @@ class Poll < ActiveRecord::Base has_many :poll_options, -> { order(:id) }, dependent: :destroy has_many :poll_votes - enum type: { regular: 0, multiple: 1, number: 2 }, _scopes: false + enum type: { regular: 0, multiple: 1, number: 2, ranked_choice: 3 }, _scopes: false enum status: { open: 0, closed: 1 }, _scopes: false @@ -43,6 +43,10 @@ class Poll < ActiveRecord::Base def can_see_voters?(user) everyone? && can_see_results?(user) end + + def ranked_choice? + type == "ranked_choice" + end end # == Schema Information diff --git a/plugins/poll/app/serializers/poll_serializer.rb b/plugins/poll/app/serializers/poll_serializer.rb index cb4272f7aa3..0e8c98071e4 100644 --- a/plugins/poll/app/serializers/poll_serializer.rb +++ b/plugins/poll/app/serializers/poll_serializer.rb @@ -16,7 +16,8 @@ class PollSerializer < ApplicationSerializer :preloaded_voters, :chart_type, :groups, - :title + :title, + :ranked_choice_outcome def public true @@ -75,4 +76,12 @@ class PollSerializer < ApplicationSerializer def include_preloaded_voters? object.can_see_voters?(scope.user) end + + def include_ranked_choice_outcome? + object.ranked_choice? + end + + def ranked_choice_outcome + DiscoursePoll::RankedChoice.outcome(object.id) + end end diff --git a/plugins/poll/assets/javascripts/discourse/components/modal/poll-ui-builder.hbs b/plugins/poll/assets/javascripts/discourse/components/modal/poll-ui-builder.hbs index 3768ad21668..038cad9e745 100644 --- a/plugins/poll/assets/javascripts/discourse/components/modal/poll-ui-builder.hbs +++ b/plugins/poll/assets/javascripts/discourse/components/modal/poll-ui-builder.hbs @@ -7,35 +7,51 @@ <:body> @@ -103,7 +119,7 @@ {{/unless}} - {{#unless this.isRegular}} + {{#unless this.rankedChoiceOrRegular}}
- {{#unless this.isNumber}} + {{#unless this.rankedChoiceOrNumber}}