diff --git a/.rubocop.yml b/.rubocop.yml index ca3696b9b..6f0d769a4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -571,6 +571,9 @@ RSpec/BeNil: Enabled: true EnforcedStyle: be +RSpec/ChangeByZero: + Enabled: true + RSpec/ContextMethod: Enabled: true diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index d1070d56a..c4b419a95 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -120,6 +120,7 @@ //= require datepicker //= require authenticity_token_refresh //= require_tree ./admin +//= require_tree ./polls //= require_tree ./sdg //= require_tree ./sdg_management //= require_tree ./custom @@ -178,6 +179,7 @@ var initialize_modules = function() { App.BudgetEditAssociations.initialize(); App.BudgetHideMoney.initialize(); App.Datepicker.initialize(); + App.PollsForm.initialize(); App.SDGRelatedListSelector.initialize(); App.SDGManagementRelationSearch.initialize(); App.AuthenticityTokenRefresh.initialize(); diff --git a/app/assets/javascripts/polls/form.js b/app/assets/javascripts/polls/form.js new file mode 100644 index 000000000..f44e66742 --- /dev/null +++ b/app/assets/javascripts/polls/form.js @@ -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); diff --git a/app/assets/stylesheets/admin/legislation/draft_versions/form.scss b/app/assets/stylesheets/admin/legislation/draft_versions/form.scss index 7764ccbf7..ac58b6e3b 100644 --- a/app/assets/stylesheets/admin/legislation/draft_versions/form.scss +++ b/app/assets/stylesheets/admin/legislation/draft_versions/form.scss @@ -90,7 +90,7 @@ @include radio-or-checkbox-and-label-alignment; span { - margin-left: 1ch; + margin-#{$global-left}: 1ch; } } } diff --git a/app/assets/stylesheets/participation.scss b/app/assets/stylesheets/participation.scss index 8700b6ef0..09b22bd4f 100644 --- a/app/assets/stylesheets/participation.scss +++ b/app/assets/stylesheets/participation.scss @@ -1396,8 +1396,7 @@ position: relative; } -.public .poll, -.poll-question { +.public .poll { border: 1px solid $border; margin-bottom: calc($line-height / 2); padding: calc($line-height / 2); @@ -1413,47 +1412,6 @@ } } -.poll-question { - padding: 0 $line-height; - - h3 { - padding-top: $line-height; - } -} - -.poll-question-options { - @include flex-with-gap($line-height * 0.25); - flex-wrap: wrap; - - .button { - min-width: rem-calc(168); - - @include breakpoint(medium down) { - width: 100%; - } - - &.answered { - background: #f4f8ec; - border: 2px solid #92ba48; - color: color-pick-contrast(#f4f8ec); - position: relative; - - &::after { - background: #92ba48; - border-radius: rem-calc(20); - color: #fff; - content: "\6c"; - font-family: "icons" !important; - font-size: rem-calc(12); - padding: calc($line-height / 4); - position: absolute; - right: -6px; - top: -6px; - } - } - } -} - // 09. Polls results and stats // --------------------------- diff --git a/app/assets/stylesheets/polls/form.scss b/app/assets/stylesheets/polls/form.scss new file mode 100644 index 000000000..f6129dbf2 --- /dev/null +++ b/app/assets/stylesheets/polls/form.scss @@ -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); + } +} diff --git a/app/components/polls/callout_component.html.erb b/app/components/polls/callout_component.html.erb new file mode 100644 index 000000000..0db5062e3 --- /dev/null +++ b/app/components/polls/callout_component.html.erb @@ -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 %> diff --git a/app/components/polls/callout_component.rb b/app/components/polls/callout_component.rb new file mode 100644 index 000000000..aa1dbf2f9 --- /dev/null +++ b/app/components/polls/callout_component.rb @@ -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 diff --git a/app/components/polls/form_component.html.erb b/app/components/polls/form_component.html.erb new file mode 100644 index 000000000..8b0d1fe78 --- /dev/null +++ b/app/components/polls/form_component.html.erb @@ -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 %> diff --git a/app/components/polls/form_component.rb b/app/components/polls/form_component.rb new file mode 100644 index 000000000..48dafc51f --- /dev/null +++ b/app/components/polls/form_component.rb @@ -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 diff --git a/app/components/polls/questions/options_component.html.erb b/app/components/polls/questions/options_component.html.erb deleted file mode 100644 index a183b2aac..000000000 --- a/app/components/polls/questions/options_component.html.erb +++ /dev/null @@ -1,35 +0,0 @@ -
- <% 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| %> - <%= question_option.title %> - <% end %> - <% end %> -
diff --git a/app/components/polls/questions/options_component.rb b/app/components/polls/questions/options_component.rb deleted file mode 100644 index 25875cd6e..000000000 --- a/app/components/polls/questions/options_component.rb +++ /dev/null @@ -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 diff --git a/app/components/polls/questions/question_component.html.erb b/app/components/polls/questions/question_component.html.erb index 46563a87b..075fc4dc1 100644 --- a/app/components/polls/questions/question_component.html.erb +++ b/app/components/polls/questions/question_component.html.erb @@ -1,22 +1,23 @@ -
-

- <%= question.title %> -

+
> + <%= question.title %> - <% if question.votation_type.present? %> - - <%= t("poll_questions.description.#{question.vote_type}", maximum: question.max_votes) %> - + <% if multiple_choice? %> + <%= multiple_choice_help_text %> + + <% question.question_options.each do |option| %> + <%= multiple_choice_field(option) %> + <% end %> + <% else %> + <% question.question_options.each do |option| %> + <%= single_choice_field(option) %> + <% end %> <% end %> -
- <%= render Polls::Questions::OptionsComponent.new(question) %> -
- <% if question.options_with_read_more? %> -
+ <% end %> -
+ <%= form.error_for(:"question_#{question.id}") %> +
diff --git a/app/components/polls/questions/question_component.rb b/app/components/polls/questions/question_component.rb index ba52796b5..cd74b39fc 100644 --- a/app/components/polls/questions/question_component.rb +++ b/app/components/polls/questions/question_component.rb @@ -1,13 +1,74 @@ class Polls::Questions::QuestionComponent < ApplicationComponent - attr_reader :question + attr_reader :question, :form, :disabled + alias_method :disabled?, :disabled - def initialize(question:) + def initialize(question, form:, disabled: false) @question = question + @form = form + @disabled = disabled end - def options_read_more_links - safe_join(question.options_with_read_more.map do |option| - link_to option.title, "#option_#{option.id}" - end, ", ") - end + private + + def fieldset_attributes + tag.attributes( + id: dom_id(question), + disabled: ("disabled" if disabled?), + class: fieldset_class, + data: { max_votes: question.max_votes } + ) + end + + def fieldset_class + if multiple_choice? + "multiple-choice" + else + "single-choice" + end + end + + def options_read_more_links + safe_join(question.options_with_read_more.map do |option| + link_to option.title, "#option_#{option.id}" + end, ", ") + end + + def multiple_choice? + question.multiple? + end + + def multiple_choice_help_text + tag.span( + t("poll_questions.description.multiple", maximum: question.max_votes), + class: "help-text" + ) + end + + def multiple_choice_field(option) + choice_field(option) do + check_box_tag "web_vote[#{question.id}][option_id][]", + option.id, + checked?(option), + id: "web_vote_option_#{option.id}" + end + end + + def single_choice_field(option) + choice_field(option) do + radio_button_tag "web_vote[#{question.id}][option_id]", + option.id, + checked?(option), + id: "web_vote_option_#{option.id}" + end + end + + def choice_field(option, &block) + label_tag("web_vote_option_#{option.id}") do + block.call + option.title + end + end + + def checked?(option) + form.object.answers[question.id].find { |answer| answer.option_id == option.id } + end end diff --git a/app/controllers/polls/answers_controller.rb b/app/controllers/polls/answers_controller.rb deleted file mode 100644 index a969c5edf..000000000 --- a/app/controllers/polls/answers_controller.rb +++ /dev/null @@ -1,36 +0,0 @@ -class Polls::AnswersController < ApplicationController - load_and_authorize_resource :question, class: "::Poll::Question" - load_and_authorize_resource :answer, class: "::Poll::Answer", - through: :question, - through_association: :answers, - only: :destroy - - def create - authorize! :answer, @question - - answer = @question.find_or_initialize_user_answer(current_user, params[:option_id]) - answer.save_and_record_voter_participation - - respond_to do |format| - format.html do - redirect_to request.referer - end - format.js do - render :show - end - end - end - - def destroy - @answer.destroy_and_remove_voter_participation - - respond_to do |format| - format.html do - redirect_to request.referer - end - format.js do - render :show - end - end - end -end diff --git a/app/controllers/polls_controller.rb b/app/controllers/polls_controller.rb index 80b72309b..d09e15812 100644 --- a/app/controllers/polls_controller.rb +++ b/app/controllers/polls_controller.rb @@ -9,7 +9,7 @@ class PollsController < ApplicationController load_and_authorize_resource has_filters %w[current expired] - has_orders %w[most_voted newest oldest], only: :show + has_orders %w[most_voted newest oldest], only: [:show, :answer] def index @polls = Kaminari.paginate_array( @@ -18,10 +18,25 @@ class PollsController < ApplicationController end def show - @questions = @poll.questions.for_render.sort_for_list + @web_vote = Poll::WebVote.new(@poll, current_user) @comment_tree = CommentTree.new(@poll, params[:page], @current_order) end + def answer + @web_vote = Poll::WebVote.new(@poll, current_user) + + if @web_vote.update(answer_params) + if answer_params.blank? + redirect_to @poll, notice: t("flash.actions.create.poll_voter_blank") + else + redirect_to @poll, notice: t("flash.actions.create.poll_voter") + end + else + @comment_tree = CommentTree.new(@poll, params[:page], @current_order) + render :show + end + end + def stats @stats = Poll::Stats.new(@poll).tap(&:generate) end @@ -38,4 +53,8 @@ class PollsController < ApplicationController def load_active_poll @active_poll = ActivePoll.first end + + def answer_params + params[:web_vote] || {} + end end diff --git a/app/models/abilities/common.rb b/app/models/abilities/common.rb index 369630981..429ff7ef7 100644 --- a/app/models/abilities/common.rb +++ b/app/models/abilities/common.rb @@ -110,12 +110,6 @@ module Abilities can :answer, Poll do |poll| poll.answerable_by?(user) end - can :answer, Poll::Question do |question| - question.answerable_by?(user) - end - can :destroy, Poll::Answer do |answer| - answer.author == user && answer.question.answerable_by?(user) - end end can [:create, :show], ProposalNotification, proposal: { author_id: user.id } diff --git a/app/models/poll.rb b/app/models/poll.rb index 68c15face..8012f2fcd 100644 --- a/app/models/poll.rb +++ b/app/models/poll.rb @@ -23,6 +23,7 @@ class Poll < ApplicationRecord has_many :officer_assignments, through: :booth_assignments has_many :officers, through: :officer_assignments has_many :questions, inverse_of: :poll, dependent: :destroy + has_many :answers, through: :questions has_many :comments, as: :commentable, inverse_of: :commentable has_many :ballot_sheets diff --git a/app/models/poll/answer.rb b/app/models/poll/answer.rb index b9c52fb38..d4b0150e2 100644 --- a/app/models/poll/answer.rb +++ b/app/models/poll/answer.rb @@ -17,32 +17,13 @@ class Poll::Answer < ApplicationRecord scope :by_author, ->(author_id) { where(author_id: author_id) } scope :by_question, ->(question_id) { where(question_id: question_id) } - def save_and_record_voter_participation - author.with_lock do - save! - Poll::Voter.find_or_create_by!(user: author, poll: poll, origin: "web") - end - end - - def destroy_and_remove_voter_participation - transaction do - destroy! - - if author.poll_answers.where(question_id: poll.question_ids).none? - Poll::Voter.find_by(user: author, poll: poll, origin: "web").destroy! - end - end - end - private def max_votes return if !question || !author || persisted? - author.with_lock do - if question.answers.by_author(author).count >= question.max_votes - errors.add(:answer, "Maximum number of votes per user exceeded") - end + if question.answers.by_author(author).count >= question.max_votes + errors.add(:answer, "Maximum number of votes per user exceeded") end end end diff --git a/app/models/poll/question.rb b/app/models/poll/question.rb index f1015bba7..93e9c4453 100644 --- a/app/models/poll/question.rb +++ b/app/models/poll/question.rb @@ -41,14 +41,6 @@ class Poll::Question < ApplicationRecord end end - delegate :answerable_by?, to: :poll - - def self.answerable_by(user) - return none if user.nil? || user.unverified? - - where(poll_id: Poll.answerable_by(user).pluck(:id)) - end - def options_total_votes question_options.reduce(0) { |total, question_option| total + question_option.total_votes } end diff --git a/app/models/poll/stats.rb b/app/models/poll/stats.rb index 8a0b1562b..5e03114b4 100644 --- a/app/models/poll/stats.rb +++ b/app/models/poll/stats.rb @@ -42,11 +42,11 @@ class Poll::Stats end def total_web_valid - voters.where(origin: "web").count - total_web_white + voters.where(origin: "web", user_id: poll.answers.select(:author_id).distinct).count end def total_web_white - 0 + voters.where(origin: "web").count - total_web_valid end def total_web_null diff --git a/app/models/poll/web_vote.rb b/app/models/poll/web_vote.rb new file mode 100644 index 000000000..9ceda20e5 --- /dev/null +++ b/app/models/poll/web_vote.rb @@ -0,0 +1,81 @@ +class Poll::WebVote + include ActiveModel::Validations + attr_reader :poll, :user + delegate :t, to: "ApplicationController.helpers" + + validate :max_answers + + def initialize(poll, user) + @poll = poll + @user = user + end + + def questions + poll.questions.for_render.sort_for_list + end + + def answers + @answers ||= questions.to_h do |question| + [question.id, question.answers.where(author: user)] + end + end + + def update(params) + all_valid = true + + user.with_lock do + self.answers = given_answers(params) + + questions.each do |question| + question.answers.where(author: user).where.not(id: answers[question.id].map(&:id)).destroy_all + + if valid? && answers[question.id].all?(&:valid?) + Poll::Voter.find_or_create_by!(user: user, poll: poll, origin: "web") + answers[question.id].each(&:save!) + else + all_valid = false + end + end + + raise ActiveRecord::Rollback unless all_valid + end + + all_valid + end + + def to_key + end + + def persisted? + Poll::Voter.where(user: user, poll: poll, origin: "web").exists? + end + + private + + attr_writer :answers + + def given_answers(params) + questions.to_h do |question| + [question.id, answers_for_question(question, params[question.id.to_s])] + end + end + + def answers_for_question(question, question_params) + return [] unless question_params + + Array(question_params[:option_id]).map do |option_id| + question.find_or_initialize_user_answer(user, option_id) + end + end + + def max_answers + questions.each do |question| + if answers[question.id].count > question.max_votes + errors.add( + :"question_#{question.id}", + t("polls.form.maximum_exceeded", maximum: question.max_votes, given: answers[question.id].count) + ) + end + end + end +end diff --git a/app/views/polls/_callout.html.erb b/app/views/polls/_callout.html.erb deleted file mode 100644 index 432d8c984..000000000 --- a/app/views/polls/_callout.html.erb +++ /dev/null @@ -1,22 +0,0 @@ -<% unless can?(:answer, @poll) %> - <% if current_user.nil? %> -
- <%= sanitize(t("polls.show.cant_answer_not_logged_in", - signin: link_to_signin(class: "probe-message"), - signup: link_to_signup(class: "probe-message"))) %> -
- <% elsif current_user.unverified? %> -
- <%= sanitize(t("polls.show.cant_answer_verify", - verify_link: link_to(t("polls.show.verify_link"), verification_path))) %> -
- <% elsif @poll.expired? %> -
- <%= t("polls.show.cant_answer_expired") %> -
- <% else %> -
- <%= t("polls.show.cant_answer_wrong_geozone") %> -
- <% end %> -<% end %> diff --git a/app/views/polls/answers/show.js.erb b/app/views/polls/answers/show.js.erb deleted file mode 100644 index 1cfab79f7..000000000 --- a/app/views/polls/answers/show.js.erb +++ /dev/null @@ -1 +0,0 @@ -$("#<%= dom_id(@question) %>_options").html("<%= j render Polls::Questions::OptionsComponent.new(@question) %>"); diff --git a/app/views/polls/show.html.erb b/app/views/polls/show.html.erb index f90679a9d..0836560d3 100644 --- a/app/views/polls/show.html.erb +++ b/app/views/polls/show.html.erb @@ -16,29 +16,15 @@
- <%= render "callout" %> - - <% if @poll.voted_in_booth?(current_user) %> -
- <%= t("polls.show.already_voted_in_booth") %> -
- <% else %> - - <% if current_user && @poll.voted_in_web?(current_user) && !@poll.expired? %> -
- <%= t("polls.show.already_voted_in_web") %> -
- <% end %> - <% end %> - - <%= render Polls::Questions::QuestionComponent.with_collection(@questions) %> + <%= render Polls::CalloutComponent.new(@poll) %> + <%= render Polls::FormComponent.new(@web_vote) %>
-

<%= t("polls.show.more_info_title") %>

+

<%= t("polls.show.more_info_title") %>

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