diff --git a/app/assets/javascripts/admin/votation_types/fields.js b/app/assets/javascripts/admin/votation_types/fields.js index 85649e675..378d7474b 100644 --- a/app/assets/javascripts/admin/votation_types/fields.js +++ b/app/assets/javascripts/admin/votation_types/fields.js @@ -2,16 +2,18 @@ "use strict"; App.AdminVotationTypesFields = { adjustForm: function() { - if ($(this).val() === "unique") { - $(".max-votes").hide(); - $(".description-unique").show(); - $(".description-multiple").hide(); - $(".votation-type-max-votes").prop("disabled", true); - } else { + var select_field = $(this); + + $("[data-vote-type]").hide(0, function() { + $("[data-vote-type=" + select_field.val() + "]").show(); + }); + + if (select_field.val() === "multiple") { $(".max-votes").show(); - $(".description-unique").hide(); - $(".description-multiple").show(); $(".votation-type-max-votes").prop("disabled", false); + } else { + $(".max-votes").hide(); + $(".votation-type-max-votes").prop("disabled", true); } }, initialize: function() { 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/admin/votation_types/fields_component.html.erb b/app/components/admin/votation_types/fields_component.html.erb index f55d74579..89183ee8b 100644 --- a/app/components/admin/votation_types/fields_component.html.erb +++ b/app/components/admin/votation_types/fields_component.html.erb @@ -4,12 +4,9 @@
- - <%= t("admin.polls.votation_type.unique_description") %> - - + <% descriptions.each do |vote_type, text| %> + <%= description_tag(vote_type, text) %> + <% end %>
diff --git a/app/components/admin/votation_types/fields_component.rb b/app/components/admin/votation_types/fields_component.rb index 6fe487397..dc2fc09a0 100644 --- a/app/components/admin/votation_types/fields_component.rb +++ b/app/components/admin/votation_types/fields_component.rb @@ -4,4 +4,18 @@ class Admin::VotationTypes::FieldsComponent < ApplicationComponent def initialize(form:) @form = form end + + private + + def descriptions + { + unique: t("admin.polls.votation_type.unique_description"), + multiple: t("admin.polls.votation_type.multiple_description"), + open: t("admin.polls.votation_type.open_description") + } + end + + def description_tag(vote_type, text) + tag.span(text, data: { vote_type: vote_type }) + end end diff --git a/app/components/officing/results/form_component.html.erb b/app/components/officing/results/form_component.html.erb index d1c7870ec..3572ac33b 100644 --- a/app/components/officing/results/form_component.html.erb +++ b/app/components/officing/results/form_component.html.erb @@ -8,7 +8,7 @@ - <% poll.questions.each do |question| %> + <% poll.questions.for_physical_votes.each do |question| %>
<%= question.title %> <% question.question_options.each_with_index do |option, i| %> diff --git a/app/components/officing/results/index_component.html.erb b/app/components/officing/results/index_component.html.erb index 046f2319d..98f667ce3 100644 --- a/app/components/officing/results/index_component.html.erb +++ b/app/components/officing/results/index_component.html.erb @@ -29,7 +29,7 @@ - <% @poll.questions.each do |question| %> + <% @poll.questions.for_physical_votes.each do |question| %> <%= render Admin::Poll::Results::QuestionComponent.new(question, @partial_results) %> <% end %> diff --git a/app/components/polls/poll_component.html.erb b/app/components/polls/poll_component.html.erb index 17002ba0c..917f14366 100644 --- a/app/components/polls/poll_component.html.erb +++ b/app/components/polls/poll_component.html.erb @@ -8,13 +8,9 @@
- <% if poll.questions.one? %> -

<%= link_to_poll poll.questions.first.title, poll %>

-
<%= dates %>
- <% else %> -

<%= link_to_poll poll.name, poll %>

-
<%= dates %>
- +

<%= link_to header_text, path %>

+
<%= dates %>
+ <% if poll.questions.many? %>
- <% if poll.expired? %> - <%= link_to_poll t("polls.index.participate_button_expired"), poll, class: "button hollow expanded" %> - <% else %> - <%= link_to_poll t("polls.index.participate_button"), poll, class: "button hollow expanded" %> - <% end %> + <%= link_to link_text, path, class: "button hollow expanded" %> diff --git a/app/components/polls/poll_component.rb b/app/components/polls/poll_component.rb index af70041d7..4e7d465de 100644 --- a/app/components/polls/poll_component.rb +++ b/app/components/polls/poll_component.rb @@ -12,13 +12,29 @@ class Polls::PollComponent < ApplicationComponent t("polls.dates", open_at: l(poll.starts_at.to_date), closed_at: l(poll.ends_at.to_date)) end - def link_to_poll(text, poll, options = {}) - if can?(:results, poll) - link_to text, results_poll_path(id: poll.slug || poll.id), options - elsif can?(:stats, poll) - link_to text, stats_poll_path(id: poll.slug || poll.id), options + def header_text + if poll.questions.one? + poll.questions.first.title else - link_to text, poll_path(id: poll.slug || poll.id), options + poll.name + end + end + + def link_text + if poll.expired? + t("polls.index.participate_button_expired") + else + t("polls.index.participate_button") + end + end + + def path + if can?(:results, poll) + results_poll_path(id: poll.slug || poll.id) + elsif can?(:stats, poll) + stats_poll_path(id: poll.slug || poll.id) + else + poll_path(id: poll.slug || poll.id) end end end 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/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/components/polls/results/question_component.rb b/app/components/polls/results/question_component.rb index ce41ca1fc..ba84d2108 100644 --- a/app/components/polls/results/question_component.rb +++ b/app/components/polls/results/question_component.rb @@ -1,7 +1,7 @@ class Polls::Results::QuestionComponent < ApplicationComponent attr_reader :question - def initialize(question:) + def initialize(question) @question = question end diff --git a/app/components/polls/results_component.html.erb b/app/components/polls/results_component.html.erb index 28e33ad82..13fc53a43 100644 --- a/app/components/polls/results_component.html.erb +++ b/app/components/polls/results_component.html.erb @@ -3,7 +3,7 @@
<%= render Polls::PollHeaderComponent.new(poll) %> - <%= render "poll_subnav" %> + <%= render "polls/poll_subnav" %>
@@ -16,7 +16,9 @@
- <%= render Polls::Results::QuestionComponent.with_collection(poll.questions) %> + <% poll.questions.each do |question| %> + <%= render Polls::Results::QuestionComponent.new(question) %> + <% end %>
diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb index 9e9c123ce..857280fe0 100644 --- a/app/models/abilities/administrator.rb +++ b/app/models/abilities/administrator.rb @@ -101,7 +101,7 @@ module Abilities end can [:read, :order_options], Poll::Question::Option can [:create, :update, :destroy], Poll::Question::Option do |option| - can?(:update, option.question) + can?(:update, option.question) && option.question.accepts_options? end can :read, Poll::Question::Option::Video can [:create, :update, :destroy], Poll::Question::Option::Video do |video| diff --git a/app/models/poll/answer.rb b/app/models/poll/answer.rb index 7663aa42b..664e2f426 100644 --- a/app/models/poll/answer.rb +++ b/app/models/poll/answer.rb @@ -9,21 +9,9 @@ class Poll::Answer < ApplicationRecord validates :author, presence: true validates :answer, presence: true validates :option, uniqueness: { scope: :author_id }, allow_nil: true - validate :max_votes - validates :answer, inclusion: { in: ->(poll_answer) { poll_answer.option.possible_answers }}, if: ->(poll_answer) { poll_answer.option.present? } scope :by_author, ->(author_id) { where(author_id: author_id) } scope :by_question, ->(question_id) { where(question_id: question_id) } - - private - - def max_votes - return if !question || !author || persisted? - - if question.answers.by_author(author).count >= question.max_votes - errors.add(:answer, "Maximum number of votes per user exceeded") - end - end end diff --git a/app/models/poll/question.rb b/app/models/poll/question.rb index 97d1d2c60..ce719493a 100644 --- a/app/models/poll/question.rb +++ b/app/models/poll/question.rb @@ -27,10 +27,11 @@ class Poll::Question < ApplicationRecord accepts_nested_attributes_for :question_options, reject_if: :all_blank, allow_destroy: true accepts_nested_attributes_for :votation_type - delegate :multiple?, :vote_type, to: :votation_type, allow_nil: true + delegate :multiple?, :open?, :vote_type, to: :votation_type, allow_nil: true scope :sort_for_list, -> { order(Arel.sql("poll_questions.proposal_id IS NULL"), :created_at) } scope :for_render, -> { includes(:author, :proposal) } + scope :for_physical_votes, -> { left_joins(:votation_type).merge(VotationType.accepts_options) } def copy_attributes_from_proposal(proposal) if proposal.present? @@ -61,6 +62,10 @@ class Poll::Question < ApplicationRecord votation_type.nil? || votation_type.unique? end + def accepts_options? + votation_type.nil? || votation_type.accepts_options? + end + def max_votes if multiple? votation_type.max_votes @@ -69,23 +74,51 @@ 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 + 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) - 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, answer: option.title } end end + + def open_ended_total_answers + open_ended_valid_answers_count + open_ended_blank_answers_count + 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/app/models/votation_type.rb b/app/models/votation_type.rb index 71d61c62d..2302760c4 100644 --- a/app/models/votation_type.rb +++ b/app/models/votation_type.rb @@ -1,17 +1,31 @@ class VotationType < ApplicationRecord belongs_to :questionable, polymorphic: true + validate :cannot_be_open_ended_if_question_has_options + QUESTIONABLE_TYPES = %w[Poll::Question].freeze - enum :vote_type, { unique: 0, multiple: 1 } + enum :vote_type, { unique: 0, multiple: 1, open: 2 } validates :questionable, presence: true validates :questionable_type, inclusion: { in: ->(*) { QUESTIONABLE_TYPES }} validates :max_votes, presence: true, if: :max_votes_required? + scope :accepts_options, -> { where.not(vote_type: "open") } + + def accepts_options? + !open? + end + private def max_votes_required? multiple? end + + def cannot_be_open_ended_if_question_has_options + if questionable&.question_options&.any? && !accepts_options? + errors.add(:vote_type, :cannot_change_to_open_ended) + end + end end diff --git a/app/views/admin/poll/questions/show.html.erb b/app/views/admin/poll/questions/show.html.erb index 35b09b9f9..d9c83803d 100644 --- a/app/views/admin/poll/questions/show.html.erb +++ b/app/views/admin/poll/questions/show.html.erb @@ -35,7 +35,7 @@
<%= VotationType.human_attribute_name("vote_type.#{@question.vote_type}") %>

- <% if @question.max_votes.present? %> + <% if @question.multiple? %>

<%= VotationType.human_attribute_name("max_votes") %>
@@ -46,57 +46,59 @@ -

- <% if can?(:create, Poll::Question::Option.new(question: @question)) %> - <%= link_to t("admin.questions.show.add_answer"), new_admin_question_option_path(@question), - class: "button float-right" %> - <% else %> -
- <%= t("admin.questions.no_edit") %> -
- <% end %> -
- - - - - - - - - - - - - - - - <% @question.question_options.each do |option| %> - - - - - - - - +<% if @question.accepts_options? %> +
+ <% if can?(:create, Poll::Question::Option.new(question: @question)) %> + <%= link_to t("admin.questions.show.add_answer"), new_admin_question_option_path(@question), + class: "button float-right" %> + <% else %> +
+ <%= t("admin.questions.no_edit") %> +
<% end %> -
-
<%= t("admin.questions.show.valid_answers") %>
<%= t("admin.questions.show.answers.title") %><%= t("admin.questions.show.answers.description") %><%= t("admin.questions.show.answers.images") %><%= t("admin.questions.show.answers.documents") %><%= t("admin.questions.show.answers.videos") %><%= t("admin.actions.actions") %>
<%= option.title %><%= wysiwyg(option.description) %> - (<%= option.images.count %>) -
- <%= link_to t("admin.questions.show.answers.images_list"), - admin_option_images_path(option) %> -
- (<%= option.documents.count rescue 0 %>) -
- <%= link_to t("admin.questions.show.answers.documents_list"), - admin_option_documents_path(option) %> -
- (<%= option.videos.count %>) -
- <%= link_to t("admin.questions.show.answers.video_list"), - admin_option_videos_path(option) %> -
- <%= render Admin::Poll::Questions::Options::TableActionsComponent.new(option) %> -
+ + + + + + + + + + + + + + + + + <% @question.question_options.each do |option| %> + + + + + + + + + <% end %> + +
<%= t("admin.questions.show.valid_answers") %>
<%= t("admin.questions.show.answers.title") %><%= t("admin.questions.show.answers.description") %><%= t("admin.questions.show.answers.images") %><%= t("admin.questions.show.answers.documents") %><%= t("admin.questions.show.answers.videos") %><%= t("admin.actions.actions") %>
<%= option.title %><%= wysiwyg(option.description) %> + (<%= option.images.count %>) +
+ <%= link_to t("admin.questions.show.answers.images_list"), + admin_option_images_path(option) %> +
+ (<%= option.documents.count rescue 0 %>) +
+ <%= link_to t("admin.questions.show.answers.documents_list"), + admin_option_documents_path(option) %> +
+ (<%= option.videos.count %>) +
+ <%= link_to t("admin.questions.show.answers.video_list"), + admin_option_videos_path(option) %> +
+ <%= render Admin::Poll::Questions::Options::TableActionsComponent.new(option) %> +
+<% end %> diff --git a/config/locales/en/activerecord.yml b/config/locales/en/activerecord.yml index 951fa9168..ec1402b59 100644 --- a/config/locales/en/activerecord.yml +++ b/config/locales/en/activerecord.yml @@ -534,6 +534,7 @@ en: max_votes: Maximum number of votes vote_type: Votation type votation_type/vote_type: + open: Open-ended unique: Unique answer multiple: Multiple answers cookies/vendor: @@ -628,6 +629,10 @@ en: attributes: code: invalid: "must start with the same code as its target followed by a dot and end with a number" + votation_type: + attributes: + vote_type: + cannot_change_to_open_ended: "can't change to open-ended type because you've already defined possible valid answers for this question" messages: translations_too_short: Is mandatory to provide one translation at least record_invalid: "Validation failed: %{errors}" diff --git a/config/locales/en/admin.yml b/config/locales/en/admin.yml index 80289656d..d6917a5e5 100644 --- a/config/locales/en/admin.yml +++ b/config/locales/en/admin.yml @@ -1131,6 +1131,7 @@ en: title: "Votation type" unique_description: "It's only possible to answer one time to the question." multiple_description: "Allows to choose multiple answers. It's possible to set the maximum number of answers." + open_description: "Open-ended question that allows users to provide a single answer in their own words." questions: index: create: "Create question" 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/activerecord.yml b/config/locales/es/activerecord.yml index 8148cefbd..44e866faa 100644 --- a/config/locales/es/activerecord.yml +++ b/config/locales/es/activerecord.yml @@ -534,6 +534,7 @@ es: max_votes: Número máximo de votos vote_type: Tipo de votación votation_type/vote_type: + open: Respuesta abierta unique: Respuesta única multiple: Respuesta múltiple cookies/vendor: @@ -628,6 +629,10 @@ es: attributes: code: invalid: "debe empezar con el código de su meta seguido de un punto y terminar con un número" + votation_type: + attributes: + vote_type: + cannot_change_to_open_ended: "no se puede cambiar a respuesta abierta el tipo de votación porque ya has definido posibles respuestas válidas para esta pregunta" messages: translations_too_short: El obligatorio proporcionar una traducción como mínimo record_invalid: "Error de validación: %{errors}" diff --git a/config/locales/es/admin.yml b/config/locales/es/admin.yml index 3f7bac10d..8db768fed 100644 --- a/config/locales/es/admin.yml +++ b/config/locales/es/admin.yml @@ -1131,6 +1131,7 @@ es: title: "Tipo de votación" unique_description: "Solo se puede responder a la pregunta con una única respuesta." multiple_description: "Permite elegir más de una respuesta. Se puede elegir el número máximo de respuestas." + open_description: "Pregunta abierta que permite al usuario dar una única respuesta con sus propias palabras." questions: index: create: "Crear pregunta ciudadana" 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/db/dev_seeds/polls.rb b/db/dev_seeds/polls.rb index 78403006e..8e5a952c9 100644 --- a/db/dev_seeds/polls.rb +++ b/db/dev_seeds/polls.rb @@ -52,6 +52,7 @@ end section "Creating Poll Questions & Options" do Poll.find_each do |poll| (3..5).to_a.sample.times do + vote_type = VotationType.vote_types.keys.sample question_title = Faker::Lorem.sentence(word_count: 3).truncate(60) + "?" question = Poll::Question.new(author: User.sample, title: question_title, @@ -62,33 +63,29 @@ section "Creating Poll Questions & Options" do end end question.save! - Faker::Lorem.words(number: (2..4).to_a.sample).each_with_index do |title, index| - description = "

#{Faker::Lorem.paragraphs.join("

")}

" - option = Poll::Question::Option.new(question: question, - title: title.capitalize, - description: description, - given_order: index + 1) - Setting.enabled_locales.map do |locale| - Globalize.with_locale(locale) do - option.title = "#{title} (#{locale})" - option.description = "#{description} (#{locale})" + + question.create_votation_type!(vote_type: vote_type, max_votes: (3 if vote_type == "multiple")) + + if question.accepts_options? + Faker::Lorem.words(number: (2..4).to_a.sample).each_with_index do |title, index| + description = "

#{Faker::Lorem.paragraphs.join("

")}

" + option = Poll::Question::Option.new(question: question, + title: title.capitalize, + description: description, + given_order: index + 1) + Setting.enabled_locales.map do |locale| + Globalize.with_locale(locale) do + option.title = "#{title} (#{locale})" + option.description = "#{description} (#{locale})" + end end + option.save! end - option.save! end end end end -section "Creating Poll Votation types" do - poll = Poll.first - - poll.questions.each do |question| - vote_type = VotationType.vote_types.keys.sample - question.create_votation_type!(vote_type: vote_type, max_votes: (3 unless vote_type == "unique")) - end -end - section "Creating Poll Booths & BoothAssignments" do 20.times do |i| Poll::Booth.create(name: "Booth #{i}", @@ -158,9 +155,16 @@ section "Creating Poll Voters" do poll.questions.each do |question| next unless [true, false].sample - Poll::Answer.create!(question_id: question.id, - author: user, - answer: question.question_options.sample.title) + if question.accepts_options? + option = question.question_options.sample + Poll::Answer.create!(question_id: question.id, + author: user, + option: option, + answer: option.title) + else + text = Faker::Lorem.sentence(word_count: (6..14).to_a.sample) + Poll::Answer.create!(question_id: question.id, author: user, answer: text) + end end end @@ -252,6 +256,7 @@ section "Creating Poll Questions from Proposals" do end option.save! end + question.create_votation_type!(vote_type: "unique") end end diff --git a/spec/components/officing/results/form_component_spec.rb b/spec/components/officing/results/form_component_spec.rb index 81f19e38e..b0e46dc2b 100644 --- a/spec/components/officing/results/form_component_spec.rb +++ b/spec/components/officing/results/form_component_spec.rb @@ -16,4 +16,13 @@ describe Officing::Results::FormComponent do expect(page).to have_field "Invalid ballots", with: 0, type: :number expect(page).to have_field "Valid ballots", with: 0, type: :number end + + it "does not render open-ended questions" do + create(:poll_question_open, poll: poll, title: "What do you want?") + + render_inline Officing::Results::FormComponent.new(poll, Poll::OfficerAssignment.none) + + expect(page).not_to have_content "What do you want?" + expect(page).to have_css "fieldset", text: "Agreed?" + 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/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 new file mode 100644 index 000000000..d3a7257e3 --- /dev/null +++ b/spec/components/polls/results_component_spec.rb @@ -0,0 +1,36 @@ +require "rails_helper" + +describe Polls::ResultsComponent do + let(:poll) { create(:poll) } + + 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, :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_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%)" }] + + 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/factories/polls.rb b/spec/factories/polls.rb index 358af5e80..a97df79d5 100644 --- a/spec/factories/polls.rb +++ b/spec/factories/polls.rb @@ -60,6 +60,7 @@ FactoryBot.define do trait :yes_no do after(:create) do |question| + create(:votation_type_unique, questionable: question) create(:poll_question_option, question: question, title: "Yes") create(:poll_question_option, question: question, title: "No") end @@ -94,6 +95,12 @@ FactoryBot.define do create(:votation_type_multiple, questionable: question, max_votes: evaluator.max_votes) end end + + factory :poll_question_open do + after(:create) do |question| + create(:votation_type_open, questionable: question) + end + end end factory :poll_question_option, class: "Poll::Question::Option" do diff --git a/spec/factories/votation_type.rb b/spec/factories/votation_type.rb index 5a9e77e7d..ef91b836a 100644 --- a/spec/factories/votation_type.rb +++ b/spec/factories/votation_type.rb @@ -9,6 +9,10 @@ FactoryBot.define do max_votes { 3 } end + factory :votation_type_open do + vote_type { "open" } + end + questionable factory: :poll_question end end diff --git a/spec/models/abilities/administrator_spec.rb b/spec/models/abilities/administrator_spec.rb index 24b10a9cc..e6678ee21 100644 --- a/spec/models/abilities/administrator_spec.rb +++ b/spec/models/abilities/administrator_spec.rb @@ -20,6 +20,8 @@ describe Abilities::Administrator do let(:future_poll) { create(:poll, :future) } let(:current_poll_question) { create(:poll_question) } let(:future_poll_question) { create(:poll_question, poll: future_poll) } + let(:future_poll_question_open) { create(:poll_question_open, poll: future_poll) } + let(:future_poll_question_option_open) { future_poll_question_open.question_options.new } let(:current_poll_question_option) { create(:poll_question_option) } let(:future_poll_question_option) { create(:poll_question_option, poll: future_poll) } let(:current_poll_option_video) { create(:poll_option_video, option: current_poll_question_option) } @@ -143,6 +145,9 @@ describe Abilities::Administrator do it { should_not be_able_to(:create, current_poll_question_option) } it { should_not be_able_to(:update, current_poll_question_option) } it { should_not be_able_to(:destroy, current_poll_question_option) } + it { should_not be_able_to(:create, future_poll_question_option_open) } + it { should_not be_able_to(:update, future_poll_question_option_open) } + it { should_not be_able_to(:destroy, future_poll_question_option_open) } it { should be_able_to(:create, future_poll_option_video) } it { should be_able_to(:update, future_poll_option_video) } diff --git a/spec/models/poll/answer_spec.rb b/spec/models/poll/answer_spec.rb index a70ed3a4d..e72fd5da6 100644 --- a/spec/models/poll/answer_spec.rb +++ b/spec/models/poll/answer_spec.rb @@ -30,23 +30,9 @@ describe Poll::Answer do expect(answer).not_to be_valid end - it "is not valid if there's already an answer to that question" do - author = create(:user) - question = create(:poll_question, :yes_no) - - create(:poll_answer, author: author, question: question) - - answer = build(:poll_answer, author: author, question: question) - - expect(answer).not_to be_valid - end - - it "is not valid when user already reached multiple answers question max votes" do - author = create(:user) - question = create(:poll_question_multiple, :abc, max_votes: 2) - create(:poll_answer, author: author, question: question, answer: "Answer A") - create(:poll_answer, author: author, question: question, answer: "Answer B") - answer = build(:poll_answer, author: author, question: question, answer: "Answer C") + it "is not valid without an answer when question is open-ended" do + answer.question = create(:poll_question_open) + answer.answer = nil expect(answer).not_to be_valid end diff --git a/spec/models/poll/question_spec.rb b/spec/models/poll/question_spec.rb index ddd57aa58..36945c8c8 100644 --- a/spec/models/poll/question_spec.rb +++ b/spec/models/poll/question_spec.rb @@ -86,4 +86,204 @@ RSpec.describe Poll::Question do expect(question.options_total_votes).to eq 3 end end + + describe "#find_or_initialize_user_answer" do + let(:user) { create(:user) } + let(:other_user) { create(:user) } + + context "unique question" do + let(:question) { create(:poll_question_unique, :abc) } + let(:answer_a) { question.question_options.find_by(title: "Answer A") } + let(:answer_b) { question.question_options.find_by(title: "Answer B") } + + it "finds the existing answer for the same user" 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, option_id: answer_b.id) + + expect(answer).to eq existing_answer + expect(answer.author).to eq user + expect(answer.option).to eq answer_b + expect(answer.answer).to eq "Answer B" + end + + 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, 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 + let(:question) { create(:poll_question_multiple, :abc, max_votes: 3) } + let(:answer_a) { question.question_options.find_by(title: "Answer A") } + let(:answer_b) { question.question_options.find_by(title: "Answer B") } + + it "finds the existing answer for the same user and option" 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, option_id: answer_a.id) + + expect(answer).to eq existing_answer + expect(answer.author).to eq user + expect(answer.option).to eq answer_a + expect(answer.answer).to eq "Answer A" + end + + it "initializes a new answer when selecting a different option" 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, option_id: answer_b.id) + + expect(answer).to be_new_record + expect(answer.author).to eq user + expect(answer.option).to eq answer_b + 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 + + describe "scopes" do + describe ".for_physical_votes" do + it "returns unique and multiple, but not open_ended" do + question_unique = create(:poll_question_unique) + question_multiple = create(:poll_question_multiple) + question_open_ended = create(:poll_question_open) + + result = Poll::Question.for_physical_votes + + expect(result).to match_array [question_unique, question_multiple] + expect(result).not_to include question_open_ended + 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 diff --git a/spec/models/poll/web_vote_spec.rb b/spec/models/poll/web_vote_spec.rb index aaa235597..42ef8af86 100644 --- a/spec/models/poll/web_vote_spec.rb +++ b/spec/models/poll/web_vote_spec.rb @@ -44,6 +44,21 @@ describe Poll::WebVote do expect(voter.poll_id).to eq answer.poll.id end + it "updates existing multiple options instead of adding new ones" do + question = create(:poll_question_multiple, :abc, poll: poll, max_votes: 2) + option_a = question.question_options.find_by(title: "Answer A") + option_b = question.question_options.find_by(title: "Answer B") + option_c = question.question_options.find_by(title: "Answer C") + + create(:poll_answer, author: user, question: question, option: option_a) + create(:poll_answer, author: user, question: question, option: option_b) + + web_vote.update(question.id.to_s => { option_id: [option_c.id.to_s] }) + + expect(question.reload.answers.size).to eq 1 + expect(question.reload.answers.first.option).to eq option_c + end + it "does not save the answer if the voter is invalid" do allow_any_instance_of(Poll::Voter).to receive(:valid?).and_return(false) @@ -55,6 +70,28 @@ describe Poll::WebVote do expect(question.answers).to be_blank end + it "does not save the answer if it exceeds the allowed max votes" do + question = create(:poll_question_multiple, :abc, poll: poll, max_votes: 2) + + result = web_vote.update(question.id.to_s => { option_id: question.question_options.ids.map(&:to_s) }) + + expect(result).to be false + expect(poll.voters).to be_blank + expect(question.answers).to be_blank + end + + it "does not save the answer if unique question receives multiple options" do + question = create(:poll_question, :yes_no, poll: poll) + + result = web_vote.update( + question.id.to_s => { option_id: question.question_options.ids.map(&:to_s) } + ) + + expect(result).to be false + expect(poll.voters).to be_blank + expect(question.answers).to be_blank + end + it "creates a voter but does not create answers when leaving everything blank" do web_vote.update({}) @@ -119,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/models/votation_type_spec.rb b/spec/models/votation_type_spec.rb index 4c947ddd6..3bae901bd 100644 --- a/spec/models/votation_type_spec.rb +++ b/spec/models/votation_type_spec.rb @@ -1,7 +1,7 @@ require "rails_helper" describe VotationType do - let(:vote_types) { %i[votation_type_unique votation_type_multiple] } + let(:vote_types) { %i[votation_type_unique votation_type_multiple votation_type_open] } let(:votation_type) { build(vote_types.sample) } it "is valid" do @@ -27,9 +27,51 @@ describe VotationType do expect(votation_type).to be_valid + votation_type.vote_type = "open" + + expect(votation_type).to be_valid + votation_type.vote_type = "multiple" expect(votation_type).not_to be_valid expect(votation_type.errors[:max_votes]).to include "can't be blank" end + + describe "#cannot_be_open_ended_if_question_has_options" do + it "allows changing to open-ended when the question has no options" do + votation_type = create(:votation_type_unique) + + votation_type.vote_type = :open + + expect(votation_type).to be_valid + end + + it "blocks changing to open-ended when the question has options" do + votation_type = create(:votation_type_unique) + create(:poll_question_option, question: votation_type.questionable) + + votation_type.vote_type = :open + + expect(votation_type).not_to be_valid + error = votation_type.errors[:vote_type].first + expect(error).to eq "can't change to open-ended type " \ + "because you've already defined possible valid answers for this question" + end + end + + describe "scopes" do + describe ".accepts_options" do + it "includes unique and multiple, excludes open_ended" do + question_unique = create(:poll_question_unique) + question_multiple = create(:poll_question_multiple) + question_open_ended = create(:poll_question_open) + + accepts_options = VotationType.accepts_options + + expect(accepts_options).to match_array [question_unique.votation_type, + question_multiple.votation_type] + expect(accepts_options).not_to include question_open_ended.votation_type + end + end + end end diff --git a/spec/system/admin/poll/questions_spec.rb b/spec/system/admin/poll/questions_spec.rb index 127dca219..ac913fe07 100644 --- a/spec/system/admin/poll/questions_spec.rb +++ b/spec/system/admin/poll/questions_spec.rb @@ -71,26 +71,63 @@ describe "Admin poll questions", :admin do poll = create(:poll, :future) visit admin_poll_path(poll) click_link "Create question" + expect(page).to have_content "It's only possible to answer one time to the question." end scenario "Unique" do fill_in "Question", with: "Question with unique answer" select "Unique answer", from: "Votation type" + expect(page).to have_content "It's only possible to answer one time to the question." + expect(page).not_to have_content "Allows to choose multiple answers." + expect(page).not_to have_field "Maximum number of votes" + expect(page).not_to have_content "Open-ended question that allows users to provide " \ + "a single answer in their own words." + click_button "Save" expect(page).to have_content "Question with unique answer" expect(page).to have_content "Unique answer" + expect(page).not_to have_content "Maximum number of votes" + expect(page).to have_link "Add answer" + expect(page).to have_table "Valid answers" end scenario "Multiple" do fill_in "Question", with: "Question with multiple answers" select "Multiple answers", from: "Votation type" + + expect(page).not_to have_content "It's only possible to answer one time to the question." + expect(page).to have_content "Allows to choose multiple answers." + expect(page).not_to have_content "Open-ended question that allows users to provide " \ + "a single answer in their own words." + fill_in "Maximum number of votes", with: 6 click_button "Save" expect(page).to have_content "Question with multiple answers" expect(page).to have_content "Multiple answers" + expect(page).to have_text "Maximum number of votes 6", normalize_ws: true + expect(page).to have_link "Add answer" + expect(page).to have_table "Valid answers" + end + + scenario "Open-ended" do + fill_in "Question", with: "What do you want?" + select "Open-ended", from: "Votation type" + + expect(page).not_to have_content "Allows to choose multiple answers." + expect(page).not_to have_field "Maximum number of votes" + expect(page).to have_content "Open-ended question that allows users to provide " \ + "a single answer in their own words." + + click_button "Save" + + expect(page).to have_content "What do you want?" + expect(page).to have_content "Open-ended" + expect(page).not_to have_content "Maximum number of votes" + expect(page).not_to have_link "Add answer" + expect(page).not_to have_table "Valid answers" end end end @@ -134,7 +171,7 @@ describe "Admin poll questions", :admin do scenario "Update" do poll = create(:poll, :future) - question = create(:poll_question, poll: poll) + question = create(:poll_question_open, poll: poll) old_title = question.title new_title = "Vegetables are great and everyone should have one" @@ -145,6 +182,12 @@ describe "Admin poll questions", :admin do end expect(page).to have_link "Go back", href: admin_poll_path(poll) + expect(page).to have_select "Votation type", selected: "Open-ended" + expect(page).not_to have_content "Allows to choose multiple answers." + expect(page).not_to have_field "Maximum number of votes" + expect(page).to have_content "Open-ended question that allows users to provide " \ + "a single answer in their own words." + fill_in "Question", with: new_title click_button "Save" diff --git a/spec/system/officing/results_spec.rb b/spec/system/officing/results_spec.rb index e17da5857..35348b95c 100644 --- a/spec/system/officing/results_spec.rb +++ b/spec/system/officing/results_spec.rb @@ -4,8 +4,8 @@ describe "Officing Results", :with_frozen_time do let(:poll) { create(:poll, ends_at: 1.day.ago) } let(:booth) { create(:poll_booth, polls: [poll]) } let(:poll_officer) { create(:poll_officer) } - let(:question_1) { create(:poll_question, poll: poll) } - let(:question_2) { create(:poll_question, poll: poll) } + let(:question_1) { create(:poll_question_unique, poll: poll) } + let(:question_2) { create(:poll_question_unique, poll: poll) } before do create(:poll_shift, :recount_scrutiny_task, officer: poll_officer, booth: booth, date: Date.current) @@ -15,6 +15,8 @@ describe "Officing Results", :with_frozen_time do create(:poll_question_option, title: "Today", question: question_2, given_order: 1) create(:poll_question_option, title: "Tomorrow", question: question_2, given_order: 2) + create(:poll_question_open, poll: poll, title: "What do you want?") + login_as(poll_officer.user) set_officing_booth(booth) end @@ -69,6 +71,8 @@ describe "Officing Results", :with_frozen_time do fill_in "Tomorrow", with: "444" end + expect(page).not_to have_css "fieldset", text: "What do you want?" + fill_in "Totally blank ballots", with: "66" fill_in "Invalid ballots", with: "77" fill_in "Valid ballots", with: "88" @@ -100,6 +104,7 @@ describe "Officing Results", :with_frozen_time do booth_assignment_id: partial_result.booth_assignment_id) within("#question_#{question_1.id}_0_result") { expect(page).to have_content("7777") } + expect(page).not_to have_content "What do you want?" visit new_officing_poll_result_path(poll) @@ -136,5 +141,6 @@ describe "Officing Results", :with_frozen_time do within("#total_results") { expect(page).to have_content("8") } within("#question_#{question_1.id}_0_result") { expect(page).to have_content("5555") } within("#question_#{question_1.id}_1_result") { expect(page).to have_content("200") } + expect(page).not_to have_content "What do you want?" end end diff --git a/spec/system/polls/polls_spec.rb b/spec/system/polls/polls_spec.rb index c18cbbdd2..d9d3fdd42 100644 --- a/spec/system/polls/polls_spec.rb +++ b/spec/system/polls/polls_spec.rb @@ -180,15 +180,11 @@ describe "Polls" do scenario "Level 2 users answering" do poll.update!(geozone_restricted_to: [geozone]) - create(:poll_question, :yes_no, poll: poll, title: "Do you agree?") + question = create(:poll_question, :yes_no, poll: poll, title: "Do you agree?") login_as(create(:user, :level_two, geozone: geozone)) - visit poll_path(poll) + vote_for_poll_via_web(poll, question => "Yes") - within_fieldset("Do you agree?") { choose "Yes" } - click_button "Vote" - - expect(page).to have_content "Thank you for voting!" expect(page).to have_content "You have already participated in this poll. " \ "If you vote again it will be overwritten." diff --git a/spec/system/polls/results_spec.rb b/spec/system/polls/results_spec.rb deleted file mode 100644 index 9e269d3ff..000000000 --- a/spec/system/polls/results_spec.rb +++ /dev/null @@ -1,58 +0,0 @@ -require "rails_helper" - -describe "Poll Results" do - scenario "List each Poll question" do - user1 = create(:user, :level_two) - user2 = create(:user, :level_two) - user3 = create(:user, :level_two) - - poll = create(:poll, results_enabled: true) - question1 = create(:poll_question, poll: poll) - option1 = create(:poll_question_option, question: question1, title: "Yes") - option2 = create(:poll_question_option, question: question1, title: "No") - - question2 = create(:poll_question, poll: poll) - option3 = create(:poll_question_option, question: question2, title: "Blue") - option4 = create(:poll_question_option, question: question2, title: "Green") - option5 = create(:poll_question_option, question: question2, title: "Yellow") - - login_as user1 - vote_for_poll_via_web(poll, question1 => "Yes", question2 => "Blue") - logout - - login_as user2 - vote_for_poll_via_web(poll, question1 => "Yes", question2 => "Green") - logout - - login_as user3 - vote_for_poll_via_web(poll, question1 => "No", question2 => "Yellow") - logout - - travel_to(poll.ends_at + 1.day) - - visit results_poll_path(poll) - - expect(page).to have_content(question1.title) - expect(page).to have_content(question2.title) - - within("#question_#{question1.id}_results_table") do - expect(find("#option_#{option1.id}_result")).to have_content("2 (66.67%)") - expect(find("#option_#{option2.id}_result")).to have_content("1 (33.33%)") - end - - within("#question_#{question2.id}_results_table") do - expect(find("#option_#{option3.id}_result")).to have_content("1 (33.33%)") - expect(find("#option_#{option4.id}_result")).to have_content("1 (33.33%)") - expect(find("#option_#{option5.id}_result")).to have_content("1 (33.33%)") - end - end - - scenario "Results for polls with questions but without options" do - poll = create(:poll, :expired, results_enabled: true) - question = create(:poll_question, poll: poll) - - visit results_poll_path(poll) - - expect(page).to have_content question.title - end -end diff --git a/spec/system/polls/votation_types_spec.rb b/spec/system/polls/votation_types_spec.rb index 9ccbcbd97..9d5de41b5 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 - create(:poll_question_unique, :yes_no, poll: poll, title: "Is it that bad?") + scenario "Unique, multiple and open answers" do + create(:poll_question, :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 diff --git a/spec/system/polls/voter_spec.rb b/spec/system/polls/voter_spec.rb index 0a4f2947f..291fec82c 100644 --- a/spec/system/polls/voter_spec.rb +++ b/spec/system/polls/voter_spec.rb @@ -18,12 +18,9 @@ describe "Voter" do user = create(:user, :level_two) login_as user - visit poll_path(poll) - within_fieldset("Is this question stupid?") { choose "Yes" } - click_button "Vote" + vote_for_poll_via_web(poll, question => "Yes") - expect(page).to have_content "Thank you for voting!" expect(page).to have_content "You have already participated in this poll. " \ "If you vote again it will be overwritten." end