Merge pull request #6061 from consuldemocracy/poll_text_answers

Add support for essay poll questions
This commit is contained in:
Sebastia
2025-10-16 15:30:22 +02:00
committed by GitHub
43 changed files with 856 additions and 293 deletions

View File

@@ -2,16 +2,18 @@
"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 {
var select_field = $(this);
$("[data-vote-type]").hide(0, function() {
$("[data-vote-type=" + select_field.val() + "]").show();
});
if (select_field.val() === "multiple") {
$(".max-votes").show();
$(".description-unique").hide();
$(".description-multiple").show();
$(".votation-type-max-votes").prop("disabled", false);
} else {
$(".max-votes").hide();
$(".votation-type-max-votes").prop("disabled", true);
}
},
initialize: function() {

View File

@@ -1,33 +1,42 @@
.poll-form {
fieldset {
fieldset,
.poll-question-open-ended {
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;
margin-top: calc($line-height / 2);
}
}
fieldset {
legend {
@include header-font-size(h3);
float: $global-left;
margin-bottom: 0;
label {
@include radio-or-checkbox-and-label-alignment;
font-weight: normal;
+ * {
clear: $global-left;
}
}
&:first-of-type::before {
content: "\A";
margin-top: calc($line-height / 2);
white-space: pre;
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;
}
}
}
.poll-question-open-ended {
label {
@include header-font-size(h3);
line-height: 1.5;
}
}

View File

@@ -4,12 +4,9 @@
<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>
<% descriptions.each do |vote_type, text| %>
<%= description_tag(vote_type, text) %>
<% end %>
</div>
</div>

View File

@@ -4,4 +4,18 @@ class Admin::VotationTypes::FieldsComponent < ApplicationComponent
def initialize(form:)
@form = form
end
private
def descriptions
{
unique: t("admin.polls.votation_type.unique_description"),
multiple: t("admin.polls.votation_type.multiple_description"),
open: t("admin.polls.votation_type.open_description")
}
end
def description_tag(vote_type, text)
tag.span(text, data: { vote_type: vote_type })
end
end

View File

@@ -8,7 +8,7 @@
</div>
</div>
<% poll.questions.each do |question| %>
<% poll.questions.for_physical_votes.each do |question| %>
<fieldset class="row">
<legend class="column"><%= question.title %></legend>
<% question.question_options.each_with_index do |option, i| %>

View File

@@ -29,7 +29,7 @@
</tbody>
</table>
<% @poll.questions.each do |question| %>
<% @poll.questions.for_physical_votes.each do |question| %>
<%= render Admin::Poll::Results::QuestionComponent.new(question, @partial_results) %>
<% end %>
</div>

View File

@@ -8,13 +8,9 @@
</div>
<div class="poll-info">
<% if poll.questions.one? %>
<h4><%= link_to_poll poll.questions.first.title, poll %></h4>
<div class="dates"><%= dates %></div>
<% else %>
<h4><%= link_to_poll poll.name, poll %></h4>
<div class="dates"><%= dates %></div>
<h4><%= link_to header_text, path %></h4>
<div class="dates"><%= dates %></div>
<% if poll.questions.many? %>
<ul class="margin-top">
<% poll.questions.sort_for_list.each do |question| %>
<li><%= question.title %></li>
@@ -25,9 +21,5 @@
<%= render SDG::TagListComponent.new(poll, limit: 5, linkable: false) %>
</div>
<% if poll.expired? %>
<%= link_to_poll t("polls.index.participate_button_expired"), poll, class: "button hollow expanded" %>
<% else %>
<%= link_to_poll t("polls.index.participate_button"), poll, class: "button hollow expanded" %>
<% end %>
<%= link_to link_text, path, class: "button hollow expanded" %>
</div>

View File

@@ -12,13 +12,29 @@ class Polls::PollComponent < ApplicationComponent
t("polls.dates", open_at: l(poll.starts_at.to_date), closed_at: l(poll.ends_at.to_date))
end
def link_to_poll(text, poll, options = {})
if can?(:results, poll)
link_to text, results_poll_path(id: poll.slug || poll.id), options
elsif can?(:stats, poll)
link_to text, stats_poll_path(id: poll.slug || poll.id), options
def header_text
if poll.questions.one?
poll.questions.first.title
else
link_to text, poll_path(id: poll.slug || poll.id), options
poll.name
end
end
def link_text
if poll.expired?
t("polls.index.participate_button_expired")
else
t("polls.index.participate_button")
end
end
def path
if can?(:results, poll)
results_poll_path(id: poll.slug || poll.id)
elsif can?(:stats, poll)
stats_poll_path(id: poll.slug || poll.id)
else
poll_path(id: poll.slug || poll.id)
end
end
end

View File

@@ -1,23 +1,31 @@
<fieldset <%= fieldset_attributes %>>
<legend><%= question.title %></legend>
<% if multiple_choice? %>
<%= multiple_choice_help_text %>
<% question.question_options.each do |option| %>
<%= multiple_choice_field(option) %>
<% if question.open? %>
<div class="poll-question-open-ended">
<%= fields_for "web_vote[#{question.id}]" do |f| %>
<%= f.text_area :answer, label: question.title, value: existing_answer, rows: 3 %>
<% end %>
<% else %>
<% question.question_options.each do |option| %>
<%= single_choice_field(option) %>
<% end %>
<% end %>
</div>
<% else %>
<fieldset <%= fieldset_attributes %>>
<legend><%= question.title %></legend>
<% if question.options_with_read_more? %>
<div class="read-more-links">
<p><%= t("poll_questions.read_more_about") %></p>
<p><%= options_read_more_links %></p>
</div>
<% end %>
<%= form.error_for(:"question_#{question.id}") %>
</fieldset>
<% 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 %>
<% if question.options_with_read_more? %>
<div class="read-more-links">
<p><%= t("poll_questions.read_more_about") %></p>
<p><%= options_read_more_links %></p>
</div>
<% end %>
<%= form.error_for(:"question_#{question.id}") %>
</fieldset>
<% end %>

View File

@@ -33,6 +33,10 @@ class Polls::Questions::QuestionComponent < ApplicationComponent
end, ", ")
end
def existing_answer
form.object.answers[question.id]&.first&.answer
end
def multiple_choice?
question.multiple?
end

View File

@@ -1,25 +1,46 @@
<h3 id="<%= question.title.parameterize %>"><%= question.title %></h3>
<table id="question_<%= question.id %>_results_table">
<thead>
<tr>
<%- question.question_options.each do |option| %>
<th scope="col" class="<%= option_styles(option) %>">
<% if most_voted_option?(option) %>
<span class="show-for-sr"><%= t("polls.show.results.most_voted_answer") %></span>
<% end %>
<%= option.title %>
</th>
<% end %>
</tr>
</thead>
<tbody>
<tr>
<%- question.question_options.each do |option| %>
<td id="option_<%= option.id %>_result" class="<%= option_styles(option) %>">
<%= option.total_votes %>
(<%= option.total_votes_percentage.round(2) %>%)
<% if question.accepts_options? %>
<thead>
<tr>
<%- question.question_options.each do |option| %>
<th scope="col" class="<%= option_styles(option) %>">
<% if most_voted_option?(option) %>
<span class="show-for-sr"><%= t("polls.show.results.most_voted_answer") %></span>
<% end %>
<%= option.title %>
</th>
<% end %>
</tr>
</thead>
<tbody>
<tr>
<%- question.question_options.each do |option| %>
<td class="<%= option_styles(option) %>">
<%= option.total_votes %>
(<%= option.total_votes_percentage.round(2) %>%)
</td>
<% end %>
</tr>
</tbody>
<% else %>
<thead>
<tr>
<th scope="col"><%= t("polls.show.results.open_ended.valid") %></th>
<th scope="col"><%= t("polls.show.results.open_ended.blank") %></th>
</tr>
</thead>
<tbody>
<tr>
<td>
<%= question.open_ended_valid_answers_count %>
(<%= question.open_ended_valid_percentage.round(2) %>%)
</td>
<% end %>
</tr>
</tbody>
<td>
<%= question.open_ended_blank_answers_count %>
(<%= question.open_ended_blank_percentage.round(2) %>%)
</td>
</tr>
</tbody>
<% end %>
</table>

View File

@@ -1,7 +1,7 @@
class Polls::Results::QuestionComponent < ApplicationComponent
attr_reader :question
def initialize(question:)
def initialize(question)
@question = question
end

View File

@@ -3,7 +3,7 @@
<div class="polls-results">
<%= render Polls::PollHeaderComponent.new(poll) %>
<%= render "poll_subnav" %>
<%= render "polls/poll_subnav" %>
<div class="polls-results-content">
<div>
@@ -16,7 +16,9 @@
</div>
<div>
<%= render Polls::Results::QuestionComponent.with_collection(poll.questions) %>
<% poll.questions.each do |question| %>
<%= render Polls::Results::QuestionComponent.new(question) %>
<% end %>
</div>
</div>
</div>

View File

@@ -101,7 +101,7 @@ module Abilities
end
can [:read, :order_options], Poll::Question::Option
can [:create, :update, :destroy], Poll::Question::Option do |option|
can?(:update, option.question)
can?(:update, option.question) && option.question.accepts_options?
end
can :read, Poll::Question::Option::Video
can [:create, :update, :destroy], Poll::Question::Option::Video do |video|

View File

@@ -9,21 +9,9 @@ class Poll::Answer < ApplicationRecord
validates :author, presence: true
validates :answer, presence: true
validates :option, uniqueness: { scope: :author_id }, allow_nil: true
validate :max_votes
validates :answer, inclusion: { in: ->(poll_answer) { poll_answer.option.possible_answers }},
if: ->(poll_answer) { poll_answer.option.present? }
scope :by_author, ->(author_id) { where(author_id: author_id) }
scope :by_question, ->(question_id) { where(question_id: question_id) }
private
def max_votes
return if !question || !author || persisted?
if question.answers.by_author(author).count >= question.max_votes
errors.add(:answer, "Maximum number of votes per user exceeded")
end
end
end

View File

@@ -27,10 +27,11 @@ class Poll::Question < ApplicationRecord
accepts_nested_attributes_for :question_options, reject_if: :all_blank, allow_destroy: true
accepts_nested_attributes_for :votation_type
delegate :multiple?, :vote_type, to: :votation_type, allow_nil: true
delegate :multiple?, :open?, :vote_type, to: :votation_type, allow_nil: true
scope :sort_for_list, -> { order(Arel.sql("poll_questions.proposal_id IS NULL"), :created_at) }
scope :for_render, -> { includes(:author, :proposal) }
scope :for_physical_votes, -> { left_joins(:votation_type).merge(VotationType.accepts_options) }
def copy_attributes_from_proposal(proposal)
if proposal.present?
@@ -61,6 +62,10 @@ class Poll::Question < ApplicationRecord
votation_type.nil? || votation_type.unique?
end
def accepts_options?
votation_type.nil? || votation_type.accepts_options?
end
def max_votes
if multiple?
votation_type.max_votes
@@ -69,23 +74,51 @@ class Poll::Question < ApplicationRecord
end
end
def find_or_initialize_user_answer(user, option_id)
option = question_options.find(option_id)
def find_or_initialize_user_answer(user, option_id: nil, answer_text: nil)
answer = answers.find_or_initialize_by(find_by_attributes(user, option_id))
if accepts_options?
option = question_options.find(option_id)
answer.option = option
answer.answer = option.title
else
answer.answer = answer_text
end
answer = answers.find_or_initialize_by(find_by_attributes(user, option))
answer.option = option
answer.answer = option.title
answer
end
def open_ended_valid_answers_count
answers.count
end
def open_ended_blank_answers_count
poll.voters.count - open_ended_valid_answers_count
end
def open_ended_valid_percentage
return 0.0 if open_ended_total_answers.zero?
(open_ended_valid_answers_count * 100.0) / open_ended_total_answers
end
def open_ended_blank_percentage
return 0.0 if open_ended_total_answers.zero?
(open_ended_blank_answers_count * 100.0) / open_ended_total_answers
end
private
def find_by_attributes(user, option)
case vote_type
when "unique", nil
def find_by_attributes(user, option_id)
if multiple?
{ author: user, option_id: option_id }
else
{ author: user }
when "multiple"
{ author: user, answer: option.title }
end
end
def open_ended_total_answers
open_ended_valid_answers_count + open_ended_blank_answers_count
end
end

View File

@@ -63,8 +63,17 @@ class Poll::WebVote
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)
if question.open?
answer_text = question_params[:answer].to_s.strip
if answer_text.present?
[question.find_or_initialize_user_answer(user, answer_text: answer_text)]
else
[]
end
else
Array(question_params[:option_id]).map do |option_id|
question.find_or_initialize_user_answer(user, option_id: option_id)
end
end
end

View File

@@ -1,17 +1,31 @@
class VotationType < ApplicationRecord
belongs_to :questionable, polymorphic: true
validate :cannot_be_open_ended_if_question_has_options
QUESTIONABLE_TYPES = %w[Poll::Question].freeze
enum :vote_type, { unique: 0, multiple: 1 }
enum :vote_type, { unique: 0, multiple: 1, open: 2 }
validates :questionable, presence: true
validates :questionable_type, inclusion: { in: ->(*) { QUESTIONABLE_TYPES }}
validates :max_votes, presence: true, if: :max_votes_required?
scope :accepts_options, -> { where.not(vote_type: "open") }
def accepts_options?
!open?
end
private
def max_votes_required?
multiple?
end
def cannot_be_open_ended_if_question_has_options
if questionable&.question_options&.any? && !accepts_options?
errors.add(:vote_type, :cannot_change_to_open_ended)
end
end
end

View File

@@ -35,7 +35,7 @@
<br>
<%= VotationType.human_attribute_name("vote_type.#{@question.vote_type}") %>
</p>
<% if @question.max_votes.present? %>
<% if @question.multiple? %>
<p>
<strong><%= VotationType.human_attribute_name("max_votes") %></strong>
<br>
@@ -46,57 +46,59 @@
</div>
</div>
<div class="clear">
<% if can?(:create, Poll::Question::Option.new(question: @question)) %>
<%= link_to t("admin.questions.show.add_answer"), new_admin_question_option_path(@question),
class: "button float-right" %>
<% else %>
<div class="callout warning">
<strong><%= t("admin.questions.no_edit") %></strong>
</div>
<% end %>
</div>
<table class="margin-top">
<caption><%= t("admin.questions.show.valid_answers") %></caption>
<thead>
<tr>
<th><%= t("admin.questions.show.answers.title") %></th>
<th scope="col" class="medium-7"><%= t("admin.questions.show.answers.description") %></th>
<th scope="col" class="text-center"><%= t("admin.questions.show.answers.images") %></th>
<th scope="col" class="text-center"><%= t("admin.questions.show.answers.documents") %></th>
<th scope="col" class="text-center"><%= t("admin.questions.show.answers.videos") %></th>
<th><%= t("admin.actions.actions") %></th>
</tr>
</thead>
<tbody class="sortable" data-js-url="<%= admin_question_options_order_options_path(@question.id) %>">
<% @question.question_options.each do |option| %>
<tr id="<%= dom_id(option) %>" class="poll_question_option" data-option-id="<%= option.id %>">
<td class="align-top"><%= option.title %></td>
<td class="align-top break"><%= wysiwyg(option.description) %></td>
<td class="align-top text-center">
(<%= option.images.count %>)
<br>
<%= link_to t("admin.questions.show.answers.images_list"),
admin_option_images_path(option) %>
</td>
<td class="align-top text-center">
(<%= option.documents.count rescue 0 %>)
<br>
<%= link_to t("admin.questions.show.answers.documents_list"),
admin_option_documents_path(option) %>
</td>
<td class="align-top text-center">
(<%= option.videos.count %>)
<br>
<%= link_to t("admin.questions.show.answers.video_list"),
admin_option_videos_path(option) %>
</td>
<td>
<%= render Admin::Poll::Questions::Options::TableActionsComponent.new(option) %>
</td>
</tr>
<% if @question.accepts_options? %>
<div class="clear">
<% if can?(:create, Poll::Question::Option.new(question: @question)) %>
<%= link_to t("admin.questions.show.add_answer"), new_admin_question_option_path(@question),
class: "button float-right" %>
<% else %>
<div class="callout warning">
<strong><%= t("admin.questions.no_edit") %></strong>
</div>
<% end %>
</tbody>
</table>
</div>
<table class="margin-top">
<caption><%= t("admin.questions.show.valid_answers") %></caption>
<thead>
<tr>
<th><%= t("admin.questions.show.answers.title") %></th>
<th scope="col" class="medium-7"><%= t("admin.questions.show.answers.description") %></th>
<th scope="col" class="text-center"><%= t("admin.questions.show.answers.images") %></th>
<th scope="col" class="text-center"><%= t("admin.questions.show.answers.documents") %></th>
<th scope="col" class="text-center"><%= t("admin.questions.show.answers.videos") %></th>
<th><%= t("admin.actions.actions") %></th>
</tr>
</thead>
<tbody class="sortable" data-js-url="<%= admin_question_options_order_options_path(@question.id) %>">
<% @question.question_options.each do |option| %>
<tr id="<%= dom_id(option) %>" class="poll_question_option" data-option-id="<%= option.id %>">
<td class="align-top"><%= option.title %></td>
<td class="align-top break"><%= wysiwyg(option.description) %></td>
<td class="align-top text-center">
(<%= option.images.count %>)
<br>
<%= link_to t("admin.questions.show.answers.images_list"),
admin_option_images_path(option) %>
</td>
<td class="align-top text-center">
(<%= option.documents.count rescue 0 %>)
<br>
<%= link_to t("admin.questions.show.answers.documents_list"),
admin_option_documents_path(option) %>
</td>
<td class="align-top text-center">
(<%= option.videos.count %>)
<br>
<%= link_to t("admin.questions.show.answers.video_list"),
admin_option_videos_path(option) %>
</td>
<td>
<%= render Admin::Poll::Questions::Options::TableActionsComponent.new(option) %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% end %>

View File

@@ -534,6 +534,7 @@ en:
max_votes: Maximum number of votes
vote_type: Votation type
votation_type/vote_type:
open: Open-ended
unique: Unique answer
multiple: Multiple answers
cookies/vendor:
@@ -628,6 +629,10 @@ en:
attributes:
code:
invalid: "must start with the same code as its target followed by a dot and end with a number"
votation_type:
attributes:
vote_type:
cannot_change_to_open_ended: "can't change to open-ended type because you've already defined possible valid answers for this question"
messages:
translations_too_short: Is mandatory to provide one translation at least
record_invalid: "Validation failed: %{errors}"

View File

@@ -1131,6 +1131,7 @@ en:
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."
open_description: "Open-ended question that allows users to provide a single answer in their own words."
questions:
index:
create: "Create question"

View File

@@ -639,6 +639,9 @@ en:
results:
title: "Questions"
most_voted_answer: "Most voted answer: "
open_ended:
valid: Valid
blank: Blank
poll_header:
back_to_proposal: Back to proposal
poll_questions:

View File

@@ -534,6 +534,7 @@ es:
max_votes: Número máximo de votos
vote_type: Tipo de votación
votation_type/vote_type:
open: Respuesta abierta
unique: Respuesta única
multiple: Respuesta múltiple
cookies/vendor:
@@ -628,6 +629,10 @@ es:
attributes:
code:
invalid: "debe empezar con el código de su meta seguido de un punto y terminar con un número"
votation_type:
attributes:
vote_type:
cannot_change_to_open_ended: "no se puede cambiar a respuesta abierta el tipo de votación porque ya has definido posibles respuestas válidas para esta pregunta"
messages:
translations_too_short: El obligatorio proporcionar una traducción como mínimo
record_invalid: "Error de validación: %{errors}"

View File

@@ -1131,6 +1131,7 @@ es:
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."
open_description: "Pregunta abierta que permite al usuario dar una única respuesta con sus propias palabras."
questions:
index:
create: "Crear pregunta ciudadana"

View File

@@ -639,6 +639,9 @@ es:
results:
title: "Preguntas"
most_voted_answer: "Respuesta más votada: "
open_ended:
valid: Válidos
blank: En blanco
poll_header:
back_to_proposal: Volver a la propuesta
poll_questions:

View File

@@ -52,6 +52,7 @@ end
section "Creating Poll Questions & Options" do
Poll.find_each do |poll|
(3..5).to_a.sample.times do
vote_type = VotationType.vote_types.keys.sample
question_title = Faker::Lorem.sentence(word_count: 3).truncate(60) + "?"
question = Poll::Question.new(author: User.sample,
title: question_title,
@@ -62,33 +63,29 @@ section "Creating Poll Questions & Options" do
end
end
question.save!
Faker::Lorem.words(number: (2..4).to_a.sample).each_with_index do |title, index|
description = "<p>#{Faker::Lorem.paragraphs.join("</p><p>")}</p>"
option = Poll::Question::Option.new(question: question,
title: title.capitalize,
description: description,
given_order: index + 1)
Setting.enabled_locales.map do |locale|
Globalize.with_locale(locale) do
option.title = "#{title} (#{locale})"
option.description = "#{description} (#{locale})"
question.create_votation_type!(vote_type: vote_type, max_votes: (3 if vote_type == "multiple"))
if question.accepts_options?
Faker::Lorem.words(number: (2..4).to_a.sample).each_with_index do |title, index|
description = "<p>#{Faker::Lorem.paragraphs.join("</p><p>")}</p>"
option = Poll::Question::Option.new(question: question,
title: title.capitalize,
description: description,
given_order: index + 1)
Setting.enabled_locales.map do |locale|
Globalize.with_locale(locale) do
option.title = "#{title} (#{locale})"
option.description = "#{description} (#{locale})"
end
end
option.save!
end
option.save!
end
end
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}",
@@ -158,9 +155,16 @@ section "Creating Poll Voters" do
poll.questions.each do |question|
next unless [true, false].sample
Poll::Answer.create!(question_id: question.id,
author: user,
answer: question.question_options.sample.title)
if question.accepts_options?
option = question.question_options.sample
Poll::Answer.create!(question_id: question.id,
author: user,
option: option,
answer: option.title)
else
text = Faker::Lorem.sentence(word_count: (6..14).to_a.sample)
Poll::Answer.create!(question_id: question.id, author: user, answer: text)
end
end
end
@@ -252,6 +256,7 @@ section "Creating Poll Questions from Proposals" do
end
option.save!
end
question.create_votation_type!(vote_type: "unique")
end
end

View File

@@ -16,4 +16,13 @@ describe Officing::Results::FormComponent do
expect(page).to have_field "Invalid ballots", with: 0, type: :number
expect(page).to have_field "Valid ballots", with: 0, type: :number
end
it "does not render open-ended questions" do
create(:poll_question_open, poll: poll, title: "What do you want?")
render_inline Officing::Results::FormComponent.new(poll, Poll::OfficerAssignment.none)
expect(page).not_to have_content "What do you want?"
expect(page).to have_css "fieldset", text: "Agreed?"
end
end

View File

@@ -69,5 +69,26 @@ describe Polls::Questions::QuestionComponent do
expect(page).to have_field "Yes", type: :radio, checked: true
expect(page).to have_field "No", type: :radio, checked: false
end
context "Open-ended question" do
let(:question) { create(:poll_question_open, poll: poll, title: "What do you want?") }
before { create(:poll_answer, author: user, question: question, answer: "I don't know") }
it "renders text area with persisted answer" do
render_inline Polls::Questions::QuestionComponent.new(question, form: form)
expect(page).to have_field "What do you want?", type: :textarea, with: "I don't know"
end
it "renders unsaved form text over the persisted value" do
web_vote.answers[question.id] = [
build(:poll_answer, question: question, author: user, answer: "Typed (unsaved)")
]
render_inline Polls::Questions::QuestionComponent.new(question, form: form)
expect(page).to have_field "What do you want?", type: :textarea, with: "Typed (unsaved)"
end
end
end
end

View File

@@ -0,0 +1,47 @@
require "rails_helper"
describe Polls::Results::QuestionComponent do
context "question that accepts options" do
let(:question) { create(:poll_question, :yes_no) }
let(:option_yes) { question.question_options.find_by(title: "Yes") }
let(:option_no) { question.question_options.find_by(title: "No") }
it "renders results table content" do
create(:poll_answer, question: question, option: option_yes)
create(:poll_answer, question: question, option: option_no)
render_inline Polls::Results::QuestionComponent.new(question)
expect(page).to have_table with_rows: [{ "Most voted answer: Yes" => "1 (50.0%)",
"No" => "1 (50.0%)" }]
page.find("table") do |table|
expect(table).to have_css "th.win", count: 1
expect(table).to have_css "td.win", count: 1
end
end
end
context "question that does not accept options" do
let(:open_ended_question) { create(:poll_question_open) }
it "renders open_ended headers and empty counts when there are no participants" do
render_inline Polls::Results::QuestionComponent.new(open_ended_question)
expect(page).to have_table with_rows: [{ "Valid" => "0 (0.0%)",
"Blank" => "0 (0.0%)" }]
end
it "renders counts and percentages provided by the model metrics" do
allow(open_ended_question).to receive_messages(
open_ended_valid_answers_count: 3,
open_ended_blank_answers_count: 1
)
render_inline Polls::Results::QuestionComponent.new(open_ended_question)
expect(page).to have_table with_rows: [{ "Valid" => "3 (75.0%)",
"Blank" => "1 (25.0%)" }]
end
end
end

View File

@@ -0,0 +1,36 @@
require "rails_helper"
describe Polls::ResultsComponent do
let(:poll) { create(:poll) }
let(:question_1) { create(:poll_question, :yes_no, poll: poll, title: "Do you like Consul Democracy?") }
let(:option_yes) { question_1.question_options.find_by(title: "Yes") }
let(:option_no) { question_1.question_options.find_by(title: "No") }
let(:question_2) { create(:poll_question, :abc, poll: poll, title: "Which option do you prefer?") }
let(:option_a) { question_2.question_options.find_by(title: "Answer A") }
let(:option_b) { question_2.question_options.find_by(title: "Answer B") }
let(:option_c) { question_2.question_options.find_by(title: "Answer C") }
it "renders results content" do
create_list(:poll_answer, 2, question: question_1, option: option_yes)
create(:poll_answer, question: question_1, option: option_no)
create(:poll_answer, question: question_2, option: option_a)
create(:poll_answer, question: question_2, option: option_b)
create(:poll_answer, question: question_2, option: option_c)
render_inline Polls::ResultsComponent.new(poll)
expect(page).to have_content "Do you like Consul Democracy?"
expect(page).to have_table "question_#{question_1.id}_results_table",
with_rows: [{ "Most voted answer: Yes" => "2 (66.67%)",
"No" => "1 (33.33%)" }]
expect(page).to have_content "Which option do you prefer?"
expect(page).to have_table "question_#{question_2.id}_results_table",
with_rows: [{ "Most voted answer: Answer A" => "1 (33.33%)",
"Answer B" => "1 (33.33%)",
"Answer C" => "1 (33.33%)" }]
end
end

View File

@@ -60,6 +60,7 @@ FactoryBot.define do
trait :yes_no do
after(:create) do |question|
create(:votation_type_unique, questionable: question)
create(:poll_question_option, question: question, title: "Yes")
create(:poll_question_option, question: question, title: "No")
end
@@ -94,6 +95,12 @@ FactoryBot.define do
create(:votation_type_multiple, questionable: question, max_votes: evaluator.max_votes)
end
end
factory :poll_question_open do
after(:create) do |question|
create(:votation_type_open, questionable: question)
end
end
end
factory :poll_question_option, class: "Poll::Question::Option" do

View File

@@ -9,6 +9,10 @@ FactoryBot.define do
max_votes { 3 }
end
factory :votation_type_open do
vote_type { "open" }
end
questionable factory: :poll_question
end
end

View File

@@ -20,6 +20,8 @@ describe Abilities::Administrator do
let(:future_poll) { create(:poll, :future) }
let(:current_poll_question) { create(:poll_question) }
let(:future_poll_question) { create(:poll_question, poll: future_poll) }
let(:future_poll_question_open) { create(:poll_question_open, poll: future_poll) }
let(:future_poll_question_option_open) { future_poll_question_open.question_options.new }
let(:current_poll_question_option) { create(:poll_question_option) }
let(:future_poll_question_option) { create(:poll_question_option, poll: future_poll) }
let(:current_poll_option_video) { create(:poll_option_video, option: current_poll_question_option) }
@@ -143,6 +145,9 @@ describe Abilities::Administrator do
it { should_not be_able_to(:create, current_poll_question_option) }
it { should_not be_able_to(:update, current_poll_question_option) }
it { should_not be_able_to(:destroy, current_poll_question_option) }
it { should_not be_able_to(:create, future_poll_question_option_open) }
it { should_not be_able_to(:update, future_poll_question_option_open) }
it { should_not be_able_to(:destroy, future_poll_question_option_open) }
it { should be_able_to(:create, future_poll_option_video) }
it { should be_able_to(:update, future_poll_option_video) }

View File

@@ -30,23 +30,9 @@ describe Poll::Answer do
expect(answer).not_to be_valid
end
it "is not valid if there's already an answer to that question" do
author = create(:user)
question = create(:poll_question, :yes_no)
create(:poll_answer, author: author, question: question)
answer = build(:poll_answer, author: author, question: question)
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")
it "is not valid without an answer when question is open-ended" do
answer.question = create(:poll_question_open)
answer.answer = nil
expect(answer).not_to be_valid
end

View File

@@ -86,4 +86,204 @@ RSpec.describe Poll::Question do
expect(question.options_total_votes).to eq 3
end
end
describe "#find_or_initialize_user_answer" do
let(:user) { create(:user) }
let(:other_user) { create(:user) }
context "unique question" do
let(:question) { create(:poll_question_unique, :abc) }
let(:answer_a) { question.question_options.find_by(title: "Answer A") }
let(:answer_b) { question.question_options.find_by(title: "Answer B") }
it "finds the existing answer for the same user" do
existing_answer = create(:poll_answer, question: question, author: user, option: answer_a)
create(:poll_answer, question: question, author: other_user, option: answer_b)
answer = question.find_or_initialize_user_answer(user, option_id: answer_b.id)
expect(answer).to eq existing_answer
expect(answer.author).to eq user
expect(answer.option).to eq answer_b
expect(answer.answer).to eq "Answer B"
end
it "initializes a new answer when only another user has answered" do
create(:poll_answer, question: question, author: other_user, option: answer_a)
answer = question.find_or_initialize_user_answer(user, option_id: answer_a.id)
expect(answer).to be_new_record
expect(answer.author).to eq user
expect(answer.option).to eq answer_a
expect(answer.answer).to eq "Answer A"
end
it "raises when option_id is invalid" do
expect do
question.find_or_initialize_user_answer(user, option_id: 999999)
end.to raise_error(ActiveRecord::RecordNotFound)
end
it "raises when option_id is nil" do
expect do
question.find_or_initialize_user_answer(user, answer_text: "ignored")
end.to raise_error(ActiveRecord::RecordNotFound)
end
end
context "multiple question" do
let(:question) { create(:poll_question_multiple, :abc, max_votes: 3) }
let(:answer_a) { question.question_options.find_by(title: "Answer A") }
let(:answer_b) { question.question_options.find_by(title: "Answer B") }
it "finds the existing answer for the same user and option" do
existing_answer = create(:poll_answer, question: question, author: user, option: answer_a)
create(:poll_answer, question: question, author: other_user, option: answer_a)
answer = question.find_or_initialize_user_answer(user, option_id: answer_a.id)
expect(answer).to eq existing_answer
expect(answer.author).to eq user
expect(answer.option).to eq answer_a
expect(answer.answer).to eq "Answer A"
end
it "initializes a new answer when selecting a different option" do
create(:poll_answer, question: question, author: user, option: answer_a)
create(:poll_answer, question: question, author: other_user, option: answer_b)
answer = question.find_or_initialize_user_answer(user, option_id: answer_b.id)
expect(answer).to be_new_record
expect(answer.author).to eq user
expect(answer.option).to eq answer_b
expect(answer.answer).to eq "Answer B"
end
end
context "Open-ended question" do
let(:question) { create(:poll_question_open) }
it "ignores invalid option_id and uses answer_text" do
answer = question.find_or_initialize_user_answer(user, option_id: 999999, answer_text: "Hi")
expect(answer.option).to be nil
expect(answer.answer).to eq "Hi"
end
it "ignores option_id when nil and assigns answer with option set to nil" do
answer = question.find_or_initialize_user_answer(user, answer_text: "Hi")
expect(answer.option).to be nil
expect(answer.answer).to eq "Hi"
end
it "reuses the existing poll answer for the user and updates answer" do
existing = create(:poll_answer, question: question, author: user, answer: "Before")
answer = question.find_or_initialize_user_answer(user, answer_text: "After")
expect(answer).to eq existing
expect(answer.author).to eq user
expect(answer.answer).to eq "After"
end
end
end
describe "scopes" do
describe ".for_physical_votes" do
it "returns unique and multiple, but not open_ended" do
question_unique = create(:poll_question_unique)
question_multiple = create(:poll_question_multiple)
question_open_ended = create(:poll_question_open)
result = Poll::Question.for_physical_votes
expect(result).to match_array [question_unique, question_multiple]
expect(result).not_to include question_open_ended
end
end
end
context "open-ended results" do
let(:poll) { create(:poll) }
let!(:question_open) { create(:poll_question_open, poll: poll) }
it "includes voters who didn't answer any questions in blank answers count" do
create(:poll_voter, poll: poll)
expect(question_open.open_ended_blank_answers_count).to eq 1
expect(question_open.open_ended_valid_answers_count).to eq 0
end
describe "#open_ended_valid_answers_count" do
it "returns 0 when there are no answers" do
expect(question_open.open_ended_valid_answers_count).to eq 0
end
it "counts answers" do
create(:poll_answer, question: question_open, answer: "Hello")
create(:poll_answer, question: question_open, answer: "Bye")
expect(question_open.open_ended_valid_answers_count).to eq 2
end
end
describe "#open_ended_blank_answers_count" do
let(:another_question) { create(:poll_question, :yes_no, poll: poll) }
let(:option_yes) { another_question.question_options.find_by(title: "Yes") }
let(:option_no) { another_question.question_options.find_by(title: "No") }
it "counts valid participants of the poll who did not answer the open-ended question" do
voters = create_list(:poll_voter, 3, poll: poll)
voters.each do |voter|
create(:poll_answer, question: another_question, author: voter.user, option: option_yes)
end
create(:poll_answer, question: question_open, author: voters.sample.user, answer: "Free text")
expect(question_open.open_ended_valid_answers_count).to eq 1
expect(question_open.open_ended_blank_answers_count).to eq 2
end
it "returns 0 when there are no valid participants in the poll" do
expect(question_open.open_ended_blank_answers_count).to eq 0
end
it "counts every user one time even if they answered many questions" do
multiple_question = create(:poll_question_multiple, :abc, poll: poll)
option_a = multiple_question.question_options.find_by(title: "Answer A")
option_b = multiple_question.question_options.find_by(title: "Answer B")
another_question_open = create(:poll_question_open, poll: poll)
voter = create(:poll_voter, poll: poll)
create(:poll_answer, question: multiple_question, author: voter.user, option: option_a)
create(:poll_answer, question: multiple_question, author: voter.user, option: option_b)
create(:poll_answer, question: another_question, author: voter.user, option: option_yes)
create(:poll_answer, question: another_question_open, author: voter.user, answer: "Free text")
expect(question_open.open_ended_blank_answers_count).to eq 1
end
end
describe "percentages" do
it "returns 0.0 when there aren't any answers" do
expect(question_open.open_ended_valid_percentage).to eq 0.0
expect(question_open.open_ended_blank_percentage).to eq 0.0
end
it "calculates valid and blank percentages based on counts" do
another_question = create(:poll_question, :yes_no, poll: poll)
option_yes = another_question.question_options.find_by(title: "Yes")
voters = create_list(:poll_voter, 4, poll: poll)
voters.each do |voter|
create(:poll_answer, question: another_question, author: voter.user, option: option_yes)
end
create(:poll_answer, question: question_open, author: voters.sample.user, answer: "A")
expect(question_open.open_ended_valid_percentage).to eq 25.0
expect(question_open.open_ended_blank_percentage).to eq 75.0
end
end
end
end

View File

@@ -44,6 +44,21 @@ describe Poll::WebVote do
expect(voter.poll_id).to eq answer.poll.id
end
it "updates existing multiple options instead of adding new ones" 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, author: user, question: question, option: option_a)
create(:poll_answer, author: user, question: question, option: option_b)
web_vote.update(question.id.to_s => { option_id: [option_c.id.to_s] })
expect(question.reload.answers.size).to eq 1
expect(question.reload.answers.first.option).to eq option_c
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)
@@ -55,6 +70,28 @@ describe Poll::WebVote do
expect(question.answers).to be_blank
end
it "does not save the answer if it exceeds the allowed max votes" do
question = create(:poll_question_multiple, :abc, poll: poll, max_votes: 2)
result = web_vote.update(question.id.to_s => { option_id: question.question_options.ids.map(&:to_s) })
expect(result).to be false
expect(poll.voters).to be_blank
expect(question.answers).to be_blank
end
it "does not save the answer if unique question receives multiple options" do
question = create(:poll_question, :yes_no, poll: poll)
result = web_vote.update(
question.id.to_s => { option_id: question.question_options.ids.map(&:to_s) }
)
expect(result).to be false
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({})
@@ -119,5 +156,47 @@ describe Poll::WebVote do
expect(Poll::Answer.count).to be 2
end
end
context "Open-ended questions" do
let!(:open_ended_question) { create(:poll_question_open, poll: poll) }
it "creates one answer when text is present" do
web_vote.update(open_ended_question.id.to_s => { answer: " Hi " })
expect(poll.reload.voters.size).to eq 1
open_answer = open_ended_question.reload.answers.find_by(author: user)
expect(open_answer.answer).to eq "Hi"
expect(open_answer.option_id).to be nil
end
it "does not create an answer but create voters when text is blank or only spaces" do
web_vote.update(open_ended_question.id.to_s => { answer: " " })
expect(poll.reload.voters.size).to eq 1
expect(open_ended_question.reload.answers.where(author: user)).to be_empty
end
it "deletes existing answer but keeps voters when leaving open-ended blank" do
create(:poll_answer, question: open_ended_question, author: user, answer: "Old answer")
web_vote.update(open_ended_question.id.to_s => { answer: " " })
expect(poll.reload.voters.size).to eq 1
expect(open_ended_question.reload.answers.where(author: user)).to be_empty
end
it "updates existing open answer without creating duplicates" do
existing = create(:poll_answer, question: open_ended_question, author: user, answer: "Old text")
web_vote.update(open_ended_question.id.to_s => { answer: " New text " })
updated = open_ended_question.reload.answers.find_by(author: user)
expect(updated.id).to eq existing.id
expect(updated.answer).to eq "New text"
expect(updated.option_id).to be nil
expect(poll.reload.voters.size).to eq 1
end
end
end
end

View File

@@ -1,7 +1,7 @@
require "rails_helper"
describe VotationType do
let(:vote_types) { %i[votation_type_unique votation_type_multiple] }
let(:vote_types) { %i[votation_type_unique votation_type_multiple votation_type_open] }
let(:votation_type) { build(vote_types.sample) }
it "is valid" do
@@ -27,9 +27,51 @@ describe VotationType do
expect(votation_type).to be_valid
votation_type.vote_type = "open"
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
describe "#cannot_be_open_ended_if_question_has_options" do
it "allows changing to open-ended when the question has no options" do
votation_type = create(:votation_type_unique)
votation_type.vote_type = :open
expect(votation_type).to be_valid
end
it "blocks changing to open-ended when the question has options" do
votation_type = create(:votation_type_unique)
create(:poll_question_option, question: votation_type.questionable)
votation_type.vote_type = :open
expect(votation_type).not_to be_valid
error = votation_type.errors[:vote_type].first
expect(error).to eq "can't change to open-ended type " \
"because you've already defined possible valid answers for this question"
end
end
describe "scopes" do
describe ".accepts_options" do
it "includes unique and multiple, excludes open_ended" do
question_unique = create(:poll_question_unique)
question_multiple = create(:poll_question_multiple)
question_open_ended = create(:poll_question_open)
accepts_options = VotationType.accepts_options
expect(accepts_options).to match_array [question_unique.votation_type,
question_multiple.votation_type]
expect(accepts_options).not_to include question_open_ended.votation_type
end
end
end
end

View File

@@ -71,26 +71,63 @@ describe "Admin poll questions", :admin do
poll = create(:poll, :future)
visit admin_poll_path(poll)
click_link "Create question"
expect(page).to have_content "It's only possible to answer one time to the question."
end
scenario "Unique" do
fill_in "Question", with: "Question with unique answer"
select "Unique answer", from: "Votation type"
expect(page).to have_content "It's only possible to answer one time to the question."
expect(page).not_to have_content "Allows to choose multiple answers."
expect(page).not_to have_field "Maximum number of votes"
expect(page).not_to have_content "Open-ended question that allows users to provide " \
"a single answer in their own words."
click_button "Save"
expect(page).to have_content "Question with unique answer"
expect(page).to have_content "Unique answer"
expect(page).not_to have_content "Maximum number of votes"
expect(page).to have_link "Add answer"
expect(page).to have_table "Valid answers"
end
scenario "Multiple" do
fill_in "Question", with: "Question with multiple answers"
select "Multiple answers", from: "Votation type"
expect(page).not_to have_content "It's only possible to answer one time to the question."
expect(page).to have_content "Allows to choose multiple answers."
expect(page).not_to have_content "Open-ended question that allows users to provide " \
"a single answer in their own words."
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"
expect(page).to have_text "Maximum number of votes 6", normalize_ws: true
expect(page).to have_link "Add answer"
expect(page).to have_table "Valid answers"
end
scenario "Open-ended" do
fill_in "Question", with: "What do you want?"
select "Open-ended", from: "Votation type"
expect(page).not_to have_content "Allows to choose multiple answers."
expect(page).not_to have_field "Maximum number of votes"
expect(page).to have_content "Open-ended question that allows users to provide " \
"a single answer in their own words."
click_button "Save"
expect(page).to have_content "What do you want?"
expect(page).to have_content "Open-ended"
expect(page).not_to have_content "Maximum number of votes"
expect(page).not_to have_link "Add answer"
expect(page).not_to have_table "Valid answers"
end
end
end
@@ -134,7 +171,7 @@ describe "Admin poll questions", :admin do
scenario "Update" do
poll = create(:poll, :future)
question = create(:poll_question, poll: poll)
question = create(:poll_question_open, poll: poll)
old_title = question.title
new_title = "Vegetables are great and everyone should have one"
@@ -145,6 +182,12 @@ describe "Admin poll questions", :admin do
end
expect(page).to have_link "Go back", href: admin_poll_path(poll)
expect(page).to have_select "Votation type", selected: "Open-ended"
expect(page).not_to have_content "Allows to choose multiple answers."
expect(page).not_to have_field "Maximum number of votes"
expect(page).to have_content "Open-ended question that allows users to provide " \
"a single answer in their own words."
fill_in "Question", with: new_title
click_button "Save"

View File

@@ -4,8 +4,8 @@ describe "Officing Results", :with_frozen_time do
let(:poll) { create(:poll, ends_at: 1.day.ago) }
let(:booth) { create(:poll_booth, polls: [poll]) }
let(:poll_officer) { create(:poll_officer) }
let(:question_1) { create(:poll_question, poll: poll) }
let(:question_2) { create(:poll_question, poll: poll) }
let(:question_1) { create(:poll_question_unique, poll: poll) }
let(:question_2) { create(:poll_question_unique, poll: poll) }
before do
create(:poll_shift, :recount_scrutiny_task, officer: poll_officer, booth: booth, date: Date.current)
@@ -15,6 +15,8 @@ describe "Officing Results", :with_frozen_time do
create(:poll_question_option, title: "Today", question: question_2, given_order: 1)
create(:poll_question_option, title: "Tomorrow", question: question_2, given_order: 2)
create(:poll_question_open, poll: poll, title: "What do you want?")
login_as(poll_officer.user)
set_officing_booth(booth)
end
@@ -69,6 +71,8 @@ describe "Officing Results", :with_frozen_time do
fill_in "Tomorrow", with: "444"
end
expect(page).not_to have_css "fieldset", text: "What do you want?"
fill_in "Totally blank ballots", with: "66"
fill_in "Invalid ballots", with: "77"
fill_in "Valid ballots", with: "88"
@@ -100,6 +104,7 @@ describe "Officing Results", :with_frozen_time do
booth_assignment_id: partial_result.booth_assignment_id)
within("#question_#{question_1.id}_0_result") { expect(page).to have_content("7777") }
expect(page).not_to have_content "What do you want?"
visit new_officing_poll_result_path(poll)
@@ -136,5 +141,6 @@ describe "Officing Results", :with_frozen_time do
within("#total_results") { expect(page).to have_content("8") }
within("#question_#{question_1.id}_0_result") { expect(page).to have_content("5555") }
within("#question_#{question_1.id}_1_result") { expect(page).to have_content("200") }
expect(page).not_to have_content "What do you want?"
end
end

View File

@@ -180,15 +180,11 @@ describe "Polls" do
scenario "Level 2 users answering" do
poll.update!(geozone_restricted_to: [geozone])
create(:poll_question, :yes_no, poll: poll, title: "Do you agree?")
question = create(:poll_question, :yes_no, poll: poll, title: "Do you agree?")
login_as(create(:user, :level_two, geozone: geozone))
visit poll_path(poll)
vote_for_poll_via_web(poll, question => "Yes")
within_fieldset("Do you agree?") { choose "Yes" }
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."

View File

@@ -1,58 +0,0 @@
require "rails_helper"
describe "Poll Results" do
scenario "List each Poll question" do
user1 = create(:user, :level_two)
user2 = create(:user, :level_two)
user3 = create(:user, :level_two)
poll = create(:poll, results_enabled: true)
question1 = create(:poll_question, poll: poll)
option1 = create(:poll_question_option, question: question1, title: "Yes")
option2 = create(:poll_question_option, question: question1, title: "No")
question2 = create(:poll_question, poll: poll)
option3 = create(:poll_question_option, question: question2, title: "Blue")
option4 = create(:poll_question_option, question: question2, title: "Green")
option5 = create(:poll_question_option, question: question2, title: "Yellow")
login_as user1
vote_for_poll_via_web(poll, question1 => "Yes", question2 => "Blue")
logout
login_as user2
vote_for_poll_via_web(poll, question1 => "Yes", question2 => "Green")
logout
login_as user3
vote_for_poll_via_web(poll, question1 => "No", question2 => "Yellow")
logout
travel_to(poll.ends_at + 1.day)
visit results_poll_path(poll)
expect(page).to have_content(question1.title)
expect(page).to have_content(question2.title)
within("#question_#{question1.id}_results_table") do
expect(find("#option_#{option1.id}_result")).to have_content("2 (66.67%)")
expect(find("#option_#{option2.id}_result")).to have_content("1 (33.33%)")
end
within("#question_#{question2.id}_results_table") do
expect(find("#option_#{option3.id}_result")).to have_content("1 (33.33%)")
expect(find("#option_#{option4.id}_result")).to have_content("1 (33.33%)")
expect(find("#option_#{option5.id}_result")).to have_content("1 (33.33%)")
end
end
scenario "Results for polls with questions but without options" do
poll = create(:poll, :expired, results_enabled: true)
question = create(:poll_question, poll: poll)
visit results_poll_path(poll)
expect(page).to have_content question.title
end
end

View File

@@ -8,9 +8,10 @@ describe "Poll Votation Type" do
login_as(author)
end
scenario "Unique and multiple answers" do
create(:poll_question_unique, :yes_no, poll: poll, title: "Is it that bad?")
scenario "Unique, multiple and open answers" do
create(:poll_question, :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?")
create(:poll_question_open, poll: poll, title: "What do you think?")
visit poll_path(poll)
@@ -21,6 +22,10 @@ describe "Poll Votation Type" do
check "Answer C"
end
within(".poll-question-open-ended") do
fill_in "What do you think?", with: "I believe it's great"
end
click_button "Vote"
expect(page).to have_content "Thank you for voting!"
@@ -39,6 +44,10 @@ describe "Poll Votation Type" do
expect(page).to have_field "Answer E", type: :checkbox, checked: false
end
within(".poll-question-open-ended") do
expect(page).to have_field "What do you think?", with: "I believe it's great"
end
expect(page).to have_button "Vote"
end

View File

@@ -18,12 +18,9 @@ describe "Voter" do
user = create(:user, :level_two)
login_as user
visit poll_path(poll)
within_fieldset("Is this question stupid?") { choose "Yes" }
click_button "Vote"
vote_for_poll_via_web(poll, question => "Yes")
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