Merge pull request #5540 from consuldemocracy/poll_form

Use checkboxes and radio buttons on poll forms
This commit is contained in:
Javi Martín
2025-08-14 14:13:26 +02:00
committed by GitHub
46 changed files with 909 additions and 765 deletions

View File

@@ -571,6 +571,9 @@ RSpec/BeNil:
Enabled: true
EnforcedStyle: be
RSpec/ChangeByZero:
Enabled: true
RSpec/ContextMethod:
Enabled: true

View File

@@ -120,6 +120,7 @@
//= require datepicker
//= require authenticity_token_refresh
//= require_tree ./admin
//= require_tree ./polls
//= require_tree ./sdg
//= require_tree ./sdg_management
//= require_tree ./custom
@@ -178,6 +179,7 @@ var initialize_modules = function() {
App.BudgetEditAssociations.initialize();
App.BudgetHideMoney.initialize();
App.Datepicker.initialize();
App.PollsForm.initialize();
App.SDGRelatedListSelector.initialize();
App.SDGManagementRelationSearch.initialize();
App.AuthenticityTokenRefresh.initialize();

View File

@@ -0,0 +1,27 @@
(function() {
"use strict";
App.PollsForm = {
updateMultipleChoiceStatus: function(fieldset) {
var max_votes = $(fieldset).attr("data-max-votes");
var checked_boxes = $(fieldset).find(":checkbox:checked");
var unchecked_boxes = $(fieldset).find(":checkbox:not(:checked)");
if (checked_boxes.length >= max_votes) {
$(unchecked_boxes).prop("disabled", true);
} else {
$(fieldset).find(":checkbox").prop("disabled", false);
}
},
initialize: function() {
$(".poll-form .multiple-choice").each(function() {
App.PollsForm.updateMultipleChoiceStatus(this);
});
$(".poll-form .multiple-choice :checkbox").on("change", function() {
var fieldset = $(this).closest("fieldset");
App.PollsForm.updateMultipleChoiceStatus(fieldset);
});
}
};
}).call(this);

View File

@@ -90,7 +90,7 @@
@include radio-or-checkbox-and-label-alignment;
span {
margin-left: 1ch;
margin-#{$global-left}: 1ch;
}
}
}

View File

@@ -1396,8 +1396,7 @@
position: relative;
}
.public .poll,
.poll-question {
.public .poll {
border: 1px solid $border;
margin-bottom: calc($line-height / 2);
padding: calc($line-height / 2);
@@ -1413,47 +1412,6 @@
}
}
.poll-question {
padding: 0 $line-height;
h3 {
padding-top: $line-height;
}
}
.poll-question-options {
@include flex-with-gap($line-height * 0.25);
flex-wrap: wrap;
.button {
min-width: rem-calc(168);
@include breakpoint(medium down) {
width: 100%;
}
&.answered {
background: #f4f8ec;
border: 2px solid #92ba48;
color: color-pick-contrast(#f4f8ec);
position: relative;
&::after {
background: #92ba48;
border-radius: rem-calc(20);
color: #fff;
content: "\6c";
font-family: "icons" !important;
font-size: rem-calc(12);
padding: calc($line-height / 4);
position: absolute;
right: -6px;
top: -6px;
}
}
}
}
// 09. Polls results and stats
// ---------------------------

View File

@@ -0,0 +1,56 @@
.poll-form {
fieldset {
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;
}
}
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;
}
}
.help-text {
display: block;
font-size: 1em;
font-style: normal;
font-weight: bold;
}
.read-more-links {
margin-top: calc($line-height / 2);
* {
margin-bottom: 0;
}
* + * {
margin-top: calc($line-height / 4);
}
}
[type=submit] {
margin-top: calc($line-height * 3 / 4);
}
}

View File

@@ -0,0 +1,21 @@
<% if can?(:answer, poll) %>
<% if voted_in_booth? %>
<%= callout(t("polls.show.already_voted_in_booth")) %>
<% elsif voted_in_web? %>
<% if voted_blank? %>
<%= callout(t("polls.show.already_voted_blank_in_web")) %>
<% else %>
<%= callout(t("polls.show.already_voted_in_web")) %>
<% end %>
<% end %>
<% else %>
<% if current_user.nil? %>
<%= callout(not_logged_in_text, html_class: "primary") %>
<% elsif current_user.unverified? %>
<%= callout(unverified_text) %>
<% elsif poll.expired? %>
<%= callout(t("polls.show.cant_answer_expired"), html_class: "alert") %>
<% else %>
<%= callout(t("polls.show.cant_answer_wrong_geozone")) %>
<% end %>
<% end %>

View File

@@ -0,0 +1,37 @@
class Polls::CalloutComponent < ApplicationComponent
attr_reader :poll
use_helpers :can?, :current_user, :link_to_signin, :link_to_signup
def initialize(poll)
@poll = poll
end
private
def voted_in_booth?
poll.voted_in_booth?(current_user)
end
def voted_in_web?
poll.voted_in_web?(current_user)
end
def voted_blank?
poll.answers.where(author: current_user).none?
end
def callout(text, html_class: "warning")
tag.div(text, class: "callout #{html_class}")
end
def not_logged_in_text
sanitize(t("polls.show.cant_answer_not_logged_in",
signin: link_to_signin,
signup: link_to_signup))
end
def unverified_text
sanitize(t("polls.show.cant_answer_verify",
verify_link: link_to(t("polls.show.verify_link"), verification_path)))
end
end

View File

@@ -0,0 +1,7 @@
<%= form_for web_vote, form_attributes do |f| %>
<% questions.each do |question| %>
<%= render Polls::Questions::QuestionComponent.new(question, form: f, disabled: disabled?) %>
<% end %>
<%= f.submit(class: "button", value: t("polls.form.vote"), disabled: disabled?) %>
<% end %>

View File

@@ -0,0 +1,19 @@
class Polls::FormComponent < ApplicationComponent
attr_reader :web_vote
use_helpers :cannot?, :current_user
delegate :poll, :questions, to: :web_vote
def initialize(web_vote)
@web_vote = web_vote
end
private
def form_attributes
{ url: answer_poll_path(poll), method: :post, html: { class: "poll-form" }}
end
def disabled?
cannot?(:answer, poll) || poll.voted_in_booth?(current_user)
end
end

View File

@@ -1,35 +0,0 @@
<div class="poll-question-options">
<% if can?(:answer, question) && !question.poll.voted_in_booth?(current_user) %>
<% question_options.each do |question_option| %>
<% if already_answered?(question_option) %>
<%= button_to question_option.title,
question_answer_path(question, user_answer(question_option)),
method: :delete,
remote: true,
title: t("poll_questions.show.voted", answer: question_option.title),
class: "button answered",
"aria-pressed": true %>
<% else %>
<%= button_to question_option.title,
question_answers_path(question, option_id: question_option.id),
remote: true,
title: t("poll_questions.show.vote_answer", answer: question_option.title),
class: "button secondary hollow",
"aria-pressed": false,
disabled: disable_option?(question_option) %>
<% end %>
<% end %>
<% elsif !user_signed_in? %>
<% question_options.each do |question_option| %>
<%= link_to question_option.title, new_user_session_path, class: "button secondary hollow" %>
<% end %>
<% elsif !current_user.level_two_or_three_verified? %>
<% question_options.each do |question_option| %>
<%= link_to question_option.title, verification_path, class: "button secondary hollow" %>
<% end %>
<% else %>
<% question_options.each do |question_option| %>
<span class="button secondary hollow disabled"><%= question_option.title %></span>
<% end %>
<% end %>
</div>

View File

@@ -1,30 +0,0 @@
class Polls::Questions::OptionsComponent < ApplicationComponent
attr_reader :question
use_helpers :can?, :current_user, :user_signed_in?
def initialize(question)
@question = question
end
def already_answered?(question_option)
user_answer(question_option).present?
end
def question_options
question.question_options
end
def user_answer(question_option)
user_answers.find_by(answer: question_option.title)
end
def disable_option?(question_option)
question.multiple? && user_answers.count == question.max_votes
end
private
def user_answers
@user_answers ||= question.answers.by_author(current_user)
end
end

View File

@@ -1,22 +1,23 @@
<div id="<%= dom_id(question) %>" class="poll-question">
<h3>
<%= question.title %>
</h3>
<fieldset <%= fieldset_attributes %>>
<legend><%= question.title %></legend>
<% if question.votation_type.present? %>
<strong>
<%= t("poll_questions.description.#{question.vote_type}", maximum: question.max_votes) %>
</strong>
<% 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 %>
<div id="<%= dom_id(question) %>_options" class="padding">
<%= render Polls::Questions::OptionsComponent.new(question) %>
</div>
<% if question.options_with_read_more? %>
<div>
<div class="read-more-links">
<p><%= t("poll_questions.read_more_about") %></p>
<p><%= options_read_more_links %></p>
</div>
<% end %>
</div>
<%= form.error_for(:"question_#{question.id}") %>
</fieldset>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,22 +0,0 @@
<% unless can?(:answer, @poll) %>
<% if current_user.nil? %>
<div class="callout primary">
<%= sanitize(t("polls.show.cant_answer_not_logged_in",
signin: link_to_signin(class: "probe-message"),
signup: link_to_signup(class: "probe-message"))) %>
</div>
<% elsif current_user.unverified? %>
<div class="callout warning">
<%= sanitize(t("polls.show.cant_answer_verify",
verify_link: link_to(t("polls.show.verify_link"), verification_path))) %>
</div>
<% elsif @poll.expired? %>
<div class="callout alert">
<%= t("polls.show.cant_answer_expired") %>
</div>
<% else %>
<div class="callout warning">
<%= t("polls.show.cant_answer_wrong_geozone") %>
</div>
<% end %>
<% end %>

View File

@@ -1 +0,0 @@
$("#<%= dom_id(@question) %>_options").html("<%= j render Polls::Questions::OptionsComponent.new(@question) %>");

View File

@@ -16,29 +16,15 @@
<div class="row margin">
<div class="small-12 medium-9 column">
<%= render "callout" %>
<% if @poll.voted_in_booth?(current_user) %>
<div class="callout warning">
<%= t("polls.show.already_voted_in_booth") %>
</div>
<% else %>
<% if current_user && @poll.voted_in_web?(current_user) && !@poll.expired? %>
<div class="callout warning">
<%= t("polls.show.already_voted_in_web") %>
</div>
<% end %>
<% end %>
<%= render Polls::Questions::QuestionComponent.with_collection(@questions) %>
<%= render Polls::CalloutComponent.new(@poll) %>
<%= render Polls::FormComponent.new(@web_vote) %>
</div>
</div>
<div class="expanded poll-more-info">
<div class="row margin">
<div class="small-12 medium-9 column">
<h3><%= t("polls.show.more_info_title") %></h3>
<h2><%= t("polls.show.more_info_title") %></h2>
<%= auto_link_already_sanitized_html simple_format(@poll.description) %>
</div>
</div>
@@ -46,7 +32,7 @@
<div id="poll_more_info_options" class="expanded poll-more-info-options">
<div class="row padding">
<%= render Polls::Questions::ReadMoreComponent.with_collection(@questions) %>
<%= render Polls::Questions::ReadMoreComponent.with_collection(@web_vote.questions) %>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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. " \