From f3050a1aa5e59018fbd0b2ab7f36a4f67451805e Mon Sep 17 00:00:00 2001 From: taitus Date: Fri, 22 Aug 2025 07:44:39 +0200 Subject: [PATCH] Manage correctly results and stats for open-ended questions Note that we are not including Poll::PartialResults for open-ended questions resutls. The reason is that we do not contemplate the possibility of there being open questions in booths. Manually counting and introducing the votes in the system is not feasible. --- .../polls/results/question_component.html.erb | 63 +++++++++----- app/models/poll/question.rb | 24 ++++++ config/locales/en/general.yml | 3 + config/locales/es/general.yml | 3 + .../polls/results/question_component_spec.rb | 47 +++++++++++ .../polls/results_component_spec.rb | 49 ++++------- spec/models/poll/question_spec.rb | 83 +++++++++++++++++++ 7 files changed, 220 insertions(+), 52 deletions(-) create mode 100644 spec/components/polls/results/question_component_spec.rb diff --git a/app/components/polls/results/question_component.html.erb b/app/components/polls/results/question_component.html.erb index 512d012f8..422ec44bb 100644 --- a/app/components/polls/results/question_component.html.erb +++ b/app/components/polls/results/question_component.html.erb @@ -1,25 +1,46 @@

<%= question.title %>

- - - <%- question.question_options.each do |option| %> - - <% end %> - - - - - <%- question.question_options.each do |option| %> - + + <%- question.question_options.each do |option| %> + + <% end %> + + + + + <%- question.question_options.each do |option| %> + + <% end %> + + + <% else %> + + + + + + + + + - <% end %> - - + + + + <% end %>
- <% if most_voted_option?(option) %> - <%= t("polls.show.results.most_voted_answer") %> - <% end %> - <%= option.title %> -
- <%= option.total_votes %> - (<%= option.total_votes_percentage.round(2) %>%) + <% if question.accepts_options? %> +
+ <% if most_voted_option?(option) %> + <%= t("polls.show.results.most_voted_answer") %> + <% end %> + <%= option.title %> +
+ <%= option.total_votes %> + (<%= option.total_votes_percentage.round(2) %>%) +
<%= t("polls.show.results.open_ended.valid") %><%= t("polls.show.results.open_ended.blank") %>
+ <%= question.open_ended_valid_answers_count %> + (<%= question.open_ended_valid_percentage.round(2) %>%)
+ <%= question.open_ended_blank_answers_count %> + (<%= question.open_ended_blank_percentage.round(2) %>%) +
diff --git a/app/models/poll/question.rb b/app/models/poll/question.rb index f40d47d33..d6c9fc763 100644 --- a/app/models/poll/question.rb +++ b/app/models/poll/question.rb @@ -87,6 +87,26 @@ class Poll::Question < ApplicationRecord answer end + def open_ended_valid_answers_count + answers.count + end + + def open_ended_blank_answers_count + poll.voters.count - open_ended_valid_answers_count + end + + def open_ended_valid_percentage + return 0.0 if open_ended_total_answers.zero? + + (open_ended_valid_answers_count * 100.0) / open_ended_total_answers + end + + def open_ended_blank_percentage + return 0.0 if open_ended_total_answers.zero? + + (open_ended_blank_answers_count * 100.0) / open_ended_total_answers + end + private def find_by_attributes(user, option_id) @@ -96,4 +116,8 @@ class Poll::Question < ApplicationRecord { author: user } end end + + def open_ended_total_answers + open_ended_valid_answers_count + open_ended_blank_answers_count + end end diff --git a/config/locales/en/general.yml b/config/locales/en/general.yml index f4f37c0a9..ae79e1ba7 100644 --- a/config/locales/en/general.yml +++ b/config/locales/en/general.yml @@ -639,6 +639,9 @@ en: results: title: "Questions" most_voted_answer: "Most voted answer: " + open_ended: + valid: Valid + blank: Blank poll_header: back_to_proposal: Back to proposal poll_questions: diff --git a/config/locales/es/general.yml b/config/locales/es/general.yml index ca07be430..dd7e63c3e 100644 --- a/config/locales/es/general.yml +++ b/config/locales/es/general.yml @@ -639,6 +639,9 @@ es: results: title: "Preguntas" most_voted_answer: "Respuesta más votada: " + open_ended: + valid: Válidos + blank: En blanco poll_header: back_to_proposal: Volver a la propuesta poll_questions: diff --git a/spec/components/polls/results/question_component_spec.rb b/spec/components/polls/results/question_component_spec.rb new file mode 100644 index 000000000..562bb0d28 --- /dev/null +++ b/spec/components/polls/results/question_component_spec.rb @@ -0,0 +1,47 @@ +require "rails_helper" + +describe Polls::Results::QuestionComponent do + context "question that accepts options" do + let(:question) { create(:poll_question, :yes_no) } + let(:option_yes) { question.question_options.find_by(title: "Yes") } + let(:option_no) { question.question_options.find_by(title: "No") } + + it "renders results table content" do + create(:poll_answer, question: question, option: option_yes) + create(:poll_answer, question: question, option: option_no) + + render_inline Polls::Results::QuestionComponent.new(question) + + expect(page).to have_table with_rows: [{ "Most voted answer: Yes" => "1 (50.0%)", + "No" => "1 (50.0%)" }] + + page.find("table") do |table| + expect(table).to have_css "th.win", count: 1 + expect(table).to have_css "td.win", count: 1 + end + end + end + + context "question that does not accept options" do + let(:open_ended_question) { create(:poll_question_open) } + + it "renders open_ended headers and empty counts when there are no participants" do + render_inline Polls::Results::QuestionComponent.new(open_ended_question) + + expect(page).to have_table with_rows: [{ "Valid" => "0 (0.0%)", + "Blank" => "0 (0.0%)" }] + end + + it "renders counts and percentages provided by the model metrics" do + allow(open_ended_question).to receive_messages( + open_ended_valid_answers_count: 3, + open_ended_blank_answers_count: 1 + ) + + render_inline Polls::Results::QuestionComponent.new(open_ended_question) + + expect(page).to have_table with_rows: [{ "Valid" => "3 (75.0%)", + "Blank" => "1 (25.0%)" }] + end + end +end diff --git a/spec/components/polls/results_component_spec.rb b/spec/components/polls/results_component_spec.rb index 26c086215..d3a7257e3 100644 --- a/spec/components/polls/results_component_spec.rb +++ b/spec/components/polls/results_component_spec.rb @@ -3,47 +3,34 @@ require "rails_helper" describe Polls::ResultsComponent do let(:poll) { create(:poll) } - let(:question_1) { create(:poll_question, poll: poll, title: "Do you like Consul Democracy?") } - let(:option_yes) { create(:poll_question_option, question: question_1, title: "Yes") } - let(:option_no) { create(:poll_question_option, question: question_1, title: "No") } + let(:question_1) { create(:poll_question, :yes_no, poll: poll, title: "Do you like Consul Democracy?") } + let(:option_yes) { question_1.question_options.find_by(title: "Yes") } + let(:option_no) { question_1.question_options.find_by(title: "No") } - let(:question_2) { create(:poll_question, poll: poll, title: "What's your favorite color?") } - let(:option_blue) { create(:poll_question_option, question: question_2, title: "Blue") } - let(:option_green) { create(:poll_question_option, question: question_2, title: "Green") } - let(:option_yellow) { create(:poll_question_option, question: question_2, title: "Yellow") } + let(:question_2) { create(:poll_question, :abc, poll: poll, title: "Which option do you prefer?") } + let(:option_a) { question_2.question_options.find_by(title: "Answer A") } + let(:option_b) { question_2.question_options.find_by(title: "Answer B") } + let(:option_c) { question_2.question_options.find_by(title: "Answer C") } it "renders results content" do create_list(:poll_answer, 2, question: question_1, option: option_yes) create(:poll_answer, question: question_1, option: option_no) - create(:poll_answer, question: question_2, option: option_blue) - create(:poll_answer, question: question_2, option: option_green) - create(:poll_answer, question: question_2, option: option_yellow) + create(:poll_answer, question: question_2, option: option_a) + create(:poll_answer, question: question_2, option: option_b) + create(:poll_answer, question: question_2, option: option_c) render_inline Polls::ResultsComponent.new(poll) expect(page).to have_content "Do you like Consul Democracy?" + expect(page).to have_table "question_#{question_1.id}_results_table", + with_rows: [{ "Most voted answer: Yes" => "2 (66.67%)", + "No" => "1 (33.33%)" }] - page.find("#question_#{question_1.id}_results_table") do |table| - expect(table).to have_css "#option_#{option_yes.id}_result", text: "2 (66.67%)", normalize_ws: true - expect(table).to have_css "#option_#{option_no.id}_result", text: "1 (33.33%)", normalize_ws: true - end - - expect(page).to have_content "What's your favorite color?" - - page.find("#question_#{question_2.id}_results_table") do |table| - expect(table).to have_css "#option_#{option_blue.id}_result", text: "1 (33.33%)", normalize_ws: true - expect(table).to have_css "#option_#{option_green.id}_result", text: "1 (33.33%)", normalize_ws: true - expect(table).to have_css "#option_#{option_yellow.id}_result", text: "1 (33.33%)", normalize_ws: true - end - end - - it "renders results for polls with questions but without answers" do - poll = create(:poll, :expired, results_enabled: true) - question = create(:poll_question, poll: poll) - - render_inline Polls::ResultsComponent.new(poll) - - expect(page).to have_content question.title + expect(page).to have_content "Which option do you prefer?" + expect(page).to have_table "question_#{question_2.id}_results_table", + with_rows: [{ "Most voted answer: Answer A" => "1 (33.33%)", + "Answer B" => "1 (33.33%)", + "Answer C" => "1 (33.33%)" }] end end diff --git a/spec/models/poll/question_spec.rb b/spec/models/poll/question_spec.rb index f0f5e0882..f882b0503 100644 --- a/spec/models/poll/question_spec.rb +++ b/spec/models/poll/question_spec.rb @@ -188,4 +188,87 @@ RSpec.describe Poll::Question do end end end + + context "open-ended results" do + let(:poll) { create(:poll) } + let!(:question_open) { create(:poll_question_open, poll: poll) } + + it "includes voters who didn't answer any questions in blank answers count" do + create(:poll_voter, poll: poll) + + expect(question_open.open_ended_blank_answers_count).to eq 1 + expect(question_open.open_ended_valid_answers_count).to eq 0 + end + + describe "#open_ended_valid_answers_count" do + it "returns 0 when there are no answers" do + expect(question_open.open_ended_valid_answers_count).to eq 0 + end + + it "counts answers" do + create(:poll_answer, question: question_open, answer: "Hello") + create(:poll_answer, question: question_open, answer: "Bye") + + expect(question_open.open_ended_valid_answers_count).to eq 2 + end + end + + describe "#open_ended_blank_answers_count" do + let(:another_question) { create(:poll_question, :yes_no, poll: poll) } + let(:option_yes) { another_question.question_options.find_by(title: "Yes") } + let(:option_no) { another_question.question_options.find_by(title: "No") } + + it "counts valid participants of the poll who did not answer the open-ended question" do + voters = create_list(:poll_voter, 3, poll: poll) + voters.each do |voter| + create(:poll_answer, question: another_question, author: voter.user, option: option_yes) + end + create(:poll_answer, question: question_open, author: voters.sample.user, answer: "Free text") + + expect(question_open.open_ended_valid_answers_count).to eq 1 + expect(question_open.open_ended_blank_answers_count).to eq 2 + end + + it "returns 0 when there are no valid participants in the poll" do + expect(question_open.open_ended_blank_answers_count).to eq 0 + end + + it "counts every user one time even if they answered many questions" do + multiple_question = create(:poll_question_multiple, :abc, poll: poll) + option_a = multiple_question.question_options.find_by(title: "Answer A") + option_b = multiple_question.question_options.find_by(title: "Answer B") + another_question_open = create(:poll_question_open, poll: poll) + + voter = create(:poll_voter, poll: poll) + + create(:poll_answer, question: multiple_question, author: voter.user, option: option_a) + create(:poll_answer, question: multiple_question, author: voter.user, option: option_b) + create(:poll_answer, question: another_question, author: voter.user, option: option_yes) + create(:poll_answer, question: another_question_open, author: voter.user, answer: "Free text") + + expect(question_open.open_ended_blank_answers_count).to eq 1 + end + end + + describe "percentages" do + it "returns 0.0 when there aren't any answers" do + expect(question_open.open_ended_valid_percentage).to eq 0.0 + expect(question_open.open_ended_blank_percentage).to eq 0.0 + end + + it "calculates valid and blank percentages based on counts" do + another_question = create(:poll_question, :yes_no, poll: poll) + option_yes = another_question.question_options.find_by(title: "Yes") + + voters = create_list(:poll_voter, 4, poll: poll) + voters.each do |voter| + create(:poll_answer, question: another_question, author: voter.user, option: option_yes) + end + create(:poll_answer, question: question_open, author: voters.sample.user, answer: "A") + + expect(question_open.open_ended_valid_percentage).to eq 25.0 + expect(question_open.open_ended_blank_percentage).to eq 75.0 + end + end + end end