Merge pull request #5540 from consuldemocracy/poll_form
Use checkboxes and radio buttons on poll forms
This commit is contained in:
@@ -571,6 +571,9 @@ RSpec/BeNil:
|
||||
Enabled: true
|
||||
EnforcedStyle: be
|
||||
|
||||
RSpec/ChangeByZero:
|
||||
Enabled: true
|
||||
|
||||
RSpec/ContextMethod:
|
||||
Enabled: true
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
27
app/assets/javascripts/polls/form.js
Normal file
27
app/assets/javascripts/polls/form.js
Normal 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);
|
||||
@@ -90,7 +90,7 @@
|
||||
@include radio-or-checkbox-and-label-alignment;
|
||||
|
||||
span {
|
||||
margin-left: 1ch;
|
||||
margin-#{$global-left}: 1ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// ---------------------------
|
||||
|
||||
|
||||
56
app/assets/stylesheets/polls/form.scss
Normal file
56
app/assets/stylesheets/polls/form.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
21
app/components/polls/callout_component.html.erb
Normal file
21
app/components/polls/callout_component.html.erb
Normal 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 %>
|
||||
37
app/components/polls/callout_component.rb
Normal file
37
app/components/polls/callout_component.rb
Normal 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
|
||||
7
app/components/polls/form_component.html.erb
Normal file
7
app/components/polls/form_component.html.erb
Normal 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 %>
|
||||
19
app/components/polls/form_component.rb
Normal file
19
app/components/polls/form_component.rb
Normal 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
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
81
app/models/poll/web_vote.rb
Normal file
81
app/models/poll/web_vote.rb
Normal 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
|
||||
@@ -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 %>
|
||||
@@ -1 +0,0 @@
|
||||
$("#<%= dom_id(@question) %>_options").html("<%= j render Polls::Questions::OptionsComponent.new(@question) %>");
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
27
spec/components/polls/callout_component_spec.rb
Normal file
27
spec/components/polls/callout_component_spec.rb
Normal 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
|
||||
88
spec/components/polls/form_component_spec.rb
Normal file
88
spec/components/polls/form_component_spec.rb
Normal 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
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
111
spec/models/poll/web_vote_spec.rb
Normal file
111
spec/models/poll/web_vote_spec.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. " \
|
||||
|
||||
Reference in New Issue
Block a user