Merge pull request #5012 from consul/multiple_answers
Add poll questions that accept multiple answers per user
This commit is contained in:
21
app/assets/javascripts/admin/votation_types/fields.js
Normal file
21
app/assets/javascripts/admin/votation_types/fields.js
Normal file
@@ -0,0 +1,21 @@
|
||||
(function() {
|
||||
"use strict";
|
||||
App.AdminVotationTypesFields = {
|
||||
adjustForm: function() {
|
||||
if ($(this).val() === "unique") {
|
||||
$(".max-votes").hide();
|
||||
$(".description-unique").show();
|
||||
$(".description-multiple").hide();
|
||||
$(".votation-type-max-votes").prop("disabled", true);
|
||||
} else {
|
||||
$(".max-votes").show();
|
||||
$(".description-unique").hide();
|
||||
$(".description-multiple").show();
|
||||
$(".votation-type-max-votes").prop("disabled", false);
|
||||
}
|
||||
},
|
||||
initialize: function() {
|
||||
$(".votation-type-vote-type").on("change", App.AdminVotationTypesFields.adjustForm).trigger("change");
|
||||
}
|
||||
};
|
||||
}).call(this);
|
||||
@@ -167,6 +167,7 @@ var initialize_modules = function() {
|
||||
}
|
||||
App.AdminBudgetsWizardCreationStep.initialize();
|
||||
App.AdminMachineLearningScripts.initialize();
|
||||
App.AdminVotationTypesFields.initialize();
|
||||
App.BudgetEditAssociations.initialize();
|
||||
App.BudgetHideMoney.initialize();
|
||||
App.Datepicker.initialize();
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
attribute: "data-answer-id"
|
||||
});
|
||||
$.ajax({
|
||||
url: $(".sortable").data("js-url"),
|
||||
url: $(this).data("js-url"),
|
||||
data: {
|
||||
ordered_list: new_order
|
||||
},
|
||||
|
||||
@@ -29,6 +29,14 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="small-12">
|
||||
<%= f.fields_for :votation_type do |votation_f| %>
|
||||
<%= render Admin::VotationTypes::FieldsComponent.new(form: votation_f) %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="small-12 medium-4 large-2 margin-top column">
|
||||
<%= f.submit(class: "button success expanded", value: t("shared.save")) %>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<div class="small-12 medium-6 column">
|
||||
<%= form.enum_select :vote_type, {}, class: "votation-type-vote-type" %>
|
||||
</div>
|
||||
|
||||
<div class="small-12 column">
|
||||
<div class="callout primary">
|
||||
<span class="description-unique">
|
||||
<%= t("admin.polls.votation_type.unique_description") %>
|
||||
</span>
|
||||
<span class="description-multiple hidden">
|
||||
<%= t("admin.polls.votation_type.multiple_description") %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="small-12 medium-6 column end">
|
||||
<div class="max-votes">
|
||||
<%= form.number_field :max_votes, min: 2, max: 999, class: "votation-type-max-votes" %>
|
||||
</div>
|
||||
</div>
|
||||
7
app/components/admin/votation_types/fields_component.rb
Normal file
7
app/components/admin/votation_types/fields_component.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
class Admin::VotationTypes::FieldsComponent < ApplicationComponent
|
||||
attr_reader :form
|
||||
|
||||
def initialize(form:)
|
||||
@form = form
|
||||
end
|
||||
end
|
||||
@@ -1,16 +1,22 @@
|
||||
<div class="poll-question-answers">
|
||||
<% if can?(:answer, question) && !question.poll.voted_in_booth?(current_user) %>
|
||||
<% question_answers.each do |question_answer| %>
|
||||
<% if already_answered?(question_answer) && !voted_before_sign_in? %>
|
||||
<span class="button answered"
|
||||
title="<%= t("poll_questions.show.voted", answer: question_answer.title) %>">
|
||||
<% if already_answered?(question_answer) %>
|
||||
<%= button_to question_answer_path(question, user_answer(question_answer)),
|
||||
method: :delete,
|
||||
remote: true,
|
||||
title: t("poll_questions.show.voted", answer: question_answer.title),
|
||||
class: "button answered",
|
||||
"aria-pressed": true do %>
|
||||
<%= question_answer.title %>
|
||||
</span>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= button_to answer_question_path(question, answer: question_answer.title),
|
||||
remote: true,
|
||||
title: t("poll_questions.show.vote_answer", answer: question_answer.title),
|
||||
class: "button secondary hollow" do %>
|
||||
class: "button secondary hollow",
|
||||
"aria-pressed": false,
|
||||
disabled: disable_answer?(question_answer) do %>
|
||||
<%= question_answer.title %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -7,19 +7,21 @@ class Polls::Questions::AnswersComponent < ApplicationComponent
|
||||
end
|
||||
|
||||
def already_answered?(question_answer)
|
||||
user_answers.find_by(answer: question_answer.title).present?
|
||||
end
|
||||
|
||||
def voted_before_sign_in?
|
||||
user_answers.any? do |vote|
|
||||
vote.updated_at < current_user.current_sign_in_at
|
||||
end
|
||||
user_answer(question_answer).present?
|
||||
end
|
||||
|
||||
def question_answers
|
||||
question.question_answers
|
||||
end
|
||||
|
||||
def user_answer(question_answer)
|
||||
user_answers.find_by(answer: question_answer.title)
|
||||
end
|
||||
|
||||
def disable_answer?(question_answer)
|
||||
question.multiple? && user_answers.count == question.max_votes
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_answers
|
||||
|
||||
@@ -3,7 +3,20 @@
|
||||
<%= question.title %>
|
||||
</h3>
|
||||
|
||||
<% if question.votation_type.present? %>
|
||||
<strong>
|
||||
<%= t("poll_questions.description.#{question.vote_type}", maximum: question.max_votes) %>
|
||||
</strong>
|
||||
<% end %>
|
||||
|
||||
<div id="<%= dom_id(question) %>_answers" class="padding">
|
||||
<%= render Polls::Questions::AnswersComponent.new(question) %>
|
||||
</div>
|
||||
|
||||
<% if question.answers_with_read_more? %>
|
||||
<div>
|
||||
<p><%= t("poll_questions.read_more_about") %></p>
|
||||
<p><%= answers_read_more_links %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -4,4 +4,10 @@ class Polls::Questions::QuestionComponent < ApplicationComponent
|
||||
def initialize(question:)
|
||||
@question = question
|
||||
end
|
||||
|
||||
def answers_read_more_links
|
||||
safe_join(question.answers_with_read_more.map do |answer|
|
||||
link_to answer.title, "#answer_#{answer.id}"
|
||||
end, ", ")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
<div class="small-12 medium-6 column end answer <%= cycle("first", "") %>" id="answer_<%= answer.id %>">
|
||||
<h3><%= answer.title %></h3>
|
||||
|
||||
<% if answer.images.any? %>
|
||||
<%= render "polls/gallery", answer: answer %>
|
||||
<% end %>
|
||||
|
||||
<% if answer.description.present? %>
|
||||
<div class="margin-top">
|
||||
<div id="answer_description_<%= answer.id %>" class="answer-description short" data-toggler="short">
|
||||
<%= wysiwyg(answer.description) %>
|
||||
</div>
|
||||
<div class="read-more">
|
||||
<button type="button" id="read_more_<%= answer.id %>"
|
||||
data-toggle="answer_description_<%= answer.id %> read_more_<%= answer.id %> read_less_<%= answer.id %>"
|
||||
data-toggler="hide">
|
||||
<%= t("polls.show.read_more", answer: answer.title) %>
|
||||
</button>
|
||||
<button type="button" id="read_less_<%= answer.id %>"
|
||||
data-toggle="answer_description_<%= answer.id %> read_more_<%= answer.id %> read_less_<%= answer.id %>"
|
||||
data-toggler="hide"
|
||||
class="hide">
|
||||
<%= t("polls.show.read_less", answer: answer.title) %>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if answer.documents.present? %>
|
||||
<div class="document-link">
|
||||
<p>
|
||||
<span class="icon-document"></span>
|
||||
<strong><%= t("polls.show.documents") %></strong>
|
||||
</p>
|
||||
|
||||
<% answer.documents.each do |document| %>
|
||||
<%= link_to document.title,
|
||||
document.attachment,
|
||||
target: "_blank",
|
||||
rel: "nofollow" %><br>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if answer.videos.present? %>
|
||||
<div class="video-link">
|
||||
<p>
|
||||
<span class="icon-video"></span>
|
||||
<strong><%= t("polls.show.videos") %></strong>
|
||||
</p>
|
||||
|
||||
<% answer.videos.each do |video| %>
|
||||
<%= link_to video.title,
|
||||
video.url,
|
||||
target: "_blank",
|
||||
rel: "nofollow" %><br>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -1,9 +0,0 @@
|
||||
class Polls::Questions::ReadMoreAnswerComponent < ApplicationComponent
|
||||
with_collection_parameter :answer
|
||||
attr_reader :answer
|
||||
delegate :wysiwyg, to: :helpers
|
||||
|
||||
def initialize(answer:)
|
||||
@answer = answer
|
||||
end
|
||||
end
|
||||
63
app/components/polls/questions/read_more_component.html.erb
Normal file
63
app/components/polls/questions/read_more_component.html.erb
Normal file
@@ -0,0 +1,63 @@
|
||||
<h2><%= question.title %></h2>
|
||||
<% question.answers_with_read_more.each do |answer| %>
|
||||
<div class="small-12 medium-6 column end answer <%= cycle("first", "") %>" id="answer_<%= answer.id %>">
|
||||
<h3><%= answer.title %></h3>
|
||||
|
||||
<div class="margin-top">
|
||||
<% if answer.description.present? %>
|
||||
<div id="answer_description_<%= answer.id %>" class="answer-description short" data-toggler="short">
|
||||
<%= wysiwyg(answer.description) %>
|
||||
</div>
|
||||
<div class="read-more">
|
||||
<button type="button" id="read_more_<%= answer.id %>"
|
||||
data-toggle="answer_description_<%= answer.id %> read_more_<%= answer.id %> read_less_<%= answer.id %>"
|
||||
data-toggler="hide">
|
||||
<%= t("polls.show.read_more", answer: answer.title) %>
|
||||
</button>
|
||||
<button type="button" id="read_less_<%= answer.id %>"
|
||||
data-toggle="answer_description_<%= answer.id %> read_more_<%= answer.id %> read_less_<%= answer.id %>"
|
||||
data-toggler="hide"
|
||||
class="hide">
|
||||
<%= t("polls.show.read_less", answer: answer.title) %>
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if answer.images.any? %>
|
||||
<%= render "polls/gallery", answer: answer %>
|
||||
<% end %>
|
||||
|
||||
<% if answer.documents.present? %>
|
||||
<div class="document-link">
|
||||
<p>
|
||||
<span class="icon-document"></span>
|
||||
<strong><%= t("polls.show.documents") %></strong>
|
||||
</p>
|
||||
|
||||
<% answer.documents.each do |document| %>
|
||||
<%= link_to document.title,
|
||||
document.attachment,
|
||||
target: "_blank",
|
||||
rel: "nofollow" %><br>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if answer.videos.present? %>
|
||||
<div class="video-link">
|
||||
<p>
|
||||
<span class="icon-video"></span>
|
||||
<strong><%= t("polls.show.videos") %></strong>
|
||||
</p>
|
||||
|
||||
<% answer.videos.each do |video| %>
|
||||
<%= link_to video.title,
|
||||
video.url,
|
||||
target: "_blank",
|
||||
rel: "nofollow" %><br>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
13
app/components/polls/questions/read_more_component.rb
Normal file
13
app/components/polls/questions/read_more_component.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
class Polls::Questions::ReadMoreComponent < ApplicationComponent
|
||||
with_collection_parameter :question
|
||||
attr_reader :question
|
||||
delegate :wysiwyg, to: :helpers
|
||||
|
||||
def initialize(question:)
|
||||
@question = question
|
||||
end
|
||||
|
||||
def render?
|
||||
question.answers_with_read_more?
|
||||
end
|
||||
end
|
||||
@@ -14,10 +14,10 @@ class Admin::Poll::QuestionsController < Admin::Poll::BaseController
|
||||
end
|
||||
|
||||
def new
|
||||
@polls = Poll.all
|
||||
proposal = Proposal.find(params[:proposal_id]) if params[:proposal_id].present?
|
||||
@question.copy_attributes_from_proposal(proposal)
|
||||
@question.poll = @poll
|
||||
@question.votation_type = VotationType.new
|
||||
|
||||
authorize! :create, @question
|
||||
end
|
||||
@@ -58,8 +58,7 @@ class Admin::Poll::QuestionsController < Admin::Poll::BaseController
|
||||
end
|
||||
|
||||
def allowed_params
|
||||
attributes = [:poll_id, :question, :proposal_id]
|
||||
|
||||
attributes = [:poll_id, :question, :proposal_id, votation_type_attributes: [:vote_type, :max_votes]]
|
||||
[*attributes, translation_params(Poll::Question)]
|
||||
end
|
||||
|
||||
|
||||
19
app/controllers/polls/answers_controller.rb
Normal file
19
app/controllers/polls/answers_controller.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
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
|
||||
|
||||
def destroy
|
||||
@answer.destroy_and_remove_voter_participation
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_to request.referer
|
||||
end
|
||||
format.js do
|
||||
render "polls/questions/answers"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,9 +5,7 @@ class Polls::QuestionsController < ApplicationController
|
||||
has_orders %w[most_voted newest oldest], only: :show
|
||||
|
||||
def answer
|
||||
answer = @question.answers.find_or_initialize_by(author: current_user)
|
||||
|
||||
answer.answer = params[:answer]
|
||||
answer = @question.find_or_initialize_user_answer(current_user, params[:answer])
|
||||
answer.save_and_record_voter_participation
|
||||
|
||||
respond_to do |format|
|
||||
@@ -15,7 +13,7 @@ class Polls::QuestionsController < ApplicationController
|
||||
redirect_to request.referer
|
||||
end
|
||||
format.js do
|
||||
render :answer
|
||||
render :answers
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,10 +19,7 @@ class PollsController < ApplicationController
|
||||
|
||||
def show
|
||||
@questions = @poll.questions.for_render.sort_for_list
|
||||
@poll_questions_answers = Poll::Question::Answer.where(question: @poll.questions)
|
||||
.with_content.order(:given_order)
|
||||
@commentable = @poll
|
||||
@comment_tree = CommentTree.new(@commentable, params[:page], @current_order)
|
||||
@comment_tree = CommentTree.new(@poll, params[:page], @current_order)
|
||||
end
|
||||
|
||||
def stats
|
||||
|
||||
@@ -112,6 +112,9 @@ module Abilities
|
||||
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 }
|
||||
|
||||
30
app/models/concerns/questionable.rb
Normal file
30
app/models/concerns/questionable.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
module Questionable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_one :votation_type, as: :questionable, dependent: :destroy
|
||||
accepts_nested_attributes_for :votation_type
|
||||
delegate :max_votes, :multiple?, :vote_type, to: :votation_type, allow_nil: true
|
||||
end
|
||||
|
||||
def unique?
|
||||
votation_type.nil? || votation_type.unique?
|
||||
end
|
||||
|
||||
def find_or_initialize_user_answer(user, title)
|
||||
answer = answers.find_or_initialize_by(find_by_attributes(user, title))
|
||||
answer.answer = title
|
||||
answer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_by_attributes(user, title)
|
||||
case vote_type
|
||||
when "unique", nil
|
||||
{ author: user }
|
||||
when "multiple"
|
||||
{ author: user, answer: title }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -7,6 +7,7 @@ class Poll::Answer < ApplicationRecord
|
||||
validates :question, presence: true
|
||||
validates :author, presence: true
|
||||
validates :answer, presence: true
|
||||
validate :max_votes
|
||||
|
||||
validates :answer, inclusion: { in: ->(a) { a.question.possible_answers }},
|
||||
unless: ->(a) { a.question.blank? }
|
||||
@@ -21,4 +22,27 @@ class Poll::Answer < ApplicationRecord
|
||||
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 || question&.unique? || persisted?
|
||||
|
||||
author.reload
|
||||
author.lock!
|
||||
|
||||
if question.answers.by_author(author).count >= question.max_votes
|
||||
errors.add(:answer, "Maximum number of votes per user exceeded")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class Poll::Question < ApplicationRecord
|
||||
include Measurable
|
||||
include Searchable
|
||||
include Questionable
|
||||
|
||||
acts_as_paranoid column: :hidden_at
|
||||
include ActsAsParanoidAliases
|
||||
@@ -73,4 +74,12 @@ class Poll::Question < ApplicationRecord
|
||||
def possible_answers
|
||||
question_answers.joins(:translations).pluck("poll_question_answer_translations.title")
|
||||
end
|
||||
|
||||
def answers_with_read_more?
|
||||
answers_with_read_more.any?
|
||||
end
|
||||
|
||||
def answers_with_read_more
|
||||
question_answers.select(&:with_read_more?)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -40,4 +40,8 @@ class Poll::Question::Answer < ApplicationRecord
|
||||
def total_votes_percentage
|
||||
question.answers_total_votes.zero? ? 0 : (total_votes * 100.0) / question.answers_total_votes
|
||||
end
|
||||
|
||||
def with_read_more?
|
||||
description.present? || images.any? || documents.any? || videos.any?
|
||||
end
|
||||
end
|
||||
|
||||
17
app/models/votation_type.rb
Normal file
17
app/models/votation_type.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
class VotationType < ApplicationRecord
|
||||
belongs_to :questionable, polymorphic: true
|
||||
|
||||
QUESTIONABLE_TYPES = %w[Poll::Question].freeze
|
||||
|
||||
enum vote_type: %w[unique multiple]
|
||||
|
||||
validates :questionable, presence: true
|
||||
validates :questionable_type, inclusion: { in: ->(*) { QUESTIONABLE_TYPES }}
|
||||
validates :max_votes, presence: true, if: :max_votes_required?
|
||||
|
||||
private
|
||||
|
||||
def max_votes_required?
|
||||
multiple?
|
||||
end
|
||||
end
|
||||
@@ -28,6 +28,21 @@
|
||||
<%= link_to @question.proposal.title, proposal_path(@question.proposal) %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<% if @question.votation_type.present? %>
|
||||
<p>
|
||||
<strong><%= t("admin.polls.votation_type.title") %></strong>
|
||||
<br>
|
||||
<%= VotationType.human_attribute_name("vote_type.#{@question.vote_type}") %>
|
||||
</p>
|
||||
<% if @question.max_votes.present? %>
|
||||
<p>
|
||||
<strong><%= VotationType.human_attribute_name("max_votes") %></strong>
|
||||
<br>
|
||||
<%= @question.max_votes %>
|
||||
</p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
<div id="poll_more_info_answers" class="expanded poll-more-info-answers">
|
||||
<div class="row padding">
|
||||
<%= render Polls::Questions::ReadMoreAnswerComponent.with_collection(@poll_questions_answers) %>
|
||||
<%= render Polls::Questions::ReadMoreComponent.with_collection(@questions) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -145,6 +145,9 @@ en:
|
||||
local_census_record:
|
||||
one: Local census record
|
||||
other: Local census records
|
||||
votation_type:
|
||||
one: Votation type
|
||||
other: Votation types
|
||||
attributes:
|
||||
budget:
|
||||
name: "Name"
|
||||
@@ -451,7 +454,7 @@ en:
|
||||
description: Description
|
||||
poll/question/answer/translation:
|
||||
title: Answer
|
||||
description: Description
|
||||
description: "Description (optional)"
|
||||
poll/question/answer/video:
|
||||
title: Title
|
||||
url: External video
|
||||
@@ -514,6 +517,12 @@ en:
|
||||
document_number: Document number
|
||||
date_of_birth: Date of birth
|
||||
postal_code: Postal code
|
||||
votation_type:
|
||||
max_votes: Maximum number of votes
|
||||
vote_type: Votation type
|
||||
votation_type/vote_type:
|
||||
unique: Unique answer
|
||||
multiple: Multiple answers
|
||||
errors:
|
||||
models:
|
||||
user:
|
||||
|
||||
@@ -1107,6 +1107,10 @@ en:
|
||||
alert: "This action will remove the poll and all its associated questions."
|
||||
success_notice: "Poll deleted successfully"
|
||||
unable_notice: "You cannot delete a poll that has votes"
|
||||
votation_type:
|
||||
title: "Votation type"
|
||||
unique_description: "It's only possible to answer one time to the question."
|
||||
multiple_description: "Allows to choose multiple answers. It's possible to set the maximum number of answers."
|
||||
questions:
|
||||
index:
|
||||
title: "Questions"
|
||||
|
||||
@@ -636,6 +636,10 @@ en:
|
||||
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:
|
||||
new:
|
||||
title: "Send notification"
|
||||
|
||||
@@ -145,6 +145,9 @@ es:
|
||||
local_census_record:
|
||||
one: Registro del censo local
|
||||
other: Registros del censo local
|
||||
votation_type:
|
||||
one: Tipo de votación
|
||||
other: Tipos de votación
|
||||
attributes:
|
||||
budget:
|
||||
name: "Nombre"
|
||||
@@ -451,7 +454,7 @@ es:
|
||||
description: Descripción
|
||||
poll/question/answer/translation:
|
||||
title: Respuesta
|
||||
description: Descripción
|
||||
description: "Descripción (opcional)"
|
||||
poll/question/answer/video:
|
||||
title: Título
|
||||
url: Vídeo externo
|
||||
@@ -514,6 +517,12 @@ es:
|
||||
document_number: Número de documento
|
||||
date_of_birth: Fecha de nacimiento
|
||||
postal_code: Código postal
|
||||
votation_type:
|
||||
max_votes: Número máximo de votos
|
||||
vote_type: Tipo de votación
|
||||
votation_type/vote_type:
|
||||
unique: Respuesta única
|
||||
multiple: Respuesta múltiple
|
||||
errors:
|
||||
models:
|
||||
user:
|
||||
|
||||
@@ -1106,6 +1106,10 @@ es:
|
||||
alert: "Esta acción eliminará la votación y todas sus preguntas asociadas."
|
||||
success_notice: "Votación eliminada correctamente"
|
||||
unable_notice: "No se pueden eliminar votaciones con votos"
|
||||
votation_type:
|
||||
title: "Tipo de votación"
|
||||
unique_description: "Solo se puede responder a la pregunta con una única respuesta."
|
||||
multiple_description: "Permite elegir más de una respuesta. Se puede elegir el número máximo de respuestas."
|
||||
questions:
|
||||
index:
|
||||
title: "Preguntas de votaciones"
|
||||
|
||||
@@ -636,6 +636,10 @@ es:
|
||||
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:
|
||||
new:
|
||||
title: "Enviar notificación"
|
||||
|
||||
@@ -6,6 +6,7 @@ resources :polls, only: [:show, :index] do
|
||||
|
||||
resources :questions, controller: "polls/questions", shallow: true do
|
||||
post :answer, on: :member
|
||||
resources :answers, controller: "polls/answers", only: :destroy, shallow: false
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ end
|
||||
|
||||
section "Creating Poll Questions & Answers" do
|
||||
Poll.find_each do |poll|
|
||||
(1..4).to_a.sample.times do
|
||||
(3..5).to_a.sample.times do
|
||||
question_title = Faker::Lorem.sentence(word_count: 3).truncate(60) + "?"
|
||||
question = Poll::Question.new(author: User.all.sample,
|
||||
title: question_title,
|
||||
@@ -84,6 +84,15 @@ section "Creating Poll Questions & Answers" do
|
||||
end
|
||||
end
|
||||
|
||||
section "Creating Poll Votation types" do
|
||||
poll = Poll.first
|
||||
|
||||
poll.questions.each do |question|
|
||||
vote_type = VotationType.vote_types.keys.sample
|
||||
question.create_votation_type!(vote_type: vote_type, max_votes: (3 unless vote_type == "unique"))
|
||||
end
|
||||
end
|
||||
|
||||
section "Creating Poll Booths & BoothAssignments" do
|
||||
20.times do |i|
|
||||
Poll::Booth.create(name: "Booth #{i}",
|
||||
|
||||
12
db/migrate/20220323233643_add_votation_types.rb
Normal file
12
db/migrate/20220323233643_add_votation_types.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
class AddVotationTypes < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :votation_types do |t|
|
||||
t.integer :questionable_id
|
||||
t.string :questionable_type
|
||||
t.integer :vote_type
|
||||
t.integer :max_votes
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1698,6 +1698,15 @@ ActiveRecord::Schema.define(version: 2022_09_15_154808) do
|
||||
t.index ["user_id"], name: "index_visits_on_user_id"
|
||||
end
|
||||
|
||||
create_table "votation_types", force: :cascade do |t|
|
||||
t.integer "questionable_id"
|
||||
t.string "questionable_type"
|
||||
t.integer "vote_type"
|
||||
t.integer "max_votes"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "votes", id: :serial, force: :cascade do |t|
|
||||
t.string "votable_type"
|
||||
t.integer "votable_id"
|
||||
|
||||
@@ -18,31 +18,33 @@ describe Polls::Questions::AnswersComponent do
|
||||
|
||||
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 a span instead of a button for existing user answers" do
|
||||
it "renders button to destroy current user answers" do
|
||||
user = create(:user, :verified)
|
||||
allow(user).to receive(:current_sign_in_at).and_return(user.created_at)
|
||||
create(:poll_answer, author: user, question: question, answer: "Yes")
|
||||
sign_in(user)
|
||||
|
||||
render_inline Polls::Questions::AnswersComponent.new(question)
|
||||
|
||||
expect(page).to have_selector "span", text: "Yes"
|
||||
expect(page).not_to have_button "Yes"
|
||||
expect(page).to have_button "No"
|
||||
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 "hides current answer and shows buttons in successive sessions" do
|
||||
it "renders disabled buttons when max votes is reached" do
|
||||
user = create(:user, :verified)
|
||||
create(:poll_answer, author: user, question: question, answer: "Yes")
|
||||
allow(user).to receive(:current_sign_in_at).and_return(Time.current)
|
||||
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::AnswersComponent.new(question)
|
||||
|
||||
expect(page).to have_button "Yes"
|
||||
expect(page).to have_button "No"
|
||||
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
|
||||
|
||||
18
spec/components/polls/questions/question_component_spec.rb
Normal file
18
spec/components/polls/questions/question_component_spec.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe Polls::Questions::QuestionComponent do
|
||||
it "renders more information links when any question answer has additional information" do
|
||||
question = create(:poll_question)
|
||||
answer_a = create(:poll_question_answer, question: question, title: "Answer A")
|
||||
answer_b = create(:poll_question_answer, question: question, title: "Answer B")
|
||||
allow_any_instance_of(Poll::Question::Answer).to receive(:with_read_more?).and_return(true)
|
||||
|
||||
render_inline Polls::Questions::QuestionComponent.new(question: question)
|
||||
|
||||
poll_question = page.find("#poll_question_#{question.id}")
|
||||
expect(poll_question).to have_content("Read more about")
|
||||
expect(poll_question).to have_link("Answer A", href: "#answer_#{answer_a.id}")
|
||||
expect(poll_question).to have_link("Answer B", href: "#answer_#{answer_b.id}")
|
||||
expect(poll_question).to have_content("Answer A, Answer B")
|
||||
end
|
||||
end
|
||||
@@ -1,32 +0,0 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe Polls::Questions::ReadMoreAnswerComponent do
|
||||
include Rails.application.routes.url_helpers
|
||||
let(:poll) { create(:poll) }
|
||||
let(:question) { create(:poll_question, poll: poll) }
|
||||
let(:answer) { create(:poll_question_answer, question: question) }
|
||||
|
||||
it "renders answers with videos" do
|
||||
create(:poll_answer_video, answer: answer, title: "Awesome video", url: "youtube.com/watch?v=123")
|
||||
|
||||
render_inline Polls::Questions::ReadMoreAnswerComponent.new(answer: answer)
|
||||
|
||||
expect(page).to have_link("Awesome video", href: "youtube.com/watch?v=123")
|
||||
end
|
||||
|
||||
it "renders answers with images" do
|
||||
create(:image, imageable: answer, title: "The yes movement")
|
||||
|
||||
render_inline Polls::Questions::ReadMoreAnswerComponent.new(answer: answer)
|
||||
|
||||
expect(page).to have_css "img[alt='The yes movement']"
|
||||
end
|
||||
|
||||
it "renders answers with documents" do
|
||||
create(:document, documentable: answer, title: "The yes movement")
|
||||
|
||||
render_inline Polls::Questions::ReadMoreAnswerComponent.new(answer: answer)
|
||||
|
||||
expect(page).to have_link("The yes movement")
|
||||
end
|
||||
end
|
||||
57
spec/components/polls/questions/read_more_component_spec.rb
Normal file
57
spec/components/polls/questions/read_more_component_spec.rb
Normal file
@@ -0,0 +1,57 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe Polls::Questions::ReadMoreComponent do
|
||||
include Rails.application.routes.url_helpers
|
||||
let(:poll) { create(:poll) }
|
||||
let(:question) { create(:poll_question, poll: poll, title: "Question title?") }
|
||||
let(:answer) { create(:poll_question_answer, question: question) }
|
||||
|
||||
it "renders question title" do
|
||||
create(:poll_question_answer, question: question, description: "Question answer description")
|
||||
|
||||
render_inline Polls::Questions::ReadMoreComponent.new(question: question)
|
||||
|
||||
expect(page).to have_content "Question title?"
|
||||
end
|
||||
|
||||
it "renders answers in the given order" do
|
||||
create(:poll_question_answer, title: "Answer A", question: question, given_order: 2)
|
||||
create(:poll_question_answer, title: "Answer B", question: question, given_order: 1)
|
||||
|
||||
render_inline Polls::Questions::ReadMoreComponent.new(question: question)
|
||||
|
||||
expect("Answer B").to appear_before("Answer A")
|
||||
end
|
||||
|
||||
it "does not render when answers does not have more information" do
|
||||
answer.update!(description: nil)
|
||||
|
||||
render_inline Polls::Questions::ReadMoreComponent.new(question: question)
|
||||
|
||||
expect(page).not_to be_rendered
|
||||
end
|
||||
|
||||
it "renders answers with videos" do
|
||||
create(:poll_answer_video, answer: answer, title: "Awesome video", url: "youtube.com/watch?v=123")
|
||||
|
||||
render_inline Polls::Questions::ReadMoreComponent.new(question: question)
|
||||
|
||||
expect(page).to have_link("Awesome video", href: "youtube.com/watch?v=123")
|
||||
end
|
||||
|
||||
it "renders answers with images" do
|
||||
create(:image, imageable: answer, title: "The yes movement")
|
||||
|
||||
render_inline Polls::Questions::ReadMoreComponent.new(question: question)
|
||||
|
||||
expect(page).to have_css "img[alt='The yes movement']"
|
||||
end
|
||||
|
||||
it "renders answers with documents" do
|
||||
create(:document, documentable: answer, title: "The yes movement")
|
||||
|
||||
render_inline Polls::Questions::ReadMoreComponent.new(question: question)
|
||||
|
||||
expect(page).to have_link("The yes movement")
|
||||
end
|
||||
end
|
||||
@@ -58,6 +58,28 @@ FactoryBot.define do
|
||||
create(:poll_question_answer, question: question, title: "No")
|
||||
end
|
||||
end
|
||||
|
||||
trait :abc do
|
||||
after(:create) do |question, evaluator|
|
||||
%w[A B C].each do |letter|
|
||||
create(:poll_question_answer, question: question, title: "Answer #{letter}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
factory :poll_question_unique do
|
||||
after(:create) do |question|
|
||||
create(:votation_type_unique, questionable: question)
|
||||
end
|
||||
end
|
||||
|
||||
factory :poll_question_multiple do
|
||||
transient { max_votes { 3 } }
|
||||
|
||||
after(:create) do |question, evaluator|
|
||||
create(:votation_type_multiple, questionable: question, max_votes: evaluator.max_votes)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
factory :poll_question_answer, class: "Poll::Question::Answer" do
|
||||
|
||||
14
spec/factories/votation_type.rb
Normal file
14
spec/factories/votation_type.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
FactoryBot.define do
|
||||
factory :votation_type do
|
||||
factory :votation_type_unique do
|
||||
vote_type { "unique" }
|
||||
end
|
||||
|
||||
factory :votation_type_multiple do
|
||||
vote_type { "multiple" }
|
||||
max_votes { 3 }
|
||||
end
|
||||
|
||||
association :questionable, factory: :poll_question
|
||||
end
|
||||
end
|
||||
@@ -232,6 +232,18 @@ describe Abilities::Common do
|
||||
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
|
||||
|
||||
context "without geozone" do
|
||||
before { user.geozone = nil }
|
||||
|
||||
|
||||
@@ -23,6 +23,30 @@ describe Poll::Answer do
|
||||
expect(answer).not_to be_valid
|
||||
end
|
||||
|
||||
it "is not valid when user already reached multiple answers question max votes" do
|
||||
author = create(:user)
|
||||
question = create(:poll_question_multiple, :abc, max_votes: 2)
|
||||
create(:poll_answer, author: author, question: question, answer: "Answer A")
|
||||
create(:poll_answer, author: author, question: question, answer: "Answer B")
|
||||
answer = build(:poll_answer, author: author, question: question, answer: "Answer C")
|
||||
|
||||
expect(answer).not_to be_valid
|
||||
end
|
||||
|
||||
it "validates max votes when creating answers at the same time", :race_condition 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 |a|
|
||||
Thread.new { a.save }
|
||||
end.each(&:join)
|
||||
|
||||
expect(Poll::Answer.count).to be 2
|
||||
end
|
||||
|
||||
it "is valid for answers included in the Poll::Question's question_answers list" do
|
||||
question = create(:poll_question)
|
||||
create(:poll_question_answer, title: "One", question: question)
|
||||
@@ -82,4 +106,32 @@ describe Poll::Answer do
|
||||
expect(answer).not_to be_persisted
|
||||
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
|
||||
|
||||
@@ -34,4 +34,30 @@ describe Poll::Question::Answer do
|
||||
expect(Poll::Question::Answer.with_content).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe "#with_read_more?" do
|
||||
it "returns false when the answer does not have description, images, videos nor documents" do
|
||||
poll_question_answer = build(:poll_question_answer, description: nil)
|
||||
|
||||
expect(poll_question_answer.with_read_more?).to be_falsy
|
||||
end
|
||||
|
||||
it "returns true when the answer has description, images, videos or documents" do
|
||||
poll_question_answer = build(:poll_question_answer, description: "Answer description")
|
||||
|
||||
expect(poll_question_answer.with_read_more?).to be_truthy
|
||||
|
||||
poll_question_answer = build(:poll_question_answer, :with_image)
|
||||
|
||||
expect(poll_question_answer.with_read_more?).to be_truthy
|
||||
|
||||
poll_question_answer = build(:poll_question_answer, :with_document)
|
||||
|
||||
expect(poll_question_answer.with_read_more?).to be_truthy
|
||||
|
||||
poll_question_answer = build(:poll_question_answer, :with_video)
|
||||
|
||||
expect(poll_question_answer.with_read_more?).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
35
spec/models/votation_type_spec.rb
Normal file
35
spec/models/votation_type_spec.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe VotationType do
|
||||
let(:vote_types) { %i[votation_type_unique votation_type_multiple] }
|
||||
let(:votation_type) { build(vote_types.sample) }
|
||||
|
||||
it "is valid" do
|
||||
expect(votation_type).to be_valid
|
||||
end
|
||||
|
||||
it "is not valid without questionable" do
|
||||
votation_type.questionable = nil
|
||||
|
||||
expect(votation_type).not_to be_valid
|
||||
end
|
||||
|
||||
it "is not valid when questionable_type is not allowed" do
|
||||
votation_type.questionable_type = Poll::Answer
|
||||
|
||||
expect(votation_type).not_to be_valid
|
||||
expect(votation_type.errors[:questionable_type]).to include "is not included in the list"
|
||||
end
|
||||
|
||||
it "is not valid when max_votes is undefined for multiple votation_type" do
|
||||
votation_type.max_votes = nil
|
||||
votation_type.vote_type = "unique"
|
||||
|
||||
expect(votation_type).to be_valid
|
||||
|
||||
votation_type.vote_type = "multiple"
|
||||
|
||||
expect(votation_type).not_to be_valid
|
||||
expect(votation_type.errors[:max_votes]).to include "can't be blank"
|
||||
end
|
||||
end
|
||||
@@ -5,8 +5,8 @@ module Polls
|
||||
within("#poll_question_#{question.id}_answers") do
|
||||
click_button answer
|
||||
|
||||
expect(page).to have_css("span.answered", text: answer)
|
||||
expect(page).not_to have_button(answer)
|
||||
expect(page).to have_button("You have voted #{answer}")
|
||||
expect(page).not_to have_button("Vote #{answer}")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -65,6 +65,34 @@ describe "Admin poll questions", :admin do
|
||||
|
||||
expect(page).not_to have_link "Create question"
|
||||
end
|
||||
|
||||
describe "With votation type" do
|
||||
before do
|
||||
poll = create(:poll, :future)
|
||||
visit admin_poll_path(poll)
|
||||
click_link "Create question"
|
||||
end
|
||||
|
||||
scenario "Unique" do
|
||||
fill_in "Question", with: "Question with unique answer"
|
||||
select "Unique answer", from: "Votation type"
|
||||
|
||||
click_button "Save"
|
||||
|
||||
expect(page).to have_content "Question with unique answer"
|
||||
expect(page).to have_content "Unique answer"
|
||||
end
|
||||
|
||||
scenario "Multiple" do
|
||||
fill_in "Question", with: "Question with multiple answers"
|
||||
select "Multiple answers", from: "Votation type"
|
||||
fill_in "Maximum number of votes", with: 6
|
||||
click_button "Save"
|
||||
|
||||
expect(page).to have_content "Question with multiple answers"
|
||||
expect(page).to have_content "Multiple answers"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
scenario "Create from proposal" do
|
||||
|
||||
@@ -183,18 +183,6 @@ describe "Polls" do
|
||||
expect("Second question").to appear_before("Third question")
|
||||
end
|
||||
|
||||
scenario "More info answers appear in the given order" do
|
||||
question = create(:poll_question, poll: poll)
|
||||
answer1 = create(:poll_question_answer, title: "First", question: question, given_order: 2)
|
||||
answer2 = create(:poll_question_answer, title: "Second", question: question, given_order: 1)
|
||||
|
||||
visit poll_path(poll)
|
||||
|
||||
within("div.poll-more-info-answers") do
|
||||
expect(answer2.title).to appear_before(answer1.title)
|
||||
end
|
||||
end
|
||||
|
||||
scenario "Buttons to slide through images work back and forth" do
|
||||
question = create(:poll_question, :yes_no, poll: poll)
|
||||
create(:image, imageable: question.question_answers.last, title: "The no movement")
|
||||
@@ -258,10 +246,10 @@ describe "Polls" do
|
||||
visit poll_path(poll)
|
||||
|
||||
within("#poll_question_#{question.id}_answers") do
|
||||
click_button "Yes"
|
||||
click_button "Vote Yes"
|
||||
|
||||
expect(page).not_to have_button "Yes"
|
||||
expect(page).to have_button "No"
|
||||
expect(page).to have_button "You have voted Yes"
|
||||
expect(page).to have_button "Vote No"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -278,51 +266,13 @@ describe "Polls" do
|
||||
within("#poll_question_#{question.id}_answers") do
|
||||
click_button "Yes"
|
||||
|
||||
expect(page).not_to have_button "Yes"
|
||||
expect(page).to have_button "No"
|
||||
expect(page).to have_button "You have voted Yes"
|
||||
expect(page).to have_button "Vote No"
|
||||
|
||||
click_button "No"
|
||||
|
||||
expect(page).not_to have_button "No"
|
||||
expect(page).to have_button "Yes"
|
||||
end
|
||||
end
|
||||
|
||||
scenario "Level 2 votes, signs out, signs in, votes again" do
|
||||
poll.update!(geozone_restricted: true)
|
||||
poll.geozones << geozone
|
||||
|
||||
question = create(:poll_question, :yes_no, poll: poll)
|
||||
user = create(:user, :level_two, geozone: geozone)
|
||||
|
||||
login_as user
|
||||
visit poll_path(poll)
|
||||
|
||||
within("#poll_question_#{question.id}_answers") do
|
||||
click_button "Yes"
|
||||
|
||||
expect(page).not_to have_button "Yes"
|
||||
expect(page).to have_button "No"
|
||||
end
|
||||
|
||||
click_link "Sign out"
|
||||
login_as user
|
||||
visit poll_path(poll)
|
||||
within("#poll_question_#{question.id}_answers") do
|
||||
click_button "Yes"
|
||||
|
||||
expect(page).not_to have_button "Yes"
|
||||
expect(page).to have_button "No"
|
||||
end
|
||||
|
||||
click_link "Sign out"
|
||||
login_as user
|
||||
visit poll_path(poll)
|
||||
within("#poll_question_#{question.id}_answers") do
|
||||
click_button "No"
|
||||
|
||||
expect(page).not_to have_button "No"
|
||||
expect(page).to have_button "Yes"
|
||||
expect(page).to have_button "Vote Yes"
|
||||
expect(page).to have_button "You have voted No"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -359,7 +309,7 @@ describe "Polls" do
|
||||
within("#poll_question_#{question.id}_answers") do
|
||||
click_button "Yes"
|
||||
|
||||
expect(page).not_to have_button "Yes"
|
||||
expect(page).to have_button "You have voted Yes"
|
||||
expect(page).to have_button "No"
|
||||
end
|
||||
end
|
||||
|
||||
69
spec/system/polls/votation_types_spec.rb
Normal file
69
spec/system/polls/votation_types_spec.rb
Normal file
@@ -0,0 +1,69 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe "Poll Votation Type" do
|
||||
let(:author) { create(:user, :level_two) }
|
||||
|
||||
before do
|
||||
login_as(author)
|
||||
end
|
||||
|
||||
scenario "Unique answer" do
|
||||
question = create(:poll_question_unique, :yes_no)
|
||||
|
||||
visit poll_path(question.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 "#poll_question_#{question.id}_answers" 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")
|
||||
end
|
||||
end
|
||||
|
||||
scenario "Multiple answers" do
|
||||
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(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")
|
||||
|
||||
within "#poll_question_#{question.id}_answers" do
|
||||
click_button "Vote Answer A"
|
||||
|
||||
expect(page).to have_button("You have voted Answer A")
|
||||
|
||||
click_button "Vote Answer C"
|
||||
|
||||
expect(page).to have_button("You have voted Answer C")
|
||||
expect(page).to have_button("Vote Answer B", disabled: true)
|
||||
|
||||
click_button "You have voted Answer A"
|
||||
|
||||
expect(page).to have_button("Vote Answer A")
|
||||
expect(page).to have_button("Vote Answer B")
|
||||
|
||||
click_button "You have voted Answer C"
|
||||
|
||||
expect(page).to have_button("Vote Answer C")
|
||||
|
||||
click_button "Vote Answer B"
|
||||
|
||||
expect(page).to have_button("You have voted Answer B")
|
||||
expect(page).to have_button("Vote Answer A")
|
||||
expect(page).to have_button("Vote Answer C")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,12 +3,10 @@ require "rails_helper"
|
||||
describe "Voter" do
|
||||
context "Origin", :with_frozen_time do
|
||||
let(:poll) { create(:poll) }
|
||||
let(:question) { create(:poll_question, poll: poll) }
|
||||
let!(:question) { create(:poll_question, :yes_no, poll: poll) }
|
||||
let(:booth) { create(:poll_booth) }
|
||||
let(:officer) { create(:poll_officer) }
|
||||
let(:admin) { create(:administrator) }
|
||||
let!(:answer_yes) { create(:poll_question_answer, question: question, title: "Yes") }
|
||||
let!(:answer_no) { create(:poll_question_answer, question: question, title: "No") }
|
||||
|
||||
before do
|
||||
create(:geozone, :in_census)
|
||||
@@ -23,12 +21,40 @@ describe "Voter" do
|
||||
visit poll_path(poll)
|
||||
|
||||
within("#poll_question_#{question.id}_answers") do
|
||||
click_button answer_yes.title
|
||||
expect(page).not_to have_button(answer_yes.title)
|
||||
click_button "Vote Yes"
|
||||
|
||||
expect(page).to have_button("You have voted Yes")
|
||||
expect(page).not_to have_button("Vote Yes")
|
||||
end
|
||||
|
||||
expect(Poll::Voter.count).to eq(1)
|
||||
expect(Poll::Voter.first.origin).to eq("web")
|
||||
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")
|
||||
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}_answers") do
|
||||
click_button "You have voted Yes"
|
||||
|
||||
expect(page).to have_button("Vote Yes")
|
||||
expect(page).to have_button("Vote No")
|
||||
end
|
||||
|
||||
visit poll_path(poll)
|
||||
|
||||
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
|
||||
@@ -38,8 +64,8 @@ describe "Voter" do
|
||||
visit poll_path(poll)
|
||||
|
||||
within("#poll_question_#{question.id}_answers") do
|
||||
expect(page).to have_link(answer_yes.title, href: verification_path)
|
||||
expect(page).to have_link(answer_no.title, href: verification_path)
|
||||
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")
|
||||
@@ -118,7 +144,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, answer_yes.title)
|
||||
vote_for_poll_via_web(poll, question, "Yes")
|
||||
expect(Poll::Voter.count).to eq(1)
|
||||
|
||||
click_link "Sign out"
|
||||
@@ -145,7 +171,7 @@ describe "Voter" do
|
||||
visit poll_path(poll)
|
||||
|
||||
within("#poll_question_#{question.id}_answers") do
|
||||
expect(page).not_to have_button(answer_yes.title)
|
||||
expect(page).not_to have_button("Yes")
|
||||
end
|
||||
expect(page).to have_content "You have already participated in a physical booth. You can not participate again."
|
||||
expect(Poll::Voter.count).to eq(1)
|
||||
@@ -163,31 +189,6 @@ describe "Voter" do
|
||||
expect(page).to have_content "1"
|
||||
end
|
||||
end
|
||||
|
||||
scenario "Trying to vote in web again" do
|
||||
login_as user
|
||||
vote_for_poll_via_web(poll, question, answer_yes.title)
|
||||
expect(Poll::Voter.count).to eq(1)
|
||||
|
||||
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("#poll_question_#{question.id}_answers") do
|
||||
expect(page).not_to have_button(answer_yes.title)
|
||||
end
|
||||
|
||||
unfreeze_time
|
||||
|
||||
click_link "Sign out"
|
||||
|
||||
login_as user
|
||||
visit poll_path(poll)
|
||||
|
||||
within("#poll_question_#{question.id}_answers") do
|
||||
expect(page).to have_button(answer_yes.title)
|
||||
expect(page).to have_button(answer_no.title)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
scenario "Voting in poll and then verifiying account" do
|
||||
@@ -209,7 +210,7 @@ describe "Voter" do
|
||||
visit poll_path(poll)
|
||||
|
||||
within("#poll_question_#{question.id}_answers") do
|
||||
expect(page).not_to have_button(answer_yes.title)
|
||||
expect(page).not_to have_button("Yes")
|
||||
end
|
||||
|
||||
expect(page).to have_content "You have already participated in a physical booth. You can not participate again."
|
||||
|
||||
Reference in New Issue
Block a user