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
|
Enabled: true
|
||||||
EnforcedStyle: be
|
EnforcedStyle: be
|
||||||
|
|
||||||
|
RSpec/ChangeByZero:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
RSpec/ContextMethod:
|
RSpec/ContextMethod:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
//= require datepicker
|
//= require datepicker
|
||||||
//= require authenticity_token_refresh
|
//= require authenticity_token_refresh
|
||||||
//= require_tree ./admin
|
//= require_tree ./admin
|
||||||
|
//= require_tree ./polls
|
||||||
//= require_tree ./sdg
|
//= require_tree ./sdg
|
||||||
//= require_tree ./sdg_management
|
//= require_tree ./sdg_management
|
||||||
//= require_tree ./custom
|
//= require_tree ./custom
|
||||||
@@ -178,6 +179,7 @@ var initialize_modules = function() {
|
|||||||
App.BudgetEditAssociations.initialize();
|
App.BudgetEditAssociations.initialize();
|
||||||
App.BudgetHideMoney.initialize();
|
App.BudgetHideMoney.initialize();
|
||||||
App.Datepicker.initialize();
|
App.Datepicker.initialize();
|
||||||
|
App.PollsForm.initialize();
|
||||||
App.SDGRelatedListSelector.initialize();
|
App.SDGRelatedListSelector.initialize();
|
||||||
App.SDGManagementRelationSearch.initialize();
|
App.SDGManagementRelationSearch.initialize();
|
||||||
App.AuthenticityTokenRefresh.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;
|
@include radio-or-checkbox-and-label-alignment;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
margin-left: 1ch;
|
margin-#{$global-left}: 1ch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1396,8 +1396,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.public .poll,
|
.public .poll {
|
||||||
.poll-question {
|
|
||||||
border: 1px solid $border;
|
border: 1px solid $border;
|
||||||
margin-bottom: calc($line-height / 2);
|
margin-bottom: calc($line-height / 2);
|
||||||
padding: 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
|
// 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">
|
<fieldset <%= fieldset_attributes %>>
|
||||||
<h3>
|
<legend><%= question.title %></legend>
|
||||||
<%= question.title %>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<% if question.votation_type.present? %>
|
<% if multiple_choice? %>
|
||||||
<strong>
|
<%= multiple_choice_help_text %>
|
||||||
<%= t("poll_questions.description.#{question.vote_type}", maximum: question.max_votes) %>
|
|
||||||
</strong>
|
<% question.question_options.each do |option| %>
|
||||||
|
<%= multiple_choice_field(option) %>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<% question.question_options.each do |option| %>
|
||||||
|
<%= single_choice_field(option) %>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div id="<%= dom_id(question) %>_options" class="padding">
|
|
||||||
<%= render Polls::Questions::OptionsComponent.new(question) %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% if question.options_with_read_more? %>
|
<% if question.options_with_read_more? %>
|
||||||
<div>
|
<div class="read-more-links">
|
||||||
<p><%= t("poll_questions.read_more_about") %></p>
|
<p><%= t("poll_questions.read_more_about") %></p>
|
||||||
<p><%= options_read_more_links %></p>
|
<p><%= options_read_more_links %></p>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
<%= form.error_for(:"question_#{question.id}") %>
|
||||||
|
</fieldset>
|
||||||
|
|||||||
@@ -1,8 +1,30 @@
|
|||||||
class Polls::Questions::QuestionComponent < ApplicationComponent
|
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
|
@question = question
|
||||||
|
@form = form
|
||||||
|
@disabled = disabled
|
||||||
|
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
|
end
|
||||||
|
|
||||||
def options_read_more_links
|
def options_read_more_links
|
||||||
@@ -10,4 +32,43 @@ class Polls::Questions::QuestionComponent < ApplicationComponent
|
|||||||
link_to option.title, "#option_#{option.id}"
|
link_to option.title, "#option_#{option.id}"
|
||||||
end, ", ")
|
end, ", ")
|
||||||
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
|
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
|
load_and_authorize_resource
|
||||||
|
|
||||||
has_filters %w[current expired]
|
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
|
def index
|
||||||
@polls = Kaminari.paginate_array(
|
@polls = Kaminari.paginate_array(
|
||||||
@@ -18,10 +18,25 @@ class PollsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show
|
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)
|
@comment_tree = CommentTree.new(@poll, params[:page], @current_order)
|
||||||
end
|
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
|
def stats
|
||||||
@stats = Poll::Stats.new(@poll).tap(&:generate)
|
@stats = Poll::Stats.new(@poll).tap(&:generate)
|
||||||
end
|
end
|
||||||
@@ -38,4 +53,8 @@ class PollsController < ApplicationController
|
|||||||
def load_active_poll
|
def load_active_poll
|
||||||
@active_poll = ActivePoll.first
|
@active_poll = ActivePoll.first
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def answer_params
|
||||||
|
params[:web_vote] || {}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -110,12 +110,6 @@ module Abilities
|
|||||||
can :answer, Poll do |poll|
|
can :answer, Poll do |poll|
|
||||||
poll.answerable_by?(user)
|
poll.answerable_by?(user)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
can [:create, :show], ProposalNotification, proposal: { author_id: user.id }
|
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 :officer_assignments, through: :booth_assignments
|
||||||
has_many :officers, through: :officer_assignments
|
has_many :officers, through: :officer_assignments
|
||||||
has_many :questions, inverse_of: :poll, dependent: :destroy
|
has_many :questions, inverse_of: :poll, dependent: :destroy
|
||||||
|
has_many :answers, through: :questions
|
||||||
has_many :comments, as: :commentable, inverse_of: :commentable
|
has_many :comments, as: :commentable, inverse_of: :commentable
|
||||||
has_many :ballot_sheets
|
has_many :ballot_sheets
|
||||||
|
|
||||||
|
|||||||
@@ -17,32 +17,13 @@ class Poll::Answer < ApplicationRecord
|
|||||||
scope :by_author, ->(author_id) { where(author_id: author_id) }
|
scope :by_author, ->(author_id) { where(author_id: author_id) }
|
||||||
scope :by_question, ->(question_id) { where(question_id: question_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
|
private
|
||||||
|
|
||||||
def max_votes
|
def max_votes
|
||||||
return if !question || !author || persisted?
|
return if !question || !author || persisted?
|
||||||
|
|
||||||
author.with_lock do
|
|
||||||
if question.answers.by_author(author).count >= question.max_votes
|
if question.answers.by_author(author).count >= question.max_votes
|
||||||
errors.add(:answer, "Maximum number of votes per user exceeded")
|
errors.add(:answer, "Maximum number of votes per user exceeded")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@@ -41,14 +41,6 @@ class Poll::Question < ApplicationRecord
|
|||||||
end
|
end
|
||||||
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
|
def options_total_votes
|
||||||
question_options.reduce(0) { |total, question_option| total + question_option.total_votes }
|
question_options.reduce(0) { |total, question_option| total + question_option.total_votes }
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -42,11 +42,11 @@ class Poll::Stats
|
|||||||
end
|
end
|
||||||
|
|
||||||
def total_web_valid
|
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
|
end
|
||||||
|
|
||||||
def total_web_white
|
def total_web_white
|
||||||
0
|
voters.where(origin: "web").count - total_web_valid
|
||||||
end
|
end
|
||||||
|
|
||||||
def total_web_null
|
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="row margin">
|
||||||
<div class="small-12 medium-9 column">
|
<div class="small-12 medium-9 column">
|
||||||
<%= render "callout" %>
|
<%= render Polls::CalloutComponent.new(@poll) %>
|
||||||
|
<%= render Polls::FormComponent.new(@web_vote) %>
|
||||||
<% 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) %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="expanded poll-more-info">
|
<div class="expanded poll-more-info">
|
||||||
<div class="row margin">
|
<div class="row margin">
|
||||||
<div class="small-12 medium-9 column">
|
<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) %>
|
<%= auto_link_already_sanitized_html simple_format(@poll.description) %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,7 +32,7 @@
|
|||||||
|
|
||||||
<div id="poll_more_info_options" class="expanded poll-more-info-options">
|
<div id="poll_more_info_options" class="expanded poll-more-info-options">
|
||||||
<div class="row padding">
|
<div class="row padding">
|
||||||
<%= render Polls::Questions::ReadMoreComponent.with_collection(@questions) %>
|
<%= render Polls::Questions::ReadMoreComponent.with_collection(@web_vote.questions) %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -577,6 +577,9 @@ en:
|
|||||||
polls:
|
polls:
|
||||||
dates: "From %{open_at} to %{closed_at}"
|
dates: "From %{open_at} to %{closed_at}"
|
||||||
final_date: "Final recounts/Results"
|
final_date: "Final recounts/Results"
|
||||||
|
form:
|
||||||
|
vote: "Vote"
|
||||||
|
maximum_exceeded: "you've selected %{given} answers, but the maximum you can select is %{maximum}"
|
||||||
index:
|
index:
|
||||||
filters:
|
filters:
|
||||||
current: "Open"
|
current: "Open"
|
||||||
@@ -602,6 +605,7 @@ en:
|
|||||||
show:
|
show:
|
||||||
already_voted_in_booth: "You have already participated in a physical booth. You can not participate again."
|
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_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
|
back: Back to voting
|
||||||
cant_answer_not_logged_in: "You must %{signin} or %{signup} to participate."
|
cant_answer_not_logged_in: "You must %{signin} or %{signup} to participate."
|
||||||
comments_tab: Comments
|
comments_tab: Comments
|
||||||
@@ -634,11 +638,7 @@ en:
|
|||||||
poll_header:
|
poll_header:
|
||||||
back_to_proposal: Back to proposal
|
back_to_proposal: Back to proposal
|
||||||
poll_questions:
|
poll_questions:
|
||||||
show:
|
|
||||||
vote_answer: "Vote %{answer}"
|
|
||||||
voted: "You have voted %{answer}"
|
|
||||||
description:
|
description:
|
||||||
unique: "You can select a maximum of 1 answer."
|
|
||||||
multiple: "You can select a maximum of %{maximum} answers."
|
multiple: "You can select a maximum of %{maximum} answers."
|
||||||
read_more_about: "Read more about:"
|
read_more_about: "Read more about:"
|
||||||
proposal_notifications:
|
proposal_notifications:
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ en:
|
|||||||
poll_question_option: "Answer created successfully"
|
poll_question_option: "Answer created successfully"
|
||||||
poll_question_option_video: "Video created successfully"
|
poll_question_option_video: "Video created successfully"
|
||||||
poll_question_option_image: "Image uploaded 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: "Proposal created successfully."
|
||||||
proposal_notification: "Your message has been sent correctly."
|
proposal_notification: "Your message has been sent correctly."
|
||||||
budget_investment: "Budget Investment created successfully."
|
budget_investment: "Budget Investment created successfully."
|
||||||
|
|||||||
@@ -577,6 +577,9 @@ es:
|
|||||||
polls:
|
polls:
|
||||||
dates: "Desde el %{open_at} hasta el %{closed_at}"
|
dates: "Desde el %{open_at} hasta el %{closed_at}"
|
||||||
final_date: "Recuento final/Resultados"
|
final_date: "Recuento final/Resultados"
|
||||||
|
form:
|
||||||
|
vote: "Votar"
|
||||||
|
maximum_exceeded: "has seleccionado %{given} respuestas, pero el máximo que puedes seleccionar es %{maximum}"
|
||||||
index:
|
index:
|
||||||
filters:
|
filters:
|
||||||
current: "Abiertas"
|
current: "Abiertas"
|
||||||
@@ -602,6 +605,7 @@ es:
|
|||||||
show:
|
show:
|
||||||
already_voted_in_booth: "Ya has participado en esta votación en urnas presenciales, no puedes volver a participar."
|
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_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
|
back: Volver a votaciones
|
||||||
cant_answer_not_logged_in: "Necesitas %{signin} o %{signup} para participar."
|
cant_answer_not_logged_in: "Necesitas %{signin} o %{signup} para participar."
|
||||||
comments_tab: Comentarios
|
comments_tab: Comentarios
|
||||||
@@ -634,11 +638,7 @@ es:
|
|||||||
poll_header:
|
poll_header:
|
||||||
back_to_proposal: Volver a la propuesta
|
back_to_proposal: Volver a la propuesta
|
||||||
poll_questions:
|
poll_questions:
|
||||||
show:
|
|
||||||
vote_answer: "Votar %{answer}"
|
|
||||||
voted: "Has votado %{answer}"
|
|
||||||
description:
|
description:
|
||||||
unique: "Puedes seleccionar un máximo de 1 respuesta."
|
|
||||||
multiple: "Puedes seleccionar un máximo de %{maximum} respuestas."
|
multiple: "Puedes seleccionar un máximo de %{maximum} respuestas."
|
||||||
read_more_about: "Leer más:"
|
read_more_about: "Leer más:"
|
||||||
proposal_notifications:
|
proposal_notifications:
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ es:
|
|||||||
poll_question_option: "Respuesta creada correctamente"
|
poll_question_option: "Respuesta creada correctamente"
|
||||||
poll_question_option_video: "Vídeo creado correctamente"
|
poll_question_option_video: "Vídeo creado correctamente"
|
||||||
poll_question_option_image: "Imagen cargada 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: "Propuesta creada correctamente."
|
||||||
proposal_notification: "Tu mensaje ha sido enviado correctamente."
|
proposal_notification: "Tu mensaje ha sido enviado correctamente."
|
||||||
budget_investment: "Proyecto de gasto creado correctamente."
|
budget_investment: "Proyecto de gasto creado correctamente."
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ resources :polls, only: [:show, :index] do
|
|||||||
member do
|
member do
|
||||||
get :stats
|
get :stats
|
||||||
get :results
|
get :results
|
||||||
end
|
post :answer
|
||||||
|
|
||||||
resources :questions, controller: "polls/questions", shallow: true, only: [] do
|
|
||||||
resources :answers, controller: "polls/answers", only: [:create, :destroy], shallow: false
|
|
||||||
end
|
end
|
||||||
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"
|
require "rails_helper"
|
||||||
|
|
||||||
describe Polls::Questions::QuestionComponent do
|
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
|
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)
|
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}")
|
page.find("#poll_question_#{question.id}") do |poll_question|
|
||||||
expect(poll_question).to have_content("Read more about")
|
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 "Yes", href: "#option_#{option_yes.id}"
|
||||||
expect(poll_question).to have_link("Answer B", href: "#option_#{option_b.id}")
|
expect(poll_question).to have_link "No", href: "#option_#{option_no.id}"
|
||||||
expect(poll_question).to have_content("Answer A, Answer B")
|
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
|
||||||
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)
|
expect { get :index }.to raise_exception(FeatureFlags::FeatureDisabled)
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|||||||
@@ -212,8 +212,14 @@ FactoryBot.define do
|
|||||||
factory :poll_answer, class: "Poll::Answer" do
|
factory :poll_answer, class: "Poll::Answer" do
|
||||||
question factory: [:poll_question, :yes_no]
|
question factory: [:poll_question, :yes_no]
|
||||||
author factory: [:user, :level_two]
|
author factory: [:user, :level_two]
|
||||||
answer { question.question_options.sample.title }
|
option do
|
||||||
option { question.question_options.find_by(title: answer) }
|
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
|
end
|
||||||
|
|
||||||
factory :poll_partial_result, class: "Poll::PartialResult" do
|
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_own_geozone) { create(:poll, geozone_restricted_to: [geozone]) }
|
||||||
let(:poll_from_other_geozone) { create(:poll, geozone_restricted_to: [create(: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(:own_proposal_document) { build(:document, documentable: own_proposal) }
|
||||||
let(:proposal_document) { build(:document, documentable: proposal) }
|
let(:proposal_document) { build(:document, documentable: proposal) }
|
||||||
let(:own_budget_investment_document) { build(:document, documentable: own_investment_in_accepting_budget) }
|
let(:own_budget_investment_document) { build(:document, documentable: own_investment_in_accepting_budget) }
|
||||||
@@ -255,36 +243,22 @@ describe Abilities::Common do
|
|||||||
it { should be_able_to(:answer, current_poll) }
|
it { should be_able_to(:answer, current_poll) }
|
||||||
it { should_not be_able_to(:answer, expired_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_from_own_geozone) }
|
||||||
it { should be_able_to(:answer, poll_question_from_all_geozones) }
|
it { should be_able_to(:answer, poll) }
|
||||||
it { should_not be_able_to(:answer, poll_question_from_other_geozone) }
|
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_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_from_other_geozone) }
|
||||||
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
|
|
||||||
|
|
||||||
context "without geozone" do
|
context "without geozone" do
|
||||||
before { user.geozone = nil }
|
before { user.geozone = nil }
|
||||||
|
|
||||||
it { should_not be_able_to(:answer, poll_question_from_own_geozone) }
|
it { should_not be_able_to(:answer, poll_from_own_geozone) }
|
||||||
it { should be_able_to(:answer, poll_question_from_all_geozones) }
|
it { should be_able_to(:answer, poll) }
|
||||||
it { should_not be_able_to(:answer, poll_question_from_other_geozone) }
|
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_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_from_other_geozone) }
|
||||||
it { should_not be_able_to(:answer, expired_poll_question_from_other_geozone) }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -342,23 +316,22 @@ describe Abilities::Common do
|
|||||||
it { should be_able_to(:answer, current_poll) }
|
it { should be_able_to(:answer, current_poll) }
|
||||||
it { should_not be_able_to(:answer, expired_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_from_own_geozone) }
|
||||||
it { should be_able_to(:answer, poll_question_from_all_geozones) }
|
it { should be_able_to(:answer, poll) }
|
||||||
it { should_not be_able_to(:answer, poll_question_from_other_geozone) }
|
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_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_from_other_geozone) }
|
||||||
it { should_not be_able_to(:answer, expired_poll_question_from_other_geozone) }
|
|
||||||
|
|
||||||
context "without geozone" do
|
context "without geozone" do
|
||||||
before { user.geozone = nil }
|
before { user.geozone = nil }
|
||||||
it { should_not be_able_to(:answer, poll_question_from_own_geozone) }
|
it { should_not be_able_to(:answer, poll_from_own_geozone) }
|
||||||
it { should be_able_to(:answer, poll_question_from_all_geozones) }
|
it { should be_able_to(:answer, poll) }
|
||||||
it { should_not be_able_to(:answer, poll_question_from_other_geozone) }
|
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_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) }
|
||||||
it { should_not be_able_to(:answer, expired_poll_question_from_other_geozone) }
|
it { should_not be_able_to(:answer, expired_poll_from_other_geozone) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -101,136 +101,5 @@ describe Poll::Answer do
|
|||||||
|
|
||||||
expect(build(:poll_answer, question: question, answer: "Four")).not_to be_valid
|
expect(build(:poll_answer, question: question, answer: "Four")).not_to be_valid
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ describe Poll::Stats do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe "total participants" do
|
describe "total participants" do
|
||||||
before { allow(stats).to receive(:total_web_white).and_return(1) }
|
|
||||||
|
|
||||||
it "supports every channel" do
|
it "supports every channel" do
|
||||||
3.times { create(:poll_voter, :from_web, poll: poll) }
|
3.times { create(:poll_voter, :from_web, poll: poll) }
|
||||||
create(:poll_recount, :from_booth, poll: poll,
|
create(:poll_recount, :from_booth, poll: poll,
|
||||||
@@ -49,15 +47,29 @@ describe Poll::Stats do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe "#total_web_valid" do
|
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
|
2.times do
|
||||||
3.times { create(:poll_voter, :from_web, poll: poll) }
|
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)
|
expect(stats.total_web_valid).to eq(2)
|
||||||
end
|
end
|
||||||
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
|
describe "#total_web_null" do
|
||||||
it "returns 0" do
|
it "returns 0" do
|
||||||
expect(stats.total_web_null).to eq(0)
|
expect(stats.total_web_null).to eq(0)
|
||||||
@@ -93,8 +105,8 @@ describe Poll::Stats do
|
|||||||
|
|
||||||
describe "valid percentage by channel" do
|
describe "valid percentage by channel" do
|
||||||
it "is relative to the total amount of valid votes" 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_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_web).to eq(33.333)
|
||||||
expect(stats.valid_percentage_booth).to eq(66.667)
|
expect(stats.valid_percentage_booth).to eq(66.667)
|
||||||
@@ -123,7 +135,7 @@ describe Poll::Stats do
|
|||||||
|
|
||||||
describe "#total_valid_votes" do
|
describe "#total_valid_votes" do
|
||||||
it "counts valid votes from every channel" 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: 3, white_amount: 10)
|
||||||
create(:poll_recount, :from_booth, poll: poll, total_amount: 4, null_amount: 20)
|
create(:poll_recount, :from_booth, poll: poll, total_amount: 4, null_amount: 20)
|
||||||
|
|
||||||
@@ -150,10 +162,9 @@ describe Poll::Stats do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe "total percentage by type" do
|
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
|
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,
|
create(:poll_recount, :from_booth, poll: poll,
|
||||||
total_amount: 8,
|
total_amount: 8,
|
||||||
white_amount: 5,
|
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
|
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)
|
visit poll_path(poll)
|
||||||
|
|
||||||
within("#poll_question_#{question.id}_options") do
|
questions_with_options.each do |question, option|
|
||||||
click_button option
|
within_fieldset(question.title) { choose option }
|
||||||
|
|
||||||
expect(page).to have_button("You have voted #{option}")
|
|
||||||
expect(page).not_to have_button("Vote #{option}")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
click_button "Vote"
|
||||||
|
|
||||||
|
expect(page).to have_content "Thank you for voting!"
|
||||||
end
|
end
|
||||||
|
|
||||||
def vote_for_poll_via_booth
|
def vote_for_poll_via_booth
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ describe "Polls" do
|
|||||||
|
|
||||||
expect(page).not_to have_css ".already-answer"
|
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
|
visit polls_path
|
||||||
|
|
||||||
@@ -168,14 +168,6 @@ describe "Polls" do
|
|||||||
end
|
end
|
||||||
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
|
scenario "Level 1 users" do
|
||||||
poll.update!(geozone_restricted_to: [geozone])
|
poll.update!(geozone_restricted_to: [geozone])
|
||||||
create(:poll_question, :yes_no, poll: poll)
|
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")
|
expect(page).to have_content("You must verify your account in order to answer")
|
||||||
end
|
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
|
scenario "Level 2 users answering" do
|
||||||
poll.update!(geozone_restricted_to: [geozone])
|
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)
|
login_as(create(:user, :level_two, geozone: geozone))
|
||||||
user = create(:user, :level_two, geozone: geozone)
|
|
||||||
|
|
||||||
login_as user
|
|
||||||
visit poll_path(poll)
|
visit poll_path(poll)
|
||||||
|
|
||||||
within("#poll_question_#{question.id}_options") do
|
within_fieldset("Do you agree?") { choose "Yes" }
|
||||||
click_button "Vote Yes"
|
click_button "Vote"
|
||||||
|
|
||||||
expect(page).to have_button "You have voted Yes"
|
expect(page).to have_content "Thank you for voting!"
|
||||||
expect(page).to have_button "Vote No"
|
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
|
end
|
||||||
|
|
||||||
|
expect(page).to have_button "Vote"
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario "Level 2 users changing answer" do
|
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)
|
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
|
login_as user
|
||||||
visit poll_path(poll)
|
visit poll_path(poll)
|
||||||
|
|
||||||
within("#poll_question_#{question.id}_options") do
|
expect(page).to have_content "You have already participated in this poll. " \
|
||||||
click_button "Yes"
|
"If you vote again it will be overwritten."
|
||||||
|
|
||||||
expect(page).to have_button "You have voted Yes"
|
within_fieldset("Do you agree?") do
|
||||||
expect(page).to have_button "Vote No"
|
expect(page).to have_field "Yes", type: :radio, checked: true
|
||||||
|
|
||||||
click_button "No"
|
choose "No"
|
||||||
|
|
||||||
expect(page).to have_button "Vote Yes"
|
|
||||||
expect(page).to have_button "You have voted No"
|
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
scenario "Shows SDG tags when feature is enabled" do
|
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("Not restricted").to appear_before("Geozone Poll")
|
||||||
expect("Geozone Poll").to appear_before("A Poll")
|
expect("Geozone Poll").to appear_before("A Poll")
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
context "Booth & Website", :with_frozen_time do
|
context "Booth & Website", :with_frozen_time do
|
||||||
@@ -283,7 +292,7 @@ describe "Polls" do
|
|||||||
scenario "Already voted on booth cannot vote on website" 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_shift, officer: officer, booth: booth, date: Date.current, task: :vote_collection)
|
||||||
create(:poll_officer_assignment, officer: officer, poll: poll, booth: booth, date: Date.current)
|
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)
|
user = create(:user, :level_two, :in_census)
|
||||||
|
|
||||||
login_as(officer.user)
|
login_as(officer.user)
|
||||||
@@ -304,12 +313,9 @@ describe "Polls" do
|
|||||||
expect(page).to have_content "You have already participated in a physical booth. " \
|
expect(page).to have_content "You have already participated in a physical booth. " \
|
||||||
"You can not participate again."
|
"You can not participate again."
|
||||||
|
|
||||||
within("#poll_question_#{question.id}_options") do
|
within_fieldset("Have you voted using a booth?") do
|
||||||
expect(page).to have_content("Yes")
|
expect(page).to have_field "Yes", type: :radio, disabled: true
|
||||||
expect(page).to have_content("No")
|
expect(page).to have_field "No", type: :radio, disabled: true
|
||||||
|
|
||||||
expect(page).not_to have_button "Yes"
|
|
||||||
expect(page).not_to have_button "No"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,18 +17,15 @@ describe "Poll Results" do
|
|||||||
option5 = create(:poll_question_option, question: question2, title: "Yellow")
|
option5 = create(:poll_question_option, question: question2, title: "Yellow")
|
||||||
|
|
||||||
login_as user1
|
login_as user1
|
||||||
vote_for_poll_via_web(poll, question1, "Yes")
|
vote_for_poll_via_web(poll, question1 => "Yes", question2 => "Blue")
|
||||||
vote_for_poll_via_web(poll, question2, "Blue")
|
|
||||||
logout
|
logout
|
||||||
|
|
||||||
login_as user2
|
login_as user2
|
||||||
vote_for_poll_via_web(poll, question1, "Yes")
|
vote_for_poll_via_web(poll, question1 => "Yes", question2 => "Green")
|
||||||
vote_for_poll_via_web(poll, question2, "Green")
|
|
||||||
logout
|
logout
|
||||||
|
|
||||||
login_as user3
|
login_as user3
|
||||||
vote_for_poll_via_web(poll, question1, "No")
|
vote_for_poll_via_web(poll, question1 => "No", question2 => "Yellow")
|
||||||
vote_for_poll_via_web(poll, question2, "Yellow")
|
|
||||||
logout
|
logout
|
||||||
|
|
||||||
travel_to(poll.ends_at + 1.day)
|
travel_to(poll.ends_at + 1.day)
|
||||||
|
|||||||
@@ -2,68 +2,102 @@ require "rails_helper"
|
|||||||
|
|
||||||
describe "Poll Votation Type" do
|
describe "Poll Votation Type" do
|
||||||
let(:author) { create(:user, :level_two) }
|
let(:author) { create(:user, :level_two) }
|
||||||
|
let(:poll) { create(:poll) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
login_as(author)
|
login_as(author)
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario "Unique answer" do
|
scenario "Unique and multiple answers" do
|
||||||
question = create(:poll_question_unique, :yes_no)
|
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."
|
within_fieldset("Is it that bad?") { choose "Yes" }
|
||||||
expect(page).to have_content(question.title)
|
|
||||||
expect(page).to have_button("Vote Yes")
|
|
||||||
expect(page).to have_button("Vote No")
|
|
||||||
|
|
||||||
within "#poll_question_#{question.id}_options" do
|
within_fieldset("Which ones do you prefer?") do
|
||||||
click_button "Yes"
|
check "Answer A"
|
||||||
|
check "Answer C"
|
||||||
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")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario "Multiple answers" do
|
click_button "Vote"
|
||||||
question = create(:poll_question_multiple, :abc, max_votes: 2)
|
|
||||||
visit poll_path(question.poll)
|
|
||||||
|
|
||||||
expect(page).to have_content "You can select a maximum of 2 answers."
|
expect(page).to have_content "Thank you for voting!"
|
||||||
expect(page).to have_content(question.title)
|
expect(page).to have_content "You have already participated in this poll. " \
|
||||||
expect(page).to have_button("Vote Answer A")
|
"If you vote again it will be overwritten."
|
||||||
expect(page).to have_button("Vote Answer B")
|
|
||||||
expect(page).to have_button("Vote Answer C")
|
|
||||||
|
|
||||||
within "#poll_question_#{question.id}_options" do
|
within_fieldset("Is it that bad?") do
|
||||||
click_button "Vote Answer A"
|
expect(page).to have_field "Yes", type: :radio, checked: true
|
||||||
|
end
|
||||||
|
|
||||||
expect(page).to have_button("You have voted Answer A")
|
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
|
||||||
|
|
||||||
click_button "Vote Answer C"
|
expect(page).to have_button "Vote"
|
||||||
|
end
|
||||||
|
|
||||||
expect(page).to have_button("You have voted Answer C")
|
scenario "Maximum votes has been reached" do
|
||||||
expect(page).to have_button("Vote Answer B", disabled: true)
|
question = create(:poll_question_multiple, :abc, poll: poll, max_votes: 2)
|
||||||
|
create(:poll_answer, author: author, question: question, answer: "Answer A")
|
||||||
|
|
||||||
click_button "You have voted Answer A"
|
visit poll_path(poll)
|
||||||
|
|
||||||
expect(page).to have_button("Vote Answer A")
|
expect(page).to have_field "Answer A", type: :checkbox, checked: true
|
||||||
expect(page).to have_button("Vote Answer B")
|
expect(page).to have_field "Answer B", type: :checkbox, checked: false
|
||||||
|
expect(page).to have_field "Answer C", type: :checkbox, checked: false
|
||||||
|
|
||||||
click_button "You have voted Answer C"
|
check "Answer C"
|
||||||
|
|
||||||
expect(page).to have_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
|
||||||
|
|
||||||
click_button "Vote Answer B"
|
click_button "Vote"
|
||||||
|
|
||||||
expect(page).to have_button("You have voted Answer B")
|
expect(page).to have_content "Thank you for voting!"
|
||||||
expect(page).to have_button("Vote Answer A")
|
expect(page).to have_field "Answer A", type: :checkbox, checked: true
|
||||||
expect(page).to have_button("Vote Answer C")
|
expect(page).to have_field "Answer B", type: :checkbox, checked: false, disabled: true
|
||||||
|
expect(page).to have_field "Answer C", type: :checkbox, checked: true
|
||||||
|
|
||||||
|
uncheck "Answer A"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
scenario "Too many answers", :no_js do
|
||||||
|
create(:poll_question_multiple, :abcde, poll: poll, max_votes: 2, title: "Which ones are correct?")
|
||||||
|
|
||||||
|
visit poll_path(poll)
|
||||||
|
check "Answer A"
|
||||||
|
check "Answer B"
|
||||||
|
check "Answer D"
|
||||||
|
click_button "Vote"
|
||||||
|
|
||||||
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ require "rails_helper"
|
|||||||
describe "Voter" do
|
describe "Voter" do
|
||||||
context "Origin", :with_frozen_time do
|
context "Origin", :with_frozen_time do
|
||||||
let(:poll) { create(:poll) }
|
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(:booth) { create(:poll_booth) }
|
||||||
let(:officer) { create(:poll_officer) }
|
let(:officer) { create(:poll_officer) }
|
||||||
let(:admin) { create(:administrator) }
|
let(:admin) { create(:administrator) }
|
||||||
@@ -20,57 +20,12 @@ describe "Voter" do
|
|||||||
login_as user
|
login_as user
|
||||||
visit poll_path(poll)
|
visit poll_path(poll)
|
||||||
|
|
||||||
within("#poll_question_#{question.id}_options") do
|
within_fieldset("Is this question stupid?") { choose "Yes" }
|
||||||
click_button "Vote Yes"
|
click_button "Vote"
|
||||||
|
|
||||||
expect(page).to have_button("You have voted Yes")
|
expect(page).to have_content "Thank you for voting!"
|
||||||
expect(page).not_to have_button("Vote Yes")
|
expect(page).to have_content "You have already participated in this poll. " \
|
||||||
end
|
"If you vote again it will be overwritten."
|
||||||
|
|
||||||
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"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario "Voting in booth" do
|
scenario "Voting in booth" do
|
||||||
@@ -142,7 +97,7 @@ describe "Voter" do
|
|||||||
|
|
||||||
scenario "Trying to vote in web and then in booth" do
|
scenario "Trying to vote in web and then in booth" do
|
||||||
login_as user
|
login_as user
|
||||||
vote_for_poll_via_web(poll, question, "Yes")
|
vote_for_poll_via_web(poll, question => "Yes")
|
||||||
|
|
||||||
logout
|
logout
|
||||||
login_through_form_as_officer(officer)
|
login_through_form_as_officer(officer)
|
||||||
@@ -165,8 +120,9 @@ describe "Voter" do
|
|||||||
login_as user
|
login_as user
|
||||||
visit poll_path(poll)
|
visit poll_path(poll)
|
||||||
|
|
||||||
within("#poll_question_#{question.id}_options") do
|
within_fieldset "Is this question stupid?" do
|
||||||
expect(page).not_to have_button("Yes")
|
expect(page).to have_field "Yes", type: :radio, disabled: true
|
||||||
|
expect(page).to have_field "No", type: :radio, disabled: true
|
||||||
end
|
end
|
||||||
expect(page).to have_content "You have already participated in a physical booth. " \
|
expect(page).to have_content "You have already participated in a physical booth. " \
|
||||||
"You can not participate again."
|
"You can not participate again."
|
||||||
@@ -185,7 +141,7 @@ describe "Voter" do
|
|||||||
end
|
end
|
||||||
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")
|
allow_any_instance_of(Verification::Sms).to receive(:generate_confirmation_code).and_return("1357")
|
||||||
user = create(:user)
|
user = create(:user)
|
||||||
admin_user = admin.user
|
admin_user = admin.user
|
||||||
@@ -203,8 +159,9 @@ describe "Voter" do
|
|||||||
|
|
||||||
visit poll_path(poll)
|
visit poll_path(poll)
|
||||||
|
|
||||||
within("#poll_question_#{question.id}_options") do
|
within_fieldset "Is this question stupid?" do
|
||||||
expect(page).not_to have_button("Yes")
|
expect(page).to have_field "Yes", type: :radio, disabled: true
|
||||||
|
expect(page).to have_field "No", type: :radio, disabled: true
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(page).to have_content "You have already participated in a physical booth. " \
|
expect(page).to have_content "You have already participated in a physical booth. " \
|
||||||
|
|||||||
Reference in New Issue
Block a user