diff --git a/app/assets/stylesheets/polls/form.scss b/app/assets/stylesheets/polls/form.scss index f6129dbf2..eb46dde94 100644 --- a/app/assets/stylesheets/polls/form.scss +++ b/app/assets/stylesheets/polls/form.scss @@ -1,33 +1,42 @@ .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); - } - - legend { - @include header-font-size(h3); - float: $global-left; - margin-bottom: 0; + * { - clear: $global-left; + margin-top: calc($line-height / 2); } } + fieldset { + legend { + @include header-font-size(h3); + float: $global-left; + margin-bottom: 0; - label { - @include radio-or-checkbox-and-label-alignment; - font-weight: normal; + + * { + clear: $global-left; + } + } - &:first-of-type::before { - content: "\A"; - margin-top: calc($line-height / 2); - white-space: pre; + label { + @include radio-or-checkbox-and-label-alignment; + font-weight: normal; + + &:first-of-type::before { + content: "\A"; + margin-top: calc($line-height / 2); + white-space: pre; + } + } + } + + .poll-question-open-ended { + label { + @include header-font-size(h3); + line-height: 1.5; } } diff --git a/app/components/polls/questions/question_component.html.erb b/app/components/polls/questions/question_component.html.erb index 075fc4dc1..614b014d9 100644 --- a/app/components/polls/questions/question_component.html.erb +++ b/app/components/polls/questions/question_component.html.erb @@ -1,23 +1,31 @@ -
> - <%= question.title %> - - <% if multiple_choice? %> - <%= multiple_choice_help_text %> - - <% question.question_options.each do |option| %> - <%= multiple_choice_field(option) %> +<% if question.open? %> +
+ <%= fields_for "web_vote[#{question.id}]" do |f| %> + <%= f.text_area :answer, label: question.title, value: existing_answer, rows: 3 %> <% end %> - <% else %> - <% question.question_options.each do |option| %> - <%= single_choice_field(option) %> - <% end %> - <% end %> +
+<% else %> +
> + <%= question.title %> - <% if question.options_with_read_more? %> - - <% end %> - <%= form.error_for(:"question_#{question.id}") %> -
+ <% if multiple_choice? %> + <%= multiple_choice_help_text %> + + <% question.question_options.each do |option| %> + <%= multiple_choice_field(option) %> + <% end %> + <% else %> + <% question.question_options.each do |option| %> + <%= single_choice_field(option) %> + <% end %> + <% end %> + + <% if question.options_with_read_more? %> + + <% end %> + <%= form.error_for(:"question_#{question.id}") %> +
+<% end %> diff --git a/app/components/polls/questions/question_component.rb b/app/components/polls/questions/question_component.rb index cd74b39fc..c0cc2500a 100644 --- a/app/components/polls/questions/question_component.rb +++ b/app/components/polls/questions/question_component.rb @@ -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 diff --git a/app/models/poll/question.rb b/app/models/poll/question.rb index dcbe1c613..f40d47d33 100644 --- a/app/models/poll/question.rb +++ b/app/models/poll/question.rb @@ -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)) + + if accepts_options? + option = question_options.find(option_id) + answer.option = option + answer.answer = option.title + else + answer.answer = answer_text + end - answer = answers.find_or_initialize_by(find_by_attributes(user, option)) - answer.option = option - answer.answer = option.title 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 diff --git a/app/models/poll/web_vote.rb b/app/models/poll/web_vote.rb index 9ceda20e5..f3f26f4a3 100644 --- a/app/models/poll/web_vote.rb +++ b/app/models/poll/web_vote.rb @@ -63,8 +63,17 @@ class Poll::WebVote def answers_for_question(question, question_params) return [] unless question_params - Array(question_params[:option_id]).map do |option_id| - question.find_or_initialize_user_answer(user, option_id) + 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: option_id) + end end end diff --git a/spec/components/polls/questions/question_component_spec.rb b/spec/components/polls/questions/question_component_spec.rb index 1b0e7c219..61deadffb 100644 --- a/spec/components/polls/questions/question_component_spec.rb +++ b/spec/components/polls/questions/question_component_spec.rb @@ -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 diff --git a/spec/models/poll/question_spec.rb b/spec/models/poll/question_spec.rb index 320debbdb..f0f5e0882 100644 --- a/spec/models/poll/question_spec.rb +++ b/spec/models/poll/question_spec.rb @@ -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 diff --git a/spec/models/poll/web_vote_spec.rb b/spec/models/poll/web_vote_spec.rb index d8c1caa5b..42ef8af86 100644 --- a/spec/models/poll/web_vote_spec.rb +++ b/spec/models/poll/web_vote_spec.rb @@ -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 diff --git a/spec/system/polls/votation_types_spec.rb b/spec/system/polls/votation_types_spec.rb index 9ccbcbd97..444c785de 100644 --- a/spec/system/polls/votation_types_spec.rb +++ b/spec/system/polls/votation_types_spec.rb @@ -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