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.
This commit is contained in:
taitus
2025-08-22 07:44:39 +02:00
parent 2a2edd17d1
commit f3050a1aa5
7 changed files with 220 additions and 52 deletions

View File

@@ -1,5 +1,6 @@
<h3 id="<%= question.title.parameterize %>"><%= question.title %></h3> <h3 id="<%= question.title.parameterize %>"><%= question.title %></h3>
<table id="question_<%= question.id %>_results_table"> <table id="question_<%= question.id %>_results_table">
<% if question.accepts_options? %>
<thead> <thead>
<tr> <tr>
<%- question.question_options.each do |option| %> <%- question.question_options.each do |option| %>
@@ -15,11 +16,31 @@
<tbody> <tbody>
<tr> <tr>
<%- question.question_options.each do |option| %> <%- question.question_options.each do |option| %>
<td id="option_<%= option.id %>_result" class="<%= option_styles(option) %>"> <td class="<%= option_styles(option) %>">
<%= option.total_votes %> <%= option.total_votes %>
(<%= option.total_votes_percentage.round(2) %>%) (<%= option.total_votes_percentage.round(2) %>%)
</td> </td>
<% end %> <% end %>
</tr> </tr>
</tbody> </tbody>
<% else %>
<thead>
<tr>
<th scope="col"><%= t("polls.show.results.open_ended.valid") %></th>
<th scope="col"><%= t("polls.show.results.open_ended.blank") %></th>
</tr>
</thead>
<tbody>
<tr>
<td>
<%= question.open_ended_valid_answers_count %>
(<%= question.open_ended_valid_percentage.round(2) %>%)
</td>
<td>
<%= question.open_ended_blank_answers_count %>
(<%= question.open_ended_blank_percentage.round(2) %>%)
</td>
</tr>
</tbody>
<% end %>
</table> </table>

View File

@@ -87,6 +87,26 @@ class Poll::Question < ApplicationRecord
answer answer
end 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 private
def find_by_attributes(user, option_id) def find_by_attributes(user, option_id)
@@ -96,4 +116,8 @@ class Poll::Question < ApplicationRecord
{ author: user } { author: user }
end end
end end
def open_ended_total_answers
open_ended_valid_answers_count + open_ended_blank_answers_count
end
end end

View File

@@ -639,6 +639,9 @@ en:
results: results:
title: "Questions" title: "Questions"
most_voted_answer: "Most voted answer: " most_voted_answer: "Most voted answer: "
open_ended:
valid: Valid
blank: Blank
poll_header: poll_header:
back_to_proposal: Back to proposal back_to_proposal: Back to proposal
poll_questions: poll_questions:

View File

@@ -639,6 +639,9 @@ es:
results: results:
title: "Preguntas" title: "Preguntas"
most_voted_answer: "Respuesta más votada: " most_voted_answer: "Respuesta más votada: "
open_ended:
valid: Válidos
blank: En blanco
poll_header: poll_header:
back_to_proposal: Volver a la propuesta back_to_proposal: Volver a la propuesta
poll_questions: poll_questions:

View File

@@ -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

View File

@@ -3,47 +3,34 @@ require "rails_helper"
describe Polls::ResultsComponent do describe Polls::ResultsComponent do
let(:poll) { create(:poll) } let(:poll) { create(:poll) }
let(:question_1) { create(:poll_question, poll: poll, title: "Do you like Consul Democracy?") } let(:question_1) { create(:poll_question, :yes_no, poll: poll, title: "Do you like Consul Democracy?") }
let(:option_yes) { create(:poll_question_option, question: question_1, title: "Yes") } let(:option_yes) { question_1.question_options.find_by(title: "Yes") }
let(:option_no) { create(:poll_question_option, question: question_1, title: "No") } 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(:question_2) { create(:poll_question, :abc, poll: poll, title: "Which option do you prefer?") }
let(:option_blue) { create(:poll_question_option, question: question_2, title: "Blue") } let(:option_a) { question_2.question_options.find_by(title: "Answer A") }
let(:option_green) { create(:poll_question_option, question: question_2, title: "Green") } let(:option_b) { question_2.question_options.find_by(title: "Answer B") }
let(:option_yellow) { create(:poll_question_option, question: question_2, title: "Yellow") } let(:option_c) { question_2.question_options.find_by(title: "Answer C") }
it "renders results content" do it "renders results content" do
create_list(:poll_answer, 2, question: question_1, option: option_yes) 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_1, option: option_no)
create(:poll_answer, question: question_2, option: option_blue) create(:poll_answer, question: question_2, option: option_a)
create(:poll_answer, question: question_2, option: option_green) create(:poll_answer, question: question_2, option: option_b)
create(:poll_answer, question: question_2, option: option_yellow) create(:poll_answer, question: question_2, option: option_c)
render_inline Polls::ResultsComponent.new(poll) render_inline Polls::ResultsComponent.new(poll)
expect(page).to have_content "Do you like Consul Democracy?" 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(page).to have_content "Which option do you prefer?"
expect(table).to have_css "#option_#{option_yes.id}_result", text: "2 (66.67%)", normalize_ws: true expect(page).to have_table "question_#{question_2.id}_results_table",
expect(table).to have_css "#option_#{option_no.id}_result", text: "1 (33.33%)", normalize_ws: true with_rows: [{ "Most voted answer: Answer A" => "1 (33.33%)",
end "Answer B" => "1 (33.33%)",
"Answer C" => "1 (33.33%)" }]
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
end end
end end

View File

@@ -188,4 +188,87 @@ RSpec.describe Poll::Question do
end end
end 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 end