Enable voting for open-ended questions in public section

This commit is contained in:
taitus
2025-08-08 15:30:46 +02:00
parent 62e1c13e7e
commit 83b206f0b7
9 changed files with 201 additions and 57 deletions

View File

@@ -1,14 +1,16 @@
.poll-form {
fieldset {
fieldset,
.poll-question-open-ended {
border: 1px solid $border;
border-radius: $global-radius;
padding: $line-height;
}
fieldset + fieldset {
+ * {
margin-top: calc($line-height / 2);
}
}
fieldset {
legend {
@include header-font-size(h3);
float: $global-left;
@@ -19,7 +21,6 @@
}
}
label {
@include radio-or-checkbox-and-label-alignment;
font-weight: normal;
@@ -30,6 +31,14 @@
white-space: pre;
}
}
}
.poll-question-open-ended {
label {
@include header-font-size(h3);
line-height: 1.5;
}
}
.help-text {
display: block;

View File

@@ -1,3 +1,10 @@
<% if question.open? %>
<div class="poll-question-open-ended">
<%= fields_for "web_vote[#{question.id}]" do |f| %>
<%= f.text_area :answer, label: question.title, value: existing_answer, rows: 3 %>
<% end %>
</div>
<% else %>
<fieldset <%= fieldset_attributes %>>
<legend><%= question.title %></legend>
@@ -21,3 +28,4 @@
<% end %>
<%= form.error_for(:"question_#{question.id}") %>
</fieldset>
<% end %>

View File

@@ -33,6 +33,10 @@ class Polls::Questions::QuestionComponent < ApplicationComponent
end, ", ")
end
def existing_answer
form.object.answers[question.id]&.first&.answer
end
def multiple_choice?
question.multiple?
end

View File

@@ -73,23 +73,27 @@ class Poll::Question < ApplicationRecord
end
end
def find_or_initialize_user_answer(user, option_id)
option = question_options.find(option_id)
def find_or_initialize_user_answer(user, option_id: nil, answer_text: nil)
answer = answers.find_or_initialize_by(find_by_attributes(user, option_id))
answer = answers.find_or_initialize_by(find_by_attributes(user, option))
if accepts_options?
option = question_options.find(option_id)
answer.option = option
answer.answer = option.title
else
answer.answer = answer_text
end
answer
end
private
def find_by_attributes(user, option)
case vote_type
when "unique", nil
def find_by_attributes(user, option_id)
if multiple?
{ author: user, option_id: option_id }
else
{ author: user }
when "multiple"
{ author: user, option: option }
end
end
end

View File

@@ -63,8 +63,17 @@ class Poll::WebVote
def answers_for_question(question, question_params)
return [] unless question_params
if question.open?
answer_text = question_params[:answer].to_s.strip
if answer_text.present?
[question.find_or_initialize_user_answer(user, answer_text: answer_text)]
else
[]
end
else
Array(question_params[:option_id]).map do |option_id|
question.find_or_initialize_user_answer(user, option_id)
question.find_or_initialize_user_answer(user, option_id: option_id)
end
end
end

View File

@@ -69,5 +69,26 @@ describe Polls::Questions::QuestionComponent do
expect(page).to have_field "Yes", type: :radio, checked: true
expect(page).to have_field "No", type: :radio, checked: false
end
context "Open-ended question" do
let(:question) { create(:poll_question_open, poll: poll, title: "What do you want?") }
before { create(:poll_answer, author: user, question: question, answer: "I don't know") }
it "renders text area with persisted answer" do
render_inline Polls::Questions::QuestionComponent.new(question, form: form)
expect(page).to have_field "What do you want?", type: :textarea, with: "I don't know"
end
it "renders unsaved form text over the persisted value" do
web_vote.answers[question.id] = [
build(:poll_answer, question: question, author: user, answer: "Typed (unsaved)")
]
render_inline Polls::Questions::QuestionComponent.new(question, form: form)
expect(page).to have_field "What do you want?", type: :textarea, with: "Typed (unsaved)"
end
end
end
end

View File

@@ -100,7 +100,7 @@ RSpec.describe Poll::Question do
existing_answer = create(:poll_answer, question: question, author: user, option: answer_a)
create(:poll_answer, question: question, author: other_user, option: answer_b)
answer = question.find_or_initialize_user_answer(user, answer_b.id)
answer = question.find_or_initialize_user_answer(user, option_id: answer_b.id)
expect(answer).to eq existing_answer
expect(answer.author).to eq user
@@ -111,13 +111,25 @@ RSpec.describe Poll::Question do
it "initializes a new answer when only another user has answered" do
create(:poll_answer, question: question, author: other_user, option: answer_a)
answer = question.find_or_initialize_user_answer(user, answer_a.id)
answer = question.find_or_initialize_user_answer(user, option_id: answer_a.id)
expect(answer).to be_new_record
expect(answer.author).to eq user
expect(answer.option).to eq answer_a
expect(answer.answer).to eq "Answer A"
end
it "raises when option_id is invalid" do
expect do
question.find_or_initialize_user_answer(user, option_id: 999999)
end.to raise_error(ActiveRecord::RecordNotFound)
end
it "raises when option_id is nil" do
expect do
question.find_or_initialize_user_answer(user, answer_text: "ignored")
end.to raise_error(ActiveRecord::RecordNotFound)
end
end
context "multiple question" do
@@ -129,7 +141,7 @@ RSpec.describe Poll::Question do
existing_answer = create(:poll_answer, question: question, author: user, option: answer_a)
create(:poll_answer, question: question, author: other_user, option: answer_a)
answer = question.find_or_initialize_user_answer(user, answer_a.id)
answer = question.find_or_initialize_user_answer(user, option_id: answer_a.id)
expect(answer).to eq existing_answer
expect(answer.author).to eq user
@@ -141,7 +153,7 @@ RSpec.describe Poll::Question do
create(:poll_answer, question: question, author: user, option: answer_a)
create(:poll_answer, question: question, author: other_user, option: answer_b)
answer = question.find_or_initialize_user_answer(user, answer_b.id)
answer = question.find_or_initialize_user_answer(user, option_id: answer_b.id)
expect(answer).to be_new_record
expect(answer.author).to eq user
@@ -149,5 +161,31 @@ RSpec.describe Poll::Question do
expect(answer.answer).to eq "Answer B"
end
end
context "Open-ended question" do
let(:question) { create(:poll_question_open) }
it "ignores invalid option_id and uses answer_text" do
answer = question.find_or_initialize_user_answer(user, option_id: 999999, answer_text: "Hi")
expect(answer.option).to be nil
expect(answer.answer).to eq "Hi"
end
it "ignores option_id when nil and assigns answer with option set to nil" do
answer = question.find_or_initialize_user_answer(user, answer_text: "Hi")
expect(answer.option).to be nil
expect(answer.answer).to eq "Hi"
end
it "reuses the existing poll answer for the user and updates answer" do
existing = create(:poll_answer, question: question, author: user, answer: "Before")
answer = question.find_or_initialize_user_answer(user, answer_text: "After")
expect(answer).to eq existing
expect(answer.author).to eq user
expect(answer.answer).to eq "After"
end
end
end
end

View File

@@ -156,5 +156,47 @@ describe Poll::WebVote do
expect(Poll::Answer.count).to be 2
end
end
context "Open-ended questions" do
let!(:open_ended_question) { create(:poll_question_open, poll: poll) }
it "creates one answer when text is present" do
web_vote.update(open_ended_question.id.to_s => { answer: " Hi " })
expect(poll.reload.voters.size).to eq 1
open_answer = open_ended_question.reload.answers.find_by(author: user)
expect(open_answer.answer).to eq "Hi"
expect(open_answer.option_id).to be nil
end
it "does not create an answer but create voters when text is blank or only spaces" do
web_vote.update(open_ended_question.id.to_s => { answer: " " })
expect(poll.reload.voters.size).to eq 1
expect(open_ended_question.reload.answers.where(author: user)).to be_empty
end
it "deletes existing answer but keeps voters when leaving open-ended blank" do
create(:poll_answer, question: open_ended_question, author: user, answer: "Old answer")
web_vote.update(open_ended_question.id.to_s => { answer: " " })
expect(poll.reload.voters.size).to eq 1
expect(open_ended_question.reload.answers.where(author: user)).to be_empty
end
it "updates existing open answer without creating duplicates" do
existing = create(:poll_answer, question: open_ended_question, author: user, answer: "Old text")
web_vote.update(open_ended_question.id.to_s => { answer: " New text " })
updated = open_ended_question.reload.answers.find_by(author: user)
expect(updated.id).to eq existing.id
expect(updated.answer).to eq "New text"
expect(updated.option_id).to be nil
expect(poll.reload.voters.size).to eq 1
end
end
end
end

View File

@@ -8,9 +8,10 @@ describe "Poll Votation Type" do
login_as(author)
end
scenario "Unique and multiple answers" do
scenario "Unique, multiple and open answers" do
create(:poll_question_unique, :yes_no, poll: poll, title: "Is it that bad?")
create(:poll_question_multiple, :abcde, poll: poll, max_votes: 3, title: "Which ones do you prefer?")
create(:poll_question_open, poll: poll, title: "What do you think?")
visit poll_path(poll)
@@ -21,6 +22,10 @@ describe "Poll Votation Type" do
check "Answer C"
end
within(".poll-question-open-ended") do
fill_in "What do you think?", with: "I believe it's great"
end
click_button "Vote"
expect(page).to have_content "Thank you for voting!"
@@ -39,6 +44,10 @@ describe "Poll Votation Type" do
expect(page).to have_field "Answer E", type: :checkbox, checked: false
end
within(".poll-question-open-ended") do
expect(page).to have_field "What do you think?", with: "I believe it's great"
end
expect(page).to have_button "Vote"
end