Use checkboxes and radio buttons on poll forms
Our original interface to vote in a poll had a few issues: * Since there was no button to send the form, it wasn't clear that selecting an option would automatically store it in the database. * The interface was almost identical for single-choice questions and multiple-choice questions, which made it hard to know which type of question we were answering. * Adding other type of questions, like open answers, was hard since we would have to add a different submit button for each answer. So we're now using radio buttons for single-choice questions and checkboxes for multiple-choice questions, which are the native controls designed for these purposes, and a button to send the whole form. Since we don't have a database table for poll ballots like we have for budget ballots, we're adding a new `Poll::WebVote` model to manage poll ballots. We're using WebVote instead of Ballot or Vote because they could be mistaken with other vote classes. Note that browsers don't allow removing answers with radio buttons, so once somebody has voted in a single-choice question, they can't remove the vote unless they manually edit their HTML. This is the same behavior we had before commit7df0e9a96. As mentioned inc2010f975, we're now adding the `ChangeByZero` rubocop rule, since we've removed the test that used `and change`.
This commit is contained in:
@@ -571,6 +571,9 @@ RSpec/BeNil:
|
||||
Enabled: true
|
||||
EnforcedStyle: be
|
||||
|
||||
RSpec/ChangeByZero:
|
||||
Enabled: true
|
||||
|
||||
RSpec/ContextMethod:
|
||||
Enabled: true
|
||||
|
||||
|
||||
@@ -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
|
||||
// ---------------------------
|
||||
|
||||
|
||||
29
app/assets/stylesheets/polls/form.scss
Normal file
29
app/assets/stylesheets/polls/form.scss
Normal file
@@ -0,0 +1,29 @@
|
||||
.poll-form {
|
||||
label {
|
||||
@include radio-or-checkbox-and-label-alignment;
|
||||
font-weight: normal;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: calc($line-height / 2);
|
||||
}
|
||||
}
|
||||
|
||||
fieldset + fieldset {
|
||||
margin-top: calc($line-height / 2);
|
||||
}
|
||||
|
||||
legend {
|
||||
@include header-font-size(h3);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
font-size: 1em;
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
[type=submit] {
|
||||
margin-top: calc($line-height * 3 / 4);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
<% questions.each do |question| %>
|
||||
<%= render Polls::Questions::QuestionComponent.new(question) %>
|
||||
<%= form_for poll, form_attributes do |f| %>
|
||||
<% questions.each do |question| %>
|
||||
<%= render Polls::Questions::QuestionComponent.new(question, disabled: disabled?) %>
|
||||
<% end %>
|
||||
|
||||
<%= f.submit(class: "button", value: t("polls.form.vote"), disabled: disabled?) %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
class Polls::FormComponent < ApplicationComponent
|
||||
attr_reader :questions
|
||||
attr_reader :web_vote
|
||||
use_helpers :cannot?, :current_user
|
||||
delegate :poll, :questions, to: :web_vote
|
||||
|
||||
def initialize(questions)
|
||||
@questions = questions
|
||||
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
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -1,22 +1,22 @@
|
||||
<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>
|
||||
<p><%= t("poll_questions.read_more_about") %></p>
|
||||
<p><%= options_read_more_links %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -1,13 +1,65 @@
|
||||
class Polls::Questions::QuestionComponent < ApplicationComponent
|
||||
attr_reader :question
|
||||
attr_reader :question, :disabled
|
||||
alias_method :disabled?, :disabled
|
||||
use_helpers :current_user
|
||||
|
||||
def initialize(question)
|
||||
def initialize(question, disabled: false)
|
||||
@question = question
|
||||
@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?),
|
||||
data: { max_votes: question.max_votes }
|
||||
)
|
||||
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)
|
||||
question.answers.where(author: current_user, option: option).any?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
@@ -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,21 @@ 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)
|
||||
redirect_to @poll, notice: t("flash.actions.create.poll_voter")
|
||||
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 +49,8 @@ class PollsController < ApplicationController
|
||||
def load_active_poll
|
||||
@active_poll = ActivePoll.first
|
||||
end
|
||||
|
||||
def answer_params
|
||||
params[:web_vote] || {}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -41,8 +41,6 @@ class Poll::Question < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
delegate :answerable_by?, to: :poll
|
||||
|
||||
def options_total_votes
|
||||
question_options.reduce(0) { |total, question_option| total + question_option.total_votes }
|
||||
end
|
||||
|
||||
43
app/models/poll/web_vote.rb
Normal file
43
app/models/poll/web_vote.rb
Normal file
@@ -0,0 +1,43 @@
|
||||
class Poll::WebVote
|
||||
attr_reader :poll, :user
|
||||
|
||||
def initialize(poll, user)
|
||||
@poll = poll
|
||||
@user = user
|
||||
end
|
||||
|
||||
def questions
|
||||
poll.questions.for_render.sort_for_list
|
||||
end
|
||||
|
||||
def update(params)
|
||||
all_valid = true
|
||||
|
||||
user.with_lock do
|
||||
unless questions.any? { |question| params.dig(question.id.to_s, :option_id).present? }
|
||||
Poll::Voter.find_by(user: user, poll: poll, origin: "web")&.destroy!
|
||||
end
|
||||
|
||||
questions.each do |question|
|
||||
question.answers.where(author: user).destroy_all
|
||||
next unless params[question.id.to_s]
|
||||
|
||||
option_ids = params[question.id.to_s][:option_id]
|
||||
|
||||
answers = Array(option_ids).map do |option_id|
|
||||
question.find_or_initialize_user_answer(user, option_id)
|
||||
end
|
||||
|
||||
if answers.map(&:valid?).all?(true)
|
||||
Poll::Voter.find_or_create_by!(user: user, poll: poll, origin: "web")
|
||||
answers.each(&:save!)
|
||||
else
|
||||
all_valid = false
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
all_valid
|
||||
end
|
||||
end
|
||||
@@ -1 +0,0 @@
|
||||
$("#<%= dom_id(@question) %>_options").html("<%= j render Polls::Questions::OptionsComponent.new(@question) %>");
|
||||
@@ -31,7 +31,7 @@
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= render Polls::FormComponent.new(@questions) %>
|
||||
<%= render Polls::FormComponent.new(@web_vote) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -46,7 +46,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>
|
||||
|
||||
|
||||
@@ -577,6 +577,8 @@ en:
|
||||
polls:
|
||||
dates: "From %{open_at} to %{closed_at}"
|
||||
final_date: "Final recounts/Results"
|
||||
form:
|
||||
vote: "Vote"
|
||||
index:
|
||||
filters:
|
||||
current: "Open"
|
||||
@@ -634,11 +636,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:
|
||||
|
||||
@@ -10,6 +10,7 @@ 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!"
|
||||
proposal: "Proposal created successfully."
|
||||
proposal_notification: "Your message has been sent correctly."
|
||||
budget_investment: "Budget Investment created successfully."
|
||||
|
||||
@@ -577,6 +577,8 @@ es:
|
||||
polls:
|
||||
dates: "Desde el %{open_at} hasta el %{closed_at}"
|
||||
final_date: "Recuento final/Resultados"
|
||||
form:
|
||||
vote: "Votar"
|
||||
index:
|
||||
filters:
|
||||
current: "Abiertas"
|
||||
@@ -634,11 +636,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:
|
||||
|
||||
@@ -10,6 +10,7 @@ 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!"
|
||||
proposal: "Propuesta creada correctamente."
|
||||
proposal_notification: "Tu mensaje ha sido enviado correctamente."
|
||||
budget_investment: "Proyecto de gasto creado correctamente."
|
||||
|
||||
@@ -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
|
||||
|
||||
70
spec/components/polls/form_component_spec.rb
Normal file
70
spec/components/polls/form_component_spec.rb
Normal file
@@ -0,0 +1,70 @@
|
||||
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
|
||||
|
||||
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) }
|
||||
|
||||
it "renders disabled fields for users from another geozone" do
|
||||
poll.geozones << geozone
|
||||
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 enabled fields for same-geozone users" do
|
||||
poll.geozones << geozone
|
||||
sign_in(create(:user, :level_two, geozone: geozone))
|
||||
|
||||
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
|
||||
@@ -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
|
||||
@@ -1,18 +1,83 @@
|
||||
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") }
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
expect("Yes").to appear_before("No")
|
||||
end
|
||||
|
||||
it "renders disabled answers when given the disabled parameter" do
|
||||
render_inline Polls::Questions::QuestionComponent.new(question, disabled: true)
|
||||
|
||||
page.find("fieldset[disabled]") do |fieldset|
|
||||
expect(fieldset).to have_field "Yes"
|
||||
expect(fieldset).to have_field "No"
|
||||
end
|
||||
end
|
||||
|
||||
skip "disables fields when maximum votes has been reached" do # TODO: requires JavaScript
|
||||
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::QuestionComponent.new(question)
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
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
|
||||
render_inline Polls::Questions::QuestionComponent.new(create(:poll_question_multiple, :abc))
|
||||
|
||||
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)
|
||||
|
||||
expect(page).to have_field "Yes", type: :radio, checked: true
|
||||
expect(page).to have_field "No", type: :radio, checked: false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
110
spec/models/poll/web_vote_spec.rb
Normal file
110
spec/models/poll/web_vote_spec.rb
Normal file
@@ -0,0 +1,110 @@
|
||||
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
|
||||
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
|
||||
|
||||
voter = poll.voters.first
|
||||
answer = question.answers.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 "does not create voters or answers when leaving everything blank" do
|
||||
web_vote.update({})
|
||||
|
||||
expect(poll.reload.voters.size).to eq 0
|
||||
expect(question.reload.answers.size).to eq 0
|
||||
end
|
||||
|
||||
it "deletes existing answers and voter 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 0
|
||||
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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -199,41 +199,82 @@ describe "Polls" do
|
||||
|
||||
scenario "Level 2 users answering" do
|
||||
poll.update!(geozone_restricted_to: [geozone])
|
||||
create(:poll_question, :yes_no, poll: poll, title: "Do you agree?")
|
||||
|
||||
question = create(:poll_question, :yes_no, poll: poll)
|
||||
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!"
|
||||
expect(page).not_to have_content "You have already participated"
|
||||
|
||||
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 +300,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 +310,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 +331,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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -7,63 +7,38 @@ describe "Poll Votation Type" do
|
||||
login_as(author)
|
||||
end
|
||||
|
||||
scenario "Unique answer" do
|
||||
question = create(:poll_question_unique, :yes_no)
|
||||
scenario "Unique and multiple answers" do
|
||||
poll = create(:poll)
|
||||
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
|
||||
end
|
||||
|
||||
scenario "Multiple answers" do
|
||||
question = create(:poll_question_multiple, :abc, max_votes: 2)
|
||||
visit poll_path(question.poll)
|
||||
click_button "Vote"
|
||||
|
||||
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")
|
||||
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 "#poll_question_#{question.id}_options" do
|
||||
click_button "Vote Answer A"
|
||||
|
||||
expect(page).to have_button("You have voted Answer A")
|
||||
|
||||
click_button "Vote Answer C"
|
||||
|
||||
expect(page).to have_button("You have voted Answer C")
|
||||
expect(page).to have_button("Vote Answer B", disabled: true)
|
||||
|
||||
click_button "You have voted Answer A"
|
||||
|
||||
expect(page).to have_button("Vote Answer A")
|
||||
expect(page).to have_button("Vote Answer B")
|
||||
|
||||
click_button "You have voted Answer C"
|
||||
|
||||
expect(page).to have_button("Vote Answer C")
|
||||
|
||||
click_button "Vote Answer B"
|
||||
|
||||
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("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
|
||||
end
|
||||
|
||||
@@ -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,41 +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")
|
||||
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 via web as unverified user" do
|
||||
@@ -63,9 +34,9 @@ describe "Voter" do
|
||||
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)
|
||||
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 must verify your account in order to answer"
|
||||
@@ -142,7 +113,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 +136,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."
|
||||
@@ -203,8 +175,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. " \
|
||||
|
||||
Reference in New Issue
Block a user