diff --git a/app/components/polls/questions/question_component.rb b/app/components/polls/questions/question_component.rb
index ba52796b5..cd74b39fc 100644
--- a/app/components/polls/questions/question_component.rb
+++ b/app/components/polls/questions/question_component.rb
@@ -1,13 +1,74 @@
class Polls::Questions::QuestionComponent < ApplicationComponent
- attr_reader :question
+ attr_reader :question, :form, :disabled
+ alias_method :disabled?, :disabled
- def initialize(question:)
+ def initialize(question, form:, disabled: false)
@question = question
+ @form = form
+ @disabled = disabled
end
- def options_read_more_links
- safe_join(question.options_with_read_more.map do |option|
- link_to option.title, "#option_#{option.id}"
- end, ", ")
- end
+ private
+
+ def fieldset_attributes
+ tag.attributes(
+ id: dom_id(question),
+ disabled: ("disabled" if disabled?),
+ class: fieldset_class,
+ data: { max_votes: question.max_votes }
+ )
+ end
+
+ def fieldset_class
+ if multiple_choice?
+ "multiple-choice"
+ else
+ "single-choice"
+ end
+ end
+
+ def options_read_more_links
+ safe_join(question.options_with_read_more.map do |option|
+ link_to option.title, "#option_#{option.id}"
+ end, ", ")
+ end
+
+ def multiple_choice?
+ question.multiple?
+ end
+
+ def multiple_choice_help_text
+ tag.span(
+ t("poll_questions.description.multiple", maximum: question.max_votes),
+ class: "help-text"
+ )
+ end
+
+ def multiple_choice_field(option)
+ choice_field(option) do
+ check_box_tag "web_vote[#{question.id}][option_id][]",
+ option.id,
+ checked?(option),
+ id: "web_vote_option_#{option.id}"
+ end
+ end
+
+ def single_choice_field(option)
+ choice_field(option) do
+ radio_button_tag "web_vote[#{question.id}][option_id]",
+ option.id,
+ checked?(option),
+ id: "web_vote_option_#{option.id}"
+ end
+ end
+
+ def choice_field(option, &block)
+ label_tag("web_vote_option_#{option.id}") do
+ block.call + option.title
+ end
+ end
+
+ def checked?(option)
+ form.object.answers[question.id].find { |answer| answer.option_id == option.id }
+ end
end
diff --git a/app/controllers/polls/answers_controller.rb b/app/controllers/polls/answers_controller.rb
deleted file mode 100644
index a969c5edf..000000000
--- a/app/controllers/polls/answers_controller.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-class Polls::AnswersController < ApplicationController
- load_and_authorize_resource :question, class: "::Poll::Question"
- load_and_authorize_resource :answer, class: "::Poll::Answer",
- through: :question,
- through_association: :answers,
- only: :destroy
-
- def create
- authorize! :answer, @question
-
- answer = @question.find_or_initialize_user_answer(current_user, params[:option_id])
- answer.save_and_record_voter_participation
-
- respond_to do |format|
- format.html do
- redirect_to request.referer
- end
- format.js do
- render :show
- end
- end
- end
-
- def destroy
- @answer.destroy_and_remove_voter_participation
-
- respond_to do |format|
- format.html do
- redirect_to request.referer
- end
- format.js do
- render :show
- end
- end
- end
-end
diff --git a/app/controllers/polls_controller.rb b/app/controllers/polls_controller.rb
index 80b72309b..d09e15812 100644
--- a/app/controllers/polls_controller.rb
+++ b/app/controllers/polls_controller.rb
@@ -9,7 +9,7 @@ class PollsController < ApplicationController
load_and_authorize_resource
has_filters %w[current expired]
- has_orders %w[most_voted newest oldest], only: :show
+ has_orders %w[most_voted newest oldest], only: [:show, :answer]
def index
@polls = Kaminari.paginate_array(
@@ -18,10 +18,25 @@ class PollsController < ApplicationController
end
def show
- @questions = @poll.questions.for_render.sort_for_list
+ @web_vote = Poll::WebVote.new(@poll, current_user)
@comment_tree = CommentTree.new(@poll, params[:page], @current_order)
end
+ def answer
+ @web_vote = Poll::WebVote.new(@poll, current_user)
+
+ if @web_vote.update(answer_params)
+ if answer_params.blank?
+ redirect_to @poll, notice: t("flash.actions.create.poll_voter_blank")
+ else
+ redirect_to @poll, notice: t("flash.actions.create.poll_voter")
+ end
+ else
+ @comment_tree = CommentTree.new(@poll, params[:page], @current_order)
+ render :show
+ end
+ end
+
def stats
@stats = Poll::Stats.new(@poll).tap(&:generate)
end
@@ -38,4 +53,8 @@ class PollsController < ApplicationController
def load_active_poll
@active_poll = ActivePoll.first
end
+
+ def answer_params
+ params[:web_vote] || {}
+ end
end
diff --git a/app/models/abilities/common.rb b/app/models/abilities/common.rb
index 369630981..429ff7ef7 100644
--- a/app/models/abilities/common.rb
+++ b/app/models/abilities/common.rb
@@ -110,12 +110,6 @@ module Abilities
can :answer, Poll do |poll|
poll.answerable_by?(user)
end
- can :answer, Poll::Question do |question|
- question.answerable_by?(user)
- end
- can :destroy, Poll::Answer do |answer|
- answer.author == user && answer.question.answerable_by?(user)
- end
end
can [:create, :show], ProposalNotification, proposal: { author_id: user.id }
diff --git a/app/models/poll.rb b/app/models/poll.rb
index 68c15face..8012f2fcd 100644
--- a/app/models/poll.rb
+++ b/app/models/poll.rb
@@ -23,6 +23,7 @@ class Poll < ApplicationRecord
has_many :officer_assignments, through: :booth_assignments
has_many :officers, through: :officer_assignments
has_many :questions, inverse_of: :poll, dependent: :destroy
+ has_many :answers, through: :questions
has_many :comments, as: :commentable, inverse_of: :commentable
has_many :ballot_sheets
diff --git a/app/models/poll/answer.rb b/app/models/poll/answer.rb
index b9c52fb38..d4b0150e2 100644
--- a/app/models/poll/answer.rb
+++ b/app/models/poll/answer.rb
@@ -17,32 +17,13 @@ class Poll::Answer < ApplicationRecord
scope :by_author, ->(author_id) { where(author_id: author_id) }
scope :by_question, ->(question_id) { where(question_id: question_id) }
- def save_and_record_voter_participation
- author.with_lock do
- save!
- Poll::Voter.find_or_create_by!(user: author, poll: poll, origin: "web")
- end
- end
-
- def destroy_and_remove_voter_participation
- transaction do
- destroy!
-
- if author.poll_answers.where(question_id: poll.question_ids).none?
- Poll::Voter.find_by(user: author, poll: poll, origin: "web").destroy!
- end
- end
- end
-
private
def max_votes
return if !question || !author || persisted?
- author.with_lock do
- if question.answers.by_author(author).count >= question.max_votes
- errors.add(:answer, "Maximum number of votes per user exceeded")
- end
+ 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 f1015bba7..93e9c4453 100644
--- a/app/models/poll/question.rb
+++ b/app/models/poll/question.rb
@@ -41,14 +41,6 @@ class Poll::Question < ApplicationRecord
end
end
- delegate :answerable_by?, to: :poll
-
- def self.answerable_by(user)
- return none if user.nil? || user.unverified?
-
- where(poll_id: Poll.answerable_by(user).pluck(:id))
- end
-
def options_total_votes
question_options.reduce(0) { |total, question_option| total + question_option.total_votes }
end
diff --git a/app/models/poll/stats.rb b/app/models/poll/stats.rb
index 8a0b1562b..5e03114b4 100644
--- a/app/models/poll/stats.rb
+++ b/app/models/poll/stats.rb
@@ -42,11 +42,11 @@ class Poll::Stats
end
def total_web_valid
- voters.where(origin: "web").count - total_web_white
+ voters.where(origin: "web", user_id: poll.answers.select(:author_id).distinct).count
end
def total_web_white
- 0
+ voters.where(origin: "web").count - total_web_valid
end
def total_web_null
diff --git a/app/models/poll/web_vote.rb b/app/models/poll/web_vote.rb
new file mode 100644
index 000000000..9ceda20e5
--- /dev/null
+++ b/app/models/poll/web_vote.rb
@@ -0,0 +1,81 @@
+class Poll::WebVote
+ include ActiveModel::Validations
+ attr_reader :poll, :user
+ delegate :t, to: "ApplicationController.helpers"
+
+ validate :max_answers
+
+ def initialize(poll, user)
+ @poll = poll
+ @user = user
+ end
+
+ def questions
+ poll.questions.for_render.sort_for_list
+ end
+
+ def answers
+ @answers ||= questions.to_h do |question|
+ [question.id, question.answers.where(author: user)]
+ end
+ end
+
+ def update(params)
+ all_valid = true
+
+ user.with_lock do
+ self.answers = given_answers(params)
+
+ questions.each do |question|
+ question.answers.where(author: user).where.not(id: answers[question.id].map(&:id)).destroy_all
+
+ if valid? && answers[question.id].all?(&:valid?)
+ Poll::Voter.find_or_create_by!(user: user, poll: poll, origin: "web")
+ answers[question.id].each(&:save!)
+ else
+ all_valid = false
+ end
+ end
+
+ raise ActiveRecord::Rollback unless all_valid
+ end
+
+ all_valid
+ end
+
+ def to_key
+ end
+
+ def persisted?
+ Poll::Voter.where(user: user, poll: poll, origin: "web").exists?
+ end
+
+ private
+
+ attr_writer :answers
+
+ def given_answers(params)
+ questions.to_h do |question|
+ [question.id, answers_for_question(question, params[question.id.to_s])]
+ end
+ end
+
+ 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)
+ end
+ end
+
+ def max_answers
+ questions.each do |question|
+ if answers[question.id].count > question.max_votes
+ errors.add(
+ :"question_#{question.id}",
+ t("polls.form.maximum_exceeded", maximum: question.max_votes, given: answers[question.id].count)
+ )
+ end
+ end
+ end
+end
diff --git a/app/views/polls/_callout.html.erb b/app/views/polls/_callout.html.erb
deleted file mode 100644
index 432d8c984..000000000
--- a/app/views/polls/_callout.html.erb
+++ /dev/null
@@ -1,22 +0,0 @@
-<% unless can?(:answer, @poll) %>
- <% if current_user.nil? %>
-
- <%= sanitize(t("polls.show.cant_answer_not_logged_in",
- signin: link_to_signin(class: "probe-message"),
- signup: link_to_signup(class: "probe-message"))) %>
-
- <%= sanitize(t("polls.show.cant_answer_verify",
- verify_link: link_to(t("polls.show.verify_link"), verification_path))) %>
-
- <% end %>
-<% end %>
diff --git a/app/views/polls/answers/show.js.erb b/app/views/polls/answers/show.js.erb
deleted file mode 100644
index 1cfab79f7..000000000
--- a/app/views/polls/answers/show.js.erb
+++ /dev/null
@@ -1 +0,0 @@
-$("#<%= dom_id(@question) %>_options").html("<%= j render Polls::Questions::OptionsComponent.new(@question) %>");
diff --git a/app/views/polls/show.html.erb b/app/views/polls/show.html.erb
index f90679a9d..0836560d3 100644
--- a/app/views/polls/show.html.erb
+++ b/app/views/polls/show.html.erb
@@ -16,29 +16,15 @@
-
<%= t("polls.show.more_info_title") %>
+ <%= t("polls.show.more_info_title") %>
<%= auto_link_already_sanitized_html simple_format(@poll.description) %>
@@ -46,7 +32,7 @@
- <%= render Polls::Questions::ReadMoreComponent.with_collection(@questions) %>
+ <%= render Polls::Questions::ReadMoreComponent.with_collection(@web_vote.questions) %>
diff --git a/config/locales/en/general.yml b/config/locales/en/general.yml
index 0c0745e00..0c4347877 100644
--- a/config/locales/en/general.yml
+++ b/config/locales/en/general.yml
@@ -577,6 +577,9 @@ en:
polls:
dates: "From %{open_at} to %{closed_at}"
final_date: "Final recounts/Results"
+ form:
+ vote: "Vote"
+ maximum_exceeded: "you've selected %{given} answers, but the maximum you can select is %{maximum}"
index:
filters:
current: "Open"
@@ -602,6 +605,7 @@ en:
show:
already_voted_in_booth: "You have already participated in a physical booth. You can not participate again."
already_voted_in_web: "You have already participated in this poll. If you vote again it will be overwritten."
+ already_voted_blank_in_web: "You have already participated in this poll by casting a blank vote. If you vote again it will be overwritten."
back: Back to voting
cant_answer_not_logged_in: "You must %{signin} or %{signup} to participate."
comments_tab: Comments
@@ -634,11 +638,7 @@ en:
poll_header:
back_to_proposal: Back to proposal
poll_questions:
- show:
- vote_answer: "Vote %{answer}"
- voted: "You have voted %{answer}"
description:
- unique: "You can select a maximum of 1 answer."
multiple: "You can select a maximum of %{maximum} answers."
read_more_about: "Read more about:"
proposal_notifications:
diff --git a/config/locales/en/responders.yml b/config/locales/en/responders.yml
index 550a5b9f1..27db5c6cc 100644
--- a/config/locales/en/responders.yml
+++ b/config/locales/en/responders.yml
@@ -10,6 +10,8 @@ en:
poll_question_option: "Answer created successfully"
poll_question_option_video: "Video created successfully"
poll_question_option_image: "Image uploaded successfully"
+ poll_voter: "Thank you for voting!"
+ poll_voter_blank: "Thank you for voting! Your vote has been registered as a blank vote."
proposal: "Proposal created successfully."
proposal_notification: "Your message has been sent correctly."
budget_investment: "Budget Investment created successfully."
diff --git a/config/locales/es/general.yml b/config/locales/es/general.yml
index db8622d81..2347e58e2 100644
--- a/config/locales/es/general.yml
+++ b/config/locales/es/general.yml
@@ -577,6 +577,9 @@ es:
polls:
dates: "Desde el %{open_at} hasta el %{closed_at}"
final_date: "Recuento final/Resultados"
+ form:
+ vote: "Votar"
+ maximum_exceeded: "has seleccionado %{given} respuestas, pero el máximo que puedes seleccionar es %{maximum}"
index:
filters:
current: "Abiertas"
@@ -602,6 +605,7 @@ es:
show:
already_voted_in_booth: "Ya has participado en esta votación en urnas presenciales, no puedes volver a participar."
already_voted_in_web: "Ya has participado en esta votación. Si vuelves a votar se sobreescribirá tu resultado anterior."
+ already_voted_blank_in_web: "Ya has participado en esta votación mediante un voto en blanco. Si vuelves a votar se sobreescribirá tu resultado anterior."
back: Volver a votaciones
cant_answer_not_logged_in: "Necesitas %{signin} o %{signup} para participar."
comments_tab: Comentarios
@@ -634,11 +638,7 @@ es:
poll_header:
back_to_proposal: Volver a la propuesta
poll_questions:
- show:
- vote_answer: "Votar %{answer}"
- voted: "Has votado %{answer}"
description:
- unique: "Puedes seleccionar un máximo de 1 respuesta."
multiple: "Puedes seleccionar un máximo de %{maximum} respuestas."
read_more_about: "Leer más:"
proposal_notifications:
diff --git a/config/locales/es/responders.yml b/config/locales/es/responders.yml
index 49a78fad1..059804ad9 100644
--- a/config/locales/es/responders.yml
+++ b/config/locales/es/responders.yml
@@ -10,6 +10,8 @@ es:
poll_question_option: "Respuesta creada correctamente"
poll_question_option_video: "Vídeo creado correctamente"
poll_question_option_image: "Imagen cargada correctamente"
+ poll_voter: "¡Gracias por votar!"
+ poll_voter_blank: "¡Gracias por votar! Tu voto se ha contabilizado como en blanco."
proposal: "Propuesta creada correctamente."
proposal_notification: "Tu mensaje ha sido enviado correctamente."
budget_investment: "Proyecto de gasto creado correctamente."
diff --git a/config/routes/poll.rb b/config/routes/poll.rb
index 754c7c4d2..9c44c0f21 100644
--- a/config/routes/poll.rb
+++ b/config/routes/poll.rb
@@ -2,9 +2,6 @@ resources :polls, only: [:show, :index] do
member do
get :stats
get :results
- end
-
- resources :questions, controller: "polls/questions", shallow: true, only: [] do
- resources :answers, controller: "polls/answers", only: [:create, :destroy], shallow: false
+ post :answer
end
end
diff --git a/spec/components/polls/callout_component_spec.rb b/spec/components/polls/callout_component_spec.rb
new file mode 100644
index 000000000..40866e636
--- /dev/null
+++ b/spec/components/polls/callout_component_spec.rb
@@ -0,0 +1,27 @@
+require "rails_helper"
+
+describe Polls::CalloutComponent do
+ it "asks anonymous users to sign in" do
+ render_inline Polls::CalloutComponent.new(create(:poll))
+
+ expect(page).to have_content "You must sign in or sign up to participate"
+ end
+
+ it "shows a message to level 2 users when a poll has finished" do
+ sign_in(create(:user, :level_two))
+
+ render_inline Polls::CalloutComponent.new(create(:poll, :expired))
+
+ expect(page).to have_content "This poll has finished"
+ end
+
+ it "asks unverified users to verify their account" do
+ sign_in(create(:user, :incomplete_verification))
+
+ render_inline Polls::CalloutComponent.new(create(:poll))
+
+ expect(page).to have_content "You must verify your account in order to answer"
+ expect(page).not_to have_content "You have already participated in this poll. " \
+ "If you vote again it will be overwritten"
+ end
+end
diff --git a/spec/components/polls/form_component_spec.rb b/spec/components/polls/form_component_spec.rb
new file mode 100644
index 000000000..43f7eeea0
--- /dev/null
+++ b/spec/components/polls/form_component_spec.rb
@@ -0,0 +1,88 @@
+require "rails_helper"
+
+describe Polls::FormComponent do
+ let(:user) { create(:user, :level_two) }
+ let(:poll) { create(:poll) }
+ let(:web_vote) { Poll::WebVote.new(poll, user) }
+ before { create(:poll_question, :yes_no, poll: poll) }
+
+ it "renders disabled fields when the user has already voted in a booth" do
+ create(:poll_voter, :from_booth, poll: poll, user: user)
+ sign_in(user)
+
+ render_inline Polls::FormComponent.new(web_vote)
+
+ page.find("fieldset[disabled]") do |fieldset|
+ expect(fieldset).to have_field "Yes"
+ expect(fieldset).to have_field "No"
+ end
+
+ expect(page).to have_button "Vote", disabled: true
+ end
+
+ it "renders disabled answers to unverified users" do
+ sign_in(create(:user, :incomplete_verification))
+
+ render_inline Polls::FormComponent.new(web_vote)
+
+ page.find("fieldset[disabled]") do |fieldset|
+ expect(fieldset).to have_field "Yes"
+ expect(fieldset).to have_field "No"
+ end
+
+ expect(page).to have_button "Vote", disabled: true
+ end
+
+ context "expired poll" do
+ let(:poll) { create(:poll, :expired) }
+
+ it "renders disabled fields when the poll has expired" do
+ sign_in(user)
+
+ render_inline Polls::FormComponent.new(web_vote)
+
+ page.find("fieldset[disabled]") do |fieldset|
+ expect(fieldset).to have_field "Yes"
+ expect(fieldset).to have_field "No"
+ end
+
+ expect(page).to have_button "Vote", disabled: true
+ end
+ end
+
+ context "geozone restricted poll" do
+ let(:poll) { create(:poll, geozone_restricted: true) }
+ let(:geozone) { create(:geozone) }
+ before { poll.geozones << geozone }
+
+ context "user from another geozone" do
+ let(:user) { create(:user, :level_two) }
+ before { sign_in(user) }
+
+ it "renders disabled fields" do
+ render_inline Polls::FormComponent.new(web_vote)
+
+ page.find("fieldset[disabled]") do |fieldset|
+ expect(fieldset).to have_field "Yes"
+ expect(fieldset).to have_field "No"
+ end
+
+ expect(page).to have_button "Vote", disabled: true
+ end
+ end
+
+ context "user from the same geozone" do
+ let(:user) { create(:user, :level_two, geozone: geozone) }
+ before { sign_in(user) }
+
+ it "renders enabled answers" do
+ render_inline Polls::FormComponent.new(web_vote)
+
+ expect(page).not_to have_css "fieldset[disabled]"
+ expect(page).to have_field "Yes"
+ expect(page).to have_field "No"
+ expect(page).to have_button "Vote"
+ end
+ end
+ end
+end
diff --git a/spec/components/polls/questions/options_component_spec.rb b/spec/components/polls/questions/options_component_spec.rb
deleted file mode 100644
index c50416cd5..000000000
--- a/spec/components/polls/questions/options_component_spec.rb
+++ /dev/null
@@ -1,112 +0,0 @@
-require "rails_helper"
-
-describe Polls::Questions::OptionsComponent do
- include Rails.application.routes.url_helpers
- let(:poll) { create(:poll) }
- let(:question) { create(:poll_question, :yes_no, poll: poll) }
-
- it "renders answers in given order" do
- render_inline Polls::Questions::OptionsComponent.new(question)
-
- expect("Yes").to appear_before("No")
- end
-
- it "renders buttons to vote question answers" do
- sign_in(create(:user, :verified))
-
- render_inline Polls::Questions::OptionsComponent.new(question)
-
- expect(page).to have_button "Yes"
- expect(page).to have_button "No"
- expect(page).to have_css "button[aria-pressed='false']", count: 2
- end
-
- it "renders button to destroy current user answers" do
- user = create(:user, :verified)
- create(:poll_answer, author: user, question: question, answer: "Yes")
- sign_in(user)
-
- render_inline Polls::Questions::OptionsComponent.new(question)
-
- expect(page).to have_button "You have voted Yes"
- expect(page).to have_button "Vote No"
- expect(page).to have_css "button[aria-pressed='true']", text: "Yes"
- end
-
- it "renders disabled buttons when max votes is reached" do
- user = create(:user, :verified)
- question = create(:poll_question_multiple, :abc, max_votes: 2, author: user)
- create(:poll_answer, author: user, question: question, answer: "Answer A")
- create(:poll_answer, author: user, question: question, answer: "Answer C")
- sign_in(user)
-
- render_inline Polls::Questions::OptionsComponent.new(question)
-
- expect(page).to have_button "You have voted Answer A"
- expect(page).to have_button "Vote Answer B", disabled: true
- expect(page).to have_button "You have voted Answer C"
- end
-
- it "when user is not signed in, renders answers links pointing to user sign in path" do
- render_inline Polls::Questions::OptionsComponent.new(question)
-
- expect(page).to have_link "Yes", href: new_user_session_path
- expect(page).to have_link "No", href: new_user_session_path
- end
-
- it "when user is not verified, renders answers links pointing to user verification in path" do
- sign_in(create(:user))
-
- render_inline Polls::Questions::OptionsComponent.new(question)
-
- expect(page).to have_link "Yes", href: verification_path
- expect(page).to have_link "No", href: verification_path
- end
-
- it "when user already voted in booth it renders disabled answers" do
- user = create(:user, :level_two)
- create(:poll_voter, :from_booth, poll: poll, user: user)
- sign_in(user)
-
- render_inline Polls::Questions::OptionsComponent.new(question)
-
- expect(page).to have_css "span.disabled", text: "Yes"
- expect(page).to have_css "span.disabled", text: "No"
- end
-
- it "user cannot vote when poll expired it renders disabled answers" do
- question = create(:poll_question, :yes_no, poll: create(:poll, :expired))
- sign_in(create(:user, :level_two))
-
- render_inline Polls::Questions::OptionsComponent.new(question)
-
- expect(page).to have_css "span.disabled", text: "Yes"
- expect(page).to have_css "span.disabled", text: "No"
- end
-
- describe "geozone" do
- let(:poll) { create(:poll, geozone_restricted: true) }
- let(:geozone) { create(:geozone) }
- let(:question) { create(:poll_question, :yes_no, poll: poll) }
-
- it "when geozone which is not theirs it renders disabled answers" do
- poll.geozones << geozone
- sign_in(create(:user, :level_two))
-
- render_inline Polls::Questions::OptionsComponent.new(question)
-
- expect(page).to have_css "span.disabled", text: "Yes"
- expect(page).to have_css "span.disabled", text: "No"
- end
-
- it "reading a same-geozone poll it renders buttons to vote question answers" do
- poll.geozones << geozone
- sign_in(create(:user, :level_two, geozone: geozone))
-
- render_inline Polls::Questions::OptionsComponent.new(question)
-
- expect(page).to have_button "Yes"
- expect(page).to have_button "No"
- end
- end
-end
diff --git a/spec/components/polls/questions/question_component_spec.rb b/spec/components/polls/questions/question_component_spec.rb
index 607906ad6..1b0e7c219 100644
--- a/spec/components/polls/questions/question_component_spec.rb
+++ b/spec/components/polls/questions/question_component_spec.rb
@@ -1,18 +1,73 @@
require "rails_helper"
describe Polls::Questions::QuestionComponent do
+ let(:poll) { create(:poll) }
+ let(:question) { create(:poll_question, :yes_no, poll: poll) }
+ let(:option_yes) { question.question_options.find_by(title: "Yes") }
+ let(:option_no) { question.question_options.find_by(title: "No") }
+ let(:user) { User.new }
+ let(:web_vote) { Poll::WebVote.new(poll, user) }
+ let(:form) { ConsulFormBuilder.new(:web_vote, web_vote, ApplicationController.new.view_context, {}) }
+
it "renders more information links when any question option has additional information" do
- question = create(:poll_question)
- option_a = create(:poll_question_option, question: question, title: "Answer A")
- option_b = create(:poll_question_option, question: question, title: "Answer B")
allow_any_instance_of(Poll::Question::Option).to receive(:with_read_more?).and_return(true)
- render_inline Polls::Questions::QuestionComponent.new(question: question)
+ render_inline Polls::Questions::QuestionComponent.new(question, form: form)
- poll_question = page.find("#poll_question_#{question.id}")
- expect(poll_question).to have_content("Read more about")
- expect(poll_question).to have_link("Answer A", href: "#option_#{option_a.id}")
- expect(poll_question).to have_link("Answer B", href: "#option_#{option_b.id}")
- expect(poll_question).to have_content("Answer A, Answer B")
+ page.find("#poll_question_#{question.id}") do |poll_question|
+ expect(poll_question).to have_content "Read more about"
+ expect(poll_question).to have_link "Yes", href: "#option_#{option_yes.id}"
+ expect(poll_question).to have_link "No", href: "#option_#{option_no.id}"
+ expect(poll_question).to have_content "Yes, No"
+ end
+ end
+
+ it "renders answers in given order" do
+ render_inline Polls::Questions::QuestionComponent.new(question, form: form)
+
+ expect("Yes").to appear_before("No")
+ end
+
+ it "renders disabled answers when given the disabled parameter" do
+ render_inline Polls::Questions::QuestionComponent.new(question, form: form, disabled: true)
+
+ page.find("fieldset[disabled]") do |fieldset|
+ expect(fieldset).to have_field "Yes"
+ expect(fieldset).to have_field "No"
+ end
+ end
+
+ context "Verified user" do
+ let(:user) { create(:user, :level_two) }
+ before { sign_in(user) }
+
+ it "renders radio buttons for single-choice questions" do
+ render_inline Polls::Questions::QuestionComponent.new(question, form: form)
+
+ expect(page).to have_field "Yes", type: :radio
+ expect(page).to have_field "No", type: :radio
+ expect(page).to have_field type: :radio, checked: false, count: 2
+ end
+
+ it "renders checkboxes for multiple-choice questions" do
+ question = create(:poll_question_multiple, :abc, poll: poll)
+
+ render_inline Polls::Questions::QuestionComponent.new(question, form: form)
+
+ expect(page).to have_field "Answer A", type: :checkbox
+ expect(page).to have_field "Answer B", type: :checkbox
+ expect(page).to have_field "Answer C", type: :checkbox
+ expect(page).to have_field type: :checkbox, checked: false, count: 3
+ expect(page).not_to have_field type: :checkbox, checked: true
+ end
+
+ it "selects the option when users have already voted" do
+ create(:poll_answer, author: user, question: question, option: option_yes)
+
+ render_inline Polls::Questions::QuestionComponent.new(question, form: form)
+
+ expect(page).to have_field "Yes", type: :radio, checked: true
+ expect(page).to have_field "No", type: :radio, checked: false
+ end
end
end
diff --git a/spec/controllers/polls/answers_controller_spec.rb b/spec/controllers/polls/answers_controller_spec.rb
deleted file mode 100644
index a5286da3c..000000000
--- a/spec/controllers/polls/answers_controller_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-require "rails_helper"
-
-describe Polls::AnswersController do
- describe "POST create" do
- it "doesn't create duplicate records on simultaneous requests", :race_condition do
- question = create(:poll_question_multiple, :abc)
- sign_in(create(:user, :level_two))
-
- 2.times.map do
- Thread.new do
- post :create, params: {
- question_id: question.id,
- option_id: question.question_options.find_by(title: "Answer A").id,
- format: :js
- }
- rescue ActiveRecord::RecordInvalid
- end
- end.each(&:join)
-
- expect(Poll::Answer.count).to eq 1
- end
- end
-end
diff --git a/spec/controllers/polls_controller_spec.rb b/spec/controllers/polls_controller_spec.rb
index 296c1a0d6..6a0a03c2e 100644
--- a/spec/controllers/polls_controller_spec.rb
+++ b/spec/controllers/polls_controller_spec.rb
@@ -8,4 +8,25 @@ describe PollsController do
expect { get :index }.to raise_exception(FeatureFlags::FeatureDisabled)
end
end
+
+ describe "POST answer" do
+ it "doesn't create duplicate records on simultaneous requests", :race_condition do
+ question = create(:poll_question_multiple, :abc)
+ sign_in(create(:user, :level_two))
+
+ 2.times.map do
+ Thread.new do
+ post :answer, params: {
+ id: question.poll.id,
+ web_vote: {
+ question.id.to_s => { option_id: question.question_options.find_by(title: "Answer A").id }
+ }
+ }
+ rescue AbstractController::DoubleRenderError
+ end
+ end.each(&:join)
+
+ expect(Poll::Answer.count).to eq 1
+ end
+ end
end
diff --git a/spec/factories/polls.rb b/spec/factories/polls.rb
index 735c20cb8..100029ec7 100644
--- a/spec/factories/polls.rb
+++ b/spec/factories/polls.rb
@@ -212,8 +212,14 @@ FactoryBot.define do
factory :poll_answer, class: "Poll::Answer" do
question factory: [:poll_question, :yes_no]
author factory: [:user, :level_two]
- answer { question.question_options.sample.title }
- option { question.question_options.find_by(title: answer) }
+ option do
+ if answer
+ question.question_options.find_by(title: answer)
+ else
+ question.question_options.sample
+ end
+ end
+ after(:build) { |poll_answer| poll_answer.answer ||= poll_answer.option&.title }
end
factory :poll_partial_result, class: "Poll::PartialResult" do
diff --git a/spec/models/abilities/common_spec.rb b/spec/models/abilities/common_spec.rb
index f79295d13..3aa184bf7 100644
--- a/spec/models/abilities/common_spec.rb
+++ b/spec/models/abilities/common_spec.rb
@@ -51,18 +51,6 @@ describe Abilities::Common do
let(:poll_from_own_geozone) { create(:poll, geozone_restricted_to: [geozone]) }
let(:poll_from_other_geozone) { create(:poll, geozone_restricted_to: [create(:geozone)]) }
- let(:poll_question_from_own_geozone) { create(:poll_question, poll: poll_from_own_geozone) }
- let(:poll_question_from_other_geozone) { create(:poll_question, poll: poll_from_other_geozone) }
- let(:poll_question_from_all_geozones) { create(:poll_question, poll: poll) }
-
- let(:expired_poll_question_from_own_geozone) do
- create(:poll_question, poll: expired_poll_from_own_geozone)
- end
- let(:expired_poll_question_from_other_geozone) do
- create(:poll_question, poll: expired_poll_from_other_geozone)
- end
- let(:expired_poll_question_from_all_geozones) { create(:poll_question, poll: expired_poll) }
-
let(:own_proposal_document) { build(:document, documentable: own_proposal) }
let(:proposal_document) { build(:document, documentable: proposal) }
let(:own_budget_investment_document) { build(:document, documentable: own_investment_in_accepting_budget) }
@@ -252,39 +240,25 @@ describe Abilities::Common do
end
describe "Poll" do
- it { should be_able_to(:answer, current_poll) }
- it { should_not be_able_to(:answer, expired_poll) }
+ it { should be_able_to(:answer, current_poll) }
+ it { should_not be_able_to(:answer, expired_poll) }
- it { should be_able_to(:answer, poll_question_from_own_geozone) }
- it { should be_able_to(:answer, poll_question_from_all_geozones) }
- it { should_not be_able_to(:answer, poll_question_from_other_geozone) }
+ it { should be_able_to(:answer, poll_from_own_geozone) }
+ it { should be_able_to(:answer, poll) }
+ it { should_not be_able_to(:answer, poll_from_other_geozone) }
- it { should_not be_able_to(:answer, expired_poll_question_from_own_geozone) }
- it { should_not be_able_to(:answer, expired_poll_question_from_all_geozones) }
- it { should_not be_able_to(:answer, expired_poll_question_from_other_geozone) }
-
- context "Poll::Answer" do
- let(:own_answer) { create(:poll_answer, author: user) }
- let(:other_user_answer) { create(:poll_answer) }
- let(:expired_poll) { create(:poll, :expired) }
- let(:question) { create(:poll_question, :yes_no, poll: expired_poll) }
- let(:expired_poll_answer) { create(:poll_answer, author: user, question: question, answer: "Yes") }
-
- it { should be_able_to(:destroy, own_answer) }
- it { should_not be_able_to(:destroy, other_user_answer) }
- it { should_not be_able_to(:destroy, expired_poll_answer) }
- end
+ it { should_not be_able_to(:answer, expired_poll_from_own_geozone) }
+ it { should_not be_able_to(:answer, expired_poll_from_other_geozone) }
context "without geozone" do
before { user.geozone = nil }
- it { should_not be_able_to(:answer, poll_question_from_own_geozone) }
- it { should be_able_to(:answer, poll_question_from_all_geozones) }
- it { should_not be_able_to(:answer, poll_question_from_other_geozone) }
+ it { should_not be_able_to(:answer, poll_from_own_geozone) }
+ it { should be_able_to(:answer, poll) }
+ it { should_not be_able_to(:answer, poll_from_other_geozone) }
- it { should_not be_able_to(:answer, expired_poll_question_from_own_geozone) }
- it { should_not be_able_to(:answer, expired_poll_question_from_all_geozones) }
- it { should_not be_able_to(:answer, expired_poll_question_from_other_geozone) }
+ it { should_not be_able_to(:answer, expired_poll_from_own_geozone) }
+ it { should_not be_able_to(:answer, expired_poll_from_other_geozone) }
end
end
@@ -339,26 +313,25 @@ describe Abilities::Common do
it { should be_able_to(:show, own_direct_message) }
it { should_not be_able_to(:show, create(:direct_message)) }
- it { should be_able_to(:answer, current_poll) }
- it { should_not be_able_to(:answer, expired_poll) }
+ it { should be_able_to(:answer, current_poll) }
+ it { should_not be_able_to(:answer, expired_poll) }
- it { should be_able_to(:answer, poll_question_from_own_geozone) }
- it { should be_able_to(:answer, poll_question_from_all_geozones) }
- it { should_not be_able_to(:answer, poll_question_from_other_geozone) }
+ it { should be_able_to(:answer, poll_from_own_geozone) }
+ it { should be_able_to(:answer, poll) }
+ it { should_not be_able_to(:answer, poll_from_other_geozone) }
- it { should_not be_able_to(:answer, expired_poll_question_from_own_geozone) }
- it { should_not be_able_to(:answer, expired_poll_question_from_all_geozones) }
- it { should_not be_able_to(:answer, expired_poll_question_from_other_geozone) }
+ it { should_not be_able_to(:answer, expired_poll_from_own_geozone) }
+ it { should_not be_able_to(:answer, expired_poll_from_other_geozone) }
context "without geozone" do
before { user.geozone = nil }
- it { should_not be_able_to(:answer, poll_question_from_own_geozone) }
- it { should be_able_to(:answer, poll_question_from_all_geozones) }
- it { should_not be_able_to(:answer, poll_question_from_other_geozone) }
+ it { should_not be_able_to(:answer, poll_from_own_geozone) }
+ it { should be_able_to(:answer, poll) }
+ it { should_not be_able_to(:answer, poll_from_other_geozone) }
- it { should_not be_able_to(:answer, expired_poll_question_from_own_geozone) }
- it { should_not be_able_to(:answer, expired_poll_question_from_all_geozones) }
- it { should_not be_able_to(:answer, expired_poll_question_from_other_geozone) }
+ it { should_not be_able_to(:answer, expired_poll_from_own_geozone) }
+ it { should_not be_able_to(:answer, expired_poll) }
+ it { should_not be_able_to(:answer, expired_poll_from_other_geozone) }
end
end
diff --git a/spec/models/poll/answer_spec.rb b/spec/models/poll/answer_spec.rb
index 4f3ca154e..aec882fc5 100644
--- a/spec/models/poll/answer_spec.rb
+++ b/spec/models/poll/answer_spec.rb
@@ -101,136 +101,5 @@ describe Poll::Answer do
expect(build(:poll_answer, question: question, answer: "Four")).not_to be_valid
end
-
- context "creating answers at the same time", :race_condition do
- it "validates max votes on single-answer questions" do
- author = create(:user)
- question = create(:poll_question, :yes_no)
-
- answer = build(:poll_answer, author: author, question: question, answer: "Yes")
- other_answer = build(:poll_answer, author: author, question: question, answer: "No")
-
- [answer, other_answer].map do |poll_answer|
- Thread.new { poll_answer.save }
- end.each(&:join)
-
- expect(Poll::Answer.count).to be 1
- end
-
- it "validates max votes on multiple-answer questions" do
- author = create(:user, :level_two)
- question = create(:poll_question_multiple, :abc, max_votes: 2)
- create(:poll_answer, question: question, answer: "Answer A", author: author)
- answer = build(:poll_answer, question: question, answer: "Answer B", author: author)
- other_answer = build(:poll_answer, question: question, answer: "Answer C", author: author)
-
- [answer, other_answer].map do |poll_answer|
- Thread.new { poll_answer.save }
- end.each(&:join)
-
- expect(Poll::Answer.count).to be 2
- end
- end
- end
-
- describe "#save_and_record_voter_participation" do
- let(:author) { create(:user, :level_two) }
- let(:poll) { create(:poll) }
- let(:question) { create(:poll_question, :yes_no, poll: poll) }
-
- it "creates a poll_voter with user and poll data" do
- answer = create(:poll_answer, question: question, author: author, answer: "Yes")
- expect(answer.poll.voters).to be_blank
-
- answer.save_and_record_voter_participation
- expect(poll.reload.voters.size).to eq(1)
- voter = poll.voters.first
-
- expect(voter.document_number).to eq(answer.author.document_number)
- expect(voter.poll_id).to eq(answer.poll.id)
- expect(voter.officer_id).to be nil
- end
-
- it "updates a poll_voter with user and poll data" do
- answer = create(:poll_answer, question: question, author: author, answer: "Yes")
- answer.save_and_record_voter_participation
-
- expect(poll.reload.voters.size).to eq(1)
-
- updated_answer = answer.question.find_or_initialize_user_answer(
- answer.author,
- answer.question.question_options.excluding(answer.option).sample.id
- )
- updated_answer.save_and_record_voter_participation
-
- expect(poll.reload.voters.size).to eq(1)
-
- voter = poll.voters.first
- expect(voter.document_number).to eq(updated_answer.author.document_number)
- expect(voter.poll_id).to eq(updated_answer.poll.id)
- 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)
- answer = build(:poll_answer)
-
- expect do
- answer.save_and_record_voter_participation
- end.to raise_error(ActiveRecord::RecordInvalid)
-
- expect(answer).not_to be_persisted
- end
-
- it "does not create two voters when creating two answers at the same time", :race_condition do
- answer = build(:poll_answer, question: question, author: author, answer: "Yes")
- other_answer = build(:poll_answer, question: question, author: author, answer: "No")
-
- [answer, other_answer].map do |poll_answer|
- Thread.new do
- poll_answer.save_and_record_voter_participation
- rescue ActiveRecord::RecordInvalid
- end
- end.each(&:join)
-
- expect(Poll::Voter.count).to be 1
- end
-
- it "does not create two voters when calling the method twice at the same time", :race_condition do
- answer = create(:poll_answer, question: question, author: author, answer: "Yes")
-
- 2.times.map do
- Thread.new { answer.save_and_record_voter_participation }
- end.each(&:join)
-
- expect(Poll::Voter.count).to be 1
- end
- end
-
- describe "#destroy_and_remove_voter_participation" do
- let(:poll) { create(:poll) }
- let(:question) { create(:poll_question, :yes_no, poll: poll) }
-
- it "destroys voter record and answer when it was the only user's answer" do
- answer = build(:poll_answer, question: question)
- answer.save_and_record_voter_participation
-
- expect { answer.destroy_and_remove_voter_participation }
- .to change { Poll::Answer.count }.by(-1)
- .and change { Poll::Voter.count }.by(-1)
- end
-
- it "destroys the answer but does not destroy the voter record when the user
- has answered other poll questions" do
- answer = build(:poll_answer, question: question)
- answer.save_and_record_voter_participation
- other_question = create(:poll_question, :yes_no, poll: poll)
- other_answer = build(:poll_answer, question: other_question, author: answer.author)
- other_answer.save_and_record_voter_participation
-
- expect(other_answer).to be_persisted
- expect { answer.destroy_and_remove_voter_participation }
- .to change { Poll::Answer.count }.by(-1)
- .and change { Poll::Voter.count }.by(0)
- end
end
end
diff --git a/spec/models/poll/stats_spec.rb b/spec/models/poll/stats_spec.rb
index 7b962a2c0..0debf20e0 100644
--- a/spec/models/poll/stats_spec.rb
+++ b/spec/models/poll/stats_spec.rb
@@ -14,8 +14,6 @@ describe Poll::Stats do
end
describe "total participants" do
- before { allow(stats).to receive(:total_web_white).and_return(1) }
-
it "supports every channel" do
3.times { create(:poll_voter, :from_web, poll: poll) }
create(:poll_recount, :from_booth, poll: poll,
@@ -49,15 +47,29 @@ describe Poll::Stats do
end
describe "#total_web_valid" do
- before { allow(stats).to receive(:total_web_white).and_return(1) }
+ it "returns only votes containing answers" do
+ question = create(:poll_question, :yes_no, poll: poll)
- it "returns only valid votes" do
- 3.times { create(:poll_voter, :from_web, poll: poll) }
+ 2.times do
+ voter = create(:poll_voter, :from_web, poll: poll)
+ create(:poll_answer, author: voter.user, question: question)
+ end
+ create(:poll_voter, :from_web, poll: poll)
expect(stats.total_web_valid).to eq(2)
end
end
+ describe "#total_web_white" do
+ it "returns voters with no answers" do
+ question = create(:poll_question, :yes_no, poll: poll)
+ 3.times { create(:poll_voter, :from_web, poll: poll) }
+ create(:poll_answer, author: poll.voters.last.user, question: question)
+
+ expect(stats.total_web_white).to eq(2)
+ end
+ end
+
describe "#total_web_null" do
it "returns 0" do
expect(stats.total_web_null).to eq(0)
@@ -93,8 +105,8 @@ describe Poll::Stats do
describe "valid percentage by channel" do
it "is relative to the total amount of valid votes" do
+ allow(stats).to receive(:total_web_valid).and_return(1)
create(:poll_recount, :from_booth, poll: poll, total_amount: 2)
- create(:poll_voter, :from_web, poll: poll)
expect(stats.valid_percentage_web).to eq(33.333)
expect(stats.valid_percentage_booth).to eq(66.667)
@@ -123,7 +135,7 @@ describe Poll::Stats do
describe "#total_valid_votes" do
it "counts valid votes from every channel" do
- 2.times { create(:poll_voter, :from_web, poll: poll) }
+ allow(stats).to receive(:total_web_valid).and_return(2)
create(:poll_recount, :from_booth, poll: poll, total_amount: 3, white_amount: 10)
create(:poll_recount, :from_booth, poll: poll, total_amount: 4, null_amount: 20)
@@ -150,10 +162,9 @@ describe Poll::Stats do
end
describe "total percentage by type" do
- before { allow(stats).to receive(:total_web_white).and_return(1) }
+ before { allow(stats).to receive_messages(total_web_white: 1, total_web_valid: 2) }
it "is relative to the total amount of votes" do
- 3.times { create(:poll_voter, :from_web, poll: poll) }
create(:poll_recount, :from_booth, poll: poll,
total_amount: 8,
white_amount: 5,
diff --git a/spec/models/poll/web_vote_spec.rb b/spec/models/poll/web_vote_spec.rb
new file mode 100644
index 000000000..f150b7a12
--- /dev/null
+++ b/spec/models/poll/web_vote_spec.rb
@@ -0,0 +1,111 @@
+require "rails_helper"
+
+describe Poll::WebVote do
+ describe "#update" do
+ let(:user) { create(:user, :level_two) }
+ let(:poll) { create(:poll) }
+ let!(:question) { create(:poll_question, :yes_no, poll: poll) }
+ let(:option_yes) { question.question_options.find_by(title: "Yes") }
+ let(:option_no) { question.question_options.find_by(title: "No") }
+ let(:web_vote) { Poll::WebVote.new(poll, user) }
+
+ it "creates a poll_voter with user and poll data" do
+ expect(poll.voters).to be_blank
+ expect(question.answers).to be_blank
+
+ web_vote.update(question.id.to_s => { option_id: option_yes.id.to_s })
+
+ expect(poll.reload.voters.size).to eq 1
+ expect(question.reload.answers.size).to eq 1
+
+ voter = poll.voters.first
+ answer = question.answers.first
+
+ expect(answer.author).to eq user
+ expect(voter.document_number).to eq user.document_number
+ expect(voter.poll_id).to eq answer.poll.id
+ expect(voter.officer_id).to be nil
+ end
+
+ it "updates a poll_voter with user and poll data" do
+ answer = create(:poll_answer, question: question, author: user, option: option_yes)
+
+ web_vote.update(question.id.to_s => { option_id: option_no.id.to_s })
+
+ expect(poll.reload.voters.size).to eq 1
+ expect(question.reload.answers.size).to eq 1
+ expect(question.answers.first).to eq answer.reload
+
+ voter = poll.voters.first
+
+ expect(answer.author).to eq user
+ expect(answer.option).to eq option_no
+ expect(voter.document_number).to eq answer.author.document_number
+ expect(voter.poll_id).to eq answer.poll.id
+ 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)
+
+ expect do
+ web_vote.update(question.id.to_s => { option_id: option_yes.id.to_s })
+ end.to raise_error(ActiveRecord::RecordInvalid)
+
+ 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({})
+
+ expect(poll.reload.voters.size).to eq 1
+ expect(question.reload.answers.size).to eq 0
+ end
+
+ it "deletes existing answers but keeps voters when no answers are given" do
+ create(:poll_answer, question: question, author: user, option: option_yes)
+ create(:poll_voter, poll: poll, user: user)
+
+ web_vote.update({})
+
+ expect(poll.reload.voters.size).to eq 1
+ expect(poll.voters.first.user).to eq user
+ expect(question.reload.answers.size).to eq 0
+ end
+
+ context "creating answers at the same time", :race_condition do
+ it "does not create two voters or two answers for two different answers" do
+ [option_yes, option_no].map do |option|
+ Thread.new { web_vote.update(question.id.to_s => { option_id: option.id.to_s }) }
+ end.each(&:join)
+
+ expect(Poll::Voter.count).to be 1
+ expect(Poll::Answer.count).to be 1
+ end
+
+ it "does not create two voters for duplicate answers" do
+ 2.times.map do
+ Thread.new { web_vote.update(question.id.to_s => { option_id: option_yes.id.to_s }) }
+ end.each(&:join)
+
+ expect(Poll::Voter.count).to be 1
+ end
+
+ it "validates max votes on multiple-answer questions" 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, question: question, author: user, option: option_a)
+
+ [option_b, option_c].map do |option|
+ Thread.new do
+ web_vote.update(question.id.to_s => { option_id: [option_a.id.to_s, option.id.to_s] })
+ end
+ end.each(&:join)
+
+ expect(Poll::Answer.count).to be 2
+ end
+ end
+ end
+end
diff --git a/spec/support/common_actions/polls.rb b/spec/support/common_actions/polls.rb
index 0340567f6..f6d064f51 100644
--- a/spec/support/common_actions/polls.rb
+++ b/spec/support/common_actions/polls.rb
@@ -1,13 +1,14 @@
module Polls
- def vote_for_poll_via_web(poll, question, option)
+ def vote_for_poll_via_web(poll, questions_with_options)
visit poll_path(poll)
- within("#poll_question_#{question.id}_options") do
- click_button option
-
- expect(page).to have_button("You have voted #{option}")
- expect(page).not_to have_button("Vote #{option}")
+ questions_with_options.each do |question, option|
+ within_fieldset(question.title) { choose option }
end
+
+ click_button "Vote"
+
+ expect(page).to have_content "Thank you for voting!"
end
def vote_for_poll_via_booth
diff --git a/spec/system/polls/polls_spec.rb b/spec/system/polls/polls_spec.rb
index 0030460c8..c18cbbdd2 100644
--- a/spec/system/polls/polls_spec.rb
+++ b/spec/system/polls/polls_spec.rb
@@ -96,7 +96,7 @@ describe "Polls" do
expect(page).not_to have_css ".already-answer"
- vote_for_poll_via_web(poll_with_question, question, "Yes")
+ vote_for_poll_via_web(poll_with_question, question => "Yes")
visit polls_path
@@ -168,14 +168,6 @@ describe "Polls" do
end
end
- scenario "Non-logged in users" do
- create(:poll_question, :yes_no, poll: poll)
-
- visit poll_path(poll)
-
- expect(page).to have_content("You must sign in or sign up to participate")
- end
-
scenario "Level 1 users" do
poll.update!(geozone_restricted_to: [geozone])
create(:poll_question, :yes_no, poll: poll)
@@ -186,54 +178,85 @@ describe "Polls" do
expect(page).to have_content("You must verify your account in order to answer")
end
- scenario "Level 2 users in an expired poll" do
- expired_poll = create(:poll, :expired)
- create(:poll_question, :yes_no, poll: expired_poll)
-
- login_as(create(:user, :level_two, geozone: geozone))
-
- visit poll_path(expired_poll)
-
- expect(page).to have_content("This poll has finished")
- end
-
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)
- user = create(:user, :level_two, geozone: geozone)
-
- login_as user
+ login_as(create(:user, :level_two, geozone: geozone))
visit poll_path(poll)
- within("#poll_question_#{question.id}_options") do
- click_button "Vote Yes"
+ within_fieldset("Do you agree?") { choose "Yes" }
+ click_button "Vote"
- expect(page).to have_button "You have voted Yes"
- expect(page).to have_button "Vote No"
+ 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."
+
+ within_fieldset("Do you agree?") do
+ expect(page).to have_field "Yes", type: :radio, checked: true
end
+
+ expect(page).to have_button "Vote"
end
scenario "Level 2 users changing answer" do
- poll.update!(geozone_restricted_to: [geozone])
-
- question = create(:poll_question, :yes_no, poll: poll)
user = create(:user, :level_two, geozone: geozone)
+ question = create(:poll_question, :yes_no, poll: poll, title: "Do you agree?")
+
+ poll.update!(geozone_restricted_to: [geozone])
+ create(:poll_answer, author: user, question: question, answer: "Yes")
+ create(:poll_voter, poll: poll, user: user)
login_as user
visit poll_path(poll)
- within("#poll_question_#{question.id}_options") do
- click_button "Yes"
+ expect(page).to have_content "You have already participated in this poll. " \
+ "If you vote again it will be overwritten."
- expect(page).to have_button "You have voted Yes"
- expect(page).to have_button "Vote No"
+ within_fieldset("Do you agree?") do
+ expect(page).to have_field "Yes", type: :radio, checked: true
- click_button "No"
-
- expect(page).to have_button "Vote Yes"
- expect(page).to have_button "You have voted No"
+ choose "No"
end
+
+ click_button "Vote"
+
+ expect(page).to have_content "Thank you for voting!"
+
+ within_fieldset("Do you agree?") do
+ expect(page).to have_field "No", type: :radio, checked: true
+ expect(page).to have_field "Yes", type: :radio, checked: false
+ end
+
+ expect(page).to have_button "Vote"
+ end
+
+ scenario "Level 2 users deleting their answer" do
+ user = create(:user, :level_two, geozone: geozone)
+ question = create(:poll_question_multiple, :abc, poll: poll, title: "Which ones are better?")
+
+ create(:poll_answer, author: user, question: question, answer: "Answer A")
+ create(:poll_voter, poll: poll, user: user)
+
+ login_as user
+ visit poll_path(poll)
+
+ expect(page).to have_content "You have already participated in this poll. " \
+ "If you vote again it will be overwritten."
+
+ within_fieldset("Which ones are better?") { uncheck "Answer A" }
+ click_button "Vote"
+
+ expect(page).to have_content "Thank you for voting! Your vote has been registered as a blank vote."
+ expect(page).to have_content "You have already participated in this poll by casting a blank vote. " \
+ "If you vote again it will be overwritten."
+
+ within_fieldset("Which ones are better?") do
+ expect(page).to have_field type: :checkbox, checked: false, count: 3
+ expect(page).not_to have_field type: :checkbox, checked: true
+ end
+
+ expect(page).to have_button "Vote"
end
scenario "Shows SDG tags when feature is enabled" do
@@ -259,20 +282,6 @@ describe "Polls" do
expect("Not restricted").to appear_before("Geozone Poll")
expect("Geozone Poll").to appear_before("A Poll")
end
-
- scenario "Level 2 users answering in a browser without javascript", :no_js do
- question = create(:poll_question, :yes_no, poll: poll)
- user = create(:user, :level_two)
- login_as user
- visit poll_path(poll)
-
- within("#poll_question_#{question.id}_options") do
- click_button "Yes"
-
- expect(page).to have_button "You have voted Yes"
- expect(page).to have_button "No"
- end
- end
end
context "Booth & Website", :with_frozen_time do
@@ -283,7 +292,7 @@ describe "Polls" do
scenario "Already voted on booth cannot vote on website" do
create(:poll_shift, officer: officer, booth: booth, date: Date.current, task: :vote_collection)
create(:poll_officer_assignment, officer: officer, poll: poll, booth: booth, date: Date.current)
- question = create(:poll_question, :yes_no, poll: poll)
+ create(:poll_question, :yes_no, poll: poll, title: "Have you voted using a booth?")
user = create(:user, :level_two, :in_census)
login_as(officer.user)
@@ -304,12 +313,9 @@ describe "Polls" do
expect(page).to have_content "You have already participated in a physical booth. " \
"You can not participate again."
- within("#poll_question_#{question.id}_options") do
- expect(page).to have_content("Yes")
- expect(page).to have_content("No")
-
- expect(page).not_to have_button "Yes"
- expect(page).not_to have_button "No"
+ within_fieldset("Have you voted using a booth?") do
+ expect(page).to have_field "Yes", type: :radio, disabled: true
+ expect(page).to have_field "No", type: :radio, disabled: true
end
end
end
diff --git a/spec/system/polls/results_spec.rb b/spec/system/polls/results_spec.rb
index 78b45d997..9e269d3ff 100644
--- a/spec/system/polls/results_spec.rb
+++ b/spec/system/polls/results_spec.rb
@@ -17,18 +17,15 @@ describe "Poll Results" do
option5 = create(:poll_question_option, question: question2, title: "Yellow")
login_as user1
- vote_for_poll_via_web(poll, question1, "Yes")
- vote_for_poll_via_web(poll, question2, "Blue")
+ vote_for_poll_via_web(poll, question1 => "Yes", question2 => "Blue")
logout
login_as user2
- vote_for_poll_via_web(poll, question1, "Yes")
- vote_for_poll_via_web(poll, question2, "Green")
+ vote_for_poll_via_web(poll, question1 => "Yes", question2 => "Green")
logout
login_as user3
- vote_for_poll_via_web(poll, question1, "No")
- vote_for_poll_via_web(poll, question2, "Yellow")
+ vote_for_poll_via_web(poll, question1 => "No", question2 => "Yellow")
logout
travel_to(poll.ends_at + 1.day)
diff --git a/spec/system/polls/votation_types_spec.rb b/spec/system/polls/votation_types_spec.rb
index 46d444376..9ccbcbd97 100644
--- a/spec/system/polls/votation_types_spec.rb
+++ b/spec/system/polls/votation_types_spec.rb
@@ -2,68 +2,102 @@ require "rails_helper"
describe "Poll Votation Type" do
let(:author) { create(:user, :level_two) }
+ let(:poll) { create(:poll) }
before do
login_as(author)
end
- scenario "Unique answer" do
- question = create(:poll_question_unique, :yes_no)
+ scenario "Unique and multiple 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?")
- visit poll_path(question.poll)
+ visit poll_path(poll)
- expect(page).to have_content "You can select a maximum of 1 answer."
- expect(page).to have_content(question.title)
- expect(page).to have_button("Vote Yes")
- expect(page).to have_button("Vote No")
+ within_fieldset("Is it that bad?") { choose "Yes" }
- within "#poll_question_#{question.id}_options" do
- click_button "Yes"
-
- expect(page).to have_button("You have voted Yes")
- expect(page).to have_button("Vote No")
-
- click_button "No"
-
- expect(page).to have_button("Vote Yes")
- expect(page).to have_button("You have voted No")
+ within_fieldset("Which ones do you prefer?") do
+ check "Answer A"
+ check "Answer C"
end
+
+ 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."
+
+ within_fieldset("Is it that bad?") do
+ expect(page).to have_field "Yes", type: :radio, checked: true
+ end
+
+ within_fieldset("Which ones do you prefer?") do
+ expect(page).to have_field "Answer A", type: :checkbox, checked: true
+ expect(page).to have_field "Answer B", type: :checkbox, checked: false
+ expect(page).to have_field "Answer C", type: :checkbox, checked: true
+ expect(page).to have_field "Answer D", type: :checkbox, checked: false
+ expect(page).to have_field "Answer E", type: :checkbox, checked: false
+ end
+
+ expect(page).to have_button "Vote"
end
- scenario "Multiple answers" do
- question = create(:poll_question_multiple, :abc, max_votes: 2)
- visit poll_path(question.poll)
+ scenario "Maximum votes has been reached" do
+ question = create(:poll_question_multiple, :abc, poll: poll, max_votes: 2)
+ create(:poll_answer, author: author, question: question, answer: "Answer A")
- expect(page).to have_content "You can select a maximum of 2 answers."
- expect(page).to have_content(question.title)
- expect(page).to have_button("Vote Answer A")
- expect(page).to have_button("Vote Answer B")
- expect(page).to have_button("Vote Answer C")
+ visit poll_path(poll)
- within "#poll_question_#{question.id}_options" do
- click_button "Vote Answer A"
+ expect(page).to have_field "Answer A", type: :checkbox, checked: true
+ expect(page).to have_field "Answer B", type: :checkbox, checked: false
+ expect(page).to have_field "Answer C", type: :checkbox, checked: false
- expect(page).to have_button("You have voted Answer A")
+ check "Answer C"
- click_button "Vote Answer C"
+ expect(page).to have_field "Answer A", type: :checkbox, checked: true
+ expect(page).to have_field "Answer B", type: :checkbox, checked: false, disabled: true
+ expect(page).to have_field "Answer C", type: :checkbox, checked: true
- expect(page).to have_button("You have voted Answer C")
- expect(page).to have_button("Vote Answer B", disabled: true)
+ click_button "Vote"
- click_button "You have voted Answer A"
+ expect(page).to have_content "Thank you for voting!"
+ expect(page).to have_field "Answer A", type: :checkbox, checked: true
+ expect(page).to have_field "Answer B", type: :checkbox, checked: false, disabled: true
+ expect(page).to have_field "Answer C", type: :checkbox, checked: true
- expect(page).to have_button("Vote Answer A")
- expect(page).to have_button("Vote Answer B")
+ uncheck "Answer A"
- click_button "You have voted Answer C"
+ expect(page).to have_field "Answer A", type: :checkbox, checked: false
+ expect(page).to have_field "Answer B", type: :checkbox, checked: false
+ expect(page).to have_field "Answer C", type: :checkbox, checked: true
+ end
- expect(page).to have_button("Vote Answer C")
+ scenario "Too many answers", :no_js do
+ create(:poll_question_multiple, :abcde, poll: poll, max_votes: 2, title: "Which ones are correct?")
- click_button "Vote Answer B"
+ visit poll_path(poll)
+ check "Answer A"
+ check "Answer B"
+ check "Answer D"
+ click_button "Vote"
- expect(page).to have_button("You have voted Answer B")
- expect(page).to have_button("Vote Answer A")
- expect(page).to have_button("Vote Answer C")
+ within_fieldset("Which ones are correct?") do
+ expect(page).to have_content "you've selected 3 answers, but the maximum you can select is 2"
+ expect(page).to have_field "Answer A", type: :checkbox, checked: true
+ expect(page).to have_field "Answer B", type: :checkbox, checked: true
+ expect(page).to have_field "Answer C", type: :checkbox, checked: false
+ expect(page).to have_field "Answer D", type: :checkbox, checked: true
+ expect(page).to have_field "Answer E", type: :checkbox, checked: false
+ end
+
+ expect(page).not_to have_content "Thank you for voting!"
+
+ visit poll_path(poll)
+
+ expect(page).not_to have_content "but the maximum you can select"
+
+ within_fieldset("Which ones are correct?") do
+ expect(page).to have_field type: :checkbox, checked: false, count: 5
end
end
end
diff --git a/spec/system/polls/voter_spec.rb b/spec/system/polls/voter_spec.rb
index 74dde4f60..0a4f2947f 100644
--- a/spec/system/polls/voter_spec.rb
+++ b/spec/system/polls/voter_spec.rb
@@ -3,7 +3,7 @@ require "rails_helper"
describe "Voter" do
context "Origin", :with_frozen_time do
let(:poll) { create(:poll) }
- let!(:question) { create(:poll_question, :yes_no, poll: poll) }
+ let!(:question) { create(:poll_question, :yes_no, poll: poll, title: "Is this question stupid?") }
let(:booth) { create(:poll_booth) }
let(:officer) { create(:poll_officer) }
let(:admin) { create(:administrator) }
@@ -20,57 +20,12 @@ describe "Voter" do
login_as user
visit poll_path(poll)
- within("#poll_question_#{question.id}_options") do
- click_button "Vote Yes"
+ within_fieldset("Is this question stupid?") { choose "Yes" }
+ click_button "Vote"
- expect(page).to have_button("You have voted Yes")
- expect(page).not_to have_button("Vote Yes")
- end
-
- refresh
-
- expect(page).to have_content("You have already participated in this poll.")
- expect(page).to have_content("If you vote again it will be overwritten")
- end
-
- scenario "Remove vote via web - Standard" do
- user = create(:user, :level_two)
- create(:poll_answer, question: question, author: user, answer: "Yes")
- create(:poll_voter, poll: poll, user: user)
-
- login_as user
- visit poll_path(poll)
-
- expect(page).to have_content("You have already participated in this poll.")
- expect(page).to have_content("If you vote again it will be overwritten")
-
- within("#poll_question_#{question.id}_options") do
- click_button "You have voted Yes"
-
- expect(page).to have_button("Vote Yes")
- expect(page).to have_button("Vote No")
- end
-
- refresh
-
- expect(page).not_to have_content("You have already participated in this poll.")
- expect(page).not_to have_content("If you vote again it will be overwritten")
- end
-
- scenario "Voting via web as unverified user" do
- user = create(:user, :incomplete_verification)
-
- login_as user
- visit poll_path(poll)
-
- within("#poll_question_#{question.id}_options") do
- expect(page).to have_link("Yes", href: verification_path)
- expect(page).to have_link("No", href: verification_path)
- end
-
- expect(page).to have_content "You must verify your account in order to answer"
- expect(page).not_to have_content "You have already participated in this poll. " \
- "If you vote again it will be overwritten"
+ 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
scenario "Voting in booth" do
@@ -142,7 +97,7 @@ describe "Voter" do
scenario "Trying to vote in web and then in booth" do
login_as user
- vote_for_poll_via_web(poll, question, "Yes")
+ vote_for_poll_via_web(poll, question => "Yes")
logout
login_through_form_as_officer(officer)
@@ -165,8 +120,9 @@ describe "Voter" do
login_as user
visit poll_path(poll)
- within("#poll_question_#{question.id}_options") do
- expect(page).not_to have_button("Yes")
+ within_fieldset "Is this question stupid?" do
+ expect(page).to have_field "Yes", type: :radio, disabled: true
+ expect(page).to have_field "No", type: :radio, disabled: true
end
expect(page).to have_content "You have already participated in a physical booth. " \
"You can not participate again."
@@ -185,7 +141,7 @@ describe "Voter" do
end
end
- scenario "Voting in poll and then verifiying account" do
+ scenario "Voting in poll and then verifying account" do
allow_any_instance_of(Verification::Sms).to receive(:generate_confirmation_code).and_return("1357")
user = create(:user)
admin_user = admin.user
@@ -203,8 +159,9 @@ describe "Voter" do
visit poll_path(poll)
- within("#poll_question_#{question.id}_options") do
- expect(page).not_to have_button("Yes")
+ within_fieldset "Is this question stupid?" do
+ expect(page).to have_field "Yes", type: :radio, disabled: true
+ expect(page).to have_field "No", type: :radio, disabled: true
end
expect(page).to have_content "You have already participated in a physical booth. " \