Merge pull request #6061 from consuldemocracy/poll_text_answers
Add support for essay poll questions
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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| %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class Polls::Results::QuestionComponent < ApplicationComponent
|
||||
attr_reader :question
|
||||
|
||||
def initialize(question:)
|
||||
def initialize(question)
|
||||
@question = question
|
||||
end
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
47
spec/components/polls/results/question_component_spec.rb
Normal file
47
spec/components/polls/results/question_component_spec.rb
Normal 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
|
||||
36
spec/components/polls/results_component_spec.rb
Normal file
36
spec/components/polls/results_component_spec.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user